@geminixiang/mama 0.1.9 → 0.2.0-beta.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/README.md +173 -16
- package/dist/adapter.d.ts +8 -1
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js.map +1 -1
- package/dist/adapters/discord/context.d.ts.map +1 -1
- package/dist/adapters/discord/context.js +1 -0
- package/dist/adapters/discord/context.js.map +1 -1
- package/dist/adapters/slack/bot.d.ts +12 -0
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +86 -10
- package/dist/adapters/slack/bot.js.map +1 -1
- package/dist/adapters/slack/context.d.ts.map +1 -1
- package/dist/adapters/slack/context.js +48 -28
- package/dist/adapters/slack/context.js.map +1 -1
- package/dist/adapters/slack/tools/attach.d.ts.map +1 -1
- package/dist/adapters/slack/tools/attach.js +4 -2
- package/dist/adapters/slack/tools/attach.js.map +1 -1
- package/dist/adapters/telegram/bot.d.ts +6 -3
- package/dist/adapters/telegram/bot.d.ts.map +1 -1
- package/dist/adapters/telegram/bot.js +115 -63
- package/dist/adapters/telegram/bot.js.map +1 -1
- package/dist/adapters/telegram/context.d.ts +2 -2
- package/dist/adapters/telegram/context.d.ts.map +1 -1
- package/dist/adapters/telegram/context.js +92 -65
- package/dist/adapters/telegram/context.js.map +1 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +214 -39
- package/dist/agent.js.map +1 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +46 -13
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +15 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +29 -2
- package/dist/context.js.map +1 -1
- package/dist/events.d.ts +10 -5
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +44 -10
- package/dist/events.js.map +1 -1
- package/dist/instrument.d.ts +2 -0
- package/dist/instrument.d.ts.map +1 -0
- package/dist/instrument.js +7 -0
- package/dist/instrument.js.map +1 -0
- package/dist/log.d.ts +1 -0
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +6 -5
- package/dist/log.js.map +1 -1
- package/dist/main.d.ts +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +137 -59
- package/dist/main.js.map +1 -1
- package/dist/sandbox.d.ts +7 -1
- package/dist/sandbox.d.ts.map +1 -1
- package/dist/sandbox.js +127 -27
- package/dist/sandbox.js.map +1 -1
- package/dist/sentry.d.ts +31 -0
- package/dist/sentry.d.ts.map +1 -0
- package/dist/sentry.js +205 -0
- package/dist/sentry.js.map +1 -0
- package/dist/session-store.d.ts +76 -0
- package/dist/session-store.d.ts.map +1 -0
- package/dist/session-store.js +189 -0
- package/dist/session-store.js.map +1 -0
- package/package.json +13 -12
package/dist/agent.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Agent } from "@mariozechner/pi-agent-core";
|
|
2
2
|
import { getModel } from "@mariozechner/pi-ai";
|
|
3
|
-
import { AgentSession, AuthStorage, convertToLlm,
|
|
4
|
-
import { existsSync,
|
|
3
|
+
import { AgentSession, AuthStorage, convertToLlm, DefaultResourceLoader, formatSkillsForPrompt, loadSkillsFromDir, ModelRegistry, } from "@mariozechner/pi-coding-agent";
|
|
4
|
+
import { existsSync, readFileSync } from "fs";
|
|
5
5
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
6
6
|
import { homedir } from "os";
|
|
7
7
|
import { join } from "path";
|
|
@@ -9,7 +9,10 @@ import { loadAgentConfig } from "./config.js";
|
|
|
9
9
|
import { createMamaSettingsManager, syncLogToSessionManager } from "./context.js";
|
|
10
10
|
import * as log from "./log.js";
|
|
11
11
|
import { createExecutor } from "./sandbox.js";
|
|
12
|
+
import { addLifecycleBreadcrumb, metricAttributes } from "./sentry.js";
|
|
13
|
+
import { createManagedSessionFileAtPath, extractSessionSuffix, extractSessionUuid, forkThreadSessionFile, getSessionDir, getThreadSessionFile, openManagedSession, resolveChannelSessionFile, resolveManagedSessionFile, tryResolveThreadSession, } from "./session-store.js";
|
|
12
14
|
import { createMamaTools } from "./tools/index.js";
|
|
15
|
+
import * as Sentry from "@sentry/node";
|
|
13
16
|
const IMAGE_MIME_TYPES = {
|
|
14
17
|
jpg: "image/jpeg",
|
|
15
18
|
jpeg: "image/jpeg",
|
|
@@ -86,6 +89,7 @@ function loadMamaSkills(channelDir, workspacePath) {
|
|
|
86
89
|
function buildSystemPrompt(workspacePath, channelId, memory, sandboxConfig, platform, skills) {
|
|
87
90
|
const channelPath = `${workspacePath}/${channelId}`;
|
|
88
91
|
const isDocker = sandboxConfig.type === "docker";
|
|
92
|
+
const isFirecracker = sandboxConfig.type === "firecracker";
|
|
89
93
|
// Format channel mappings
|
|
90
94
|
const channelMappings = platform.channels.length > 0
|
|
91
95
|
? platform.channels.map((c) => `${c.id}\t#${c.name}`).join("\n")
|
|
@@ -99,7 +103,12 @@ function buildSystemPrompt(workspacePath, channelId, memory, sandboxConfig, plat
|
|
|
99
103
|
- Bash working directory: / (use cd or absolute paths)
|
|
100
104
|
- Install tools with: apk add <package>
|
|
101
105
|
- Your changes persist across sessions`
|
|
102
|
-
:
|
|
106
|
+
: isFirecracker
|
|
107
|
+
? `You are running inside a Firecracker microVM.
|
|
108
|
+
- Bash working directory: / (use cd or absolute paths)
|
|
109
|
+
- Install tools with: apt-get install <package> (Debian-based)
|
|
110
|
+
- Your changes persist across sessions`
|
|
111
|
+
: `You are running directly on the host machine.
|
|
103
112
|
- Bash working directory: ${process.cwd()}
|
|
104
113
|
- Be careful with system modifications`;
|
|
105
114
|
return `You are mama, a ${platform.name} bot assistant. Be concise. No emojis.
|
|
@@ -108,6 +117,7 @@ function buildSystemPrompt(workspacePath, channelId, memory, sandboxConfig, plat
|
|
|
108
117
|
- For current date/time, use: date
|
|
109
118
|
- You have access to previous conversation context including tool results from prior turns.
|
|
110
119
|
- For older history beyond your context, search log.jsonl (contains user messages and your final responses, but not tool results).
|
|
120
|
+
- User messages include a \`[in-thread:TS]\` marker when sent from within a Slack thread (TS is the root message timestamp). Without this marker, the message is a top-level channel message.
|
|
111
121
|
|
|
112
122
|
${platform.formattingGuide}
|
|
113
123
|
|
|
@@ -163,17 +173,17 @@ You can schedule events that wake you up at specific times or when external thin
|
|
|
163
173
|
|
|
164
174
|
**Immediate** - Triggers as soon as harness sees the file. Use in scripts/webhooks to signal external events.
|
|
165
175
|
\`\`\`json
|
|
166
|
-
{"type": "immediate", "channelId": "${channelId}", "text": "New GitHub issue opened"}
|
|
176
|
+
{"type": "immediate", "platform": "${platform.name}", "channelId": "${channelId}", "text": "New GitHub issue opened"}
|
|
167
177
|
\`\`\`
|
|
168
178
|
|
|
169
179
|
**One-shot** - Triggers once at a specific time. Use for reminders.
|
|
170
180
|
\`\`\`json
|
|
171
|
-
{"type": "one-shot", "channelId": "${channelId}", "text": "Remind Mario about dentist", "at": "2025-12-15T09:00:00+01:00"}
|
|
181
|
+
{"type": "one-shot", "platform": "${platform.name}", "channelId": "${channelId}", "text": "Remind Mario about dentist", "at": "2025-12-15T09:00:00+01:00"}
|
|
172
182
|
\`\`\`
|
|
173
183
|
|
|
174
184
|
**Periodic** - Triggers on a cron schedule. Use for recurring tasks.
|
|
175
185
|
\`\`\`json
|
|
176
|
-
{"type": "periodic", "channelId": "${channelId}", "text": "Check inbox and summarize", "schedule": "0 9 * * 1-5", "timezone": "${Intl.DateTimeFormat().resolvedOptions().timeZone}"}
|
|
186
|
+
{"type": "periodic", "platform": "${platform.name}", "channelId": "${channelId}", "text": "Check inbox and summarize", "schedule": "0 9 * * 1-5", "timezone": "${Intl.DateTimeFormat().resolvedOptions().timeZone}"}
|
|
177
187
|
\`\`\`
|
|
178
188
|
|
|
179
189
|
### Cron Format
|
|
@@ -186,11 +196,14 @@ You can schedule events that wake you up at specific times or when external thin
|
|
|
186
196
|
### Timezones
|
|
187
197
|
All \`at\` timestamps must include offset (e.g., \`+01:00\`). Periodic events use IANA timezone names. The harness runs in ${Intl.DateTimeFormat().resolvedOptions().timeZone}. When users mention times without timezone, assume ${Intl.DateTimeFormat().resolvedOptions().timeZone}.
|
|
188
198
|
|
|
199
|
+
### Platform Routing
|
|
200
|
+
Set \`platform\` to the target bot platform (\`${platform.name}\` for this conversation). When only one platform is running, omitting \`platform\` is allowed for backward compatibility, but include it by default to avoid ambiguity.
|
|
201
|
+
|
|
189
202
|
### Creating Events
|
|
190
203
|
Use unique filenames to avoid overwriting existing events. Include a timestamp or random suffix:
|
|
191
204
|
\`\`\`bash
|
|
192
205
|
cat > ${workspacePath}/events/dentist-reminder-$(date +%s).json << 'EOF'
|
|
193
|
-
{"type": "one-shot", "channelId": "${channelId}", "text": "Dentist tomorrow", "at": "2025-12-14T09:00:00+01:00"}
|
|
206
|
+
{"type": "one-shot", "platform": "${platform.name}", "channelId": "${channelId}", "text": "Dentist tomorrow", "at": "2025-12-14T09:00:00+01:00"}
|
|
194
207
|
EOF
|
|
195
208
|
\`\`\`
|
|
196
209
|
Or check if file exists first before creating.
|
|
@@ -238,6 +251,7 @@ Update this file whenever you modify the environment. On fresh container, read i
|
|
|
238
251
|
Format: \`{"date":"...","ts":"...","user":"...","userName":"...","text":"...","isBot":false}\`
|
|
239
252
|
The log contains user messages and your final responses (not tool calls/results).
|
|
240
253
|
${isDocker ? "Install jq: apk add jq" : ""}
|
|
254
|
+
${isFirecracker ? "Install jq: apt-get install jq" : ""}
|
|
241
255
|
|
|
242
256
|
\`\`\`bash
|
|
243
257
|
# Recent messages
|
|
@@ -350,12 +364,45 @@ export async function createRunner(sandboxConfig, sessionKey, channelId, channel
|
|
|
350
364
|
};
|
|
351
365
|
const systemPrompt = buildSystemPrompt(workspacePath, channelId, memory, sandboxConfig, emptyPlatform, skills);
|
|
352
366
|
// Create session manager and settings manager
|
|
353
|
-
//
|
|
354
|
-
|
|
355
|
-
const sessionDir =
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
367
|
+
// Channel sessions use {channelDir}/sessions/current.
|
|
368
|
+
// Thread sessions use fixed files: {channelDir}/sessions/{threadTs}.jsonl
|
|
369
|
+
const sessionDir = getSessionDir(channelDir, sessionKey);
|
|
370
|
+
const isThread = sessionKey.includes(":");
|
|
371
|
+
let sessionManager;
|
|
372
|
+
let contextFile;
|
|
373
|
+
if (isThread) {
|
|
374
|
+
const threadFile = getThreadSessionFile(channelDir, sessionKey);
|
|
375
|
+
const existing = tryResolveThreadSession(threadFile);
|
|
376
|
+
if (existing) {
|
|
377
|
+
contextFile = existing;
|
|
378
|
+
sessionManager = openManagedSession(contextFile, sessionDir, channelDir);
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
const channelSource = resolveChannelSessionFile(channelDir);
|
|
382
|
+
if (channelSource) {
|
|
383
|
+
try {
|
|
384
|
+
contextFile = forkThreadSessionFile(channelSource, threadFile, channelDir);
|
|
385
|
+
sessionManager = openManagedSession(contextFile, sessionDir, channelDir);
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
contextFile = createManagedSessionFileAtPath(threadFile, channelDir);
|
|
389
|
+
sessionManager = openManagedSession(contextFile, sessionDir, channelDir);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
contextFile = createManagedSessionFileAtPath(threadFile, channelDir);
|
|
394
|
+
sessionManager = openManagedSession(contextFile, sessionDir, channelDir);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
// Channel/DM session: normal resolve
|
|
400
|
+
contextFile = resolveManagedSessionFile(sessionDir, channelDir);
|
|
401
|
+
sessionManager = openManagedSession(contextFile, sessionDir, channelDir);
|
|
402
|
+
}
|
|
403
|
+
const sessionUuid = extractSessionUuid(contextFile);
|
|
404
|
+
// Used for Slack thread filtering — for non-Slack platforms this is effectively a no-op
|
|
405
|
+
const rootTs = extractSessionSuffix(sessionKey);
|
|
359
406
|
const settingsManager = createMamaSettingsManager(join(channelDir, ".."));
|
|
360
407
|
// Create AuthStorage and ModelRegistry
|
|
361
408
|
// Auth stored outside workspace so agent can't access it
|
|
@@ -371,7 +418,7 @@ export async function createRunner(sandboxConfig, sessionKey, channelId, channel
|
|
|
371
418
|
},
|
|
372
419
|
convertToLlm,
|
|
373
420
|
getApiKey: async () => {
|
|
374
|
-
const key = await modelRegistry.
|
|
421
|
+
const key = await modelRegistry.getApiKeyForProvider(model.provider);
|
|
375
422
|
if (!key)
|
|
376
423
|
throw new Error(`No API key for provider "${model.provider}". Set the appropriate environment variable or configure via auth.json`);
|
|
377
424
|
return key;
|
|
@@ -383,25 +430,33 @@ export async function createRunner(sandboxConfig, sessionKey, channelId, channel
|
|
|
383
430
|
agent.replaceMessages(loadedSession.messages);
|
|
384
431
|
log.logInfo(`[${channelId}] Loaded ${loadedSession.messages.length} messages from context.jsonl`);
|
|
385
432
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
433
|
+
// Load extensions, skills, prompts, themes via DefaultResourceLoader
|
|
434
|
+
// This reads ~/.pi/agent/settings.json (packages, extensions enable/disable)
|
|
435
|
+
// and discovers resources from standard locations + npm/git packages.
|
|
436
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
437
|
+
cwd: workspaceDir,
|
|
438
|
+
systemPrompt,
|
|
439
|
+
});
|
|
440
|
+
try {
|
|
441
|
+
await resourceLoader.reload();
|
|
442
|
+
const extResult = resourceLoader.getExtensions();
|
|
443
|
+
if (extResult.errors.length > 0) {
|
|
444
|
+
for (const err of extResult.errors) {
|
|
445
|
+
log.logWarning(`[${channelId}] Extension load error: ${err.path}`, err.error);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
log.logInfo(`[${channelId}] Loaded ${extResult.extensions.length} extension(s): ${extResult.extensions.map((e) => e.path).join(", ")}`);
|
|
449
|
+
}
|
|
450
|
+
catch (error) {
|
|
451
|
+
log.logWarning(`[${channelId}] Failed to load resources`, String(error));
|
|
452
|
+
}
|
|
398
453
|
const baseToolsOverride = Object.fromEntries(tools.map((tool) => [tool.name, tool]));
|
|
399
454
|
// Create AgentSession wrapper
|
|
400
455
|
const session = new AgentSession({
|
|
401
456
|
agent,
|
|
402
457
|
sessionManager,
|
|
403
458
|
settingsManager,
|
|
404
|
-
cwd:
|
|
459
|
+
cwd: workspaceDir,
|
|
405
460
|
modelRegistry,
|
|
406
461
|
resourceLoader,
|
|
407
462
|
baseToolsOverride,
|
|
@@ -419,6 +474,7 @@ export async function createRunner(sandboxConfig, sessionKey, channelId, channel
|
|
|
419
474
|
cacheWrite: 0,
|
|
420
475
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
421
476
|
},
|
|
477
|
+
llmCallCount: 0,
|
|
422
478
|
stopReason: "stop",
|
|
423
479
|
errorMessage: undefined,
|
|
424
480
|
};
|
|
@@ -428,6 +484,7 @@ export async function createRunner(sandboxConfig, sessionKey, channelId, channel
|
|
|
428
484
|
if (!runState.responseCtx || !runState.logCtx || !runState.queue)
|
|
429
485
|
return;
|
|
430
486
|
const { responseCtx, logCtx, queue, pendingTools } = runState;
|
|
487
|
+
const baseAttrs = { channel_id: logCtx.channelId, session_id: logCtx.sessionId };
|
|
431
488
|
if (event.type === "tool_execution_start") {
|
|
432
489
|
const agentEvent = event;
|
|
433
490
|
const args = agentEvent.args;
|
|
@@ -437,12 +494,13 @@ export async function createRunner(sandboxConfig, sessionKey, channelId, channel
|
|
|
437
494
|
args: agentEvent.args,
|
|
438
495
|
startTime: Date.now(),
|
|
439
496
|
});
|
|
497
|
+
addLifecycleBreadcrumb("agent.tool.started", {
|
|
498
|
+
tool: agentEvent.toolName,
|
|
499
|
+
...baseAttrs,
|
|
500
|
+
});
|
|
440
501
|
log.logToolStart(logCtx, agentEvent.toolName, label, agentEvent.args);
|
|
441
|
-
//
|
|
442
|
-
|
|
443
|
-
for (const part of labelParts) {
|
|
444
|
-
queue.enqueue(() => responseCtx.respond(part), "tool label");
|
|
445
|
-
}
|
|
502
|
+
// Tool labels are omitted from the main message to reduce Slack noise.
|
|
503
|
+
// Tool execution details are still posted to the thread (see tool_execution_end).
|
|
446
504
|
}
|
|
447
505
|
else if (event.type === "tool_execution_end") {
|
|
448
506
|
const agentEvent = event;
|
|
@@ -450,6 +508,26 @@ export async function createRunner(sandboxConfig, sessionKey, channelId, channel
|
|
|
450
508
|
const pending = pendingTools.get(agentEvent.toolCallId);
|
|
451
509
|
pendingTools.delete(agentEvent.toolCallId);
|
|
452
510
|
const durationMs = pending ? Date.now() - pending.startTime : 0;
|
|
511
|
+
Sentry.metrics.count("agent.tool.calls", 1, {
|
|
512
|
+
attributes: metricAttributes({
|
|
513
|
+
tool: agentEvent.toolName,
|
|
514
|
+
error: String(agentEvent.isError),
|
|
515
|
+
...baseAttrs,
|
|
516
|
+
}),
|
|
517
|
+
});
|
|
518
|
+
Sentry.metrics.distribution("agent.tool.duration", durationMs, {
|
|
519
|
+
unit: "millisecond",
|
|
520
|
+
attributes: metricAttributes({
|
|
521
|
+
tool: agentEvent.toolName,
|
|
522
|
+
...baseAttrs,
|
|
523
|
+
}),
|
|
524
|
+
});
|
|
525
|
+
addLifecycleBreadcrumb("agent.tool.completed", {
|
|
526
|
+
tool: agentEvent.toolName,
|
|
527
|
+
error: agentEvent.isError,
|
|
528
|
+
duration_ms: durationMs,
|
|
529
|
+
...baseAttrs,
|
|
530
|
+
});
|
|
453
531
|
if (agentEvent.isError) {
|
|
454
532
|
log.logToolError(logCtx, agentEvent.toolName, durationMs, resultStr);
|
|
455
533
|
}
|
|
@@ -469,7 +547,12 @@ export async function createRunner(sandboxConfig, sessionKey, channelId, channel
|
|
|
469
547
|
if (argsFormatted)
|
|
470
548
|
threadMessage += `\`\`\`\n${argsFormatted}\n\`\`\`\n`;
|
|
471
549
|
threadMessage += `*Result:*\n\`\`\`\n${resultStr}\n\`\`\``;
|
|
472
|
-
|
|
550
|
+
// Only post thread details for tools with meaningful output (bash, attach).
|
|
551
|
+
// Skip read/write/edit to reduce Slack noise — their results are in the log.
|
|
552
|
+
const quietTools = new Set(["read", "write", "edit"]);
|
|
553
|
+
if (!quietTools.has(agentEvent.toolName)) {
|
|
554
|
+
queue.enqueueMessage(threadMessage, "thread", "tool result thread", false);
|
|
555
|
+
}
|
|
473
556
|
if (agentEvent.isError) {
|
|
474
557
|
queue.enqueue(() => responseCtx.respond(`_Error: ${truncate(resultStr, 200)}_`), "tool error");
|
|
475
558
|
}
|
|
@@ -477,6 +560,13 @@ export async function createRunner(sandboxConfig, sessionKey, channelId, channel
|
|
|
477
560
|
else if (event.type === "message_start") {
|
|
478
561
|
const agentEvent = event;
|
|
479
562
|
if (agentEvent.message.role === "assistant") {
|
|
563
|
+
runState.llmCallCount += 1;
|
|
564
|
+
addLifecycleBreadcrumb("agent.llm.call.started", {
|
|
565
|
+
call_index: runState.llmCallCount,
|
|
566
|
+
provider: model.provider,
|
|
567
|
+
model: agentConfig.model,
|
|
568
|
+
...baseAttrs,
|
|
569
|
+
});
|
|
480
570
|
log.logResponseStart(logCtx);
|
|
481
571
|
}
|
|
482
572
|
}
|
|
@@ -500,6 +590,44 @@ export async function createRunner(sandboxConfig, sessionKey, channelId, channel
|
|
|
500
590
|
runState.totalUsage.cost.cacheRead += assistantMsg.usage.cost.cacheRead;
|
|
501
591
|
runState.totalUsage.cost.cacheWrite += assistantMsg.usage.cost.cacheWrite;
|
|
502
592
|
runState.totalUsage.cost.total += assistantMsg.usage.cost.total;
|
|
593
|
+
// Per-turn LLM metrics
|
|
594
|
+
const llmAttributes = metricAttributes({
|
|
595
|
+
provider: model.provider,
|
|
596
|
+
model: agentConfig.model,
|
|
597
|
+
...baseAttrs,
|
|
598
|
+
stop_reason: assistantMsg.stopReason,
|
|
599
|
+
error: Boolean(assistantMsg.errorMessage),
|
|
600
|
+
});
|
|
601
|
+
Sentry.metrics.count("agent.llm.calls", 1, { attributes: llmAttributes });
|
|
602
|
+
Sentry.metrics.distribution("agent.llm.tokens_in", assistantMsg.usage.input, {
|
|
603
|
+
attributes: llmAttributes,
|
|
604
|
+
});
|
|
605
|
+
Sentry.metrics.distribution("agent.llm.tokens_out", assistantMsg.usage.output, {
|
|
606
|
+
attributes: llmAttributes,
|
|
607
|
+
});
|
|
608
|
+
if (assistantMsg.usage.cacheRead > 0) {
|
|
609
|
+
Sentry.metrics.distribution("agent.llm.cache_read", assistantMsg.usage.cacheRead, {
|
|
610
|
+
attributes: llmAttributes,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
if (assistantMsg.usage.cacheWrite > 0) {
|
|
614
|
+
Sentry.metrics.distribution("agent.llm.cache_write", assistantMsg.usage.cacheWrite, {
|
|
615
|
+
attributes: llmAttributes,
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
Sentry.metrics.distribution("agent.llm.cost_per_turn", assistantMsg.usage.cost.total, {
|
|
619
|
+
attributes: llmAttributes,
|
|
620
|
+
});
|
|
621
|
+
addLifecycleBreadcrumb("agent.llm.call.completed", {
|
|
622
|
+
call_index: runState.llmCallCount,
|
|
623
|
+
provider: model.provider,
|
|
624
|
+
model: agentConfig.model,
|
|
625
|
+
stop_reason: assistantMsg.stopReason,
|
|
626
|
+
error: Boolean(assistantMsg.errorMessage),
|
|
627
|
+
input_tokens: assistantMsg.usage.input,
|
|
628
|
+
output_tokens: assistantMsg.usage.output,
|
|
629
|
+
cost_total_usd: assistantMsg.usage.cost.total,
|
|
630
|
+
});
|
|
503
631
|
}
|
|
504
632
|
const content = agentEvent.message.content;
|
|
505
633
|
const thinkingParts = [];
|
|
@@ -521,15 +649,18 @@ export async function createRunner(sandboxConfig, sessionKey, channelId, channel
|
|
|
521
649
|
if (text.trim()) {
|
|
522
650
|
log.logResponse(logCtx, text);
|
|
523
651
|
queue.enqueueMessage(text, "main", "response main");
|
|
524
|
-
|
|
652
|
+
// Only overflow to thread for texts that will be truncated in main
|
|
653
|
+
if (text.length > SLACK_MAX_LENGTH) {
|
|
654
|
+
queue.enqueueMessage(text, "thread", "response thread", false);
|
|
655
|
+
}
|
|
525
656
|
}
|
|
526
657
|
}
|
|
527
658
|
}
|
|
528
|
-
else if (event.type === "
|
|
659
|
+
else if (event.type === "compaction_start") {
|
|
529
660
|
log.logInfo(`Auto-compaction started (reason: ${event.reason})`);
|
|
530
661
|
queue.enqueue(() => responseCtx.respond("_Compacting context..._"), "compaction start");
|
|
531
662
|
}
|
|
532
|
-
else if (event.type === "
|
|
663
|
+
else if (event.type === "compaction_end") {
|
|
533
664
|
const compEvent = event;
|
|
534
665
|
if (compEvent.result) {
|
|
535
666
|
log.logInfo(`Auto-compaction complete: ${compEvent.result.tokensBefore} tokens compacted`);
|
|
@@ -570,7 +701,11 @@ export async function createRunner(sandboxConfig, sessionKey, channelId, channel
|
|
|
570
701
|
// Sync messages from log.jsonl that arrived while we were offline or busy
|
|
571
702
|
// Exclude the current message (it will be added via prompt())
|
|
572
703
|
// Default sync range is 10 days (handled by syncLogToSessionManager)
|
|
573
|
-
|
|
704
|
+
// Thread filter ensures only messages from this session's thread are synced
|
|
705
|
+
const threadFilter = message.sessionKey.includes(":")
|
|
706
|
+
? { scope: "thread", rootTs, threadTs: message.threadTs }
|
|
707
|
+
: { scope: "top-level", rootTs };
|
|
708
|
+
const syncedCount = await syncLogToSessionManager(sessionManager, channelDir, message.id, undefined, threadFilter);
|
|
574
709
|
if (syncedCount > 0) {
|
|
575
710
|
log.logInfo(`[${channelId}] Synced ${syncedCount} messages from log.jsonl`);
|
|
576
711
|
}
|
|
@@ -597,6 +732,7 @@ export async function createRunner(sandboxConfig, sessionKey, channelId, channel
|
|
|
597
732
|
channelId: sessionChannel,
|
|
598
733
|
userName: message.userName,
|
|
599
734
|
channelName: undefined,
|
|
735
|
+
sessionId: sessionUuid,
|
|
600
736
|
};
|
|
601
737
|
runState.pendingTools.clear();
|
|
602
738
|
runState.totalUsage = {
|
|
@@ -606,6 +742,7 @@ export async function createRunner(sandboxConfig, sessionKey, channelId, channel
|
|
|
606
742
|
cacheWrite: 0,
|
|
607
743
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
608
744
|
};
|
|
745
|
+
runState.llmCallCount = 0;
|
|
609
746
|
runState.stopReason = "stop";
|
|
610
747
|
runState.errorMessage = undefined;
|
|
611
748
|
// Create queue for this run
|
|
@@ -651,7 +788,8 @@ export async function createRunner(sandboxConfig, sessionKey, channelId, channel
|
|
|
651
788
|
const offsetHours = pad(Math.floor(Math.abs(offset) / 60));
|
|
652
789
|
const offsetMins = pad(Math.abs(offset) % 60);
|
|
653
790
|
const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}${offsetSign}${offsetHours}:${offsetMins}`;
|
|
654
|
-
|
|
791
|
+
const threadContext = message.threadTs ? ` [in-thread:${message.threadTs}]` : "";
|
|
792
|
+
let userMessage = `[${timestamp}] [${message.userName || "unknown"}]${threadContext}: ${message.text}`;
|
|
655
793
|
const imageAttachments = [];
|
|
656
794
|
const nonImagePaths = [];
|
|
657
795
|
for (const a of message.attachments || []) {
|
|
@@ -685,6 +823,14 @@ export async function createRunner(sandboxConfig, sessionKey, channelId, channel
|
|
|
685
823
|
imageAttachmentCount: imageAttachments.length,
|
|
686
824
|
};
|
|
687
825
|
await writeFile(join(channelDir, "last_prompt.jsonl"), JSON.stringify(debugContext, null, 2));
|
|
826
|
+
addLifecycleBreadcrumb("agent.prompt.sent", {
|
|
827
|
+
provider: model.provider,
|
|
828
|
+
model: agentConfig.model,
|
|
829
|
+
channel_id: sessionChannel,
|
|
830
|
+
session_id: sessionUuid,
|
|
831
|
+
attachment_count: message.attachments?.length ?? 0,
|
|
832
|
+
image_attachment_count: imageAttachments.length,
|
|
833
|
+
});
|
|
688
834
|
await session.prompt(userMessage, imageAttachments.length > 0 ? { images: imageAttachments } : undefined);
|
|
689
835
|
// Wait for queued messages
|
|
690
836
|
await queueChain;
|
|
@@ -750,11 +896,40 @@ export async function createRunner(sandboxConfig, sessionKey, channelId, channel
|
|
|
750
896
|
lastAssistantMessage.usage.cacheWrite
|
|
751
897
|
: 0;
|
|
752
898
|
const contextWindow = model.contextWindow || 200000;
|
|
899
|
+
// Run-level Sentry metrics
|
|
900
|
+
const { totalUsage } = runState;
|
|
901
|
+
const runMetricAttributes = metricAttributes({
|
|
902
|
+
provider: model.provider,
|
|
903
|
+
model: agentConfig.model,
|
|
904
|
+
channel_id: sessionChannel,
|
|
905
|
+
session_id: sessionUuid,
|
|
906
|
+
stop_reason: runState.stopReason,
|
|
907
|
+
llm_calls: runState.llmCallCount,
|
|
908
|
+
});
|
|
909
|
+
Sentry.metrics.distribution("agent.run.tokens_in", totalUsage.input, {
|
|
910
|
+
attributes: runMetricAttributes,
|
|
911
|
+
});
|
|
912
|
+
Sentry.metrics.distribution("agent.run.tokens_out", totalUsage.output, {
|
|
913
|
+
attributes: runMetricAttributes,
|
|
914
|
+
});
|
|
915
|
+
Sentry.metrics.distribution("agent.run.cache_read", totalUsage.cacheRead, {
|
|
916
|
+
attributes: runMetricAttributes,
|
|
917
|
+
});
|
|
918
|
+
Sentry.metrics.distribution("agent.run.cache_write", totalUsage.cacheWrite, {
|
|
919
|
+
attributes: runMetricAttributes,
|
|
920
|
+
});
|
|
921
|
+
Sentry.metrics.distribution("agent.run.cost", totalUsage.cost.total, {
|
|
922
|
+
attributes: runMetricAttributes,
|
|
923
|
+
});
|
|
924
|
+
Sentry.metrics.gauge("agent.context.utilization", contextTokens / contextWindow, {
|
|
925
|
+
unit: "ratio",
|
|
926
|
+
attributes: runMetricAttributes,
|
|
927
|
+
});
|
|
753
928
|
const summary = log.logUsageSummary(runState.logCtx, runState.totalUsage, contextTokens, contextWindow);
|
|
754
929
|
// Split long summaries to avoid msg_too_long
|
|
755
930
|
const summaryParts = splitForSlack(summary);
|
|
756
931
|
for (const part of summaryParts) {
|
|
757
|
-
runState.queue.enqueue(() => responseCtx.respondInThread(part), "usage summary");
|
|
932
|
+
runState.queue.enqueue(() => responseCtx.respondInThread(part, { style: "muted" }), "usage summary");
|
|
758
933
|
}
|
|
759
934
|
await queueChain;
|
|
760
935
|
}
|