@oyasmi/pipiclaw 0.5.1 → 0.5.3

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 (193) hide show
  1. package/README.md +308 -209
  2. package/dist/agent/channel-runner.d.ts +47 -0
  3. package/dist/agent/channel-runner.js +441 -0
  4. package/dist/agent/index.d.ts +3 -0
  5. package/dist/agent/index.js +2 -0
  6. package/dist/agent/progress-formatter.d.ts +4 -0
  7. package/dist/agent/progress-formatter.js +52 -0
  8. package/dist/agent/run-queue.d.ts +7 -0
  9. package/dist/agent/run-queue.js +26 -0
  10. package/dist/agent/runner-factory.d.ts +3 -0
  11. package/dist/agent/runner-factory.js +10 -0
  12. package/dist/agent/session-events.d.ts +14 -0
  13. package/dist/agent/session-events.js +215 -0
  14. package/dist/agent/session-resource-gate.d.ts +10 -0
  15. package/dist/agent/session-resource-gate.js +44 -0
  16. package/dist/agent/type-guards.d.ts +22 -0
  17. package/dist/agent/type-guards.js +106 -0
  18. package/dist/agent/types.d.ts +160 -0
  19. package/dist/agent/types.js +22 -0
  20. package/dist/agent.d.ts +2 -16
  21. package/dist/agent.js +1 -782
  22. package/dist/command-extension.d.ts +0 -1
  23. package/dist/command-extension.js +0 -1
  24. package/dist/commands.d.ts +0 -1
  25. package/dist/commands.js +0 -1
  26. package/dist/config-loader.d.ts +0 -1
  27. package/dist/config-loader.js +1 -2
  28. package/dist/context.d.ts +58 -15
  29. package/dist/context.js +50 -8
  30. package/dist/index.d.ts +12 -13
  31. package/dist/index.js +12 -13
  32. package/dist/log.d.ts +0 -1
  33. package/dist/log.js +0 -1
  34. package/dist/main.d.ts +0 -1
  35. package/dist/main.js +5 -405
  36. package/dist/memory/bootstrap.d.ts +6 -0
  37. package/dist/memory/bootstrap.js +46 -0
  38. package/dist/{memory-candidates.d.ts → memory/candidates.d.ts} +1 -1
  39. package/dist/{memory-candidates.js → memory/candidates.js} +33 -21
  40. package/dist/memory/chinese-words.d.ts +1 -0
  41. package/dist/memory/chinese-words.js +273 -0
  42. package/dist/{memory-consolidation.d.ts → memory/consolidation.d.ts} +0 -1
  43. package/dist/{memory-consolidation.js → memory/consolidation.js} +26 -35
  44. package/dist/{memory-files.d.ts → memory/files.d.ts} +0 -6
  45. package/dist/{memory-files.js → memory/files.js} +11 -36
  46. package/dist/{memory-lifecycle.d.ts → memory/lifecycle.d.ts} +23 -6
  47. package/dist/memory/lifecycle.js +246 -0
  48. package/dist/{memory-recall.d.ts → memory/recall.d.ts} +2 -2
  49. package/dist/memory/recall.js +501 -0
  50. package/dist/{session-memory.d.ts → memory/session.d.ts} +1 -1
  51. package/dist/{session-memory.js → memory/session.js} +31 -62
  52. package/dist/model-utils.d.ts +0 -1
  53. package/dist/model-utils.js +0 -1
  54. package/dist/paths.d.ts +0 -1
  55. package/dist/paths.js +0 -1
  56. package/dist/prompt-builder.d.ts +0 -1
  57. package/dist/prompt-builder.js +0 -1
  58. package/dist/runtime/bootstrap.d.ts +47 -0
  59. package/dist/runtime/bootstrap.js +450 -0
  60. package/dist/{delivery.d.ts → runtime/delivery.d.ts} +0 -1
  61. package/dist/{delivery.js → runtime/delivery.js} +1 -2
  62. package/dist/{dingtalk.d.ts → runtime/dingtalk.d.ts} +10 -1
  63. package/dist/{dingtalk.js → runtime/dingtalk.js} +87 -28
  64. package/dist/{events.d.ts → runtime/events.d.ts} +0 -1
  65. package/dist/{events.js → runtime/events.js} +1 -2
  66. package/dist/{store.d.ts → runtime/store.d.ts} +5 -1
  67. package/dist/{store.js → runtime/store.js} +60 -20
  68. package/dist/sandbox.d.ts +0 -1
  69. package/dist/sandbox.js +1 -2
  70. package/dist/{llm-json.d.ts → shared/llm-json.d.ts} +0 -1
  71. package/dist/{llm-json.js → shared/llm-json.js} +0 -1
  72. package/dist/shared/markdown-sections.d.ts +6 -0
  73. package/dist/{markdown-sections.js → shared/markdown-sections.js} +10 -4
  74. package/dist/{shell-escape.d.ts → shared/shell-escape.d.ts} +0 -1
  75. package/dist/{shell-escape.js → shared/shell-escape.js} +0 -1
  76. package/dist/shared/text-utils.d.ts +9 -0
  77. package/dist/shared/text-utils.js +36 -0
  78. package/dist/shared/type-guards.d.ts +5 -0
  79. package/dist/shared/type-guards.js +12 -0
  80. package/dist/shared/types.d.ts +14 -0
  81. package/dist/shared/types.js +1 -0
  82. package/dist/sidecar-worker.d.ts +0 -1
  83. package/dist/sidecar-worker.js +1 -8
  84. package/dist/{sub-agents.d.ts → subagents/discovery.d.ts} +0 -1
  85. package/dist/{sub-agents.js → subagents/discovery.js} +2 -3
  86. package/dist/{tools/subagent.d.ts → subagents/tool.d.ts} +2 -16
  87. package/dist/{tools/subagent.js → subagents/tool.js} +16 -38
  88. package/dist/tools/attach.d.ts +0 -1
  89. package/dist/tools/attach.js +0 -1
  90. package/dist/tools/bash.d.ts +0 -1
  91. package/dist/tools/bash.js +0 -1
  92. package/dist/tools/edit.d.ts +0 -1
  93. package/dist/tools/edit.js +1 -2
  94. package/dist/tools/index.d.ts +1 -2
  95. package/dist/tools/index.js +1 -2
  96. package/dist/tools/read.d.ts +0 -1
  97. package/dist/tools/read.js +1 -2
  98. package/dist/tools/truncate.d.ts +0 -1
  99. package/dist/tools/truncate.js +0 -1
  100. package/dist/tools/write-content.d.ts +0 -1
  101. package/dist/tools/write-content.js +1 -2
  102. package/dist/tools/write.d.ts +0 -1
  103. package/dist/tools/write.js +0 -1
  104. package/package.json +9 -3
  105. package/CHANGELOG.md +0 -47
  106. package/dist/agent.d.ts.map +0 -1
  107. package/dist/agent.js.map +0 -1
  108. package/dist/command-extension.d.ts.map +0 -1
  109. package/dist/command-extension.js.map +0 -1
  110. package/dist/commands.d.ts.map +0 -1
  111. package/dist/commands.js.map +0 -1
  112. package/dist/config-loader.d.ts.map +0 -1
  113. package/dist/config-loader.js.map +0 -1
  114. package/dist/context.d.ts.map +0 -1
  115. package/dist/context.js.map +0 -1
  116. package/dist/delivery.d.ts.map +0 -1
  117. package/dist/delivery.js.map +0 -1
  118. package/dist/dingtalk.d.ts.map +0 -1
  119. package/dist/dingtalk.js.map +0 -1
  120. package/dist/events.d.ts.map +0 -1
  121. package/dist/events.js.map +0 -1
  122. package/dist/index.d.ts.map +0 -1
  123. package/dist/index.js.map +0 -1
  124. package/dist/llm-json.d.ts.map +0 -1
  125. package/dist/llm-json.js.map +0 -1
  126. package/dist/log.d.ts.map +0 -1
  127. package/dist/log.js.map +0 -1
  128. package/dist/main.d.ts.map +0 -1
  129. package/dist/main.js.map +0 -1
  130. package/dist/markdown-sections.d.ts +0 -6
  131. package/dist/markdown-sections.d.ts.map +0 -1
  132. package/dist/markdown-sections.js.map +0 -1
  133. package/dist/memory-candidates.d.ts.map +0 -1
  134. package/dist/memory-candidates.js.map +0 -1
  135. package/dist/memory-consolidation.d.ts.map +0 -1
  136. package/dist/memory-consolidation.js.map +0 -1
  137. package/dist/memory-files.d.ts.map +0 -1
  138. package/dist/memory-files.js.map +0 -1
  139. package/dist/memory-lifecycle.d.ts.map +0 -1
  140. package/dist/memory-lifecycle.js +0 -150
  141. package/dist/memory-lifecycle.js.map +0 -1
  142. package/dist/memory-recall.d.ts.map +0 -1
  143. package/dist/memory-recall.js +0 -218
  144. package/dist/memory-recall.js.map +0 -1
  145. package/dist/model-utils.d.ts.map +0 -1
  146. package/dist/model-utils.js.map +0 -1
  147. package/dist/paths.d.ts.map +0 -1
  148. package/dist/paths.js.map +0 -1
  149. package/dist/prompt-builder.d.ts.map +0 -1
  150. package/dist/prompt-builder.js.map +0 -1
  151. package/dist/sandbox.d.ts.map +0 -1
  152. package/dist/sandbox.js.map +0 -1
  153. package/dist/session-memory-files.d.ts +0 -2
  154. package/dist/session-memory-files.d.ts.map +0 -1
  155. package/dist/session-memory-files.js +0 -2
  156. package/dist/session-memory-files.js.map +0 -1
  157. package/dist/session-memory.d.ts.map +0 -1
  158. package/dist/session-memory.js.map +0 -1
  159. package/dist/shell-escape.d.ts.map +0 -1
  160. package/dist/shell-escape.js.map +0 -1
  161. package/dist/sidecar-worker.d.ts.map +0 -1
  162. package/dist/sidecar-worker.js.map +0 -1
  163. package/dist/store.d.ts.map +0 -1
  164. package/dist/store.js.map +0 -1
  165. package/dist/sub-agents.d.ts.map +0 -1
  166. package/dist/sub-agents.js.map +0 -1
  167. package/dist/tools/attach.d.ts.map +0 -1
  168. package/dist/tools/attach.js.map +0 -1
  169. package/dist/tools/bash.d.ts.map +0 -1
  170. package/dist/tools/bash.js.map +0 -1
  171. package/dist/tools/edit.d.ts.map +0 -1
  172. package/dist/tools/edit.js.map +0 -1
  173. package/dist/tools/index.d.ts.map +0 -1
  174. package/dist/tools/index.js.map +0 -1
  175. package/dist/tools/read.d.ts.map +0 -1
  176. package/dist/tools/read.js.map +0 -1
  177. package/dist/tools/subagent.d.ts.map +0 -1
  178. package/dist/tools/subagent.js.map +0 -1
  179. package/dist/tools/truncate.d.ts.map +0 -1
  180. package/dist/tools/truncate.js.map +0 -1
  181. package/dist/tools/write-content.d.ts.map +0 -1
  182. package/dist/tools/write-content.js.map +0 -1
  183. package/dist/tools/write.d.ts.map +0 -1
  184. package/dist/tools/write.js.map +0 -1
  185. package/docs/improve-memory/design.md +0 -537
  186. package/docs/improve-memory/interfaces-and-tests.md +0 -473
  187. package/docs/improve-memory/spec.md +0 -357
  188. package/docs/memory-rfc.md +0 -297
  189. package/docs/proj-review.md +0 -188
  190. package/docs/subagent/pi-subagent-analyse.txt +0 -190
  191. package/docs/subagent/pi-subagent-design.txt +0 -266
  192. package/docs/subagent/pi-subagent-phase1-plan.txt +0 -529
  193. package/docs/test-supplementation-plan.md +0 -553
