@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.
Files changed (122) hide show
  1. package/CHANGELOG.md +3 -0
  2. package/LICENSE +184 -0
  3. package/README.md +267 -230
  4. package/dist/agent.d.ts.map +1 -1
  5. package/dist/agent.js +158 -76
  6. package/dist/agent.js.map +1 -1
  7. package/dist/command-extension.d.ts.map +1 -1
  8. package/dist/command-extension.js.map +1 -1
  9. package/dist/commands.d.ts.map +1 -1
  10. package/dist/commands.js.map +1 -1
  11. package/dist/config-loader.d.ts.map +1 -1
  12. package/dist/config-loader.js.map +1 -1
  13. package/dist/context.d.ts +18 -0
  14. package/dist/context.d.ts.map +1 -1
  15. package/dist/context.js +26 -2
  16. package/dist/context.js.map +1 -1
  17. package/dist/delivery.d.ts.map +1 -1
  18. package/dist/delivery.js +11 -14
  19. package/dist/delivery.js.map +1 -1
  20. package/dist/dingtalk.d.ts.map +1 -1
  21. package/dist/dingtalk.js +26 -26
  22. package/dist/dingtalk.js.map +1 -1
  23. package/dist/events.d.ts.map +1 -1
  24. package/dist/events.js +5 -8
  25. package/dist/events.js.map +1 -1
  26. package/dist/index.d.ts +24 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +24 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/llm-json.d.ts +7 -0
  31. package/dist/llm-json.d.ts.map +1 -0
  32. package/dist/llm-json.js +77 -0
  33. package/dist/llm-json.js.map +1 -0
  34. package/dist/log.d.ts.map +1 -1
  35. package/dist/log.js.map +1 -1
  36. package/dist/main.d.ts.map +1 -1
  37. package/dist/main.js.map +1 -1
  38. package/dist/markdown-sections.d.ts +6 -0
  39. package/dist/markdown-sections.d.ts.map +1 -0
  40. package/dist/markdown-sections.js +34 -0
  41. package/dist/markdown-sections.js.map +1 -0
  42. package/dist/memory-candidates.d.ts +21 -0
  43. package/dist/memory-candidates.d.ts.map +1 -0
  44. package/dist/memory-candidates.js +126 -0
  45. package/dist/memory-candidates.js.map +1 -0
  46. package/dist/memory-consolidation.d.ts.map +1 -1
  47. package/dist/memory-consolidation.js +28 -49
  48. package/dist/memory-consolidation.js.map +1 -1
  49. package/dist/memory-files.d.ts +3 -0
  50. package/dist/memory-files.d.ts.map +1 -1
  51. package/dist/memory-files.js +51 -0
  52. package/dist/memory-files.js.map +1 -1
  53. package/dist/memory-lifecycle.d.ts +9 -0
  54. package/dist/memory-lifecycle.d.ts.map +1 -1
  55. package/dist/memory-lifecycle.js +67 -2
  56. package/dist/memory-lifecycle.js.map +1 -1
  57. package/dist/memory-recall.d.ts +29 -0
  58. package/dist/memory-recall.d.ts.map +1 -0
  59. package/dist/memory-recall.js +218 -0
  60. package/dist/memory-recall.js.map +1 -0
  61. package/dist/model-utils.d.ts.map +1 -1
  62. package/dist/model-utils.js.map +1 -1
  63. package/dist/paths.d.ts.map +1 -1
  64. package/dist/prompt-builder.d.ts.map +1 -1
  65. package/dist/prompt-builder.js +7 -2
  66. package/dist/prompt-builder.js.map +1 -1
  67. package/dist/sandbox.d.ts.map +1 -1
  68. package/dist/sandbox.js +0 -1
  69. package/dist/sandbox.js.map +1 -1
  70. package/dist/session-memory-files.d.ts +2 -0
  71. package/dist/session-memory-files.d.ts.map +1 -0
  72. package/dist/session-memory-files.js +2 -0
  73. package/dist/session-memory-files.js.map +1 -0
  74. package/dist/session-memory.d.ts +22 -0
  75. package/dist/session-memory.d.ts.map +1 -0
  76. package/dist/session-memory.js +274 -0
  77. package/dist/session-memory.js.map +1 -0
  78. package/dist/shell-escape.d.ts.map +1 -1
  79. package/dist/shell-escape.js.map +1 -1
  80. package/dist/sidecar-worker.d.ts +27 -0
  81. package/dist/sidecar-worker.d.ts.map +1 -0
  82. package/dist/sidecar-worker.js +105 -0
  83. package/dist/sidecar-worker.js.map +1 -0
  84. package/dist/store.d.ts.map +1 -1
  85. package/dist/store.js +2 -3
  86. package/dist/store.js.map +1 -1
  87. package/dist/sub-agents.d.ts +10 -0
  88. package/dist/sub-agents.d.ts.map +1 -1
  89. package/dist/sub-agents.js +132 -10
  90. package/dist/sub-agents.js.map +1 -1
  91. package/dist/tools/attach.d.ts.map +1 -1
  92. package/dist/tools/attach.js.map +1 -1
  93. package/dist/tools/bash.d.ts.map +1 -1
  94. package/dist/tools/bash.js.map +1 -1
  95. package/dist/tools/edit.d.ts.map +1 -1
  96. package/dist/tools/edit.js.map +1 -1
  97. package/dist/tools/index.d.ts +3 -0
  98. package/dist/tools/index.d.ts.map +1 -1
  99. package/dist/tools/index.js +2 -0
  100. package/dist/tools/index.js.map +1 -1
  101. package/dist/tools/read.d.ts.map +1 -1
  102. package/dist/tools/read.js.map +1 -1
  103. package/dist/tools/subagent.d.ts +6 -0
  104. package/dist/tools/subagent.d.ts.map +1 -1
  105. package/dist/tools/subagent.js +127 -12
  106. package/dist/tools/subagent.js.map +1 -1
  107. package/dist/tools/truncate.d.ts.map +1 -1
  108. package/dist/tools/truncate.js.map +1 -1
  109. package/dist/tools/write-content.d.ts.map +1 -1
  110. package/dist/tools/write-content.js.map +1 -1
  111. package/dist/tools/write.d.ts.map +1 -1
  112. package/dist/tools/write.js.map +1 -1
  113. package/docs/improve-memory/design.md +537 -0
  114. package/docs/improve-memory/interfaces-and-tests.md +473 -0
  115. package/docs/improve-memory/spec.md +357 -0
  116. package/docs/memory-rfc.md +297 -0
  117. package/docs/proj-review.md +188 -0
  118. package/docs/subagent/pi-subagent-analyse.txt +190 -0
  119. package/docs/subagent/pi-subagent-design.txt +266 -0
  120. package/docs/subagent/pi-subagent-phase1-plan.txt +529 -0
  121. package/docs/test-supplementation-plan.md +553 -0
  122. package/package.json +71 -53
