diff --git a/docs/en/advanced/multi-agent.md b/docs/en/advanced/multi-agent.md index 4b8f0df..7967413 100644 --- a/docs/en/advanced/multi-agent.md +++ b/docs/en/advanced/multi-agent.md @@ -76,8 +76,8 @@ class AgentPool { strategy?: 'crash' | 'manual'; }): Promise; - // Destroy an agent - async destroy(agentId: string): Promise; + // Delete an agent + async delete(agentId: string): Promise; } ``` @@ -190,7 +190,7 @@ deps.toolRegistry.register('task_run', () => taskRunTool); When an Agent calls `task_run`: -1. Agent specifies `agentTemplateId`, `prompt`, and optional `context` +1. Agent specifies `agentTemplateId`, `prompt`, optional `context`, and optional `model` 2. SDK creates a sub-Agent with the specified template 3. Sub-Agent processes the task 4. Result returns to parent Agent @@ -203,6 +203,7 @@ interface TaskRunParams { prompt: string; // Detailed instructions agentTemplateId: string; // Template ID to use context?: string; // Additional context + model?: string | { provider: string; model: string }; // Optional model override } ``` @@ -217,6 +218,14 @@ interface TaskRunResult { } ``` +**Model Inheritance Notes (`delegateTask`):** +- `task_run` accepts an optional `model` argument; when omitted, delegated sub-Agents reuse the parent Agent's `ModelProvider` instance. +- If you need explicit model control, call `agent.delegateTask(...)` directly: + - omit `model`: inherit parent model instance + - `model: string`: keep parent provider type, override model ID (custom providers require `modelFactory`) + - `model: { provider, model }`: explicitly choose provider + model (custom providers usually require `modelFactory` when provider differs) + - `model: ModelProvider`: use provided provider instance directly + ### Sub-Agent Configuration Configure sub-agent behavior in template: diff --git a/docs/en/guides/providers.md b/docs/en/guides/providers.md index e45779a..5c8cd42 100644 --- a/docs/en/guides/providers.md +++ b/docs/en/guides/providers.md @@ -247,7 +247,6 @@ const provider = new GeminiProvider( { thinking: { level: 'medium', // 'minimal' | 'low' | 'medium' | 'high' - includeThoughts: true, }, } ); diff --git a/docs/en/guides/thinking.md b/docs/en/guides/thinking.md index c7a17a3..08958de 100644 --- a/docs/en/guides/thinking.md +++ b/docs/en/guides/thinking.md @@ -101,7 +101,6 @@ const provider = new GeminiProvider( { thinking: { level: 'medium', // 'minimal' | 'low' | 'medium' | 'high' - includeThoughts: true, }, reasoningTransport: 'text', } diff --git a/docs/en/guides/tools.md b/docs/en/guides/tools.md index 4b8eca6..d017170 100644 --- a/docs/en/guides/tools.md +++ b/docs/en/guides/tools.md @@ -60,8 +60,65 @@ const agent = await Agent.create({ ### Task (Sub-Agent) -- `task_run`: Dispatch sub-Agents from template pool, supports `subagent_type`, `context`, `model_name` parameters -- Templates can limit depth and available templates via `runtime.subagents` +- `task_run`: Delegate complex work to a sub-Agent selected from your template pool. +- Parameters: + - `description`: Short task title (recommended 3-5 words) + - `prompt`: Detailed instructions for the sub-Agent + - `agentTemplateId`: Must match a registered template ID + - `context`: Optional extra background (appended to the prompt) + - `model`: Optional model override + - `string`: keep parent provider, override model ID + - `{ provider, model }`: explicitly choose provider + model +- Return fields: + - `status`: `ok` or `paused` + - `template`: Template ID that was used + - `text`: Sub-Agent output + - `permissionIds`: Pending permission IDs (if any) +- Templates can restrict delegation depth and allowed template IDs via `runtime.subagents`. + +**Minimal Example:** + +```typescript +import { createTaskRunTool } from '@shareai-lab/kode-sdk'; + +const templates = [ + { id: 'researcher', system: 'Research and return structured findings.', whenToUse: 'Need search + analysis' }, + { id: 'writer', system: 'Turn findings into publishable copy.', whenToUse: 'Need final draft' }, +]; + +const taskRunTool = createTaskRunTool(templates); +deps.toolRegistry.register('task_run', () => taskRunTool); + +// Example tool-call args: +// { +// "description": "Research pricing", +// "prompt": "Analyze 3 competitors and provide a price table plus recommended range.", +// "agentTemplateId": "researcher", +// "context": "Target market: North America SMB", +// "model": { "provider": "openai", "model": "gpt-4.1-mini" } +// } +``` + +**Common Errors:** +- `Agent template 'xxx' not found`: `agentTemplateId` is not in the `createTaskRunTool(templates)` list. +- Delegation stops unexpectedly: check `runtime.subagents` limits (depth/allowed templates). + +**delegateTask Model Behavior (Important):** +- In `task_run`, `model` is optional. If omitted, sub-Agent reuses parent Agent's `ModelProvider` instance by default. +- If you call `agent.delegateTask(...)` directly, model resolution is: + - `model` omitted: reuse parent `ModelProvider` instance (no `modelFactory` required) + - `model` is `string`: keep parent provider type and only override model ID (for custom providers, this path requires `modelFactory`) + - `model` is `{ provider, model }`: explicitly choose provider + model (if provider differs from parent, custom providers usually require `modelFactory`) + - `model` is `ModelProvider`: use that instance directly + +```typescript +// Direct call with explicit model override +await agent.delegateTask({ + templateId: 'researcher', + prompt: 'Analyze competitors and produce a pricing matrix.', + model: 'gpt-4.1', // same provider type as parent, model id overridden +}); +``` ### Skills Tool diff --git a/docs/en/reference/api.md b/docs/en/reference/api.md index 1e35dc0..fe291d6 100644 --- a/docs/en/reference/api.md +++ b/docs/en/reference/api.md @@ -130,6 +130,25 @@ Creates a forked Agent from a snapshot. async fork(sel?: SnapshotId | { at?: string }): Promise ``` +#### `agent.delegateTask(config)` + +Create and run a delegated sub-Agent task (commonly used by `task_run`). + +```typescript +async delegateTask(config: { + templateId: string; + prompt: string; + model?: string | { provider: string; model: string } | ModelProvider; + tools?: string[]; +}): Promise +``` + +**Model resolution rules:** +- `model` omitted: reuse parent `ModelProvider` instance. +- `model` is `string`: keep parent provider type and override only model ID (for custom providers, this path requires `modelFactory`). +- `model` is `{ provider, model }`: explicitly choose provider + model (for custom providers, this path usually requires `modelFactory` when provider differs). +- `model` is `ModelProvider`: use the provided instance directly. + #### `agent.status()` Returns current Agent status. @@ -237,6 +256,8 @@ interface AgentConfig { tools?: string[]; // Tool names to enable exposeThinking?: boolean; // Emit thinking events retainThinking?: boolean; // Keep thinking in message history + multimodalContinuation?: 'history'; // Preserve multimodal context across turns + multimodalRetention?: { keepRecent?: number }; // Keep recent multimodal items overrides?: { permission?: PermissionConfig; todo?: TodoConfig; @@ -482,7 +503,7 @@ class AgentTemplateRegistry { bulkRegister(templates: AgentTemplateDefinition[]): void; has(id: string): boolean; get(id: string): AgentTemplateDefinition; - list(): string[]; + list(): AgentTemplateDefinition[]; } ``` @@ -521,7 +542,7 @@ class AgentPool { async status(agentId: string): Promise; async fork(agentId: string, snapshotSel?: SnapshotId | { at?: string }): Promise; async resume(agentId: string, config: AgentConfig, opts?: { autoRun?: boolean; strategy?: ResumeStrategy }): Promise; - async destroy(agentId: string): Promise; + async delete(agentId: string): Promise; } ``` @@ -573,9 +594,10 @@ import { AnthropicProvider } from '@shareai-lab/kode-sdk'; const provider = new AnthropicProvider( process.env.ANTHROPIC_API_KEY!, process.env.ANTHROPIC_MODEL_ID ?? 'claude-sonnet-4-20250514', + process.env.ANTHROPIC_BASE_URL, // optional + process.env.HTTPS_PROXY, // optional { thinking: { enabled: true, budgetTokens: 10000 }, - cache: { breakpoints: 4 }, } ); ``` @@ -588,6 +610,8 @@ import { OpenAIProvider } from '@shareai-lab/kode-sdk'; const provider = new OpenAIProvider( process.env.OPENAI_API_KEY!, process.env.OPENAI_MODEL_ID ?? 'gpt-4o', + process.env.OPENAI_BASE_URL, // optional + process.env.HTTPS_PROXY, // optional { api: 'responses', responses: { reasoning: { effort: 'medium' } }, @@ -603,8 +627,10 @@ import { GeminiProvider } from '@shareai-lab/kode-sdk'; const provider = new GeminiProvider( process.env.GOOGLE_API_KEY!, process.env.GEMINI_MODEL_ID ?? 'gemini-2.0-flash', + process.env.GEMINI_BASE_URL, // optional + process.env.HTTPS_PROXY, // optional { - thinking: { level: 'medium', includeThoughts: true }, + thinking: { level: 'medium' }, } ); ``` diff --git a/docs/zh-CN/advanced/multi-agent.md b/docs/zh-CN/advanced/multi-agent.md index 57cd1c0..6501f67 100644 --- a/docs/zh-CN/advanced/multi-agent.md +++ b/docs/zh-CN/advanced/multi-agent.md @@ -76,8 +76,8 @@ class AgentPool { strategy?: 'crash' | 'manual'; }): Promise; - // 销毁 agent - async destroy(agentId: string): Promise; + // 删除 agent + async delete(agentId: string): Promise; } ``` @@ -190,7 +190,7 @@ deps.toolRegistry.register('task_run', () => taskRunTool); 当 Agent 调用 `task_run` 时: -1. Agent 指定 `agentTemplateId`、`prompt` 和可选的 `context` +1. Agent 指定 `agentTemplateId`、`prompt`、可选 `context` 与可选 `model` 2. SDK 使用指定模板创建子 Agent 3. 子 Agent 处理任务 4. 结果返回给父 Agent @@ -203,6 +203,7 @@ interface TaskRunParams { prompt: string; // 详细指令 agentTemplateId: string; // 使用的模板 ID context?: string; // 额外上下文 + model?: string | { provider: string; model: string }; // 可选模型覆盖 } ``` @@ -217,6 +218,14 @@ interface TaskRunResult { } ``` +**模型继承说明(`delegateTask`):** +- `task_run` 支持可选 `model` 参数;不传时,默认让被委派的子 Agent 复用父 Agent 的 `ModelProvider` 实例。 +- 如果你需要显式控制模型,请直接调用 `agent.delegateTask(...)`: + - 不传 `model`:继承父模型实例 + - `model: string`:保持父 provider 类型,仅覆盖模型 ID(自定义 provider 需配合 `modelFactory`) + - `model: { provider, model }`:显式指定 provider + model(provider 与父模型不同时,自定义 provider 通常需配合 `modelFactory`) + - `model: ModelProvider`:直接使用传入的 provider 实例 + ### 子 Agent 配置 在模板中配置子 agent 行为: diff --git a/docs/zh-CN/guides/providers.md b/docs/zh-CN/guides/providers.md index 454819c..da2faef 100644 --- a/docs/zh-CN/guides/providers.md +++ b/docs/zh-CN/guides/providers.md @@ -247,7 +247,6 @@ const provider = new GeminiProvider( { thinking: { level: 'medium', // 'minimal' | 'low' | 'medium' | 'high' - includeThoughts: true, }, } ); diff --git a/docs/zh-CN/guides/thinking.md b/docs/zh-CN/guides/thinking.md index ad211e9..9250043 100644 --- a/docs/zh-CN/guides/thinking.md +++ b/docs/zh-CN/guides/thinking.md @@ -101,7 +101,6 @@ const provider = new GeminiProvider( { thinking: { level: 'medium', // 'minimal' | 'low' | 'medium' | 'high' - includeThoughts: true, }, reasoningTransport: 'text', } diff --git a/docs/zh-CN/guides/tools.md b/docs/zh-CN/guides/tools.md index a72102e..755fad8 100644 --- a/docs/zh-CN/guides/tools.md +++ b/docs/zh-CN/guides/tools.md @@ -60,8 +60,65 @@ const agent = await Agent.create({ ### Task(子代理) -- `task_run`:根据模板池派发子 Agent,支持 `subagent_type`、`context`、`model_name` 参数 -- 模板可以通过 `runtime.subagents` 限制深度与可选模板 +- `task_run`:把复杂任务委派给指定模板的子 Agent。 +- 参数说明: + - `description`:任务短标题(建议 3-5 词) + - `prompt`:对子 Agent 的详细执行指令 + - `agentTemplateId`:必须是模板池中已注册的模板 ID + - `context`:可选,附加背景信息(会拼接到 prompt 后) + - `model`:可选的模型覆盖参数 + - `string`:沿用父 provider,仅覆盖 model ID + - `{ provider, model }`:显式指定 provider + model +- 返回结果: + - `status`:`ok` 或 `paused` + - `template`:实际使用的模板 ID + - `text`:子 Agent 输出 + - `permissionIds`:待审批权限 ID 列表(如有) +- 模板可以通过 `runtime.subagents` 限制递归深度和可选模板。 + +**最小示例:** + +```typescript +import { createTaskRunTool } from '@shareai-lab/kode-sdk'; + +const templates = [ + { id: 'researcher', system: '你负责调研并给出结构化结论。', whenToUse: '需要先检索再总结' }, + { id: 'writer', system: '你负责把结果整理成可发布文稿。', whenToUse: '需要生成最终文稿' }, +]; + +const taskRunTool = createTaskRunTool(templates); +deps.toolRegistry.register('task_run', () => taskRunTool); + +// Agent 在工具调用时传参示例: +// { +// "description": "调研竞品定价", +// "prompt": "调研 3 个主要竞品,输出价格对比表和建议定价区间。", +// "agentTemplateId": "researcher", +// "context": "目标市场:北美中小企业", +// "model": { "provider": "openai", "model": "gpt-4.1-mini" } +// } +``` + +**常见问题:** +- `Agent template 'xxx' not found`:`agentTemplateId` 不在传入 `createTaskRunTool(templates)` 的列表中。 +- 无法继续委派:检查模板的 `runtime.subagents` 配置是否限制了可用模板或深度。 + +**delegateTask 的模型行为(重要):** +- `task_run` 中 `model` 是可选参数;不传时,子 Agent 默认复用父 Agent 的 `ModelProvider` 实例。 +- 如果你直接调用 `agent.delegateTask(...)`,模型解析规则为: + - 不传 `model`:复用父 `ModelProvider` 实例(不依赖 `modelFactory`) + - `model` 为 `string`:沿用父 provider 类型,仅覆盖 model ID(自定义 provider 走这条时需要 `modelFactory`) + - `model` 为 `{ provider, model }`:显式指定 provider + model(provider 与父模型不同时,自定义 provider 通常需要 `modelFactory`) + - `model` 为 `ModelProvider`:直接使用该实例 + +```typescript +// 直接调用并覆盖 model +await agent.delegateTask({ + templateId: 'researcher', + prompt: '分析竞品并输出定价矩阵。', + model: 'gpt-4.1', // 继承父 provider 类型,只覆盖模型 ID +}); +``` ### Skills 工具 diff --git a/docs/zh-CN/reference/api.md b/docs/zh-CN/reference/api.md index 7a03824..37b35bc 100644 --- a/docs/zh-CN/reference/api.md +++ b/docs/zh-CN/reference/api.md @@ -130,6 +130,25 @@ async snapshot(label?: string): Promise async fork(sel?: SnapshotId | { at?: string }): Promise ``` +#### `agent.delegateTask(config)` + +创建并执行被委派的子 Agent 任务(`task_run` 的核心调用路径)。 + +```typescript +async delegateTask(config: { + templateId: string; + prompt: string; + model?: string | { provider: string; model: string } | ModelProvider; + tools?: string[]; +}): Promise +``` + +**模型解析规则:** +- 不传 `model`:复用父 `ModelProvider` 实例。 +- `model` 为 `string`:保持父 provider 类型,仅覆盖模型 ID(自定义 provider 走该路径时需要 `modelFactory`)。 +- `model` 为 `{ provider, model }`:显式指定 provider + model(provider 与父模型不同时,自定义 provider 通常需要 `modelFactory`)。 +- `model` 为 `ModelProvider`:直接使用传入实例。 + #### `agent.status()` 返回当前 Agent 状态。 @@ -237,6 +256,8 @@ interface AgentConfig { tools?: string[]; // 要启用的工具名称 exposeThinking?: boolean; // 发送思考事件 retainThinking?: boolean; // 在消息历史中保留思考 + multimodalContinuation?: 'history'; // 跨轮保留多模态上下文 + multimodalRetention?: { keepRecent?: number }; // 保留最近 N 条多模态内容 overrides?: { permission?: PermissionConfig; todo?: TodoConfig; @@ -482,7 +503,7 @@ class AgentTemplateRegistry { bulkRegister(templates: AgentTemplateDefinition[]): void; has(id: string): boolean; get(id: string): AgentTemplateDefinition; - list(): string[]; + list(): AgentTemplateDefinition[]; } ``` @@ -521,7 +542,7 @@ class AgentPool { async status(agentId: string): Promise; async fork(agentId: string, snapshotSel?: SnapshotId | { at?: string }): Promise; async resume(agentId: string, config: AgentConfig, opts?: { autoRun?: boolean; strategy?: ResumeStrategy }): Promise; - async destroy(agentId: string): Promise; + async delete(agentId: string): Promise; } ``` @@ -573,9 +594,10 @@ import { AnthropicProvider } from '@shareai-lab/kode-sdk'; const provider = new AnthropicProvider( process.env.ANTHROPIC_API_KEY!, process.env.ANTHROPIC_MODEL_ID ?? 'claude-sonnet-4-20250514', + process.env.ANTHROPIC_BASE_URL, // 可选 + process.env.HTTPS_PROXY, // 可选 { thinking: { enabled: true, budgetTokens: 10000 }, - cache: { breakpoints: 4 }, } ); ``` @@ -588,6 +610,8 @@ import { OpenAIProvider } from '@shareai-lab/kode-sdk'; const provider = new OpenAIProvider( process.env.OPENAI_API_KEY!, process.env.OPENAI_MODEL_ID ?? 'gpt-4o', + process.env.OPENAI_BASE_URL, // 可选 + process.env.HTTPS_PROXY, // 可选 { api: 'responses', responses: { reasoning: { effort: 'medium' } }, @@ -603,8 +627,10 @@ import { GeminiProvider } from '@shareai-lab/kode-sdk'; const provider = new GeminiProvider( process.env.GOOGLE_API_KEY!, process.env.GEMINI_MODEL_ID ?? 'gemini-2.0-flash', + process.env.GEMINI_BASE_URL, // 可选 + process.env.HTTPS_PROXY, // 可选 { - thinking: { level: 'medium', includeThoughts: true }, + thinking: { level: 'medium' }, } ); ``` diff --git a/package-lock.json b/package-lock.json index ccf84f4..12b3899 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@shareai-lab/kode-sdk", - "version": "2.7.0", + "version": "2.7.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@shareai-lab/kode-sdk", - "version": "2.7.0", + "version": "2.7.4", "license": "MIT", "dependencies": { "@alibaba-group/opensandbox": "~0.1.4", diff --git a/package.json b/package.json index 34a4643..588aaf1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@shareai-lab/kode-sdk", - "version": "2.7.0", + "version": "2.7.4", "description": "Event-driven, long-running AI Agent development framework with enterprise-grade persistence and context management", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/core/agent.ts b/src/core/agent.ts index 2f32f12..a77dc3f 100644 --- a/src/core/agent.ts +++ b/src/core/agent.ts @@ -138,6 +138,11 @@ export interface CompleteResult { permissionIds?: string[]; } +export interface DelegateTaskModelConfig { + provider: string; + model: string; +} + export interface StreamOptions { since?: Bookmark; kinds?: Array; @@ -692,14 +697,12 @@ export class Agent { async delegateTask(config: { templateId: string; prompt: string; - model?: string; + model?: string | ModelProvider | DelegateTaskModelConfig; tools?: string[]; }): Promise { + const parentModelConfig = this.model.toConfig(); const subAgentConfig: AgentConfig = { templateId: config.templateId, - modelConfig: config.model - ? { provider: 'anthropic', model: config.model } - : this.model.toConfig(), sandbox: this.sandboxConfig || { kind: 'local', workDir: this.sandbox.workDir }, tools: config.tools, multimodalContinuation: this.multimodalContinuation, @@ -711,6 +714,31 @@ export class Agent { }, }; + if (typeof config.model === 'string') { + subAgentConfig.modelConfig = { + ...parentModelConfig, + model: config.model, + }; + } else if (isDelegateTaskModelConfig(config.model)) { + const sameProvider = config.model.provider === parentModelConfig.provider; + subAgentConfig.modelConfig = sameProvider + ? { + ...parentModelConfig, + model: config.model.model, + } + : { + provider: config.model.provider, + model: config.model.model, + }; + } else if (isModelProviderInstance(config.model)) { + subAgentConfig.model = config.model; + } else if (config.model) { + throw new Error('Invalid delegateTask model override: expected string, { provider, model }, or ModelProvider'); + } else { + // Reuse parent model instance to avoid requiring a factory for custom providers. + subAgentConfig.model = this.model; + } + const subAgent = await Agent.create(subAgentConfig, this.deps); subAgent.lineage = [...this.lineage, this.agentId]; try { @@ -2545,6 +2573,27 @@ function buildToolConfig(id: string, template: AgentTemplateDefinition, template return undefined; } +function isModelProviderInstance(value: unknown): value is ModelProvider { + if (!value || typeof value !== 'object') { + return false; + } + const candidate = value as Record; + return typeof candidate.complete === 'function' + && typeof candidate.stream === 'function' + && typeof candidate.toConfig === 'function'; +} + +function isDelegateTaskModelConfig(value: unknown): value is DelegateTaskModelConfig { + if (!value || typeof value !== 'object') { + return false; + } + const candidate = value as Record; + return typeof candidate.provider === 'string' + && candidate.provider.length > 0 + && typeof candidate.model === 'string' + && candidate.model.length > 0; +} + function encodeUlid(time: number, length: number, chars: string): string { let remaining = time; const encoded = Array(length); diff --git a/src/tools/task_run/index.ts b/src/tools/task_run/index.ts index 7bcb0c5..4ea48ea 100644 --- a/src/tools/task_run/index.ts +++ b/src/tools/task_run/index.ts @@ -15,6 +15,14 @@ export function createTaskRunTool(templates: AgentTemplate[]) { throw new Error('Cannot create task_run tool: no agent templates provided'); } + const modelOverrideSchema = z.union([ + z.string().describe('Model ID override while keeping parent provider'), + z.object({ + provider: z.string().describe('Provider ID override (e.g. anthropic/openai/gemini/custom)'), + model: z.string().describe('Model ID for the selected provider'), + }).describe('Explicit provider + model override'), + ]).optional(); + const TaskRun = tool({ name: 'task_run', description: DESCRIPTION, @@ -23,9 +31,10 @@ export function createTaskRunTool(templates: AgentTemplate[]) { prompt: z.string().describe('Detailed instructions for the sub-agent'), agentTemplateId: z.string().describe('Agent template ID to use for this task'), context: z.string().optional().describe('Additional context to append'), + model: modelOverrideSchema, }), async execute(args, ctx: ToolContext) { - const { description, prompt, agentTemplateId, context } = args; + const { description, prompt, agentTemplateId, context, model } = args; const template = templates.find((tpl) => tpl.id === agentTemplateId); @@ -54,6 +63,7 @@ export function createTaskRunTool(templates: AgentTemplate[]) { const result = await ctx.agent.delegateTask({ templateId: template.id, prompt: detailedPrompt, + model, tools: template.tools, }); diff --git a/src/tools/task_run/prompt.ts b/src/tools/task_run/prompt.ts index 5fef7fc..ba6b231 100644 --- a/src/tools/task_run/prompt.ts +++ b/src/tools/task_run/prompt.ts @@ -11,6 +11,10 @@ Instructions: - Always provide a concise "description" (3-5 words) and a detailed "prompt" outlining deliverables. - REQUIRED: Set "agentTemplateId" to one of the available template IDs below. - Optionally supply "context" for extra background information. +- Optional "model" override: + - string: keep parent provider, override model id + - { provider, model }: explicitly choose provider + model + - omitted: inherit parent model instance - The tool returns the sub-agent's final text and any pending permissions. Available agent templates: diff --git a/tests/unit/core/delegate-task.test.ts b/tests/unit/core/delegate-task.test.ts index f54a1e8..cc0d8d1 100644 --- a/tests/unit/core/delegate-task.test.ts +++ b/tests/unit/core/delegate-task.test.ts @@ -1,5 +1,9 @@ -import { builtin } from '../../../src'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { Agent, AgentTemplateRegistry, JSONStore, SandboxFactory, ToolRegistry, builtin } from '../../../src'; import { createUnitTestAgent } from '../../helpers/setup'; +import { MockProvider } from '../../mock-provider'; import { TestRunner, expect } from '../../helpers/utils'; const runner = new TestRunner('Agent 子任务委派'); @@ -45,6 +49,171 @@ runner expect.toBeTruthy(deps.templateRegistry.has('unit-sub-writer')); await cleanup(); + }) + .test('delegateTask 在未提供 modelFactory 时可继承自定义 ModelProvider', async () => { + const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kode-delegate-work-')); + const storeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kode-delegate-store-')); + + const templates = new AgentTemplateRegistry(); + templates.register({ + id: 'unit-main-custom-provider', + systemPrompt: '主代理', + tools: [], + }); + templates.register({ + id: 'unit-sub-custom-provider', + systemPrompt: '子代理', + tools: [], + }); + + const deps = { + store: new JSONStore(storeDir), + templateRegistry: templates, + sandboxFactory: new SandboxFactory(), + toolRegistry: new ToolRegistry(), + }; + + const agent = await Agent.create({ + templateId: 'unit-main-custom-provider', + model: new MockProvider([{ text: 'custom-provider-ok' }]), + sandbox: { kind: 'local', workDir, enforceBoundary: true }, + }, deps); + + try { + const result = await agent.delegateTask({ + templateId: 'unit-sub-custom-provider', + prompt: '请处理该任务', + }); + expect.toEqual(result.status, 'ok'); + expect.toContain(result.text || '', 'custom-provider-ok'); + } finally { + await (agent as any).sandbox?.dispose?.(); + fs.rmSync(workDir, { recursive: true, force: true }); + fs.rmSync(storeDir, { recursive: true, force: true }); + } + }) + .test('delegateTask 的字符串 model 覆盖应沿用父 provider', async () => { + const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kode-delegate-work-')); + const storeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kode-delegate-store-')); + + const templates = new AgentTemplateRegistry(); + templates.register({ + id: 'unit-main-model-override', + systemPrompt: '主代理', + tools: [], + }); + templates.register({ + id: 'unit-sub-model-override', + systemPrompt: '子代理', + tools: [], + }); + + const deps = { + store: new JSONStore(storeDir), + templateRegistry: templates, + sandboxFactory: new SandboxFactory(), + toolRegistry: new ToolRegistry(), + modelFactory: (config: any) => { + if (config.provider !== 'mock') { + throw new Error(`unexpected provider: ${config.provider}`); + } + return new MockProvider([{ text: `provider=${config.provider};model=${config.model}` }]); + }, + }; + + const agent = await Agent.create({ + templateId: 'unit-main-model-override', + model: new MockProvider([{ text: 'parent' }]), + sandbox: { kind: 'local', workDir, enforceBoundary: true }, + }, deps); + + try { + const result = await agent.delegateTask({ + templateId: 'unit-sub-model-override', + prompt: '请处理该任务', + model: 'mock-v2', + }); + expect.toEqual(result.status, 'ok'); + expect.toContain(result.text || '', 'provider=mock;model=mock-v2'); + } finally { + await (agent as any).sandbox?.dispose?.(); + fs.rmSync(workDir, { recursive: true, force: true }); + fs.rmSync(storeDir, { recursive: true, force: true }); + } + }) + .test('delegateTask 支持 provider+model 对象覆盖', async () => { + const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kode-delegate-work-')); + const storeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kode-delegate-store-')); + + const templates = new AgentTemplateRegistry(); + templates.register({ + id: 'unit-main-provider-model-override', + systemPrompt: '主代理', + tools: [], + }); + templates.register({ + id: 'unit-sub-provider-model-override', + systemPrompt: '子代理', + tools: [], + }); + + const deps = { + store: new JSONStore(storeDir), + templateRegistry: templates, + sandboxFactory: new SandboxFactory(), + toolRegistry: new ToolRegistry(), + modelFactory: (config: any) => new MockProvider([{ text: `provider=${config.provider};model=${config.model}` }]), + }; + + const agent = await Agent.create({ + templateId: 'unit-main-provider-model-override', + model: new MockProvider([{ text: 'parent' }]), + sandbox: { kind: 'local', workDir, enforceBoundary: true }, + }, deps); + + try { + const result = await agent.delegateTask({ + templateId: 'unit-sub-provider-model-override', + prompt: '请处理该任务', + model: { provider: 'mock-alt', model: 'mock-alt-v2' }, + }); + expect.toEqual(result.status, 'ok'); + expect.toContain(result.text || '', 'provider=mock-alt;model=mock-alt-v2'); + } finally { + await (agent as any).sandbox?.dispose?.(); + fs.rmSync(workDir, { recursive: true, force: true }); + fs.rmSync(storeDir, { recursive: true, force: true }); + } + }) + .test('task_run 可透传 model 覆盖参数到 delegateTask', async () => { + const taskTool = builtin.task([{ id: 'researcher' }]); + if (!taskTool) { + throw new Error('无法创建 task_run 工具'); + } + + let captured: any; + const result = await taskTool.exec( + { + description: '调研', + prompt: '请调研竞品', + agentTemplateId: 'researcher', + model: { provider: 'openai', model: 'gpt-4.1-mini' }, + }, + { + agentId: 'unit-agent', + sandbox: {} as any, + agent: { + delegateTask: async (config: any) => { + captured = config; + return { status: 'ok', text: 'done', permissionIds: [] }; + }, + }, + } as any + ); + + expect.toEqual((result as any).status, 'ok'); + expect.toEqual(captured.model.provider, 'openai'); + expect.toEqual(captured.model.model, 'gpt-4.1-mini'); }); export async function run() {