@oyasmi/pipiclaw 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +247 -0
- package/dist/agent.d.ts +18 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +938 -0
- package/dist/agent.js.map +1 -0
- package/dist/commands.d.ts +9 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +45 -0
- package/dist/commands.js.map +1 -0
- package/dist/context.d.ts +139 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +432 -0
- package/dist/context.js.map +1 -0
- package/dist/delivery.d.ts +4 -0
- package/dist/delivery.d.ts.map +1 -0
- package/dist/delivery.js +221 -0
- package/dist/delivery.js.map +1 -0
- package/dist/dingtalk.d.ts +109 -0
- package/dist/dingtalk.d.ts.map +1 -0
- package/dist/dingtalk.js +655 -0
- package/dist/dingtalk.js.map +1 -0
- package/dist/events.d.ts +51 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +287 -0
- package/dist/events.js.map +1 -0
- package/dist/log.d.ts +33 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +188 -0
- package/dist/log.js.map +1 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +298 -0
- package/dist/main.js.map +1 -0
- package/dist/paths.d.ts +8 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +10 -0
- package/dist/paths.js.map +1 -0
- package/dist/sandbox.d.ts +34 -0
- package/dist/sandbox.d.ts.map +1 -0
- package/dist/sandbox.js +180 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/shell-escape.d.ts +6 -0
- package/dist/shell-escape.d.ts.map +1 -0
- package/dist/shell-escape.js +8 -0
- package/dist/shell-escape.js.map +1 -0
- package/dist/store.d.ts +41 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +110 -0
- package/dist/store.js.map +1 -0
- package/dist/tools/attach.d.ts +14 -0
- package/dist/tools/attach.d.ts.map +1 -0
- package/dist/tools/attach.js +35 -0
- package/dist/tools/attach.js.map +1 -0
- package/dist/tools/bash.d.ts +10 -0
- package/dist/tools/bash.d.ts.map +1 -0
- package/dist/tools/bash.js +78 -0
- package/dist/tools/bash.js.map +1 -0
- package/dist/tools/edit.d.ts +11 -0
- package/dist/tools/edit.d.ts.map +1 -0
- package/dist/tools/edit.js +129 -0
- package/dist/tools/edit.js.map +1 -0
- package/dist/tools/index.d.ts +5 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +15 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/read.d.ts +11 -0
- package/dist/tools/read.d.ts.map +1 -0
- package/dist/tools/read.js +132 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/truncate.d.ts +57 -0
- package/dist/tools/truncate.d.ts.map +1 -0
- package/dist/tools/truncate.js +184 -0
- package/dist/tools/truncate.js.map +1 -0
- package/dist/tools/write.d.ts +10 -0
- package/dist/tools/write.d.ts.map +1 -0
- package/dist/tools/write.js +31 -0
- package/dist/tools/write.js.map +1 -0
- package/package.json +54 -0
package/dist/agent.js
ADDED
|
@@ -0,0 +1,938 @@
|
|
|
1
|
+
import { Agent } from "@mariozechner/pi-agent-core";
|
|
2
|
+
import { getModel } from "@mariozechner/pi-ai";
|
|
3
|
+
import { AgentSession, AuthStorage, convertToLlm, DefaultResourceLoader, formatSkillsForPrompt, loadSkillsFromDir, ModelRegistry, SessionManager, } from "@mariozechner/pi-coding-agent";
|
|
4
|
+
import { existsSync, readFileSync } from "fs";
|
|
5
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
6
|
+
import { basename, join } from "path";
|
|
7
|
+
import { renderBuiltInHelp } from "./commands.js";
|
|
8
|
+
import { PipiclawSettingsManager, syncLogToSessionManager } from "./context.js";
|
|
9
|
+
import * as log from "./log.js";
|
|
10
|
+
import { APP_HOME_DIR, AUTH_CONFIG_PATH, MODELS_CONFIG_PATH } from "./paths.js";
|
|
11
|
+
import { createExecutor } from "./sandbox.js";
|
|
12
|
+
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
|
+
function isSilentOutcome(outcome) {
|
|
16
|
+
return outcome.kind === "silent";
|
|
17
|
+
}
|
|
18
|
+
function isFinalOutcome(outcome) {
|
|
19
|
+
return outcome.kind === "final";
|
|
20
|
+
}
|
|
21
|
+
function getFinalOutcomeText(outcome) {
|
|
22
|
+
return isFinalOutcome(outcome) ? outcome.text : null;
|
|
23
|
+
}
|
|
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, AGENT.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 AGENT.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 AGENT.md (global)
|
|
132
|
+
const workspaceAgentPath = join(channelDir, "..", "AGENT.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 AGENT.md", `${workspaceAgentPath}: ${error}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Read channel-specific AGENT.md (overrides/extends global)
|
|
145
|
+
const channelAgentPath = join(channelDir, "AGENT.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 AGENT.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
|
+
// ============================================================================
|
|
223
|
+
// System Prompt Builder
|
|
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. AGENT.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
|
+
├── AGENT.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
|
+
├── AGENT.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. AGENT.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
|
|
390
|
+
// ============================================================================
|
|
391
|
+
function truncate(text, maxLen) {
|
|
392
|
+
if (text.length <= maxLen)
|
|
393
|
+
return text;
|
|
394
|
+
return `${text.substring(0, maxLen - 3)}...`;
|
|
395
|
+
}
|
|
396
|
+
function sanitizeProgressText(text) {
|
|
397
|
+
return text
|
|
398
|
+
.replace(/\uFFFC/g, "")
|
|
399
|
+
.replace(/\r/g, "")
|
|
400
|
+
.trim();
|
|
401
|
+
}
|
|
402
|
+
function formatProgressEntry(kind, text) {
|
|
403
|
+
const cleaned = sanitizeProgressText(text);
|
|
404
|
+
if (!cleaned)
|
|
405
|
+
return "";
|
|
406
|
+
const normalized = cleaned.replace(/\n+/g, " ").trim();
|
|
407
|
+
switch (kind) {
|
|
408
|
+
case "tool":
|
|
409
|
+
return `Running: ${normalized}`;
|
|
410
|
+
case "thinking":
|
|
411
|
+
return `Thinking: ${normalized}`;
|
|
412
|
+
case "error":
|
|
413
|
+
return `Error: ${normalized}`;
|
|
414
|
+
case "assistant":
|
|
415
|
+
return normalized;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function extractToolResultText(result) {
|
|
419
|
+
if (typeof result === "string") {
|
|
420
|
+
return result;
|
|
421
|
+
}
|
|
422
|
+
if (result &&
|
|
423
|
+
typeof result === "object" &&
|
|
424
|
+
"content" in result &&
|
|
425
|
+
Array.isArray(result.content)) {
|
|
426
|
+
const content = result.content;
|
|
427
|
+
const textParts = [];
|
|
428
|
+
for (const part of content) {
|
|
429
|
+
if (part.type === "text" && part.text) {
|
|
430
|
+
textParts.push(part.text);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (textParts.length > 0) {
|
|
434
|
+
return textParts.join("\n");
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return JSON.stringify(result);
|
|
438
|
+
}
|
|
439
|
+
// Cache runners per channel
|
|
440
|
+
const channelRunners = new Map();
|
|
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 = {
|
|
511
|
+
ctx: null,
|
|
512
|
+
logCtx: null,
|
|
513
|
+
queue: null,
|
|
514
|
+
pendingTools: new Map(),
|
|
515
|
+
totalUsage: {
|
|
516
|
+
input: 0,
|
|
517
|
+
output: 0,
|
|
518
|
+
cacheRead: 0,
|
|
519
|
+
cacheWrite: 0,
|
|
520
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
521
|
+
},
|
|
522
|
+
stopReason: "stop",
|
|
523
|
+
errorMessage: undefined,
|
|
524
|
+
finalOutcome: { kind: "none" },
|
|
525
|
+
finalResponseDelivered: false,
|
|
526
|
+
};
|
|
527
|
+
const sendCommandReply = async (ctx, text) => {
|
|
528
|
+
const delivered = await ctx.respondPlain(text);
|
|
529
|
+
if (!delivered) {
|
|
530
|
+
await ctx.replaceMessage(text);
|
|
531
|
+
await ctx.flush();
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
const handleModelBuiltinCommand = async (ctx, args) => {
|
|
535
|
+
modelRegistry.refresh();
|
|
536
|
+
const availableModels = await modelRegistry.getAvailable();
|
|
537
|
+
const currentModel = session.model;
|
|
538
|
+
if (!args.trim()) {
|
|
539
|
+
const current = currentModel ? `\`${formatModelReference(currentModel)}\`` : "(none)";
|
|
540
|
+
const available = availableModels.length > 0 ? formatModelList(availableModels, currentModel) : "- (none)";
|
|
541
|
+
await sendCommandReply(ctx, `# Model
|
|
542
|
+
|
|
543
|
+
Current model: ${current}
|
|
544
|
+
|
|
545
|
+
Use \`/model <provider/modelId>\` or \`/model <modelId>\` to switch. Bare model IDs must resolve uniquely.
|
|
546
|
+
|
|
547
|
+
Available models:
|
|
548
|
+
${available}`);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
const match = findExactModelReferenceMatch(args, availableModels);
|
|
552
|
+
if (match.match) {
|
|
553
|
+
await session.setModel(match.match);
|
|
554
|
+
activeModel = match.match;
|
|
555
|
+
await sendCommandReply(ctx, `已切换模型到 \`${formatModelReference(match.match)}\`。`);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
const available = availableModels.length > 0 ? formatModelList(availableModels, currentModel, 10) : "- (none)";
|
|
559
|
+
if (match.ambiguous) {
|
|
560
|
+
await sendCommandReply(ctx, `未切换模型:\`${args.trim()}\` 匹配到多个模型。请改用精确的 \`provider/modelId\` 形式。
|
|
561
|
+
|
|
562
|
+
Available models:
|
|
563
|
+
${available}`);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
await sendCommandReply(ctx, `未找到模型 \`${args.trim()}\`。请使用精确的 \`provider/modelId\` 或唯一的 \`modelId\`。
|
|
567
|
+
|
|
568
|
+
Available models:
|
|
569
|
+
${available}`);
|
|
570
|
+
};
|
|
571
|
+
const handleBuiltInCommand = async (ctx, command) => {
|
|
572
|
+
try {
|
|
573
|
+
switch (command.name) {
|
|
574
|
+
case "help":
|
|
575
|
+
await sendCommandReply(ctx, renderBuiltInHelp());
|
|
576
|
+
return;
|
|
577
|
+
case "new": {
|
|
578
|
+
const completed = await session.newSession();
|
|
579
|
+
await sendCommandReply(ctx, completed
|
|
580
|
+
? `已开启新会话。
|
|
581
|
+
|
|
582
|
+
Session ID: \`${session.sessionId}\``
|
|
583
|
+
: "新会话已取消。");
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
case "compact": {
|
|
587
|
+
const result = await session.compact(command.args || undefined);
|
|
588
|
+
await sendCommandReply(ctx, `已压缩当前会话上下文。
|
|
589
|
+
|
|
590
|
+
- Tokens before compaction: \`${result.tokensBefore}\`
|
|
591
|
+
- Summary:
|
|
592
|
+
|
|
593
|
+
\`\`\`text
|
|
594
|
+
${result.summary}
|
|
595
|
+
\`\`\``);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
case "session": {
|
|
599
|
+
const stats = session.getSessionStats();
|
|
600
|
+
const currentModel = session.model ? `\`${formatModelReference(session.model)}\`` : "(none)";
|
|
601
|
+
const sessionFile = stats.sessionFile ? `\`${basename(stats.sessionFile)}\`` : "(none)";
|
|
602
|
+
await sendCommandReply(ctx, `# Session
|
|
603
|
+
|
|
604
|
+
- Session ID: \`${stats.sessionId}\`
|
|
605
|
+
- Session file: ${sessionFile}
|
|
606
|
+
- Model: ${currentModel}
|
|
607
|
+
- Thinking level: \`${session.thinkingLevel}\`
|
|
608
|
+
- User messages: \`${stats.userMessages}\`
|
|
609
|
+
- Assistant messages: \`${stats.assistantMessages}\`
|
|
610
|
+
- Tool calls: \`${stats.toolCalls}\`
|
|
611
|
+
- Tool results: \`${stats.toolResults}\`
|
|
612
|
+
- Total messages: \`${stats.totalMessages}\`
|
|
613
|
+
- Tokens: \`${stats.tokens.total}\` (input \`${stats.tokens.input}\`, output \`${stats.tokens.output}\`, cache read \`${stats.tokens.cacheRead}\`, cache write \`${stats.tokens.cacheWrite}\`)
|
|
614
|
+
- Cost: \`$${stats.cost.toFixed(4)}\``);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
case "model":
|
|
618
|
+
await handleModelBuiltinCommand(ctx, command.args);
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
catch (err) {
|
|
623
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
624
|
+
log.logWarning(`[${channelId}] Built-in command failed`, errMsg);
|
|
625
|
+
await sendCommandReply(ctx, `命令执行失败:${errMsg}`);
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
// Subscribe to events ONCE
|
|
629
|
+
session.subscribe(async (event) => {
|
|
630
|
+
if (!runState.ctx || !runState.logCtx || !runState.queue)
|
|
631
|
+
return;
|
|
632
|
+
const { ctx, logCtx, queue, pendingTools } = runState;
|
|
633
|
+
if (event.type === "tool_execution_start") {
|
|
634
|
+
const agentEvent = event;
|
|
635
|
+
const args = agentEvent.args;
|
|
636
|
+
const label = args.label || agentEvent.toolName;
|
|
637
|
+
pendingTools.set(agentEvent.toolCallId, {
|
|
638
|
+
toolName: agentEvent.toolName,
|
|
639
|
+
args: agentEvent.args,
|
|
640
|
+
startTime: Date.now(),
|
|
641
|
+
});
|
|
642
|
+
log.logToolStart(logCtx, agentEvent.toolName, label, agentEvent.args);
|
|
643
|
+
queue.enqueue(() => ctx.respond(formatProgressEntry("tool", label), false), "tool label");
|
|
644
|
+
}
|
|
645
|
+
else if (event.type === "tool_execution_end") {
|
|
646
|
+
const agentEvent = event;
|
|
647
|
+
const resultStr = extractToolResultText(agentEvent.result);
|
|
648
|
+
const pending = pendingTools.get(agentEvent.toolCallId);
|
|
649
|
+
pendingTools.delete(agentEvent.toolCallId);
|
|
650
|
+
const durationMs = pending ? Date.now() - pending.startTime : 0;
|
|
651
|
+
if (agentEvent.isError) {
|
|
652
|
+
log.logToolError(logCtx, agentEvent.toolName, durationMs, resultStr);
|
|
653
|
+
}
|
|
654
|
+
else {
|
|
655
|
+
log.logToolSuccess(logCtx, agentEvent.toolName, durationMs, resultStr);
|
|
656
|
+
}
|
|
657
|
+
if (agentEvent.isError) {
|
|
658
|
+
queue.enqueue(() => ctx.respond(formatProgressEntry("error", truncate(resultStr, 200)), false), "tool error");
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
else if (event.type === "message_start") {
|
|
662
|
+
const agentEvent = event;
|
|
663
|
+
if (agentEvent.message.role === "assistant") {
|
|
664
|
+
log.logResponseStart(logCtx);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
else if (event.type === "message_end") {
|
|
668
|
+
const agentEvent = event;
|
|
669
|
+
if (agentEvent.message.role === "assistant") {
|
|
670
|
+
const assistantMsg = agentEvent.message;
|
|
671
|
+
if (assistantMsg.stopReason) {
|
|
672
|
+
runState.stopReason = assistantMsg.stopReason;
|
|
673
|
+
}
|
|
674
|
+
if (assistantMsg.errorMessage) {
|
|
675
|
+
runState.errorMessage = assistantMsg.errorMessage;
|
|
676
|
+
}
|
|
677
|
+
if (assistantMsg.usage) {
|
|
678
|
+
runState.totalUsage.input += assistantMsg.usage.input;
|
|
679
|
+
runState.totalUsage.output += assistantMsg.usage.output;
|
|
680
|
+
runState.totalUsage.cacheRead += assistantMsg.usage.cacheRead;
|
|
681
|
+
runState.totalUsage.cacheWrite += assistantMsg.usage.cacheWrite;
|
|
682
|
+
runState.totalUsage.cost.input += assistantMsg.usage.cost.input;
|
|
683
|
+
runState.totalUsage.cost.output += assistantMsg.usage.cost.output;
|
|
684
|
+
runState.totalUsage.cost.cacheRead += assistantMsg.usage.cost.cacheRead;
|
|
685
|
+
runState.totalUsage.cost.cacheWrite += assistantMsg.usage.cost.cacheWrite;
|
|
686
|
+
runState.totalUsage.cost.total += assistantMsg.usage.cost.total;
|
|
687
|
+
}
|
|
688
|
+
const content = agentEvent.message.content;
|
|
689
|
+
const thinkingParts = [];
|
|
690
|
+
const textParts = [];
|
|
691
|
+
let hasToolCalls = false;
|
|
692
|
+
for (const part of content) {
|
|
693
|
+
if (part.type === "thinking") {
|
|
694
|
+
thinkingParts.push(part.thinking);
|
|
695
|
+
}
|
|
696
|
+
else if (part.type === "text") {
|
|
697
|
+
textParts.push(part.text);
|
|
698
|
+
}
|
|
699
|
+
else if (part.type === "toolCall") {
|
|
700
|
+
hasToolCalls = true;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
const text = textParts.join("\n");
|
|
704
|
+
for (const thinking of thinkingParts) {
|
|
705
|
+
log.logThinking(logCtx, thinking);
|
|
706
|
+
queue.enqueue(() => ctx.respond(formatProgressEntry("thinking", thinking), false), "thinking");
|
|
707
|
+
}
|
|
708
|
+
if (hasToolCalls && text.trim()) {
|
|
709
|
+
queue.enqueue(() => ctx.respond(formatProgressEntry("assistant", text), false), "assistant progress");
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
else if (event.type === "turn_end") {
|
|
714
|
+
const turnEvent = event;
|
|
715
|
+
if (turnEvent.message.role === "assistant" && turnEvent.toolResults.length === 0) {
|
|
716
|
+
if (turnEvent.message.stopReason === "error" || turnEvent.message.stopReason === "aborted") {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
const finalContent = turnEvent.message.content;
|
|
720
|
+
const finalText = finalContent
|
|
721
|
+
.filter((part) => part.type === "text" && !!part.text)
|
|
722
|
+
.map((part) => part.text)
|
|
723
|
+
.join("\n");
|
|
724
|
+
const trimmedFinalText = finalText.trim();
|
|
725
|
+
if (!trimmedFinalText) {
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
if (trimmedFinalText === "[SILENT]" || trimmedFinalText.startsWith("[SILENT]")) {
|
|
729
|
+
runState.finalOutcome = { kind: "silent" };
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
if (runState.finalOutcome.kind === "final" && runState.finalOutcome.text.trim() === trimmedFinalText) {
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
runState.finalOutcome = { kind: "final", text: finalText };
|
|
736
|
+
log.logResponse(logCtx, finalText);
|
|
737
|
+
queue.enqueue(async () => {
|
|
738
|
+
const delivered = await ctx.respondPlain(finalText);
|
|
739
|
+
if (delivered) {
|
|
740
|
+
runState.finalResponseDelivered = true;
|
|
741
|
+
}
|
|
742
|
+
}, "final response");
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
else if (event.type === "auto_compaction_start") {
|
|
746
|
+
log.logInfo(`Auto-compaction started (reason: ${event.reason})`);
|
|
747
|
+
queue.enqueue(() => ctx.respond(formatProgressEntry("assistant", "Compacting context..."), false), "compaction start");
|
|
748
|
+
}
|
|
749
|
+
else if (event.type === "auto_compaction_end") {
|
|
750
|
+
const compEvent = event;
|
|
751
|
+
if (compEvent.result) {
|
|
752
|
+
log.logInfo(`Auto-compaction complete: ${compEvent.result.tokensBefore} tokens compacted`);
|
|
753
|
+
}
|
|
754
|
+
else if (compEvent.aborted) {
|
|
755
|
+
log.logInfo("Auto-compaction aborted");
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
else if (event.type === "auto_retry_start") {
|
|
759
|
+
const retryEvent = event;
|
|
760
|
+
log.logWarning(`Retrying (${retryEvent.attempt}/${retryEvent.maxAttempts})`, retryEvent.errorMessage);
|
|
761
|
+
queue.enqueue(() => ctx.respond(formatProgressEntry("assistant", `Retrying (${retryEvent.attempt}/${retryEvent.maxAttempts})...`), false), "retry");
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
return {
|
|
765
|
+
async handleBuiltinCommand(ctx, command) {
|
|
766
|
+
await handleBuiltInCommand(ctx, command);
|
|
767
|
+
},
|
|
768
|
+
async run(ctx, _store) {
|
|
769
|
+
// Reset per-run state
|
|
770
|
+
runState.ctx = ctx;
|
|
771
|
+
runState.logCtx = {
|
|
772
|
+
channelId: ctx.message.channel,
|
|
773
|
+
userName: ctx.message.userName,
|
|
774
|
+
channelName: ctx.channelName,
|
|
775
|
+
};
|
|
776
|
+
runState.pendingTools.clear();
|
|
777
|
+
runState.totalUsage = {
|
|
778
|
+
input: 0,
|
|
779
|
+
output: 0,
|
|
780
|
+
cacheRead: 0,
|
|
781
|
+
cacheWrite: 0,
|
|
782
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
783
|
+
};
|
|
784
|
+
runState.stopReason = "stop";
|
|
785
|
+
runState.errorMessage = undefined;
|
|
786
|
+
runState.finalOutcome = { kind: "none" };
|
|
787
|
+
runState.finalResponseDelivered = false;
|
|
788
|
+
// Create queue for this run
|
|
789
|
+
let queueChain = Promise.resolve();
|
|
790
|
+
runState.queue = {
|
|
791
|
+
enqueue(fn, errorContext) {
|
|
792
|
+
queueChain = queueChain.then(async () => {
|
|
793
|
+
try {
|
|
794
|
+
await fn();
|
|
795
|
+
}
|
|
796
|
+
catch (err) {
|
|
797
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
798
|
+
log.logWarning(`DingTalk API error (${errorContext})`, errMsg);
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
},
|
|
802
|
+
enqueueMessage(text, target, errorContext, doLog = true) {
|
|
803
|
+
this.enqueue(() => (target === "main" ? ctx.respond(text, doLog) : ctx.respondInThread(text)), errorContext);
|
|
804
|
+
},
|
|
805
|
+
};
|
|
806
|
+
try {
|
|
807
|
+
// Ensure channel directory exists
|
|
808
|
+
await mkdir(channelDir, { recursive: true });
|
|
809
|
+
// Update system prompt and runtime resources with fresh config
|
|
810
|
+
const soul = getSoul(workspaceDir);
|
|
811
|
+
const agentConfig = getAgentConfig(channelDir);
|
|
812
|
+
const memory = getMemory(channelDir);
|
|
813
|
+
const skills = loadPipiclawSkills(channelDir, workspacePath);
|
|
814
|
+
currentSkills = skills;
|
|
815
|
+
const systemPrompt = buildSystemPrompt(workspacePath, channelId, soul, agentConfig, memory, sandboxConfig, skills);
|
|
816
|
+
session.agent.setSystemPrompt(systemPrompt);
|
|
817
|
+
await session.reload();
|
|
818
|
+
// Sync messages from log.jsonl
|
|
819
|
+
const syncedCount = syncLogToSessionManager(sessionManager, channelDir, ctx.message.ts);
|
|
820
|
+
if (syncedCount > 0) {
|
|
821
|
+
log.logInfo(`[${channelId}] Synced ${syncedCount} messages from log.jsonl`);
|
|
822
|
+
}
|
|
823
|
+
// Reload messages from context.jsonl
|
|
824
|
+
const reloadedSession = sessionManager.buildSessionContext();
|
|
825
|
+
if (reloadedSession.messages.length > 0) {
|
|
826
|
+
agent.replaceMessages(reloadedSession.messages);
|
|
827
|
+
log.logInfo(`[${channelId}] Reloaded ${reloadedSession.messages.length} messages from context`);
|
|
828
|
+
}
|
|
829
|
+
// Log context info
|
|
830
|
+
log.logInfo(`Context sizes - system: ${systemPrompt.length} chars, memory: ${memory.length} chars`);
|
|
831
|
+
// Build user message with timestamp and username prefix
|
|
832
|
+
const now = new Date();
|
|
833
|
+
const pad = (n) => n.toString().padStart(2, "0");
|
|
834
|
+
const offset = -now.getTimezoneOffset();
|
|
835
|
+
const offsetSign = offset >= 0 ? "+" : "-";
|
|
836
|
+
const offsetHours = pad(Math.floor(Math.abs(offset) / 60));
|
|
837
|
+
const offsetMins = pad(Math.abs(offset) % 60);
|
|
838
|
+
const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}${offsetSign}${offsetHours}:${offsetMins}`;
|
|
839
|
+
const userMessage = `[${timestamp}] [${ctx.message.userName || "unknown"}]: ${ctx.message.text}`;
|
|
840
|
+
// Debug: write context to last_prompt.json (only with PIPICLAW_DEBUG=1)
|
|
841
|
+
if (process.env.PIPICLAW_DEBUG) {
|
|
842
|
+
const debugContext = {
|
|
843
|
+
systemPrompt,
|
|
844
|
+
messages: session.messages,
|
|
845
|
+
newUserMessage: userMessage,
|
|
846
|
+
};
|
|
847
|
+
await writeFile(join(channelDir, "last_prompt.json"), JSON.stringify(debugContext, null, 2));
|
|
848
|
+
}
|
|
849
|
+
await session.prompt(userMessage);
|
|
850
|
+
}
|
|
851
|
+
catch (err) {
|
|
852
|
+
runState.stopReason = "error";
|
|
853
|
+
runState.errorMessage = err instanceof Error ? err.message : String(err);
|
|
854
|
+
log.logWarning(`[${channelId}] Runner failed`, runState.errorMessage);
|
|
855
|
+
}
|
|
856
|
+
finally {
|
|
857
|
+
await queueChain;
|
|
858
|
+
const finalOutcome = runState.finalOutcome;
|
|
859
|
+
const finalOutcomeText = getFinalOutcomeText(finalOutcome);
|
|
860
|
+
try {
|
|
861
|
+
if (runState.stopReason === "error" && runState.errorMessage && !runState.finalResponseDelivered) {
|
|
862
|
+
try {
|
|
863
|
+
await ctx.replaceMessage("_Sorry, something went wrong_");
|
|
864
|
+
}
|
|
865
|
+
catch (err) {
|
|
866
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
867
|
+
log.logWarning("Failed to post error message", errMsg);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
else if (isSilentOutcome(finalOutcome)) {
|
|
871
|
+
try {
|
|
872
|
+
await ctx.deleteMessage();
|
|
873
|
+
log.logInfo("Silent response - deleted message");
|
|
874
|
+
}
|
|
875
|
+
catch (err) {
|
|
876
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
877
|
+
log.logWarning("Failed to delete message for silent response", errMsg);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
else if (finalOutcomeText && !runState.finalResponseDelivered) {
|
|
881
|
+
try {
|
|
882
|
+
await ctx.replaceMessage(finalOutcomeText);
|
|
883
|
+
}
|
|
884
|
+
catch (err) {
|
|
885
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
886
|
+
log.logWarning("Failed to replace message with final text", errMsg);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
await ctx.flush();
|
|
890
|
+
}
|
|
891
|
+
finally {
|
|
892
|
+
await ctx.close();
|
|
893
|
+
}
|
|
894
|
+
// Log usage summary
|
|
895
|
+
if (runState.totalUsage.cost.total > 0) {
|
|
896
|
+
const messages = session.messages;
|
|
897
|
+
const lastAssistantMessage = messages
|
|
898
|
+
.slice()
|
|
899
|
+
.reverse()
|
|
900
|
+
.find((m) => m.role === "assistant" && m.stopReason !== "aborted");
|
|
901
|
+
const contextTokens = lastAssistantMessage
|
|
902
|
+
? lastAssistantMessage.usage.input +
|
|
903
|
+
lastAssistantMessage.usage.output +
|
|
904
|
+
lastAssistantMessage.usage.cacheRead +
|
|
905
|
+
lastAssistantMessage.usage.cacheWrite
|
|
906
|
+
: 0;
|
|
907
|
+
const currentRunModel = session.model ?? activeModel;
|
|
908
|
+
const contextWindow = currentRunModel.contextWindow || 200000;
|
|
909
|
+
log.logUsageSummary(runState.logCtx, runState.totalUsage, contextTokens, contextWindow);
|
|
910
|
+
}
|
|
911
|
+
// Clear run state
|
|
912
|
+
runState.ctx = null;
|
|
913
|
+
runState.logCtx = null;
|
|
914
|
+
runState.queue = null;
|
|
915
|
+
}
|
|
916
|
+
return { stopReason: runState.stopReason, errorMessage: runState.errorMessage };
|
|
917
|
+
},
|
|
918
|
+
abort() {
|
|
919
|
+
session.abort();
|
|
920
|
+
},
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Translate container path back to host path for file operations
|
|
925
|
+
*/
|
|
926
|
+
export function translateToHostPath(containerPath, channelDir, workspacePath, channelId) {
|
|
927
|
+
if (workspacePath === "/workspace") {
|
|
928
|
+
const prefix = `/workspace/${channelId}/`;
|
|
929
|
+
if (containerPath.startsWith(prefix)) {
|
|
930
|
+
return join(channelDir, containerPath.slice(prefix.length));
|
|
931
|
+
}
|
|
932
|
+
if (containerPath.startsWith("/workspace/")) {
|
|
933
|
+
return join(channelDir, "..", containerPath.slice("/workspace/".length));
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
return containerPath;
|
|
937
|
+
}
|
|
938
|
+
//# sourceMappingURL=agent.js.map
|