@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
@@ -0,0 +1,47 @@
1
+ import { type BuiltInCommand } from "../commands.js";
2
+ import type { DingTalkContext } from "../runtime/dingtalk.js";
3
+ import type { ChannelStore } from "../runtime/store.js";
4
+ import { type SandboxConfig } from "../sandbox.js";
5
+ import { type AgentRunner } from "./types.js";
6
+ export declare class ChannelRunner implements AgentRunner {
7
+ private readonly sandboxConfig;
8
+ private readonly channelId;
9
+ private readonly channelDir;
10
+ private readonly workspacePath;
11
+ private readonly workspaceDir;
12
+ private readonly session;
13
+ private readonly agent;
14
+ private readonly sessionManager;
15
+ private readonly settingsManager;
16
+ private readonly modelRegistry;
17
+ private readonly memoryLifecycle;
18
+ private readonly sessionResourceGate;
19
+ private readonly sessionReady;
20
+ private subAgentDiscovery;
21
+ private activeModel;
22
+ private currentSkills;
23
+ private firstTurnMemoryBootstrapPending;
24
+ private runState;
25
+ constructor(sandboxConfig: SandboxConfig, channelId: string, channelDir: string);
26
+ run(ctx: DingTalkContext, store: ChannelStore): Promise<{
27
+ stopReason: string;
28
+ errorMessage?: string;
29
+ }>;
30
+ handleBuiltinCommand(ctx: DingTalkContext, command: BuiltInCommand): Promise<void>;
31
+ queueSteer(text: string, userName?: string): Promise<void>;
32
+ queueFollowUp(text: string, userName?: string): Promise<void>;
33
+ abort(): Promise<void>;
34
+ private sendCommandReply;
35
+ private requireQueuedMessage;
36
+ private shouldPreserveRawInput;
37
+ private formatUserMessage;
38
+ private queueBusyMessage;
39
+ private resetRunState;
40
+ private refreshSessionResources;
41
+ private initializeSession;
42
+ private reloadSessionResources;
43
+ private ensureSessionReady;
44
+ private refreshSubAgentDiscovery;
45
+ private subscribeToSessionEvents;
46
+ private buildFirstTurnMemoryBootstrap;
47
+ }
@@ -0,0 +1,441 @@
1
+ import { Agent } from "@mariozechner/pi-agent-core";
2
+ import { AgentSession, AuthStorage, convertToLlm, DefaultResourceLoader, ModelRegistry, SessionManager, } from "@mariozechner/pi-coding-agent";
3
+ import { mkdir, readFile, writeFile } from "fs/promises";
4
+ import { dirname, join, resolve } from "path";
5
+ import { 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 { buildFirstTurnMemoryBootstrap as renderFirstTurnMemoryBootstrap } from "../memory/bootstrap.js";
11
+ import { createMemoryCandidateCache } from "../memory/candidates.js";
12
+ import { getChannelMemoryPath } from "../memory/files.js";
13
+ import { MemoryLifecycle } from "../memory/lifecycle.js";
14
+ import { recallRelevantMemory } from "../memory/recall.js";
15
+ import { resolveInitialModel } from "../model-utils.js";
16
+ import { APP_HOME_DIR, AUTH_CONFIG_PATH, MODELS_CONFIG_PATH } from "../paths.js";
17
+ import { buildAppendSystemPrompt } from "../prompt-builder.js";
18
+ import { createExecutor } from "../sandbox.js";
19
+ import { HAN_REGEX } from "../shared/text-utils.js";
20
+ import { isRecord } from "../shared/type-guards.js";
21
+ import { discoverSubAgents, formatSubAgentList } from "../subagents/discovery.js";
22
+ import { createPipiclawTools } from "../tools/index.js";
23
+ import { clipUserInput } from "./progress-formatter.js";
24
+ import { createRunQueue } from "./run-queue.js";
25
+ import { handleSessionEvent } from "./session-events.js";
26
+ import { SessionResourceGate } from "./session-resource-gate.js";
27
+ import { getLastAssistantUsage } from "./type-guards.js";
28
+ import { createEmptyRunState, MAX_USER_MESSAGE_CHARS, } from "./types.js";
29
+ function isSilentOutcome(outcome) {
30
+ return outcome.kind === "silent";
31
+ }
32
+ function isFinalOutcome(outcome) {
33
+ return outcome.kind === "final";
34
+ }
35
+ function getFinalOutcomeText(outcome) {
36
+ return isFinalOutcome(outcome) ? outcome.text : null;
37
+ }
38
+ function createModelRegistry(authStorage, modelsJsonPath) {
39
+ const registryClass = ModelRegistry;
40
+ return typeof registryClass.create === "function"
41
+ ? registryClass.create(authStorage, modelsJsonPath)
42
+ : new registryClass(authStorage, modelsJsonPath);
43
+ }
44
+ function asSdkSettingsManager(manager) {
45
+ return manager;
46
+ }
47
+ export class ChannelRunner {
48
+ constructor(sandboxConfig, channelId, channelDir) {
49
+ this.firstTurnMemoryBootstrapPending = true;
50
+ // --- Per run ---
51
+ this.runState = createEmptyRunState();
52
+ this.sandboxConfig = sandboxConfig;
53
+ this.channelId = channelId;
54
+ this.channelDir = channelDir;
55
+ const executor = createExecutor(sandboxConfig);
56
+ this.workspaceDir = resolve(dirname(channelDir));
57
+ this.workspacePath = executor.getWorkspacePath(this.workspaceDir);
58
+ // Initial skill summaries
59
+ const initialSkills = loadPipiclawSkills(channelDir, this.workspacePath);
60
+ this.currentSkills = initialSkills;
61
+ // Create session manager
62
+ const contextFile = join(channelDir, "context.jsonl");
63
+ this.sessionManager = SessionManager.open(contextFile, channelDir);
64
+ this.settingsManager = new PipiclawSettingsManager(APP_HOME_DIR);
65
+ // Create AuthStorage and ModelRegistry
66
+ const authStorage = AuthStorage.create(AUTH_CONFIG_PATH);
67
+ this.modelRegistry = createModelRegistry(authStorage, MODELS_CONFIG_PATH);
68
+ // Resolve model: prefer saved global default, fall back to first available model
69
+ this.activeModel = resolveInitialModel(this.modelRegistry, this.settingsManager);
70
+ log.logInfo(`Using model: ${this.activeModel.provider}/${this.activeModel.id} (${this.activeModel.name})`);
71
+ this.subAgentDiscovery = this.refreshSubAgentDiscovery();
72
+ // Create tools
73
+ const tools = createPipiclawTools({
74
+ executor,
75
+ getCurrentModel: () => this.activeModel,
76
+ getAvailableModels: () => this.modelRegistry.getAvailable(),
77
+ resolveApiKey: async (model) => getApiKeyForModel(this.modelRegistry, model),
78
+ workspaceDir: this.workspaceDir,
79
+ channelDir: this.channelDir,
80
+ workspacePath: this.workspacePath,
81
+ channelId: this.channelId,
82
+ sandboxConfig: this.sandboxConfig,
83
+ getSubAgentDiscovery: () => this.subAgentDiscovery,
84
+ getMemoryRecallSettings: () => this.settingsManager.getMemoryRecallSettings(),
85
+ });
86
+ // Create agent
87
+ this.agent = new Agent({
88
+ initialState: {
89
+ systemPrompt: "",
90
+ model: this.activeModel,
91
+ thinkingLevel: "off",
92
+ tools,
93
+ },
94
+ convertToLlm,
95
+ getApiKey: async () => getApiKeyForModel(this.modelRegistry, this.activeModel),
96
+ });
97
+ this.memoryLifecycle = new MemoryLifecycle({
98
+ channelId: this.channelId,
99
+ channelDir: this.channelDir,
100
+ getMessages: () => this.session.messages,
101
+ getSessionEntries: () => this.sessionManager.getBranch(),
102
+ getModel: () => this.session.model ?? this.activeModel,
103
+ resolveApiKey: async (model) => getApiKeyForModel(this.modelRegistry, model),
104
+ getSessionMemorySettings: () => this.settingsManager.getSessionMemorySettings(),
105
+ });
106
+ const resourceLoader = new DefaultResourceLoader({
107
+ cwd: process.cwd(),
108
+ agentDir: APP_HOME_DIR,
109
+ settingsManager: asSdkSettingsManager(this.settingsManager),
110
+ extensionFactories: [
111
+ this.memoryLifecycle.createExtensionFactory(),
112
+ createCommandExtension({
113
+ getCurrentModel: () => this.session.model ?? this.activeModel,
114
+ getAvailableModels: async () => {
115
+ this.modelRegistry.refresh();
116
+ return await this.modelRegistry.getAvailable();
117
+ },
118
+ getSessionStats: () => this.session.getSessionStats(),
119
+ getThinkingLevel: () => this.session.thinkingLevel,
120
+ switchModel: async (model) => {
121
+ await this.session.setModel(model);
122
+ this.activeModel = model;
123
+ },
124
+ refreshSessionResources: async () => {
125
+ await this.refreshSessionResources();
126
+ },
127
+ }),
128
+ ],
129
+ appendSystemPromptOverride: (base) => {
130
+ const soul = getSoul(this.workspaceDir);
131
+ const sections = [...base];
132
+ if (soul) {
133
+ sections.unshift(soul);
134
+ }
135
+ sections.push(buildAppendSystemPrompt(this.workspacePath, this.channelId, this.sandboxConfig, {
136
+ subAgentList: formatSubAgentList(this.subAgentDiscovery.agents),
137
+ }));
138
+ return sections;
139
+ },
140
+ agentsFilesOverride: () => {
141
+ const agentConfig = getAgentConfig(this.channelDir);
142
+ return {
143
+ agentsFiles: agentConfig ? [{ path: `${this.workspacePath}/AGENTS.md`, content: agentConfig }] : [],
144
+ };
145
+ },
146
+ skillsOverride: (base) => ({
147
+ skills: [...base.skills, ...this.currentSkills],
148
+ diagnostics: base.diagnostics,
149
+ }),
150
+ });
151
+ const baseToolsOverride = Object.fromEntries(tools.map((tool) => [tool.name, tool]));
152
+ // Create AgentSession
153
+ this.session = new AgentSession({
154
+ agent: this.agent,
155
+ sessionManager: this.sessionManager,
156
+ settingsManager: asSdkSettingsManager(this.settingsManager),
157
+ cwd: process.cwd(),
158
+ modelRegistry: this.modelRegistry,
159
+ resourceLoader,
160
+ baseToolsOverride,
161
+ });
162
+ this.sessionResourceGate = new SessionResourceGate(async () => {
163
+ await this.reloadSessionResources();
164
+ });
165
+ // Subscribe to session events
166
+ this.subscribeToSessionEvents();
167
+ this.sessionReady = this.initializeSession();
168
+ }
169
+ // === Public API ===
170
+ async run(ctx, store) {
171
+ this.resetRunState(ctx, store);
172
+ const runQueue = createRunQueue(ctx);
173
+ this.runState.queue = runQueue.queue;
174
+ try {
175
+ await this.ensureSessionReady();
176
+ // Ensure channel directory exists
177
+ await mkdir(this.channelDir, { recursive: true });
178
+ const candidateCache = createMemoryCandidateCache();
179
+ const clippedInput = clipUserInput(ctx.message.text, MAX_USER_MESSAGE_CHARS);
180
+ const userMessage = this.formatUserMessage(clippedInput, ctx.message.userName);
181
+ let promptText = this.shouldPreserveRawInput(ctx.message.text) ? clippedInput : userMessage;
182
+ let recalledContextText = "";
183
+ let durableMemoryBootstrapText = "";
184
+ this.memoryLifecycle.noteUserTurnStarted();
185
+ if (!this.shouldPreserveRawInput(ctx.message.text)) {
186
+ const recallSettings = this.settingsManager.getMemoryRecallSettings();
187
+ if (recallSettings.enabled) {
188
+ const recall = await recallRelevantMemory({
189
+ query: clippedInput,
190
+ workspaceDir: this.workspaceDir,
191
+ channelDir: this.channelDir,
192
+ maxCandidates: recallSettings.maxCandidates,
193
+ maxInjected: recallSettings.maxInjected,
194
+ maxChars: recallSettings.maxChars,
195
+ rerankWithModel: recallSettings.rerankWithModel,
196
+ autoRerank: HAN_REGEX.test(clippedInput),
197
+ model: this.session.model ?? this.activeModel,
198
+ resolveApiKey: async (model) => getApiKeyForModel(this.modelRegistry, model),
199
+ candidateCache,
200
+ });
201
+ if (recall.renderedText) {
202
+ recalledContextText = recall.renderedText;
203
+ promptText = `${recall.renderedText}\n\n<user_message>\n${promptText}\n</user_message>`;
204
+ }
205
+ }
206
+ if (this.firstTurnMemoryBootstrapPending) {
207
+ durableMemoryBootstrapText = await this.buildFirstTurnMemoryBootstrap();
208
+ if (durableMemoryBootstrapText) {
209
+ promptText = `${durableMemoryBootstrapText}\n\n${promptText}`;
210
+ }
211
+ this.firstTurnMemoryBootstrapPending = false;
212
+ }
213
+ }
214
+ // Debug: write context to last_prompt.json (only with PIPICLAW_DEBUG=1)
215
+ if (process.env.PIPICLAW_DEBUG) {
216
+ const debugContext = {
217
+ systemPrompt: this.agent.state.systemPrompt,
218
+ messages: this.session.messages,
219
+ durableMemoryBootstrap: durableMemoryBootstrapText || undefined,
220
+ recalledContext: recalledContextText || undefined,
221
+ newUserMessage: promptText,
222
+ };
223
+ await writeFile(join(this.channelDir, "last_prompt.json"), JSON.stringify(debugContext, null, 2));
224
+ }
225
+ await this.sessionResourceGate.runPrompt(async () => {
226
+ await this.session.prompt(promptText);
227
+ });
228
+ }
229
+ catch (err) {
230
+ this.runState.stopReason = "error";
231
+ this.runState.errorMessage = err instanceof Error ? err.message : String(err);
232
+ log.logWarning(`[${this.channelId}] Runner failed`, this.runState.errorMessage);
233
+ }
234
+ finally {
235
+ await runQueue.drain();
236
+ const finalOutcome = this.runState.finalOutcome;
237
+ const finalOutcomeText = getFinalOutcomeText(finalOutcome);
238
+ try {
239
+ if (this.runState.stopReason === "error" &&
240
+ this.runState.errorMessage &&
241
+ !this.runState.finalResponseDelivered) {
242
+ try {
243
+ await ctx.replaceMessage("_Sorry, something went wrong_");
244
+ }
245
+ catch (err) {
246
+ const errMsg = err instanceof Error ? err.message : String(err);
247
+ log.logWarning("Failed to post error message", errMsg);
248
+ }
249
+ }
250
+ else if (isSilentOutcome(finalOutcome)) {
251
+ try {
252
+ await ctx.deleteMessage();
253
+ log.logInfo("Silent response - deleted message");
254
+ }
255
+ catch (err) {
256
+ const errMsg = err instanceof Error ? err.message : String(err);
257
+ log.logWarning("Failed to delete message for silent response", errMsg);
258
+ }
259
+ }
260
+ else if (finalOutcomeText && !this.runState.finalResponseDelivered) {
261
+ try {
262
+ await ctx.replaceMessage(finalOutcomeText);
263
+ }
264
+ catch (err) {
265
+ const errMsg = err instanceof Error ? err.message : String(err);
266
+ log.logWarning("Failed to replace message with final text", errMsg);
267
+ }
268
+ }
269
+ await ctx.flush();
270
+ }
271
+ finally {
272
+ await ctx.close();
273
+ }
274
+ // Log usage summary
275
+ if (this.runState.totalUsage.cost.total > 0) {
276
+ const lastAssistantMessage = getLastAssistantUsage(this.session.messages);
277
+ const contextTokens = lastAssistantMessage
278
+ ? lastAssistantMessage.usage.input +
279
+ lastAssistantMessage.usage.output +
280
+ lastAssistantMessage.usage.cacheRead +
281
+ lastAssistantMessage.usage.cacheWrite
282
+ : 0;
283
+ const currentRunModel = this.session.model ?? this.activeModel;
284
+ const contextWindow = currentRunModel.contextWindow || 200000;
285
+ log.logUsageSummary(this.runState.logCtx, this.runState.totalUsage, contextTokens, contextWindow);
286
+ }
287
+ // Clear run state
288
+ this.runState.ctx = null;
289
+ this.runState.logCtx = null;
290
+ this.runState.queue = null;
291
+ }
292
+ return { stopReason: this.runState.stopReason, errorMessage: this.runState.errorMessage };
293
+ }
294
+ async handleBuiltinCommand(ctx, command) {
295
+ try {
296
+ switch (command.name) {
297
+ case "help":
298
+ await this.sendCommandReply(ctx, renderBuiltInHelp());
299
+ return;
300
+ case "stop":
301
+ await this.sendCommandReply(ctx, "No task is running. Use `/stop` only while a task is running.");
302
+ return;
303
+ case "steer":
304
+ this.requireQueuedMessage(command.args, "steer");
305
+ await this.sendCommandReply(ctx, "No task is running. Send the message directly instead of using `/steer`.");
306
+ return;
307
+ case "followup":
308
+ this.requireQueuedMessage(command.args, "followup");
309
+ await this.sendCommandReply(ctx, "No task is running. Send the message directly now, or use `/followup` while a task is running.");
310
+ return;
311
+ }
312
+ }
313
+ catch (err) {
314
+ const errMsg = err instanceof Error ? err.message : String(err);
315
+ log.logWarning(`[${this.channelId}] Built-in command failed`, errMsg);
316
+ await this.sendCommandReply(ctx, `命令执行失败:${errMsg}`);
317
+ }
318
+ }
319
+ async queueSteer(text, userName) {
320
+ await this.queueBusyMessage("steer", this.requireQueuedMessage(text, "steer"), userName);
321
+ }
322
+ async queueFollowUp(text, userName) {
323
+ await this.queueBusyMessage("followUp", this.requireQueuedMessage(text, "followup"), userName);
324
+ }
325
+ async abort() {
326
+ await this.session.abort();
327
+ }
328
+ // === Private helpers ===
329
+ async sendCommandReply(ctx, text) {
330
+ const delivered = await ctx.respondPlain(text);
331
+ if (!delivered) {
332
+ await ctx.replaceMessage(text);
333
+ await ctx.flush();
334
+ }
335
+ }
336
+ requireQueuedMessage(text, commandName) {
337
+ const trimmedText = text.trim();
338
+ if (!trimmedText) {
339
+ throw new Error(`/${commandName} requires a message.`);
340
+ }
341
+ return trimmedText;
342
+ }
343
+ shouldPreserveRawInput(text) {
344
+ return text.trim().startsWith("/");
345
+ }
346
+ formatUserMessage(text, userName, now = new Date()) {
347
+ const pad = (n) => n.toString().padStart(2, "0");
348
+ const offset = -now.getTimezoneOffset();
349
+ const offsetSign = offset >= 0 ? "+" : "-";
350
+ const offsetHours = pad(Math.floor(Math.abs(offset) / 60));
351
+ const offsetMins = pad(Math.abs(offset) % 60);
352
+ const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}${offsetSign}${offsetHours}:${offsetMins}`;
353
+ return `[${timestamp}] [${userName || "unknown"}]: ${text}`;
354
+ }
355
+ async queueBusyMessage(delivery, text, userName) {
356
+ if (!this.session.isStreaming) {
357
+ throw new Error("No task is currently running.");
358
+ }
359
+ const clippedText = clipUserInput(text, MAX_USER_MESSAGE_CHARS);
360
+ if (clippedText !== text.trim()) {
361
+ log.logWarning(`[${this.channelId}] Queued message exceeded ${MAX_USER_MESSAGE_CHARS} chars and was clipped`);
362
+ }
363
+ await this.sessionResourceGate.runPrompt(async () => {
364
+ await this.session.prompt(this.formatUserMessage(clippedText, userName), {
365
+ streamingBehavior: delivery,
366
+ });
367
+ });
368
+ }
369
+ resetRunState(ctx, store) {
370
+ this.runState = createEmptyRunState();
371
+ this.runState.ctx = ctx;
372
+ this.runState.logCtx = {
373
+ channelId: ctx.message.channel,
374
+ userName: ctx.message.userName,
375
+ channelName: ctx.channelName,
376
+ };
377
+ this.runState.store = store;
378
+ }
379
+ async refreshSessionResources() {
380
+ await this.ensureSessionReady();
381
+ await this.sessionResourceGate.requestRefresh();
382
+ }
383
+ async initializeSession() {
384
+ await this.reloadSessionResources();
385
+ }
386
+ async reloadSessionResources() {
387
+ const skills = loadPipiclawSkills(this.channelDir, this.workspacePath);
388
+ this.currentSkills = skills;
389
+ this.subAgentDiscovery = this.refreshSubAgentDiscovery();
390
+ this.firstTurnMemoryBootstrapPending = true;
391
+ await this.session.reload();
392
+ }
393
+ async ensureSessionReady() {
394
+ await this.sessionReady;
395
+ }
396
+ refreshSubAgentDiscovery() {
397
+ this.modelRegistry.refresh();
398
+ const discovery = discoverSubAgents(this.workspaceDir, this.modelRegistry.getAvailable());
399
+ for (const warning of discovery.warnings) {
400
+ log.logWarning(`Sub-agent config warning (${this.channelId})`, warning);
401
+ }
402
+ return discovery;
403
+ }
404
+ // === Session event subscription ===
405
+ subscribeToSessionEvents() {
406
+ this.session.subscribe(async (event) => {
407
+ if (isRecord(event) && "reason" in event && event.reason === "new") {
408
+ this.firstTurnMemoryBootstrapPending = true;
409
+ }
410
+ if (!this.runState.ctx || !this.runState.logCtx || !this.runState.queue)
411
+ return;
412
+ await handleSessionEvent(event, {
413
+ ctx: this.runState.ctx,
414
+ logCtx: this.runState.logCtx,
415
+ queue: this.runState.queue,
416
+ pendingTools: this.runState.pendingTools,
417
+ store: this.runState.store,
418
+ runState: this.runState,
419
+ memoryLifecycle: this.memoryLifecycle,
420
+ });
421
+ });
422
+ }
423
+ async buildFirstTurnMemoryBootstrap() {
424
+ const readOptionalFile = async (path) => {
425
+ try {
426
+ return await readFile(path, "utf-8");
427
+ }
428
+ catch {
429
+ return "";
430
+ }
431
+ };
432
+ const [channelMemory, workspaceMemory] = await Promise.all([
433
+ readOptionalFile(getChannelMemoryPath(this.channelDir)),
434
+ readOptionalFile(join(this.workspaceDir, "MEMORY.md")),
435
+ ]);
436
+ return renderFirstTurnMemoryBootstrap({
437
+ channelMemory,
438
+ workspaceMemory,
439
+ });
440
+ }
441
+ }
@@ -0,0 +1,3 @@
1
+ export { ChannelRunner } from "./channel-runner.js";
2
+ export { getOrCreateRunner } from "./runner-factory.js";
3
+ export type { AgentRunner } from "./types.js";
@@ -0,0 +1,2 @@
1
+ export { ChannelRunner } from "./channel-runner.js";
2
+ export { getOrCreateRunner } from "./runner-factory.js";
@@ -0,0 +1,4 @@
1
+ import type { ProgressEntryKind } from "./types.js";
2
+ export declare function clipUserInput(text: string, maxChars: number): string;
3
+ export declare function formatProgressEntry(kind: ProgressEntryKind, text: string): string;
4
+ export declare function extractToolResultText(result: unknown): string;
@@ -0,0 +1,52 @@
1
+ function sanitizeProgressText(text) {
2
+ return text
3
+ .replace(/\uFFFC/g, "")
4
+ .replace(/\r/g, "")
5
+ .trim();
6
+ }
7
+ export function clipUserInput(text, maxChars) {
8
+ const normalized = text.replace(/\r/g, "").trim();
9
+ if (normalized.length <= maxChars) {
10
+ return normalized;
11
+ }
12
+ const headChars = Math.floor(maxChars * 0.6);
13
+ const tailChars = maxChars - headChars;
14
+ return `${normalized.slice(0, headChars)}\n\n[... omitted ${normalized.length - maxChars} chars ...]\n\n${normalized.slice(-tailChars)}`;
15
+ }
16
+ export function formatProgressEntry(kind, text) {
17
+ const cleaned = sanitizeProgressText(text);
18
+ if (!cleaned)
19
+ return "";
20
+ const normalized = cleaned.replace(/\n+/g, " ").trim();
21
+ switch (kind) {
22
+ case "tool":
23
+ return `Running: ${normalized}`;
24
+ case "thinking":
25
+ return `Thinking: ${normalized}`;
26
+ case "error":
27
+ return `Error: ${normalized}`;
28
+ case "assistant":
29
+ return normalized;
30
+ }
31
+ }
32
+ export function extractToolResultText(result) {
33
+ if (typeof result === "string") {
34
+ return result;
35
+ }
36
+ if (result &&
37
+ typeof result === "object" &&
38
+ "content" in result &&
39
+ Array.isArray(result.content)) {
40
+ const content = result.content;
41
+ const textParts = [];
42
+ for (const part of content) {
43
+ if (part.type === "text" && part.text) {
44
+ textParts.push(part.text);
45
+ }
46
+ }
47
+ if (textParts.length > 0) {
48
+ return textParts.join("\n");
49
+ }
50
+ }
51
+ return JSON.stringify(result);
52
+ }
@@ -0,0 +1,7 @@
1
+ import type { DingTalkContext } from "../runtime/dingtalk.js";
2
+ import type { RunQueue } from "./types.js";
3
+ export interface CreatedRunQueue {
4
+ queue: RunQueue;
5
+ drain: () => Promise<void>;
6
+ }
7
+ export declare function createRunQueue(ctx: DingTalkContext): CreatedRunQueue;
@@ -0,0 +1,26 @@
1
+ import * as log from "../log.js";
2
+ export function createRunQueue(ctx) {
3
+ let queueChain = Promise.resolve();
4
+ const queue = {
5
+ enqueue: (fn, errorContext) => {
6
+ queueChain = queueChain.then(async () => {
7
+ try {
8
+ await fn();
9
+ }
10
+ catch (err) {
11
+ const errMsg = err instanceof Error ? err.message : String(err);
12
+ log.logWarning(`DingTalk API error (${errorContext})`, errMsg);
13
+ }
14
+ });
15
+ },
16
+ enqueueMessage: function (text, target, errorContext, doLog = true) {
17
+ this.enqueue(() => (target === "main" ? ctx.respond(text, doLog) : ctx.respondInThread(text)), errorContext);
18
+ },
19
+ };
20
+ return {
21
+ queue,
22
+ drain: async () => {
23
+ await queueChain;
24
+ },
25
+ };
26
+ }
@@ -0,0 +1,3 @@
1
+ import type { SandboxConfig } from "../sandbox.js";
2
+ import type { AgentRunner } from "./types.js";
3
+ export declare function getOrCreateRunner(sandboxConfig: SandboxConfig, channelId: string, channelDir: string): AgentRunner;
@@ -0,0 +1,10 @@
1
+ import { ChannelRunner } from "./channel-runner.js";
2
+ const channelRunners = new Map();
3
+ export function getOrCreateRunner(sandboxConfig, channelId, channelDir) {
4
+ const existing = channelRunners.get(channelId);
5
+ if (existing)
6
+ return existing;
7
+ const runner = new ChannelRunner(sandboxConfig, channelId, channelDir);
8
+ channelRunners.set(channelId, runner);
9
+ return runner;
10
+ }
@@ -0,0 +1,14 @@
1
+ import type { MemoryLifecycle } from "../memory/lifecycle.js";
2
+ import type { DingTalkContext } from "../runtime/dingtalk.js";
3
+ import type { ChannelStore } from "../runtime/store.js";
4
+ import type { PendingTool, RunLogContext, RunQueue, RunState } from "./types.js";
5
+ export interface SessionEventHandlerContext {
6
+ ctx: DingTalkContext;
7
+ logCtx: RunLogContext;
8
+ queue: RunQueue;
9
+ pendingTools: Map<string, PendingTool>;
10
+ store: ChannelStore | null;
11
+ runState: RunState;
12
+ memoryLifecycle: MemoryLifecycle;
13
+ }
14
+ export declare function handleSessionEvent(event: unknown, context: SessionEventHandlerContext): Promise<void>;