@oyasmi/pipiclaw 0.3.5 → 0.5.0
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/CHANGELOG.md +3 -0
- package/LICENSE +184 -0
- package/README.md +267 -230
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +158 -76
- package/dist/agent.js.map +1 -1
- package/dist/command-extension.d.ts.map +1 -1
- package/dist/command-extension.js.map +1 -1
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js.map +1 -1
- package/dist/config-loader.d.ts.map +1 -1
- package/dist/config-loader.js.map +1 -1
- package/dist/context.d.ts +18 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +26 -2
- package/dist/context.js.map +1 -1
- package/dist/delivery.d.ts.map +1 -1
- package/dist/delivery.js +11 -14
- package/dist/delivery.js.map +1 -1
- package/dist/dingtalk.d.ts.map +1 -1
- package/dist/dingtalk.js +26 -26
- package/dist/dingtalk.js.map +1 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +5 -8
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/llm-json.d.ts +7 -0
- package/dist/llm-json.d.ts.map +1 -0
- package/dist/llm-json.js +77 -0
- package/dist/llm-json.js.map +1 -0
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js.map +1 -1
- package/dist/markdown-sections.d.ts +6 -0
- package/dist/markdown-sections.d.ts.map +1 -0
- package/dist/markdown-sections.js +34 -0
- package/dist/markdown-sections.js.map +1 -0
- package/dist/memory-candidates.d.ts +21 -0
- package/dist/memory-candidates.d.ts.map +1 -0
- package/dist/memory-candidates.js +126 -0
- package/dist/memory-candidates.js.map +1 -0
- package/dist/memory-consolidation.d.ts.map +1 -1
- package/dist/memory-consolidation.js +28 -49
- package/dist/memory-consolidation.js.map +1 -1
- package/dist/memory-files.d.ts +3 -0
- package/dist/memory-files.d.ts.map +1 -1
- package/dist/memory-files.js +51 -0
- package/dist/memory-files.js.map +1 -1
- package/dist/memory-lifecycle.d.ts +9 -0
- package/dist/memory-lifecycle.d.ts.map +1 -1
- package/dist/memory-lifecycle.js +67 -2
- package/dist/memory-lifecycle.js.map +1 -1
- package/dist/memory-recall.d.ts +29 -0
- package/dist/memory-recall.d.ts.map +1 -0
- package/dist/memory-recall.js +218 -0
- package/dist/memory-recall.js.map +1 -0
- package/dist/model-utils.d.ts.map +1 -1
- package/dist/model-utils.js.map +1 -1
- package/dist/paths.d.ts.map +1 -1
- package/dist/prompt-builder.d.ts.map +1 -1
- package/dist/prompt-builder.js +7 -2
- package/dist/prompt-builder.js.map +1 -1
- package/dist/sandbox.d.ts.map +1 -1
- package/dist/sandbox.js +0 -1
- package/dist/sandbox.js.map +1 -1
- package/dist/session-memory-files.d.ts +2 -0
- package/dist/session-memory-files.d.ts.map +1 -0
- package/dist/session-memory-files.js +2 -0
- package/dist/session-memory-files.js.map +1 -0
- package/dist/session-memory.d.ts +22 -0
- package/dist/session-memory.d.ts.map +1 -0
- package/dist/session-memory.js +274 -0
- package/dist/session-memory.js.map +1 -0
- package/dist/shell-escape.d.ts.map +1 -1
- package/dist/shell-escape.js.map +1 -1
- package/dist/sidecar-worker.d.ts +27 -0
- package/dist/sidecar-worker.d.ts.map +1 -0
- package/dist/sidecar-worker.js +105 -0
- package/dist/sidecar-worker.js.map +1 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +2 -3
- package/dist/store.js.map +1 -1
- package/dist/sub-agents.d.ts +10 -0
- package/dist/sub-agents.d.ts.map +1 -1
- package/dist/sub-agents.js +132 -10
- package/dist/sub-agents.js.map +1 -1
- package/dist/tools/attach.d.ts.map +1 -1
- package/dist/tools/attach.js.map +1 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +2 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/subagent.d.ts +6 -0
- package/dist/tools/subagent.d.ts.map +1 -1
- package/dist/tools/subagent.js +127 -12
- package/dist/tools/subagent.js.map +1 -1
- package/dist/tools/truncate.d.ts.map +1 -1
- package/dist/tools/truncate.js.map +1 -1
- package/dist/tools/write-content.d.ts.map +1 -1
- package/dist/tools/write-content.js.map +1 -1
- package/dist/tools/write.d.ts.map +1 -1
- package/dist/tools/write.js.map +1 -1
- package/docs/improve-memory/design.md +537 -0
- package/docs/improve-memory/interfaces-and-tests.md +473 -0
- package/docs/improve-memory/spec.md +357 -0
- package/docs/memory-rfc.md +297 -0
- package/docs/proj-review.md +188 -0
- package/docs/subagent/pi-subagent-analyse.txt +190 -0
- package/docs/subagent/pi-subagent-design.txt +266 -0
- package/docs/subagent/pi-subagent-phase1-plan.txt +529 -0
- package/docs/test-supplementation-plan.md +553 -0
- package/package.json +71 -53
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;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"]}
|
|
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;AAQrD,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;AAw+BD,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,WAAW,CAOlH"}
|
package/dist/agent.js
CHANGED
|
@@ -7,7 +7,9 @@ import { renderBuiltInHelp } from "./commands.js";
|
|
|
7
7
|
import { getAgentConfig, getApiKeyForModel, getSoul, loadPipiclawSkills } from "./config-loader.js";
|
|
8
8
|
import { PipiclawSettingsManager } from "./context.js";
|
|
9
9
|
import * as log from "./log.js";
|
|
10
|
+
import { createMemoryCandidateCache } from "./memory-candidates.js";
|
|
10
11
|
import { MemoryLifecycle } from "./memory-lifecycle.js";
|
|
12
|
+
import { recallRelevantMemory } from "./memory-recall.js";
|
|
11
13
|
import { resolveInitialModel } from "./model-utils.js";
|
|
12
14
|
import { APP_HOME_DIR, AUTH_CONFIG_PATH, MODELS_CONFIG_PATH } from "./paths.js";
|
|
13
15
|
import { buildAppendSystemPrompt } from "./prompt-builder.js";
|
|
@@ -37,12 +39,23 @@ function truncate(text, maxLen) {
|
|
|
37
39
|
return text;
|
|
38
40
|
return `${text.substring(0, maxLen - 3)}...`;
|
|
39
41
|
}
|
|
42
|
+
const MAX_USER_MESSAGE_CHARS = 12_000;
|
|
43
|
+
const HAN_REGEX = /\p{Script=Han}/u;
|
|
40
44
|
function sanitizeProgressText(text) {
|
|
41
45
|
return text
|
|
42
46
|
.replace(/\uFFFC/g, "")
|
|
43
47
|
.replace(/\r/g, "")
|
|
44
48
|
.trim();
|
|
45
49
|
}
|
|
50
|
+
function clipUserInput(text, maxChars) {
|
|
51
|
+
const normalized = text.replace(/\r/g, "").trim();
|
|
52
|
+
if (normalized.length <= maxChars) {
|
|
53
|
+
return normalized;
|
|
54
|
+
}
|
|
55
|
+
const headChars = Math.floor(maxChars * 0.6);
|
|
56
|
+
const tailChars = maxChars - headChars;
|
|
57
|
+
return `${normalized.slice(0, headChars)}\n\n[... omitted ${normalized.length - maxChars} chars ...]\n\n${normalized.slice(-tailChars)}`;
|
|
58
|
+
}
|
|
46
59
|
function formatProgressEntry(kind, text) {
|
|
47
60
|
const cleaned = sanitizeProgressText(text);
|
|
48
61
|
if (!cleaned)
|
|
@@ -139,30 +152,74 @@ function createEmptyRunState() {
|
|
|
139
152
|
finalResponseDelivered: false,
|
|
140
153
|
};
|
|
141
154
|
}
|
|
155
|
+
function isRecord(value) {
|
|
156
|
+
return typeof value === "object" && value !== null;
|
|
157
|
+
}
|
|
158
|
+
function isMessageWithRole(value) {
|
|
159
|
+
return isRecord(value) && typeof value.role === "string";
|
|
160
|
+
}
|
|
161
|
+
function isAssistantEventMessage(value) {
|
|
162
|
+
return (isMessageWithRole(value) && value.role === "assistant" && Array.isArray(value.content));
|
|
163
|
+
}
|
|
164
|
+
function isThinkingPart(part) {
|
|
165
|
+
return part.type === "thinking" && typeof part.thinking === "string";
|
|
166
|
+
}
|
|
167
|
+
function isTextPart(part) {
|
|
168
|
+
return part.type === "text" && typeof part.text === "string";
|
|
169
|
+
}
|
|
170
|
+
function extractLabelFromArgs(args) {
|
|
171
|
+
if (!isRecord(args)) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
return typeof args.label === "string" && args.label.trim() ? args.label.trim() : null;
|
|
175
|
+
}
|
|
176
|
+
function hasEventType(value, type) {
|
|
177
|
+
return isRecord(value) && value.type === type;
|
|
178
|
+
}
|
|
179
|
+
function isToolExecutionStartEvent(value) {
|
|
180
|
+
return (hasEventType(value, "tool_execution_start") &&
|
|
181
|
+
typeof value.toolCallId === "string" &&
|
|
182
|
+
typeof value.toolName === "string");
|
|
183
|
+
}
|
|
184
|
+
function isToolExecutionUpdateEvent(value) {
|
|
185
|
+
return (hasEventType(value, "tool_execution_update") &&
|
|
186
|
+
typeof value.toolCallId === "string" &&
|
|
187
|
+
typeof value.toolName === "string");
|
|
188
|
+
}
|
|
189
|
+
function isToolExecutionEndEvent(value) {
|
|
190
|
+
return (hasEventType(value, "tool_execution_end") &&
|
|
191
|
+
typeof value.toolCallId === "string" &&
|
|
192
|
+
typeof value.toolName === "string" &&
|
|
193
|
+
typeof value.isError === "boolean");
|
|
194
|
+
}
|
|
195
|
+
function isMessageStartEvent(value) {
|
|
196
|
+
return hasEventType(value, "message_start") && "message" in value;
|
|
197
|
+
}
|
|
198
|
+
function isMessageEndEvent(value) {
|
|
199
|
+
return hasEventType(value, "message_end") && "message" in value;
|
|
200
|
+
}
|
|
201
|
+
function isTurnEndEvent(value) {
|
|
202
|
+
return hasEventType(value, "turn_end") && "message" in value && Array.isArray(value.toolResults);
|
|
203
|
+
}
|
|
204
|
+
function isAutoCompactionStartEvent(value) {
|
|
205
|
+
return hasEventType(value, "auto_compaction_start") && (value.reason === "threshold" || value.reason === "overflow");
|
|
206
|
+
}
|
|
207
|
+
function isAutoCompactionEndEvent(value) {
|
|
208
|
+
return hasEventType(value, "auto_compaction_end");
|
|
209
|
+
}
|
|
210
|
+
function isAutoRetryStartEvent(value) {
|
|
211
|
+
return (hasEventType(value, "auto_retry_start") &&
|
|
212
|
+
typeof value.attempt === "number" &&
|
|
213
|
+
typeof value.maxAttempts === "number" &&
|
|
214
|
+
typeof value.errorMessage === "string");
|
|
215
|
+
}
|
|
142
216
|
// ============================================================================
|
|
143
217
|
// ChannelRunner
|
|
144
218
|
// ============================================================================
|
|
145
219
|
class ChannelRunner {
|
|
146
|
-
// --- Constructed once ---
|
|
147
|
-
sandboxConfig;
|
|
148
|
-
channelId;
|
|
149
|
-
channelDir;
|
|
150
|
-
workspacePath;
|
|
151
|
-
workspaceDir;
|
|
152
|
-
session;
|
|
153
|
-
agent;
|
|
154
|
-
sessionManager;
|
|
155
|
-
settingsManager;
|
|
156
|
-
modelRegistry;
|
|
157
|
-
memoryLifecycle;
|
|
158
|
-
sessionReady;
|
|
159
|
-
subAgentDiscovery;
|
|
160
|
-
// --- Mutable across runs ---
|
|
161
|
-
activeModel;
|
|
162
|
-
currentSkills;
|
|
163
|
-
// --- Per run ---
|
|
164
|
-
runState = createEmptyRunState();
|
|
165
220
|
constructor(sandboxConfig, channelId, channelDir) {
|
|
221
|
+
// --- Per run ---
|
|
222
|
+
this.runState = createEmptyRunState();
|
|
166
223
|
this.sandboxConfig = sandboxConfig;
|
|
167
224
|
this.channelId = channelId;
|
|
168
225
|
this.channelDir = channelDir;
|
|
@@ -190,10 +247,12 @@ class ChannelRunner {
|
|
|
190
247
|
getAvailableModels: () => this.modelRegistry.getAvailable(),
|
|
191
248
|
resolveApiKey: async (model) => getApiKeyForModel(this.modelRegistry, model),
|
|
192
249
|
workspaceDir: this.workspaceDir,
|
|
250
|
+
channelDir: this.channelDir,
|
|
193
251
|
workspacePath: this.workspacePath,
|
|
194
252
|
channelId: this.channelId,
|
|
195
253
|
sandboxConfig: this.sandboxConfig,
|
|
196
254
|
getSubAgentDiscovery: () => this.subAgentDiscovery,
|
|
255
|
+
getMemoryRecallSettings: () => this.settingsManager.getMemoryRecallSettings(),
|
|
197
256
|
});
|
|
198
257
|
// Create agent
|
|
199
258
|
this.agent = new Agent({
|
|
@@ -213,6 +272,7 @@ class ChannelRunner {
|
|
|
213
272
|
getSessionEntries: () => this.sessionManager.getBranch(),
|
|
214
273
|
getModel: () => this.session.model ?? this.activeModel,
|
|
215
274
|
resolveApiKey: async (model) => getApiKeyForModel(this.modelRegistry, model),
|
|
275
|
+
getSessionMemorySettings: () => this.settingsManager.getSessionMemorySettings(),
|
|
216
276
|
});
|
|
217
277
|
const resourceLoader = new DefaultResourceLoader({
|
|
218
278
|
cwd: process.cwd(),
|
|
@@ -299,13 +359,39 @@ class ChannelRunner {
|
|
|
299
359
|
await this.ensureSessionReady();
|
|
300
360
|
// Ensure channel directory exists
|
|
301
361
|
await mkdir(this.channelDir, { recursive: true });
|
|
302
|
-
const
|
|
303
|
-
const
|
|
362
|
+
const candidateCache = createMemoryCandidateCache();
|
|
363
|
+
const clippedInput = clipUserInput(ctx.message.text, MAX_USER_MESSAGE_CHARS);
|
|
364
|
+
const userMessage = this.formatUserMessage(clippedInput, ctx.message.userName);
|
|
365
|
+
let promptText = this.shouldPreserveRawInput(ctx.message.text) ? clippedInput : userMessage;
|
|
366
|
+
let recalledContextText = "";
|
|
367
|
+
if (!this.shouldPreserveRawInput(ctx.message.text)) {
|
|
368
|
+
const recallSettings = this.settingsManager.getMemoryRecallSettings();
|
|
369
|
+
if (recallSettings.enabled) {
|
|
370
|
+
const recall = await recallRelevantMemory({
|
|
371
|
+
query: clippedInput,
|
|
372
|
+
workspaceDir: this.workspaceDir,
|
|
373
|
+
channelDir: this.channelDir,
|
|
374
|
+
maxCandidates: recallSettings.maxCandidates,
|
|
375
|
+
maxInjected: recallSettings.maxInjected,
|
|
376
|
+
maxChars: recallSettings.maxChars,
|
|
377
|
+
rerankWithModel: recallSettings.rerankWithModel,
|
|
378
|
+
autoRerank: HAN_REGEX.test(clippedInput),
|
|
379
|
+
model: this.session.model ?? this.activeModel,
|
|
380
|
+
resolveApiKey: async (model) => getApiKeyForModel(this.modelRegistry, model),
|
|
381
|
+
candidateCache,
|
|
382
|
+
});
|
|
383
|
+
if (recall.renderedText) {
|
|
384
|
+
recalledContextText = recall.renderedText;
|
|
385
|
+
promptText = `${recall.renderedText}\n\n<user_message>\n${promptText}\n</user_message>`;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
304
389
|
// Debug: write context to last_prompt.json (only with PIPICLAW_DEBUG=1)
|
|
305
390
|
if (process.env.PIPICLAW_DEBUG) {
|
|
306
391
|
const debugContext = {
|
|
307
392
|
systemPrompt: this.agent.state.systemPrompt,
|
|
308
393
|
messages: this.session.messages,
|
|
394
|
+
recalledContext: recalledContextText || undefined,
|
|
309
395
|
newUserMessage: promptText,
|
|
310
396
|
};
|
|
311
397
|
await writeFile(join(this.channelDir, "last_prompt.json"), JSON.stringify(debugContext, null, 2));
|
|
@@ -446,7 +532,11 @@ class ChannelRunner {
|
|
|
446
532
|
if (!this.session.isStreaming) {
|
|
447
533
|
throw new Error("No task is currently running.");
|
|
448
534
|
}
|
|
449
|
-
|
|
535
|
+
const clippedText = clipUserInput(text, MAX_USER_MESSAGE_CHARS);
|
|
536
|
+
if (clippedText !== text.trim()) {
|
|
537
|
+
log.logWarning(`[${this.channelId}] Queued message exceeded ${MAX_USER_MESSAGE_CHARS} chars and was clipped`);
|
|
538
|
+
}
|
|
539
|
+
await this.session.prompt(this.formatUserMessage(clippedText, userName), {
|
|
450
540
|
streamingBehavior: delivery,
|
|
451
541
|
});
|
|
452
542
|
}
|
|
@@ -490,41 +580,37 @@ class ChannelRunner {
|
|
|
490
580
|
if (!this.runState.ctx || !this.runState.logCtx || !this.runState.queue)
|
|
491
581
|
return;
|
|
492
582
|
const { ctx, logCtx, queue, pendingTools, store } = this.runState;
|
|
493
|
-
if (event
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
toolName: agentEvent.toolName,
|
|
499
|
-
args: agentEvent.args,
|
|
583
|
+
if (isToolExecutionStartEvent(event)) {
|
|
584
|
+
const label = extractLabelFromArgs(event.args) || event.toolName;
|
|
585
|
+
pendingTools.set(event.toolCallId, {
|
|
586
|
+
toolName: event.toolName,
|
|
587
|
+
args: event.args,
|
|
500
588
|
startTime: Date.now(),
|
|
501
589
|
});
|
|
502
|
-
|
|
590
|
+
this.memoryLifecycle.noteToolCall();
|
|
591
|
+
log.logToolStart(logCtx, event.toolName, label, isRecord(event.args) ? event.args : {});
|
|
503
592
|
queue.enqueue(() => ctx.respond(formatProgressEntry("tool", label), false), "tool label");
|
|
504
593
|
}
|
|
505
|
-
else if (event
|
|
506
|
-
|
|
507
|
-
if (agentEvent.toolName !== "subagent") {
|
|
594
|
+
else if (isToolExecutionUpdateEvent(event)) {
|
|
595
|
+
if (event.toolName !== "subagent") {
|
|
508
596
|
return;
|
|
509
597
|
}
|
|
510
|
-
const partialText = truncate(extractToolResultText(
|
|
598
|
+
const partialText = truncate(extractToolResultText(event.partialResult), 200);
|
|
511
599
|
if (!partialText.trim()) {
|
|
512
600
|
return;
|
|
513
601
|
}
|
|
514
602
|
queue.enqueue(() => ctx.respond(formatProgressEntry("tool", partialText), false), "tool update");
|
|
515
603
|
}
|
|
516
|
-
else if (event
|
|
517
|
-
const
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
pendingTools.delete(agentEvent.toolCallId);
|
|
604
|
+
else if (isToolExecutionEndEvent(event)) {
|
|
605
|
+
const resultStr = extractToolResultText(event.result);
|
|
606
|
+
const pending = pendingTools.get(event.toolCallId);
|
|
607
|
+
pendingTools.delete(event.toolCallId);
|
|
521
608
|
const durationMs = pending ? Date.now() - pending.startTime : 0;
|
|
522
|
-
const subAgentDetails =
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
? agentEvent.result.details
|
|
609
|
+
const subAgentDetails = event.toolName === "subagent" &&
|
|
610
|
+
isRecord(event.result) &&
|
|
611
|
+
"details" in event.result &&
|
|
612
|
+
isSubAgentToolDetails(event.result.details)
|
|
613
|
+
? event.result.details
|
|
528
614
|
: null;
|
|
529
615
|
if (subAgentDetails) {
|
|
530
616
|
mergeSubAgentUsage(this.runState.totalUsage, subAgentDetails);
|
|
@@ -536,7 +622,7 @@ class ChannelRunner {
|
|
|
536
622
|
: "subagent";
|
|
537
623
|
queue.enqueue(() => store?.logSubAgentRun(logCtx.channelId, {
|
|
538
624
|
date: new Date().toISOString(),
|
|
539
|
-
toolCallId:
|
|
625
|
+
toolCallId: event.toolCallId,
|
|
540
626
|
label,
|
|
541
627
|
agent: subAgentDetails.agent,
|
|
542
628
|
source: subAgentDetails.source,
|
|
@@ -555,26 +641,24 @@ class ChannelRunner {
|
|
|
555
641
|
},
|
|
556
642
|
}) ?? Promise.resolve(), "sub-agent run log");
|
|
557
643
|
}
|
|
558
|
-
const treatAsError =
|
|
644
|
+
const treatAsError = event.isError || Boolean(subAgentDetails?.failed);
|
|
559
645
|
if (treatAsError) {
|
|
560
|
-
log.logToolError(logCtx,
|
|
646
|
+
log.logToolError(logCtx, event.toolName, durationMs, resultStr);
|
|
561
647
|
}
|
|
562
648
|
else {
|
|
563
|
-
log.logToolSuccess(logCtx,
|
|
649
|
+
log.logToolSuccess(logCtx, event.toolName, durationMs, resultStr);
|
|
564
650
|
}
|
|
565
651
|
if (treatAsError) {
|
|
566
652
|
queue.enqueue(() => ctx.respond(formatProgressEntry("error", truncate(resultStr, 200)), false), "tool error");
|
|
567
653
|
}
|
|
568
654
|
}
|
|
569
|
-
else if (event
|
|
570
|
-
|
|
571
|
-
if (agentEvent.message.role === "assistant") {
|
|
655
|
+
else if (isMessageStartEvent(event)) {
|
|
656
|
+
if (isAssistantEventMessage(event.message)) {
|
|
572
657
|
log.logResponseStart(logCtx);
|
|
573
658
|
}
|
|
574
659
|
}
|
|
575
|
-
else if (event
|
|
576
|
-
const
|
|
577
|
-
const commandResultText = extractCustomCommandResultText(agentEvent.message);
|
|
660
|
+
else if (isMessageEndEvent(event)) {
|
|
661
|
+
const commandResultText = extractCustomCommandResultText(event.message);
|
|
578
662
|
if (commandResultText) {
|
|
579
663
|
this.runState.finalOutcome = { kind: "final", text: commandResultText };
|
|
580
664
|
log.logResponse(logCtx, commandResultText);
|
|
@@ -587,8 +671,8 @@ class ChannelRunner {
|
|
|
587
671
|
}, "command result");
|
|
588
672
|
return;
|
|
589
673
|
}
|
|
590
|
-
if (
|
|
591
|
-
const assistantMsg =
|
|
674
|
+
if (isAssistantEventMessage(event.message)) {
|
|
675
|
+
const assistantMsg = event.message;
|
|
592
676
|
if (assistantMsg.stopReason) {
|
|
593
677
|
this.runState.stopReason = assistantMsg.stopReason;
|
|
594
678
|
}
|
|
@@ -606,15 +690,15 @@ class ChannelRunner {
|
|
|
606
690
|
this.runState.totalUsage.cost.cacheWrite += assistantMsg.usage.cost.cacheWrite;
|
|
607
691
|
this.runState.totalUsage.cost.total += assistantMsg.usage.cost.total;
|
|
608
692
|
}
|
|
609
|
-
const content =
|
|
693
|
+
const content = assistantMsg.content;
|
|
610
694
|
const thinkingParts = [];
|
|
611
695
|
const textParts = [];
|
|
612
696
|
let hasToolCalls = false;
|
|
613
697
|
for (const part of content) {
|
|
614
|
-
if (part
|
|
698
|
+
if (isThinkingPart(part)) {
|
|
615
699
|
thinkingParts.push(part.thinking);
|
|
616
700
|
}
|
|
617
|
-
else if (part
|
|
701
|
+
else if (isTextPart(part)) {
|
|
618
702
|
textParts.push(part.text);
|
|
619
703
|
}
|
|
620
704
|
else if (part.type === "toolCall") {
|
|
@@ -631,14 +715,12 @@ class ChannelRunner {
|
|
|
631
715
|
}
|
|
632
716
|
}
|
|
633
717
|
}
|
|
634
|
-
else if (event
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
if (turnEvent.message.stopReason === "error" || turnEvent.message.stopReason === "aborted") {
|
|
718
|
+
else if (isTurnEndEvent(event)) {
|
|
719
|
+
if (isAssistantEventMessage(event.message) && event.toolResults.length === 0) {
|
|
720
|
+
if (event.message.stopReason === "error" || event.message.stopReason === "aborted") {
|
|
638
721
|
return;
|
|
639
722
|
}
|
|
640
|
-
const
|
|
641
|
-
const finalText = finalContent
|
|
723
|
+
const finalText = event.message.content
|
|
642
724
|
.filter((part) => part.type === "text" && !!part.text)
|
|
643
725
|
.map((part) => part.text)
|
|
644
726
|
.join("\n");
|
|
@@ -648,6 +730,7 @@ class ChannelRunner {
|
|
|
648
730
|
}
|
|
649
731
|
if (trimmedFinalText === "[SILENT]" || trimmedFinalText.startsWith("[SILENT]")) {
|
|
650
732
|
this.runState.finalOutcome = { kind: "silent" };
|
|
733
|
+
this.memoryLifecycle.noteCompletedAssistantTurn();
|
|
651
734
|
return;
|
|
652
735
|
}
|
|
653
736
|
if (this.runState.finalOutcome.kind === "final" &&
|
|
@@ -655,6 +738,7 @@ class ChannelRunner {
|
|
|
655
738
|
return;
|
|
656
739
|
}
|
|
657
740
|
this.runState.finalOutcome = { kind: "final", text: finalText };
|
|
741
|
+
this.memoryLifecycle.noteCompletedAssistantTurn();
|
|
658
742
|
log.logResponse(logCtx, finalText);
|
|
659
743
|
queue.enqueue(async () => {
|
|
660
744
|
const delivered = await ctx.respondPlain(finalText);
|
|
@@ -664,23 +748,21 @@ class ChannelRunner {
|
|
|
664
748
|
}, "final response");
|
|
665
749
|
}
|
|
666
750
|
}
|
|
667
|
-
else if (event
|
|
751
|
+
else if (isAutoCompactionStartEvent(event)) {
|
|
668
752
|
log.logInfo(`Auto-compaction started (reason: ${event.reason})`);
|
|
669
753
|
queue.enqueue(() => ctx.respond(formatProgressEntry("assistant", "Compacting context..."), false), "compaction start");
|
|
670
754
|
}
|
|
671
|
-
else if (event
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
log.logInfo(`Auto-compaction complete: ${compEvent.result.tokensBefore} tokens compacted`);
|
|
755
|
+
else if (isAutoCompactionEndEvent(event)) {
|
|
756
|
+
if (event.result) {
|
|
757
|
+
log.logInfo(`Auto-compaction complete: ${event.result.tokensBefore} tokens compacted`);
|
|
675
758
|
}
|
|
676
|
-
else if (
|
|
759
|
+
else if (event.aborted) {
|
|
677
760
|
log.logInfo("Auto-compaction aborted");
|
|
678
761
|
}
|
|
679
762
|
}
|
|
680
|
-
else if (event
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
queue.enqueue(() => ctx.respond(formatProgressEntry("assistant", `Retrying (${retryEvent.attempt}/${retryEvent.maxAttempts})...`), false), "retry");
|
|
763
|
+
else if (isAutoRetryStartEvent(event)) {
|
|
764
|
+
log.logWarning(`Retrying (${event.attempt}/${event.maxAttempts})`, event.errorMessage);
|
|
765
|
+
queue.enqueue(() => ctx.respond(formatProgressEntry("assistant", `Retrying (${event.attempt}/${event.maxAttempts})...`), false), "retry");
|
|
684
766
|
}
|
|
685
767
|
});
|
|
686
768
|
}
|