@oyasmi/pipiclaw 0.4.0 → 0.5.1

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 (73) hide show
  1. package/README.md +43 -5
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +156 -57
  4. package/dist/agent.js.map +1 -1
  5. package/dist/context.d.ts +18 -0
  6. package/dist/context.d.ts.map +1 -1
  7. package/dist/context.js +26 -0
  8. package/dist/context.js.map +1 -1
  9. package/dist/index.d.ts +7 -3
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +6 -2
  12. package/dist/index.js.map +1 -1
  13. package/dist/llm-json.d.ts +7 -0
  14. package/dist/llm-json.d.ts.map +1 -0
  15. package/dist/llm-json.js +77 -0
  16. package/dist/llm-json.js.map +1 -0
  17. package/dist/markdown-sections.d.ts +6 -0
  18. package/dist/markdown-sections.d.ts.map +1 -0
  19. package/dist/markdown-sections.js +34 -0
  20. package/dist/markdown-sections.js.map +1 -0
  21. package/dist/memory-candidates.d.ts +21 -0
  22. package/dist/memory-candidates.d.ts.map +1 -0
  23. package/dist/memory-candidates.js +126 -0
  24. package/dist/memory-candidates.js.map +1 -0
  25. package/dist/memory-consolidation.d.ts.map +1 -1
  26. package/dist/memory-consolidation.js +28 -49
  27. package/dist/memory-consolidation.js.map +1 -1
  28. package/dist/memory-files.d.ts +3 -0
  29. package/dist/memory-files.d.ts.map +1 -1
  30. package/dist/memory-files.js +51 -0
  31. package/dist/memory-files.js.map +1 -1
  32. package/dist/memory-lifecycle.d.ts +9 -0
  33. package/dist/memory-lifecycle.d.ts.map +1 -1
  34. package/dist/memory-lifecycle.js +66 -0
  35. package/dist/memory-lifecycle.js.map +1 -1
  36. package/dist/memory-recall.d.ts +29 -0
  37. package/dist/memory-recall.d.ts.map +1 -0
  38. package/dist/memory-recall.js +218 -0
  39. package/dist/memory-recall.js.map +1 -0
  40. package/dist/prompt-builder.d.ts.map +1 -1
  41. package/dist/prompt-builder.js +7 -2
  42. package/dist/prompt-builder.js.map +1 -1
  43. package/dist/session-memory-files.d.ts +2 -0
  44. package/dist/session-memory-files.d.ts.map +1 -0
  45. package/dist/session-memory-files.js +2 -0
  46. package/dist/session-memory-files.js.map +1 -0
  47. package/dist/session-memory.d.ts +22 -0
  48. package/dist/session-memory.d.ts.map +1 -0
  49. package/dist/session-memory.js +274 -0
  50. package/dist/session-memory.js.map +1 -0
  51. package/dist/sidecar-worker.d.ts +27 -0
  52. package/dist/sidecar-worker.d.ts.map +1 -0
  53. package/dist/sidecar-worker.js +105 -0
  54. package/dist/sidecar-worker.js.map +1 -0
  55. package/dist/sub-agents.d.ts +10 -0
  56. package/dist/sub-agents.d.ts.map +1 -1
  57. package/dist/sub-agents.js +90 -0
  58. package/dist/sub-agents.js.map +1 -1
  59. package/dist/tools/index.d.ts +3 -0
  60. package/dist/tools/index.d.ts.map +1 -1
  61. package/dist/tools/index.js +2 -0
  62. package/dist/tools/index.js.map +1 -1
  63. package/dist/tools/subagent.d.ts +6 -0
  64. package/dist/tools/subagent.d.ts.map +1 -1
  65. package/dist/tools/subagent.js +127 -12
  66. package/dist/tools/subagent.js.map +1 -1
  67. package/docs/improve-memory/design.md +537 -0
  68. package/docs/improve-memory/interfaces-and-tests.md +473 -0
  69. package/docs/improve-memory/spec.md +357 -0
  70. package/docs/memory-rfc.md +7 -1
  71. package/docs/proj-review.md +188 -0
  72. package/docs/test-supplementation-plan.md +553 -0
  73. package/package.json +3 -1
