feat(dsh): remote managed-agent as on-demand tool + bl managed-agent run

Rework the managed-agent integration so a dsh user can, in plain
language, have a Bailian cloud agent created and run a task — no
hand-written agents.yaml, no prior apply.

New `bl managed-agent run --prompt <task> [--instructions] [--model]
[--agent]`: one step that idempotently materializes a cloud agent + its
environment, then opens a session and streams the result. It mirrors the
OpenAgentPack webui backend's ensure+run recipe (resolveProjectConfigFrom
Object → syncAgentResourcesWithStateBackend → readProjectRuntime +
startSessionRun) from an in-memory config, reusing the existing
credential spine in _engine/credentials.ts. State persists under the bl
config dir (~/.bailian/managed-agent/<agent>/), never the user's cwd, so
repeat runs with the same --agent reuse the materialized agent. Unlike
apply it provisions without --yes, since running is the intent.

dsh side: replace the SubagentProvider with a plain tool
`bailian_run_remote_task` (packages/dsh/src/tool-managed-agent). The
subagent seam did not fit: in the web profile every tool-subagent row is
disabled in the host plane (delegation lives in agent presets), a
provider fixes one agent identity in config, and the default numeric
maxDepth would fail-mount a no-depthLimit provider. As a tool the model
calls it directly and fills `instructions` from the user's intent, so the
remote agent's role is defined per task. Enabled by default — it creates
nothing at load, only on invocation.

LLM row: configure the base bundle's existing llm-pi-ai row instead of
mounting a second pi-ai instance (a second instance re-declares pi-ai's
global configurable-provider catalog and fails boot on a duplicate
amazon-bedrock). TokenPlan reads a dedicated BAILIAN_TOKENPLAN_API_KEY,
not DASHSCOPE_API_KEY: TokenPlan (sk-sp-) and pay-as-you-go (sk-ws-) keys
401 each other's endpoints, so sharing one var would silently break
whichever plugin lost.

