@oyasmi/pipiclaw 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.d.ts +0 -4
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +441 -835
- package/dist/agent.js.map +1 -1
- package/dist/config-loader.d.ts +31 -0
- package/dist/config-loader.d.ts.map +1 -0
- package/dist/config-loader.js +146 -0
- package/dist/config-loader.js.map +1 -0
- package/dist/dingtalk.d.ts +2 -0
- package/dist/dingtalk.d.ts.map +1 -1
- package/dist/dingtalk.js +10 -0
- package/dist/dingtalk.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/model-utils.d.ts +11 -0
- package/dist/model-utils.d.ts.map +1 -0
- package/dist/model-utils.js +73 -0
- package/dist/model-utils.js.map +1 -0
- package/dist/prompt-builder.d.ts +4 -0
- package/dist/prompt-builder.d.ts.map +1 -0
- package/dist/prompt-builder.js +164 -0
- package/dist/prompt-builder.js.map +1 -0
- package/dist/sandbox.d.ts.map +1 -1
- package/dist/sandbox.js +1 -1
- package/dist/sandbox.js.map +1 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +1 -1
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/index.d.ts +1 -2
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +2 -9
- package/dist/tools/index.js.map +1 -1
- package/package.json +2 -2
package/dist/agent.js
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { Agent } from "@mariozechner/pi-agent-core";
|
|
2
|
-
import {
|
|
3
|
-
import { AgentSession, AuthStorage, convertToLlm, DefaultResourceLoader, formatSkillsForPrompt, loadSkillsFromDir, ModelRegistry, SessionManager, } from "@mariozechner/pi-coding-agent";
|
|
4
|
-
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { AgentSession, AuthStorage, convertToLlm, DefaultResourceLoader, ModelRegistry, SessionManager, } from "@mariozechner/pi-coding-agent";
|
|
5
3
|
import { mkdir, writeFile } from "fs/promises";
|
|
6
4
|
import { basename, join } from "path";
|
|
7
5
|
import { renderBuiltInHelp } from "./commands.js";
|
|
6
|
+
import { getAgentConfig, getApiKeyForModel, getMemory, getSoul, loadPipiclawSkills } from "./config-loader.js";
|
|
8
7
|
import { PipiclawSettingsManager, syncLogToSessionManager } from "./context.js";
|
|
9
8
|
import * as log from "./log.js";
|
|
9
|
+
import { findExactModelReferenceMatch, formatModelList, formatModelReference, resolveInitialModel, } from "./model-utils.js";
|
|
10
10
|
import { APP_HOME_DIR, AUTH_CONFIG_PATH, MODELS_CONFIG_PATH } from "./paths.js";
|
|
11
|
+
import { buildSystemPrompt } from "./prompt-builder.js";
|
|
11
12
|
import { createExecutor } from "./sandbox.js";
|
|
12
13
|
import { createPipiclawTools } from "./tools/index.js";
|
|
13
|
-
// Default model - will be overridden by ModelRegistry if custom models are configured
|
|
14
|
-
const defaultModel = getModel("anthropic", "claude-sonnet-4-5");
|
|
15
14
|
function isSilentOutcome(outcome) {
|
|
16
15
|
return outcome.kind === "silent";
|
|
17
16
|
}
|
|
@@ -21,372 +20,8 @@ function isFinalOutcome(outcome) {
|
|
|
21
20
|
function getFinalOutcomeText(outcome) {
|
|
22
21
|
return isFinalOutcome(outcome) ? outcome.text : null;
|
|
23
22
|
}
|
|
24
|
-
function formatModelReference(model) {
|
|
25
|
-
return `${model.provider}/${model.id}`;
|
|
26
|
-
}
|
|
27
|
-
function findExactModelReferenceMatch(modelReference, availableModels) {
|
|
28
|
-
const trimmedReference = modelReference.trim();
|
|
29
|
-
if (!trimmedReference) {
|
|
30
|
-
return { ambiguous: false };
|
|
31
|
-
}
|
|
32
|
-
const normalizedReference = trimmedReference.toLowerCase();
|
|
33
|
-
const canonicalMatches = availableModels.filter((model) => `${model.provider}/${model.id}`.toLowerCase() === normalizedReference);
|
|
34
|
-
if (canonicalMatches.length === 1) {
|
|
35
|
-
return { match: canonicalMatches[0], ambiguous: false };
|
|
36
|
-
}
|
|
37
|
-
if (canonicalMatches.length > 1) {
|
|
38
|
-
return { ambiguous: true };
|
|
39
|
-
}
|
|
40
|
-
const slashIndex = trimmedReference.indexOf("/");
|
|
41
|
-
if (slashIndex !== -1) {
|
|
42
|
-
const provider = trimmedReference.substring(0, slashIndex).trim();
|
|
43
|
-
const modelId = trimmedReference.substring(slashIndex + 1).trim();
|
|
44
|
-
if (provider && modelId) {
|
|
45
|
-
const providerMatches = availableModels.filter((model) => model.provider.toLowerCase() === provider.toLowerCase() &&
|
|
46
|
-
model.id.toLowerCase() === modelId.toLowerCase());
|
|
47
|
-
if (providerMatches.length === 1) {
|
|
48
|
-
return { match: providerMatches[0], ambiguous: false };
|
|
49
|
-
}
|
|
50
|
-
if (providerMatches.length > 1) {
|
|
51
|
-
return { ambiguous: true };
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
const idMatches = availableModels.filter((model) => model.id.toLowerCase() === normalizedReference);
|
|
56
|
-
if (idMatches.length === 1) {
|
|
57
|
-
return { match: idMatches[0], ambiguous: false };
|
|
58
|
-
}
|
|
59
|
-
return { ambiguous: idMatches.length > 1 };
|
|
60
|
-
}
|
|
61
|
-
function formatModelList(models, currentModel, limit = 20) {
|
|
62
|
-
const refs = models
|
|
63
|
-
.slice()
|
|
64
|
-
.sort((a, b) => formatModelReference(a).localeCompare(formatModelReference(b)))
|
|
65
|
-
.map((model) => {
|
|
66
|
-
const ref = formatModelReference(model);
|
|
67
|
-
const marker = currentModel && currentModel.provider === model.provider && currentModel.id === model.id
|
|
68
|
-
? " (current)"
|
|
69
|
-
: "";
|
|
70
|
-
return `- \`${ref}\`${marker}`;
|
|
71
|
-
});
|
|
72
|
-
if (refs.length <= limit) {
|
|
73
|
-
return refs.join("\n");
|
|
74
|
-
}
|
|
75
|
-
return `${refs.slice(0, limit).join("\n")}\n- ... and ${refs.length - limit} more`;
|
|
76
|
-
}
|
|
77
|
-
function resolveInitialModel(modelRegistry, settingsManager) {
|
|
78
|
-
const savedProvider = settingsManager.getDefaultProvider();
|
|
79
|
-
const savedModelId = settingsManager.getDefaultModel();
|
|
80
|
-
const availableModels = modelRegistry.getAvailable();
|
|
81
|
-
if (savedProvider && savedModelId) {
|
|
82
|
-
const savedModel = modelRegistry.find(savedProvider, savedModelId);
|
|
83
|
-
if (savedModel &&
|
|
84
|
-
availableModels.some((model) => model.provider === savedModel.provider && model.id === savedModel.id)) {
|
|
85
|
-
return savedModel;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
if (availableModels.length > 0) {
|
|
89
|
-
return availableModels[0];
|
|
90
|
-
}
|
|
91
|
-
return defaultModel;
|
|
92
|
-
}
|
|
93
|
-
async function getApiKeyForModel(modelRegistry, model) {
|
|
94
|
-
const key = await modelRegistry.getApiKeyForProvider(model.provider);
|
|
95
|
-
if (key)
|
|
96
|
-
return key;
|
|
97
|
-
// Fallback: try anthropic env var
|
|
98
|
-
const envKey = process.env.ANTHROPIC_API_KEY;
|
|
99
|
-
if (envKey)
|
|
100
|
-
return envKey;
|
|
101
|
-
throw new Error(`No API key found for provider: ${model.provider}.\n\n` +
|
|
102
|
-
"Configure API key in ~/.pi/agent/models.json or set ANTHROPIC_API_KEY environment variable.");
|
|
103
|
-
}
|
|
104
|
-
// ============================================================================
|
|
105
|
-
// Configuration file loaders: SOUL.md, AGENTS.md, MEMORY.md
|
|
106
|
-
// ============================================================================
|
|
107
|
-
/**
|
|
108
|
-
* Load SOUL.md — defines the agent's identity, personality, and communication style.
|
|
109
|
-
* Only loaded from workspace root (global).
|
|
110
|
-
*/
|
|
111
|
-
function getSoul(workspaceDir) {
|
|
112
|
-
const soulPath = join(workspaceDir, "SOUL.md");
|
|
113
|
-
if (existsSync(soulPath)) {
|
|
114
|
-
try {
|
|
115
|
-
const content = readFileSync(soulPath, "utf-8").trim();
|
|
116
|
-
if (content)
|
|
117
|
-
return content;
|
|
118
|
-
}
|
|
119
|
-
catch (error) {
|
|
120
|
-
log.logWarning("Failed to read SOUL.md", `${soulPath}: ${error}`);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
return "";
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Load AGENTS.md — defines the agent's behavior instructions, capabilities, and constraints.
|
|
127
|
-
* Supports both global (workspace root) and channel-level override.
|
|
128
|
-
*/
|
|
129
|
-
function getAgentConfig(channelDir) {
|
|
130
|
-
const parts = [];
|
|
131
|
-
// Read workspace-level AGENTS.md (global)
|
|
132
|
-
const workspaceAgentPath = join(channelDir, "..", "AGENTS.md");
|
|
133
|
-
if (existsSync(workspaceAgentPath)) {
|
|
134
|
-
try {
|
|
135
|
-
const content = readFileSync(workspaceAgentPath, "utf-8").trim();
|
|
136
|
-
if (content) {
|
|
137
|
-
parts.push(content);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
catch (error) {
|
|
141
|
-
log.logWarning("Failed to read workspace AGENTS.md", `${workspaceAgentPath}: ${error}`);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
// Read channel-specific AGENTS.md (overrides/extends global)
|
|
145
|
-
const channelAgentPath = join(channelDir, "AGENTS.md");
|
|
146
|
-
if (existsSync(channelAgentPath)) {
|
|
147
|
-
try {
|
|
148
|
-
const content = readFileSync(channelAgentPath, "utf-8").trim();
|
|
149
|
-
if (content) {
|
|
150
|
-
parts.push(content);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
catch (error) {
|
|
154
|
-
log.logWarning("Failed to read channel AGENTS.md", `${channelAgentPath}: ${error}`);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
return parts.join("\n\n");
|
|
158
|
-
}
|
|
159
|
-
function getMemory(channelDir) {
|
|
160
|
-
const parts = [];
|
|
161
|
-
// Read workspace-level memory (shared across all channels)
|
|
162
|
-
const workspaceMemoryPath = join(channelDir, "..", "MEMORY.md");
|
|
163
|
-
if (existsSync(workspaceMemoryPath)) {
|
|
164
|
-
try {
|
|
165
|
-
const content = readFileSync(workspaceMemoryPath, "utf-8").trim();
|
|
166
|
-
if (content) {
|
|
167
|
-
parts.push(`### Global Workspace Memory\n${content}`);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
catch (error) {
|
|
171
|
-
log.logWarning("Failed to read workspace memory", `${workspaceMemoryPath}: ${error}`);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
// Read channel-specific memory
|
|
175
|
-
const channelMemoryPath = join(channelDir, "MEMORY.md");
|
|
176
|
-
if (existsSync(channelMemoryPath)) {
|
|
177
|
-
try {
|
|
178
|
-
const content = readFileSync(channelMemoryPath, "utf-8").trim();
|
|
179
|
-
if (content) {
|
|
180
|
-
parts.push(`### Channel-Specific Memory\n${content}`);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
catch (error) {
|
|
184
|
-
log.logWarning("Failed to read channel memory", `${channelMemoryPath}: ${error}`);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
if (parts.length === 0) {
|
|
188
|
-
return "(no working memory yet)";
|
|
189
|
-
}
|
|
190
|
-
const combined = parts.join("\n\n");
|
|
191
|
-
// Warn if memory is getting too large (consumes system prompt token budget)
|
|
192
|
-
if (combined.length > 5000) {
|
|
193
|
-
return `\u26a0\ufe0f Memory is large (${combined.length} chars). Consolidate: remove outdated entries, merge duplicates, tighten descriptions.\n\n${combined}`;
|
|
194
|
-
}
|
|
195
|
-
return combined;
|
|
196
|
-
}
|
|
197
|
-
function loadPipiclawSkills(channelDir, workspacePath) {
|
|
198
|
-
const skillMap = new Map();
|
|
199
|
-
const hostWorkspacePath = join(channelDir, "..");
|
|
200
|
-
const translatePath = (hostPath) => {
|
|
201
|
-
if (hostPath.startsWith(hostWorkspacePath)) {
|
|
202
|
-
return workspacePath + hostPath.slice(hostWorkspacePath.length);
|
|
203
|
-
}
|
|
204
|
-
return hostPath;
|
|
205
|
-
};
|
|
206
|
-
// Load workspace-level skills (global)
|
|
207
|
-
const workspaceSkillsDir = join(hostWorkspacePath, "skills");
|
|
208
|
-
for (const skill of loadSkillsFromDir({ dir: workspaceSkillsDir, source: "workspace" }).skills) {
|
|
209
|
-
skill.filePath = translatePath(skill.filePath);
|
|
210
|
-
skill.baseDir = translatePath(skill.baseDir);
|
|
211
|
-
skillMap.set(skill.name, skill);
|
|
212
|
-
}
|
|
213
|
-
// Load channel-specific skills
|
|
214
|
-
const channelSkillsDir = join(channelDir, "skills");
|
|
215
|
-
for (const skill of loadSkillsFromDir({ dir: channelSkillsDir, source: "channel" }).skills) {
|
|
216
|
-
skill.filePath = translatePath(skill.filePath);
|
|
217
|
-
skill.baseDir = translatePath(skill.baseDir);
|
|
218
|
-
skillMap.set(skill.name, skill);
|
|
219
|
-
}
|
|
220
|
-
return Array.from(skillMap.values());
|
|
221
|
-
}
|
|
222
23
|
// ============================================================================
|
|
223
|
-
//
|
|
224
|
-
// ============================================================================
|
|
225
|
-
function buildSystemPrompt(workspacePath, channelId, soul, agentConfig, memory, sandboxConfig, skills) {
|
|
226
|
-
const channelPath = `${workspacePath}/${channelId}`;
|
|
227
|
-
const isDocker = sandboxConfig.type === "docker";
|
|
228
|
-
const envDescription = isDocker
|
|
229
|
-
? `You are running inside a Docker container (Alpine Linux).
|
|
230
|
-
- Bash working directory: / (use cd or absolute paths)
|
|
231
|
-
- Install tools with: apk add <package>
|
|
232
|
-
- Your changes persist across sessions`
|
|
233
|
-
: `You are running directly on the host machine.
|
|
234
|
-
- Bash working directory: ${process.cwd()}
|
|
235
|
-
- Be careful with system modifications`;
|
|
236
|
-
// Build system prompt with configuration file layering:
|
|
237
|
-
// 1. SOUL.md (identity/personality)
|
|
238
|
-
// 2. Core instructions
|
|
239
|
-
// 3. AGENTS.md (behavior instructions)
|
|
240
|
-
// 4. Skills, Events, Memory
|
|
241
|
-
const sections = [];
|
|
242
|
-
// 1. SOUL.md — Agent identity
|
|
243
|
-
if (soul) {
|
|
244
|
-
sections.push(soul);
|
|
245
|
-
}
|
|
246
|
-
else {
|
|
247
|
-
sections.push("You are a DingTalk bot assistant. Be concise and helpful.");
|
|
248
|
-
}
|
|
249
|
-
// 2. Core instructions
|
|
250
|
-
sections.push(`## Context
|
|
251
|
-
- For current date/time, use: date
|
|
252
|
-
- You have access to previous conversation context including tool results from prior turns.
|
|
253
|
-
- For older history beyond your context, search log.jsonl (contains user messages and your final responses, but not tool results).
|
|
254
|
-
|
|
255
|
-
## Formatting
|
|
256
|
-
Use Markdown for formatting. DingTalk AI Card supports basic Markdown:
|
|
257
|
-
Bold: **text**, Italic: *text*, Code: \`code\`, Block: \`\`\`code\`\`\`, Links: [text](url)
|
|
258
|
-
|
|
259
|
-
## Environment
|
|
260
|
-
${envDescription}
|
|
261
|
-
|
|
262
|
-
## Workspace Layout
|
|
263
|
-
${workspacePath}/
|
|
264
|
-
├── SOUL.md # Your identity/personality (read-only)
|
|
265
|
-
├── AGENTS.md # Custom behavior instructions (read-only)
|
|
266
|
-
├── MEMORY.md # Global memory (all channels, you can read/write)
|
|
267
|
-
├── skills/ # Global CLI tools you create
|
|
268
|
-
├── events/ # Scheduled events
|
|
269
|
-
└── ${channelId}/ # This channel
|
|
270
|
-
├── AGENTS.md # Channel-specific instructions (read-only)
|
|
271
|
-
├── MEMORY.md # Channel-specific memory (you can read/write)
|
|
272
|
-
├── log.jsonl # Message history (no tool results)
|
|
273
|
-
├── scratch/ # Your working directory
|
|
274
|
-
└── skills/ # Channel-specific tools`);
|
|
275
|
-
// 3. AGENTS.md — User-defined instructions
|
|
276
|
-
if (agentConfig) {
|
|
277
|
-
sections.push(`## Agent Instructions\n${agentConfig}`);
|
|
278
|
-
}
|
|
279
|
-
// 4. Skills
|
|
280
|
-
sections.push(`## Skills (Custom CLI Tools)
|
|
281
|
-
You can create reusable CLI tools for recurring tasks (email, APIs, data processing, etc.).
|
|
282
|
-
|
|
283
|
-
### Creating Skills
|
|
284
|
-
Store in \`${workspacePath}/skills/<name>/\` (global) or \`${channelPath}/skills/<name>/\` (channel-specific).
|
|
285
|
-
Each skill directory needs a \`SKILL.md\` with YAML frontmatter:
|
|
286
|
-
|
|
287
|
-
\`\`\`markdown
|
|
288
|
-
---
|
|
289
|
-
name: skill-name
|
|
290
|
-
description: Short description of what this skill does
|
|
291
|
-
---
|
|
292
|
-
|
|
293
|
-
# Skill Name
|
|
294
|
-
|
|
295
|
-
Usage instructions, examples, etc.
|
|
296
|
-
Scripts are in: {baseDir}/
|
|
297
|
-
\`\`\`
|
|
298
|
-
|
|
299
|
-
\`name\` and \`description\` are required. Use \`{baseDir}\` as placeholder for the skill's directory path.
|
|
300
|
-
|
|
301
|
-
### Available Skills
|
|
302
|
-
${skills.length > 0 ? formatSkillsForPrompt(skills) : "(no skills installed yet)"}`);
|
|
303
|
-
// 5. Events
|
|
304
|
-
sections.push(`## Events
|
|
305
|
-
You can schedule events that wake you up at specific times or when external things happen. Events are JSON files in \`${workspacePath}/events/\`.
|
|
306
|
-
|
|
307
|
-
### Event Types
|
|
308
|
-
|
|
309
|
-
**Immediate** - Triggers as soon as harness sees the file.
|
|
310
|
-
\`\`\`json
|
|
311
|
-
{"type": "immediate", "channelId": "${channelId}", "text": "New event occurred"}
|
|
312
|
-
\`\`\`
|
|
313
|
-
|
|
314
|
-
**One-shot** - Triggers once at a specific time.
|
|
315
|
-
\`\`\`json
|
|
316
|
-
{"type": "one-shot", "channelId": "${channelId}", "text": "Reminder", "at": "2025-12-15T09:00:00+08:00"}
|
|
317
|
-
\`\`\`
|
|
318
|
-
|
|
319
|
-
**Periodic** - Triggers on a cron schedule.
|
|
320
|
-
\`\`\`json
|
|
321
|
-
{"type": "periodic", "channelId": "${channelId}", "text": "Check inbox", "schedule": "0 9 * * 1-5", "timezone": "${Intl.DateTimeFormat().resolvedOptions().timeZone}"}
|
|
322
|
-
\`\`\`
|
|
323
|
-
|
|
324
|
-
### Cron Format
|
|
325
|
-
\`minute hour day-of-month month day-of-week\`
|
|
326
|
-
|
|
327
|
-
### Creating Events
|
|
328
|
-
\`\`\`bash
|
|
329
|
-
cat > ${workspacePath}/events/reminder-$(date +%s).json << 'EOF'
|
|
330
|
-
{"type": "one-shot", "channelId": "${channelId}", "text": "Reminder text", "at": "2025-12-14T09:00:00+08:00"}
|
|
331
|
-
EOF
|
|
332
|
-
\`\`\`
|
|
333
|
-
|
|
334
|
-
### Silent Completion
|
|
335
|
-
For periodic events where there's nothing to report, respond with just \`[SILENT]\`. This deletes the status message. Use this to avoid spam when periodic checks find nothing.
|
|
336
|
-
|
|
337
|
-
### Limits
|
|
338
|
-
Maximum 5 events can be queued.`);
|
|
339
|
-
// 6. Memory
|
|
340
|
-
sections.push(`## Memory
|
|
341
|
-
Write to MEMORY.md files to persist context across conversations.
|
|
342
|
-
- Global (${workspacePath}/MEMORY.md): skills, preferences, project info
|
|
343
|
-
- Channel (${channelPath}/MEMORY.md): channel-specific decisions, ongoing work
|
|
344
|
-
|
|
345
|
-
### Guidelines
|
|
346
|
-
- Keep each MEMORY.md concise (target: under 50 lines)
|
|
347
|
-
- Use clear headers to organize entries (## Preferences, ## Projects, etc.)
|
|
348
|
-
- Remove outdated entries when they are no longer relevant
|
|
349
|
-
- Merge duplicate or redundant items
|
|
350
|
-
- Prefer structured formats (lists, key-value pairs) over prose
|
|
351
|
-
- Update when you learn something important or when asked to remember something
|
|
352
|
-
|
|
353
|
-
### Current Memory
|
|
354
|
-
${memory}`);
|
|
355
|
-
// 7. System Configuration Log
|
|
356
|
-
sections.push(`## System Configuration Log
|
|
357
|
-
Maintain ${workspacePath}/SYSTEM.md to log all environment modifications:
|
|
358
|
-
- Installed packages (apk add, npm install, pip install)
|
|
359
|
-
- Environment variables set
|
|
360
|
-
- Config files modified
|
|
361
|
-
- Skill dependencies installed
|
|
362
|
-
|
|
363
|
-
Update this file whenever you modify the environment.`);
|
|
364
|
-
// 8. Tools
|
|
365
|
-
sections.push(`## Tools
|
|
366
|
-
- bash: Run shell commands (primary tool). Install packages as needed.
|
|
367
|
-
- read: Read files
|
|
368
|
-
- write: Create/overwrite files
|
|
369
|
-
- edit: Surgical file edits
|
|
370
|
-
- attach: Share files (note: DingTalk file sharing is limited, output as text when possible)
|
|
371
|
-
|
|
372
|
-
Each tool requires a "label" parameter (shown to user).`);
|
|
373
|
-
// 9. Log Queries
|
|
374
|
-
sections.push(`## Log Queries (for older history)
|
|
375
|
-
Format: \`{"date":"...","ts":"...","user":"...","userName":"...","text":"...","isBot":false}\`
|
|
376
|
-
The log contains user messages and your final responses (not tool calls/results).
|
|
377
|
-
${isDocker ? "Install jq: apk add jq" : ""}
|
|
378
|
-
|
|
379
|
-
\`\`\`bash
|
|
380
|
-
# Recent messages
|
|
381
|
-
tail -30 log.jsonl | jq -c '{date: .date[0:19], user: (.userName // .user), text}'
|
|
382
|
-
|
|
383
|
-
# Search for specific topic
|
|
384
|
-
grep -i "topic" log.jsonl | jq -c '{date: .date[0:19], user: (.userName // .user), text}'
|
|
385
|
-
\`\`\``);
|
|
386
|
-
return sections.join("\n\n");
|
|
387
|
-
}
|
|
388
|
-
// ============================================================================
|
|
389
|
-
// Agent Runner
|
|
24
|
+
// Text helpers
|
|
390
25
|
// ============================================================================
|
|
391
26
|
function truncate(text, maxLen) {
|
|
392
27
|
if (text.length <= maxLen)
|
|
@@ -436,78 +71,8 @@ function extractToolResultText(result) {
|
|
|
436
71
|
}
|
|
437
72
|
return JSON.stringify(result);
|
|
438
73
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
export function getOrCreateRunner(sandboxConfig, channelId, channelDir) {
|
|
442
|
-
const existing = channelRunners.get(channelId);
|
|
443
|
-
if (existing)
|
|
444
|
-
return existing;
|
|
445
|
-
const runner = createRunner(sandboxConfig, channelId, channelDir);
|
|
446
|
-
channelRunners.set(channelId, runner);
|
|
447
|
-
return runner;
|
|
448
|
-
}
|
|
449
|
-
function createRunner(sandboxConfig, channelId, channelDir) {
|
|
450
|
-
const executor = createExecutor(sandboxConfig);
|
|
451
|
-
const workspacePath = executor.getWorkspacePath(channelDir.replace(`/${channelId}`, ""));
|
|
452
|
-
const workspaceDir = join(channelDir, "..");
|
|
453
|
-
// Create tools
|
|
454
|
-
const tools = createPipiclawTools(executor);
|
|
455
|
-
// Initial system prompt
|
|
456
|
-
const soul = getSoul(workspaceDir);
|
|
457
|
-
const agentConfig = getAgentConfig(channelDir);
|
|
458
|
-
const memory = getMemory(channelDir);
|
|
459
|
-
const initialSkills = loadPipiclawSkills(channelDir, workspacePath);
|
|
460
|
-
let currentSkills = initialSkills;
|
|
461
|
-
const systemPrompt = buildSystemPrompt(workspacePath, channelId, soul, agentConfig, memory, sandboxConfig, initialSkills);
|
|
462
|
-
// Create session manager
|
|
463
|
-
const contextFile = join(channelDir, "context.jsonl");
|
|
464
|
-
const sessionManager = SessionManager.open(contextFile, channelDir);
|
|
465
|
-
const settingsManager = new PipiclawSettingsManager(APP_HOME_DIR);
|
|
466
|
-
// Create AuthStorage and ModelRegistry
|
|
467
|
-
const authStorage = AuthStorage.create(AUTH_CONFIG_PATH);
|
|
468
|
-
const modelRegistry = new ModelRegistry(authStorage, MODELS_CONFIG_PATH);
|
|
469
|
-
// Resolve model: prefer saved global default, fall back to first available model
|
|
470
|
-
let activeModel = resolveInitialModel(modelRegistry, settingsManager);
|
|
471
|
-
log.logInfo(`Using model: ${activeModel.provider}/${activeModel.id} (${activeModel.name})`);
|
|
472
|
-
// Create agent
|
|
473
|
-
const agent = new Agent({
|
|
474
|
-
initialState: {
|
|
475
|
-
systemPrompt,
|
|
476
|
-
model: activeModel,
|
|
477
|
-
thinkingLevel: "off",
|
|
478
|
-
tools,
|
|
479
|
-
},
|
|
480
|
-
convertToLlm,
|
|
481
|
-
getApiKey: async () => getApiKeyForModel(modelRegistry, activeModel),
|
|
482
|
-
});
|
|
483
|
-
// Load existing messages
|
|
484
|
-
const loadedSession = sessionManager.buildSessionContext();
|
|
485
|
-
if (loadedSession.messages.length > 0) {
|
|
486
|
-
agent.replaceMessages(loadedSession.messages);
|
|
487
|
-
log.logInfo(`[${channelId}] Loaded ${loadedSession.messages.length} messages from context.jsonl`);
|
|
488
|
-
}
|
|
489
|
-
const resourceLoader = new DefaultResourceLoader({
|
|
490
|
-
cwd: process.cwd(),
|
|
491
|
-
agentDir: APP_HOME_DIR,
|
|
492
|
-
settingsManager: settingsManager,
|
|
493
|
-
skillsOverride: (base) => ({
|
|
494
|
-
skills: [...base.skills, ...currentSkills],
|
|
495
|
-
diagnostics: base.diagnostics,
|
|
496
|
-
}),
|
|
497
|
-
});
|
|
498
|
-
const baseToolsOverride = Object.fromEntries(tools.map((tool) => [tool.name, tool]));
|
|
499
|
-
// Create AgentSession
|
|
500
|
-
const session = new AgentSession({
|
|
501
|
-
agent,
|
|
502
|
-
sessionManager,
|
|
503
|
-
settingsManager: settingsManager,
|
|
504
|
-
cwd: process.cwd(),
|
|
505
|
-
modelRegistry,
|
|
506
|
-
resourceLoader,
|
|
507
|
-
baseToolsOverride,
|
|
508
|
-
});
|
|
509
|
-
// Mutable per-run state
|
|
510
|
-
const runState = {
|
|
74
|
+
function createEmptyRunState() {
|
|
75
|
+
return {
|
|
511
76
|
ctx: null,
|
|
512
77
|
logCtx: null,
|
|
513
78
|
queue: null,
|
|
@@ -524,450 +89,491 @@ function createRunner(sandboxConfig, channelId, channelDir) {
|
|
|
524
89
|
finalOutcome: { kind: "none" },
|
|
525
90
|
finalResponseDelivered: false,
|
|
526
91
|
};
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
92
|
+
}
|
|
93
|
+
// ============================================================================
|
|
94
|
+
// ChannelRunner
|
|
95
|
+
// ============================================================================
|
|
96
|
+
class ChannelRunner {
|
|
97
|
+
// --- Constructed once ---
|
|
98
|
+
sandboxConfig;
|
|
99
|
+
channelId;
|
|
100
|
+
channelDir;
|
|
101
|
+
workspacePath;
|
|
102
|
+
workspaceDir;
|
|
103
|
+
session;
|
|
104
|
+
agent;
|
|
105
|
+
sessionManager;
|
|
106
|
+
settingsManager;
|
|
107
|
+
modelRegistry;
|
|
108
|
+
// --- Mutable across runs ---
|
|
109
|
+
activeModel;
|
|
110
|
+
currentSkills;
|
|
111
|
+
// --- Per run ---
|
|
112
|
+
runState = createEmptyRunState();
|
|
113
|
+
constructor(sandboxConfig, channelId, channelDir) {
|
|
114
|
+
this.sandboxConfig = sandboxConfig;
|
|
115
|
+
this.channelId = channelId;
|
|
116
|
+
this.channelDir = channelDir;
|
|
117
|
+
const executor = createExecutor(sandboxConfig);
|
|
118
|
+
this.workspacePath = executor.getWorkspacePath(channelDir.replace(`/${channelId}`, ""));
|
|
119
|
+
this.workspaceDir = join(channelDir, "..");
|
|
120
|
+
// Create tools
|
|
121
|
+
const tools = createPipiclawTools(executor);
|
|
122
|
+
// Initial system prompt
|
|
123
|
+
const soul = getSoul(this.workspaceDir);
|
|
124
|
+
const agentConfig = getAgentConfig(channelDir);
|
|
125
|
+
const memory = getMemory(channelDir);
|
|
126
|
+
const initialSkills = loadPipiclawSkills(channelDir, this.workspacePath);
|
|
127
|
+
this.currentSkills = initialSkills;
|
|
128
|
+
const systemPrompt = buildSystemPrompt(this.workspacePath, channelId, soul, agentConfig, memory, sandboxConfig, initialSkills);
|
|
129
|
+
// Create session manager
|
|
130
|
+
const contextFile = join(channelDir, "context.jsonl");
|
|
131
|
+
this.sessionManager = SessionManager.open(contextFile, channelDir);
|
|
132
|
+
this.settingsManager = new PipiclawSettingsManager(APP_HOME_DIR);
|
|
133
|
+
// Create AuthStorage and ModelRegistry
|
|
134
|
+
const authStorage = AuthStorage.create(AUTH_CONFIG_PATH);
|
|
135
|
+
this.modelRegistry = new ModelRegistry(authStorage, MODELS_CONFIG_PATH);
|
|
136
|
+
// Resolve model: prefer saved global default, fall back to first available model
|
|
137
|
+
this.activeModel = resolveInitialModel(this.modelRegistry, this.settingsManager);
|
|
138
|
+
log.logInfo(`Using model: ${this.activeModel.provider}/${this.activeModel.id} (${this.activeModel.name})`);
|
|
139
|
+
// Create agent
|
|
140
|
+
this.agent = new Agent({
|
|
141
|
+
initialState: {
|
|
142
|
+
systemPrompt,
|
|
143
|
+
model: this.activeModel,
|
|
144
|
+
thinkingLevel: "off",
|
|
145
|
+
tools,
|
|
146
|
+
},
|
|
147
|
+
convertToLlm,
|
|
148
|
+
getApiKey: async () => getApiKeyForModel(this.modelRegistry, this.activeModel),
|
|
149
|
+
});
|
|
150
|
+
// Load existing messages
|
|
151
|
+
const loadedSession = this.sessionManager.buildSessionContext();
|
|
152
|
+
if (loadedSession.messages.length > 0) {
|
|
153
|
+
this.agent.replaceMessages(loadedSession.messages);
|
|
154
|
+
log.logInfo(`[${channelId}] Loaded ${loadedSession.messages.length} messages from context.jsonl`);
|
|
155
|
+
}
|
|
156
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
157
|
+
cwd: process.cwd(),
|
|
158
|
+
agentDir: APP_HOME_DIR,
|
|
159
|
+
settingsManager: this.settingsManager,
|
|
160
|
+
skillsOverride: (base) => ({
|
|
161
|
+
skills: [...base.skills, ...this.currentSkills],
|
|
162
|
+
diagnostics: base.diagnostics,
|
|
163
|
+
}),
|
|
164
|
+
});
|
|
165
|
+
const baseToolsOverride = Object.fromEntries(tools.map((tool) => [tool.name, tool]));
|
|
166
|
+
// Create AgentSession
|
|
167
|
+
this.session = new AgentSession({
|
|
168
|
+
agent: this.agent,
|
|
169
|
+
sessionManager: this.sessionManager,
|
|
170
|
+
settingsManager: this.settingsManager,
|
|
171
|
+
cwd: process.cwd(),
|
|
172
|
+
modelRegistry: this.modelRegistry,
|
|
173
|
+
resourceLoader,
|
|
174
|
+
baseToolsOverride,
|
|
175
|
+
});
|
|
176
|
+
// Subscribe to session events
|
|
177
|
+
this.subscribeToSessionEvents();
|
|
178
|
+
}
|
|
179
|
+
// === Public API ===
|
|
180
|
+
async run(ctx, _store) {
|
|
181
|
+
this.resetRunState(ctx);
|
|
182
|
+
// Create queue for this run
|
|
183
|
+
let queueChain = Promise.resolve();
|
|
184
|
+
this.runState.queue = {
|
|
185
|
+
enqueue: (fn, errorContext) => {
|
|
186
|
+
queueChain = queueChain.then(async () => {
|
|
187
|
+
try {
|
|
188
|
+
await fn();
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
192
|
+
log.logWarning(`DingTalk API error (${errorContext})`, errMsg);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
},
|
|
196
|
+
enqueueMessage: function (text, target, errorContext, doLog = true) {
|
|
197
|
+
this.enqueue(() => (target === "main" ? ctx.respond(text, doLog) : ctx.respondInThread(text)), errorContext);
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
try {
|
|
201
|
+
// Ensure channel directory exists
|
|
202
|
+
await mkdir(this.channelDir, { recursive: true });
|
|
203
|
+
// Update system prompt and runtime resources with fresh config
|
|
204
|
+
const soul = getSoul(this.workspaceDir);
|
|
205
|
+
const agentConfig = getAgentConfig(this.channelDir);
|
|
206
|
+
const memory = getMemory(this.channelDir);
|
|
207
|
+
const skills = loadPipiclawSkills(this.channelDir, this.workspacePath);
|
|
208
|
+
this.currentSkills = skills;
|
|
209
|
+
const systemPrompt = buildSystemPrompt(this.workspacePath, this.channelId, soul, agentConfig, memory, this.sandboxConfig, skills);
|
|
210
|
+
this.session.agent.setSystemPrompt(systemPrompt);
|
|
211
|
+
await this.session.reload();
|
|
212
|
+
// Sync messages from log.jsonl
|
|
213
|
+
const syncedCount = syncLogToSessionManager(this.sessionManager, this.channelDir, ctx.message.ts);
|
|
214
|
+
if (syncedCount > 0) {
|
|
215
|
+
log.logInfo(`[${this.channelId}] Synced ${syncedCount} messages from log.jsonl`);
|
|
216
|
+
}
|
|
217
|
+
// Reload messages from context.jsonl
|
|
218
|
+
const reloadedSession = this.sessionManager.buildSessionContext();
|
|
219
|
+
if (reloadedSession.messages.length > 0) {
|
|
220
|
+
this.agent.replaceMessages(reloadedSession.messages);
|
|
221
|
+
log.logInfo(`[${this.channelId}] Reloaded ${reloadedSession.messages.length} messages from context`);
|
|
222
|
+
}
|
|
223
|
+
// Log context info
|
|
224
|
+
log.logInfo(`Context sizes - system: ${systemPrompt.length} chars, memory: ${memory.length} chars`);
|
|
225
|
+
// Build user message with timestamp and username prefix
|
|
226
|
+
const now = new Date();
|
|
227
|
+
const pad = (n) => n.toString().padStart(2, "0");
|
|
228
|
+
const offset = -now.getTimezoneOffset();
|
|
229
|
+
const offsetSign = offset >= 0 ? "+" : "-";
|
|
230
|
+
const offsetHours = pad(Math.floor(Math.abs(offset) / 60));
|
|
231
|
+
const offsetMins = pad(Math.abs(offset) % 60);
|
|
232
|
+
const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}${offsetSign}${offsetHours}:${offsetMins}`;
|
|
233
|
+
const userMessage = `[${timestamp}] [${ctx.message.userName || "unknown"}]: ${ctx.message.text}`;
|
|
234
|
+
// Debug: write context to last_prompt.json (only with PIPICLAW_DEBUG=1)
|
|
235
|
+
if (process.env.PIPICLAW_DEBUG) {
|
|
236
|
+
const debugContext = {
|
|
237
|
+
systemPrompt,
|
|
238
|
+
messages: this.session.messages,
|
|
239
|
+
newUserMessage: userMessage,
|
|
240
|
+
};
|
|
241
|
+
await writeFile(join(this.channelDir, "last_prompt.json"), JSON.stringify(debugContext, null, 2));
|
|
242
|
+
}
|
|
243
|
+
await this.session.prompt(userMessage);
|
|
575
244
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
245
|
+
catch (err) {
|
|
246
|
+
this.runState.stopReason = "error";
|
|
247
|
+
this.runState.errorMessage = err instanceof Error ? err.message : String(err);
|
|
248
|
+
log.logWarning(`[${this.channelId}] Runner failed`, this.runState.errorMessage);
|
|
249
|
+
}
|
|
250
|
+
finally {
|
|
251
|
+
await queueChain;
|
|
252
|
+
const finalOutcome = this.runState.finalOutcome;
|
|
253
|
+
const finalOutcomeText = getFinalOutcomeText(finalOutcome);
|
|
254
|
+
try {
|
|
255
|
+
if (this.runState.stopReason === "error" &&
|
|
256
|
+
this.runState.errorMessage &&
|
|
257
|
+
!this.runState.finalResponseDelivered) {
|
|
258
|
+
try {
|
|
259
|
+
await ctx.replaceMessage("_Sorry, something went wrong_");
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
263
|
+
log.logWarning("Failed to post error message", errMsg);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
else if (isSilentOutcome(finalOutcome)) {
|
|
267
|
+
try {
|
|
268
|
+
await ctx.deleteMessage();
|
|
269
|
+
log.logInfo("Silent response - deleted message");
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
273
|
+
log.logWarning("Failed to delete message for silent response", errMsg);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
else if (finalOutcomeText && !this.runState.finalResponseDelivered) {
|
|
277
|
+
try {
|
|
278
|
+
await ctx.replaceMessage(finalOutcomeText);
|
|
279
|
+
}
|
|
280
|
+
catch (err) {
|
|
281
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
282
|
+
log.logWarning("Failed to replace message with final text", errMsg);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
await ctx.flush();
|
|
286
|
+
}
|
|
287
|
+
finally {
|
|
288
|
+
await ctx.close();
|
|
289
|
+
}
|
|
290
|
+
// Log usage summary
|
|
291
|
+
if (this.runState.totalUsage.cost.total > 0) {
|
|
292
|
+
const messages = this.session.messages;
|
|
293
|
+
const lastAssistantMessage = messages
|
|
294
|
+
.slice()
|
|
295
|
+
.reverse()
|
|
296
|
+
.find((m) => m.role === "assistant" && m.stopReason !== "aborted");
|
|
297
|
+
const contextTokens = lastAssistantMessage
|
|
298
|
+
? lastAssistantMessage.usage.input +
|
|
299
|
+
lastAssistantMessage.usage.output +
|
|
300
|
+
lastAssistantMessage.usage.cacheRead +
|
|
301
|
+
lastAssistantMessage.usage.cacheWrite
|
|
302
|
+
: 0;
|
|
303
|
+
const currentRunModel = this.session.model ?? this.activeModel;
|
|
304
|
+
const contextWindow = currentRunModel.contextWindow || 200000;
|
|
305
|
+
log.logUsageSummary(this.runState.logCtx, this.runState.totalUsage, contextTokens, contextWindow);
|
|
306
|
+
}
|
|
307
|
+
// Clear run state
|
|
308
|
+
this.runState.ctx = null;
|
|
309
|
+
this.runState.logCtx = null;
|
|
310
|
+
this.runState.queue = null;
|
|
583
311
|
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
${available}`);
|
|
588
|
-
};
|
|
589
|
-
const handleBuiltInCommand = async (ctx, command) => {
|
|
312
|
+
return { stopReason: this.runState.stopReason, errorMessage: this.runState.errorMessage };
|
|
313
|
+
}
|
|
314
|
+
async handleBuiltinCommand(ctx, command) {
|
|
590
315
|
try {
|
|
591
316
|
switch (command.name) {
|
|
592
317
|
case "help":
|
|
593
|
-
await sendCommandReply(ctx, renderBuiltInHelp());
|
|
318
|
+
await this.sendCommandReply(ctx, renderBuiltInHelp());
|
|
594
319
|
return;
|
|
595
320
|
case "new": {
|
|
596
|
-
const completed = await session.newSession();
|
|
597
|
-
await sendCommandReply(ctx, completed
|
|
598
|
-
? `已开启新会话。
|
|
599
|
-
|
|
600
|
-
Session ID: \`${session.sessionId}\``
|
|
601
|
-
: "新会话已取消。");
|
|
321
|
+
const completed = await this.session.newSession();
|
|
322
|
+
await this.sendCommandReply(ctx, completed ? `已开启新会话。\n\nSession ID: \`${this.session.sessionId}\`` : "新会话已取消。");
|
|
602
323
|
return;
|
|
603
324
|
}
|
|
604
325
|
case "compact": {
|
|
605
|
-
const result = await session.compact(command.args || undefined);
|
|
606
|
-
await sendCommandReply(ctx,
|
|
607
|
-
|
|
608
|
-
- Tokens before compaction: \`${result.tokensBefore}\`
|
|
609
|
-
- Summary:
|
|
610
|
-
|
|
611
|
-
\`\`\`text
|
|
612
|
-
${result.summary}
|
|
613
|
-
\`\`\``);
|
|
326
|
+
const result = await this.session.compact(command.args || undefined);
|
|
327
|
+
await this.sendCommandReply(ctx, `已压缩当前会话上下文。\n\n- Tokens before compaction: \`${result.tokensBefore}\`\n- Summary:\n\n\`\`\`text\n${result.summary}\n\`\`\``);
|
|
614
328
|
return;
|
|
615
329
|
}
|
|
616
330
|
case "session": {
|
|
617
|
-
const stats = session.getSessionStats();
|
|
618
|
-
const currentModel = session.model ? `\`${formatModelReference(session.model)}\`` : "(none)";
|
|
331
|
+
const stats = this.session.getSessionStats();
|
|
332
|
+
const currentModel = this.session.model ? `\`${formatModelReference(this.session.model)}\`` : "(none)";
|
|
619
333
|
const sessionFile = stats.sessionFile ? `\`${basename(stats.sessionFile)}\`` : "(none)";
|
|
620
|
-
await sendCommandReply(ctx, `# Session
|
|
621
|
-
|
|
622
|
-
- Session ID: \`${stats.sessionId}\`
|
|
623
|
-
- Session file: ${sessionFile}
|
|
624
|
-
- Model: ${currentModel}
|
|
625
|
-
- Thinking level: \`${session.thinkingLevel}\`
|
|
626
|
-
- User messages: \`${stats.userMessages}\`
|
|
627
|
-
- Assistant messages: \`${stats.assistantMessages}\`
|
|
628
|
-
- Tool calls: \`${stats.toolCalls}\`
|
|
629
|
-
- Tool results: \`${stats.toolResults}\`
|
|
630
|
-
- Total messages: \`${stats.totalMessages}\`
|
|
631
|
-
- Tokens: \`${stats.tokens.total}\` (input \`${stats.tokens.input}\`, output \`${stats.tokens.output}\`, cache read \`${stats.tokens.cacheRead}\`, cache write \`${stats.tokens.cacheWrite}\`)
|
|
632
|
-
- Cost: \`$${stats.cost.toFixed(4)}\``);
|
|
334
|
+
await this.sendCommandReply(ctx, `# Session\n\n- Session ID: \`${stats.sessionId}\`\n- Session file: ${sessionFile}\n- Model: ${currentModel}\n- Thinking level: \`${this.session.thinkingLevel}\`\n- User messages: \`${stats.userMessages}\`\n- Assistant messages: \`${stats.assistantMessages}\`\n- Tool calls: \`${stats.toolCalls}\`\n- Tool results: \`${stats.toolResults}\`\n- Total messages: \`${stats.totalMessages}\`\n- Tokens: \`${stats.tokens.total}\` (input \`${stats.tokens.input}\`, output \`${stats.tokens.output}\`, cache read \`${stats.tokens.cacheRead}\`, cache write \`${stats.tokens.cacheWrite}\`)\n- Cost: \`$${stats.cost.toFixed(4)}\``);
|
|
633
335
|
return;
|
|
634
336
|
}
|
|
635
337
|
case "model":
|
|
636
|
-
await
|
|
338
|
+
await this.handleModelCommand(ctx, command.args);
|
|
637
339
|
return;
|
|
638
340
|
case "stop":
|
|
639
|
-
await sendCommandReply(ctx, "No task is running. Use `/stop` only while a task is running.");
|
|
341
|
+
await this.sendCommandReply(ctx, "No task is running. Use `/stop` only while a task is running.");
|
|
640
342
|
return;
|
|
641
343
|
case "steer":
|
|
642
|
-
requireQueuedMessage(command.args, "steer");
|
|
643
|
-
await sendCommandReply(ctx, "No task is running. Send the message directly instead of using `/steer`.");
|
|
344
|
+
this.requireQueuedMessage(command.args, "steer");
|
|
345
|
+
await this.sendCommandReply(ctx, "No task is running. Send the message directly instead of using `/steer`.");
|
|
644
346
|
return;
|
|
645
347
|
case "followup":
|
|
646
|
-
requireQueuedMessage(command.args, "followup");
|
|
647
|
-
await sendCommandReply(ctx, "No task is running. Send the message directly now, or use `/followup` while a task is running.");
|
|
348
|
+
this.requireQueuedMessage(command.args, "followup");
|
|
349
|
+
await this.sendCommandReply(ctx, "No task is running. Send the message directly now, or use `/followup` while a task is running.");
|
|
648
350
|
return;
|
|
649
351
|
}
|
|
650
352
|
}
|
|
651
353
|
catch (err) {
|
|
652
354
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
653
|
-
log.logWarning(`[${channelId}] Built-in command failed`, errMsg);
|
|
654
|
-
await sendCommandReply(ctx, `命令执行失败:${errMsg}`);
|
|
355
|
+
log.logWarning(`[${this.channelId}] Built-in command failed`, errMsg);
|
|
356
|
+
await this.sendCommandReply(ctx, `命令执行失败:${errMsg}`);
|
|
655
357
|
}
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
queue.enqueue(() => ctx.respond(formatProgressEntry("tool", label), false), "tool label");
|
|
358
|
+
}
|
|
359
|
+
async queueSteer(text) {
|
|
360
|
+
await this.queueBusyMessage("steer", this.requireQueuedMessage(text, "steer"));
|
|
361
|
+
}
|
|
362
|
+
async queueFollowUp(text) {
|
|
363
|
+
await this.queueBusyMessage("followUp", this.requireQueuedMessage(text, "followup"));
|
|
364
|
+
}
|
|
365
|
+
abort() {
|
|
366
|
+
this.session.abort();
|
|
367
|
+
}
|
|
368
|
+
// === Private helpers ===
|
|
369
|
+
async sendCommandReply(ctx, text) {
|
|
370
|
+
const delivered = await ctx.respondPlain(text);
|
|
371
|
+
if (!delivered) {
|
|
372
|
+
await ctx.replaceMessage(text);
|
|
373
|
+
await ctx.flush();
|
|
673
374
|
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
const durationMs = pending ? Date.now() - pending.startTime : 0;
|
|
680
|
-
if (agentEvent.isError) {
|
|
681
|
-
log.logToolError(logCtx, agentEvent.toolName, durationMs, resultStr);
|
|
682
|
-
}
|
|
683
|
-
else {
|
|
684
|
-
log.logToolSuccess(logCtx, agentEvent.toolName, durationMs, resultStr);
|
|
685
|
-
}
|
|
686
|
-
if (agentEvent.isError) {
|
|
687
|
-
queue.enqueue(() => ctx.respond(formatProgressEntry("error", truncate(resultStr, 200)), false), "tool error");
|
|
688
|
-
}
|
|
375
|
+
}
|
|
376
|
+
requireQueuedMessage(text, commandName) {
|
|
377
|
+
const trimmedText = text.trim();
|
|
378
|
+
if (!trimmedText) {
|
|
379
|
+
throw new Error(`/${commandName} requires a message.`);
|
|
689
380
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
381
|
+
return trimmedText;
|
|
382
|
+
}
|
|
383
|
+
async queueBusyMessage(delivery, text) {
|
|
384
|
+
if (!this.session.isStreaming) {
|
|
385
|
+
throw new Error("No task is currently running.");
|
|
695
386
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
if (agentEvent.message.role === "assistant") {
|
|
699
|
-
const assistantMsg = agentEvent.message;
|
|
700
|
-
if (assistantMsg.stopReason) {
|
|
701
|
-
runState.stopReason = assistantMsg.stopReason;
|
|
702
|
-
}
|
|
703
|
-
if (assistantMsg.errorMessage) {
|
|
704
|
-
runState.errorMessage = assistantMsg.errorMessage;
|
|
705
|
-
}
|
|
706
|
-
if (assistantMsg.usage) {
|
|
707
|
-
runState.totalUsage.input += assistantMsg.usage.input;
|
|
708
|
-
runState.totalUsage.output += assistantMsg.usage.output;
|
|
709
|
-
runState.totalUsage.cacheRead += assistantMsg.usage.cacheRead;
|
|
710
|
-
runState.totalUsage.cacheWrite += assistantMsg.usage.cacheWrite;
|
|
711
|
-
runState.totalUsage.cost.input += assistantMsg.usage.cost.input;
|
|
712
|
-
runState.totalUsage.cost.output += assistantMsg.usage.cost.output;
|
|
713
|
-
runState.totalUsage.cost.cacheRead += assistantMsg.usage.cost.cacheRead;
|
|
714
|
-
runState.totalUsage.cost.cacheWrite += assistantMsg.usage.cost.cacheWrite;
|
|
715
|
-
runState.totalUsage.cost.total += assistantMsg.usage.cost.total;
|
|
716
|
-
}
|
|
717
|
-
const content = agentEvent.message.content;
|
|
718
|
-
const thinkingParts = [];
|
|
719
|
-
const textParts = [];
|
|
720
|
-
let hasToolCalls = false;
|
|
721
|
-
for (const part of content) {
|
|
722
|
-
if (part.type === "thinking") {
|
|
723
|
-
thinkingParts.push(part.thinking);
|
|
724
|
-
}
|
|
725
|
-
else if (part.type === "text") {
|
|
726
|
-
textParts.push(part.text);
|
|
727
|
-
}
|
|
728
|
-
else if (part.type === "toolCall") {
|
|
729
|
-
hasToolCalls = true;
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
const text = textParts.join("\n");
|
|
733
|
-
for (const thinking of thinkingParts) {
|
|
734
|
-
log.logThinking(logCtx, thinking);
|
|
735
|
-
queue.enqueue(() => ctx.respond(formatProgressEntry("thinking", thinking), false), "thinking");
|
|
736
|
-
}
|
|
737
|
-
if (hasToolCalls && text.trim()) {
|
|
738
|
-
queue.enqueue(() => ctx.respond(formatProgressEntry("assistant", text), false), "assistant progress");
|
|
739
|
-
}
|
|
740
|
-
}
|
|
387
|
+
if (delivery === "followUp") {
|
|
388
|
+
await this.session.followUp(text);
|
|
741
389
|
}
|
|
742
|
-
else
|
|
743
|
-
|
|
744
|
-
if (turnEvent.message.role === "assistant" && turnEvent.toolResults.length === 0) {
|
|
745
|
-
if (turnEvent.message.stopReason === "error" || turnEvent.message.stopReason === "aborted") {
|
|
746
|
-
return;
|
|
747
|
-
}
|
|
748
|
-
const finalContent = turnEvent.message.content;
|
|
749
|
-
const finalText = finalContent
|
|
750
|
-
.filter((part) => part.type === "text" && !!part.text)
|
|
751
|
-
.map((part) => part.text)
|
|
752
|
-
.join("\n");
|
|
753
|
-
const trimmedFinalText = finalText.trim();
|
|
754
|
-
if (!trimmedFinalText) {
|
|
755
|
-
return;
|
|
756
|
-
}
|
|
757
|
-
if (trimmedFinalText === "[SILENT]" || trimmedFinalText.startsWith("[SILENT]")) {
|
|
758
|
-
runState.finalOutcome = { kind: "silent" };
|
|
759
|
-
return;
|
|
760
|
-
}
|
|
761
|
-
if (runState.finalOutcome.kind === "final" && runState.finalOutcome.text.trim() === trimmedFinalText) {
|
|
762
|
-
return;
|
|
763
|
-
}
|
|
764
|
-
runState.finalOutcome = { kind: "final", text: finalText };
|
|
765
|
-
log.logResponse(logCtx, finalText);
|
|
766
|
-
queue.enqueue(async () => {
|
|
767
|
-
const delivered = await ctx.respondPlain(finalText);
|
|
768
|
-
if (delivered) {
|
|
769
|
-
runState.finalResponseDelivered = true;
|
|
770
|
-
}
|
|
771
|
-
}, "final response");
|
|
772
|
-
}
|
|
390
|
+
else {
|
|
391
|
+
await this.session.steer(text);
|
|
773
392
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
393
|
+
}
|
|
394
|
+
async handleModelCommand(ctx, args) {
|
|
395
|
+
this.modelRegistry.refresh();
|
|
396
|
+
const availableModels = await this.modelRegistry.getAvailable();
|
|
397
|
+
const currentModel = this.session.model;
|
|
398
|
+
if (!args.trim()) {
|
|
399
|
+
const current = currentModel ? `\`${formatModelReference(currentModel)}\`` : "(none)";
|
|
400
|
+
const available = availableModels.length > 0 ? formatModelList(availableModels, currentModel) : "- (none)";
|
|
401
|
+
await this.sendCommandReply(ctx, `# Model\n\nCurrent model: ${current}\n\nUse \`/model <provider/modelId>\` or \`/model <modelId>\` to switch. Bare model IDs must resolve uniquely.\n\nAvailable models:\n${available}`);
|
|
402
|
+
return;
|
|
777
403
|
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
log.logInfo("Auto-compaction aborted");
|
|
785
|
-
}
|
|
404
|
+
const match = findExactModelReferenceMatch(args, availableModels);
|
|
405
|
+
if (match.match) {
|
|
406
|
+
await this.session.setModel(match.match);
|
|
407
|
+
this.activeModel = match.match;
|
|
408
|
+
await this.sendCommandReply(ctx, `已切换模型到 \`${formatModelReference(match.match)}\`。`);
|
|
409
|
+
return;
|
|
786
410
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
411
|
+
const available = availableModels.length > 0 ? formatModelList(availableModels, currentModel, 10) : "- (none)";
|
|
412
|
+
if (match.ambiguous) {
|
|
413
|
+
await this.sendCommandReply(ctx, `未切换模型:\`${args.trim()}\` 匹配到多个模型。请改用精确的 \`provider/modelId\` 形式。\n\nAvailable models:\n${available}`);
|
|
414
|
+
return;
|
|
791
415
|
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
runState.logCtx
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
await fn();
|
|
830
|
-
}
|
|
831
|
-
catch (err) {
|
|
832
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
833
|
-
log.logWarning(`DingTalk API error (${errorContext})`, errMsg);
|
|
834
|
-
}
|
|
835
|
-
});
|
|
836
|
-
},
|
|
837
|
-
enqueueMessage(text, target, errorContext, doLog = true) {
|
|
838
|
-
this.enqueue(() => (target === "main" ? ctx.respond(text, doLog) : ctx.respondInThread(text)), errorContext);
|
|
839
|
-
},
|
|
840
|
-
};
|
|
841
|
-
try {
|
|
842
|
-
// Ensure channel directory exists
|
|
843
|
-
await mkdir(channelDir, { recursive: true });
|
|
844
|
-
// Update system prompt and runtime resources with fresh config
|
|
845
|
-
const soul = getSoul(workspaceDir);
|
|
846
|
-
const agentConfig = getAgentConfig(channelDir);
|
|
847
|
-
const memory = getMemory(channelDir);
|
|
848
|
-
const skills = loadPipiclawSkills(channelDir, workspacePath);
|
|
849
|
-
currentSkills = skills;
|
|
850
|
-
const systemPrompt = buildSystemPrompt(workspacePath, channelId, soul, agentConfig, memory, sandboxConfig, skills);
|
|
851
|
-
session.agent.setSystemPrompt(systemPrompt);
|
|
852
|
-
await session.reload();
|
|
853
|
-
// Sync messages from log.jsonl
|
|
854
|
-
const syncedCount = syncLogToSessionManager(sessionManager, channelDir, ctx.message.ts);
|
|
855
|
-
if (syncedCount > 0) {
|
|
856
|
-
log.logInfo(`[${channelId}] Synced ${syncedCount} messages from log.jsonl`);
|
|
416
|
+
await this.sendCommandReply(ctx, `未找到模型 \`${args.trim()}\`。请使用精确的 \`provider/modelId\` 或唯一的 \`modelId\`。\n\nAvailable models:\n${available}`);
|
|
417
|
+
}
|
|
418
|
+
resetRunState(ctx) {
|
|
419
|
+
this.runState = createEmptyRunState();
|
|
420
|
+
this.runState.ctx = ctx;
|
|
421
|
+
this.runState.logCtx = {
|
|
422
|
+
channelId: ctx.message.channel,
|
|
423
|
+
userName: ctx.message.userName,
|
|
424
|
+
channelName: ctx.channelName,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
// === Session event subscription ===
|
|
428
|
+
subscribeToSessionEvents() {
|
|
429
|
+
this.session.subscribe(async (event) => {
|
|
430
|
+
if (!this.runState.ctx || !this.runState.logCtx || !this.runState.queue)
|
|
431
|
+
return;
|
|
432
|
+
const { ctx, logCtx, queue, pendingTools } = this.runState;
|
|
433
|
+
if (event.type === "tool_execution_start") {
|
|
434
|
+
const agentEvent = event;
|
|
435
|
+
const args = agentEvent.args;
|
|
436
|
+
const label = args.label || agentEvent.toolName;
|
|
437
|
+
pendingTools.set(agentEvent.toolCallId, {
|
|
438
|
+
toolName: agentEvent.toolName,
|
|
439
|
+
args: agentEvent.args,
|
|
440
|
+
startTime: Date.now(),
|
|
441
|
+
});
|
|
442
|
+
log.logToolStart(logCtx, agentEvent.toolName, label, agentEvent.args);
|
|
443
|
+
queue.enqueue(() => ctx.respond(formatProgressEntry("tool", label), false), "tool label");
|
|
444
|
+
}
|
|
445
|
+
else if (event.type === "tool_execution_end") {
|
|
446
|
+
const agentEvent = event;
|
|
447
|
+
const resultStr = extractToolResultText(agentEvent.result);
|
|
448
|
+
const pending = pendingTools.get(agentEvent.toolCallId);
|
|
449
|
+
pendingTools.delete(agentEvent.toolCallId);
|
|
450
|
+
const durationMs = pending ? Date.now() - pending.startTime : 0;
|
|
451
|
+
if (agentEvent.isError) {
|
|
452
|
+
log.logToolError(logCtx, agentEvent.toolName, durationMs, resultStr);
|
|
857
453
|
}
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
if (reloadedSession.messages.length > 0) {
|
|
861
|
-
agent.replaceMessages(reloadedSession.messages);
|
|
862
|
-
log.logInfo(`[${channelId}] Reloaded ${reloadedSession.messages.length} messages from context`);
|
|
454
|
+
else {
|
|
455
|
+
log.logToolSuccess(logCtx, agentEvent.toolName, durationMs, resultStr);
|
|
863
456
|
}
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
// Build user message with timestamp and username prefix
|
|
867
|
-
const now = new Date();
|
|
868
|
-
const pad = (n) => n.toString().padStart(2, "0");
|
|
869
|
-
const offset = -now.getTimezoneOffset();
|
|
870
|
-
const offsetSign = offset >= 0 ? "+" : "-";
|
|
871
|
-
const offsetHours = pad(Math.floor(Math.abs(offset) / 60));
|
|
872
|
-
const offsetMins = pad(Math.abs(offset) % 60);
|
|
873
|
-
const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}${offsetSign}${offsetHours}:${offsetMins}`;
|
|
874
|
-
const userMessage = `[${timestamp}] [${ctx.message.userName || "unknown"}]: ${ctx.message.text}`;
|
|
875
|
-
// Debug: write context to last_prompt.json (only with PIPICLAW_DEBUG=1)
|
|
876
|
-
if (process.env.PIPICLAW_DEBUG) {
|
|
877
|
-
const debugContext = {
|
|
878
|
-
systemPrompt,
|
|
879
|
-
messages: session.messages,
|
|
880
|
-
newUserMessage: userMessage,
|
|
881
|
-
};
|
|
882
|
-
await writeFile(join(channelDir, "last_prompt.json"), JSON.stringify(debugContext, null, 2));
|
|
457
|
+
if (agentEvent.isError) {
|
|
458
|
+
queue.enqueue(() => ctx.respond(formatProgressEntry("error", truncate(resultStr, 200)), false), "tool error");
|
|
883
459
|
}
|
|
884
|
-
await session.prompt(userMessage);
|
|
885
460
|
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
461
|
+
else if (event.type === "message_start") {
|
|
462
|
+
const agentEvent = event;
|
|
463
|
+
if (agentEvent.message.role === "assistant") {
|
|
464
|
+
log.logResponseStart(logCtx);
|
|
465
|
+
}
|
|
890
466
|
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
try {
|
|
898
|
-
await ctx.replaceMessage("_Sorry, something went wrong_");
|
|
899
|
-
}
|
|
900
|
-
catch (err) {
|
|
901
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
902
|
-
log.logWarning("Failed to post error message", errMsg);
|
|
903
|
-
}
|
|
467
|
+
else if (event.type === "message_end") {
|
|
468
|
+
const agentEvent = event;
|
|
469
|
+
if (agentEvent.message.role === "assistant") {
|
|
470
|
+
const assistantMsg = agentEvent.message;
|
|
471
|
+
if (assistantMsg.stopReason) {
|
|
472
|
+
this.runState.stopReason = assistantMsg.stopReason;
|
|
904
473
|
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
474
|
+
if (assistantMsg.errorMessage) {
|
|
475
|
+
this.runState.errorMessage = assistantMsg.errorMessage;
|
|
476
|
+
}
|
|
477
|
+
if (assistantMsg.usage) {
|
|
478
|
+
this.runState.totalUsage.input += assistantMsg.usage.input;
|
|
479
|
+
this.runState.totalUsage.output += assistantMsg.usage.output;
|
|
480
|
+
this.runState.totalUsage.cacheRead += assistantMsg.usage.cacheRead;
|
|
481
|
+
this.runState.totalUsage.cacheWrite += assistantMsg.usage.cacheWrite;
|
|
482
|
+
this.runState.totalUsage.cost.input += assistantMsg.usage.cost.input;
|
|
483
|
+
this.runState.totalUsage.cost.output += assistantMsg.usage.cost.output;
|
|
484
|
+
this.runState.totalUsage.cost.cacheRead += assistantMsg.usage.cost.cacheRead;
|
|
485
|
+
this.runState.totalUsage.cost.cacheWrite += assistantMsg.usage.cost.cacheWrite;
|
|
486
|
+
this.runState.totalUsage.cost.total += assistantMsg.usage.cost.total;
|
|
914
487
|
}
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
488
|
+
const content = agentEvent.message.content;
|
|
489
|
+
const thinkingParts = [];
|
|
490
|
+
const textParts = [];
|
|
491
|
+
let hasToolCalls = false;
|
|
492
|
+
for (const part of content) {
|
|
493
|
+
if (part.type === "thinking") {
|
|
494
|
+
thinkingParts.push(part.thinking);
|
|
495
|
+
}
|
|
496
|
+
else if (part.type === "text") {
|
|
497
|
+
textParts.push(part.text);
|
|
918
498
|
}
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
log.logWarning("Failed to replace message with final text", errMsg);
|
|
499
|
+
else if (part.type === "toolCall") {
|
|
500
|
+
hasToolCalls = true;
|
|
922
501
|
}
|
|
923
502
|
}
|
|
924
|
-
|
|
503
|
+
const text = textParts.join("\n");
|
|
504
|
+
for (const thinking of thinkingParts) {
|
|
505
|
+
log.logThinking(logCtx, thinking);
|
|
506
|
+
queue.enqueue(() => ctx.respond(formatProgressEntry("thinking", thinking), false), "thinking");
|
|
507
|
+
}
|
|
508
|
+
if (hasToolCalls && text.trim()) {
|
|
509
|
+
queue.enqueue(() => ctx.respond(formatProgressEntry("assistant", text), false), "assistant progress");
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
else if (event.type === "turn_end") {
|
|
514
|
+
const turnEvent = event;
|
|
515
|
+
if (turnEvent.message.role === "assistant" && turnEvent.toolResults.length === 0) {
|
|
516
|
+
if (turnEvent.message.stopReason === "error" || turnEvent.message.stopReason === "aborted") {
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
const finalContent = turnEvent.message.content;
|
|
520
|
+
const finalText = finalContent
|
|
521
|
+
.filter((part) => part.type === "text" && !!part.text)
|
|
522
|
+
.map((part) => part.text)
|
|
523
|
+
.join("\n");
|
|
524
|
+
const trimmedFinalText = finalText.trim();
|
|
525
|
+
if (!trimmedFinalText) {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (trimmedFinalText === "[SILENT]" || trimmedFinalText.startsWith("[SILENT]")) {
|
|
529
|
+
this.runState.finalOutcome = { kind: "silent" };
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
if (this.runState.finalOutcome.kind === "final" &&
|
|
533
|
+
this.runState.finalOutcome.text.trim() === trimmedFinalText) {
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
this.runState.finalOutcome = { kind: "final", text: finalText };
|
|
537
|
+
log.logResponse(logCtx, finalText);
|
|
538
|
+
queue.enqueue(async () => {
|
|
539
|
+
const delivered = await ctx.respondPlain(finalText);
|
|
540
|
+
if (delivered) {
|
|
541
|
+
this.runState.finalResponseDelivered = true;
|
|
542
|
+
}
|
|
543
|
+
}, "final response");
|
|
925
544
|
}
|
|
926
|
-
|
|
927
|
-
|
|
545
|
+
}
|
|
546
|
+
else if (event.type === "auto_compaction_start") {
|
|
547
|
+
log.logInfo(`Auto-compaction started (reason: ${event.reason})`);
|
|
548
|
+
queue.enqueue(() => ctx.respond(formatProgressEntry("assistant", "Compacting context..."), false), "compaction start");
|
|
549
|
+
}
|
|
550
|
+
else if (event.type === "auto_compaction_end") {
|
|
551
|
+
const compEvent = event;
|
|
552
|
+
if (compEvent.result) {
|
|
553
|
+
log.logInfo(`Auto-compaction complete: ${compEvent.result.tokensBefore} tokens compacted`);
|
|
928
554
|
}
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
const messages = session.messages;
|
|
932
|
-
const lastAssistantMessage = messages
|
|
933
|
-
.slice()
|
|
934
|
-
.reverse()
|
|
935
|
-
.find((m) => m.role === "assistant" && m.stopReason !== "aborted");
|
|
936
|
-
const contextTokens = lastAssistantMessage
|
|
937
|
-
? lastAssistantMessage.usage.input +
|
|
938
|
-
lastAssistantMessage.usage.output +
|
|
939
|
-
lastAssistantMessage.usage.cacheRead +
|
|
940
|
-
lastAssistantMessage.usage.cacheWrite
|
|
941
|
-
: 0;
|
|
942
|
-
const currentRunModel = session.model ?? activeModel;
|
|
943
|
-
const contextWindow = currentRunModel.contextWindow || 200000;
|
|
944
|
-
log.logUsageSummary(runState.logCtx, runState.totalUsage, contextTokens, contextWindow);
|
|
555
|
+
else if (compEvent.aborted) {
|
|
556
|
+
log.logInfo("Auto-compaction aborted");
|
|
945
557
|
}
|
|
946
|
-
// Clear run state
|
|
947
|
-
runState.ctx = null;
|
|
948
|
-
runState.logCtx = null;
|
|
949
|
-
runState.queue = null;
|
|
950
558
|
}
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
}
|
|
958
|
-
/**
|
|
959
|
-
* Translate container path back to host path for file operations
|
|
960
|
-
*/
|
|
961
|
-
export function translateToHostPath(containerPath, channelDir, workspacePath, channelId) {
|
|
962
|
-
if (workspacePath === "/workspace") {
|
|
963
|
-
const prefix = `/workspace/${channelId}/`;
|
|
964
|
-
if (containerPath.startsWith(prefix)) {
|
|
965
|
-
return join(channelDir, containerPath.slice(prefix.length));
|
|
966
|
-
}
|
|
967
|
-
if (containerPath.startsWith("/workspace/")) {
|
|
968
|
-
return join(channelDir, "..", containerPath.slice("/workspace/".length));
|
|
969
|
-
}
|
|
559
|
+
else if (event.type === "auto_retry_start") {
|
|
560
|
+
const retryEvent = event;
|
|
561
|
+
log.logWarning(`Retrying (${retryEvent.attempt}/${retryEvent.maxAttempts})`, retryEvent.errorMessage);
|
|
562
|
+
queue.enqueue(() => ctx.respond(formatProgressEntry("assistant", `Retrying (${retryEvent.attempt}/${retryEvent.maxAttempts})...`), false), "retry");
|
|
563
|
+
}
|
|
564
|
+
});
|
|
970
565
|
}
|
|
971
|
-
|
|
566
|
+
}
|
|
567
|
+
// ============================================================================
|
|
568
|
+
// Factory
|
|
569
|
+
// ============================================================================
|
|
570
|
+
const channelRunners = new Map();
|
|
571
|
+
export function getOrCreateRunner(sandboxConfig, channelId, channelDir) {
|
|
572
|
+
const existing = channelRunners.get(channelId);
|
|
573
|
+
if (existing)
|
|
574
|
+
return existing;
|
|
575
|
+
const runner = new ChannelRunner(sandboxConfig, channelId, channelDir);
|
|
576
|
+
channelRunners.set(channelId, runner);
|
|
577
|
+
return runner;
|
|
972
578
|
}
|
|
973
579
|
//# sourceMappingURL=agent.js.map
|