@mariozechner/pi-mom 0.10.1 → 0.10.2
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 +44 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +221 -145
- package/dist/agent.js.map +1 -1
- package/dist/log.d.ts +1 -0
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +3 -0
- package/dist/log.js.map +1 -1
- package/dist/slack.d.ts +31 -2
- package/dist/slack.d.ts.map +1 -1
- package/dist/slack.js +111 -6
- package/dist/slack.js.map +1 -1
- package/package.json +3 -3
package/dist/agent.js
CHANGED
|
@@ -1,13 +1,34 @@
|
|
|
1
1
|
import { Agent, ProviderTransport } from "@mariozechner/pi-agent-core";
|
|
2
2
|
import { getModel } from "@mariozechner/pi-ai";
|
|
3
3
|
import { existsSync, readFileSync } from "fs";
|
|
4
|
-
import { mkdir } from "fs/promises";
|
|
4
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
5
5
|
import { join } from "path";
|
|
6
6
|
import * as log from "./log.js";
|
|
7
7
|
import { createExecutor } from "./sandbox.js";
|
|
8
8
|
import { createMomTools, setUploadFunction } from "./tools/index.js";
|
|
9
9
|
// Hardcoded model for now
|
|
10
10
|
const model = getModel("anthropic", "claude-sonnet-4-5");
|
|
11
|
+
/**
|
|
12
|
+
* Convert Date.now() to Slack timestamp format (seconds.microseconds)
|
|
13
|
+
* Uses a monotonic counter to ensure ordering even within the same millisecond
|
|
14
|
+
*/
|
|
15
|
+
let lastTsMs = 0;
|
|
16
|
+
let tsCounter = 0;
|
|
17
|
+
function toSlackTs() {
|
|
18
|
+
const now = Date.now();
|
|
19
|
+
if (now === lastTsMs) {
|
|
20
|
+
// Same millisecond - increment counter for sub-ms ordering
|
|
21
|
+
tsCounter++;
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
// New millisecond - reset counter
|
|
25
|
+
lastTsMs = now;
|
|
26
|
+
tsCounter = 0;
|
|
27
|
+
}
|
|
28
|
+
const seconds = Math.floor(now / 1000);
|
|
29
|
+
const micros = (now % 1000) * 1000 + tsCounter; // ms to micros + counter
|
|
30
|
+
return `${seconds}.${micros.toString().padStart(6, "0")}`;
|
|
31
|
+
}
|
|
11
32
|
function getAnthropicApiKey() {
|
|
12
33
|
const key = process.env.ANTHROPIC_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY;
|
|
13
34
|
if (!key) {
|
|
@@ -15,29 +36,74 @@ function getAnthropicApiKey() {
|
|
|
15
36
|
}
|
|
16
37
|
return key;
|
|
17
38
|
}
|
|
18
|
-
function getRecentMessages(channelDir,
|
|
39
|
+
function getRecentMessages(channelDir, turnCount) {
|
|
19
40
|
const logPath = join(channelDir, "log.jsonl");
|
|
20
41
|
if (!existsSync(logPath)) {
|
|
21
42
|
return "(no message history yet)";
|
|
22
43
|
}
|
|
23
44
|
const content = readFileSync(logPath, "utf-8");
|
|
24
45
|
const lines = content.trim().split("\n").filter(Boolean);
|
|
25
|
-
|
|
26
|
-
if (recentLines.length === 0) {
|
|
46
|
+
if (lines.length === 0) {
|
|
27
47
|
return "(no message history yet)";
|
|
28
48
|
}
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
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) {
|
|
32
53
|
try {
|
|
33
|
-
|
|
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) {
|
|
34
101
|
const date = (msg.date || "").substring(0, 19);
|
|
35
|
-
const user = msg.userName || msg.user;
|
|
102
|
+
const user = msg.userName || msg.user || "";
|
|
36
103
|
const text = msg.text || "";
|
|
37
104
|
const attachments = (msg.attachments || []).map((a) => a.local).join(",");
|
|
38
105
|
formatted.push(`${date}\t${user}\t${text}\t${attachments}`);
|
|
39
106
|
}
|
|
40
|
-
catch (error) { }
|
|
41
107
|
}
|
|
42
108
|
return formatted.join("\n");
|
|
43
109
|
}
|
|
@@ -74,148 +140,103 @@ function getMemory(channelDir) {
|
|
|
74
140
|
}
|
|
75
141
|
return parts.join("\n\n");
|
|
76
142
|
}
|
|
77
|
-
function buildSystemPrompt(workspacePath, channelId, memory, sandboxConfig) {
|
|
143
|
+
function buildSystemPrompt(workspacePath, channelId, memory, sandboxConfig, channels, users) {
|
|
78
144
|
const channelPath = `${workspacePath}/${channelId}`;
|
|
79
145
|
const isDocker = sandboxConfig.type === "docker";
|
|
146
|
+
// Format channel mappings
|
|
147
|
+
const channelMappings = channels.length > 0 ? channels.map((c) => `${c.id}\t#${c.name}`).join("\n") : "(no channels loaded)";
|
|
148
|
+
// Format user mappings
|
|
149
|
+
const userMappings = users.length > 0 ? users.map((u) => `${u.id}\t@${u.userName}\t${u.displayName}`).join("\n") : "(no users loaded)";
|
|
80
150
|
const envDescription = isDocker
|
|
81
151
|
? `You are running inside a Docker container (Alpine Linux).
|
|
152
|
+
- Bash working directory: / (use cd or absolute paths)
|
|
82
153
|
- Install tools with: apk add <package>
|
|
83
|
-
- Your changes persist across sessions
|
|
84
|
-
- You have full control over this container`
|
|
154
|
+
- Your changes persist across sessions`
|
|
85
155
|
: `You are running directly on the host machine.
|
|
86
|
-
-
|
|
87
|
-
-
|
|
156
|
+
- Bash working directory: ${process.cwd()}
|
|
157
|
+
- Be careful with system modifications`;
|
|
88
158
|
const currentDate = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
|
|
89
159
|
const currentDateTime = new Date().toISOString(); // Full ISO 8601
|
|
90
|
-
return `You are mom, a
|
|
160
|
+
return `You are mom, a Slack bot assistant. Be concise. No emojis.
|
|
91
161
|
|
|
92
|
-
##
|
|
93
|
-
- Date: ${currentDate}
|
|
94
|
-
-
|
|
95
|
-
- Use this when working with dates or searching logs
|
|
162
|
+
## Context
|
|
163
|
+
- Date: ${currentDate} (${currentDateTime})
|
|
164
|
+
- You receive the last 50 conversation turns. If you need older context, search log.jsonl.
|
|
96
165
|
|
|
97
|
-
##
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
- Get to the point quickly
|
|
101
|
-
- If you need clarification, ask directly
|
|
102
|
-
- Use Slack's mrkdwn format (NOT standard Markdown):
|
|
103
|
-
- Bold: *text* (single asterisks)
|
|
104
|
-
- Italic: _text_
|
|
105
|
-
- Strikethrough: ~text~
|
|
106
|
-
- Code: \`code\`
|
|
107
|
-
- Code block: \`\`\`code\`\`\`
|
|
108
|
-
- Links: <url|text>
|
|
109
|
-
- Do NOT use **double asterisks** or [markdown](links)
|
|
166
|
+
## Slack Formatting (mrkdwn, NOT Markdown)
|
|
167
|
+
Bold: *text*, Italic: _text_, Code: \`code\`, Block: \`\`\`code\`\`\`, Links: <url|text>
|
|
168
|
+
Do NOT use **double asterisks** or [markdown](links).
|
|
110
169
|
|
|
111
|
-
##
|
|
112
|
-
${
|
|
170
|
+
## Slack IDs
|
|
171
|
+
Channels: ${channelMappings}
|
|
113
172
|
|
|
114
|
-
|
|
115
|
-
Your working directory is: ${channelPath}
|
|
173
|
+
Users: ${userMappings}
|
|
116
174
|
|
|
117
|
-
|
|
118
|
-
- ${workspacePath}/ - Root workspace (shared across all channels)
|
|
119
|
-
- MEMORY.md - GLOBAL memory visible to all channels (write global info here)
|
|
120
|
-
- ${channelId}/ - This channel's directory
|
|
121
|
-
- MEMORY.md - CHANNEL-SPECIFIC memory (only visible in this channel)
|
|
122
|
-
- scratch/ - Your working directory for files, repos, etc.
|
|
123
|
-
- log.jsonl - Message history in JSONL format (one JSON object per line)
|
|
124
|
-
- attachments/ - Files shared by users (managed by system, read-only)
|
|
175
|
+
When mentioning users, use <@username> format (e.g., <@mario>).
|
|
125
176
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
{
|
|
129
|
-
"date": "2025-11-26T10:44:00.123Z", // ISO 8601 - easy to grep by date!
|
|
130
|
-
"ts": "1732619040.123456", // Slack timestamp or epoch ms
|
|
131
|
-
"user": "U123ABC", // User ID or "bot"
|
|
132
|
-
"userName": "mario", // User handle (optional)
|
|
133
|
-
"text": "message text",
|
|
134
|
-
"isBot": false
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
**⚠️ CRITICAL: Efficient Log Queries (Avoid Context Overflow)**
|
|
138
|
-
|
|
139
|
-
Log files can be VERY LARGE (100K+ lines). The problem is getting too MANY messages, not message length.
|
|
140
|
-
Each message can be up to 10k chars - that's fine. Use head/tail to LIMIT NUMBER OF MESSAGES (10-50 at a time).
|
|
177
|
+
## Environment
|
|
178
|
+
${envDescription}
|
|
141
179
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
180
|
+
## Workspace Layout
|
|
181
|
+
${workspacePath}/
|
|
182
|
+
├── MEMORY.md # Global memory (all channels)
|
|
183
|
+
├── skills/ # Global CLI tools you create
|
|
184
|
+
└── ${channelId}/ # This channel
|
|
185
|
+
├── MEMORY.md # Channel-specific memory
|
|
186
|
+
├── log.jsonl # Full message history
|
|
187
|
+
├── attachments/ # User-shared files
|
|
188
|
+
├── scratch/ # Your working directory
|
|
189
|
+
└── skills/ # Channel-specific tools
|
|
146
190
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
191
|
+
## Skills (Custom CLI Tools)
|
|
192
|
+
You can create reusable CLI tools for recurring tasks (email, APIs, data processing, etc.).
|
|
193
|
+
Store in \`${workspacePath}/skills/<name>/\` or \`${channelPath}/skills/<name>/\`.
|
|
194
|
+
Each skill needs a \`SKILL.md\` documenting usage. Read it before using a skill.
|
|
195
|
+
List skills in global memory so you remember them.
|
|
151
196
|
|
|
152
|
-
|
|
153
|
-
|
|
197
|
+
## Memory
|
|
198
|
+
Write to MEMORY.md files to persist context across conversations.
|
|
199
|
+
- Global (${workspacePath}/MEMORY.md): skills, preferences, project info
|
|
200
|
+
- Channel (${channelPath}/MEMORY.md): channel-specific decisions, ongoing work
|
|
201
|
+
Update when you learn something important or when asked to remember something.
|
|
154
202
|
|
|
155
|
-
|
|
156
|
-
|
|
203
|
+
### Current Memory
|
|
204
|
+
${memory}
|
|
157
205
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
206
|
+
## Log Queries (CRITICAL: limit output to avoid context overflow)
|
|
207
|
+
Format: \`{"date":"...","ts":"...","user":"...","userName":"...","text":"...","isBot":false}\`
|
|
208
|
+
The log contains user messages AND your tool calls/results. Filter appropriately.
|
|
209
|
+
${isDocker ? "Install jq: apk add jq" : ""}
|
|
161
210
|
|
|
162
|
-
|
|
163
|
-
|
|
211
|
+
**Conversation only (excludes tool calls/results) - use for summaries:**
|
|
212
|
+
\`\`\`bash
|
|
213
|
+
# Recent conversation (no [Tool] or [Tool Result] lines)
|
|
214
|
+
grep -v '"text":"\\[Tool' log.jsonl | tail -30 | jq -c '{date: .date[0:19], user: (.userName // .user), text}'
|
|
164
215
|
|
|
165
|
-
#
|
|
166
|
-
grep '"
|
|
167
|
-
\`\`\`
|
|
216
|
+
# Yesterday's conversation
|
|
217
|
+
grep '"date":"2025-11-26' log.jsonl | grep -v '"text":"\\[Tool' | jq -c '{date: .date[0:19], user: (.userName // .user), text}'
|
|
168
218
|
|
|
169
|
-
|
|
219
|
+
# Specific user's messages
|
|
220
|
+
grep '"userName":"mario"' log.jsonl | grep -v '"text":"\\[Tool' | tail -20 | jq -c '{date: .date[0:19], text}'
|
|
170
221
|
\`\`\`
|
|
171
222
|
|
|
172
|
-
**
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
-
|
|
176
|
-
- Time range: grep -E '"date":"2025-11-26T(09|10|11):' log.jsonl
|
|
177
|
-
|
|
178
|
-
### Working Memory System
|
|
179
|
-
You can maintain working memory across conversations by writing MEMORY.md files.
|
|
180
|
-
|
|
181
|
-
**IMPORTANT PATH RULES:**
|
|
182
|
-
- Global memory (all channels): ${workspacePath}/MEMORY.md
|
|
183
|
-
- Channel memory (this channel only): ${channelPath}/MEMORY.md
|
|
184
|
-
|
|
185
|
-
**What to remember:**
|
|
186
|
-
- Project details and architecture → Global memory
|
|
187
|
-
- User preferences and coding style → Global memory
|
|
188
|
-
- Channel-specific context → Channel memory
|
|
189
|
-
- Recurring tasks and patterns → Appropriate memory file
|
|
190
|
-
- Credentials locations (never actual secrets) → Global memory
|
|
191
|
-
- Decisions made and their rationale → Appropriate memory file
|
|
192
|
-
|
|
193
|
-
**When to update:**
|
|
194
|
-
- After learning something important that will help in future conversations
|
|
195
|
-
- When user asks you to remember something
|
|
196
|
-
- When you discover project structure or conventions
|
|
223
|
+
**Full details (includes tool calls) - use when you need technical context:**
|
|
224
|
+
\`\`\`bash
|
|
225
|
+
# Raw recent entries
|
|
226
|
+
tail -20 log.jsonl | jq -c '{date: .date[0:19], user: (.userName // .user), text}'
|
|
197
227
|
|
|
198
|
-
|
|
199
|
-
|
|
228
|
+
# Count all messages
|
|
229
|
+
wc -l log.jsonl
|
|
230
|
+
\`\`\`
|
|
200
231
|
|
|
201
232
|
## Tools
|
|
202
|
-
|
|
203
|
-
- bash: Run shell commands (this is your main tool)
|
|
233
|
+
- bash: Run shell commands (primary tool). Install packages as needed.
|
|
204
234
|
- read: Read files
|
|
205
|
-
-
|
|
206
|
-
-
|
|
207
|
-
- attach: Share
|
|
208
|
-
|
|
209
|
-
Each tool requires a "label" parameter - brief description shown to the user.
|
|
235
|
+
- write: Create/overwrite files
|
|
236
|
+
- edit: Surgical file edits
|
|
237
|
+
- attach: Share files to Slack
|
|
210
238
|
|
|
211
|
-
|
|
212
|
-
- Be concise and helpful
|
|
213
|
-
- Use bash for most operations
|
|
214
|
-
- If you need a tool, install it
|
|
215
|
-
- If you need credentials, ask the user
|
|
216
|
-
|
|
217
|
-
## CRITICAL
|
|
218
|
-
- DO NOT USE EMOJIS. KEEP YOUR RESPONSES AS SHORT AS POSSIBLE.
|
|
239
|
+
Each tool requires a "label" parameter (shown to user).
|
|
219
240
|
`;
|
|
220
241
|
}
|
|
221
242
|
function truncate(text, maxLen) {
|
|
@@ -289,7 +310,10 @@ export function createAgentRunner(sandboxConfig) {
|
|
|
289
310
|
const workspacePath = executor.getWorkspacePath(channelDir.replace(`/${channelId}`, ""));
|
|
290
311
|
const recentMessages = getRecentMessages(channelDir, 50);
|
|
291
312
|
const memory = getMemory(channelDir);
|
|
292
|
-
const systemPrompt = buildSystemPrompt(workspacePath, channelId, memory, sandboxConfig);
|
|
313
|
+
const systemPrompt = buildSystemPrompt(workspacePath, channelId, memory, sandboxConfig, ctx.channels, ctx.users);
|
|
314
|
+
// Debug: log context sizes
|
|
315
|
+
log.logInfo(`Context sizes - system: ${systemPrompt.length} chars, messages: ${recentMessages.length} chars, memory: ${memory.length} chars`);
|
|
316
|
+
log.logInfo(`Channels: ${ctx.channels.length}, Users: ${ctx.users.length}`);
|
|
293
317
|
// Set up file upload function for the attach tool
|
|
294
318
|
// For Docker, we need to translate paths back to host
|
|
295
319
|
setUploadFunction(async (filePath, title) => {
|
|
@@ -334,13 +358,51 @@ export function createAgentRunner(sandboxConfig) {
|
|
|
334
358
|
};
|
|
335
359
|
// Track stop reason
|
|
336
360
|
let stopReason = "stop";
|
|
361
|
+
// Slack message limit is 40,000 characters - split into multiple messages if needed
|
|
362
|
+
const SLACK_MAX_LENGTH = 40000;
|
|
363
|
+
const splitForSlack = (text) => {
|
|
364
|
+
if (text.length <= SLACK_MAX_LENGTH)
|
|
365
|
+
return [text];
|
|
366
|
+
const parts = [];
|
|
367
|
+
let remaining = text;
|
|
368
|
+
let partNum = 1;
|
|
369
|
+
while (remaining.length > 0) {
|
|
370
|
+
const chunk = remaining.substring(0, SLACK_MAX_LENGTH - 50);
|
|
371
|
+
remaining = remaining.substring(SLACK_MAX_LENGTH - 50);
|
|
372
|
+
const suffix = remaining.length > 0 ? `\n_(continued ${partNum}...)_` : "";
|
|
373
|
+
parts.push(chunk + suffix);
|
|
374
|
+
partNum++;
|
|
375
|
+
}
|
|
376
|
+
return parts;
|
|
377
|
+
};
|
|
337
378
|
// Promise queue to ensure ctx.respond/respondInThread calls execute in order
|
|
379
|
+
// Handles errors gracefully by posting to thread instead of crashing
|
|
338
380
|
const queue = {
|
|
339
381
|
chain: Promise.resolve(),
|
|
340
|
-
enqueue(fn) {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
382
|
+
enqueue(fn, errorContext) {
|
|
383
|
+
this.chain = this.chain.then(async () => {
|
|
384
|
+
try {
|
|
385
|
+
await fn();
|
|
386
|
+
}
|
|
387
|
+
catch (err) {
|
|
388
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
389
|
+
log.logWarning(`Slack API error (${errorContext})`, errMsg);
|
|
390
|
+
// Try to post error to thread, but don't crash if that fails too
|
|
391
|
+
try {
|
|
392
|
+
await ctx.respondInThread(`_Error: ${errMsg}_`);
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
// Ignore - we tried our best
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
},
|
|
400
|
+
// Enqueue a message that may need splitting
|
|
401
|
+
enqueueMessage(text, target, errorContext, log = true) {
|
|
402
|
+
const parts = splitForSlack(text);
|
|
403
|
+
for (const part of parts) {
|
|
404
|
+
this.enqueue(() => (target === "main" ? ctx.respond(part, log) : ctx.respondInThread(part)), errorContext);
|
|
405
|
+
}
|
|
344
406
|
},
|
|
345
407
|
flush() {
|
|
346
408
|
return this.chain;
|
|
@@ -363,14 +425,14 @@ export function createAgentRunner(sandboxConfig) {
|
|
|
363
425
|
// Log to jsonl
|
|
364
426
|
await store.logMessage(ctx.message.channel, {
|
|
365
427
|
date: new Date().toISOString(),
|
|
366
|
-
ts:
|
|
428
|
+
ts: toSlackTs(),
|
|
367
429
|
user: "bot",
|
|
368
430
|
text: `[Tool] ${event.toolName}: ${JSON.stringify(event.args)}`,
|
|
369
431
|
attachments: [],
|
|
370
432
|
isBot: true,
|
|
371
433
|
});
|
|
372
434
|
// Show label in main message only
|
|
373
|
-
queue.enqueue(() => ctx.respond(`_→ ${label}_
|
|
435
|
+
queue.enqueue(() => ctx.respond(`_→ ${label}_`, false), "tool label");
|
|
374
436
|
break;
|
|
375
437
|
}
|
|
376
438
|
case "tool_execution_end": {
|
|
@@ -388,7 +450,7 @@ export function createAgentRunner(sandboxConfig) {
|
|
|
388
450
|
// Log to jsonl
|
|
389
451
|
await store.logMessage(ctx.message.channel, {
|
|
390
452
|
date: new Date().toISOString(),
|
|
391
|
-
ts:
|
|
453
|
+
ts: toSlackTs(),
|
|
392
454
|
user: "bot",
|
|
393
455
|
text: `[Tool Result] ${event.toolName}: ${event.isError ? "ERROR: " : ""}${truncate(resultStr, 1000)}`,
|
|
394
456
|
attachments: [],
|
|
@@ -400,7 +462,6 @@ export function createAgentRunner(sandboxConfig) {
|
|
|
400
462
|
? formatToolArgsForSlack(event.toolName, pending.args)
|
|
401
463
|
: "(args not found)";
|
|
402
464
|
const duration = (durationMs / 1000).toFixed(1);
|
|
403
|
-
const threadResult = truncate(resultStr, 2000);
|
|
404
465
|
let threadMessage = `*${event.isError ? "✗" : "✓"} ${event.toolName}*`;
|
|
405
466
|
if (label) {
|
|
406
467
|
threadMessage += `: ${label}`;
|
|
@@ -409,11 +470,11 @@ export function createAgentRunner(sandboxConfig) {
|
|
|
409
470
|
if (argsFormatted) {
|
|
410
471
|
threadMessage += "```\n" + argsFormatted + "\n```\n";
|
|
411
472
|
}
|
|
412
|
-
threadMessage += "*Result:*\n```\n" +
|
|
413
|
-
queue.
|
|
473
|
+
threadMessage += "*Result:*\n```\n" + resultStr + "\n```";
|
|
474
|
+
queue.enqueueMessage(threadMessage, "thread", "tool result thread", false);
|
|
414
475
|
// Show brief error in main message if failed
|
|
415
476
|
if (event.isError) {
|
|
416
|
-
queue.enqueue(() => ctx.respond(`_Error: ${truncate(resultStr, 200)}_
|
|
477
|
+
queue.enqueue(() => ctx.respond(`_Error: ${truncate(resultStr, 200)}_`, false), "tool error");
|
|
417
478
|
}
|
|
418
479
|
break;
|
|
419
480
|
}
|
|
@@ -461,14 +522,14 @@ export function createAgentRunner(sandboxConfig) {
|
|
|
461
522
|
// Post thinking to main message and thread
|
|
462
523
|
for (const thinking of thinkingParts) {
|
|
463
524
|
log.logThinking(logCtx, thinking);
|
|
464
|
-
queue.
|
|
465
|
-
queue.
|
|
525
|
+
queue.enqueueMessage(`_${thinking}_`, "main", "thinking main");
|
|
526
|
+
queue.enqueueMessage(`_${thinking}_`, "thread", "thinking thread", false);
|
|
466
527
|
}
|
|
467
528
|
// Post text to main message and thread
|
|
468
529
|
if (text.trim()) {
|
|
469
530
|
log.logResponse(logCtx, text);
|
|
470
|
-
queue.
|
|
471
|
-
queue.
|
|
531
|
+
queue.enqueueMessage(text, "main", "response main");
|
|
532
|
+
queue.enqueueMessage(text, "thread", "response thread", false);
|
|
472
533
|
}
|
|
473
534
|
}
|
|
474
535
|
break;
|
|
@@ -476,11 +537,16 @@ export function createAgentRunner(sandboxConfig) {
|
|
|
476
537
|
});
|
|
477
538
|
// Run the agent with user's message
|
|
478
539
|
// Prepend recent messages to the user prompt (not system prompt) for better caching
|
|
479
|
-
|
|
540
|
+
// The current message is already the last entry in recentMessages
|
|
541
|
+
const userPrompt = `Conversation history (last 50 turns). Respond to the last message.\n` +
|
|
480
542
|
`Format: date TAB user TAB text TAB attachments\n\n` +
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
543
|
+
recentMessages;
|
|
544
|
+
// Debug: write full context to file
|
|
545
|
+
const toolDefs = tools.map((t) => ({ name: t.name, description: t.description, parameters: t.parameters }));
|
|
546
|
+
const debugPrompt = `=== SYSTEM PROMPT (${systemPrompt.length} chars) ===\n\n${systemPrompt}\n\n` +
|
|
547
|
+
`=== TOOL DEFINITIONS (${JSON.stringify(toolDefs).length} chars) ===\n\n${JSON.stringify(toolDefs, null, 2)}\n\n` +
|
|
548
|
+
`=== USER PROMPT (${userPrompt.length} chars) ===\n\n${userPrompt}`;
|
|
549
|
+
await writeFile(join(channelDir, "last_prompt.txt"), debugPrompt, "utf-8");
|
|
484
550
|
await agent.prompt(userPrompt);
|
|
485
551
|
// Wait for all queued respond calls to complete
|
|
486
552
|
await queue.flush();
|
|
@@ -492,12 +558,22 @@ export function createAgentRunner(sandboxConfig) {
|
|
|
492
558
|
.map((c) => c.text)
|
|
493
559
|
.join("\n") || "";
|
|
494
560
|
if (finalText.trim()) {
|
|
495
|
-
|
|
561
|
+
try {
|
|
562
|
+
// For the main message, truncate if too long (full text is in thread)
|
|
563
|
+
const mainText = finalText.length > SLACK_MAX_LENGTH
|
|
564
|
+
? finalText.substring(0, SLACK_MAX_LENGTH - 50) + "\n\n_(see thread for full response)_"
|
|
565
|
+
: finalText;
|
|
566
|
+
await ctx.replaceMessage(mainText);
|
|
567
|
+
}
|
|
568
|
+
catch (err) {
|
|
569
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
570
|
+
log.logWarning("Failed to replace message with final text", errMsg);
|
|
571
|
+
}
|
|
496
572
|
}
|
|
497
573
|
// Log usage summary if there was any usage
|
|
498
574
|
if (totalUsage.cost.total > 0) {
|
|
499
575
|
const summary = log.logUsageSummary(logCtx, totalUsage);
|
|
500
|
-
queue.enqueue(() => ctx.respondInThread(summary));
|
|
576
|
+
queue.enqueue(() => ctx.respondInThread(summary), "usage summary");
|
|
501
577
|
await queue.flush();
|
|
502
578
|
}
|
|
503
579
|
return { stopReason };
|