package/README.md CHANGED
@@ -11,7 +11,7 @@ npm package: [`@oyasmi/pipiclaw`](https://www.npmjs.com/package/@oyasmi/pipiclaw
11
11
  - 钉钉优先:原生支持 DingTalk Stream Mode,不需要自己再包一层消息桥
12
12
  - 过程可见:思考、工具执行和状态更新可以持续流式展示到 AI Card
13
13
  - 任务不中断:忙碌时支持 steer、follow-up 和 stop,而不是简单丢弃新消息
14
- - 有记忆,但不过载:`MEMORY.md` / `HISTORY.md` 分层管理,避免上下文无限膨胀
14
+ - 有记忆,但不过载:`SESSION.md` / `MEMORY.md` / `HISTORY.md` 分层管理,避免上下文无限膨胀
15
15
  - 支持子代理:主代理可以把 review、research、planning 等任务委派给独立上下文的 sub-agent
16
16
  - 适合长期运行:每个私聊和群聊都有稳定的 channel workspace、日志和事件目录
17
17
  - 保持可编排:模型、技能、workspace 文件和事件都可以通过普通文件管理
@@ -22,7 +22,7 @@ npm package: [`@oyasmi/pipiclaw`](https://www.npmjs.com/package/@oyasmi/pipiclaw
22
22
  - 内置 slash commands:`/help`、`/new`、`/compact`、`/session`、`/model`
23
23
  - 忙碌时普通消息默认作为 steer 注入当前任务,也支持显式 `/steer`、`/followup`、`/stop`
24
24
  - workspace 级 `SOUL.md`、`AGENTS.md`、`MEMORY.md`
25
- - channel 级 `MEMORY.md`、`HISTORY.md`、`skills/`
25
+ - channel 级 `SESSION.md`、`MEMORY.md`、`HISTORY.md`、`skills/`
26
26
  - 预定义 sub-agent 和临时 inline sub-agent
27
27
  - immediate / one-shot / periodic 事件调度
28
28
  - 自定义 provider / model 配置
@@ -228,6 +228,7 @@ Pipiclaw 的核心不是“一个机器人实例”,而是一组长期存在
228
228
  ├── skills/
229
229
  ├── sub-agents/
230
230
  ├── dm_{userId}/
231
+ │ ├── SESSION.md
231
232
  │ ├── MEMORY.md
232
233
  │ ├── HISTORY.md
233
234
  │ ├── .channel-meta.json
@@ -253,6 +254,7 @@ Pipiclaw 的核心不是“一个机器人实例”,而是一组长期存在
253
254
  默认不会直接注入上下文的内容:
254
255
 
255
256
  - `workspace/MEMORY.md`
257
+ - `<channel>/SESSION.md`
256
258
  - `<channel>/MEMORY.md`
257
259
  - `<channel>/HISTORY.md`
258
260
  - `<channel>/log.jsonl`
@@ -260,23 +262,42 @@ Pipiclaw 的核心不是“一个机器人实例”,而是一组长期存在
260
262
 
261
263
  这意味着 Pipiclaw 的记忆策略是“按需读取”,而不是把所有历史永远塞进 prompt。
262
264
 
265
+ 不过,运行时现在会在当前轮明显需要时,自动从 `SESSION.md` / `MEMORY.md` / `HISTORY.md` 里挑出少量相关片段,作为一个很小的 runtime context 注入当前请求。这不是整文件预加载,而是受限的 relevant-memory prefetch。
266
+
263
267
  ## Memory Model
264
268
 
265
- Pipiclaw 把记忆分成三层:
269
+ Pipiclaw 把记忆分成四层:
270
+
271
+ - `<channel>/SESSION.md`
272
+ 当前工作态。放现在正在做什么、最近卡点、下一步、活跃文件和短期约束。它是 runtime-managed working memory。
266
273
 
267
274
  - `workspace/MEMORY.md`
268
275
  稳定的全局背景,适合放团队长期约定和共享知识
269
276
  - `<channel>/MEMORY.md`
270
- channel 级 durable facts、ongoing work、decisions、open loops
277
+ channel 级 durable facts、decisions、preferences、medium-horizon open loops
271
278
  - `<channel>/HISTORY.md`
272
279
  更老上下文的摘要历史
273
280
 
274
- 运行时会在 compaction 或 session trimming 前自动做 consolidation:
281
+ 运行时行为分成两条线:
282
+
283
+ - relevant recall
284
+ 每轮可按需从 `SESSION.md` / `MEMORY.md` / `HISTORY.md` 里挑出少量最相关片段,预先送进当前 prompt
285
+ - consolidation
286
+ 在 compaction 或 session trimming 前刷新 `SESSION.md`,并把值得长期保留的内容沉淀到 `MEMORY.md` / `HISTORY.md`
287
+
288
+ consolidation 会做这些事:
275
289
 
276
290
  - 从对话中提取值得保留的 memory entries
291
+ - 更新 `SESSION.md` 中的当前工作态
277
292
  - 把旧对话块折叠进 `HISTORY.md`
278
293
  - 在必要时压缩过长的 memory/history 文件
279
294
 
295
+ 一个简单原则:
296
+
297
+ - 现在正在做什么,看 `SESSION.md`
298
+ - 稳定事实和决策,看 `MEMORY.md`
299
+ - 更老的脉络和叙事,看 `HISTORY.md`
300
+
280
301
  ## Sub-Agents
281
302
 
282
303
  Pipiclaw 支持两种 sub-agent 用法:
@@ -296,6 +317,11 @@ name: reviewer
296
317
  description: Review code changes for correctness, regressions, and missing tests
297
318
  model: anthropic/claude-sonnet-4-5
298
319
  tools: read,bash
320
+ contextMode: contextual
321
+ memory: relevant
322
+ paths:
323
+ - src/
324
+ - test/
299
325
  maxTurns: 24
300
326
  maxToolCalls: 48
301
327
  maxWallTimeSec: 300
@@ -314,6 +340,18 @@ Keep findings concise and actionable.
314
340
  - sub-agent 没有 `subagent` 工具,所以不能继续创建孙代理
315
341
  - sub-agent 隔离的是 LLM 对话上下文,不隔离文件系统
316
342
  - 运行摘要会记录到 `<channel>/subagent-runs.jsonl`
343
+ - 如果后续启用 contextual sub-agent,runtime 还会把 relevant memory 和 `SESSION.md` 摘要按角色需要带给子代理,而不是只给它一个裸任务
344
+
345
+ 几个常用 frontmatter:
346
+
347
+ - `contextMode: contextual`
348
+ 让 runtime 在任务前自动注入受限的上下文块
349
+ - `memory: session`
350
+ 只带 `SESSION.md` 的关键工作态
351
+ - `memory: relevant`
352
+ 带 `SESSION.md` 关键工作态,再加少量从 `MEMORY.md` / `HISTORY.md` / workspace memory 里召回的相关片段
353
+ - `paths`
354
+ 给子代理一个明确的关注范围,既帮助它聚焦,也会参与 recall
317
355
 
318
356
  ## Scheduled Events
319
357
 
@@ -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"}
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,6 +152,67 @@ 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
  // ============================================================================
@@ -173,10 +247,12 @@ class ChannelRunner {
173
247
  getAvailableModels: () => this.modelRegistry.getAvailable(),
174
248
  resolveApiKey: async (model) => getApiKeyForModel(this.modelRegistry, model),
175
249
  workspaceDir: this.workspaceDir,
250
+ channelDir: this.channelDir,
176
251
  workspacePath: this.workspacePath,
177
252
  channelId: this.channelId,
178
253
  sandboxConfig: this.sandboxConfig,
179
254
  getSubAgentDiscovery: () => this.subAgentDiscovery,
255
+ getMemoryRecallSettings: () => this.settingsManager.getMemoryRecallSettings(),
180
256
  });
181
257
  // Create agent
182
258
  this.agent = new Agent({
@@ -196,6 +272,7 @@ class ChannelRunner {
196
272
  getSessionEntries: () => this.sessionManager.getBranch(),
197
273
  getModel: () => this.session.model ?? this.activeModel,
198
274
  resolveApiKey: async (model) => getApiKeyForModel(this.modelRegistry, model),
275
+ getSessionMemorySettings: () => this.settingsManager.getSessionMemorySettings(),
199
276
  });
200
277
  const resourceLoader = new DefaultResourceLoader({
201
278
  cwd: process.cwd(),
@@ -282,13 +359,39 @@ class ChannelRunner {
282
359
  await this.ensureSessionReady();
283
360
  // Ensure channel directory exists
284
361
  await mkdir(this.channelDir, { recursive: true });
285
- const userMessage = this.formatUserMessage(ctx.message.text, ctx.message.userName);
286
- 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
+ }
287
389
  // Debug: write context to last_prompt.json (only with PIPICLAW_DEBUG=1)
288
390
  if (process.env.PIPICLAW_DEBUG) {
289
391
  const debugContext = {
290
392
  systemPrompt: this.agent.state.systemPrompt,
291
393
  messages: this.session.messages,
394
+ recalledContext: recalledContextText || undefined,
292
395
  newUserMessage: promptText,
293
396
  };
294
397
  await writeFile(join(this.channelDir, "last_prompt.json"), JSON.stringify(debugContext, null, 2));
@@ -429,7 +532,11 @@ class ChannelRunner {
429
532
  if (!this.session.isStreaming) {
430
533
  throw new Error("No task is currently running.");
431
534
  }
432
- 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), {
433
540
  streamingBehavior: delivery,
434
541
  });
435
542
  }
@@ -473,41 +580,37 @@ class ChannelRunner {
473
580
  if (!this.runState.ctx || !this.runState.logCtx || !this.runState.queue)
474
581
  return;
475
582
  const { ctx, logCtx, queue, pendingTools, store } = this.runState;
476
- if (event.type === "tool_execution_start") {
477
- const agentEvent = event;
478
- const args = agentEvent.args;
479
- const label = args.label || agentEvent.toolName;
480
- pendingTools.set(agentEvent.toolCallId, {
481
- toolName: agentEvent.toolName,
482
- 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,
483
588
  startTime: Date.now(),
484
589
  });
485
- log.logToolStart(logCtx, agentEvent.toolName, label, agentEvent.args);
590
+ this.memoryLifecycle.noteToolCall();
591
+ log.logToolStart(logCtx, event.toolName, label, isRecord(event.args) ? event.args : {});
486
592
  queue.enqueue(() => ctx.respond(formatProgressEntry("tool", label), false), "tool label");
487
593
  }
488
- else if (event.type === "tool_execution_update") {
489
- const agentEvent = event;
490
- if (agentEvent.toolName !== "subagent") {
594
+ else if (isToolExecutionUpdateEvent(event)) {
595
+ if (event.toolName !== "subagent") {
491
596
  return;
492
597
  }
493
- const partialText = truncate(extractToolResultText(agentEvent.partialResult), 200);
598
+ const partialText = truncate(extractToolResultText(event.partialResult), 200);
494
599
  if (!partialText.trim()) {
495
600
  return;
496
601
  }
497
602
  queue.enqueue(() => ctx.respond(formatProgressEntry("tool", partialText), false), "tool update");
498
603
  }
499
- else if (event.type === "tool_execution_end") {
500
- const agentEvent = event;
501
- const resultStr = extractToolResultText(agentEvent.result);
502
- const pending = pendingTools.get(agentEvent.toolCallId);
503
- 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);
504
608
  const durationMs = pending ? Date.now() - pending.startTime : 0;
505
- const subAgentDetails = agentEvent.toolName === "subagent" &&
506
- agentEvent.result &&
507
- typeof agentEvent.result === "object" &&
508
- "details" in agentEvent.result &&
509
- isSubAgentToolDetails(agentEvent.result.details)
510
- ? 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
511
614
  : null;
512
615
  if (subAgentDetails) {
513
616
  mergeSubAgentUsage(this.runState.totalUsage, subAgentDetails);
@@ -519,7 +622,7 @@ class ChannelRunner {
519
622
  : "subagent";
520
623
  queue.enqueue(() => store?.logSubAgentRun(logCtx.channelId, {
521
624
  date: new Date().toISOString(),
522
- toolCallId: agentEvent.toolCallId,
625
+ toolCallId: event.toolCallId,
523
626
  label,
524
627
  agent: subAgentDetails.agent,
525
628
  source: subAgentDetails.source,
@@ -538,26 +641,24 @@ class ChannelRunner {
538
641
  },
539
642
  }) ?? Promise.resolve(), "sub-agent run log");
540
643
  }
541
- const treatAsError = agentEvent.isError || Boolean(subAgentDetails?.failed);
644
+ const treatAsError = event.isError || Boolean(subAgentDetails?.failed);
542
645
  if (treatAsError) {
543
- log.logToolError(logCtx, agentEvent.toolName, durationMs, resultStr);
646
+ log.logToolError(logCtx, event.toolName, durationMs, resultStr);
544
647
  }
545
648
  else {
546
- log.logToolSuccess(logCtx, agentEvent.toolName, durationMs, resultStr);
649
+ log.logToolSuccess(logCtx, event.toolName, durationMs, resultStr);
547
650
  }
548
651
  if (treatAsError) {
549
652
  queue.enqueue(() => ctx.respond(formatProgressEntry("error", truncate(resultStr, 200)), false), "tool error");
550
653
  }
551
654
  }
552
- else if (event.type === "message_start") {
553
- const agentEvent = event;
554
- if (agentEvent.message.role === "assistant") {
655
+ else if (isMessageStartEvent(event)) {
656
+ if (isAssistantEventMessage(event.message)) {
555
657
  log.logResponseStart(logCtx);
556
658
  }
557
659
  }
558
- else if (event.type === "message_end") {
559
- const agentEvent = event;
560
- const commandResultText = extractCustomCommandResultText(agentEvent.message);
660
+ else if (isMessageEndEvent(event)) {
661
+ const commandResultText = extractCustomCommandResultText(event.message);
561
662
  if (commandResultText) {
562
663
  this.runState.finalOutcome = { kind: "final", text: commandResultText };
563
664
  log.logResponse(logCtx, commandResultText);
@@ -570,8 +671,8 @@ class ChannelRunner {
570
671
  }, "command result");
571
672
  return;
572
673
  }
573
- if (agentEvent.message.role === "assistant") {
574
- const assistantMsg = agentEvent.message;
674
+ if (isAssistantEventMessage(event.message)) {
675
+ const assistantMsg = event.message;
575
676
  if (assistantMsg.stopReason) {
576
677
  this.runState.stopReason = assistantMsg.stopReason;
577
678
  }
@@ -589,15 +690,15 @@ class ChannelRunner {
589
690
  this.runState.totalUsage.cost.cacheWrite += assistantMsg.usage.cost.cacheWrite;
590
691
  this.runState.totalUsage.cost.total += assistantMsg.usage.cost.total;
591
692
  }
592
- const content = agentEvent.message.content;
693
+ const content = assistantMsg.content;
593
694
  const thinkingParts = [];
594
695
  const textParts = [];
595
696
  let hasToolCalls = false;
596
697
  for (const part of content) {
597
- if (part.type === "thinking") {
698
+ if (isThinkingPart(part)) {
598
699
  thinkingParts.push(part.thinking);
599
700
  }
600
- else if (part.type === "text") {
701
+ else if (isTextPart(part)) {
601
702
  textParts.push(part.text);
602
703
  }
603
704
  else if (part.type === "toolCall") {
@@ -614,14 +715,12 @@ class ChannelRunner {
614
715
  }
615
716
  }
616
717
  }
617
- else if (event.type === "turn_end") {
618
- const turnEvent = event;
619
- if (turnEvent.message.role === "assistant" && turnEvent.toolResults.length === 0) {
620
- 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") {
621
721
  return;
622
722
  }
623
- const finalContent = turnEvent.message.content;
624
- const finalText = finalContent
723
+ const finalText = event.message.content
625
724
  .filter((part) => part.type === "text" && !!part.text)
626
725
  .map((part) => part.text)
627
726
  .join("\n");
@@ -631,6 +730,7 @@ class ChannelRunner {
631
730
  }
632
731
  if (trimmedFinalText === "[SILENT]" || trimmedFinalText.startsWith("[SILENT]")) {
633
732
  this.runState.finalOutcome = { kind: "silent" };
733
+ this.memoryLifecycle.noteCompletedAssistantTurn();
634
734
  return;
635
735
  }
636
736
  if (this.runState.finalOutcome.kind === "final" &&
@@ -638,6 +738,7 @@ class ChannelRunner {
638
738
  return;
639
739
  }
640
740
  this.runState.finalOutcome = { kind: "final", text: finalText };
741
+ this.memoryLifecycle.noteCompletedAssistantTurn();
641
742
  log.logResponse(logCtx, finalText);
642
743
  queue.enqueue(async () => {
643
744
  const delivered = await ctx.respondPlain(finalText);
@@ -647,23 +748,21 @@ class ChannelRunner {
647
748
  }, "final response");
648
749
  }
649
750
  }
650
- else if (event.type === "auto_compaction_start") {
751
+ else if (isAutoCompactionStartEvent(event)) {
651
752
  log.logInfo(`Auto-compaction started (reason: ${event.reason})`);
652
753
  queue.enqueue(() => ctx.respond(formatProgressEntry("assistant", "Compacting context..."), false), "compaction start");
653
754
  }
654
- else if (event.type === "auto_compaction_end") {
655
- const compEvent = event;
656
- if (compEvent.result) {
657
- 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`);
658
758
  }
659
- else if (compEvent.aborted) {
759
+ else if (event.aborted) {
660
760
  log.logInfo("Auto-compaction aborted");
661
761
  }
662
762
  }
663
- else if (event.type === "auto_retry_start") {
664
- const retryEvent = event;
665
- log.logWarning(`Retrying (${retryEvent.attempt}/${retryEvent.maxAttempts})`, retryEvent.errorMessage);
666
- 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");
667
766
  }
668
767
  });
669
768
  }