@@ -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 userMessage = this.formatUserMessage(ctx.message.text, ctx.message.userName);
303
- const promptText = this.shouldPreserveRawInput(ctx.message.text) ? ctx.message.text.trim() : userMessage;
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
- await this.session.prompt(this.formatUserMessage(text, userName), {
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.type === "tool_execution_start") {
494
- const agentEvent = event;
495
- const args = agentEvent.args;
496
- const label = args.label || agentEvent.toolName;
497
- pendingTools.set(agentEvent.toolCallId, {
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
- log.logToolStart(logCtx, agentEvent.toolName, label, agentEvent.args);
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.type === "tool_execution_update") {
506
- const agentEvent = event;
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(agentEvent.partialResult), 200);
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.type === "tool_execution_end") {
517
- const agentEvent = event;
518
- const resultStr = extractToolResultText(agentEvent.result);
519
- const pending = pendingTools.get(agentEvent.toolCallId);
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 = 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
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: agentEvent.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 = agentEvent.isError || Boolean(subAgentDetails?.failed);
644
+ const treatAsError = event.isError || Boolean(subAgentDetails?.failed);
559
645
  if (treatAsError) {
560
- log.logToolError(logCtx, agentEvent.toolName, durationMs, resultStr);
646
+ log.logToolError(logCtx, event.toolName, durationMs, resultStr);
561
647
  }
562
648
  else {
563
- log.logToolSuccess(logCtx, agentEvent.toolName, durationMs, resultStr);
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.type === "message_start") {
570
- const agentEvent = event;
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.type === "message_end") {
576
- const agentEvent = event;
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 (agentEvent.message.role === "assistant") {
591
- const assistantMsg = agentEvent.message;
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 = agentEvent.message.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.type === "thinking") {
698
+ if (isThinkingPart(part)) {
615
699
  thinkingParts.push(part.thinking);
616
700
  }
617
- else if (part.type === "text") {
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.type === "turn_end") {
635
- const turnEvent = event;
636
- if (turnEvent.message.role === "assistant" && turnEvent.toolResults.length === 0) {
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 finalContent = turnEvent.message.content;
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.type === "auto_compaction_start") {
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.type === "auto_compaction_end") {
672
- const compEvent = event;
673
- if (compEvent.result) {
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 (compEvent.aborted) {
759
+ else if (event.aborted) {
677
760
  log.logInfo("Auto-compaction aborted");
678
761
  }
679
762
  }
680
- else if (event.type === "auto_retry_start") {
681
- const retryEvent = event;
682
- log.logWarning(`Retrying (${retryEvent.attempt}/${retryEvent.maxAttempts})`, retryEvent.errorMessage);
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
  }