From f919ebae3c6c41ab406f313803ad9da5f17599af Mon Sep 17 00:00:00 2001 From: "lisheng.lisheng" Date: Sat, 15 Aug 2026 10:29:24 +0800 Subject: [PATCH] feat(dsh): remote managed-agent as on-demand tool + bl managed-agent run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 [--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//), 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 --- packages/cli/src/commands.ts | 2 + .../managed-agent/_engine/inline-runtime.ts | 122 +++++++++++ .../src/commands/managed-agent/run.ts | 137 ++++++++++++ packages/commands/src/index.ts | 1 + packages/commands/tests/e2e/topic-routes.ts | 1 + packages/dsh/README.md | 69 +++--- packages/dsh/cordis.patch.yml | 12 +- packages/dsh/package.json | 10 +- .../dsh/src/subagent-managed-agent/index.ts | 205 ------------------ packages/dsh/src/tool-managed-agent/index.ts | 131 +++++++++++ packages/dsh/vite.config.ts | 2 +- .../bailian-managed-agent/reference/index.md | 45 ++-- .../reference/managed-agent.md | 76 +++++-- 13 files changed, 519 insertions(+), 294 deletions(-) create mode 100644 packages/commands/src/commands/managed-agent/_engine/inline-runtime.ts create mode 100644 packages/commands/src/commands/managed-agent/run.ts delete mode 100644 packages/dsh/src/subagent-managed-agent/index.ts create mode 100644 packages/dsh/src/tool-managed-agent/index.ts diff --git a/packages/cli/src/commands.ts b/packages/cli/src/commands.ts index b601159..4027ae6 100644 --- a/packages/cli/src/commands.ts +++ b/packages/cli/src/commands.ts @@ -102,6 +102,7 @@ import { managedAgentValidate, managedAgentPlan, managedAgentApply, + managedAgentRun, managedAgentDestroy, managedAgentStateList, managedAgentStateShow, @@ -225,6 +226,7 @@ export const commands: Record = { "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, diff --git a/packages/commands/src/commands/managed-agent/_engine/inline-runtime.ts b/packages/commands/src/commands/managed-agent/_engine/inline-runtime.ts new file mode 100644 index 0000000..a892c76 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/_engine/inline-runtime.ts @@ -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 { + 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 { + 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, + }; +} diff --git a/packages/commands/src/commands/managed-agent/run.ts b/packages/commands/src/commands/managed-agent/run.ts new file mode 100644 index 0000000..5cdfc58 --- /dev/null +++ b/packages/commands/src/commands/managed-agent/run.ts @@ -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: "", + description: "Task to run (required)", + required: true, + }, + instructions: { + type: "string", + valueHint: "", + description: "Role/system instructions for the remote agent (default: generic assistant)", + }, + model: { + type: "string", + valueHint: "", + description: `Model for the remote agent (default: ${DEFAULT_INLINE_MODEL})`, + }, + agent: { + type: "string", + valueHint: "", + 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 [--instructions ] [--model ] [--agent ]", + 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, + }); + } + }); + }), + ); + }, +}); diff --git a/packages/commands/src/index.ts b/packages/commands/src/index.ts index 3c62602..03418e9 100644 --- a/packages/commands/src/index.ts +++ b/packages/commands/src/index.ts @@ -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"; diff --git a/packages/commands/tests/e2e/topic-routes.ts b/packages/commands/tests/e2e/topic-routes.ts index 9e18bf7..3e24704 100644 --- a/packages/commands/tests/e2e/topic-routes.ts +++ b/packages/commands/tests/e2e/topic-routes.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", diff --git a/packages/dsh/README.md b/packages/dsh/README.md index d0908be..c52dae3 100644 --- a/packages/dsh/README.md +++ b/packages/dsh/README.md @@ -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**:派发一个子任务 +- **远程任务**:说「在云端帮我跑一个任务:,它该擅长 」→ 确认模型调用 `bailian_run_remote_task`(`instructions` 由模型按 role 填)→ 首次触发云端创建 → 返回远程会话结果(需按量付费 Key + 已开通 agentstudio) --- diff --git a/packages/dsh/cordis.patch.yml b/packages/dsh/cordis.patch.yml index f36f0ee..eaca60c 100644 --- a/packages/dsh/cordis.patch.yml +++ b/packages/dsh/cordis.patch.yml @@ -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: {} diff --git a/packages/dsh/package.json b/packages/dsh/package.json index f4f1450..4312144 100644 --- a/packages/dsh/package.json +++ b/packages/dsh/package.json @@ -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" }, diff --git a/packages/dsh/src/subagent-managed-agent/index.ts b/packages/dsh/src/subagent-managed-agent/index.ts deleted file mode 100644 index bab0a5b..0000000 --- a/packages/dsh/src/subagent-managed-agent/index.ts +++ /dev/null @@ -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 = 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 => 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 { - 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 | undefined; - return { - id: SessionId(`bailian-managed-agent:${crypto.randomUUID()}`), - localAgent: undefined, - result, - dispose: (): Promise => { - disposal ??= (async (): Promise => { - controller.abort(); - await result; - })(); - return disposal; - }, - }; - } - - private async execute( - argv: readonly string[], - cwd: string, - signal: AbortSignal, - deadline: AbortSignal, - ): Promise { - try { - const response = await runBlJson(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)); -} diff --git a/packages/dsh/src/tool-managed-agent/index.ts b/packages/dsh/src/tool-managed-agent/index.ts new file mode 100644 index 0000000..942ac6a --- /dev/null +++ b/packages/dsh/src/tool-managed-agent/index.ts @@ -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 = 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(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 ?? "" }; + }, + }), + ); +} diff --git a/packages/dsh/vite.config.ts b/packages/dsh/vite.config.ts index 2dd837d..c7ae8a5 100644 --- a/packages/dsh/vite.config.ts +++ b/packages/dsh/vite.config.ts @@ -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: { diff --git a/skills/bailian-managed-agent/reference/index.md b/skills/bailian-managed-agent/reference/index.md index 24067d2..aaf4371 100644 --- a/skills/bailian-managed-agent/reference/index.md +++ b/skills/bailian-managed-agent/reference/index.md @@ -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 diff --git a/skills/bailian-managed-agent/reference/managed-agent.md b/skills/bailian-managed-agent/reference/managed-agent.md index db5e29e..67d5242 100644 --- a/skills/bailian-managed-agent/reference/managed-agent.md +++ b/skills/bailian-managed-agent/reference/managed-agent.md @@ -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 [--instructions ] [--model ] [--agent ]` | + +#### Flags + +| Flag | Type | Required | Description | +| ----------------------- | ------ | -------- | -------------------------------------------------------------------------- | +| `--prompt ` | string | yes | Task to run (required) | +| `--instructions ` | string | no | Role/system instructions for the remote agent (default: generic assistant) | +| `--model ` | string | no | Model for the remote agent (default: qwen3.8-max) | +| `--agent ` | string | no | Agent identity to create/reuse (default: dsh-remote-runner) | +| `--no-stream` | switch | no | Use polling instead of SSE streaming | +| `--api-key ` | string | no | API key | +| `--base-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 |