@mariozechner/pi-mom 0.18.1 → 0.18.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -11
- package/README.md +30 -18
- package/dist/agent.d.ts +14 -2
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +294 -331
- package/dist/agent.js.map +1 -1
- package/dist/context.d.ts +132 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +538 -0
- package/dist/context.js.map +1 -0
- package/dist/log.d.ts +1 -1
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +14 -1
- package/dist/log.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +166 -90
- package/dist/main.js.map +1 -1
- package/dist/slack.d.ts +86 -55
- package/dist/slack.d.ts.map +1 -1
- package/dist/slack.js +322 -418
- package/dist/slack.js.map +1 -1
- package/package.json +4 -3
package/dist/agent.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { Agent, ProviderTransport } from "@mariozechner/pi-agent-core";
|
|
2
2
|
import { getModel } from "@mariozechner/pi-ai";
|
|
3
|
+
import { AgentSession, messageTransformer } from "@mariozechner/pi-coding-agent";
|
|
3
4
|
import { existsSync, readFileSync } from "fs";
|
|
4
5
|
import { mkdir, writeFile } from "fs/promises";
|
|
5
6
|
import { join } from "path";
|
|
7
|
+
import { MomSessionManager, MomSettingsManager } from "./context.js";
|
|
6
8
|
import * as log from "./log.js";
|
|
7
9
|
import { createExecutor } from "./sandbox.js";
|
|
8
10
|
import { createMomTools, setUploadFunction } from "./tools/index.js";
|
|
9
|
-
// Hardcoded model for now
|
|
11
|
+
// Hardcoded model for now - TODO: make configurable (issue #63)
|
|
10
12
|
const model = getModel("anthropic", "claude-sonnet-4-5");
|
|
11
13
|
/**
|
|
12
14
|
* Convert Date.now() to Slack timestamp format (seconds.microseconds)
|
|
@@ -17,16 +19,14 @@ let tsCounter = 0;
|
|
|
17
19
|
function toSlackTs() {
|
|
18
20
|
const now = Date.now();
|
|
19
21
|
if (now === lastTsMs) {
|
|
20
|
-
// Same millisecond - increment counter for sub-ms ordering
|
|
21
22
|
tsCounter++;
|
|
22
23
|
}
|
|
23
24
|
else {
|
|
24
|
-
// New millisecond - reset counter
|
|
25
25
|
lastTsMs = now;
|
|
26
26
|
tsCounter = 0;
|
|
27
27
|
}
|
|
28
28
|
const seconds = Math.floor(now / 1000);
|
|
29
|
-
const micros = (now % 1000) * 1000 + tsCounter;
|
|
29
|
+
const micros = (now % 1000) * 1000 + tsCounter;
|
|
30
30
|
return `${seconds}.${micros.toString().padStart(6, "0")}`;
|
|
31
31
|
}
|
|
32
32
|
function getAnthropicApiKey() {
|
|
@@ -36,77 +36,6 @@ function getAnthropicApiKey() {
|
|
|
36
36
|
}
|
|
37
37
|
return key;
|
|
38
38
|
}
|
|
39
|
-
function getRecentMessages(channelDir, turnCount) {
|
|
40
|
-
const logPath = join(channelDir, "log.jsonl");
|
|
41
|
-
if (!existsSync(logPath)) {
|
|
42
|
-
return "(no message history yet)";
|
|
43
|
-
}
|
|
44
|
-
const content = readFileSync(logPath, "utf-8");
|
|
45
|
-
const lines = content.trim().split("\n").filter(Boolean);
|
|
46
|
-
if (lines.length === 0) {
|
|
47
|
-
return "(no message history yet)";
|
|
48
|
-
}
|
|
49
|
-
// Parse all messages and sort by Slack timestamp
|
|
50
|
-
// (attachment downloads can cause out-of-order logging)
|
|
51
|
-
const messages = [];
|
|
52
|
-
for (const line of lines) {
|
|
53
|
-
try {
|
|
54
|
-
messages.push(JSON.parse(line));
|
|
55
|
-
}
|
|
56
|
-
catch { }
|
|
57
|
-
}
|
|
58
|
-
messages.sort((a, b) => {
|
|
59
|
-
const tsA = parseFloat(a.ts || "0");
|
|
60
|
-
const tsB = parseFloat(b.ts || "0");
|
|
61
|
-
return tsA - tsB;
|
|
62
|
-
});
|
|
63
|
-
// Group into "turns" - a turn is either:
|
|
64
|
-
// - A single user message (isBot: false)
|
|
65
|
-
// - A sequence of consecutive bot messages (isBot: true) coalesced into one turn
|
|
66
|
-
// We walk backwards to get the last N turns
|
|
67
|
-
const turns = [];
|
|
68
|
-
let currentTurn = [];
|
|
69
|
-
let lastWasBot = null;
|
|
70
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
71
|
-
const msg = messages[i];
|
|
72
|
-
const isBot = msg.isBot === true;
|
|
73
|
-
if (lastWasBot === null) {
|
|
74
|
-
// First message
|
|
75
|
-
currentTurn.unshift(msg);
|
|
76
|
-
lastWasBot = isBot;
|
|
77
|
-
}
|
|
78
|
-
else if (isBot && lastWasBot) {
|
|
79
|
-
// Consecutive bot messages - same turn
|
|
80
|
-
currentTurn.unshift(msg);
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
// Transition - save current turn and start new one
|
|
84
|
-
turns.unshift(currentTurn);
|
|
85
|
-
currentTurn = [msg];
|
|
86
|
-
lastWasBot = isBot;
|
|
87
|
-
// Stop if we have enough turns
|
|
88
|
-
if (turns.length >= turnCount) {
|
|
89
|
-
break;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
// Don't forget the last turn we were building
|
|
94
|
-
if (currentTurn.length > 0 && turns.length < turnCount) {
|
|
95
|
-
turns.unshift(currentTurn);
|
|
96
|
-
}
|
|
97
|
-
// Flatten turns back to messages and format as TSV
|
|
98
|
-
const formatted = [];
|
|
99
|
-
for (const turn of turns) {
|
|
100
|
-
for (const msg of turn) {
|
|
101
|
-
const date = (msg.date || "").substring(0, 19);
|
|
102
|
-
const user = msg.userName || msg.user || "";
|
|
103
|
-
const text = msg.text || "";
|
|
104
|
-
const attachments = (msg.attachments || []).map((a) => a.local).join(",");
|
|
105
|
-
formatted.push(`${date}\t${user}\t${text}\t${attachments}`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return formatted.join("\n");
|
|
109
|
-
}
|
|
110
39
|
function getMemory(channelDir) {
|
|
111
40
|
const parts = [];
|
|
112
41
|
// Read workspace-level memory (shared across all channels)
|
|
@@ -155,13 +84,12 @@ function buildSystemPrompt(workspacePath, channelId, memory, sandboxConfig, chan
|
|
|
155
84
|
: `You are running directly on the host machine.
|
|
156
85
|
- Bash working directory: ${process.cwd()}
|
|
157
86
|
- Be careful with system modifications`;
|
|
158
|
-
const currentDate = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
|
|
159
|
-
const currentDateTime = new Date().toISOString(); // Full ISO 8601
|
|
160
87
|
return `You are mom, a Slack bot assistant. Be concise. No emojis.
|
|
161
88
|
|
|
162
89
|
## Context
|
|
163
|
-
-
|
|
164
|
-
- You
|
|
90
|
+
- For current date/time, use: date
|
|
91
|
+
- You have access to previous conversation context including tool results from prior turns.
|
|
92
|
+
- For older history beyond your context, search log.jsonl (contains user messages and your final responses, but not tool results).
|
|
165
93
|
|
|
166
94
|
## Slack Formatting (mrkdwn, NOT Markdown)
|
|
167
95
|
Bold: *text*, Italic: _text_, Code: \`code\`, Block: \`\`\`code\`\`\`, Links: <url|text>
|
|
@@ -183,7 +111,7 @@ ${workspacePath}/
|
|
|
183
111
|
├── skills/ # Global CLI tools you create
|
|
184
112
|
└── ${channelId}/ # This channel
|
|
185
113
|
├── MEMORY.md # Channel-specific memory
|
|
186
|
-
├── log.jsonl #
|
|
114
|
+
├── log.jsonl # Message history (no tool results)
|
|
187
115
|
├── attachments/ # User-shared files
|
|
188
116
|
├── scratch/ # Your working directory
|
|
189
117
|
└── skills/ # Channel-specific tools
|
|
@@ -212,36 +140,26 @@ Maintain ${workspacePath}/SYSTEM.md to log all environment modifications:
|
|
|
212
140
|
|
|
213
141
|
Update this file whenever you modify the environment. On fresh container, read it first to restore your setup.
|
|
214
142
|
|
|
215
|
-
## Log Queries (
|
|
143
|
+
## Log Queries (for older history)
|
|
216
144
|
Format: \`{"date":"...","ts":"...","user":"...","userName":"...","text":"...","isBot":false}\`
|
|
217
|
-
The log contains user messages
|
|
145
|
+
The log contains user messages and your final responses (not tool calls/results).
|
|
218
146
|
${isDocker ? "Install jq: apk add jq" : ""}
|
|
219
147
|
|
|
220
|
-
**Conversation only (excludes tool calls/results) - use for summaries:**
|
|
221
148
|
\`\`\`bash
|
|
222
|
-
# Recent
|
|
223
|
-
|
|
149
|
+
# Recent messages
|
|
150
|
+
tail -30 log.jsonl | jq -c '{date: .date[0:19], user: (.userName // .user), text}'
|
|
224
151
|
|
|
225
|
-
#
|
|
226
|
-
grep
|
|
152
|
+
# Search for specific topic
|
|
153
|
+
grep -i "topic" log.jsonl | jq -c '{date: .date[0:19], user: (.userName // .user), text}'
|
|
227
154
|
|
|
228
|
-
#
|
|
229
|
-
grep '"userName":"mario"' log.jsonl |
|
|
230
|
-
\`\`\`
|
|
231
|
-
|
|
232
|
-
**Full details (includes tool calls) - use when you need technical context:**
|
|
233
|
-
\`\`\`bash
|
|
234
|
-
# Raw recent entries
|
|
235
|
-
tail -20 log.jsonl | jq -c '{date: .date[0:19], user: (.userName // .user), text}'
|
|
236
|
-
|
|
237
|
-
# Count all messages
|
|
238
|
-
wc -l log.jsonl
|
|
155
|
+
# Messages from specific user
|
|
156
|
+
grep '"userName":"mario"' log.jsonl | tail -20 | jq -c '{date: .date[0:19], text}'
|
|
239
157
|
\`\`\`
|
|
240
158
|
|
|
241
159
|
## Tools
|
|
242
160
|
- bash: Run shell commands (primary tool). Install packages as needed.
|
|
243
161
|
- read: Read files
|
|
244
|
-
- write: Create/overwrite files
|
|
162
|
+
- write: Create/overwrite files
|
|
245
163
|
- edit: Surgical file edits
|
|
246
164
|
- attach: Share files to Slack
|
|
247
165
|
|
|
@@ -254,11 +172,9 @@ function truncate(text, maxLen) {
|
|
|
254
172
|
return text.substring(0, maxLen - 3) + "...";
|
|
255
173
|
}
|
|
256
174
|
function extractToolResultText(result) {
|
|
257
|
-
// If it's already a string, return it
|
|
258
175
|
if (typeof result === "string") {
|
|
259
176
|
return result;
|
|
260
177
|
}
|
|
261
|
-
// If it's an object with content array (tool result format)
|
|
262
178
|
if (result &&
|
|
263
179
|
typeof result === "object" &&
|
|
264
180
|
"content" in result &&
|
|
@@ -274,16 +190,13 @@ function extractToolResultText(result) {
|
|
|
274
190
|
return textParts.join("\n");
|
|
275
191
|
}
|
|
276
192
|
}
|
|
277
|
-
// Fallback to JSON
|
|
278
193
|
return JSON.stringify(result);
|
|
279
194
|
}
|
|
280
195
|
function formatToolArgsForSlack(_toolName, args) {
|
|
281
196
|
const lines = [];
|
|
282
197
|
for (const [key, value] of Object.entries(args)) {
|
|
283
|
-
// Skip the label - it's already shown
|
|
284
198
|
if (key === "label")
|
|
285
199
|
continue;
|
|
286
|
-
// For read tool, format path with offset/limit
|
|
287
200
|
if (key === "path" && typeof value === "string") {
|
|
288
201
|
const offset = args.offset;
|
|
289
202
|
const limit = args.limit;
|
|
@@ -295,10 +208,8 @@ function formatToolArgsForSlack(_toolName, args) {
|
|
|
295
208
|
}
|
|
296
209
|
continue;
|
|
297
210
|
}
|
|
298
|
-
// Skip offset/limit since we already handled them
|
|
299
211
|
if (key === "offset" || key === "limit")
|
|
300
212
|
continue;
|
|
301
|
-
// For other values, format them
|
|
302
213
|
if (typeof value === "string") {
|
|
303
214
|
lines.push(value);
|
|
304
215
|
}
|
|
@@ -308,259 +219,298 @@ function formatToolArgsForSlack(_toolName, args) {
|
|
|
308
219
|
}
|
|
309
220
|
return lines.join("\n");
|
|
310
221
|
}
|
|
311
|
-
|
|
312
|
-
|
|
222
|
+
// Cache runners per channel
|
|
223
|
+
const channelRunners = new Map();
|
|
224
|
+
/**
|
|
225
|
+
* Get or create an AgentRunner for a channel.
|
|
226
|
+
* Runners are cached - one per channel, persistent across messages.
|
|
227
|
+
*/
|
|
228
|
+
export function getOrCreateRunner(sandboxConfig, channelId, channelDir) {
|
|
229
|
+
const existing = channelRunners.get(channelId);
|
|
230
|
+
if (existing)
|
|
231
|
+
return existing;
|
|
232
|
+
const runner = createRunner(sandboxConfig, channelId, channelDir);
|
|
233
|
+
channelRunners.set(channelId, runner);
|
|
234
|
+
return runner;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Create a new AgentRunner for a channel.
|
|
238
|
+
* Sets up the session and subscribes to events once.
|
|
239
|
+
*/
|
|
240
|
+
function createRunner(sandboxConfig, channelId, channelDir) {
|
|
313
241
|
const executor = createExecutor(sandboxConfig);
|
|
242
|
+
const workspacePath = executor.getWorkspacePath(channelDir.replace(`/${channelId}`, ""));
|
|
243
|
+
// Create tools
|
|
244
|
+
const tools = createMomTools(executor);
|
|
245
|
+
// Initial system prompt (will be updated each run with fresh memory/channels/users)
|
|
246
|
+
const memory = getMemory(channelDir);
|
|
247
|
+
const systemPrompt = buildSystemPrompt(workspacePath, channelId, memory, sandboxConfig, [], []);
|
|
248
|
+
// Create session manager and settings manager
|
|
249
|
+
// Pass model info so new sessions get a header written immediately
|
|
250
|
+
const sessionManager = new MomSessionManager(channelDir, {
|
|
251
|
+
provider: model.provider,
|
|
252
|
+
id: model.id,
|
|
253
|
+
thinkingLevel: "off",
|
|
254
|
+
});
|
|
255
|
+
const settingsManager = new MomSettingsManager(join(channelDir, ".."));
|
|
256
|
+
// Create agent
|
|
257
|
+
const agent = new Agent({
|
|
258
|
+
initialState: {
|
|
259
|
+
systemPrompt,
|
|
260
|
+
model,
|
|
261
|
+
thinkingLevel: "off",
|
|
262
|
+
tools,
|
|
263
|
+
},
|
|
264
|
+
messageTransformer,
|
|
265
|
+
transport: new ProviderTransport({
|
|
266
|
+
getApiKey: async () => getAnthropicApiKey(),
|
|
267
|
+
}),
|
|
268
|
+
});
|
|
269
|
+
// Load existing messages
|
|
270
|
+
const loadedSession = sessionManager.loadSession();
|
|
271
|
+
if (loadedSession.messages.length > 0) {
|
|
272
|
+
agent.replaceMessages(loadedSession.messages);
|
|
273
|
+
log.logInfo(`[${channelId}] Loaded ${loadedSession.messages.length} messages from context.jsonl`);
|
|
274
|
+
}
|
|
275
|
+
// Create AgentSession wrapper
|
|
276
|
+
const session = new AgentSession({
|
|
277
|
+
agent,
|
|
278
|
+
sessionManager: sessionManager,
|
|
279
|
+
settingsManager: settingsManager,
|
|
280
|
+
});
|
|
281
|
+
// Mutable per-run state - event handler references this
|
|
282
|
+
const runState = {
|
|
283
|
+
ctx: null,
|
|
284
|
+
logCtx: null,
|
|
285
|
+
queue: null,
|
|
286
|
+
pendingTools: new Map(),
|
|
287
|
+
totalUsage: {
|
|
288
|
+
input: 0,
|
|
289
|
+
output: 0,
|
|
290
|
+
cacheRead: 0,
|
|
291
|
+
cacheWrite: 0,
|
|
292
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
293
|
+
},
|
|
294
|
+
stopReason: "stop",
|
|
295
|
+
};
|
|
296
|
+
// Subscribe to events ONCE
|
|
297
|
+
session.subscribe(async (event) => {
|
|
298
|
+
// Skip if no active run
|
|
299
|
+
if (!runState.ctx || !runState.logCtx || !runState.queue)
|
|
300
|
+
return;
|
|
301
|
+
const { ctx, logCtx, queue, pendingTools } = runState;
|
|
302
|
+
if (event.type === "tool_execution_start") {
|
|
303
|
+
const agentEvent = event;
|
|
304
|
+
const args = agentEvent.args;
|
|
305
|
+
const label = args.label || agentEvent.toolName;
|
|
306
|
+
pendingTools.set(agentEvent.toolCallId, {
|
|
307
|
+
toolName: agentEvent.toolName,
|
|
308
|
+
args: agentEvent.args,
|
|
309
|
+
startTime: Date.now(),
|
|
310
|
+
});
|
|
311
|
+
log.logToolStart(logCtx, agentEvent.toolName, label, agentEvent.args);
|
|
312
|
+
queue.enqueue(() => ctx.respond(`_→ ${label}_`, false), "tool label");
|
|
313
|
+
}
|
|
314
|
+
else if (event.type === "tool_execution_end") {
|
|
315
|
+
const agentEvent = event;
|
|
316
|
+
const resultStr = extractToolResultText(agentEvent.result);
|
|
317
|
+
const pending = pendingTools.get(agentEvent.toolCallId);
|
|
318
|
+
pendingTools.delete(agentEvent.toolCallId);
|
|
319
|
+
const durationMs = pending ? Date.now() - pending.startTime : 0;
|
|
320
|
+
if (agentEvent.isError) {
|
|
321
|
+
log.logToolError(logCtx, agentEvent.toolName, durationMs, resultStr);
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
log.logToolSuccess(logCtx, agentEvent.toolName, durationMs, resultStr);
|
|
325
|
+
}
|
|
326
|
+
// Post args + result to thread
|
|
327
|
+
const label = pending?.args ? pending.args.label : undefined;
|
|
328
|
+
const argsFormatted = pending
|
|
329
|
+
? formatToolArgsForSlack(agentEvent.toolName, pending.args)
|
|
330
|
+
: "(args not found)";
|
|
331
|
+
const duration = (durationMs / 1000).toFixed(1);
|
|
332
|
+
let threadMessage = `*${agentEvent.isError ? "✗" : "✓"} ${agentEvent.toolName}*`;
|
|
333
|
+
if (label)
|
|
334
|
+
threadMessage += `: ${label}`;
|
|
335
|
+
threadMessage += ` (${duration}s)\n`;
|
|
336
|
+
if (argsFormatted)
|
|
337
|
+
threadMessage += "```\n" + argsFormatted + "\n```\n";
|
|
338
|
+
threadMessage += "*Result:*\n```\n" + resultStr + "\n```";
|
|
339
|
+
queue.enqueueMessage(threadMessage, "thread", "tool result thread", false);
|
|
340
|
+
if (agentEvent.isError) {
|
|
341
|
+
queue.enqueue(() => ctx.respond(`_Error: ${truncate(resultStr, 200)}_`, false), "tool error");
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
else if (event.type === "message_start") {
|
|
345
|
+
const agentEvent = event;
|
|
346
|
+
if (agentEvent.message.role === "assistant") {
|
|
347
|
+
log.logResponseStart(logCtx);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
else if (event.type === "message_end") {
|
|
351
|
+
const agentEvent = event;
|
|
352
|
+
if (agentEvent.message.role === "assistant") {
|
|
353
|
+
const assistantMsg = agentEvent.message;
|
|
354
|
+
if (assistantMsg.stopReason) {
|
|
355
|
+
runState.stopReason = assistantMsg.stopReason;
|
|
356
|
+
}
|
|
357
|
+
if (assistantMsg.usage) {
|
|
358
|
+
runState.totalUsage.input += assistantMsg.usage.input;
|
|
359
|
+
runState.totalUsage.output += assistantMsg.usage.output;
|
|
360
|
+
runState.totalUsage.cacheRead += assistantMsg.usage.cacheRead;
|
|
361
|
+
runState.totalUsage.cacheWrite += assistantMsg.usage.cacheWrite;
|
|
362
|
+
runState.totalUsage.cost.input += assistantMsg.usage.cost.input;
|
|
363
|
+
runState.totalUsage.cost.output += assistantMsg.usage.cost.output;
|
|
364
|
+
runState.totalUsage.cost.cacheRead += assistantMsg.usage.cost.cacheRead;
|
|
365
|
+
runState.totalUsage.cost.cacheWrite += assistantMsg.usage.cost.cacheWrite;
|
|
366
|
+
runState.totalUsage.cost.total += assistantMsg.usage.cost.total;
|
|
367
|
+
}
|
|
368
|
+
const content = agentEvent.message.content;
|
|
369
|
+
const thinkingParts = [];
|
|
370
|
+
const textParts = [];
|
|
371
|
+
for (const part of content) {
|
|
372
|
+
if (part.type === "thinking") {
|
|
373
|
+
thinkingParts.push(part.thinking);
|
|
374
|
+
}
|
|
375
|
+
else if (part.type === "text") {
|
|
376
|
+
textParts.push(part.text);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const text = textParts.join("\n");
|
|
380
|
+
for (const thinking of thinkingParts) {
|
|
381
|
+
log.logThinking(logCtx, thinking);
|
|
382
|
+
queue.enqueueMessage(`_${thinking}_`, "main", "thinking main");
|
|
383
|
+
queue.enqueueMessage(`_${thinking}_`, "thread", "thinking thread", false);
|
|
384
|
+
}
|
|
385
|
+
if (text.trim()) {
|
|
386
|
+
log.logResponse(logCtx, text);
|
|
387
|
+
queue.enqueueMessage(text, "main", "response main");
|
|
388
|
+
queue.enqueueMessage(text, "thread", "response thread", false);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
else if (event.type === "auto_compaction_start") {
|
|
393
|
+
log.logInfo(`Auto-compaction started (reason: ${event.reason})`);
|
|
394
|
+
queue.enqueue(() => ctx.respond("_Compacting context..._", false), "compaction start");
|
|
395
|
+
}
|
|
396
|
+
else if (event.type === "auto_compaction_end") {
|
|
397
|
+
const compEvent = event;
|
|
398
|
+
if (compEvent.result) {
|
|
399
|
+
log.logInfo(`Auto-compaction complete: ${compEvent.result.tokensBefore} tokens compacted`);
|
|
400
|
+
}
|
|
401
|
+
else if (compEvent.aborted) {
|
|
402
|
+
log.logInfo("Auto-compaction aborted");
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
else if (event.type === "auto_retry_start") {
|
|
406
|
+
const retryEvent = event;
|
|
407
|
+
log.logWarning(`Retrying (${retryEvent.attempt}/${retryEvent.maxAttempts})`, retryEvent.errorMessage);
|
|
408
|
+
queue.enqueue(() => ctx.respond(`_Retrying (${retryEvent.attempt}/${retryEvent.maxAttempts})..._`, false), "retry");
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
// Slack message limit
|
|
412
|
+
const SLACK_MAX_LENGTH = 40000;
|
|
413
|
+
const splitForSlack = (text) => {
|
|
414
|
+
if (text.length <= SLACK_MAX_LENGTH)
|
|
415
|
+
return [text];
|
|
416
|
+
const parts = [];
|
|
417
|
+
let remaining = text;
|
|
418
|
+
let partNum = 1;
|
|
419
|
+
while (remaining.length > 0) {
|
|
420
|
+
const chunk = remaining.substring(0, SLACK_MAX_LENGTH - 50);
|
|
421
|
+
remaining = remaining.substring(SLACK_MAX_LENGTH - 50);
|
|
422
|
+
const suffix = remaining.length > 0 ? `\n_(continued ${partNum}...)_` : "";
|
|
423
|
+
parts.push(chunk + suffix);
|
|
424
|
+
partNum++;
|
|
425
|
+
}
|
|
426
|
+
return parts;
|
|
427
|
+
};
|
|
314
428
|
return {
|
|
315
|
-
async run(ctx,
|
|
429
|
+
async run(ctx, _store, _pendingMessages) {
|
|
316
430
|
// Ensure channel directory exists
|
|
317
431
|
await mkdir(channelDir, { recursive: true });
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const
|
|
432
|
+
// Reload messages from context.jsonl
|
|
433
|
+
// This picks up any messages synced from log.jsonl before this run
|
|
434
|
+
const reloadedSession = sessionManager.loadSession();
|
|
435
|
+
if (reloadedSession.messages.length > 0) {
|
|
436
|
+
agent.replaceMessages(reloadedSession.messages);
|
|
437
|
+
log.logInfo(`[${channelId}] Reloaded ${reloadedSession.messages.length} messages from context`);
|
|
438
|
+
}
|
|
439
|
+
// Update system prompt with fresh memory and channel/user info
|
|
321
440
|
const memory = getMemory(channelDir);
|
|
322
441
|
const systemPrompt = buildSystemPrompt(workspacePath, channelId, memory, sandboxConfig, ctx.channels, ctx.users);
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
log.logInfo(`Channels: ${ctx.channels.length}, Users: ${ctx.users.length}`);
|
|
326
|
-
// Set up file upload function for the attach tool
|
|
327
|
-
// For Docker, we need to translate paths back to host
|
|
442
|
+
session.agent.setSystemPrompt(systemPrompt);
|
|
443
|
+
// Set up file upload function
|
|
328
444
|
setUploadFunction(async (filePath, title) => {
|
|
329
445
|
const hostPath = translateToHostPath(filePath, channelDir, workspacePath, channelId);
|
|
330
446
|
await ctx.uploadFile(hostPath, title);
|
|
331
447
|
});
|
|
332
|
-
//
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
agent = new Agent({
|
|
336
|
-
initialState: {
|
|
337
|
-
systemPrompt,
|
|
338
|
-
model,
|
|
339
|
-
thinkingLevel: "off",
|
|
340
|
-
tools,
|
|
341
|
-
},
|
|
342
|
-
transport: new ProviderTransport({
|
|
343
|
-
getApiKey: async () => getAnthropicApiKey(),
|
|
344
|
-
}),
|
|
345
|
-
});
|
|
346
|
-
// Create logging context
|
|
347
|
-
const logCtx = {
|
|
448
|
+
// Reset per-run state
|
|
449
|
+
runState.ctx = ctx;
|
|
450
|
+
runState.logCtx = {
|
|
348
451
|
channelId: ctx.message.channel,
|
|
349
452
|
userName: ctx.message.userName,
|
|
350
453
|
channelName: ctx.channelName,
|
|
351
454
|
};
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
// Track usage across all assistant messages in this run
|
|
355
|
-
const totalUsage = {
|
|
455
|
+
runState.pendingTools.clear();
|
|
456
|
+
runState.totalUsage = {
|
|
356
457
|
input: 0,
|
|
357
458
|
output: 0,
|
|
358
459
|
cacheRead: 0,
|
|
359
460
|
cacheWrite: 0,
|
|
360
|
-
cost: {
|
|
361
|
-
input: 0,
|
|
362
|
-
output: 0,
|
|
363
|
-
cacheRead: 0,
|
|
364
|
-
cacheWrite: 0,
|
|
365
|
-
total: 0,
|
|
366
|
-
},
|
|
367
|
-
};
|
|
368
|
-
// Track stop reason
|
|
369
|
-
let stopReason = "stop";
|
|
370
|
-
// Slack message limit is 40,000 characters - split into multiple messages if needed
|
|
371
|
-
const SLACK_MAX_LENGTH = 40000;
|
|
372
|
-
const splitForSlack = (text) => {
|
|
373
|
-
if (text.length <= SLACK_MAX_LENGTH)
|
|
374
|
-
return [text];
|
|
375
|
-
const parts = [];
|
|
376
|
-
let remaining = text;
|
|
377
|
-
let partNum = 1;
|
|
378
|
-
while (remaining.length > 0) {
|
|
379
|
-
const chunk = remaining.substring(0, SLACK_MAX_LENGTH - 50);
|
|
380
|
-
remaining = remaining.substring(SLACK_MAX_LENGTH - 50);
|
|
381
|
-
const suffix = remaining.length > 0 ? `\n_(continued ${partNum}...)_` : "";
|
|
382
|
-
parts.push(chunk + suffix);
|
|
383
|
-
partNum++;
|
|
384
|
-
}
|
|
385
|
-
return parts;
|
|
461
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
386
462
|
};
|
|
387
|
-
|
|
388
|
-
//
|
|
389
|
-
|
|
390
|
-
|
|
463
|
+
runState.stopReason = "stop";
|
|
464
|
+
// Create queue for this run
|
|
465
|
+
let queueChain = Promise.resolve();
|
|
466
|
+
runState.queue = {
|
|
391
467
|
enqueue(fn, errorContext) {
|
|
392
|
-
|
|
468
|
+
queueChain = queueChain.then(async () => {
|
|
393
469
|
try {
|
|
394
470
|
await fn();
|
|
395
471
|
}
|
|
396
472
|
catch (err) {
|
|
397
473
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
398
474
|
log.logWarning(`Slack API error (${errorContext})`, errMsg);
|
|
399
|
-
// Try to post error to thread, but don't crash if that fails too
|
|
400
475
|
try {
|
|
401
476
|
await ctx.respondInThread(`_Error: ${errMsg}_`);
|
|
402
477
|
}
|
|
403
478
|
catch {
|
|
404
|
-
// Ignore
|
|
479
|
+
// Ignore
|
|
405
480
|
}
|
|
406
481
|
}
|
|
407
482
|
});
|
|
408
483
|
},
|
|
409
|
-
|
|
410
|
-
enqueueMessage(text, target, errorContext, log = true) {
|
|
484
|
+
enqueueMessage(text, target, errorContext, doLog = true) {
|
|
411
485
|
const parts = splitForSlack(text);
|
|
412
486
|
for (const part of parts) {
|
|
413
|
-
this.enqueue(() => (target === "main" ? ctx.respond(part,
|
|
487
|
+
this.enqueue(() => (target === "main" ? ctx.respond(part, doLog) : ctx.respondInThread(part)), errorContext);
|
|
414
488
|
}
|
|
415
489
|
},
|
|
416
|
-
flush() {
|
|
417
|
-
return this.chain;
|
|
418
|
-
},
|
|
419
490
|
};
|
|
420
|
-
//
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
// Show label in main message only
|
|
444
|
-
queue.enqueue(() => ctx.respond(`_→ ${label}_`, false), "tool label");
|
|
445
|
-
break;
|
|
446
|
-
}
|
|
447
|
-
case "tool_execution_end": {
|
|
448
|
-
const resultStr = extractToolResultText(event.result);
|
|
449
|
-
const pending = pendingTools.get(event.toolCallId);
|
|
450
|
-
pendingTools.delete(event.toolCallId);
|
|
451
|
-
const durationMs = pending ? Date.now() - pending.startTime : 0;
|
|
452
|
-
// Log to console
|
|
453
|
-
if (event.isError) {
|
|
454
|
-
log.logToolError(logCtx, event.toolName, durationMs, resultStr);
|
|
455
|
-
}
|
|
456
|
-
else {
|
|
457
|
-
log.logToolSuccess(logCtx, event.toolName, durationMs, resultStr);
|
|
458
|
-
}
|
|
459
|
-
// Log to jsonl
|
|
460
|
-
await store.logMessage(ctx.message.channel, {
|
|
461
|
-
date: new Date().toISOString(),
|
|
462
|
-
ts: toSlackTs(),
|
|
463
|
-
user: "bot",
|
|
464
|
-
text: `[Tool Result] ${event.toolName}: ${event.isError ? "ERROR: " : ""}${resultStr}`,
|
|
465
|
-
attachments: [],
|
|
466
|
-
isBot: true,
|
|
467
|
-
});
|
|
468
|
-
// Post args + result together in thread
|
|
469
|
-
const label = pending?.args ? pending.args.label : undefined;
|
|
470
|
-
const argsFormatted = pending
|
|
471
|
-
? formatToolArgsForSlack(event.toolName, pending.args)
|
|
472
|
-
: "(args not found)";
|
|
473
|
-
const duration = (durationMs / 1000).toFixed(1);
|
|
474
|
-
let threadMessage = `*${event.isError ? "✗" : "✓"} ${event.toolName}*`;
|
|
475
|
-
if (label) {
|
|
476
|
-
threadMessage += `: ${label}`;
|
|
477
|
-
}
|
|
478
|
-
threadMessage += ` (${duration}s)\n`;
|
|
479
|
-
if (argsFormatted) {
|
|
480
|
-
threadMessage += "```\n" + argsFormatted + "\n```\n";
|
|
481
|
-
}
|
|
482
|
-
threadMessage += "*Result:*\n```\n" + resultStr + "\n```";
|
|
483
|
-
queue.enqueueMessage(threadMessage, "thread", "tool result thread", false);
|
|
484
|
-
// Show brief error in main message if failed
|
|
485
|
-
if (event.isError) {
|
|
486
|
-
queue.enqueue(() => ctx.respond(`_Error: ${truncate(resultStr, 200)}_`, false), "tool error");
|
|
487
|
-
}
|
|
488
|
-
break;
|
|
489
|
-
}
|
|
490
|
-
case "message_update": {
|
|
491
|
-
// No longer stream to console - just track that we're streaming
|
|
492
|
-
break;
|
|
493
|
-
}
|
|
494
|
-
case "message_start":
|
|
495
|
-
if (event.message.role === "assistant") {
|
|
496
|
-
log.logResponseStart(logCtx);
|
|
497
|
-
}
|
|
498
|
-
break;
|
|
499
|
-
case "message_end":
|
|
500
|
-
if (event.message.role === "assistant") {
|
|
501
|
-
const assistantMsg = event.message; // AssistantMessage type
|
|
502
|
-
// Track stop reason
|
|
503
|
-
if (assistantMsg.stopReason) {
|
|
504
|
-
stopReason = assistantMsg.stopReason;
|
|
505
|
-
}
|
|
506
|
-
// Accumulate usage
|
|
507
|
-
if (assistantMsg.usage) {
|
|
508
|
-
totalUsage.input += assistantMsg.usage.input;
|
|
509
|
-
totalUsage.output += assistantMsg.usage.output;
|
|
510
|
-
totalUsage.cacheRead += assistantMsg.usage.cacheRead;
|
|
511
|
-
totalUsage.cacheWrite += assistantMsg.usage.cacheWrite;
|
|
512
|
-
totalUsage.cost.input += assistantMsg.usage.cost.input;
|
|
513
|
-
totalUsage.cost.output += assistantMsg.usage.cost.output;
|
|
514
|
-
totalUsage.cost.cacheRead += assistantMsg.usage.cost.cacheRead;
|
|
515
|
-
totalUsage.cost.cacheWrite += assistantMsg.usage.cost.cacheWrite;
|
|
516
|
-
totalUsage.cost.total += assistantMsg.usage.cost.total;
|
|
517
|
-
}
|
|
518
|
-
// Extract thinking and text from assistant message
|
|
519
|
-
const content = event.message.content;
|
|
520
|
-
const thinkingParts = [];
|
|
521
|
-
const textParts = [];
|
|
522
|
-
for (const part of content) {
|
|
523
|
-
if (part.type === "thinking") {
|
|
524
|
-
thinkingParts.push(part.thinking);
|
|
525
|
-
}
|
|
526
|
-
else if (part.type === "text") {
|
|
527
|
-
textParts.push(part.text);
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
const text = textParts.join("\n");
|
|
531
|
-
// Post thinking to main message and thread
|
|
532
|
-
for (const thinking of thinkingParts) {
|
|
533
|
-
log.logThinking(logCtx, thinking);
|
|
534
|
-
queue.enqueueMessage(`_${thinking}_`, "main", "thinking main");
|
|
535
|
-
queue.enqueueMessage(`_${thinking}_`, "thread", "thinking thread", false);
|
|
536
|
-
}
|
|
537
|
-
// Post text to main message and thread
|
|
538
|
-
if (text.trim()) {
|
|
539
|
-
log.logResponse(logCtx, text);
|
|
540
|
-
queue.enqueueMessage(text, "main", "response main");
|
|
541
|
-
queue.enqueueMessage(text, "thread", "response thread", false);
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
break;
|
|
545
|
-
}
|
|
546
|
-
});
|
|
547
|
-
// Run the agent with user's message
|
|
548
|
-
// Prepend recent messages to the user prompt (not system prompt) for better caching
|
|
549
|
-
// The current message is already the last entry in recentMessages
|
|
550
|
-
const userPrompt = `Conversation history (last 50 turns). Respond to the last message.\n` +
|
|
551
|
-
`Format: date TAB user TAB text TAB attachments\n\n` +
|
|
552
|
-
recentMessages;
|
|
553
|
-
// Debug: write full context to file
|
|
554
|
-
const toolDefs = tools.map((t) => ({ name: t.name, description: t.description, parameters: t.parameters }));
|
|
555
|
-
const debugPrompt = `=== SYSTEM PROMPT (${systemPrompt.length} chars) ===\n\n${systemPrompt}\n\n` +
|
|
556
|
-
`=== TOOL DEFINITIONS (${JSON.stringify(toolDefs).length} chars) ===\n\n${JSON.stringify(toolDefs, null, 2)}\n\n` +
|
|
557
|
-
`=== USER PROMPT (${userPrompt.length} chars) ===\n\n${userPrompt}`;
|
|
558
|
-
await writeFile(join(channelDir, "last_prompt.txt"), debugPrompt, "utf-8");
|
|
559
|
-
await agent.prompt(userPrompt);
|
|
560
|
-
// Wait for all queued respond calls to complete
|
|
561
|
-
await queue.flush();
|
|
562
|
-
// Get final assistant message text from agent state and replace main message
|
|
563
|
-
const messages = agent.state.messages;
|
|
491
|
+
// Log context info
|
|
492
|
+
log.logInfo(`Context sizes - system: ${systemPrompt.length} chars, memory: ${memory.length} chars`);
|
|
493
|
+
log.logInfo(`Channels: ${ctx.channels.length}, Users: ${ctx.users.length}`);
|
|
494
|
+
// Build user message with username prefix
|
|
495
|
+
// Format: "[username]: message" so LLM knows who's talking
|
|
496
|
+
let userMessage = `[${ctx.message.userName || "unknown"}]: ${ctx.message.text}`;
|
|
497
|
+
// Add attachment paths if any
|
|
498
|
+
if (ctx.message.attachments && ctx.message.attachments.length > 0) {
|
|
499
|
+
const attachmentPaths = ctx.message.attachments.map((a) => a.local).join("\n");
|
|
500
|
+
userMessage += `\n\nAttachments:\n${attachmentPaths}`;
|
|
501
|
+
}
|
|
502
|
+
// Debug: write context to last_prompt.jsonl
|
|
503
|
+
const debugContext = {
|
|
504
|
+
systemPrompt,
|
|
505
|
+
messages: session.messages,
|
|
506
|
+
newUserMessage: userMessage,
|
|
507
|
+
};
|
|
508
|
+
await writeFile(join(channelDir, "last_prompt.jsonl"), JSON.stringify(debugContext, null, 2));
|
|
509
|
+
await session.prompt(userMessage);
|
|
510
|
+
// Wait for queued messages
|
|
511
|
+
await queueChain;
|
|
512
|
+
// Final message update
|
|
513
|
+
const messages = session.messages;
|
|
564
514
|
const lastAssistant = messages.filter((m) => m.role === "assistant").pop();
|
|
565
515
|
const finalText = lastAssistant?.content
|
|
566
516
|
.filter((c) => c.type === "text")
|
|
@@ -568,7 +518,6 @@ export function createAgentRunner(sandboxConfig) {
|
|
|
568
518
|
.join("\n") || "";
|
|
569
519
|
if (finalText.trim()) {
|
|
570
520
|
try {
|
|
571
|
-
// For the main message, truncate if too long (full text is in thread)
|
|
572
521
|
const mainText = finalText.length > SLACK_MAX_LENGTH
|
|
573
522
|
? finalText.substring(0, SLACK_MAX_LENGTH - 50) + "\n\n_(see thread for full response)_"
|
|
574
523
|
: finalText;
|
|
@@ -579,16 +528,33 @@ export function createAgentRunner(sandboxConfig) {
|
|
|
579
528
|
log.logWarning("Failed to replace message with final text", errMsg);
|
|
580
529
|
}
|
|
581
530
|
}
|
|
582
|
-
// Log usage summary
|
|
583
|
-
if (totalUsage.cost.total > 0) {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
531
|
+
// Log usage summary with context info
|
|
532
|
+
if (runState.totalUsage.cost.total > 0) {
|
|
533
|
+
// Get last non-aborted assistant message for context calculation
|
|
534
|
+
const messages = session.messages;
|
|
535
|
+
const lastAssistantMessage = messages
|
|
536
|
+
.slice()
|
|
537
|
+
.reverse()
|
|
538
|
+
.find((m) => m.role === "assistant" && m.stopReason !== "aborted");
|
|
539
|
+
const contextTokens = lastAssistantMessage
|
|
540
|
+
? lastAssistantMessage.usage.input +
|
|
541
|
+
lastAssistantMessage.usage.output +
|
|
542
|
+
lastAssistantMessage.usage.cacheRead +
|
|
543
|
+
lastAssistantMessage.usage.cacheWrite
|
|
544
|
+
: 0;
|
|
545
|
+
const contextWindow = model.contextWindow || 200000;
|
|
546
|
+
const summary = log.logUsageSummary(runState.logCtx, runState.totalUsage, contextTokens, contextWindow);
|
|
547
|
+
runState.queue.enqueue(() => ctx.respondInThread(summary), "usage summary");
|
|
548
|
+
await queueChain;
|
|
587
549
|
}
|
|
588
|
-
|
|
550
|
+
// Clear run state
|
|
551
|
+
runState.ctx = null;
|
|
552
|
+
runState.logCtx = null;
|
|
553
|
+
runState.queue = null;
|
|
554
|
+
return { stopReason: runState.stopReason };
|
|
589
555
|
},
|
|
590
556
|
abort() {
|
|
591
|
-
|
|
557
|
+
session.abort();
|
|
592
558
|
},
|
|
593
559
|
};
|
|
594
560
|
}
|
|
@@ -597,17 +563,14 @@ export function createAgentRunner(sandboxConfig) {
|
|
|
597
563
|
*/
|
|
598
564
|
function translateToHostPath(containerPath, channelDir, workspacePath, channelId) {
|
|
599
565
|
if (workspacePath === "/workspace") {
|
|
600
|
-
// Docker mode - translate /workspace/channelId/... to host path
|
|
601
566
|
const prefix = `/workspace/${channelId}/`;
|
|
602
567
|
if (containerPath.startsWith(prefix)) {
|
|
603
568
|
return join(channelDir, containerPath.slice(prefix.length));
|
|
604
569
|
}
|
|
605
|
-
// Maybe it's just /workspace/...
|
|
606
570
|
if (containerPath.startsWith("/workspace/")) {
|
|
607
571
|
return join(channelDir, "..", containerPath.slice("/workspace/".length));
|
|
608
572
|
}
|
|
609
573
|
}
|
|
610
|
-
// Host mode or already a host path
|
|
611
574
|
return containerPath;
|
|
612
575
|
}
|
|
613
576
|
//# sourceMappingURL=agent.js.map
|