@oyasmi/pipiclaw 0.4.0 → 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/README.md +43 -5
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +156 -57
- package/dist/agent.js.map +1 -1
- package/dist/context.d.ts +18 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +26 -0
- package/dist/context.js.map +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- 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/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 +66 -0
- 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/prompt-builder.d.ts.map +1 -1
- package/dist/prompt-builder.js +7 -2
- package/dist/prompt-builder.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/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/sub-agents.d.ts +10 -0
- package/dist/sub-agents.d.ts.map +1 -1
- package/dist/sub-agents.js +90 -0
- package/dist/sub-agents.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/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/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 +7 -1
- package/docs/proj-review.md +188 -0
- package/docs/test-supplementation-plan.md +553 -0
- 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、
|
|
277
|
+
channel 级 durable facts、decisions、preferences、medium-horizon open loops
|
|
271
278
|
- `<channel>/HISTORY.md`
|
|
272
279
|
更老上下文的摘要历史
|
|
273
280
|
|
|
274
|
-
|
|
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
|
|
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;
|
|
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
|
|
286
|
-
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
|
+
}
|
|
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
|
-
|
|
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
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
|
|
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
|
|
489
|
-
|
|
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(
|
|
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
|
|
500
|
-
const
|
|
501
|
-
const
|
|
502
|
-
|
|
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 =
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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:
|
|
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 =
|
|
644
|
+
const treatAsError = event.isError || Boolean(subAgentDetails?.failed);
|
|
542
645
|
if (treatAsError) {
|
|
543
|
-
log.logToolError(logCtx,
|
|
646
|
+
log.logToolError(logCtx, event.toolName, durationMs, resultStr);
|
|
544
647
|
}
|
|
545
648
|
else {
|
|
546
|
-
log.logToolSuccess(logCtx,
|
|
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
|
|
553
|
-
|
|
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
|
|
559
|
-
const
|
|
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 (
|
|
574
|
-
const assistantMsg =
|
|
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 =
|
|
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
|
|
698
|
+
if (isThinkingPart(part)) {
|
|
598
699
|
thinkingParts.push(part.thinking);
|
|
599
700
|
}
|
|
600
|
-
else if (part
|
|
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
|
|
618
|
-
|
|
619
|
-
|
|
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
|
|
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
|
|
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
|
|
655
|
-
|
|
656
|
-
|
|
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 (
|
|
759
|
+
else if (event.aborted) {
|
|
660
760
|
log.logInfo("Auto-compaction aborted");
|
|
661
761
|
}
|
|
662
762
|
}
|
|
663
|
-
else if (event
|
|
664
|
-
|
|
665
|
-
|
|
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
|
}
|