@oyasmi/pipiclaw 0.3.3 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +66 -16
- package/dist/agent.js.map +1 -1
- package/dist/prompt-builder.d.ts.map +1 -1
- package/dist/prompt-builder.js +1 -0
- package/dist/prompt-builder.js.map +1 -1
- package/dist/store.d.ts +31 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +6 -0
- package/dist/store.js.map +1 -1
- package/dist/sub-agents.d.ts +5 -1
- package/dist/sub-agents.d.ts.map +1 -1
- package/dist/sub-agents.js +26 -7
- package/dist/sub-agents.js.map +1 -1
- package/dist/tools/index.d.ts +6 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +6 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/subagent.d.ts +23 -1
- package/dist/tools/subagent.d.ts.map +1 -1
- package/dist/tools/subagent.js +52 -15
- package/dist/tools/subagent.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -225,6 +225,7 @@ Pipiclaw 同时支持:
|
|
|
225
225
|
| `.channel-meta.json` | 不支持 | `<channel>/.channel-meta.json` | 运行时自动维护,用于主动发送和重启恢复,不建议手工编辑。 |
|
|
226
226
|
| `context.jsonl` | 不支持 | `<channel>/context.jsonl` | 原始 session 存储,冷文件,不主动加载或扫描。 |
|
|
227
227
|
| `log.jsonl` | 不支持 | `<channel>/log.jsonl` | 原始消息存储,冷文件,不主动加载或扫描。 |
|
|
228
|
+
| `subagent-runs.jsonl` | 不支持 | `<channel>/subagent-runs.jsonl` | sub-agent 运行摘要日志。记录其输出摘要、预算停止原因和 usage,便于事后审查。 |
|
|
228
229
|
|
|
229
230
|
### File Intent
|
|
230
231
|
|
|
@@ -351,7 +352,11 @@ Keep findings concise and actionable.
|
|
|
351
352
|
- `model` 可省略;省略时默认使用主 Agent 当前模型
|
|
352
353
|
- `tools` 可省略;省略时默认使用 `read,bash`
|
|
353
354
|
- 各预算字段都可省略;省略时会使用 runtime 默认值
|
|
355
|
+
- 默认预算值的设计目标是优先防止失控和成本失真,而不是追求“一个 sub-agent 包办整件大任务”;如果任务明显更重,应该显式调大预算
|
|
354
356
|
- sub-agent 不会拿到 `subagent` 工具,因此不能再创建孙 agent
|
|
357
|
+
- sub-agent 只隔离 LLM 对话上下文,不隔离文件系统和 executor;它读写的 workspace 文件对主 Agent 后续同样可见
|
|
358
|
+
- runtime 会自动给 sub-agent 注入一小段固定运行上下文,例如 workspace 根目录、channel id 和 sandbox 类型;主 Agent 仍然需要把任务本身所需的业务上下文写进 `task`
|
|
359
|
+
- 如果 sub-agent 已经产出可用结果,但因预算耗尽或中途停止未完整完成,runtime 会保留这部分结果返回给主 Agent,并在 channel 目录的 `subagent-runs.jsonl` 中记录执行摘要
|
|
355
360
|
|
|
356
361
|
### 使用建议
|
|
357
362
|
|
package/dist/agent.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,KAAK,cAAc,EAAqB,MAAM,eAAe,CAAC;AAGvE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAMrD,OAAO,EAAkB,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAS/C,MAAM,WAAW,WAAW;IAC3B,GAAG,CAAC,GAAG,EAAE,eAAe,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvG,oBAAoB,CAAC,GAAG,EAAE,eAAe,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnF,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAuwBD,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,WAAW,CAOlH","sourcesContent":["import { Agent } from \"@mariozechner/pi-agent-core\";\nimport type { Api, Model } from \"@mariozechner/pi-ai\";\nimport {\n\tAgentSession,\n\tAuthStorage,\n\tconvertToLlm,\n\tDefaultResourceLoader,\n\tModelRegistry,\n\tSessionManager,\n\ttype Skill,\n} from \"@mariozechner/pi-coding-agent\";\nimport { mkdir, writeFile } from \"fs/promises\";\nimport { dirname, join, resolve } from \"path\";\nimport { COMMAND_RESULT_CUSTOM_TYPE, createCommandExtension } from \"./command-extension.js\";\nimport { type BuiltInCommand, renderBuiltInHelp } from \"./commands.js\";\nimport { getAgentConfig, getApiKeyForModel, getSoul, loadPipiclawSkills } from \"./config-loader.js\";\nimport { PipiclawSettingsManager } from \"./context.js\";\nimport type { DingTalkContext } from \"./dingtalk.js\";\nimport * as log from \"./log.js\";\nimport { MemoryLifecycle } from \"./memory-lifecycle.js\";\nimport { resolveInitialModel } from \"./model-utils.js\";\nimport { APP_HOME_DIR, AUTH_CONFIG_PATH, MODELS_CONFIG_PATH } from \"./paths.js\";\nimport { buildAppendSystemPrompt } from \"./prompt-builder.js\";\nimport { createExecutor, type SandboxConfig } from \"./sandbox.js\";\nimport type { ChannelStore } from \"./store.js\";\nimport { discoverSubAgents, formatSubAgentList } from \"./sub-agents.js\";\nimport { createPipiclawTools } from \"./tools/index.js\";\nimport type { SubAgentToolDetails } from \"./tools/subagent.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface AgentRunner {\n\trun(ctx: DingTalkContext, store: ChannelStore): Promise<{ stopReason: string; errorMessage?: string }>;\n\thandleBuiltinCommand(ctx: DingTalkContext, command: BuiltInCommand): Promise<void>;\n\tqueueSteer(text: string, userName?: string): Promise<void>;\n\tqueueFollowUp(text: string, userName?: string): Promise<void>;\n\tabort(): Promise<void>;\n}\n\ntype FinalOutcome = { kind: \"none\" } | { kind: \"silent\" } | { kind: \"final\"; text: string };\ntype ModelRegistryClass = {\n\tcreate?: (authStorage: AuthStorage, modelsJsonPath?: string) => ModelRegistry;\n\tnew (authStorage: AuthStorage, modelsJsonPath?: string): ModelRegistry;\n};\n\nfunction isSilentOutcome(outcome: FinalOutcome): outcome is { kind: \"silent\" } {\n\treturn outcome.kind === \"silent\";\n}\n\nfunction isFinalOutcome(outcome: FinalOutcome): outcome is { kind: \"final\"; text: string } {\n\treturn outcome.kind === \"final\";\n}\n\nfunction getFinalOutcomeText(outcome: FinalOutcome): string | null {\n\treturn isFinalOutcome(outcome) ? outcome.text : null;\n}\n\nfunction createModelRegistry(authStorage: AuthStorage, modelsJsonPath: string): ModelRegistry {\n\tconst registryClass = ModelRegistry as unknown as ModelRegistryClass;\n\treturn typeof registryClass.create === \"function\"\n\t\t? registryClass.create(authStorage, modelsJsonPath)\n\t\t: new registryClass(authStorage, modelsJsonPath);\n}\n\n// ============================================================================\n// Text helpers\n// ============================================================================\n\nfunction truncate(text: string, maxLen: number): string {\n\tif (text.length <= maxLen) return text;\n\treturn `${text.substring(0, maxLen - 3)}...`;\n}\n\nfunction sanitizeProgressText(text: string): string {\n\treturn text\n\t\t.replace(/\\uFFFC/g, \"\")\n\t\t.replace(/\\r/g, \"\")\n\t\t.trim();\n}\n\nfunction formatProgressEntry(kind: \"tool\" | \"thinking\" | \"error\" | \"assistant\", text: string): string {\n\tconst cleaned = sanitizeProgressText(text);\n\tif (!cleaned) return \"\";\n\n\tconst normalized = cleaned.replace(/\\n+/g, \" \").trim();\n\tswitch (kind) {\n\t\tcase \"tool\":\n\t\t\treturn `Running: ${normalized}`;\n\t\tcase \"thinking\":\n\t\t\treturn `Thinking: ${normalized}`;\n\t\tcase \"error\":\n\t\t\treturn `Error: ${normalized}`;\n\t\tcase \"assistant\":\n\t\t\treturn normalized;\n\t}\n}\n\nfunction extractToolResultText(result: unknown): string {\n\tif (typeof result === \"string\") {\n\t\treturn result;\n\t}\n\n\tif (\n\t\tresult &&\n\t\ttypeof result === \"object\" &&\n\t\t\"content\" in result &&\n\t\tArray.isArray((result as { content: unknown }).content)\n\t) {\n\t\tconst content = (result as { content: Array<{ type: string; text?: string }> }).content;\n\t\tconst textParts: string[] = [];\n\t\tfor (const part of content) {\n\t\t\tif (part.type === \"text\" && part.text) {\n\t\t\t\ttextParts.push(part.text);\n\t\t\t}\n\t\t}\n\t\tif (textParts.length > 0) {\n\t\t\treturn textParts.join(\"\\n\");\n\t\t}\n\t}\n\n\treturn JSON.stringify(result);\n}\n\nfunction isSubAgentToolDetails(value: unknown): value is SubAgentToolDetails {\n\tif (!value || typeof value !== \"object\" || !(\"usage\" in value)) {\n\t\treturn false;\n\t}\n\n\tconst usage = (value as { usage?: unknown }).usage;\n\treturn (\n\t\t!!usage &&\n\t\ttypeof usage === \"object\" &&\n\t\t\"input\" in usage &&\n\t\t\"output\" in usage &&\n\t\t\"cacheRead\" in usage &&\n\t\t\"cacheWrite\" in usage &&\n\t\t\"cost\" in usage\n\t);\n}\n\nfunction mergeSubAgentUsage(totalUsage: UsageTotals, details: SubAgentToolDetails): void {\n\ttotalUsage.input += details.usage.input;\n\ttotalUsage.output += details.usage.output;\n\ttotalUsage.cacheRead += details.usage.cacheRead;\n\ttotalUsage.cacheWrite += details.usage.cacheWrite;\n\ttotalUsage.cost.input += details.usage.cost.input;\n\ttotalUsage.cost.output += details.usage.cost.output;\n\ttotalUsage.cost.cacheRead += details.usage.cost.cacheRead;\n\ttotalUsage.cost.cacheWrite += details.usage.cost.cacheWrite;\n\ttotalUsage.cost.total += details.usage.cost.total;\n}\n\nfunction extractCustomCommandResultText(message: unknown): string | null {\n\tif (\n\t\t!message ||\n\t\ttypeof message !== \"object\" ||\n\t\t!(\"role\" in message) ||\n\t\t!(\"customType\" in message) ||\n\t\t(message as { role?: unknown }).role !== \"custom\" ||\n\t\t(message as { customType?: unknown }).customType !== COMMAND_RESULT_CUSTOM_TYPE\n\t) {\n\t\treturn null;\n\t}\n\n\tconst content = (message as { content?: unknown }).content;\n\treturn typeof content === \"string\" && content.trim() ? content : null;\n}\n\n// ============================================================================\n// Run State\n// ============================================================================\n\ninterface PendingTool {\n\ttoolName: string;\n\targs: unknown;\n\tstartTime: number;\n}\n\ninterface UsageTotals {\n\tinput: number;\n\toutput: number;\n\tcacheRead: number;\n\tcacheWrite: number;\n\tcost: { input: number; output: number; cacheRead: number; cacheWrite: number; total: number };\n}\n\ninterface RunQueue {\n\tenqueue(fn: () => Promise<void>, errorContext: string): void;\n\tenqueueMessage(text: string, target: \"main\" | \"thread\", errorContext: string, doLog?: boolean): void;\n}\n\ninterface RunState {\n\tctx: DingTalkContext | null;\n\tlogCtx: { channelId: string; userName?: string; channelName?: string } | null;\n\tqueue: RunQueue | null;\n\tpendingTools: Map<string, PendingTool>;\n\ttotalUsage: UsageTotals;\n\tstopReason: string;\n\terrorMessage: string | undefined;\n\tfinalOutcome: FinalOutcome;\n\tfinalResponseDelivered: boolean;\n}\n\nfunction createEmptyRunState(): RunState {\n\treturn {\n\t\tctx: null,\n\t\tlogCtx: null,\n\t\tqueue: null,\n\t\tpendingTools: new Map(),\n\t\ttotalUsage: {\n\t\t\tinput: 0,\n\t\t\toutput: 0,\n\t\t\tcacheRead: 0,\n\t\t\tcacheWrite: 0,\n\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t},\n\t\tstopReason: \"stop\",\n\t\terrorMessage: undefined,\n\t\tfinalOutcome: { kind: \"none\" },\n\t\tfinalResponseDelivered: false,\n\t};\n}\n\n// ============================================================================\n// ChannelRunner\n// ============================================================================\n\nclass ChannelRunner implements AgentRunner {\n\t// --- Constructed once ---\n\tprivate readonly sandboxConfig: SandboxConfig;\n\tprivate readonly channelId: string;\n\tprivate readonly channelDir: string;\n\tprivate readonly workspacePath: string;\n\tprivate readonly workspaceDir: string;\n\tprivate readonly session: AgentSession;\n\tprivate readonly agent: Agent;\n\tprivate readonly sessionManager: SessionManager;\n\tprivate readonly settingsManager: PipiclawSettingsManager;\n\tprivate readonly modelRegistry: ModelRegistry;\n\tprivate readonly memoryLifecycle: MemoryLifecycle;\n\tprivate readonly sessionReady: Promise<void>;\n\n\t// --- Mutable across runs ---\n\tprivate activeModel: Model<Api>;\n\tprivate currentSkills: Skill[];\n\n\t// --- Per run ---\n\tprivate runState: RunState = createEmptyRunState();\n\n\tconstructor(sandboxConfig: SandboxConfig, channelId: string, channelDir: string) {\n\t\tthis.sandboxConfig = sandboxConfig;\n\t\tthis.channelId = channelId;\n\t\tthis.channelDir = channelDir;\n\n\t\tconst executor = createExecutor(sandboxConfig);\n\t\tthis.workspaceDir = resolve(dirname(channelDir));\n\t\tthis.workspacePath = executor.getWorkspacePath(this.workspaceDir);\n\n\t\t// Initial skill summaries\n\t\tconst initialSkills = loadPipiclawSkills(channelDir, this.workspacePath);\n\t\tthis.currentSkills = initialSkills;\n\n\t\t// Create session manager\n\t\tconst contextFile = join(channelDir, \"context.jsonl\");\n\t\tthis.sessionManager = SessionManager.open(contextFile, channelDir);\n\t\tthis.settingsManager = new PipiclawSettingsManager(APP_HOME_DIR);\n\n\t\t// Create AuthStorage and ModelRegistry\n\t\tconst authStorage = AuthStorage.create(AUTH_CONFIG_PATH);\n\t\tthis.modelRegistry = createModelRegistry(authStorage, MODELS_CONFIG_PATH);\n\n\t\t// Resolve model: prefer saved global default, fall back to first available model\n\t\tthis.activeModel = resolveInitialModel(this.modelRegistry, this.settingsManager);\n\t\tlog.logInfo(`Using model: ${this.activeModel.provider}/${this.activeModel.id} (${this.activeModel.name})`);\n\n\t\t// Create tools\n\t\tconst tools = createPipiclawTools({\n\t\t\texecutor,\n\t\t\tgetCurrentModel: () => this.activeModel,\n\t\t\tgetAvailableModels: () => this.modelRegistry.getAvailable(),\n\t\t\tresolveApiKey: async (model) => getApiKeyForModel(this.modelRegistry, model),\n\t\t\tworkspaceDir: this.workspaceDir,\n\t\t});\n\n\t\t// Create agent\n\t\tthis.agent = new Agent({\n\t\t\tinitialState: {\n\t\t\t\tsystemPrompt: \"\",\n\t\t\t\tmodel: this.activeModel,\n\t\t\t\tthinkingLevel: \"off\",\n\t\t\t\ttools,\n\t\t\t},\n\t\t\tconvertToLlm,\n\t\t\tgetApiKey: async () => getApiKeyForModel(this.modelRegistry, this.activeModel),\n\t\t});\n\n\t\tthis.memoryLifecycle = new MemoryLifecycle({\n\t\t\tchannelId: this.channelId,\n\t\t\tchannelDir: this.channelDir,\n\t\t\tgetMessages: () => this.session.messages,\n\t\t\tgetSessionEntries: () => this.sessionManager.getBranch(),\n\t\t\tgetModel: () => this.session.model ?? this.activeModel,\n\t\t\tresolveApiKey: async (model) => getApiKeyForModel(this.modelRegistry, model),\n\t\t});\n\n\t\tconst resourceLoader = new DefaultResourceLoader({\n\t\t\tcwd: process.cwd(),\n\t\t\tagentDir: APP_HOME_DIR,\n\t\t\tsettingsManager: this.settingsManager as any,\n\t\t\textensionFactories: [\n\t\t\t\tthis.memoryLifecycle.createExtensionFactory(),\n\t\t\t\tcreateCommandExtension({\n\t\t\t\t\tgetCurrentModel: () => this.session.model ?? this.activeModel,\n\t\t\t\t\tgetAvailableModels: async () => {\n\t\t\t\t\t\tthis.modelRegistry.refresh();\n\t\t\t\t\t\treturn await this.modelRegistry.getAvailable();\n\t\t\t\t\t},\n\t\t\t\t\tgetSessionStats: () => this.session.getSessionStats(),\n\t\t\t\t\tgetThinkingLevel: () => this.session.thinkingLevel,\n\t\t\t\t\tswitchModel: async (model) => {\n\t\t\t\t\t\tawait this.session.setModel(model);\n\t\t\t\t\t\tthis.activeModel = model;\n\t\t\t\t\t},\n\t\t\t\t\trefreshSessionResources: async () => {\n\t\t\t\t\t\tawait this.refreshSessionResources();\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t],\n\t\t\tappendSystemPromptOverride: (base) => {\n\t\t\t\tconst soul = getSoul(this.workspaceDir);\n\t\t\t\tconst sections = [...base];\n\t\t\t\tif (soul) {\n\t\t\t\t\tsections.unshift(soul);\n\t\t\t\t}\n\t\t\t\tconst subAgents = discoverSubAgents(this.workspaceDir, this.modelRegistry.getAvailable());\n\t\t\t\tsections.push(\n\t\t\t\t\tbuildAppendSystemPrompt(this.workspacePath, this.channelId, this.sandboxConfig, {\n\t\t\t\t\t\tsubAgentList: formatSubAgentList(subAgents.agents),\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t\treturn sections;\n\t\t\t},\n\t\t\tagentsFilesOverride: () => {\n\t\t\t\tconst agentConfig = getAgentConfig(this.channelDir);\n\t\t\t\treturn {\n\t\t\t\t\tagentsFiles: agentConfig ? [{ path: `${this.workspacePath}/AGENTS.md`, content: agentConfig }] : [],\n\t\t\t\t};\n\t\t\t},\n\t\t\tskillsOverride: (base) => ({\n\t\t\t\tskills: [...base.skills, ...this.currentSkills],\n\t\t\t\tdiagnostics: base.diagnostics,\n\t\t\t}),\n\t\t});\n\n\t\tconst baseToolsOverride = Object.fromEntries(tools.map((tool) => [tool.name, tool]));\n\n\t\t// Create AgentSession\n\t\tthis.session = new AgentSession({\n\t\t\tagent: this.agent,\n\t\t\tsessionManager: this.sessionManager,\n\t\t\tsettingsManager: this.settingsManager as any,\n\t\t\tcwd: process.cwd(),\n\t\t\tmodelRegistry: this.modelRegistry,\n\t\t\tresourceLoader,\n\t\t\tbaseToolsOverride,\n\t\t});\n\n\t\t// Subscribe to session events\n\t\tthis.subscribeToSessionEvents();\n\t\tthis.sessionReady = this.initializeSession();\n\t}\n\n\t// === Public API ===\n\n\tasync run(ctx: DingTalkContext, _store: ChannelStore): Promise<{ stopReason: string; errorMessage?: string }> {\n\t\tthis.resetRunState(ctx);\n\n\t\t// Create queue for this run\n\t\tlet queueChain = Promise.resolve();\n\t\tthis.runState.queue = {\n\t\t\tenqueue: (fn: () => Promise<void>, errorContext: string): void => {\n\t\t\t\tqueueChain = queueChain.then(async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait fn();\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\tconst errMsg = err instanceof Error ? err.message : String(err);\n\t\t\t\t\t\tlog.logWarning(`DingTalk API error (${errorContext})`, errMsg);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t},\n\t\t\tenqueueMessage: function (text: string, target: \"main\" | \"thread\", errorContext: string, doLog = true): void {\n\t\t\t\tthis.enqueue(\n\t\t\t\t\t() => (target === \"main\" ? ctx.respond(text, doLog) : ctx.respondInThread(text)),\n\t\t\t\t\terrorContext,\n\t\t\t\t);\n\t\t\t},\n\t\t};\n\n\t\ttry {\n\t\t\tawait this.ensureSessionReady();\n\n\t\t\t// Ensure channel directory exists\n\t\t\tawait mkdir(this.channelDir, { recursive: true });\n\n\t\t\tconst userMessage = this.formatUserMessage(ctx.message.text, ctx.message.userName);\n\t\t\tconst promptText = this.shouldPreserveRawInput(ctx.message.text) ? ctx.message.text.trim() : userMessage;\n\n\t\t\t// Debug: write context to last_prompt.json (only with PIPICLAW_DEBUG=1)\n\t\t\tif (process.env.PIPICLAW_DEBUG) {\n\t\t\t\tconst debugContext = {\n\t\t\t\t\tsystemPrompt: this.agent.state.systemPrompt,\n\t\t\t\t\tmessages: this.session.messages,\n\t\t\t\t\tnewUserMessage: promptText,\n\t\t\t\t};\n\t\t\t\tawait writeFile(join(this.channelDir, \"last_prompt.json\"), JSON.stringify(debugContext, null, 2));\n\t\t\t}\n\n\t\t\tawait this.session.prompt(promptText);\n\t\t} catch (err) {\n\t\t\tthis.runState.stopReason = \"error\";\n\t\t\tthis.runState.errorMessage = err instanceof Error ? err.message : String(err);\n\t\t\tlog.logWarning(`[${this.channelId}] Runner failed`, this.runState.errorMessage);\n\t\t} finally {\n\t\t\tawait queueChain;\n\t\t\tconst finalOutcome = this.runState.finalOutcome;\n\t\t\tconst finalOutcomeText = getFinalOutcomeText(finalOutcome);\n\n\t\t\ttry {\n\t\t\t\tif (\n\t\t\t\t\tthis.runState.stopReason === \"error\" &&\n\t\t\t\t\tthis.runState.errorMessage &&\n\t\t\t\t\t!this.runState.finalResponseDelivered\n\t\t\t\t) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait ctx.replaceMessage(\"_Sorry, something went wrong_\");\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\tconst errMsg = err instanceof Error ? err.message : String(err);\n\t\t\t\t\t\tlog.logWarning(\"Failed to post error message\", errMsg);\n\t\t\t\t\t}\n\t\t\t\t} else if (isSilentOutcome(finalOutcome)) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait ctx.deleteMessage();\n\t\t\t\t\t\tlog.logInfo(\"Silent response - deleted message\");\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\tconst errMsg = err instanceof Error ? err.message : String(err);\n\t\t\t\t\t\tlog.logWarning(\"Failed to delete message for silent response\", errMsg);\n\t\t\t\t\t}\n\t\t\t\t} else if (finalOutcomeText && !this.runState.finalResponseDelivered) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait ctx.replaceMessage(finalOutcomeText);\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\tconst errMsg = err instanceof Error ? err.message : String(err);\n\t\t\t\t\t\tlog.logWarning(\"Failed to replace message with final text\", errMsg);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tawait ctx.flush();\n\t\t\t} finally {\n\t\t\t\tawait ctx.close();\n\t\t\t}\n\n\t\t\t// Log usage summary\n\t\t\tif (this.runState.totalUsage.cost.total > 0) {\n\t\t\t\tconst messages = this.session.messages;\n\t\t\t\tconst lastAssistantMessage = messages\n\t\t\t\t\t.slice()\n\t\t\t\t\t.reverse()\n\t\t\t\t\t.find((m: any) => m.role === \"assistant\" && m.stopReason !== \"aborted\") as any;\n\n\t\t\t\tconst contextTokens = lastAssistantMessage\n\t\t\t\t\t? lastAssistantMessage.usage.input +\n\t\t\t\t\t\tlastAssistantMessage.usage.output +\n\t\t\t\t\t\tlastAssistantMessage.usage.cacheRead +\n\t\t\t\t\t\tlastAssistantMessage.usage.cacheWrite\n\t\t\t\t\t: 0;\n\t\t\t\tconst currentRunModel = this.session.model ?? this.activeModel;\n\t\t\t\tconst contextWindow = currentRunModel.contextWindow || 200000;\n\n\t\t\t\tlog.logUsageSummary(this.runState.logCtx!, this.runState.totalUsage, contextTokens, contextWindow);\n\t\t\t}\n\n\t\t\t// Clear run state\n\t\t\tthis.runState.ctx = null;\n\t\t\tthis.runState.logCtx = null;\n\t\t\tthis.runState.queue = null;\n\t\t}\n\n\t\treturn { stopReason: this.runState.stopReason, errorMessage: this.runState.errorMessage };\n\t}\n\n\tasync handleBuiltinCommand(ctx: DingTalkContext, command: BuiltInCommand): Promise<void> {\n\t\ttry {\n\t\t\tswitch (command.name) {\n\t\t\t\tcase \"help\":\n\t\t\t\t\tawait this.sendCommandReply(ctx, renderBuiltInHelp());\n\t\t\t\t\treturn;\n\t\t\t\tcase \"stop\":\n\t\t\t\t\tawait this.sendCommandReply(ctx, \"No task is running. Use `/stop` only while a task is running.\");\n\t\t\t\t\treturn;\n\t\t\t\tcase \"steer\":\n\t\t\t\t\tthis.requireQueuedMessage(command.args, \"steer\");\n\t\t\t\t\tawait this.sendCommandReply(\n\t\t\t\t\t\tctx,\n\t\t\t\t\t\t\"No task is running. Send the message directly instead of using `/steer`.\",\n\t\t\t\t\t);\n\t\t\t\t\treturn;\n\t\t\t\tcase \"followup\":\n\t\t\t\t\tthis.requireQueuedMessage(command.args, \"followup\");\n\t\t\t\t\tawait this.sendCommandReply(\n\t\t\t\t\t\tctx,\n\t\t\t\t\t\t\"No task is running. Send the message directly now, or use `/followup` while a task is running.\",\n\t\t\t\t\t);\n\t\t\t\t\treturn;\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tconst errMsg = err instanceof Error ? err.message : String(err);\n\t\t\tlog.logWarning(`[${this.channelId}] Built-in command failed`, errMsg);\n\t\t\tawait this.sendCommandReply(ctx, `命令执行失败:${errMsg}`);\n\t\t}\n\t}\n\n\tasync queueSteer(text: string, userName?: string): Promise<void> {\n\t\tawait this.queueBusyMessage(\"steer\", this.requireQueuedMessage(text, \"steer\"), userName);\n\t}\n\n\tasync queueFollowUp(text: string, userName?: string): Promise<void> {\n\t\tawait this.queueBusyMessage(\"followUp\", this.requireQueuedMessage(text, \"followup\"), userName);\n\t}\n\n\tasync abort(): Promise<void> {\n\t\tawait this.session.abort();\n\t}\n\n\t// === Private helpers ===\n\n\tprivate async sendCommandReply(ctx: DingTalkContext, text: string): Promise<void> {\n\t\tconst delivered = await ctx.respondPlain(text);\n\t\tif (!delivered) {\n\t\t\tawait ctx.replaceMessage(text);\n\t\t\tawait ctx.flush();\n\t\t}\n\t}\n\n\tprivate requireQueuedMessage(text: string, commandName: \"steer\" | \"followup\"): string {\n\t\tconst trimmedText = text.trim();\n\t\tif (!trimmedText) {\n\t\t\tthrow new Error(`/${commandName} requires a message.`);\n\t\t}\n\t\treturn trimmedText;\n\t}\n\n\tprivate shouldPreserveRawInput(text: string): boolean {\n\t\treturn text.trim().startsWith(\"/\");\n\t}\n\n\tprivate formatUserMessage(text: string, userName?: string, now: Date = new Date()): string {\n\t\tconst pad = (n: number) => n.toString().padStart(2, \"0\");\n\t\tconst offset = -now.getTimezoneOffset();\n\t\tconst offsetSign = offset >= 0 ? \"+\" : \"-\";\n\t\tconst offsetHours = pad(Math.floor(Math.abs(offset) / 60));\n\t\tconst offsetMins = pad(Math.abs(offset) % 60);\n\t\tconst timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}${offsetSign}${offsetHours}:${offsetMins}`;\n\t\treturn `[${timestamp}] [${userName || \"unknown\"}]: ${text}`;\n\t}\n\n\tprivate async queueBusyMessage(delivery: \"steer\" | \"followUp\", text: string, userName?: string): Promise<void> {\n\t\tif (!this.session.isStreaming) {\n\t\t\tthrow new Error(\"No task is currently running.\");\n\t\t}\n\n\t\tawait this.session.prompt(this.formatUserMessage(text, userName), {\n\t\t\tstreamingBehavior: delivery,\n\t\t});\n\t}\n\n\tprivate resetRunState(ctx: DingTalkContext): void {\n\t\tthis.runState = createEmptyRunState();\n\t\tthis.runState.ctx = ctx;\n\t\tthis.runState.logCtx = {\n\t\t\tchannelId: ctx.message.channel,\n\t\t\tuserName: ctx.message.userName,\n\t\t\tchannelName: ctx.channelName,\n\t\t};\n\t}\n\n\tprivate async refreshSessionResources(): Promise<void> {\n\t\tawait this.ensureSessionReady();\n\t\tconst skills = loadPipiclawSkills(this.channelDir, this.workspacePath);\n\t\tthis.currentSkills = skills;\n\t\tawait this.session.reload();\n\t}\n\n\tprivate async initializeSession(): Promise<void> {\n\t\tconst skills = loadPipiclawSkills(this.channelDir, this.workspacePath);\n\t\tthis.currentSkills = skills;\n\t\tawait this.session.reload();\n\t}\n\n\tprivate async ensureSessionReady(): Promise<void> {\n\t\tawait this.sessionReady;\n\t}\n\n\t// === Session event subscription ===\n\n\tprivate subscribeToSessionEvents(): void {\n\t\tthis.session.subscribe(async (event: any) => {\n\t\t\tif (!this.runState.ctx || !this.runState.logCtx || !this.runState.queue) return;\n\n\t\t\tconst { ctx, logCtx, queue, pendingTools } = this.runState;\n\n\t\t\tif (event.type === \"tool_execution_start\") {\n\t\t\t\tconst agentEvent = event as any & { type: \"tool_execution_start\" };\n\t\t\t\tconst args = agentEvent.args as { label?: string };\n\t\t\t\tconst label = args.label || agentEvent.toolName;\n\n\t\t\t\tpendingTools.set(agentEvent.toolCallId, {\n\t\t\t\t\ttoolName: agentEvent.toolName,\n\t\t\t\t\targs: agentEvent.args,\n\t\t\t\t\tstartTime: Date.now(),\n\t\t\t\t});\n\n\t\t\t\tlog.logToolStart(logCtx, agentEvent.toolName, label, agentEvent.args as Record<string, unknown>);\n\t\t\t\tqueue.enqueue(() => ctx.respond(formatProgressEntry(\"tool\", label), false), \"tool label\");\n\t\t\t} else if (event.type === \"tool_execution_update\") {\n\t\t\t\tconst agentEvent = event as { type: \"tool_execution_update\"; toolName: string; partialResult: unknown };\n\t\t\t\tif (agentEvent.toolName !== \"subagent\") {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst partialText = truncate(extractToolResultText(agentEvent.partialResult), 200);\n\t\t\t\tif (!partialText.trim()) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tqueue.enqueue(() => ctx.respond(formatProgressEntry(\"tool\", partialText), false), \"tool update\");\n\t\t\t} else if (event.type === \"tool_execution_end\") {\n\t\t\t\tconst agentEvent = event as any & { type: \"tool_execution_end\" };\n\t\t\t\tconst resultStr = extractToolResultText(agentEvent.result);\n\t\t\t\tconst pending = pendingTools.get(agentEvent.toolCallId);\n\t\t\t\tpendingTools.delete(agentEvent.toolCallId);\n\n\t\t\t\tconst durationMs = pending ? Date.now() - pending.startTime : 0;\n\n\t\t\t\tif (agentEvent.isError) {\n\t\t\t\t\tlog.logToolError(logCtx, agentEvent.toolName, durationMs, resultStr);\n\t\t\t\t} else {\n\t\t\t\t\tlog.logToolSuccess(logCtx, agentEvent.toolName, durationMs, resultStr);\n\t\t\t\t}\n\n\t\t\t\tif (\n\t\t\t\t\tagentEvent.toolName === \"subagent\" &&\n\t\t\t\t\tagentEvent.result &&\n\t\t\t\t\ttypeof agentEvent.result === \"object\" &&\n\t\t\t\t\t\"details\" in agentEvent.result &&\n\t\t\t\t\tisSubAgentToolDetails((agentEvent.result as { details?: unknown }).details)\n\t\t\t\t) {\n\t\t\t\t\tmergeSubAgentUsage(\n\t\t\t\t\t\tthis.runState.totalUsage,\n\t\t\t\t\t\t(agentEvent.result as { details: SubAgentToolDetails }).details,\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tif (agentEvent.isError) {\n\t\t\t\t\tqueue.enqueue(\n\t\t\t\t\t\t() => ctx.respond(formatProgressEntry(\"error\", truncate(resultStr, 200)), false),\n\t\t\t\t\t\t\"tool error\",\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t} else if (event.type === \"message_start\") {\n\t\t\t\tconst agentEvent = event as any & { type: \"message_start\" };\n\t\t\t\tif (agentEvent.message.role === \"assistant\") {\n\t\t\t\t\tlog.logResponseStart(logCtx);\n\t\t\t\t}\n\t\t\t} else if (event.type === \"message_end\") {\n\t\t\t\tconst agentEvent = event as any & { type: \"message_end\" };\n\t\t\t\tconst commandResultText = extractCustomCommandResultText(agentEvent.message);\n\t\t\t\tif (commandResultText) {\n\t\t\t\t\tthis.runState.finalOutcome = { kind: \"final\", text: commandResultText };\n\t\t\t\t\tlog.logResponse(logCtx, commandResultText);\n\t\t\t\t\tqueue.enqueue(async () => {\n\t\t\t\t\t\tconst delivered = await ctx.respondPlain(commandResultText);\n\t\t\t\t\t\tif (!delivered) {\n\t\t\t\t\t\t\tawait ctx.replaceMessage(commandResultText);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthis.runState.finalResponseDelivered = true;\n\t\t\t\t\t}, \"command result\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (agentEvent.message.role === \"assistant\") {\n\t\t\t\t\tconst assistantMsg = agentEvent.message as any;\n\n\t\t\t\t\tif (assistantMsg.stopReason) {\n\t\t\t\t\t\tthis.runState.stopReason = assistantMsg.stopReason;\n\t\t\t\t\t}\n\t\t\t\t\tif (assistantMsg.errorMessage) {\n\t\t\t\t\t\tthis.runState.errorMessage = assistantMsg.errorMessage;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (assistantMsg.usage) {\n\t\t\t\t\t\tthis.runState.totalUsage.input += assistantMsg.usage.input;\n\t\t\t\t\t\tthis.runState.totalUsage.output += assistantMsg.usage.output;\n\t\t\t\t\t\tthis.runState.totalUsage.cacheRead += assistantMsg.usage.cacheRead;\n\t\t\t\t\t\tthis.runState.totalUsage.cacheWrite += assistantMsg.usage.cacheWrite;\n\t\t\t\t\t\tthis.runState.totalUsage.cost.input += assistantMsg.usage.cost.input;\n\t\t\t\t\t\tthis.runState.totalUsage.cost.output += assistantMsg.usage.cost.output;\n\t\t\t\t\t\tthis.runState.totalUsage.cost.cacheRead += assistantMsg.usage.cost.cacheRead;\n\t\t\t\t\t\tthis.runState.totalUsage.cost.cacheWrite += assistantMsg.usage.cost.cacheWrite;\n\t\t\t\t\t\tthis.runState.totalUsage.cost.total += assistantMsg.usage.cost.total;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst content = agentEvent.message.content;\n\t\t\t\t\tconst thinkingParts: string[] = [];\n\t\t\t\t\tconst textParts: string[] = [];\n\t\t\t\t\tlet hasToolCalls = false;\n\t\t\t\t\tfor (const part of content) {\n\t\t\t\t\t\tif (part.type === \"thinking\") {\n\t\t\t\t\t\t\tthinkingParts.push((part as any).thinking);\n\t\t\t\t\t\t} else if (part.type === \"text\") {\n\t\t\t\t\t\t\ttextParts.push((part as any).text);\n\t\t\t\t\t\t} else if (part.type === \"toolCall\") {\n\t\t\t\t\t\t\thasToolCalls = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tconst text = textParts.join(\"\\n\");\n\n\t\t\t\t\tfor (const thinking of thinkingParts) {\n\t\t\t\t\t\tlog.logThinking(logCtx, thinking);\n\t\t\t\t\t\tqueue.enqueue(() => ctx.respond(formatProgressEntry(\"thinking\", thinking), false), \"thinking\");\n\t\t\t\t\t}\n\n\t\t\t\t\tif (hasToolCalls && text.trim()) {\n\t\t\t\t\t\tqueue.enqueue(() => ctx.respond(formatProgressEntry(\"assistant\", text), false), \"assistant progress\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (event.type === \"turn_end\") {\n\t\t\t\tconst turnEvent = event as any & {\n\t\t\t\t\ttype: \"turn_end\";\n\t\t\t\t\tmessage: { role: string; stopReason?: string; content: Array<{ type: string; text?: string }> };\n\t\t\t\t\ttoolResults: unknown[];\n\t\t\t\t};\n\t\t\t\tif (turnEvent.message.role === \"assistant\" && turnEvent.toolResults.length === 0) {\n\t\t\t\t\tif (turnEvent.message.stopReason === \"error\" || turnEvent.message.stopReason === \"aborted\") {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst finalContent = turnEvent.message.content as Array<{ type: string; text?: string }>;\n\t\t\t\t\tconst finalText = finalContent\n\t\t\t\t\t\t.filter((part): part is { type: \"text\"; text: string } => part.type === \"text\" && !!part.text)\n\t\t\t\t\t\t.map((part) => part.text)\n\t\t\t\t\t\t.join(\"\\n\");\n\n\t\t\t\t\tconst trimmedFinalText = finalText.trim();\n\t\t\t\t\tif (!trimmedFinalText) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (trimmedFinalText === \"[SILENT]\" || trimmedFinalText.startsWith(\"[SILENT]\")) {\n\t\t\t\t\t\tthis.runState.finalOutcome = { kind: \"silent\" };\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (\n\t\t\t\t\t\tthis.runState.finalOutcome.kind === \"final\" &&\n\t\t\t\t\t\tthis.runState.finalOutcome.text.trim() === trimmedFinalText\n\t\t\t\t\t) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tthis.runState.finalOutcome = { kind: \"final\", text: finalText };\n\t\t\t\t\tlog.logResponse(logCtx, finalText);\n\t\t\t\t\tqueue.enqueue(async () => {\n\t\t\t\t\t\tconst delivered = await ctx.respondPlain(finalText);\n\t\t\t\t\t\tif (delivered) {\n\t\t\t\t\t\t\tthis.runState.finalResponseDelivered = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}, \"final response\");\n\t\t\t\t}\n\t\t\t} else if (event.type === \"auto_compaction_start\") {\n\t\t\t\tlog.logInfo(`Auto-compaction started (reason: ${(event as any).reason})`);\n\t\t\t\tqueue.enqueue(\n\t\t\t\t\t() => ctx.respond(formatProgressEntry(\"assistant\", \"Compacting context...\"), false),\n\t\t\t\t\t\"compaction start\",\n\t\t\t\t);\n\t\t\t} else if (event.type === \"auto_compaction_end\") {\n\t\t\t\tconst compEvent = event as any;\n\t\t\t\tif (compEvent.result) {\n\t\t\t\t\tlog.logInfo(`Auto-compaction complete: ${compEvent.result.tokensBefore} tokens compacted`);\n\t\t\t\t} else if (compEvent.aborted) {\n\t\t\t\t\tlog.logInfo(\"Auto-compaction aborted\");\n\t\t\t\t}\n\t\t\t} else if (event.type === \"auto_retry_start\") {\n\t\t\t\tconst retryEvent = event as any;\n\t\t\t\tlog.logWarning(`Retrying (${retryEvent.attempt}/${retryEvent.maxAttempts})`, retryEvent.errorMessage);\n\t\t\t\tqueue.enqueue(\n\t\t\t\t\t() =>\n\t\t\t\t\t\tctx.respond(\n\t\t\t\t\t\t\tformatProgressEntry(\"assistant\", `Retrying (${retryEvent.attempt}/${retryEvent.maxAttempts})...`),\n\t\t\t\t\t\t\tfalse,\n\t\t\t\t\t\t),\n\t\t\t\t\t\"retry\",\n\t\t\t\t);\n\t\t\t}\n\t\t});\n\t}\n}\n\n// ============================================================================\n// Factory\n// ============================================================================\n\nconst channelRunners = new Map<string, AgentRunner>();\n\nexport function getOrCreateRunner(sandboxConfig: SandboxConfig, channelId: string, channelDir: string): AgentRunner {\n\tconst existing = channelRunners.get(channelId);\n\tif (existing) return existing;\n\n\tconst runner = new ChannelRunner(sandboxConfig, channelId, channelDir);\n\tchannelRunners.set(channelId, runner);\n\treturn runner;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,KAAK,cAAc,EAAqB,MAAM,eAAe,CAAC;AAGvE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAMrD,OAAO,EAAkB,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAS/C,MAAM,WAAW,WAAW;IAC3B,GAAG,CAAC,GAAG,EAAE,eAAe,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvG,oBAAoB,CAAC,GAAG,EAAE,eAAe,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnF,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AA6zBD,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,WAAW,CAOlH","sourcesContent":["import { Agent } from \"@mariozechner/pi-agent-core\";\nimport type { Api, Model } from \"@mariozechner/pi-ai\";\nimport {\n\tAgentSession,\n\tAuthStorage,\n\tconvertToLlm,\n\tDefaultResourceLoader,\n\tModelRegistry,\n\tSessionManager,\n\ttype Skill,\n} from \"@mariozechner/pi-coding-agent\";\nimport { mkdir, writeFile } from \"fs/promises\";\nimport { dirname, join, resolve } from \"path\";\nimport { COMMAND_RESULT_CUSTOM_TYPE, createCommandExtension } from \"./command-extension.js\";\nimport { type BuiltInCommand, renderBuiltInHelp } from \"./commands.js\";\nimport { getAgentConfig, getApiKeyForModel, getSoul, loadPipiclawSkills } from \"./config-loader.js\";\nimport { PipiclawSettingsManager } from \"./context.js\";\nimport type { DingTalkContext } from \"./dingtalk.js\";\nimport * as log from \"./log.js\";\nimport { MemoryLifecycle } from \"./memory-lifecycle.js\";\nimport { resolveInitialModel } from \"./model-utils.js\";\nimport { APP_HOME_DIR, AUTH_CONFIG_PATH, MODELS_CONFIG_PATH } from \"./paths.js\";\nimport { buildAppendSystemPrompt } from \"./prompt-builder.js\";\nimport { createExecutor, type SandboxConfig } from \"./sandbox.js\";\nimport type { ChannelStore } from \"./store.js\";\nimport { discoverSubAgents, formatSubAgentList, type SubAgentDiscoveryResult } from \"./sub-agents.js\";\nimport { createPipiclawTools } from \"./tools/index.js\";\nimport type { SubAgentToolDetails } from \"./tools/subagent.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface AgentRunner {\n\trun(ctx: DingTalkContext, store: ChannelStore): Promise<{ stopReason: string; errorMessage?: string }>;\n\thandleBuiltinCommand(ctx: DingTalkContext, command: BuiltInCommand): Promise<void>;\n\tqueueSteer(text: string, userName?: string): Promise<void>;\n\tqueueFollowUp(text: string, userName?: string): Promise<void>;\n\tabort(): Promise<void>;\n}\n\ntype FinalOutcome = { kind: \"none\" } | { kind: \"silent\" } | { kind: \"final\"; text: string };\ntype ModelRegistryClass = {\n\tcreate?: (authStorage: AuthStorage, modelsJsonPath?: string) => ModelRegistry;\n\tnew (authStorage: AuthStorage, modelsJsonPath?: string): ModelRegistry;\n};\n\nfunction isSilentOutcome(outcome: FinalOutcome): outcome is { kind: \"silent\" } {\n\treturn outcome.kind === \"silent\";\n}\n\nfunction isFinalOutcome(outcome: FinalOutcome): outcome is { kind: \"final\"; text: string } {\n\treturn outcome.kind === \"final\";\n}\n\nfunction getFinalOutcomeText(outcome: FinalOutcome): string | null {\n\treturn isFinalOutcome(outcome) ? outcome.text : null;\n}\n\nfunction createModelRegistry(authStorage: AuthStorage, modelsJsonPath: string): ModelRegistry {\n\tconst registryClass = ModelRegistry as unknown as ModelRegistryClass;\n\treturn typeof registryClass.create === \"function\"\n\t\t? registryClass.create(authStorage, modelsJsonPath)\n\t\t: new registryClass(authStorage, modelsJsonPath);\n}\n\n// ============================================================================\n// Text helpers\n// ============================================================================\n\nfunction truncate(text: string, maxLen: number): string {\n\tif (text.length <= maxLen) return text;\n\treturn `${text.substring(0, maxLen - 3)}...`;\n}\n\nfunction sanitizeProgressText(text: string): string {\n\treturn text\n\t\t.replace(/\\uFFFC/g, \"\")\n\t\t.replace(/\\r/g, \"\")\n\t\t.trim();\n}\n\nfunction formatProgressEntry(kind: \"tool\" | \"thinking\" | \"error\" | \"assistant\", text: string): string {\n\tconst cleaned = sanitizeProgressText(text);\n\tif (!cleaned) return \"\";\n\n\tconst normalized = cleaned.replace(/\\n+/g, \" \").trim();\n\tswitch (kind) {\n\t\tcase \"tool\":\n\t\t\treturn `Running: ${normalized}`;\n\t\tcase \"thinking\":\n\t\t\treturn `Thinking: ${normalized}`;\n\t\tcase \"error\":\n\t\t\treturn `Error: ${normalized}`;\n\t\tcase \"assistant\":\n\t\t\treturn normalized;\n\t}\n}\n\nfunction extractToolResultText(result: unknown): string {\n\tif (typeof result === \"string\") {\n\t\treturn result;\n\t}\n\n\tif (\n\t\tresult &&\n\t\ttypeof result === \"object\" &&\n\t\t\"content\" in result &&\n\t\tArray.isArray((result as { content: unknown }).content)\n\t) {\n\t\tconst content = (result as { content: Array<{ type: string; text?: string }> }).content;\n\t\tconst textParts: string[] = [];\n\t\tfor (const part of content) {\n\t\t\tif (part.type === \"text\" && part.text) {\n\t\t\t\ttextParts.push(part.text);\n\t\t\t}\n\t\t}\n\t\tif (textParts.length > 0) {\n\t\t\treturn textParts.join(\"\\n\");\n\t\t}\n\t}\n\n\treturn JSON.stringify(result);\n}\n\nfunction isSubAgentToolDetails(value: unknown): value is SubAgentToolDetails {\n\tif (!value || typeof value !== \"object\" || !(\"kind\" in value) || (value as { kind?: unknown }).kind !== \"subagent\") {\n\t\treturn false;\n\t}\n\n\tif (!(\"usage\" in value)) {\n\t\treturn false;\n\t}\n\n\tconst usage = (value as { usage?: unknown }).usage;\n\treturn (\n\t\t!!usage &&\n\t\ttypeof usage === \"object\" &&\n\t\t\"input\" in usage &&\n\t\t\"output\" in usage &&\n\t\t\"cacheRead\" in usage &&\n\t\t\"cacheWrite\" in usage &&\n\t\t\"cost\" in usage\n\t);\n}\n\nfunction mergeSubAgentUsage(totalUsage: UsageTotals, details: SubAgentToolDetails): void {\n\ttotalUsage.input += details.usage.input;\n\ttotalUsage.output += details.usage.output;\n\ttotalUsage.cacheRead += details.usage.cacheRead;\n\ttotalUsage.cacheWrite += details.usage.cacheWrite;\n\ttotalUsage.cost.input += details.usage.cost.input;\n\ttotalUsage.cost.output += details.usage.cost.output;\n\ttotalUsage.cost.cacheRead += details.usage.cost.cacheRead;\n\ttotalUsage.cost.cacheWrite += details.usage.cost.cacheWrite;\n\ttotalUsage.cost.total += details.usage.cost.total;\n}\n\nfunction extractCustomCommandResultText(message: unknown): string | null {\n\tif (\n\t\t!message ||\n\t\ttypeof message !== \"object\" ||\n\t\t!(\"role\" in message) ||\n\t\t!(\"customType\" in message) ||\n\t\t(message as { role?: unknown }).role !== \"custom\" ||\n\t\t(message as { customType?: unknown }).customType !== COMMAND_RESULT_CUSTOM_TYPE\n\t) {\n\t\treturn null;\n\t}\n\n\tconst content = (message as { content?: unknown }).content;\n\treturn typeof content === \"string\" && content.trim() ? content : null;\n}\n\n// ============================================================================\n// Run State\n// ============================================================================\n\ninterface PendingTool {\n\ttoolName: string;\n\targs: unknown;\n\tstartTime: number;\n}\n\ninterface UsageTotals {\n\tinput: number;\n\toutput: number;\n\tcacheRead: number;\n\tcacheWrite: number;\n\tcost: { input: number; output: number; cacheRead: number; cacheWrite: number; total: number };\n}\n\ninterface RunQueue {\n\tenqueue(fn: () => Promise<void>, errorContext: string): void;\n\tenqueueMessage(text: string, target: \"main\" | \"thread\", errorContext: string, doLog?: boolean): void;\n}\n\ninterface RunState {\n\tctx: DingTalkContext | null;\n\tlogCtx: { channelId: string; userName?: string; channelName?: string } | null;\n\tstore: ChannelStore | null;\n\tqueue: RunQueue | null;\n\tpendingTools: Map<string, PendingTool>;\n\ttotalUsage: UsageTotals;\n\tstopReason: string;\n\terrorMessage: string | undefined;\n\tfinalOutcome: FinalOutcome;\n\tfinalResponseDelivered: boolean;\n}\n\nfunction createEmptyRunState(): RunState {\n\treturn {\n\t\tctx: null,\n\t\tlogCtx: null,\n\t\tstore: null,\n\t\tqueue: null,\n\t\tpendingTools: new Map(),\n\t\ttotalUsage: {\n\t\t\tinput: 0,\n\t\t\toutput: 0,\n\t\t\tcacheRead: 0,\n\t\t\tcacheWrite: 0,\n\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t},\n\t\tstopReason: \"stop\",\n\t\terrorMessage: undefined,\n\t\tfinalOutcome: { kind: \"none\" },\n\t\tfinalResponseDelivered: false,\n\t};\n}\n\n// ============================================================================\n// ChannelRunner\n// ============================================================================\n\nclass ChannelRunner implements AgentRunner {\n\t// --- Constructed once ---\n\tprivate readonly sandboxConfig: SandboxConfig;\n\tprivate readonly channelId: string;\n\tprivate readonly channelDir: string;\n\tprivate readonly workspacePath: string;\n\tprivate readonly workspaceDir: string;\n\tprivate readonly session: AgentSession;\n\tprivate readonly agent: Agent;\n\tprivate readonly sessionManager: SessionManager;\n\tprivate readonly settingsManager: PipiclawSettingsManager;\n\tprivate readonly modelRegistry: ModelRegistry;\n\tprivate readonly memoryLifecycle: MemoryLifecycle;\n\tprivate readonly sessionReady: Promise<void>;\n\tprivate subAgentDiscovery: SubAgentDiscoveryResult;\n\n\t// --- Mutable across runs ---\n\tprivate activeModel: Model<Api>;\n\tprivate currentSkills: Skill[];\n\n\t// --- Per run ---\n\tprivate runState: RunState = createEmptyRunState();\n\n\tconstructor(sandboxConfig: SandboxConfig, channelId: string, channelDir: string) {\n\t\tthis.sandboxConfig = sandboxConfig;\n\t\tthis.channelId = channelId;\n\t\tthis.channelDir = channelDir;\n\n\t\tconst executor = createExecutor(sandboxConfig);\n\t\tthis.workspaceDir = resolve(dirname(channelDir));\n\t\tthis.workspacePath = executor.getWorkspacePath(this.workspaceDir);\n\n\t\t// Initial skill summaries\n\t\tconst initialSkills = loadPipiclawSkills(channelDir, this.workspacePath);\n\t\tthis.currentSkills = initialSkills;\n\n\t\t// Create session manager\n\t\tconst contextFile = join(channelDir, \"context.jsonl\");\n\t\tthis.sessionManager = SessionManager.open(contextFile, channelDir);\n\t\tthis.settingsManager = new PipiclawSettingsManager(APP_HOME_DIR);\n\n\t\t// Create AuthStorage and ModelRegistry\n\t\tconst authStorage = AuthStorage.create(AUTH_CONFIG_PATH);\n\t\tthis.modelRegistry = createModelRegistry(authStorage, MODELS_CONFIG_PATH);\n\n\t\t// Resolve model: prefer saved global default, fall back to first available model\n\t\tthis.activeModel = resolveInitialModel(this.modelRegistry, this.settingsManager);\n\t\tlog.logInfo(`Using model: ${this.activeModel.provider}/${this.activeModel.id} (${this.activeModel.name})`);\n\t\tthis.subAgentDiscovery = this.refreshSubAgentDiscovery();\n\n\t\t// Create tools\n\t\tconst tools = createPipiclawTools({\n\t\t\texecutor,\n\t\t\tgetCurrentModel: () => this.activeModel,\n\t\t\tgetAvailableModels: () => this.modelRegistry.getAvailable(),\n\t\t\tresolveApiKey: async (model) => getApiKeyForModel(this.modelRegistry, model),\n\t\t\tworkspaceDir: this.workspaceDir,\n\t\t\tworkspacePath: this.workspacePath,\n\t\t\tchannelId: this.channelId,\n\t\t\tsandboxConfig: this.sandboxConfig,\n\t\t\tgetSubAgentDiscovery: () => this.subAgentDiscovery,\n\t\t});\n\n\t\t// Create agent\n\t\tthis.agent = new Agent({\n\t\t\tinitialState: {\n\t\t\t\tsystemPrompt: \"\",\n\t\t\t\tmodel: this.activeModel,\n\t\t\t\tthinkingLevel: \"off\",\n\t\t\t\ttools,\n\t\t\t},\n\t\t\tconvertToLlm,\n\t\t\tgetApiKey: async () => getApiKeyForModel(this.modelRegistry, this.activeModel),\n\t\t});\n\n\t\tthis.memoryLifecycle = new MemoryLifecycle({\n\t\t\tchannelId: this.channelId,\n\t\t\tchannelDir: this.channelDir,\n\t\t\tgetMessages: () => this.session.messages,\n\t\t\tgetSessionEntries: () => this.sessionManager.getBranch(),\n\t\t\tgetModel: () => this.session.model ?? this.activeModel,\n\t\t\tresolveApiKey: async (model) => getApiKeyForModel(this.modelRegistry, model),\n\t\t});\n\n\t\tconst resourceLoader = new DefaultResourceLoader({\n\t\t\tcwd: process.cwd(),\n\t\t\tagentDir: APP_HOME_DIR,\n\t\t\tsettingsManager: this.settingsManager as any,\n\t\t\textensionFactories: [\n\t\t\t\tthis.memoryLifecycle.createExtensionFactory(),\n\t\t\t\tcreateCommandExtension({\n\t\t\t\t\tgetCurrentModel: () => this.session.model ?? this.activeModel,\n\t\t\t\t\tgetAvailableModels: async () => {\n\t\t\t\t\t\tthis.modelRegistry.refresh();\n\t\t\t\t\t\treturn await this.modelRegistry.getAvailable();\n\t\t\t\t\t},\n\t\t\t\t\tgetSessionStats: () => this.session.getSessionStats(),\n\t\t\t\t\tgetThinkingLevel: () => this.session.thinkingLevel,\n\t\t\t\t\tswitchModel: async (model) => {\n\t\t\t\t\t\tawait this.session.setModel(model);\n\t\t\t\t\t\tthis.activeModel = model;\n\t\t\t\t\t},\n\t\t\t\t\trefreshSessionResources: async () => {\n\t\t\t\t\t\tawait this.refreshSessionResources();\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t],\n\t\t\tappendSystemPromptOverride: (base) => {\n\t\t\t\tconst soul = getSoul(this.workspaceDir);\n\t\t\t\tconst sections = [...base];\n\t\t\t\tif (soul) {\n\t\t\t\t\tsections.unshift(soul);\n\t\t\t\t}\n\t\t\t\tsections.push(\n\t\t\t\t\tbuildAppendSystemPrompt(this.workspacePath, this.channelId, this.sandboxConfig, {\n\t\t\t\t\t\tsubAgentList: formatSubAgentList(this.subAgentDiscovery.agents),\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t\treturn sections;\n\t\t\t},\n\t\t\tagentsFilesOverride: () => {\n\t\t\t\tconst agentConfig = getAgentConfig(this.channelDir);\n\t\t\t\treturn {\n\t\t\t\t\tagentsFiles: agentConfig ? [{ path: `${this.workspacePath}/AGENTS.md`, content: agentConfig }] : [],\n\t\t\t\t};\n\t\t\t},\n\t\t\tskillsOverride: (base) => ({\n\t\t\t\tskills: [...base.skills, ...this.currentSkills],\n\t\t\t\tdiagnostics: base.diagnostics,\n\t\t\t}),\n\t\t});\n\n\t\tconst baseToolsOverride = Object.fromEntries(tools.map((tool) => [tool.name, tool]));\n\n\t\t// Create AgentSession\n\t\tthis.session = new AgentSession({\n\t\t\tagent: this.agent,\n\t\t\tsessionManager: this.sessionManager,\n\t\t\tsettingsManager: this.settingsManager as any,\n\t\t\tcwd: process.cwd(),\n\t\t\tmodelRegistry: this.modelRegistry,\n\t\t\tresourceLoader,\n\t\t\tbaseToolsOverride,\n\t\t});\n\n\t\t// Subscribe to session events\n\t\tthis.subscribeToSessionEvents();\n\t\tthis.sessionReady = this.initializeSession();\n\t}\n\n\t// === Public API ===\n\n\tasync run(ctx: DingTalkContext, store: ChannelStore): Promise<{ stopReason: string; errorMessage?: string }> {\n\t\tthis.resetRunState(ctx, store);\n\n\t\t// Create queue for this run\n\t\tlet queueChain = Promise.resolve();\n\t\tthis.runState.queue = {\n\t\t\tenqueue: (fn: () => Promise<void>, errorContext: string): void => {\n\t\t\t\tqueueChain = queueChain.then(async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait fn();\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\tconst errMsg = err instanceof Error ? err.message : String(err);\n\t\t\t\t\t\tlog.logWarning(`DingTalk API error (${errorContext})`, errMsg);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t},\n\t\t\tenqueueMessage: function (text: string, target: \"main\" | \"thread\", errorContext: string, doLog = true): void {\n\t\t\t\tthis.enqueue(\n\t\t\t\t\t() => (target === \"main\" ? ctx.respond(text, doLog) : ctx.respondInThread(text)),\n\t\t\t\t\terrorContext,\n\t\t\t\t);\n\t\t\t},\n\t\t};\n\n\t\ttry {\n\t\t\tawait this.ensureSessionReady();\n\n\t\t\t// Ensure channel directory exists\n\t\t\tawait mkdir(this.channelDir, { recursive: true });\n\n\t\t\tconst userMessage = this.formatUserMessage(ctx.message.text, ctx.message.userName);\n\t\t\tconst promptText = this.shouldPreserveRawInput(ctx.message.text) ? ctx.message.text.trim() : userMessage;\n\n\t\t\t// Debug: write context to last_prompt.json (only with PIPICLAW_DEBUG=1)\n\t\t\tif (process.env.PIPICLAW_DEBUG) {\n\t\t\t\tconst debugContext = {\n\t\t\t\t\tsystemPrompt: this.agent.state.systemPrompt,\n\t\t\t\t\tmessages: this.session.messages,\n\t\t\t\t\tnewUserMessage: promptText,\n\t\t\t\t};\n\t\t\t\tawait writeFile(join(this.channelDir, \"last_prompt.json\"), JSON.stringify(debugContext, null, 2));\n\t\t\t}\n\n\t\t\tawait this.session.prompt(promptText);\n\t\t} catch (err) {\n\t\t\tthis.runState.stopReason = \"error\";\n\t\t\tthis.runState.errorMessage = err instanceof Error ? err.message : String(err);\n\t\t\tlog.logWarning(`[${this.channelId}] Runner failed`, this.runState.errorMessage);\n\t\t} finally {\n\t\t\tawait queueChain;\n\t\t\tconst finalOutcome = this.runState.finalOutcome;\n\t\t\tconst finalOutcomeText = getFinalOutcomeText(finalOutcome);\n\n\t\t\ttry {\n\t\t\t\tif (\n\t\t\t\t\tthis.runState.stopReason === \"error\" &&\n\t\t\t\t\tthis.runState.errorMessage &&\n\t\t\t\t\t!this.runState.finalResponseDelivered\n\t\t\t\t) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait ctx.replaceMessage(\"_Sorry, something went wrong_\");\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\tconst errMsg = err instanceof Error ? err.message : String(err);\n\t\t\t\t\t\tlog.logWarning(\"Failed to post error message\", errMsg);\n\t\t\t\t\t}\n\t\t\t\t} else if (isSilentOutcome(finalOutcome)) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait ctx.deleteMessage();\n\t\t\t\t\t\tlog.logInfo(\"Silent response - deleted message\");\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\tconst errMsg = err instanceof Error ? err.message : String(err);\n\t\t\t\t\t\tlog.logWarning(\"Failed to delete message for silent response\", errMsg);\n\t\t\t\t\t}\n\t\t\t\t} else if (finalOutcomeText && !this.runState.finalResponseDelivered) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait ctx.replaceMessage(finalOutcomeText);\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\tconst errMsg = err instanceof Error ? err.message : String(err);\n\t\t\t\t\t\tlog.logWarning(\"Failed to replace message with final text\", errMsg);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tawait ctx.flush();\n\t\t\t} finally {\n\t\t\t\tawait ctx.close();\n\t\t\t}\n\n\t\t\t// Log usage summary\n\t\t\tif (this.runState.totalUsage.cost.total > 0) {\n\t\t\t\tconst messages = this.session.messages;\n\t\t\t\tconst lastAssistantMessage = messages\n\t\t\t\t\t.slice()\n\t\t\t\t\t.reverse()\n\t\t\t\t\t.find((m: any) => m.role === \"assistant\" && m.stopReason !== \"aborted\") as any;\n\n\t\t\t\tconst contextTokens = lastAssistantMessage\n\t\t\t\t\t? lastAssistantMessage.usage.input +\n\t\t\t\t\t\tlastAssistantMessage.usage.output +\n\t\t\t\t\t\tlastAssistantMessage.usage.cacheRead +\n\t\t\t\t\t\tlastAssistantMessage.usage.cacheWrite\n\t\t\t\t\t: 0;\n\t\t\t\tconst currentRunModel = this.session.model ?? this.activeModel;\n\t\t\t\tconst contextWindow = currentRunModel.contextWindow || 200000;\n\n\t\t\t\tlog.logUsageSummary(this.runState.logCtx!, this.runState.totalUsage, contextTokens, contextWindow);\n\t\t\t}\n\n\t\t\t// Clear run state\n\t\t\tthis.runState.ctx = null;\n\t\t\tthis.runState.logCtx = null;\n\t\t\tthis.runState.queue = null;\n\t\t}\n\n\t\treturn { stopReason: this.runState.stopReason, errorMessage: this.runState.errorMessage };\n\t}\n\n\tasync handleBuiltinCommand(ctx: DingTalkContext, command: BuiltInCommand): Promise<void> {\n\t\ttry {\n\t\t\tswitch (command.name) {\n\t\t\t\tcase \"help\":\n\t\t\t\t\tawait this.sendCommandReply(ctx, renderBuiltInHelp());\n\t\t\t\t\treturn;\n\t\t\t\tcase \"stop\":\n\t\t\t\t\tawait this.sendCommandReply(ctx, \"No task is running. Use `/stop` only while a task is running.\");\n\t\t\t\t\treturn;\n\t\t\t\tcase \"steer\":\n\t\t\t\t\tthis.requireQueuedMessage(command.args, \"steer\");\n\t\t\t\t\tawait this.sendCommandReply(\n\t\t\t\t\t\tctx,\n\t\t\t\t\t\t\"No task is running. Send the message directly instead of using `/steer`.\",\n\t\t\t\t\t);\n\t\t\t\t\treturn;\n\t\t\t\tcase \"followup\":\n\t\t\t\t\tthis.requireQueuedMessage(command.args, \"followup\");\n\t\t\t\t\tawait this.sendCommandReply(\n\t\t\t\t\t\tctx,\n\t\t\t\t\t\t\"No task is running. Send the message directly now, or use `/followup` while a task is running.\",\n\t\t\t\t\t);\n\t\t\t\t\treturn;\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tconst errMsg = err instanceof Error ? err.message : String(err);\n\t\t\tlog.logWarning(`[${this.channelId}] Built-in command failed`, errMsg);\n\t\t\tawait this.sendCommandReply(ctx, `命令执行失败:${errMsg}`);\n\t\t}\n\t}\n\n\tasync queueSteer(text: string, userName?: string): Promise<void> {\n\t\tawait this.queueBusyMessage(\"steer\", this.requireQueuedMessage(text, \"steer\"), userName);\n\t}\n\n\tasync queueFollowUp(text: string, userName?: string): Promise<void> {\n\t\tawait this.queueBusyMessage(\"followUp\", this.requireQueuedMessage(text, \"followup\"), userName);\n\t}\n\n\tasync abort(): Promise<void> {\n\t\tawait this.session.abort();\n\t}\n\n\t// === Private helpers ===\n\n\tprivate async sendCommandReply(ctx: DingTalkContext, text: string): Promise<void> {\n\t\tconst delivered = await ctx.respondPlain(text);\n\t\tif (!delivered) {\n\t\t\tawait ctx.replaceMessage(text);\n\t\t\tawait ctx.flush();\n\t\t}\n\t}\n\n\tprivate requireQueuedMessage(text: string, commandName: \"steer\" | \"followup\"): string {\n\t\tconst trimmedText = text.trim();\n\t\tif (!trimmedText) {\n\t\t\tthrow new Error(`/${commandName} requires a message.`);\n\t\t}\n\t\treturn trimmedText;\n\t}\n\n\tprivate shouldPreserveRawInput(text: string): boolean {\n\t\treturn text.trim().startsWith(\"/\");\n\t}\n\n\tprivate formatUserMessage(text: string, userName?: string, now: Date = new Date()): string {\n\t\tconst pad = (n: number) => n.toString().padStart(2, \"0\");\n\t\tconst offset = -now.getTimezoneOffset();\n\t\tconst offsetSign = offset >= 0 ? \"+\" : \"-\";\n\t\tconst offsetHours = pad(Math.floor(Math.abs(offset) / 60));\n\t\tconst offsetMins = pad(Math.abs(offset) % 60);\n\t\tconst timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}${offsetSign}${offsetHours}:${offsetMins}`;\n\t\treturn `[${timestamp}] [${userName || \"unknown\"}]: ${text}`;\n\t}\n\n\tprivate async queueBusyMessage(delivery: \"steer\" | \"followUp\", text: string, userName?: string): Promise<void> {\n\t\tif (!this.session.isStreaming) {\n\t\t\tthrow new Error(\"No task is currently running.\");\n\t\t}\n\n\t\tawait this.session.prompt(this.formatUserMessage(text, userName), {\n\t\t\tstreamingBehavior: delivery,\n\t\t});\n\t}\n\n\tprivate resetRunState(ctx: DingTalkContext, store: ChannelStore): void {\n\t\tthis.runState = createEmptyRunState();\n\t\tthis.runState.ctx = ctx;\n\t\tthis.runState.logCtx = {\n\t\t\tchannelId: ctx.message.channel,\n\t\t\tuserName: ctx.message.userName,\n\t\t\tchannelName: ctx.channelName,\n\t\t};\n\t\tthis.runState.store = store;\n\t}\n\n\tprivate async refreshSessionResources(): Promise<void> {\n\t\tawait this.ensureSessionReady();\n\t\tconst skills = loadPipiclawSkills(this.channelDir, this.workspacePath);\n\t\tthis.currentSkills = skills;\n\t\tthis.subAgentDiscovery = this.refreshSubAgentDiscovery();\n\t\tawait this.session.reload();\n\t}\n\n\tprivate async initializeSession(): Promise<void> {\n\t\tconst skills = loadPipiclawSkills(this.channelDir, this.workspacePath);\n\t\tthis.currentSkills = skills;\n\t\tthis.subAgentDiscovery = this.refreshSubAgentDiscovery();\n\t\tawait this.session.reload();\n\t}\n\n\tprivate async ensureSessionReady(): Promise<void> {\n\t\tawait this.sessionReady;\n\t}\n\n\tprivate refreshSubAgentDiscovery(): SubAgentDiscoveryResult {\n\t\tthis.modelRegistry.refresh();\n\t\tconst discovery = discoverSubAgents(this.workspaceDir, this.modelRegistry.getAvailable());\n\t\tfor (const warning of discovery.warnings) {\n\t\t\tlog.logWarning(`Sub-agent config warning (${this.channelId})`, warning);\n\t\t}\n\t\treturn discovery;\n\t}\n\n\t// === Session event subscription ===\n\n\tprivate subscribeToSessionEvents(): void {\n\t\tthis.session.subscribe(async (event: any) => {\n\t\t\tif (!this.runState.ctx || !this.runState.logCtx || !this.runState.queue) return;\n\n\t\t\tconst { ctx, logCtx, queue, pendingTools, store } = this.runState;\n\n\t\t\tif (event.type === \"tool_execution_start\") {\n\t\t\t\tconst agentEvent = event as any & { type: \"tool_execution_start\" };\n\t\t\t\tconst args = agentEvent.args as { label?: string };\n\t\t\t\tconst label = args.label || agentEvent.toolName;\n\n\t\t\t\tpendingTools.set(agentEvent.toolCallId, {\n\t\t\t\t\ttoolName: agentEvent.toolName,\n\t\t\t\t\targs: agentEvent.args,\n\t\t\t\t\tstartTime: Date.now(),\n\t\t\t\t});\n\n\t\t\t\tlog.logToolStart(logCtx, agentEvent.toolName, label, agentEvent.args as Record<string, unknown>);\n\t\t\t\tqueue.enqueue(() => ctx.respond(formatProgressEntry(\"tool\", label), false), \"tool label\");\n\t\t\t} else if (event.type === \"tool_execution_update\") {\n\t\t\t\tconst agentEvent = event as { type: \"tool_execution_update\"; toolName: string; partialResult: unknown };\n\t\t\t\tif (agentEvent.toolName !== \"subagent\") {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst partialText = truncate(extractToolResultText(agentEvent.partialResult), 200);\n\t\t\t\tif (!partialText.trim()) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tqueue.enqueue(() => ctx.respond(formatProgressEntry(\"tool\", partialText), false), \"tool update\");\n\t\t\t} else if (event.type === \"tool_execution_end\") {\n\t\t\t\tconst agentEvent = event as any & { type: \"tool_execution_end\" };\n\t\t\t\tconst resultStr = extractToolResultText(agentEvent.result);\n\t\t\t\tconst pending = pendingTools.get(agentEvent.toolCallId);\n\t\t\t\tpendingTools.delete(agentEvent.toolCallId);\n\n\t\t\t\tconst durationMs = pending ? Date.now() - pending.startTime : 0;\n\t\t\t\tconst subAgentDetails =\n\t\t\t\t\tagentEvent.toolName === \"subagent\" &&\n\t\t\t\t\tagentEvent.result &&\n\t\t\t\t\ttypeof agentEvent.result === \"object\" &&\n\t\t\t\t\t\"details\" in agentEvent.result &&\n\t\t\t\t\tisSubAgentToolDetails((agentEvent.result as { details?: unknown }).details)\n\t\t\t\t\t\t? (agentEvent.result as { details: SubAgentToolDetails }).details\n\t\t\t\t\t\t: null;\n\n\t\t\t\tif (subAgentDetails) {\n\t\t\t\t\tmergeSubAgentUsage(this.runState.totalUsage, subAgentDetails);\n\t\t\t\t\tconst label =\n\t\t\t\t\t\tpending?.args &&\n\t\t\t\t\t\ttypeof pending.args === \"object\" &&\n\t\t\t\t\t\t\"label\" in pending.args &&\n\t\t\t\t\t\ttypeof (pending.args as { label?: unknown }).label === \"string\"\n\t\t\t\t\t\t\t? ((pending.args as { label: string }).label ?? \"subagent\").trim()\n\t\t\t\t\t\t\t: \"subagent\";\n\t\t\t\t\tqueue.enqueue(\n\t\t\t\t\t\t() =>\n\t\t\t\t\t\t\tstore?.logSubAgentRun(logCtx.channelId, {\n\t\t\t\t\t\t\t\tdate: new Date().toISOString(),\n\t\t\t\t\t\t\t\ttoolCallId: agentEvent.toolCallId,\n\t\t\t\t\t\t\t\tlabel,\n\t\t\t\t\t\t\t\tagent: subAgentDetails.agent,\n\t\t\t\t\t\t\t\tsource: subAgentDetails.source,\n\t\t\t\t\t\t\t\tmodel: subAgentDetails.model,\n\t\t\t\t\t\t\t\ttools: [...subAgentDetails.tools],\n\t\t\t\t\t\t\t\tturns: subAgentDetails.turns,\n\t\t\t\t\t\t\t\ttoolCalls: subAgentDetails.toolCalls,\n\t\t\t\t\t\t\t\tdurationMs: subAgentDetails.durationMs,\n\t\t\t\t\t\t\t\tfailed: subAgentDetails.failed,\n\t\t\t\t\t\t\t\tfailureReason: subAgentDetails.failureReason,\n\t\t\t\t\t\t\t\toutput: resultStr.length > 16000 ? resultStr.slice(0, 16000) : resultStr,\n\t\t\t\t\t\t\t\toutputTruncated: resultStr.length > 16000,\n\t\t\t\t\t\t\t\tusage: {\n\t\t\t\t\t\t\t\t\t...subAgentDetails.usage,\n\t\t\t\t\t\t\t\t\tcost: { ...subAgentDetails.usage.cost },\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}) ?? Promise.resolve(),\n\t\t\t\t\t\t\"sub-agent run log\",\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tconst treatAsError = agentEvent.isError || Boolean(subAgentDetails?.failed);\n\t\t\t\tif (treatAsError) {\n\t\t\t\t\tlog.logToolError(logCtx, agentEvent.toolName, durationMs, resultStr);\n\t\t\t\t} else {\n\t\t\t\t\tlog.logToolSuccess(logCtx, agentEvent.toolName, durationMs, resultStr);\n\t\t\t\t}\n\n\t\t\t\tif (treatAsError) {\n\t\t\t\t\tqueue.enqueue(\n\t\t\t\t\t\t() => ctx.respond(formatProgressEntry(\"error\", truncate(resultStr, 200)), false),\n\t\t\t\t\t\t\"tool error\",\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t} else if (event.type === \"message_start\") {\n\t\t\t\tconst agentEvent = event as any & { type: \"message_start\" };\n\t\t\t\tif (agentEvent.message.role === \"assistant\") {\n\t\t\t\t\tlog.logResponseStart(logCtx);\n\t\t\t\t}\n\t\t\t} else if (event.type === \"message_end\") {\n\t\t\t\tconst agentEvent = event as any & { type: \"message_end\" };\n\t\t\t\tconst commandResultText = extractCustomCommandResultText(agentEvent.message);\n\t\t\t\tif (commandResultText) {\n\t\t\t\t\tthis.runState.finalOutcome = { kind: \"final\", text: commandResultText };\n\t\t\t\t\tlog.logResponse(logCtx, commandResultText);\n\t\t\t\t\tqueue.enqueue(async () => {\n\t\t\t\t\t\tconst delivered = await ctx.respondPlain(commandResultText);\n\t\t\t\t\t\tif (!delivered) {\n\t\t\t\t\t\t\tawait ctx.replaceMessage(commandResultText);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthis.runState.finalResponseDelivered = true;\n\t\t\t\t\t}, \"command result\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (agentEvent.message.role === \"assistant\") {\n\t\t\t\t\tconst assistantMsg = agentEvent.message as any;\n\n\t\t\t\t\tif (assistantMsg.stopReason) {\n\t\t\t\t\t\tthis.runState.stopReason = assistantMsg.stopReason;\n\t\t\t\t\t}\n\t\t\t\t\tif (assistantMsg.errorMessage) {\n\t\t\t\t\t\tthis.runState.errorMessage = assistantMsg.errorMessage;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (assistantMsg.usage) {\n\t\t\t\t\t\tthis.runState.totalUsage.input += assistantMsg.usage.input;\n\t\t\t\t\t\tthis.runState.totalUsage.output += assistantMsg.usage.output;\n\t\t\t\t\t\tthis.runState.totalUsage.cacheRead += assistantMsg.usage.cacheRead;\n\t\t\t\t\t\tthis.runState.totalUsage.cacheWrite += assistantMsg.usage.cacheWrite;\n\t\t\t\t\t\tthis.runState.totalUsage.cost.input += assistantMsg.usage.cost.input;\n\t\t\t\t\t\tthis.runState.totalUsage.cost.output += assistantMsg.usage.cost.output;\n\t\t\t\t\t\tthis.runState.totalUsage.cost.cacheRead += assistantMsg.usage.cost.cacheRead;\n\t\t\t\t\t\tthis.runState.totalUsage.cost.cacheWrite += assistantMsg.usage.cost.cacheWrite;\n\t\t\t\t\t\tthis.runState.totalUsage.cost.total += assistantMsg.usage.cost.total;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst content = agentEvent.message.content;\n\t\t\t\t\tconst thinkingParts: string[] = [];\n\t\t\t\t\tconst textParts: string[] = [];\n\t\t\t\t\tlet hasToolCalls = false;\n\t\t\t\t\tfor (const part of content) {\n\t\t\t\t\t\tif (part.type === \"thinking\") {\n\t\t\t\t\t\t\tthinkingParts.push((part as any).thinking);\n\t\t\t\t\t\t} else if (part.type === \"text\") {\n\t\t\t\t\t\t\ttextParts.push((part as any).text);\n\t\t\t\t\t\t} else if (part.type === \"toolCall\") {\n\t\t\t\t\t\t\thasToolCalls = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tconst text = textParts.join(\"\\n\");\n\n\t\t\t\t\tfor (const thinking of thinkingParts) {\n\t\t\t\t\t\tlog.logThinking(logCtx, thinking);\n\t\t\t\t\t\tqueue.enqueue(() => ctx.respond(formatProgressEntry(\"thinking\", thinking), false), \"thinking\");\n\t\t\t\t\t}\n\n\t\t\t\t\tif (hasToolCalls && text.trim()) {\n\t\t\t\t\t\tqueue.enqueue(() => ctx.respond(formatProgressEntry(\"assistant\", text), false), \"assistant progress\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (event.type === \"turn_end\") {\n\t\t\t\tconst turnEvent = event as any & {\n\t\t\t\t\ttype: \"turn_end\";\n\t\t\t\t\tmessage: { role: string; stopReason?: string; content: Array<{ type: string; text?: string }> };\n\t\t\t\t\ttoolResults: unknown[];\n\t\t\t\t};\n\t\t\t\tif (turnEvent.message.role === \"assistant\" && turnEvent.toolResults.length === 0) {\n\t\t\t\t\tif (turnEvent.message.stopReason === \"error\" || turnEvent.message.stopReason === \"aborted\") {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst finalContent = turnEvent.message.content as Array<{ type: string; text?: string }>;\n\t\t\t\t\tconst finalText = finalContent\n\t\t\t\t\t\t.filter((part): part is { type: \"text\"; text: string } => part.type === \"text\" && !!part.text)\n\t\t\t\t\t\t.map((part) => part.text)\n\t\t\t\t\t\t.join(\"\\n\");\n\n\t\t\t\t\tconst trimmedFinalText = finalText.trim();\n\t\t\t\t\tif (!trimmedFinalText) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (trimmedFinalText === \"[SILENT]\" || trimmedFinalText.startsWith(\"[SILENT]\")) {\n\t\t\t\t\t\tthis.runState.finalOutcome = { kind: \"silent\" };\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (\n\t\t\t\t\t\tthis.runState.finalOutcome.kind === \"final\" &&\n\t\t\t\t\t\tthis.runState.finalOutcome.text.trim() === trimmedFinalText\n\t\t\t\t\t) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tthis.runState.finalOutcome = { kind: \"final\", text: finalText };\n\t\t\t\t\tlog.logResponse(logCtx, finalText);\n\t\t\t\t\tqueue.enqueue(async () => {\n\t\t\t\t\t\tconst delivered = await ctx.respondPlain(finalText);\n\t\t\t\t\t\tif (delivered) {\n\t\t\t\t\t\t\tthis.runState.finalResponseDelivered = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}, \"final response\");\n\t\t\t\t}\n\t\t\t} else if (event.type === \"auto_compaction_start\") {\n\t\t\t\tlog.logInfo(`Auto-compaction started (reason: ${(event as any).reason})`);\n\t\t\t\tqueue.enqueue(\n\t\t\t\t\t() => ctx.respond(formatProgressEntry(\"assistant\", \"Compacting context...\"), false),\n\t\t\t\t\t\"compaction start\",\n\t\t\t\t);\n\t\t\t} else if (event.type === \"auto_compaction_end\") {\n\t\t\t\tconst compEvent = event as any;\n\t\t\t\tif (compEvent.result) {\n\t\t\t\t\tlog.logInfo(`Auto-compaction complete: ${compEvent.result.tokensBefore} tokens compacted`);\n\t\t\t\t} else if (compEvent.aborted) {\n\t\t\t\t\tlog.logInfo(\"Auto-compaction aborted\");\n\t\t\t\t}\n\t\t\t} else if (event.type === \"auto_retry_start\") {\n\t\t\t\tconst retryEvent = event as any;\n\t\t\t\tlog.logWarning(`Retrying (${retryEvent.attempt}/${retryEvent.maxAttempts})`, retryEvent.errorMessage);\n\t\t\t\tqueue.enqueue(\n\t\t\t\t\t() =>\n\t\t\t\t\t\tctx.respond(\n\t\t\t\t\t\t\tformatProgressEntry(\"assistant\", `Retrying (${retryEvent.attempt}/${retryEvent.maxAttempts})...`),\n\t\t\t\t\t\t\tfalse,\n\t\t\t\t\t\t),\n\t\t\t\t\t\"retry\",\n\t\t\t\t);\n\t\t\t}\n\t\t});\n\t}\n}\n\n// ============================================================================\n// Factory\n// ============================================================================\n\nconst channelRunners = new Map<string, AgentRunner>();\n\nexport function getOrCreateRunner(sandboxConfig: SandboxConfig, channelId: string, channelDir: string): AgentRunner {\n\tconst existing = channelRunners.get(channelId);\n\tif (existing) return existing;\n\n\tconst runner = new ChannelRunner(sandboxConfig, channelId, channelDir);\n\tchannelRunners.set(channelId, runner);\n\treturn runner;\n}\n"]}
|
package/dist/agent.js
CHANGED
|
@@ -81,7 +81,10 @@ function extractToolResultText(result) {
|
|
|
81
81
|
return JSON.stringify(result);
|
|
82
82
|
}
|
|
83
83
|
function isSubAgentToolDetails(value) {
|
|
84
|
-
if (!value || typeof value !== "object" || !("
|
|
84
|
+
if (!value || typeof value !== "object" || !("kind" in value) || value.kind !== "subagent") {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
if (!("usage" in value)) {
|
|
85
88
|
return false;
|
|
86
89
|
}
|
|
87
90
|
const usage = value.usage;
|
|
@@ -120,6 +123,7 @@ function createEmptyRunState() {
|
|
|
120
123
|
return {
|
|
121
124
|
ctx: null,
|
|
122
125
|
logCtx: null,
|
|
126
|
+
store: null,
|
|
123
127
|
queue: null,
|
|
124
128
|
pendingTools: new Map(),
|
|
125
129
|
totalUsage: {
|
|
@@ -152,6 +156,7 @@ class ChannelRunner {
|
|
|
152
156
|
modelRegistry;
|
|
153
157
|
memoryLifecycle;
|
|
154
158
|
sessionReady;
|
|
159
|
+
subAgentDiscovery;
|
|
155
160
|
// --- Mutable across runs ---
|
|
156
161
|
activeModel;
|
|
157
162
|
currentSkills;
|
|
@@ -177,6 +182,7 @@ class ChannelRunner {
|
|
|
177
182
|
// Resolve model: prefer saved global default, fall back to first available model
|
|
178
183
|
this.activeModel = resolveInitialModel(this.modelRegistry, this.settingsManager);
|
|
179
184
|
log.logInfo(`Using model: ${this.activeModel.provider}/${this.activeModel.id} (${this.activeModel.name})`);
|
|
185
|
+
this.subAgentDiscovery = this.refreshSubAgentDiscovery();
|
|
180
186
|
// Create tools
|
|
181
187
|
const tools = createPipiclawTools({
|
|
182
188
|
executor,
|
|
@@ -184,6 +190,10 @@ class ChannelRunner {
|
|
|
184
190
|
getAvailableModels: () => this.modelRegistry.getAvailable(),
|
|
185
191
|
resolveApiKey: async (model) => getApiKeyForModel(this.modelRegistry, model),
|
|
186
192
|
workspaceDir: this.workspaceDir,
|
|
193
|
+
workspacePath: this.workspacePath,
|
|
194
|
+
channelId: this.channelId,
|
|
195
|
+
sandboxConfig: this.sandboxConfig,
|
|
196
|
+
getSubAgentDiscovery: () => this.subAgentDiscovery,
|
|
187
197
|
});
|
|
188
198
|
// Create agent
|
|
189
199
|
this.agent = new Agent({
|
|
@@ -233,9 +243,8 @@ class ChannelRunner {
|
|
|
233
243
|
if (soul) {
|
|
234
244
|
sections.unshift(soul);
|
|
235
245
|
}
|
|
236
|
-
const subAgents = discoverSubAgents(this.workspaceDir, this.modelRegistry.getAvailable());
|
|
237
246
|
sections.push(buildAppendSystemPrompt(this.workspacePath, this.channelId, this.sandboxConfig, {
|
|
238
|
-
subAgentList: formatSubAgentList(
|
|
247
|
+
subAgentList: formatSubAgentList(this.subAgentDiscovery.agents),
|
|
239
248
|
}));
|
|
240
249
|
return sections;
|
|
241
250
|
},
|
|
@@ -266,8 +275,8 @@ class ChannelRunner {
|
|
|
266
275
|
this.sessionReady = this.initializeSession();
|
|
267
276
|
}
|
|
268
277
|
// === Public API ===
|
|
269
|
-
async run(ctx,
|
|
270
|
-
this.resetRunState(ctx);
|
|
278
|
+
async run(ctx, store) {
|
|
279
|
+
this.resetRunState(ctx, store);
|
|
271
280
|
// Create queue for this run
|
|
272
281
|
let queueChain = Promise.resolve();
|
|
273
282
|
this.runState.queue = {
|
|
@@ -441,7 +450,7 @@ class ChannelRunner {
|
|
|
441
450
|
streamingBehavior: delivery,
|
|
442
451
|
});
|
|
443
452
|
}
|
|
444
|
-
resetRunState(ctx) {
|
|
453
|
+
resetRunState(ctx, store) {
|
|
445
454
|
this.runState = createEmptyRunState();
|
|
446
455
|
this.runState.ctx = ctx;
|
|
447
456
|
this.runState.logCtx = {
|
|
@@ -449,27 +458,38 @@ class ChannelRunner {
|
|
|
449
458
|
userName: ctx.message.userName,
|
|
450
459
|
channelName: ctx.channelName,
|
|
451
460
|
};
|
|
461
|
+
this.runState.store = store;
|
|
452
462
|
}
|
|
453
463
|
async refreshSessionResources() {
|
|
454
464
|
await this.ensureSessionReady();
|
|
455
465
|
const skills = loadPipiclawSkills(this.channelDir, this.workspacePath);
|
|
456
466
|
this.currentSkills = skills;
|
|
467
|
+
this.subAgentDiscovery = this.refreshSubAgentDiscovery();
|
|
457
468
|
await this.session.reload();
|
|
458
469
|
}
|
|
459
470
|
async initializeSession() {
|
|
460
471
|
const skills = loadPipiclawSkills(this.channelDir, this.workspacePath);
|
|
461
472
|
this.currentSkills = skills;
|
|
473
|
+
this.subAgentDiscovery = this.refreshSubAgentDiscovery();
|
|
462
474
|
await this.session.reload();
|
|
463
475
|
}
|
|
464
476
|
async ensureSessionReady() {
|
|
465
477
|
await this.sessionReady;
|
|
466
478
|
}
|
|
479
|
+
refreshSubAgentDiscovery() {
|
|
480
|
+
this.modelRegistry.refresh();
|
|
481
|
+
const discovery = discoverSubAgents(this.workspaceDir, this.modelRegistry.getAvailable());
|
|
482
|
+
for (const warning of discovery.warnings) {
|
|
483
|
+
log.logWarning(`Sub-agent config warning (${this.channelId})`, warning);
|
|
484
|
+
}
|
|
485
|
+
return discovery;
|
|
486
|
+
}
|
|
467
487
|
// === Session event subscription ===
|
|
468
488
|
subscribeToSessionEvents() {
|
|
469
489
|
this.session.subscribe(async (event) => {
|
|
470
490
|
if (!this.runState.ctx || !this.runState.logCtx || !this.runState.queue)
|
|
471
491
|
return;
|
|
472
|
-
const { ctx, logCtx, queue, pendingTools } = this.runState;
|
|
492
|
+
const { ctx, logCtx, queue, pendingTools, store } = this.runState;
|
|
473
493
|
if (event.type === "tool_execution_start") {
|
|
474
494
|
const agentEvent = event;
|
|
475
495
|
const args = agentEvent.args;
|
|
@@ -499,20 +519,50 @@ class ChannelRunner {
|
|
|
499
519
|
const pending = pendingTools.get(agentEvent.toolCallId);
|
|
500
520
|
pendingTools.delete(agentEvent.toolCallId);
|
|
501
521
|
const durationMs = pending ? Date.now() - pending.startTime : 0;
|
|
502
|
-
|
|
522
|
+
const subAgentDetails = agentEvent.toolName === "subagent" &&
|
|
523
|
+
agentEvent.result &&
|
|
524
|
+
typeof agentEvent.result === "object" &&
|
|
525
|
+
"details" in agentEvent.result &&
|
|
526
|
+
isSubAgentToolDetails(agentEvent.result.details)
|
|
527
|
+
? agentEvent.result.details
|
|
528
|
+
: null;
|
|
529
|
+
if (subAgentDetails) {
|
|
530
|
+
mergeSubAgentUsage(this.runState.totalUsage, subAgentDetails);
|
|
531
|
+
const label = pending?.args &&
|
|
532
|
+
typeof pending.args === "object" &&
|
|
533
|
+
"label" in pending.args &&
|
|
534
|
+
typeof pending.args.label === "string"
|
|
535
|
+
? (pending.args.label ?? "subagent").trim()
|
|
536
|
+
: "subagent";
|
|
537
|
+
queue.enqueue(() => store?.logSubAgentRun(logCtx.channelId, {
|
|
538
|
+
date: new Date().toISOString(),
|
|
539
|
+
toolCallId: agentEvent.toolCallId,
|
|
540
|
+
label,
|
|
541
|
+
agent: subAgentDetails.agent,
|
|
542
|
+
source: subAgentDetails.source,
|
|
543
|
+
model: subAgentDetails.model,
|
|
544
|
+
tools: [...subAgentDetails.tools],
|
|
545
|
+
turns: subAgentDetails.turns,
|
|
546
|
+
toolCalls: subAgentDetails.toolCalls,
|
|
547
|
+
durationMs: subAgentDetails.durationMs,
|
|
548
|
+
failed: subAgentDetails.failed,
|
|
549
|
+
failureReason: subAgentDetails.failureReason,
|
|
550
|
+
output: resultStr.length > 16000 ? resultStr.slice(0, 16000) : resultStr,
|
|
551
|
+
outputTruncated: resultStr.length > 16000,
|
|
552
|
+
usage: {
|
|
553
|
+
...subAgentDetails.usage,
|
|
554
|
+
cost: { ...subAgentDetails.usage.cost },
|
|
555
|
+
},
|
|
556
|
+
}) ?? Promise.resolve(), "sub-agent run log");
|
|
557
|
+
}
|
|
558
|
+
const treatAsError = agentEvent.isError || Boolean(subAgentDetails?.failed);
|
|
559
|
+
if (treatAsError) {
|
|
503
560
|
log.logToolError(logCtx, agentEvent.toolName, durationMs, resultStr);
|
|
504
561
|
}
|
|
505
562
|
else {
|
|
506
563
|
log.logToolSuccess(logCtx, agentEvent.toolName, durationMs, resultStr);
|
|
507
564
|
}
|
|
508
|
-
if (
|
|
509
|
-
agentEvent.result &&
|
|
510
|
-
typeof agentEvent.result === "object" &&
|
|
511
|
-
"details" in agentEvent.result &&
|
|
512
|
-
isSubAgentToolDetails(agentEvent.result.details)) {
|
|
513
|
-
mergeSubAgentUsage(this.runState.totalUsage, agentEvent.result.details);
|
|
514
|
-
}
|
|
515
|
-
if (agentEvent.isError) {
|
|
565
|
+
if (treatAsError) {
|
|
516
566
|
queue.enqueue(() => ctx.respond(formatProgressEntry("error", truncate(resultStr, 200)), false), "tool error");
|
|
517
567
|
}
|
|
518
568
|
}
|