@mariozechner/pi-mom 0.10.0 → 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 +54 -0
- package/README.md +46 -46
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +256 -142
- package/dist/agent.js.map +1 -1
- package/dist/log.d.ts +3 -1
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +20 -2
- 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}
|
|
116
|
-
|
|
117
|
-
### Directory Structure
|
|
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)
|
|
125
|
-
|
|
126
|
-
### Message History Format
|
|
127
|
-
Each line in log.jsonl contains:
|
|
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
|
-
}
|
|
173
|
+
Users: ${userMappings}
|
|
136
174
|
|
|
137
|
-
|
|
175
|
+
When mentioning users, use <@username> format (e.g., <@mario>).
|
|
138
176
|
|
|
139
|
-
|
|
140
|
-
|
|
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.
|
|
210
|
-
|
|
211
|
-
## Guidelines
|
|
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
|
|
235
|
+
- write: Create/overwrite files
|
|
236
|
+
- edit: Surgical file edits
|
|
237
|
+
- attach: Share files to Slack
|
|
216
238
|
|
|
217
|
-
|
|
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,6 +358,56 @@ 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
|
+
};
|
|
378
|
+
// Promise queue to ensure ctx.respond/respondInThread calls execute in order
|
|
379
|
+
// Handles errors gracefully by posting to thread instead of crashing
|
|
380
|
+
const queue = {
|
|
381
|
+
chain: Promise.resolve(),
|
|
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
|
+
}
|
|
406
|
+
},
|
|
407
|
+
flush() {
|
|
408
|
+
return this.chain;
|
|
409
|
+
},
|
|
410
|
+
};
|
|
337
411
|
// Subscribe to events
|
|
338
412
|
agent.subscribe(async (event) => {
|
|
339
413
|
switch (event.type) {
|
|
@@ -351,14 +425,14 @@ export function createAgentRunner(sandboxConfig) {
|
|
|
351
425
|
// Log to jsonl
|
|
352
426
|
await store.logMessage(ctx.message.channel, {
|
|
353
427
|
date: new Date().toISOString(),
|
|
354
|
-
ts:
|
|
428
|
+
ts: toSlackTs(),
|
|
355
429
|
user: "bot",
|
|
356
430
|
text: `[Tool] ${event.toolName}: ${JSON.stringify(event.args)}`,
|
|
357
431
|
attachments: [],
|
|
358
432
|
isBot: true,
|
|
359
433
|
});
|
|
360
434
|
// Show label in main message only
|
|
361
|
-
|
|
435
|
+
queue.enqueue(() => ctx.respond(`_→ ${label}_`, false), "tool label");
|
|
362
436
|
break;
|
|
363
437
|
}
|
|
364
438
|
case "tool_execution_end": {
|
|
@@ -376,7 +450,7 @@ export function createAgentRunner(sandboxConfig) {
|
|
|
376
450
|
// Log to jsonl
|
|
377
451
|
await store.logMessage(ctx.message.channel, {
|
|
378
452
|
date: new Date().toISOString(),
|
|
379
|
-
ts:
|
|
453
|
+
ts: toSlackTs(),
|
|
380
454
|
user: "bot",
|
|
381
455
|
text: `[Tool Result] ${event.toolName}: ${event.isError ? "ERROR: " : ""}${truncate(resultStr, 1000)}`,
|
|
382
456
|
attachments: [],
|
|
@@ -388,7 +462,6 @@ export function createAgentRunner(sandboxConfig) {
|
|
|
388
462
|
? formatToolArgsForSlack(event.toolName, pending.args)
|
|
389
463
|
: "(args not found)";
|
|
390
464
|
const duration = (durationMs / 1000).toFixed(1);
|
|
391
|
-
const threadResult = truncate(resultStr, 2000);
|
|
392
465
|
let threadMessage = `*${event.isError ? "✗" : "✓"} ${event.toolName}*`;
|
|
393
466
|
if (label) {
|
|
394
467
|
threadMessage += `: ${label}`;
|
|
@@ -397,11 +470,11 @@ export function createAgentRunner(sandboxConfig) {
|
|
|
397
470
|
if (argsFormatted) {
|
|
398
471
|
threadMessage += "```\n" + argsFormatted + "\n```\n";
|
|
399
472
|
}
|
|
400
|
-
threadMessage += "*Result:*\n```\n" +
|
|
401
|
-
|
|
473
|
+
threadMessage += "*Result:*\n```\n" + resultStr + "\n```";
|
|
474
|
+
queue.enqueueMessage(threadMessage, "thread", "tool result thread", false);
|
|
402
475
|
// Show brief error in main message if failed
|
|
403
476
|
if (event.isError) {
|
|
404
|
-
|
|
477
|
+
queue.enqueue(() => ctx.respond(`_Error: ${truncate(resultStr, 200)}_`, false), "tool error");
|
|
405
478
|
}
|
|
406
479
|
break;
|
|
407
480
|
}
|
|
@@ -433,17 +506,30 @@ export function createAgentRunner(sandboxConfig) {
|
|
|
433
506
|
totalUsage.cost.cacheWrite += assistantMsg.usage.cost.cacheWrite;
|
|
434
507
|
totalUsage.cost.total += assistantMsg.usage.cost.total;
|
|
435
508
|
}
|
|
436
|
-
// Extract text from assistant message
|
|
509
|
+
// Extract thinking and text from assistant message
|
|
437
510
|
const content = event.message.content;
|
|
438
|
-
|
|
511
|
+
const thinkingParts = [];
|
|
512
|
+
const textParts = [];
|
|
439
513
|
for (const part of content) {
|
|
440
|
-
if (part.type === "
|
|
441
|
-
|
|
514
|
+
if (part.type === "thinking") {
|
|
515
|
+
thinkingParts.push(part.thinking);
|
|
516
|
+
}
|
|
517
|
+
else if (part.type === "text") {
|
|
518
|
+
textParts.push(part.text);
|
|
442
519
|
}
|
|
443
520
|
}
|
|
521
|
+
const text = textParts.join("\n");
|
|
522
|
+
// Post thinking to main message and thread
|
|
523
|
+
for (const thinking of thinkingParts) {
|
|
524
|
+
log.logThinking(logCtx, thinking);
|
|
525
|
+
queue.enqueueMessage(`_${thinking}_`, "main", "thinking main");
|
|
526
|
+
queue.enqueueMessage(`_${thinking}_`, "thread", "thinking thread", false);
|
|
527
|
+
}
|
|
528
|
+
// Post text to main message and thread
|
|
444
529
|
if (text.trim()) {
|
|
445
|
-
|
|
446
|
-
|
|
530
|
+
log.logResponse(logCtx, text);
|
|
531
|
+
queue.enqueueMessage(text, "main", "response main");
|
|
532
|
+
queue.enqueueMessage(text, "thread", "response thread", false);
|
|
447
533
|
}
|
|
448
534
|
}
|
|
449
535
|
break;
|
|
@@ -451,16 +537,44 @@ export function createAgentRunner(sandboxConfig) {
|
|
|
451
537
|
});
|
|
452
538
|
// Run the agent with user's message
|
|
453
539
|
// Prepend recent messages to the user prompt (not system prompt) for better caching
|
|
454
|
-
|
|
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` +
|
|
455
542
|
`Format: date TAB user TAB text TAB attachments\n\n` +
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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");
|
|
459
550
|
await agent.prompt(userPrompt);
|
|
551
|
+
// Wait for all queued respond calls to complete
|
|
552
|
+
await queue.flush();
|
|
553
|
+
// Get final assistant message text from agent state and replace main message
|
|
554
|
+
const messages = agent.state.messages;
|
|
555
|
+
const lastAssistant = messages.filter((m) => m.role === "assistant").pop();
|
|
556
|
+
const finalText = lastAssistant?.content
|
|
557
|
+
.filter((c) => c.type === "text")
|
|
558
|
+
.map((c) => c.text)
|
|
559
|
+
.join("\n") || "";
|
|
560
|
+
if (finalText.trim()) {
|
|
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
|
+
}
|
|
572
|
+
}
|
|
460
573
|
// Log usage summary if there was any usage
|
|
461
574
|
if (totalUsage.cost.total > 0) {
|
|
462
575
|
const summary = log.logUsageSummary(logCtx, totalUsage);
|
|
463
|
-
|
|
576
|
+
queue.enqueue(() => ctx.respondInThread(summary), "usage summary");
|
|
577
|
+
await queue.flush();
|
|
464
578
|
}
|
|
465
579
|
return { stopReason };
|
|
466
580
|
},
|