Note: the ensure+run happy path could not be verified end-to-end on the
available account — agentstudio returns 404 there, and the existing
`managed-agent apply` 404s identically against the same endpoint/key, so
the failure is account/service provisioning, not this change. Command
wiring, dry-run, config assembly, credential injection and URL
construction were all verified.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
lisheng.lisheng
2026-08-15 10:29:24 +08:00
parent 9e9911aa86
commit f919ebae3c
13 changed files with 519 additions and 294 deletions
+2
View File
@@ -102,6 +102,7 @@ import {
managedAgentValidate,
managedAgentPlan,
managedAgentApply,
managedAgentRun,
managedAgentDestroy,
managedAgentStateList,
managedAgentStateShow,
@@ -225,6 +226,7 @@ export const commands: Record<string, AnyCommand> = {
"managed-agent validate": managedAgentValidate,
"managed-agent plan": managedAgentPlan,
"managed-agent apply": managedAgentApply,
"managed-agent run": managedAgentRun,
"managed-agent destroy": managedAgentDestroy,
"managed-agent state list": managedAgentStateList,
"managed-agent state show": managedAgentStateShow,
@@ -0,0 +1,122 @@
import { mkdirSync } from "node:fs";
import { dirname, join } from "node:path";
import {
type BackendRuntimeInput,
LocalFileStateBackend,
resolveProjectConfigFromObject,
} from "@openagentpack/sdk";
import { getConfigDir } from "bailian-cli-core";
import {
assertProviderCredentials,
type CredentialHost,
injectProviderCredentials,
normalizeInterpolatedProviderBlocks,
prepareProviderEnv,
scrubCredentialEnv,
} from "./credentials.ts";
import { type HostContext, installSdkTransport } from "./transport.ts";
/** Default agent identity `bl managed-agent run` materializes and reuses. */
export const DEFAULT_INLINE_AGENT = "dsh-remote-runner";
/** Default model for the materialized agent. */
export const DEFAULT_INLINE_MODEL = "qwen3.8-max";
/** Default role when the caller supplies no `--instructions`. */
export const DEFAULT_INLINE_INSTRUCTIONS = "You are a helpful assistant. Complete the task.";
/** Environment name declared in the inline config; one cloud env per agent. */
const INLINE_ENVIRONMENT = "cloud";
export interface InlineAgentOptions {
agentName: string;
instructions: string;
model: string;
/** Override the persisted state location (defaults under the bl config dir). */
statePath?: string;
}
/**
* Slugify an agent name into a filesystem- and project-id-safe token. The state
* for each distinct agent lives in its own directory so repeat runs reuse the
* same materialized remote agent.
*/
function slugify(agentName: string): string {
const slug = agentName
.toLowerCase()
.replace(/[^a-z0-9._-]+/g, "-")
.replace(/^-+|-+$/g, "");
return slug.length > 0 ? slug : "agent";
}
/** Where a materialized agent's state is persisted (not the user's cwd). */
export function inlineStatePath(agentName: string): string {
return join(getConfigDir(), "managed-agent", slugify(agentName), "state.json");
}
/**
* The minimal in-memory project config that materializes into one cloud agent.
* `providers.bailian` carries empty `api_key`/`base_url` placeholders so
* {@link injectProviderCredentials} fills them from bl's auth chain (it only
* writes fields the block already declares).
*/
export function buildInlineConfig(opts: InlineAgentOptions): Record<string, unknown> {
return {
version: "1",
providers: {
bailian: { api_key: "", base_url: "" },
},
defaults: { provider: "bailian" },
environments: {
[INLINE_ENVIRONMENT]: {
description: "Bailian CLI cloud environment",
config: { type: "cloud", networking: { type: "unrestricted" } },
},
},
agents: {
[opts.agentName]: {
description: opts.agentName,
model: opts.model,
instructions: opts.instructions,
environment: INLINE_ENVIRONMENT,
provider: "bailian",
},
},
};
}
/**
* Build the `BackendRuntimeInput` shared by ensure (`syncAgentResourcesWith
* StateBackend`) and run (`readProjectRuntime` + `startSessionRun`). Mirrors the
* credential spine of {@link buildAgentRuntime} but sources config from an
* in-memory object instead of a file, so no `agents.yaml` or `apply` is required.
*/
export async function buildInlineBackendInput(
host: HostContext & CredentialHost,
opts: InlineAgentOptions,
): Promise<BackendRuntimeInput> {
installSdkTransport(host);
prepareProviderEnv();
const rawConfig = buildInlineConfig(opts);
const { config, projectName } = await resolveProjectConfigFromObject(rawConfig, {
projectName: slugify(opts.agentName),
});
normalizeInterpolatedProviderBlocks(config.providers);
injectProviderCredentials(config.providers, host);
scrubCredentialEnv();
assertProviderCredentials(config.providers);
const statePath = opts.statePath ?? inlineStatePath(opts.agentName);
mkdirSync(dirname(statePath), { recursive: true });
const stateBackend = new LocalFileStateBackend({ statePath });
return {
projectName,
config,
stateBackend,
stateScope: { projectId: slugify(opts.agentName) },
providers: config.providers,
};
}
@@ -0,0 +1,137 @@
import {
BailianError,
defineCommand,
detectOutputFormat,
ExitCode,
type FlagsDef,
} from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
import {
readProjectRuntime,
startSessionRun,
startSessionRunPolling,
syncAgentResourcesWithStateBackend,
} from "@openagentpack/sdk";
import { CREDENTIALS_NOTE } from "./_engine/config-loader.ts";
import { withStdoutProtected } from "./_engine/console-capture.ts";
import { withAgentErrors } from "./_engine/errors.ts";
import {
buildInlineBackendInput,
DEFAULT_INLINE_AGENT,
DEFAULT_INLINE_INSTRUCTIONS,
DEFAULT_INLINE_MODEL,
} from "./_engine/inline-runtime.ts";
import { renderCollectedEvents, streamAndRenderEvents } from "./_engine/session-render.ts";
const RUN_FLAGS = {
prompt: {
type: "string",
valueHint: "<text>",
description: "Task to run (required)",
required: true,
},
instructions: {
type: "string",
valueHint: "<text>",
description: "Role/system instructions for the remote agent (default: generic assistant)",
},
model: {
type: "string",
valueHint: "<id>",
description: `Model for the remote agent (default: ${DEFAULT_INLINE_MODEL})`,
},
agent: {
type: "string",
valueHint: "<name>",
description: `Agent identity to create/reuse (default: ${DEFAULT_INLINE_AGENT})`,
},
noStream: {
type: "switch",
description: "Use polling instead of SSE streaming",
},
} satisfies FlagsDef;
export default defineCommand({
description: "Provision (if needed) a cloud agent and run a task in one step",
auth: "apiKey",
usageArgs: "--prompt <text> [--instructions <text>] [--model <id>] [--agent <name>]",
flags: RUN_FLAGS,
exampleArgs: [
'--prompt "Summarize the latest AI news"',
'--prompt "Audit this dependency tree" --instructions "You are a security expert" --model qwen3.8-max',
],
notes: [
...CREDENTIALS_NOTE,
"Unlike `apply`, this creates/updates the cloud agent + environment on demand without --yes. The first run provisions cloud resources (may incur cost and take longer to start); later runs with the same --agent reuse them.",
],
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
const asJson = format === "json";
const agentName = flags.agent ?? DEFAULT_INLINE_AGENT;
const model = flags.model ?? DEFAULT_INLINE_MODEL;
const instructions = flags.instructions ?? DEFAULT_INLINE_INSTRUCTIONS;
if (settings.dryRun) {
emitResult(
{
would_run: {
prompt: flags.prompt,
agent: agentName,
model,
instructions,
mode: flags.noStream ? "polling" : "streaming",
},
},
format,
);
return;
}
await withAgentErrors(() =>
withStdoutProtected(async () => {
const input = await buildInlineBackendInput(ctx, { agentName, instructions, model });
// Ensure the remote agent + its cloud environment exist. Idempotent:
// a repeat run with the same agent name reuses the materialized state.
if (!asJson) process.stderr.write(`Ensuring cloud agent "${agentName}"…\n`);
const sync = await syncAgentResourcesWithStateBackend(input, agentName, {
policy: "force",
quiet: true,
});
if (sync.status !== "completed") {
const detail =
sync.error ??
sync.diagnostics.find((diag) => diag.severity === "error")?.message ??
`provisioning ended with status "${sync.status}"`;
throw new BailianError(
`Failed to provision cloud agent "${agentName}": ${detail}`,
ExitCode.GENERAL,
);
}
// Run the task inside a runtime bound to the just-materialized state.
await readProjectRuntime(input, async (runtime) => {
if (flags.noStream) {
const run = await startSessionRunPolling(runtime, flags.prompt, { agent: agentName });
if (!asJson) process.stderr.write(`Session created: ${run.session.id}\n`);
renderCollectedEvents(run, asJson, {
session_id: run.session.id,
provider: run.provider,
agent: run.agentName,
});
} else {
const run = await startSessionRun(runtime, flags.prompt, { agent: agentName });
if (!asJson) process.stderr.write(`Session created: ${run.session.id}\n`);
await streamAndRenderEvents(run.events, asJson, {
session_id: run.session.id,
provider: run.provider,
agent: run.agentName,
});
}
});
}),
);
},
});
+1
View File
@@ -99,6 +99,7 @@ export { default as managedAgentInit } from "./commands/managed-agent/init.ts";
export { default as managedAgentValidate } from "./commands/managed-agent/validate.ts";
export { default as managedAgentPlan } from "./commands/managed-agent/plan.ts";
export { default as managedAgentApply } from "./commands/managed-agent/apply.ts";
export { default as managedAgentRun } from "./commands/managed-agent/run.ts";
export { default as managedAgentDestroy } from "./commands/managed-agent/destroy.ts";
export { default as managedAgentStateList } from "./commands/managed-agent/state-list.ts";
export { default as managedAgentStateShow } from "./commands/managed-agent/state-show.ts";
@@ -177,6 +177,7 @@ export const MANAGED_AGENT_ROUTES: E2eRouteExports = {
"managed-agent validate": "managedAgentValidate",
"managed-agent plan": "managedAgentPlan",
"managed-agent apply": "managedAgentApply",
"managed-agent run": "managedAgentRun",
"managed-agent destroy": "managedAgentDestroy",
"managed-agent state list": "managedAgentStateList",
"managed-agent state rm": "managedAgentStateRm",
+33 -36
View File
@@ -4,23 +4,23 @@
一个包提供 5 个插件行,外加对 base bundle 的 `llm-pi-ai` 行做一次配置覆盖:
| row id | 能力 | 默认 | 依赖 |
| -------------------------------- | --------------------------------------------------------------- | ---- | --------------------- |
| `llm-pi-ai`(覆盖 base 行) | 把百炼 TokenPlan 网关注册成 LLM provider(`bailian-tokenplan`) | 启用 | TokenPlan Key |
| `bailian-tool-vision` | `bailian_vision_describe`:图片/视频理解 | 启用 | `bl` |
| `bailian-tool-image` | `bailian_image_generate`:文生图 | 启用 | `bl` |
| `bailian-web-search-rag` | 百炼知识库检索,注册为 `web_search` 的后端 | 停用 | 按量付费 Key + 知识库 |
| `bailian-memory` | 跨会话长期记忆(tools + 自动检索/落库) | 停用 | 按量付费 Key |
| `bailian-subagent-managed-agent` | 子 agent 跑在百炼托管运行时 | 停用 | `bl` + `agents.yaml` |
| row id | 能力 | 默认 | 依赖 |
| ---------------------------- | --------------------------------------------------------------- | ---- | --------------------- |
| `llm-pi-ai`(覆盖 base 行) | 把百炼 TokenPlan 网关注册成 LLM provider(`bailian-tokenplan`) | 启用 | TokenPlan Key |
| `bailian-tool-vision` | `bailian_vision_describe`:图片/视频理解 | 启用 | `bl` |
| `bailian-tool-image` | `bailian_image_generate`:文生图 | 启用 | `bl` |
| `bailian-tool-managed-agent` | `bailian_run_remote_task`:按需在云端创建 agent 并跑任务 | 启用 | `bl` + 按量付费 Key |
| `bailian-web-search-rag` | 百炼知识库检索,注册为 `web_search` 的后端 | 停用 | 按量付费 Key + 知识库 |
| `bailian-memory` | 跨会话长期记忆(tools + 自动检索/落库) | 停用 | 按量付费 Key |
后三个默认停用是有意的:它们要么需要部署方特有的资源 ID,要么按次计费,不该在用户没配置时就生效。
`web-search-rag` 与 `memory` 默认停用是有意的:它们要么需要部署方特有的资源 ID,要么按次计费,不该在用户没配置时就生效。`tool-managed-agent` 默认启用——它加载时不建任何资源,只有模型真正调用时才在云端创建 agent。
---
## 1. 前置条件
- Node ≥ 22.19(`dsh` 的要求)
- `bl`(vision / image / subagent 三个插件通过子进程调它)
- `bl`(vision / image / 远程任务 三个工具通过子进程调它)
```sh
npm install -g bailian-cli
@@ -105,7 +105,9 @@ Web UI 在 http://127.0.0.1:3080。
| `deepseek-v4-pro` | 否 |
| `deepseek-v4-flash-0731` | 否 |
**两个工具** — `bailian_vision_describe`、`bailian_image_generate`。
**三个工具** — `bailian_vision_describe`、`bailian_image_generate`、`bailian_run_remote_task`。
前两个走 TokenPlan(vision/image)。`bailian_run_remote_task` 见 [§3.1](#31-远程任务-bailian_run_remote_task)——它默认启用但用的是**按量付费 Key + dashscope 端点**,与 TokenPlan 那两个不同。
### 关于看图,有个坑值得知道
@@ -118,6 +120,24 @@ DeepSeek 那两个模型在 TokenPlan 网关上传图**不报错但也看不见*
`bailian_image_generate` 同理:模型能看图时返回内联图片,不能看图时降级为返回落盘路径,你可以接着用 vision 工具读它。文件不会被删除,正是为了这个衔接。
### 3.1 远程任务(`bailian_run_remote_task`)
把一个任务甩到百炼云端的托管 agent 上跑,不占本地会话。**无需预先写 `agents.yaml` 或 `apply`**:工具首次被调用时,`bl managed-agent run` 会在你的账号里幂等创建一个 agent + cloud environment,之后复用。
- 模型自己按用户意图填 `instructions`(远程 agent 的角色),`task` 是要它做的事。例如你说「在云端帮我审计这个依赖树,它该懂安全」→ 模型调 `bailian_run_remote_task(task="审计依赖树", instructions="你是安全专家")`。
- **前提**:这条路走的是 managed-agent(agentstudio)服务,需要**按量付费 Key**(`sk-ws-`)+ dashscope 端点,且账号已开通 managed-agent。TokenPlan Key 不适用。若 `DASHSCOPE_API_KEY`/端点没配好,首次调用会返回 `Bailian API 404`。
- 首次会创建云资源(可能计费、启动有延迟);同名 agent 后续复用。默认 agent 名 `dsh-remote-runner`,可在配置里改。
需要非默认的 agent 名 / 模型时:
```yaml
- id: bailian-tool-managed-agent
config:
agent: my-runner
model: qwen3.8-max
timeoutMs: 600000
```
---
## 4. 开启可选插件
@@ -181,30 +201,7 @@ dsh 自身没有跨会话记忆(`ctx.compaction` 只在单会话内压缩上
autoPersist: false
```
### 托管子 agent
把 dsh 的子 agent 派发到百炼云端运行,不占本地资源。
先在工作目录准备好清单并应用:
```sh
bl managed-agent init
bl managed-agent apply --yes
```
再开启:
```yaml
- id: bailian-subagent-managed-agent
disabled: false
config:
file: agents.yaml
agent: assistant
provider: bailian
timeoutMs: 600000
```
两个已知限制:CLI 在会话结束时才一次性输出 JSON,所以**中途没有增量进度**,被取消时也拿不到部分输出;`prepareContinuable` 未实现,即只支持一次性委派,不支持多轮续聊。
> 远程任务(`bailian_run_remote_task`)默认启用,配置见 [§3.1](#31-远程任务-bailian_run_remote_task)。
---
@@ -226,7 +223,7 @@ bl auth status
- **文生图**:让模型生成一张图
- **RAG**:问一个只有知识库里才有答案的问题
- **记忆**:会话 A 告诉它一个事实 → 关掉 → 新开会话 B 提问,看是否命中
- **子 agent**:派发一个子任务
- **远程任务**:说「在云端帮我跑一个任务:<something>,它该擅长 <role>」→ 确认模型调用 `bailian_run_remote_task`(`instructions` 由模型按 role 填)→ 首次触发云端创建 → 返回远程会话结果(需按量付费 Key + 已开通 agentstudio)
---
+6 -6
View File
@@ -76,6 +76,12 @@
- id: bailian-tool-image
name: bailian-cli-dsh/tool-image
# Enabled by default: the tool creates no resources at load time. It only
# provisions a cloud agent when the model actually calls it, and reuses it
# after — no deployment-specific ID to configure up front.
- id: bailian-tool-managed-agent
name: bailian-cli-dsh/tool-managed-agent
# Disabled by default: the knowledge base to query is deployment-specific,
# and an enabled provider with no agentId would make `web_search` ambiguous
# for everyone. Set workspaceId + agentId and flip `disabled` to use it.
@@ -89,9 +95,3 @@
name: bailian-cli-dsh/memory
disabled: true
config: {}
# Disabled by default: requires an applied `agents.yaml` in the workspace.
- id: bailian-subagent-managed-agent
name: bailian-cli-dsh/subagent-managed-agent
disabled: true
config: {}
+5 -5
View File
@@ -33,6 +33,10 @@
"types": "./src/tool-image/index.ts",
"default": "./src/tool-image/index.ts"
},
"./tool-managed-agent": {
"types": "./src/tool-managed-agent/index.ts",
"default": "./src/tool-managed-agent/index.ts"
},
"./web-search-rag": {
"types": "./src/web-search-rag/index.ts",
"default": "./src/web-search-rag/index.ts"
@@ -41,10 +45,6 @@
"types": "./src/memory/index.ts",
"default": "./src/memory/index.ts"
},
"./subagent-managed-agent": {
"types": "./src/subagent-managed-agent/index.ts",
"default": "./src/subagent-managed-agent/index.ts"
},
"./cordis.patch.yml": "./cordis.patch.yml",
"./package.json": "./package.json"
},
@@ -54,9 +54,9 @@
".": "./dist/index.mjs",
"./tool-vision": "./dist/tool-vision/index.mjs",
"./tool-image": "./dist/tool-image/index.mjs",
"./tool-managed-agent": "./dist/tool-managed-agent/index.mjs",
"./web-search-rag": "./dist/web-search-rag/index.mjs",
"./memory": "./dist/memory/index.mjs",
"./subagent-managed-agent": "./dist/subagent-managed-agent/index.mjs",
"./cordis.patch.yml": "./cordis.patch.yml",
"./package.json": "./package.json"
},
@@ -1,205 +0,0 @@
/**
* `bailian-cli-dsh/subagent-managed-agent`: runs child agents on Bailian's
* hosted managed-agent runtime instead of in this process, through
* `bl managed-agent session run`.
*
* Out-of-process delegation is an established shape here — `subagent-acp` and
* `subagent-codex` do the same over their own transports. Going through the
* CLI keeps `agents.yaml` resolution, provider selection, and SSE decoding in
* one place.
*
* Two honest limits. The CLI buffers the whole session and emits it at exit,
* so no incremental progress reaches the parent and a cancelled run yields no
* partial output. And `prepareContinuable` is deliberately absent: method
* presence IS the continuable capability, and multi-turn continuation is not
* wired up yet.
*
* @module bailian-cli-dsh/subagent-managed-agent
*/
import type { Context } from "@deepseek-ai/cordis";
import type { ContentBlock } from "@deepseek-ai/dsh-llm";
import { SessionId } from "@deepseek-ai/dsh-session";
import type {
ResolvedSubagentStartRequest,
SubagentCapabilities,
SubagentProvider,
SubagentResult,
SubagentRun,
} from "@deepseek-ai/dsh-subagent";
import type {} from "@deepseek-ai/dsh-fs";
import z from "@deepseek-ai/schemastery";
import { runBlJson } from "../shared/bl.ts";
/** Cordis plugin name used by loader diagnostics. */
export const name = "bailian-subagent-managed-agent";
/** Seams this plugin registers into. */
export const inject = ["subagents", "subprocess", "fs"];
/** Registry name callers select this transport by. */
export const BAILIAN_MANAGED_AGENT_PROVIDER = "bailian-managed-agent";
export interface Config {
/** Manifest passed as `--file`; must already be applied. */
file?: string;
/** Agent name within the manifest. */
agent?: string;
/** Backing provider understood by `bl managed-agent`. */
provider?: string;
/** Cooperative budget for one hosted run. */
timeoutMs?: number;
}
export const Config: z<Config> = z.object({
file: z.string().description("Path to agents.yaml; defaults to the CLI's own default."),
agent: z.string().description("Agent name declared in the manifest."),
provider: z.string().description("Managed-agent backing provider."),
timeoutMs: z.natural().description("Cooperative timeout budget in milliseconds."),
});
const DEFAULT_MANIFEST = "agents.yaml";
const DEFAULT_TIMEOUT_MS = 600_000;
/** A one-shot transport supports none of the start-time features. */
const CAPABILITIES: SubagentCapabilities = {
outputSchema: false,
depthLimit: false,
toolFilter: false,
persona: false,
};
interface SessionEvent {
type?: string;
content?: unknown;
role?: string;
}
interface SessionRunResponse {
session_id?: string;
events?: readonly SessionEvent[];
}
function promptText(blocks: readonly ContentBlock[]): string {
return blocks
.filter((block): block is Extract<ContentBlock, { type: "text" }> => block.type === "text")
.map((block) => block.text)
.join("\n")
.trim();
}
/** Assistant-visible text of a finished hosted session. */
function assistantOutput(events: readonly SessionEvent[]): ContentBlock[] {
const text = events
.filter((event) => event.type === "message" && typeof event.content === "string")
.map((event) => event.content as string)
.join("\n")
.trim();
return text.length > 0 ? [{ type: "text", text }] : [];
}
function isAbort(error: unknown): boolean {
return error instanceof DOMException && error.name === "AbortError";
}
class BailianManagedAgentProvider implements SubagentProvider {
readonly name = BAILIAN_MANAGED_AGENT_PROVIDER;
readonly capabilities = CAPABILITIES;
readonly inheritsParentContext = false;
constructor(
private readonly ctx: Context,
private readonly config: Config,
) {}
async start(request: ResolvedSubagentStartRequest): Promise<SubagentRun> {
const cwd = request.parent.session.header.cwd ?? process.cwd();
const manifest = this.config.file ?? DEFAULT_MANIFEST;
// Pre-publication: a missing manifest is the common misconfiguration and
// deserves a start-time rejection rather than a failed run.
const target = await this.ctx.fs.resolve(manifest, { cwd, signal: request.signal });
const info = await this.ctx.fs.stat(target, request.signal);
if (info === undefined) {
throw new Error(
`bailian-managed-agent: no manifest at "${target.displayPath}". Create one with ` +
`\`bl managed-agent init\` and apply it with \`bl managed-agent apply --yes\`.`,
);
}
const prompt = promptText(request.prompt);
if (prompt.length === 0) {
throw new Error("bailian-managed-agent: the prompt carried no text content.");
}
const argv = ["managed-agent", "session", "run", "--prompt", prompt, "--file", manifest];
if (this.config.agent !== undefined) argv.push("--agent", this.config.agent);
if (this.config.provider !== undefined) argv.push("--provider", this.config.provider);
const controller = new AbortController();
const abort = (): void => controller.abort();
request.signal.addEventListener("abort", abort, { once: true });
// The seam has no deadline of its own — cancellation arrives only through
// the caller's signal — so the transport owns one, or a wedged hosted
// session never settles.
const deadline = AbortSignal.timeout(this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS);
const combined = AbortSignal.any([controller.signal, deadline]);
// Ownership transfers on fulfillment, so every later failure settles
// through `result` — which must not reject for child-level problems.
const result = this.execute(argv, cwd, combined, deadline).finally(() => {
request.signal.removeEventListener("abort", abort);
});
let disposal: Promise<void> | undefined;
return {
id: SessionId(`bailian-managed-agent:${crypto.randomUUID()}`),
localAgent: undefined,
result,
dispose: (): Promise<void> => {
disposal ??= (async (): Promise<void> => {
controller.abort();
await result;
})();
return disposal;
},
};
}
private async execute(
argv: readonly string[],
cwd: string,
signal: AbortSignal,
deadline: AbortSignal,
): Promise<SubagentResult> {
try {
const response = await runBlJson<SessionRunResponse>(this.ctx, argv, {
cwd,
signal,
graceMs: 10_000,
});
return { output: assistantOutput(response.events ?? []), stopReason: "completed" };
} catch (error) {
if (deadline.aborted) {
return {
output: [
{
type: "text",
text: `the hosted session exceeded ${this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS}ms and was terminated`,
},
],
stopReason: "error",
};
}
// The CLI emits its JSON envelope only at exit, so a cancelled run has
// no partial output to salvage.
if (isAbort(error) || signal.aborted) return { output: [], stopReason: "aborted" };
const reason = error instanceof Error ? error.message : String(error);
return { output: [{ type: "text", text: reason }], stopReason: "error" };
}
}
}
export function apply(ctx: Context, config: Config): void {
ctx.subagents.registerProvider(new BailianManagedAgentProvider(ctx, config));
}
@@ -0,0 +1,131 @@
/**
* `bailian-cli-dsh/tool-managed-agent`: run a task on a Bailian-hosted managed
* agent, provisioned on demand, through `bl managed-agent run`.
*
* A plain tool rather than a `SubagentProvider`: in dsh's `web` profile every
* `tool-subagent` row is disabled in the host plane (delegation tools live in
* agent presets), and a subagent provider fixes one agent identity in config —
* neither fits "the model describes an intent and a remote agent is created for
* it". As a tool the model calls it directly and fills `instructions` from the
* user's intent, so the remote agent's role is defined per task.
*
* The CLI does ensure+run in one step: it materializes (idempotently) a cloud
* agent + environment under the given `agent` name on first use and reuses them
* after, so no `agents.yaml` or prior `apply` is required. First use provisions
* cloud resources — it may incur cost and take longer to start.
*
* @module bailian-cli-dsh/tool-managed-agent
*/
import type { Context } from "@deepseek-ai/cordis";
import { defineTool } from "@deepseek-ai/dsh-tools";
import type {} from "@deepseek-ai/dsh-tools";
import z from "@deepseek-ai/schemastery";
import { runBlJson } from "../shared/bl.ts";
/** Cordis plugin name used by loader diagnostics. */
export const name = "bailian-tool-managed-agent";
/** Seams this plugin registers into. */
export const inject = ["tools", "subprocess"];
/** Default agent identity provisioned and reused across calls. */
const DEFAULT_AGENT = "dsh-remote-runner";
export interface Config {
/** Agent identity to create/reuse; distinct names get distinct remote agents. */
agent?: string;
/** Model for the remote agent. */
model?: string;
/** Cooperative budget; first-run provisioning of a cloud environment is slow. */
timeoutMs?: number;
}
export const Config: z<Config> = z.object({
agent: z.string().description("Remote agent identity to create/reuse."),
model: z.string().description("Model for the remote agent."),
timeoutMs: z.natural().description("Cooperative timeout budget in milliseconds."),
});
const DEFAULT_TIMEOUT_MS = 600_000;
/** The `bl managed-agent run --output json` envelope: a session-event list. */
interface SessionRunResponse {
session_id?: string;
agent?: string;
events?: readonly { type?: string; content?: unknown; role?: string }[];
}
/** Assistant-visible text of a finished remote session. */
function assistantText(response: SessionRunResponse): string {
return (response.events ?? [])
.filter((event) => event.type === "message" && typeof event.content === "string")
.map((event) => event.content as string)
.join("\n")
.trim();
}
export function apply(ctx: Context, config: Config): void {
ctx.tools.register(
defineTool({
name: "bailian_run_remote_task",
description:
"Run a task on a Bailian-hosted cloud agent. Use for long-running or isolated work you " +
"want executed remotely rather than in this session. A remote agent is created on demand " +
"(and reused) — describe the role it should play through `instructions`, and the concrete " +
"task through `task`. Returns the remote agent's final answer.",
parameters: {
task: {
type: "string",
required: true,
description: "The concrete task for the remote agent to carry out.",
},
instructions: {
type: "string",
description:
"Role/system instructions defining what the remote agent is good at. " +
"Defaults to a generic assistant.",
},
model: {
type: "string",
description: "Override the configured model for this task.",
},
},
output: {
schema: {
type: "object",
additionalProperties: false,
properties: {
answer: { type: "string", required: true },
sessionId: { type: "string", required: true },
},
},
render: (_args, value) => [{ type: "text", text: value.answer }],
},
timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS,
async execute(args, exec) {
const argv = [
"managed-agent",
"run",
"--prompt",
args.task,
"--agent",
config.agent ?? DEFAULT_AGENT,
];
if (args.instructions !== undefined) argv.push("--instructions", args.instructions);
const model = args.model ?? config.model;
if (model !== undefined) argv.push("--model", model);
const response = await runBlJson<SessionRunResponse>(ctx, argv, {
cwd: exec.agent?.session.header.cwd ?? process.cwd(),
signal: exec.signal,
});
const answer = assistantText(response);
if (answer.length === 0) {
throw new Error("the remote agent produced no assistant output.");
}
return { answer, sessionId: response.session_id ?? "" };
},
}),
);
}
+1 -1
View File
@@ -8,9 +8,9 @@ export default defineConfig({
"src/index.ts",
"src/tool-vision/index.ts",
"src/tool-image/index.ts",
"src/tool-managed-agent/index.ts",
"src/web-search-rag/index.ts",
"src/memory/index.ts",
"src/subagent-managed-agent/index.ts",
],
minify: true,
dts: {
+23 -22
View File
@@ -9,31 +9,32 @@ Use this index for the skill-scoped quick index and global flags.
## Quick index
| Command | Description | Detail |
| --------------------------------- | ------------------------------------------------------------- | ------------------------------------ |
| `bl managed-agent apply` | Apply planned changes to create/update/delete agent resources | [managed-agent.md](managed-agent.md) |
| `bl managed-agent destroy` | Destroy all managed agent resources tracked in state | [managed-agent.md](managed-agent.md) |
| `bl managed-agent init` | Create a new agents.yaml template | [managed-agent.md](managed-agent.md) |
| `bl managed-agent plan` | Show what changes would be applied to agent infrastructure | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session create` | Create a new session for an agent | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session delete` | Delete a session | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session events` | List event history for a session | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session get` | Get details of a session | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session list` | List sessions from the provider | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session run` | Create a session, send a message, and stream the response | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session send` | Send a message to an existing session and stream the response | [managed-agent.md](managed-agent.md) |
| `bl managed-agent skill-list` | List skills from the provider's skill catalog | [managed-agent.md](managed-agent.md) |
| `bl managed-agent state import` | Import an existing remote resource into agents state | [managed-agent.md](managed-agent.md) |
| `bl managed-agent state list` | List resources tracked in agents state | [managed-agent.md](managed-agent.md) |
| `bl managed-agent state rm` | Remove a resource from state without destroying it remotely | [managed-agent.md](managed-agent.md) |
| `bl managed-agent state show` | Show details of a resource in agents state | [managed-agent.md](managed-agent.md) |
| `bl managed-agent validate` | Validate an agents.yaml configuration (offline) | [managed-agent.md](managed-agent.md) |
| Command | Description | Detail |
| --------------------------------- | -------------------------------------------------------------- | ------------------------------------ |
| `bl managed-agent apply` | Apply planned changes to create/update/delete agent resources | [managed-agent.md](managed-agent.md) |
| `bl managed-agent destroy` | Destroy all managed agent resources tracked in state | [managed-agent.md](managed-agent.md) |
| `bl managed-agent init` | Create a new agents.yaml template | [managed-agent.md](managed-agent.md) |
| `bl managed-agent plan` | Show what changes would be applied to agent infrastructure | [managed-agent.md](managed-agent.md) |
| `bl managed-agent run` | Provision (if needed) a cloud agent and run a task in one step | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session create` | Create a new session for an agent | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session delete` | Delete a session | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session events` | List event history for a session | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session get` | Get details of a session | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session list` | List sessions from the provider | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session run` | Create a session, send a message, and stream the response | [managed-agent.md](managed-agent.md) |
| `bl managed-agent session send` | Send a message to an existing session and stream the response | [managed-agent.md](managed-agent.md) |
| `bl managed-agent skill-list` | List skills from the provider's skill catalog | [managed-agent.md](managed-agent.md) |
| `bl managed-agent state import` | Import an existing remote resource into agents state | [managed-agent.md](managed-agent.md) |
| `bl managed-agent state list` | List resources tracked in agents state | [managed-agent.md](managed-agent.md) |
| `bl managed-agent state rm` | Remove a resource from state without destroying it remotely | [managed-agent.md](managed-agent.md) |
| `bl managed-agent state show` | Show details of a resource in agents state | [managed-agent.md](managed-agent.md) |
| `bl managed-agent validate` | Validate an agents.yaml configuration (offline) | [managed-agent.md](managed-agent.md) |
## By group
| Group | Commands | Reference |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `managed-agent` | `apply`, `destroy`, `init`, `plan`, `session create`, `session delete`, `session events`, `session get`, `session list`, `session run`, `session send`, `skill-list`, `state import`, `state list`, `state rm`, `state show`, `validate` | [managed-agent.md](managed-agent.md) |
| Group | Commands | Reference |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `managed-agent` | `apply`, `destroy`, `init`, `plan`, `run`, `session create`, `session delete`, `session events`, `session get`, `session list`, `session run`, `session send`, `skill-list`, `state import`, `state list`, `state rm`, `state show`, `validate` | [managed-agent.md](managed-agent.md) |
## Global flags
@@ -7,25 +7,26 @@ Index: [index.md](index.md)
## Commands in this group
| Command | Description |
| --------------------------------- | ------------------------------------------------------------- |
| `bl managed-agent apply` | Apply planned changes to create/update/delete agent resources |
| `bl managed-agent destroy` | Destroy all managed agent resources tracked in state |
| `bl managed-agent init` | Create a new agents.yaml template |
| `bl managed-agent plan` | Show what changes would be applied to agent infrastructure |
| `bl managed-agent session create` | Create a new session for an agent |
| `bl managed-agent session delete` | Delete a session |
| `bl managed-agent session events` | List event history for a session |
| `bl managed-agent session get` | Get details of a session |
| `bl managed-agent session list` | List sessions from the provider |
| `bl managed-agent session run` | Create a session, send a message, and stream the response |
| `bl managed-agent session send` | Send a message to an existing session and stream the response |
| `bl managed-agent skill-list` | List skills from the provider's skill catalog |
| `bl managed-agent state import` | Import an existing remote resource into agents state |
| `bl managed-agent state list` | List resources tracked in agents state |
| `bl managed-agent state rm` | Remove a resource from state without destroying it remotely |
| `bl managed-agent state show` | Show details of a resource in agents state |
| `bl managed-agent validate` | Validate an agents.yaml configuration (offline) |
| Command | Description |
| --------------------------------- | -------------------------------------------------------------- |
| `bl managed-agent apply` | Apply planned changes to create/update/delete agent resources |
| `bl managed-agent destroy` | Destroy all managed agent resources tracked in state |
| `bl managed-agent init` | Create a new agents.yaml template |
| `bl managed-agent plan` | Show what changes would be applied to agent infrastructure |
| `bl managed-agent run` | Provision (if needed) a cloud agent and run a task in one step |
| `bl managed-agent session create` | Create a new session for an agent |
| `bl managed-agent session delete` | Delete a session |
| `bl managed-agent session events` | List event history for a session |
| `bl managed-agent session get` | Get details of a session |
| `bl managed-agent session list` | List sessions from the provider |
| `bl managed-agent session run` | Create a session, send a message, and stream the response |
| `bl managed-agent session send` | Send a message to an existing session and stream the response |
| `bl managed-agent skill-list` | List skills from the provider's skill catalog |
| `bl managed-agent state import` | Import an existing remote resource into agents state |
| `bl managed-agent state list` | List resources tracked in agents state |
| `bl managed-agent state rm` | Remove a resource from state without destroying it remotely |
| `bl managed-agent state show` | Show details of a resource in agents state |
| `bl managed-agent validate` | Validate an agents.yaml configuration (offline) |
## Command details
@@ -170,6 +171,43 @@ bl managed-agent plan --provider bailian
bl managed-agent plan --no-refresh
```
### `bl managed-agent run`
| Field | Value |
| --------------- | ---------------------------------------------------------------------------------------------- |
| **Name** | `managed-agent run` |
| **Description** | Provision (if needed) a cloud agent and run a task in one step |
| **Usage** | `bl managed-agent run --prompt <text> [--instructions <text>] [--model <id>] [--agent <name>]` |
#### Flags
| Flag | Type | Required | Description |
| ----------------------- | ------ | -------- | -------------------------------------------------------------------------- |
| `--prompt <text>` | string | yes | Task to run (required) |
| `--instructions <text>` | string | no | Role/system instructions for the remote agent (default: generic assistant) |
| `--model <id>` | string | no | Model for the remote agent (default: qwen3.8-max) |
| `--agent <name>` | string | no | Agent identity to create/reuse (default: dsh-remote-runner) |
| `--no-stream` | switch | no | Use polling instead of SSE streaming |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
#### Notes
- Bailian credentials come from bl's auth chain: --api-key > DASHSCOPE_API_KEY > `bl auth login` (active config profile).
- Other providers read the env vars referenced in agents.yaml (e.g. ${ANTHROPIC_API_KEY}), including .env and ~/.agents/config.json.
- Resolved credentials are injected into the SDK in-memory and cleared from the environment; they never persist in process env.
- Unlike `apply`, this creates/updates the cloud agent + environment on demand without --yes. The first run provisions cloud resources (may incur cost and take longer to start); later runs with the same --agent reuse them.
#### Examples
```bash
bl managed-agent run --prompt "Summarize the latest AI news"
```
```bash
bl managed-agent run --prompt "Audit this dependency tree" --instructions "You are a security expert" --model qwen3.8-max
```
### `bl managed-agent session create`
| Field | Value |