package/dist/agent.js CHANGED
@@ -1,782 +1 @@
1
- import { Agent } from "@mariozechner/pi-agent-core";
2
- import { AgentSession, AuthStorage, convertToLlm, DefaultResourceLoader, ModelRegistry, SessionManager, } from "@mariozechner/pi-coding-agent";
3
- import { mkdir, writeFile } from "fs/promises";
4
- import { dirname, join, resolve } from "path";
5
- import { COMMAND_RESULT_CUSTOM_TYPE, createCommandExtension } from "./command-extension.js";
6
- import { renderBuiltInHelp } from "./commands.js";
7
- import { getAgentConfig, getApiKeyForModel, getSoul, loadPipiclawSkills } from "./config-loader.js";
8
- import { PipiclawSettingsManager } from "./context.js";
9
- import * as log from "./log.js";
10
- import { createMemoryCandidateCache } from "./memory-candidates.js";
11
- import { MemoryLifecycle } from "./memory-lifecycle.js";
12
- import { recallRelevantMemory } from "./memory-recall.js";
13
- import { resolveInitialModel } from "./model-utils.js";
14
- import { APP_HOME_DIR, AUTH_CONFIG_PATH, MODELS_CONFIG_PATH } from "./paths.js";
15
- import { buildAppendSystemPrompt } from "./prompt-builder.js";
16
- import { createExecutor } from "./sandbox.js";
17
- import { discoverSubAgents, formatSubAgentList } from "./sub-agents.js";
18
- import { createPipiclawTools } from "./tools/index.js";
19
- function isSilentOutcome(outcome) {
20
- return outcome.kind === "silent";
21
- }
22
- function isFinalOutcome(outcome) {
23
- return outcome.kind === "final";
24
- }
25
- function getFinalOutcomeText(outcome) {
26
- return isFinalOutcome(outcome) ? outcome.text : null;
27
- }
28
- function createModelRegistry(authStorage, modelsJsonPath) {
29
- const registryClass = ModelRegistry;
30
- return typeof registryClass.create === "function"
31
- ? registryClass.create(authStorage, modelsJsonPath)
32
- : new registryClass(authStorage, modelsJsonPath);
33
- }
34
- // ============================================================================
35
- // Text helpers
36
- // ============================================================================
37
- function truncate(text, maxLen) {
38
- if (text.length <= maxLen)
39
- return text;
40
- return `${text.substring(0, maxLen - 3)}...`;
41
- }
42
- const MAX_USER_MESSAGE_CHARS = 12_000;
43
- const HAN_REGEX = /\p{Script=Han}/u;
44
- function sanitizeProgressText(text) {
45
- return text
46
- .replace(/\uFFFC/g, "")
47
- .replace(/\r/g, "")
48
- .trim();
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
- }
59
- function formatProgressEntry(kind, text) {
60
- const cleaned = sanitizeProgressText(text);
61
- if (!cleaned)
62
- return "";
63
- const normalized = cleaned.replace(/\n+/g, " ").trim();
64
- switch (kind) {
65
- case "tool":
66
- return `Running: ${normalized}`;
67
- case "thinking":
68
- return `Thinking: ${normalized}`;
69
- case "error":
70
- return `Error: ${normalized}`;
71
- case "assistant":
72
- return normalized;
73
- }
74
- }
75
- function extractToolResultText(result) {
76
- if (typeof result === "string") {
77
- return result;
78
- }
79
- if (result &&
80
- typeof result === "object" &&
81
- "content" in result &&
82
- Array.isArray(result.content)) {
83
- const content = result.content;
84
- const textParts = [];
85
- for (const part of content) {
86
- if (part.type === "text" && part.text) {
87
- textParts.push(part.text);
88
- }
89
- }
90
- if (textParts.length > 0) {
91
- return textParts.join("\n");
92
- }
93
- }
94
- return JSON.stringify(result);
95
- }
96
- function isSubAgentToolDetails(value) {
97
- if (!value || typeof value !== "object" || !("kind" in value) || value.kind !== "subagent") {
98
- return false;
99
- }
100
- if (!("usage" in value)) {
101
- return false;
102
- }
103
- const usage = value.usage;
104
- return (!!usage &&
105
- typeof usage === "object" &&
106
- "input" in usage &&
107
- "output" in usage &&
108
- "cacheRead" in usage &&
109
- "cacheWrite" in usage &&
110
- "cost" in usage);
111
- }
112
- function mergeSubAgentUsage(totalUsage, details) {
113
- totalUsage.input += details.usage.input;
114
- totalUsage.output += details.usage.output;
115
- totalUsage.cacheRead += details.usage.cacheRead;
116
- totalUsage.cacheWrite += details.usage.cacheWrite;
117
- totalUsage.cost.input += details.usage.cost.input;
118
- totalUsage.cost.output += details.usage.cost.output;
119
- totalUsage.cost.cacheRead += details.usage.cost.cacheRead;
120
- totalUsage.cost.cacheWrite += details.usage.cost.cacheWrite;
121
- totalUsage.cost.total += details.usage.cost.total;
122
- }
123
- function extractCustomCommandResultText(message) {
124
- if (!message ||
125
- typeof message !== "object" ||
126
- !("role" in message) ||
127
- !("customType" in message) ||
128
- message.role !== "custom" ||
129
- message.customType !== COMMAND_RESULT_CUSTOM_TYPE) {
130
- return null;
131
- }
132
- const content = message.content;
133
- return typeof content === "string" && content.trim() ? content : null;
134
- }
135
- function createEmptyRunState() {
136
- return {
137
- ctx: null,
138
- logCtx: null,
139
- store: null,
140
- queue: null,
141
- pendingTools: new Map(),
142
- totalUsage: {
143
- input: 0,
144
- output: 0,
145
- cacheRead: 0,
146
- cacheWrite: 0,
147
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
148
- },
149
- stopReason: "stop",
150
- errorMessage: undefined,
151
- finalOutcome: { kind: "none" },
152
- finalResponseDelivered: false,
153
- };
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
- }
216
- // ============================================================================
217
- // ChannelRunner
218
- // ============================================================================
219
- class ChannelRunner {
220
- constructor(sandboxConfig, channelId, channelDir) {
221
- // --- Per run ---
222
- this.runState = createEmptyRunState();
223
- this.sandboxConfig = sandboxConfig;
224
- this.channelId = channelId;
225
- this.channelDir = channelDir;
226
- const executor = createExecutor(sandboxConfig);
227
- this.workspaceDir = resolve(dirname(channelDir));
228
- this.workspacePath = executor.getWorkspacePath(this.workspaceDir);
229
- // Initial skill summaries
230
- const initialSkills = loadPipiclawSkills(channelDir, this.workspacePath);
231
- this.currentSkills = initialSkills;
232
- // Create session manager
233
- const contextFile = join(channelDir, "context.jsonl");
234
- this.sessionManager = SessionManager.open(contextFile, channelDir);
235
- this.settingsManager = new PipiclawSettingsManager(APP_HOME_DIR);
236
- // Create AuthStorage and ModelRegistry
237
- const authStorage = AuthStorage.create(AUTH_CONFIG_PATH);
238
- this.modelRegistry = createModelRegistry(authStorage, MODELS_CONFIG_PATH);
239
- // Resolve model: prefer saved global default, fall back to first available model
240
- this.activeModel = resolveInitialModel(this.modelRegistry, this.settingsManager);
241
- log.logInfo(`Using model: ${this.activeModel.provider}/${this.activeModel.id} (${this.activeModel.name})`);
242
- this.subAgentDiscovery = this.refreshSubAgentDiscovery();
243
- // Create tools
244
- const tools = createPipiclawTools({
245
- executor,
246
- getCurrentModel: () => this.activeModel,
247
- getAvailableModels: () => this.modelRegistry.getAvailable(),
248
- resolveApiKey: async (model) => getApiKeyForModel(this.modelRegistry, model),
249
- workspaceDir: this.workspaceDir,
250
- channelDir: this.channelDir,
251
- workspacePath: this.workspacePath,
252
- channelId: this.channelId,
253
- sandboxConfig: this.sandboxConfig,
254
- getSubAgentDiscovery: () => this.subAgentDiscovery,
255
- getMemoryRecallSettings: () => this.settingsManager.getMemoryRecallSettings(),
256
- });
257
- // Create agent
258
- this.agent = new Agent({
259
- initialState: {
260
- systemPrompt: "",
261
- model: this.activeModel,
262
- thinkingLevel: "off",
263
- tools,
264
- },
265
- convertToLlm,
266
- getApiKey: async () => getApiKeyForModel(this.modelRegistry, this.activeModel),
267
- });
268
- this.memoryLifecycle = new MemoryLifecycle({
269
- channelId: this.channelId,
270
- channelDir: this.channelDir,
271
- getMessages: () => this.session.messages,
272
- getSessionEntries: () => this.sessionManager.getBranch(),
273
- getModel: () => this.session.model ?? this.activeModel,
274
- resolveApiKey: async (model) => getApiKeyForModel(this.modelRegistry, model),
275
- getSessionMemorySettings: () => this.settingsManager.getSessionMemorySettings(),
276
- });
277
- const resourceLoader = new DefaultResourceLoader({
278
- cwd: process.cwd(),
279
- agentDir: APP_HOME_DIR,
280
- settingsManager: this.settingsManager,
281
- extensionFactories: [
282
- this.memoryLifecycle.createExtensionFactory(),
283
- createCommandExtension({
284
- getCurrentModel: () => this.session.model ?? this.activeModel,
285
- getAvailableModels: async () => {
286
- this.modelRegistry.refresh();
287
- return await this.modelRegistry.getAvailable();
288
- },
289
- getSessionStats: () => this.session.getSessionStats(),
290
- getThinkingLevel: () => this.session.thinkingLevel,
291
- switchModel: async (model) => {
292
- await this.session.setModel(model);
293
- this.activeModel = model;
294
- },
295
- refreshSessionResources: async () => {
296
- await this.refreshSessionResources();
297
- },
298
- }),
299
- ],
300
- appendSystemPromptOverride: (base) => {
301
- const soul = getSoul(this.workspaceDir);
302
- const sections = [...base];
303
- if (soul) {
304
- sections.unshift(soul);
305
- }
306
- sections.push(buildAppendSystemPrompt(this.workspacePath, this.channelId, this.sandboxConfig, {
307
- subAgentList: formatSubAgentList(this.subAgentDiscovery.agents),
308
- }));
309
- return sections;
310
- },
311
- agentsFilesOverride: () => {
312
- const agentConfig = getAgentConfig(this.channelDir);
313
- return {
314
- agentsFiles: agentConfig ? [{ path: `${this.workspacePath}/AGENTS.md`, content: agentConfig }] : [],
315
- };
316
- },
317
- skillsOverride: (base) => ({
318
- skills: [...base.skills, ...this.currentSkills],
319
- diagnostics: base.diagnostics,
320
- }),
321
- });
322
- const baseToolsOverride = Object.fromEntries(tools.map((tool) => [tool.name, tool]));
323
- // Create AgentSession
324
- this.session = new AgentSession({
325
- agent: this.agent,
326
- sessionManager: this.sessionManager,
327
- settingsManager: this.settingsManager,
328
- cwd: process.cwd(),
329
- modelRegistry: this.modelRegistry,
330
- resourceLoader,
331
- baseToolsOverride,
332
- });
333
- // Subscribe to session events
334
- this.subscribeToSessionEvents();
335
- this.sessionReady = this.initializeSession();
336
- }
337
- // === Public API ===
338
- async run(ctx, store) {
339
- this.resetRunState(ctx, store);
340
- // Create queue for this run
341
- let queueChain = Promise.resolve();
342
- this.runState.queue = {
343
- enqueue: (fn, errorContext) => {
344
- queueChain = queueChain.then(async () => {
345
- try {
346
- await fn();
347
- }
348
- catch (err) {
349
- const errMsg = err instanceof Error ? err.message : String(err);
350
- log.logWarning(`DingTalk API error (${errorContext})`, errMsg);
351
- }
352
- });
353
- },
354
- enqueueMessage: function (text, target, errorContext, doLog = true) {
355
- this.enqueue(() => (target === "main" ? ctx.respond(text, doLog) : ctx.respondInThread(text)), errorContext);
356
- },
357
- };
358
- try {
359
- await this.ensureSessionReady();
360
- // Ensure channel directory exists
361
- await mkdir(this.channelDir, { recursive: true });
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
- }
389
- // Debug: write context to last_prompt.json (only with PIPICLAW_DEBUG=1)
390
- if (process.env.PIPICLAW_DEBUG) {
391
- const debugContext = {
392
- systemPrompt: this.agent.state.systemPrompt,
393
- messages: this.session.messages,
394
- recalledContext: recalledContextText || undefined,
395
- newUserMessage: promptText,
396
- };
397
- await writeFile(join(this.channelDir, "last_prompt.json"), JSON.stringify(debugContext, null, 2));
398
- }
399
- await this.session.prompt(promptText);
400
- }
401
- catch (err) {
402
- this.runState.stopReason = "error";
403
- this.runState.errorMessage = err instanceof Error ? err.message : String(err);
404
- log.logWarning(`[${this.channelId}] Runner failed`, this.runState.errorMessage);
405
- }
406
- finally {
407
- await queueChain;
408
- const finalOutcome = this.runState.finalOutcome;
409
- const finalOutcomeText = getFinalOutcomeText(finalOutcome);
410
- try {
411
- if (this.runState.stopReason === "error" &&
412
- this.runState.errorMessage &&
413
- !this.runState.finalResponseDelivered) {
414
- try {
415
- await ctx.replaceMessage("_Sorry, something went wrong_");
416
- }
417
- catch (err) {
418
- const errMsg = err instanceof Error ? err.message : String(err);
419
- log.logWarning("Failed to post error message", errMsg);
420
- }
421
- }
422
- else if (isSilentOutcome(finalOutcome)) {
423
- try {
424
- await ctx.deleteMessage();
425
- log.logInfo("Silent response - deleted message");
426
- }
427
- catch (err) {
428
- const errMsg = err instanceof Error ? err.message : String(err);
429
- log.logWarning("Failed to delete message for silent response", errMsg);
430
- }
431
- }
432
- else if (finalOutcomeText && !this.runState.finalResponseDelivered) {
433
- try {
434
- await ctx.replaceMessage(finalOutcomeText);
435
- }
436
- catch (err) {
437
- const errMsg = err instanceof Error ? err.message : String(err);
438
- log.logWarning("Failed to replace message with final text", errMsg);
439
- }
440
- }
441
- await ctx.flush();
442
- }
443
- finally {
444
- await ctx.close();
445
- }
446
- // Log usage summary
447
- if (this.runState.totalUsage.cost.total > 0) {
448
- const messages = this.session.messages;
449
- const lastAssistantMessage = messages
450
- .slice()
451
- .reverse()
452
- .find((m) => m.role === "assistant" && m.stopReason !== "aborted");
453
- const contextTokens = lastAssistantMessage
454
- ? lastAssistantMessage.usage.input +
455
- lastAssistantMessage.usage.output +
456
- lastAssistantMessage.usage.cacheRead +
457
- lastAssistantMessage.usage.cacheWrite
458
- : 0;
459
- const currentRunModel = this.session.model ?? this.activeModel;
460
- const contextWindow = currentRunModel.contextWindow || 200000;
461
- log.logUsageSummary(this.runState.logCtx, this.runState.totalUsage, contextTokens, contextWindow);
462
- }
463
- // Clear run state
464
- this.runState.ctx = null;
465
- this.runState.logCtx = null;
466
- this.runState.queue = null;
467
- }
468
- return { stopReason: this.runState.stopReason, errorMessage: this.runState.errorMessage };
469
- }
470
- async handleBuiltinCommand(ctx, command) {
471
- try {
472
- switch (command.name) {
473
- case "help":
474
- await this.sendCommandReply(ctx, renderBuiltInHelp());
475
- return;
476
- case "stop":
477
- await this.sendCommandReply(ctx, "No task is running. Use `/stop` only while a task is running.");
478
- return;
479
- case "steer":
480
- this.requireQueuedMessage(command.args, "steer");
481
- await this.sendCommandReply(ctx, "No task is running. Send the message directly instead of using `/steer`.");
482
- return;
483
- case "followup":
484
- this.requireQueuedMessage(command.args, "followup");
485
- await this.sendCommandReply(ctx, "No task is running. Send the message directly now, or use `/followup` while a task is running.");
486
- return;
487
- }
488
- }
489
- catch (err) {
490
- const errMsg = err instanceof Error ? err.message : String(err);
491
- log.logWarning(`[${this.channelId}] Built-in command failed`, errMsg);
492
- await this.sendCommandReply(ctx, `命令执行失败:${errMsg}`);
493
- }
494
- }
495
- async queueSteer(text, userName) {
496
- await this.queueBusyMessage("steer", this.requireQueuedMessage(text, "steer"), userName);
497
- }
498
- async queueFollowUp(text, userName) {
499
- await this.queueBusyMessage("followUp", this.requireQueuedMessage(text, "followup"), userName);
500
- }
501
- async abort() {
502
- await this.session.abort();
503
- }
504
- // === Private helpers ===
505
- async sendCommandReply(ctx, text) {
506
- const delivered = await ctx.respondPlain(text);
507
- if (!delivered) {
508
- await ctx.replaceMessage(text);
509
- await ctx.flush();
510
- }
511
- }
512
- requireQueuedMessage(text, commandName) {
513
- const trimmedText = text.trim();
514
- if (!trimmedText) {
515
- throw new Error(`/${commandName} requires a message.`);
516
- }
517
- return trimmedText;
518
- }
519
- shouldPreserveRawInput(text) {
520
- return text.trim().startsWith("/");
521
- }
522
- formatUserMessage(text, userName, now = new Date()) {
523
- const pad = (n) => n.toString().padStart(2, "0");
524
- const offset = -now.getTimezoneOffset();
525
- const offsetSign = offset >= 0 ? "+" : "-";
526
- const offsetHours = pad(Math.floor(Math.abs(offset) / 60));
527
- const offsetMins = pad(Math.abs(offset) % 60);
528
- const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}${offsetSign}${offsetHours}:${offsetMins}`;
529
- return `[${timestamp}] [${userName || "unknown"}]: ${text}`;
530
- }
531
- async queueBusyMessage(delivery, text, userName) {
532
- if (!this.session.isStreaming) {
533
- throw new Error("No task is currently running.");
534
- }
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), {
540
- streamingBehavior: delivery,
541
- });
542
- }
543
- resetRunState(ctx, store) {
544
- this.runState = createEmptyRunState();
545
- this.runState.ctx = ctx;
546
- this.runState.logCtx = {
547
- channelId: ctx.message.channel,
548
- userName: ctx.message.userName,
549
- channelName: ctx.channelName,
550
- };
551
- this.runState.store = store;
552
- }
553
- async refreshSessionResources() {
554
- await this.ensureSessionReady();
555
- const skills = loadPipiclawSkills(this.channelDir, this.workspacePath);
556
- this.currentSkills = skills;
557
- this.subAgentDiscovery = this.refreshSubAgentDiscovery();
558
- await this.session.reload();
559
- }
560
- async initializeSession() {
561
- const skills = loadPipiclawSkills(this.channelDir, this.workspacePath);
562
- this.currentSkills = skills;
563
- this.subAgentDiscovery = this.refreshSubAgentDiscovery();
564
- await this.session.reload();
565
- }
566
- async ensureSessionReady() {
567
- await this.sessionReady;
568
- }
569
- refreshSubAgentDiscovery() {
570
- this.modelRegistry.refresh();
571
- const discovery = discoverSubAgents(this.workspaceDir, this.modelRegistry.getAvailable());
572
- for (const warning of discovery.warnings) {
573
- log.logWarning(`Sub-agent config warning (${this.channelId})`, warning);
574
- }
575
- return discovery;
576
- }
577
- // === Session event subscription ===
578
- subscribeToSessionEvents() {
579
- this.session.subscribe(async (event) => {
580
- if (!this.runState.ctx || !this.runState.logCtx || !this.runState.queue)
581
- return;
582
- const { ctx, logCtx, queue, pendingTools, store } = this.runState;
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,
588
- startTime: Date.now(),
589
- });
590
- this.memoryLifecycle.noteToolCall();
591
- log.logToolStart(logCtx, event.toolName, label, isRecord(event.args) ? event.args : {});
592
- queue.enqueue(() => ctx.respond(formatProgressEntry("tool", label), false), "tool label");
593
- }
594
- else if (isToolExecutionUpdateEvent(event)) {
595
- if (event.toolName !== "subagent") {
596
- return;
597
- }
598
- const partialText = truncate(extractToolResultText(event.partialResult), 200);
599
- if (!partialText.trim()) {
600
- return;
601
- }
602
- queue.enqueue(() => ctx.respond(formatProgressEntry("tool", partialText), false), "tool update");
603
- }
604
- else if (isToolExecutionEndEvent(event)) {
605
- const resultStr = extractToolResultText(event.result);
606
- const pending = pendingTools.get(event.toolCallId);
607
- pendingTools.delete(event.toolCallId);
608
- const durationMs = pending ? Date.now() - pending.startTime : 0;
609
- const subAgentDetails = event.toolName === "subagent" &&
610
- isRecord(event.result) &&
611
- "details" in event.result &&
612
- isSubAgentToolDetails(event.result.details)
613
- ? event.result.details
614
- : null;
615
- if (subAgentDetails) {
616
- mergeSubAgentUsage(this.runState.totalUsage, subAgentDetails);
617
- const label = pending?.args &&
618
- typeof pending.args === "object" &&
619
- "label" in pending.args &&
620
- typeof pending.args.label === "string"
621
- ? (pending.args.label ?? "subagent").trim()
622
- : "subagent";
623
- queue.enqueue(() => store?.logSubAgentRun(logCtx.channelId, {
624
- date: new Date().toISOString(),
625
- toolCallId: event.toolCallId,
626
- label,
627
- agent: subAgentDetails.agent,
628
- source: subAgentDetails.source,
629
- model: subAgentDetails.model,
630
- tools: [...subAgentDetails.tools],
631
- turns: subAgentDetails.turns,
632
- toolCalls: subAgentDetails.toolCalls,
633
- durationMs: subAgentDetails.durationMs,
634
- failed: subAgentDetails.failed,
635
- failureReason: subAgentDetails.failureReason,
636
- output: resultStr.length > 16000 ? resultStr.slice(0, 16000) : resultStr,
637
- outputTruncated: resultStr.length > 16000,
638
- usage: {
639
- ...subAgentDetails.usage,
640
- cost: { ...subAgentDetails.usage.cost },
641
- },
642
- }) ?? Promise.resolve(), "sub-agent run log");
643
- }
644
- const treatAsError = event.isError || Boolean(subAgentDetails?.failed);
645
- if (treatAsError) {
646
- log.logToolError(logCtx, event.toolName, durationMs, resultStr);
647
- }
648
- else {
649
- log.logToolSuccess(logCtx, event.toolName, durationMs, resultStr);
650
- }
651
- if (treatAsError) {
652
- queue.enqueue(() => ctx.respond(formatProgressEntry("error", truncate(resultStr, 200)), false), "tool error");
653
- }
654
- }
655
- else if (isMessageStartEvent(event)) {
656
- if (isAssistantEventMessage(event.message)) {
657
- log.logResponseStart(logCtx);
658
- }
659
- }
660
- else if (isMessageEndEvent(event)) {
661
- const commandResultText = extractCustomCommandResultText(event.message);
662
- if (commandResultText) {
663
- this.runState.finalOutcome = { kind: "final", text: commandResultText };
664
- log.logResponse(logCtx, commandResultText);
665
- queue.enqueue(async () => {
666
- const delivered = await ctx.respondPlain(commandResultText);
667
- if (!delivered) {
668
- await ctx.replaceMessage(commandResultText);
669
- }
670
- this.runState.finalResponseDelivered = true;
671
- }, "command result");
672
- return;
673
- }
674
- if (isAssistantEventMessage(event.message)) {
675
- const assistantMsg = event.message;
676
- if (assistantMsg.stopReason) {
677
- this.runState.stopReason = assistantMsg.stopReason;
678
- }
679
- if (assistantMsg.errorMessage) {
680
- this.runState.errorMessage = assistantMsg.errorMessage;
681
- }
682
- if (assistantMsg.usage) {
683
- this.runState.totalUsage.input += assistantMsg.usage.input;
684
- this.runState.totalUsage.output += assistantMsg.usage.output;
685
- this.runState.totalUsage.cacheRead += assistantMsg.usage.cacheRead;
686
- this.runState.totalUsage.cacheWrite += assistantMsg.usage.cacheWrite;
687
- this.runState.totalUsage.cost.input += assistantMsg.usage.cost.input;
688
- this.runState.totalUsage.cost.output += assistantMsg.usage.cost.output;
689
- this.runState.totalUsage.cost.cacheRead += assistantMsg.usage.cost.cacheRead;
690
- this.runState.totalUsage.cost.cacheWrite += assistantMsg.usage.cost.cacheWrite;
691
- this.runState.totalUsage.cost.total += assistantMsg.usage.cost.total;
692
- }
693
- const content = assistantMsg.content;
694
- const thinkingParts = [];
695
- const textParts = [];
696
- let hasToolCalls = false;
697
- for (const part of content) {
698
- if (isThinkingPart(part)) {
699
- thinkingParts.push(part.thinking);
700
- }
701
- else if (isTextPart(part)) {
702
- textParts.push(part.text);
703
- }
704
- else if (part.type === "toolCall") {
705
- hasToolCalls = true;
706
- }
707
- }
708
- const text = textParts.join("\n");
709
- for (const thinking of thinkingParts) {
710
- log.logThinking(logCtx, thinking);
711
- queue.enqueue(() => ctx.respond(formatProgressEntry("thinking", thinking), false), "thinking");
712
- }
713
- if (hasToolCalls && text.trim()) {
714
- queue.enqueue(() => ctx.respond(formatProgressEntry("assistant", text), false), "assistant progress");
715
- }
716
- }
717
- }
718
- else if (isTurnEndEvent(event)) {
719
- if (isAssistantEventMessage(event.message) && event.toolResults.length === 0) {
720
- if (event.message.stopReason === "error" || event.message.stopReason === "aborted") {
721
- return;
722
- }
723
- const finalText = event.message.content
724
- .filter((part) => part.type === "text" && !!part.text)
725
- .map((part) => part.text)
726
- .join("\n");
727
- const trimmedFinalText = finalText.trim();
728
- if (!trimmedFinalText) {
729
- return;
730
- }
731
- if (trimmedFinalText === "[SILENT]" || trimmedFinalText.startsWith("[SILENT]")) {
732
- this.runState.finalOutcome = { kind: "silent" };
733
- this.memoryLifecycle.noteCompletedAssistantTurn();
734
- return;
735
- }
736
- if (this.runState.finalOutcome.kind === "final" &&
737
- this.runState.finalOutcome.text.trim() === trimmedFinalText) {
738
- return;
739
- }
740
- this.runState.finalOutcome = { kind: "final", text: finalText };
741
- this.memoryLifecycle.noteCompletedAssistantTurn();
742
- log.logResponse(logCtx, finalText);
743
- queue.enqueue(async () => {
744
- const delivered = await ctx.respondPlain(finalText);
745
- if (delivered) {
746
- this.runState.finalResponseDelivered = true;
747
- }
748
- }, "final response");
749
- }
750
- }
751
- else if (isAutoCompactionStartEvent(event)) {
752
- log.logInfo(`Auto-compaction started (reason: ${event.reason})`);
753
- queue.enqueue(() => ctx.respond(formatProgressEntry("assistant", "Compacting context..."), false), "compaction start");
754
- }
755
- else if (isAutoCompactionEndEvent(event)) {
756
- if (event.result) {
757
- log.logInfo(`Auto-compaction complete: ${event.result.tokensBefore} tokens compacted`);
758
- }
759
- else if (event.aborted) {
760
- log.logInfo("Auto-compaction aborted");
761
- }
762
- }
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");
766
- }
767
- });
768
- }
769
- }
770
- // ============================================================================
771
- // Factory
772
- // ============================================================================
773
- const channelRunners = new Map();
774
- export function getOrCreateRunner(sandboxConfig, channelId, channelDir) {
775
- const existing = channelRunners.get(channelId);
776
- if (existing)
777
- return existing;
778
- const runner = new ChannelRunner(sandboxConfig, channelId, channelDir);
779
- channelRunners.set(channelId, runner);
780
- return runner;
781
- }
782
- //# sourceMappingURL=agent.js.map
1
+ export { ChannelRunner, getOrCreateRunner } from "./agent/index.js";