@geminixiang/mama 0.2.0-beta.6 → 0.2.0-beta.8
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 +20 -14
- package/dist/adapter.d.ts +5 -2
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js.map +1 -1
- package/dist/adapters/discord/bot.d.ts +1 -0
- package/dist/adapters/discord/bot.d.ts.map +1 -1
- package/dist/adapters/discord/bot.js +29 -9
- package/dist/adapters/discord/bot.js.map +1 -1
- package/dist/adapters/discord/context.d.ts +1 -1
- package/dist/adapters/discord/context.d.ts.map +1 -1
- package/dist/adapters/discord/context.js +1 -2
- package/dist/adapters/discord/context.js.map +1 -1
- package/dist/adapters/slack/bot.d.ts +8 -0
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +177 -11
- package/dist/adapters/slack/bot.js.map +1 -1
- package/dist/adapters/slack/branch-manager.d.ts +1 -0
- package/dist/adapters/slack/branch-manager.d.ts.map +1 -1
- package/dist/adapters/slack/branch-manager.js +9 -8
- package/dist/adapters/slack/branch-manager.js.map +1 -1
- package/dist/adapters/slack/context.d.ts +1 -1
- package/dist/adapters/slack/context.d.ts.map +1 -1
- package/dist/adapters/slack/context.js +10 -8
- package/dist/adapters/slack/context.js.map +1 -1
- package/dist/adapters/slack/tools/attach.d.ts +1 -1
- package/dist/adapters/slack/tools/attach.d.ts.map +1 -1
- package/dist/adapters/slack/tools/attach.js.map +1 -1
- package/dist/adapters/telegram/bot.d.ts.map +1 -1
- package/dist/adapters/telegram/bot.js +33 -2
- package/dist/adapters/telegram/bot.js.map +1 -1
- package/dist/agent.d.ts +1 -2
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +507 -422
- package/dist/agent.js.map +1 -1
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +2 -0
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +41 -2
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/model.d.ts +1 -1
- package/dist/commands/model.d.ts.map +1 -1
- package/dist/commands/model.js +25 -7
- package/dist/commands/model.js.map +1 -1
- package/dist/commands/new.d.ts.map +1 -1
- package/dist/commands/new.js +1 -1
- package/dist/commands/new.js.map +1 -1
- package/dist/commands/sandbox.d.ts +10 -0
- package/dist/commands/sandbox.d.ts.map +1 -0
- package/dist/commands/sandbox.js +88 -0
- package/dist/commands/sandbox.js.map +1 -0
- package/dist/commands/session-view.d.ts.map +1 -1
- package/dist/commands/session-view.js +34 -10
- package/dist/commands/session-view.js.map +1 -1
- package/dist/commands/types.d.ts +1 -3
- package/dist/commands/types.d.ts.map +1 -1
- package/dist/commands/types.js.map +1 -1
- package/dist/commands/utils.d.ts +3 -0
- package/dist/commands/utils.d.ts.map +1 -1
- package/dist/commands/utils.js +5 -0
- package/dist/commands/utils.js.map +1 -1
- package/dist/config.d.ts +7 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +64 -23
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +2 -44
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +7 -210
- package/dist/context.js.map +1 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +15 -14
- package/dist/events.js.map +1 -1
- package/dist/execution-resolver.d.ts +3 -2
- package/dist/execution-resolver.d.ts.map +1 -1
- package/dist/execution-resolver.js +40 -7
- package/dist/execution-resolver.js.map +1 -1
- package/dist/file-guards.d.ts +6 -0
- package/dist/file-guards.d.ts.map +1 -0
- package/dist/file-guards.js +48 -0
- package/dist/file-guards.js.map +1 -0
- package/dist/log.d.ts +1 -5
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +13 -38
- package/dist/log.js.map +1 -1
- package/dist/login/index.d.ts +14 -2
- package/dist/login/index.d.ts.map +1 -1
- package/dist/login/index.js +40 -13
- package/dist/login/index.js.map +1 -1
- package/dist/login/portal.d.ts +2 -1
- package/dist/login/portal.d.ts.map +1 -1
- package/dist/login/portal.js +12 -12
- package/dist/login/portal.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +33 -28
- package/dist/main.js.map +1 -1
- package/dist/provisioner.d.ts +12 -2
- package/dist/provisioner.d.ts.map +1 -1
- package/dist/provisioner.js +43 -14
- package/dist/provisioner.js.map +1 -1
- package/dist/runtime/conversation-orchestrator.d.ts +42 -0
- package/dist/runtime/conversation-orchestrator.d.ts.map +1 -0
- package/dist/runtime/conversation-orchestrator.js +150 -0
- package/dist/runtime/conversation-orchestrator.js.map +1 -0
- package/dist/runtime/session-runtime.d.ts +1 -1
- package/dist/runtime/session-runtime.d.ts.map +1 -1
- package/dist/runtime/session-runtime.js +49 -148
- package/dist/runtime/session-runtime.js.map +1 -1
- package/dist/sandbox/cloudflare.d.ts.map +1 -1
- package/dist/sandbox/cloudflare.js +2 -2
- package/dist/sandbox/cloudflare.js.map +1 -1
- package/dist/sandbox/container.d.ts.map +1 -1
- package/dist/sandbox/container.js +1 -1
- package/dist/sandbox/container.js.map +1 -1
- package/dist/sandbox/index.d.ts.map +1 -1
- package/dist/sandbox/index.js +4 -4
- package/dist/sandbox/index.js.map +1 -1
- package/dist/sentry.d.ts +1 -1
- package/dist/sentry.d.ts.map +1 -1
- package/dist/sentry.js +2 -2
- package/dist/sentry.js.map +1 -1
- package/dist/session-store.d.ts +2 -1
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +19 -15
- package/dist/session-store.js.map +1 -1
- package/dist/session-view/portal.d.ts +6 -1
- package/dist/session-view/portal.d.ts.map +1 -1
- package/dist/session-view/portal.js +829 -71
- package/dist/session-view/portal.js.map +1 -1
- package/dist/session-view/service.d.ts.map +1 -1
- package/dist/session-view/service.js +5 -4
- package/dist/session-view/service.js.map +1 -1
- package/dist/session-view/store.d.ts +2 -1
- package/dist/session-view/store.d.ts.map +1 -1
- package/dist/session-view/store.js +2 -1
- package/dist/session-view/store.js.map +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +7 -13
- package/dist/store.js.map +1 -1
- package/dist/tool-diagnostics.d.ts +2 -0
- package/dist/tool-diagnostics.d.ts.map +1 -0
- package/dist/tool-diagnostics.js +7 -0
- package/dist/tool-diagnostics.js.map +1 -0
- package/dist/tools/bash.d.ts +1 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/edit.d.ts +1 -1
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/event.d.ts +1 -1
- package/dist/tools/event.d.ts.map +1 -1
- package/dist/tools/event.js.map +1 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/read.d.ts +1 -1
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/write.d.ts +1 -1
- package/dist/tools/write.d.ts.map +1 -1
- package/dist/tools/write.js.map +1 -1
- package/dist/vault-routing.d.ts +0 -3
- package/dist/vault-routing.d.ts.map +1 -1
- package/dist/vault-routing.js +0 -24
- package/dist/vault-routing.js.map +1 -1
- package/dist/vault.d.ts +21 -57
- package/dist/vault.d.ts.map +1 -1
- package/dist/vault.js +114 -246
- package/dist/vault.js.map +1 -1
- package/package.json +6 -4
- package/dist/bindings.d.ts +0 -45
- package/dist/bindings.d.ts.map +0 -1
- package/dist/bindings.js +0 -75
- package/dist/bindings.js.map +0 -1
package/dist/agent.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { Agent } from "@
|
|
2
|
-
import { getModel } from "@
|
|
3
|
-
import { AgentSession, AuthStorage, convertToLlm, DefaultResourceLoader, formatSkillsForPrompt, getAgentDir, loadSkillsFromDir, ModelRegistry, } from "@
|
|
1
|
+
import { Agent } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { getModel } from "@earendil-works/pi-ai";
|
|
3
|
+
import { AgentSession, AuthStorage, convertToLlm, DefaultResourceLoader, formatSkillsForPrompt, getAgentDir, loadSkillsFromDir, ModelRegistry, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { existsSync, readFileSync } from "fs";
|
|
5
5
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
6
6
|
import { homedir } from "os";
|
|
7
|
-
import { join } from "path";
|
|
7
|
+
import { join, posix } from "path";
|
|
8
8
|
import { loadAgentConfigForConversation } from "./config.js";
|
|
9
|
-
import { createMamaSettingsManager, syncLogToSessionManager } from "./context.js";
|
|
10
9
|
import { ActorExecutionResolver } from "./execution-resolver.js";
|
|
11
10
|
import * as log from "./log.js";
|
|
12
11
|
import { createExecutor } from "./sandbox.js";
|
|
13
12
|
import { addLifecycleBreadcrumb, metricAttributes } from "./sentry.js";
|
|
14
|
-
import {
|
|
13
|
+
import { extractSessionUuid, openManagedSession, } from "./session-store.js";
|
|
14
|
+
import { shouldSurfaceToolDiagnostic } from "./tool-diagnostics.js";
|
|
15
15
|
import { createMamaTools } from "./tools/index.js";
|
|
16
16
|
import * as Sentry from "@sentry/node";
|
|
17
17
|
const IMAGE_MIME_TYPES = {
|
|
@@ -94,12 +94,53 @@ function loadMamaSkills(conversationDir, workspacePath) {
|
|
|
94
94
|
}
|
|
95
95
|
return Array.from(skillMap.values());
|
|
96
96
|
}
|
|
97
|
+
function buildRuntimePaths(workspacePath, conversationId) {
|
|
98
|
+
const workspaceRoot = workspacePath.replace(/\/+$/, "") || "/";
|
|
99
|
+
const conversationPath = posix.join(workspaceRoot, conversationId);
|
|
100
|
+
return {
|
|
101
|
+
workspaceRoot,
|
|
102
|
+
conversationPath,
|
|
103
|
+
scratchPath: posix.join(conversationPath, "scratch"),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function buildEnvDescription(sandboxType, workspaceRoot) {
|
|
107
|
+
switch (sandboxType) {
|
|
108
|
+
case "image":
|
|
109
|
+
return `You are running inside a managed per-user container.
|
|
110
|
+
- Runtime workspace root: ${workspaceRoot}
|
|
111
|
+
- Bash commands start in: ${workspaceRoot}
|
|
112
|
+
- Install tools with the image's package manager
|
|
113
|
+
- Your changes persist for this user's container until it is recreated`;
|
|
114
|
+
case "container":
|
|
115
|
+
return `You are running inside a shared container.
|
|
116
|
+
- Runtime workspace root: ${workspaceRoot}
|
|
117
|
+
- Bash commands start in: ${workspaceRoot}
|
|
118
|
+
- Install tools with the container's package manager
|
|
119
|
+
- Your changes persist across sessions`;
|
|
120
|
+
case "firecracker":
|
|
121
|
+
return `You are running inside a Firecracker microVM.
|
|
122
|
+
- Runtime workspace root: ${workspaceRoot}
|
|
123
|
+
- Use cd or absolute paths; project files are under ${workspaceRoot}
|
|
124
|
+
- Install tools with: apt-get install <package> (Debian-based)
|
|
125
|
+
- Your changes persist across sessions`;
|
|
126
|
+
case "cloudflare":
|
|
127
|
+
return `You are running through a Cloudflare Sandbox bridge.
|
|
128
|
+
- Runtime workspace root: ${workspaceRoot}
|
|
129
|
+
- Bash commands start in: ${workspaceRoot}
|
|
130
|
+
- Your commands run in a remote container managed by Cloudflare
|
|
131
|
+
- Important: the remote filesystem is not automatically synced back to the host workspace`;
|
|
132
|
+
default:
|
|
133
|
+
return `You are running directly on the host machine.
|
|
134
|
+
- Runtime workspace root: ${workspaceRoot}
|
|
135
|
+
- Bash commands start in: ${process.cwd()}
|
|
136
|
+
- Be careful with system modifications`;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
97
139
|
function buildSystemPrompt(workspacePath, conversationId, conversationKind, currentUserId, memory, sandboxConfig, platform, skills) {
|
|
98
|
-
const conversationPath =
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
const isFirecracker =
|
|
102
|
-
const isCloudflareSandbox = sandboxConfig.type === "cloudflare";
|
|
140
|
+
const { workspaceRoot, conversationPath, scratchPath } = buildRuntimePaths(workspacePath, conversationId);
|
|
141
|
+
const sandboxType = sandboxConfig.type;
|
|
142
|
+
const isContainerLike = sandboxType === "container" || sandboxType === "image";
|
|
143
|
+
const isFirecracker = sandboxType === "firecracker";
|
|
103
144
|
// Format channel mappings
|
|
104
145
|
const channelMappings = platform.channels.length > 0
|
|
105
146
|
? platform.channels.map((c) => `${c.id}\t#${c.name}`).join("\n")
|
|
@@ -108,29 +149,7 @@ function buildSystemPrompt(workspacePath, conversationId, conversationKind, curr
|
|
|
108
149
|
const userMappings = platform.users.length > 0
|
|
109
150
|
? platform.users.map((u) => `${u.id}\t@${u.userName}\t${u.displayName}`).join("\n")
|
|
110
151
|
: "(no users loaded)";
|
|
111
|
-
const envDescription =
|
|
112
|
-
? `You are running inside a managed per-user container.
|
|
113
|
-
- Bash working directory: / (use cd or absolute paths)
|
|
114
|
-
- Install tools with the image's package manager
|
|
115
|
-
- Your changes persist for this user's container until it is recreated`
|
|
116
|
-
: isContainer
|
|
117
|
-
? `You are running inside a shared container.
|
|
118
|
-
- Bash working directory: / (use cd or absolute paths)
|
|
119
|
-
- Install tools with the container's package manager
|
|
120
|
-
- Your changes persist across sessions`
|
|
121
|
-
: isFirecracker
|
|
122
|
-
? `You are running inside a Firecracker microVM.
|
|
123
|
-
- Bash working directory: / (use cd or absolute paths)
|
|
124
|
-
- Install tools with: apt-get install <package> (Debian-based)
|
|
125
|
-
- Your changes persist across sessions`
|
|
126
|
-
: isCloudflareSandbox
|
|
127
|
-
? `You are running through a Cloudflare Sandbox bridge.
|
|
128
|
-
- Bash working directory: /workspace
|
|
129
|
-
- Your commands run in a remote container managed by Cloudflare
|
|
130
|
-
- Important: the remote filesystem is not automatically synced back to the host workspace`
|
|
131
|
-
: `You are running directly on the host machine.
|
|
132
|
-
- Bash working directory: ${process.cwd()}
|
|
133
|
-
- Be careful with system modifications`;
|
|
152
|
+
const envDescription = buildEnvDescription(sandboxType, workspaceRoot);
|
|
134
153
|
return `You are mama, a ${platform.name} bot assistant. Be concise. No emojis.
|
|
135
154
|
|
|
136
155
|
## Context
|
|
@@ -153,9 +172,11 @@ When mentioning users, use <@username> format (e.g., <@mario>).
|
|
|
153
172
|
|
|
154
173
|
## Environment
|
|
155
174
|
${envDescription}
|
|
175
|
+
- Default place for clones, downloads, and experiments: ${scratchPath}
|
|
176
|
+
- Do not use host-only paths unless you are running in host mode and verified they exist.
|
|
156
177
|
|
|
157
178
|
## Workspace Layout
|
|
158
|
-
${
|
|
179
|
+
${workspaceRoot}/
|
|
159
180
|
├── MEMORY.md # Global memory (all conversations)
|
|
160
181
|
├── skills/ # Global CLI tools you create
|
|
161
182
|
└── ${conversationId}/ # This conversation
|
|
@@ -166,14 +187,14 @@ ${workspacePath}/
|
|
|
166
187
|
│ ├── <timestamp>_<id>.jsonl # Top-level session files
|
|
167
188
|
│ └── <scope_id>.jsonl # Scoped thread/reply session files
|
|
168
189
|
├── attachments/ # User-shared files
|
|
169
|
-
├── scratch/ #
|
|
190
|
+
├── scratch/ # Working directory for clones/downloads/experiments: ${scratchPath}
|
|
170
191
|
└── skills/ # Conversation-specific tools
|
|
171
192
|
|
|
172
193
|
## Skills (Custom CLI Tools)
|
|
173
194
|
You can create reusable CLI tools for recurring tasks (email, APIs, data processing, etc.).
|
|
174
195
|
|
|
175
196
|
### Creating Skills
|
|
176
|
-
Store in \`${
|
|
197
|
+
Store in \`${workspaceRoot}/skills/<name>/\` (global) or \`${conversationPath}/skills/<name>/\` (conversation-specific).
|
|
177
198
|
Each skill directory needs a \`SKILL.md\` with YAML frontmatter:
|
|
178
199
|
|
|
179
200
|
\`\`\`markdown
|
|
@@ -194,7 +215,7 @@ Scripts are in: {baseDir}/
|
|
|
194
215
|
${skills.length > 0 ? formatSkillsForPrompt(skills) : "(no skills installed yet)"}
|
|
195
216
|
|
|
196
217
|
## Events
|
|
197
|
-
You can schedule events that wake you up at specific times or when external things happen. Events are JSON files in \`${
|
|
218
|
+
You can schedule events that wake you up at specific times or when external things happen. Events are JSON files in \`${workspaceRoot}/events/\`.
|
|
198
219
|
|
|
199
220
|
### Event Types
|
|
200
221
|
|
|
@@ -233,16 +254,16 @@ Prefer the \`event\` tool over manually writing JSON files; it fills \`platform\
|
|
|
233
254
|
### Creating Events
|
|
234
255
|
Use unique filenames to avoid overwriting existing events. Include a timestamp or random suffix:
|
|
235
256
|
\`\`\`bash
|
|
236
|
-
cat > ${
|
|
257
|
+
cat > ${workspaceRoot}/events/dentist-reminder-$(date +%s).json << 'EOF'
|
|
237
258
|
{"type": "one-shot", "platform": "${platform.name}", "conversationId": "${conversationId}", "conversationKind": "${conversationKind}", "userId": "${currentUserId ?? "<requester userId>"}", "text": "Dentist tomorrow", "at": "2025-12-14T09:00:00+01:00"}
|
|
238
259
|
EOF
|
|
239
260
|
\`\`\`
|
|
240
261
|
Or check if file exists first before creating.
|
|
241
262
|
|
|
242
263
|
### Managing Events
|
|
243
|
-
- List: \`ls ${
|
|
244
|
-
- View: \`cat ${
|
|
245
|
-
- Delete/cancel: \`rm ${
|
|
264
|
+
- List: \`ls ${workspaceRoot}/events/\`
|
|
265
|
+
- View: \`cat ${workspaceRoot}/events/foo.json\`
|
|
266
|
+
- Delete/cancel: \`rm ${workspaceRoot}/events/foo.json\`
|
|
246
267
|
|
|
247
268
|
### When Events Trigger
|
|
248
269
|
You receive a message like:
|
|
@@ -262,7 +283,7 @@ Maximum 5 events can be queued. Don't create excessive immediate or periodic eve
|
|
|
262
283
|
|
|
263
284
|
## Memory
|
|
264
285
|
Write to MEMORY.md files to persist context across conversations.
|
|
265
|
-
- Global (${
|
|
286
|
+
- Global (${workspaceRoot}/MEMORY.md): skills, preferences, project info
|
|
266
287
|
- Conversation (${conversationPath}/MEMORY.md): conversation-specific decisions, ongoing work
|
|
267
288
|
Update when you learn something important or when asked to remember something.
|
|
268
289
|
|
|
@@ -270,7 +291,7 @@ Update when you learn something important or when asked to remember something.
|
|
|
270
291
|
${memory}
|
|
271
292
|
|
|
272
293
|
## System Configuration Log
|
|
273
|
-
Maintain ${
|
|
294
|
+
Maintain ${workspaceRoot}/SYSTEM.md to log all environment modifications:
|
|
274
295
|
- Installed packages (apt install, npm install, uv pip install)
|
|
275
296
|
- Environment variables set
|
|
276
297
|
- Config files modified (~/.gitconfig, cron jobs, etc.)
|
|
@@ -282,8 +303,7 @@ Update this file whenever you modify the environment. On fresh container, read i
|
|
|
282
303
|
Format: \`{"date":"...","ts":"...","user":"...","userName":"...","text":"...","isBot":false}\`
|
|
283
304
|
The log contains user messages and your final responses (not tool calls/results).
|
|
284
305
|
Use \`log.jsonl\` for quick grep-style history. Use \`${conversationPath}/sessions/\` when you need structured turns, tool outputs, or branch lineage.
|
|
285
|
-
${
|
|
286
|
-
${isFirecracker ? "Install jq: apt-get install jq" : ""}
|
|
306
|
+
${isContainerLike || isFirecracker ? "Install jq: apt-get install jq" : ""}
|
|
287
307
|
|
|
288
308
|
\`\`\`bash
|
|
289
309
|
# Recent messages
|
|
@@ -315,50 +335,10 @@ function truncate(text, maxLen) {
|
|
|
315
335
|
return text;
|
|
316
336
|
return `${text.substring(0, maxLen - 3)}...`;
|
|
317
337
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const QUIET_TOOLS = new Set(["read", "write", "edit"]);
|
|
321
|
-
// Cap raw tool output before handing it to adapters. Bash output can be MB; without
|
|
322
|
-
// this each adapter's splitter would fan it out into many sequential platform posts.
|
|
323
|
-
const TOOL_RESULT_DIAGNOSTIC_CAP = 8000;
|
|
324
|
-
function extractToolResultText(result) {
|
|
325
|
-
if (typeof result === "string") {
|
|
326
|
-
return result;
|
|
327
|
-
}
|
|
328
|
-
if (result &&
|
|
329
|
-
typeof result === "object" &&
|
|
330
|
-
"content" in result &&
|
|
331
|
-
Array.isArray(result.content)) {
|
|
332
|
-
const content = result.content;
|
|
333
|
-
const textParts = [];
|
|
334
|
-
for (const part of content) {
|
|
335
|
-
if (part.type === "text" && part.text) {
|
|
336
|
-
textParts.push(part.text);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
if (textParts.length > 0) {
|
|
340
|
-
return textParts.join("\n");
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
return JSON.stringify(result);
|
|
338
|
+
function initialWorkspacePath(sandboxConfig, hostWorkspacePath) {
|
|
339
|
+
return sandboxConfig.type === "host" ? hostWorkspacePath : "/workspace";
|
|
344
340
|
}
|
|
345
|
-
|
|
346
|
-
// Agent runner
|
|
347
|
-
// ============================================================================
|
|
348
|
-
/**
|
|
349
|
-
* Create a new AgentRunner for a channel.
|
|
350
|
-
* Sets up the session and subscribes to events once.
|
|
351
|
-
*
|
|
352
|
-
* Runner caching is handled by the caller (channelStates in main.ts).
|
|
353
|
-
* This is a stateless factory function.
|
|
354
|
-
*/
|
|
355
|
-
export async function createRunner(sandboxConfig, sessionKey, conversationId, conversationDir, workspaceDir, sessionScope, vaultManager, bindingStore, provisioner) {
|
|
356
|
-
const agentConfig = loadAgentConfigForConversation(conversationDir);
|
|
357
|
-
// Initialize logger with settings from config
|
|
358
|
-
log.initLogger({
|
|
359
|
-
logFormat: agentConfig.logFormat,
|
|
360
|
-
logLevel: agentConfig.logLevel,
|
|
361
|
-
});
|
|
341
|
+
function createRunnerExecutionContext(sandboxConfig, vaultManager, provisioner, workspaceDir, hostWorkspacePath) {
|
|
362
342
|
const executionResolver = vaultManager &&
|
|
363
343
|
sandboxConfig.type !== "host" &&
|
|
364
344
|
(vaultManager.isEnabled() ||
|
|
@@ -384,68 +364,42 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
|
|
|
384
364
|
return activeExecutor.getSandboxConfig();
|
|
385
365
|
},
|
|
386
366
|
};
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const model = getModel(agentConfig.provider, agentConfig.model);
|
|
397
|
-
// Initial system prompt (will be updated each run with fresh memory/channels/users/skills)
|
|
398
|
-
const memory = await getMemory(conversationDir);
|
|
399
|
-
const skills = loadMamaSkills(conversationDir, workspacePath);
|
|
400
|
-
const emptyPlatform = {
|
|
401
|
-
name: "chat",
|
|
402
|
-
formattingGuide: "",
|
|
403
|
-
channels: [],
|
|
404
|
-
users: [],
|
|
367
|
+
return {
|
|
368
|
+
executionResolver,
|
|
369
|
+
executor,
|
|
370
|
+
getWorkspacePath: () => executor.getWorkspacePath(hostWorkspacePath),
|
|
371
|
+
async resolveExecutorForRun(context) {
|
|
372
|
+
if (!executionResolver)
|
|
373
|
+
return;
|
|
374
|
+
activeExecutor = await executionResolver.resolve(context);
|
|
375
|
+
},
|
|
405
376
|
};
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
// Platform-specific branch/fork behavior is resolved before runner creation.
|
|
410
|
-
const isThread = sessionKey.includes(":");
|
|
411
|
-
const rootTs = extractSessionSuffix(sessionKey);
|
|
412
|
-
const { sessionDir, contextFile, threadRootMessage } = sessionScope;
|
|
413
|
-
const sessionManager = openManagedSession(contextFile, sessionDir, conversationDir);
|
|
414
|
-
const threadSessionName = buildThreadSessionName(threadRootMessage);
|
|
415
|
-
if (isThread && threadSessionName && sessionManager.getSessionName() !== threadSessionName) {
|
|
416
|
-
sessionManager.appendSessionInfo(threadSessionName);
|
|
417
|
-
}
|
|
418
|
-
const sessionUuid = extractSessionUuid(contextFile);
|
|
419
|
-
const settingsManager = createMamaSettingsManager(join(conversationDir, ".."));
|
|
420
|
-
// Create AuthStorage and ModelRegistry
|
|
421
|
-
// Auth stored outside workspace so agent can't access it
|
|
377
|
+
}
|
|
378
|
+
async function createConfiguredAgentSession(params) {
|
|
379
|
+
const { conversationId, workspaceDir, workspacePath, systemPrompt, model, thinkingLevel, tools, sessionManager, settingsManager, } = params;
|
|
422
380
|
const authStorage = AuthStorage.create(join(homedir(), ".pi", "mama", "auth.json"));
|
|
423
381
|
const modelRegistry = ModelRegistry.create(authStorage);
|
|
424
|
-
// Create agent
|
|
425
382
|
const agent = new Agent({
|
|
426
383
|
initialState: {
|
|
427
384
|
systemPrompt,
|
|
428
385
|
model,
|
|
429
|
-
thinkingLevel
|
|
386
|
+
thinkingLevel,
|
|
430
387
|
tools,
|
|
431
388
|
},
|
|
432
389
|
convertToLlm,
|
|
433
390
|
getApiKey: async () => {
|
|
434
391
|
const key = await modelRegistry.getApiKeyForProvider(model.provider);
|
|
435
|
-
if (!key)
|
|
392
|
+
if (!key) {
|
|
436
393
|
throw new Error(`No API key for provider "${model.provider}". Set the appropriate environment variable or configure via auth.json`);
|
|
394
|
+
}
|
|
437
395
|
return key;
|
|
438
396
|
},
|
|
439
397
|
});
|
|
440
|
-
// Load existing messages
|
|
441
398
|
const loadedSession = sessionManager.buildSessionContext();
|
|
442
399
|
if (loadedSession.messages.length > 0) {
|
|
443
400
|
agent.state.messages = loadedSession.messages;
|
|
444
401
|
log.logInfo(`[${conversationId}] Loaded ${loadedSession.messages.length} messages from context.jsonl`);
|
|
445
402
|
}
|
|
446
|
-
// Load extensions, skills, prompts, themes via DefaultResourceLoader
|
|
447
|
-
// This reads ~/.pi/agent/settings.json (packages, extensions enable/disable)
|
|
448
|
-
// and discovers resources from standard locations + npm/git packages.
|
|
449
403
|
const resourceLoader = new DefaultResourceLoader({
|
|
450
404
|
cwd: workspaceDir,
|
|
451
405
|
agentDir: getAgentDir(),
|
|
@@ -459,111 +413,354 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
|
|
|
459
413
|
log.logWarning(`[${conversationId}] Extension load error: ${err.path}`, err.error);
|
|
460
414
|
}
|
|
461
415
|
}
|
|
462
|
-
log.logInfo(`[${conversationId}] Loaded ${extResult.extensions.length} extension(s): ${extResult.extensions.map((
|
|
416
|
+
log.logInfo(`[${conversationId}] Loaded ${extResult.extensions.length} extension(s): ${extResult.extensions.map((extension) => extension.path).join(", ")}`);
|
|
463
417
|
}
|
|
464
418
|
catch (error) {
|
|
465
419
|
log.logWarning(`[${conversationId}] Failed to load resources`, String(error));
|
|
466
420
|
}
|
|
467
421
|
const baseToolsOverride = Object.fromEntries(tools.map((tool) => [tool.name, tool]));
|
|
468
|
-
// Create AgentSession wrapper
|
|
469
422
|
const session = new AgentSession({
|
|
470
423
|
agent,
|
|
471
424
|
sessionManager,
|
|
472
425
|
settingsManager,
|
|
473
|
-
cwd:
|
|
426
|
+
cwd: workspacePath,
|
|
474
427
|
modelRegistry,
|
|
475
428
|
resourceLoader,
|
|
476
429
|
baseToolsOverride,
|
|
477
430
|
});
|
|
478
|
-
|
|
479
|
-
|
|
431
|
+
return { agent, session };
|
|
432
|
+
}
|
|
433
|
+
function createEmptyUsageTotals() {
|
|
434
|
+
return {
|
|
435
|
+
input: 0,
|
|
436
|
+
output: 0,
|
|
437
|
+
cacheRead: 0,
|
|
438
|
+
cacheWrite: 0,
|
|
439
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
function createRunState() {
|
|
443
|
+
return {
|
|
480
444
|
responseCtx: null,
|
|
481
445
|
logCtx: null,
|
|
482
446
|
queue: null,
|
|
483
447
|
pendingTools: new Map(),
|
|
484
|
-
totalUsage:
|
|
485
|
-
input: 0,
|
|
486
|
-
output: 0,
|
|
487
|
-
cacheRead: 0,
|
|
488
|
-
cacheWrite: 0,
|
|
489
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
490
|
-
},
|
|
448
|
+
totalUsage: createEmptyUsageTotals(),
|
|
491
449
|
llmCallCount: 0,
|
|
492
450
|
stopReason: "stop",
|
|
493
451
|
errorMessage: undefined,
|
|
494
452
|
};
|
|
495
|
-
|
|
453
|
+
}
|
|
454
|
+
function resetRunState(runState, responseCtx, sessionConversation, userName, sessionUuid) {
|
|
455
|
+
runState.responseCtx = responseCtx;
|
|
456
|
+
runState.logCtx = {
|
|
457
|
+
conversationId: sessionConversation,
|
|
458
|
+
userName,
|
|
459
|
+
conversationName: undefined,
|
|
460
|
+
sessionId: sessionUuid,
|
|
461
|
+
};
|
|
462
|
+
runState.pendingTools.clear();
|
|
463
|
+
runState.totalUsage = createEmptyUsageTotals();
|
|
464
|
+
runState.llmCallCount = 0;
|
|
465
|
+
runState.stopReason = "stop";
|
|
466
|
+
runState.errorMessage = undefined;
|
|
467
|
+
}
|
|
468
|
+
function createRunQueue(responseCtx) {
|
|
469
|
+
let queueChain = Promise.resolve();
|
|
470
|
+
return {
|
|
471
|
+
queue: {
|
|
472
|
+
enqueue(fn, errorContext) {
|
|
473
|
+
queueChain = queueChain.then(async () => {
|
|
474
|
+
try {
|
|
475
|
+
await fn();
|
|
476
|
+
}
|
|
477
|
+
catch (err) {
|
|
478
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
479
|
+
log.logWarning(`API error (${errorContext})`, errMsg);
|
|
480
|
+
try {
|
|
481
|
+
await responseCtx.respondDiagnostic(`Error: ${errMsg}`, { style: "error" });
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
// Ignore
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
wait: () => queueChain,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
function padTwoDigits(n) {
|
|
494
|
+
return n.toString().padStart(2, "0");
|
|
495
|
+
}
|
|
496
|
+
function formatTimestampedUserMessage(message) {
|
|
497
|
+
const now = new Date();
|
|
498
|
+
const offset = -now.getTimezoneOffset();
|
|
499
|
+
const offsetSign = offset >= 0 ? "+" : "-";
|
|
500
|
+
const offsetHours = padTwoDigits(Math.floor(Math.abs(offset) / 60));
|
|
501
|
+
const offsetMins = padTwoDigits(Math.abs(offset) % 60);
|
|
502
|
+
const timestamp = `${now.getFullYear()}-${padTwoDigits(now.getMonth() + 1)}-${padTwoDigits(now.getDate())} ` +
|
|
503
|
+
`${padTwoDigits(now.getHours())}:${padTwoDigits(now.getMinutes())}:${padTwoDigits(now.getSeconds())}` +
|
|
504
|
+
`${offsetSign}${offsetHours}:${offsetMins}`;
|
|
505
|
+
const threadContext = message.threadTs ? ` [in-thread:${message.threadTs}]` : "";
|
|
506
|
+
return `[${timestamp}] [${message.userName || "unknown"}]${threadContext}: ${message.text}`;
|
|
507
|
+
}
|
|
508
|
+
function collectMessageAttachments(message, workspacePath) {
|
|
509
|
+
const imageAttachments = [];
|
|
510
|
+
const nonImagePaths = [];
|
|
511
|
+
for (const attachment of message.attachments || []) {
|
|
512
|
+
const fullPath = `${workspacePath}/${attachment.localPath}`;
|
|
513
|
+
const mimeType = getImageMimeType(attachment.localPath);
|
|
514
|
+
if (mimeType && existsSync(fullPath)) {
|
|
515
|
+
try {
|
|
516
|
+
imageAttachments.push({
|
|
517
|
+
type: "image",
|
|
518
|
+
mimeType,
|
|
519
|
+
data: readFileSync(fullPath).toString("base64"),
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
nonImagePaths.push(fullPath);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
nonImagePaths.push(fullPath);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return { imageAttachments, nonImagePaths };
|
|
531
|
+
}
|
|
532
|
+
function buildPromptPayload(message, workspacePath) {
|
|
533
|
+
let userMessage = formatTimestampedUserMessage(message);
|
|
534
|
+
const { imageAttachments, nonImagePaths } = collectMessageAttachments(message, workspacePath);
|
|
535
|
+
if (nonImagePaths.length > 0) {
|
|
536
|
+
userMessage += `\n\n<slack_attachments>\n${nonImagePaths.join("\n")}\n</slack_attachments>`;
|
|
537
|
+
}
|
|
538
|
+
return { userMessage, imageAttachments };
|
|
539
|
+
}
|
|
540
|
+
async function writePromptDebugContext(conversationDir, systemPrompt, session, userMessage, imageAttachmentCount) {
|
|
541
|
+
const debugContext = {
|
|
542
|
+
systemPrompt,
|
|
543
|
+
messages: session.messages,
|
|
544
|
+
newUserMessage: userMessage,
|
|
545
|
+
imageAttachmentCount,
|
|
546
|
+
};
|
|
547
|
+
await writeFile(join(conversationDir, "last_prompt.jsonl"), JSON.stringify(debugContext, null, 2));
|
|
548
|
+
}
|
|
549
|
+
function getFinalAssistantText(session) {
|
|
550
|
+
const lastAssistant = session.messages.filter((message) => message.role === "assistant").pop();
|
|
551
|
+
return (lastAssistant?.content
|
|
552
|
+
.filter((content) => content.type === "text")
|
|
553
|
+
.map((content) => content.text)
|
|
554
|
+
.join("\n") || "");
|
|
555
|
+
}
|
|
556
|
+
async function finalizeRunResponse(responseCtx, session, runState) {
|
|
557
|
+
if (runState.stopReason === "error" && runState.errorMessage) {
|
|
558
|
+
try {
|
|
559
|
+
await responseCtx.replaceResponse("_Sorry, something went wrong_");
|
|
560
|
+
await responseCtx.respondDiagnostic(`Error: ${runState.errorMessage}`, {
|
|
561
|
+
style: "error",
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
catch (err) {
|
|
565
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
566
|
+
log.logWarning("Failed to post error message", errMsg);
|
|
567
|
+
}
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
const finalText = getFinalAssistantText(session);
|
|
571
|
+
if (finalText.trim() === "[SILENT]" || finalText.trim().startsWith("[SILENT]")) {
|
|
572
|
+
try {
|
|
573
|
+
await responseCtx.deleteResponse();
|
|
574
|
+
log.logInfo("Silent response - deleted message and thread");
|
|
575
|
+
}
|
|
576
|
+
catch (err) {
|
|
577
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
578
|
+
log.logWarning("Failed to delete message for silent response", errMsg);
|
|
579
|
+
}
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
if (!finalText.trim())
|
|
583
|
+
return;
|
|
584
|
+
try {
|
|
585
|
+
await responseCtx.replaceResponse(finalText);
|
|
586
|
+
}
|
|
587
|
+
catch (err) {
|
|
588
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
589
|
+
log.logWarning("Failed to replace message with final text", errMsg);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
async function reportUsageSummary(ctx) {
|
|
593
|
+
const { session, runState, responseCtx, platform, model, agentConfig, sessionConversation, sessionUuid, waitForQueue, } = ctx;
|
|
594
|
+
if (runState.totalUsage.cost.total <= 0)
|
|
595
|
+
return;
|
|
596
|
+
const lastAssistantMessage = session.messages
|
|
597
|
+
.slice()
|
|
598
|
+
.toReversed()
|
|
599
|
+
.find((message) => message.role === "assistant" && message.stopReason !== "aborted");
|
|
600
|
+
const contextTokens = lastAssistantMessage
|
|
601
|
+
? lastAssistantMessage.usage.input +
|
|
602
|
+
lastAssistantMessage.usage.output +
|
|
603
|
+
lastAssistantMessage.usage.cacheRead +
|
|
604
|
+
lastAssistantMessage.usage.cacheWrite
|
|
605
|
+
: 0;
|
|
606
|
+
const contextWindow = model.contextWindow || 200000;
|
|
607
|
+
const { totalUsage } = runState;
|
|
608
|
+
const runMetricAttributes = metricAttributes({
|
|
609
|
+
provider: model.provider,
|
|
610
|
+
model: agentConfig.model,
|
|
611
|
+
channel_id: sessionConversation,
|
|
612
|
+
session_id: sessionUuid,
|
|
613
|
+
stop_reason: runState.stopReason,
|
|
614
|
+
llm_calls: runState.llmCallCount,
|
|
615
|
+
});
|
|
616
|
+
Sentry.metrics.distribution("agent.run.tokens_in", totalUsage.input, {
|
|
617
|
+
attributes: runMetricAttributes,
|
|
618
|
+
});
|
|
619
|
+
Sentry.metrics.distribution("agent.run.tokens_out", totalUsage.output, {
|
|
620
|
+
attributes: runMetricAttributes,
|
|
621
|
+
});
|
|
622
|
+
Sentry.metrics.distribution("agent.run.cache_read", totalUsage.cacheRead, {
|
|
623
|
+
attributes: runMetricAttributes,
|
|
624
|
+
});
|
|
625
|
+
Sentry.metrics.distribution("agent.run.cache_write", totalUsage.cacheWrite, {
|
|
626
|
+
attributes: runMetricAttributes,
|
|
627
|
+
});
|
|
628
|
+
Sentry.metrics.distribution("agent.run.cost", totalUsage.cost.total, {
|
|
629
|
+
attributes: runMetricAttributes,
|
|
630
|
+
});
|
|
631
|
+
Sentry.metrics.gauge("agent.context.utilization", contextTokens / contextWindow, {
|
|
632
|
+
unit: "ratio",
|
|
633
|
+
attributes: runMetricAttributes,
|
|
634
|
+
});
|
|
635
|
+
const summary = log.logUsageSummary(runState.logCtx, runState.totalUsage, contextTokens, contextWindow);
|
|
636
|
+
if (platform.diagnostics?.showUsageSummary === true) {
|
|
637
|
+
runState.queue.enqueue(() => responseCtx.respondDiagnostic(summary, { style: "muted" }), "usage summary");
|
|
638
|
+
await waitForQueue();
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
function reloadSessionMessages(sessionManager, conversationId, agent) {
|
|
642
|
+
const messages = sessionManager.buildSessionContext().messages;
|
|
643
|
+
if (messages.length > 0) {
|
|
644
|
+
agent.state.messages = messages;
|
|
645
|
+
log.logInfo(`[${conversationId}] Reloaded ${messages.length} messages from context`);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
async function prepareRunContext(params) {
|
|
649
|
+
const { message, responseCtx, platform, conversationId, conversationDir, sessionUuid, runState, executor, executionResolver, resolveExecutorForRun, getWorkspacePath, sessionManager, session, agent, setEventContext, setUploadFunction, } = params;
|
|
650
|
+
let workspacePath = params.workspacePath;
|
|
651
|
+
const sessionConversation = message.sessionKey.split(":")[0];
|
|
652
|
+
await mkdir(join(conversationDir, "scratch"), { recursive: true });
|
|
653
|
+
if (executionResolver) {
|
|
654
|
+
await resolveExecutorForRun({
|
|
655
|
+
platform: platform.name,
|
|
656
|
+
userId: message.userId,
|
|
657
|
+
conversationId,
|
|
658
|
+
});
|
|
659
|
+
workspacePath = getWorkspacePath();
|
|
660
|
+
}
|
|
661
|
+
reloadSessionMessages(sessionManager, conversationId, agent);
|
|
662
|
+
const memory = await getMemory(conversationDir);
|
|
663
|
+
const skills = loadMamaSkills(conversationDir, workspacePath);
|
|
664
|
+
const systemPrompt = buildSystemPrompt(workspacePath, conversationId, message.conversationKind, message.userId, memory, executor.getSandboxConfig(), platform, skills);
|
|
665
|
+
session.agent.state.systemPrompt = systemPrompt;
|
|
666
|
+
setEventContext({
|
|
667
|
+
platform: platform.name,
|
|
668
|
+
conversationId,
|
|
669
|
+
conversationKind: message.conversationKind,
|
|
670
|
+
userId: message.userId,
|
|
671
|
+
sessionKey: message.sessionKey,
|
|
672
|
+
threadTs: message.threadTs,
|
|
673
|
+
});
|
|
674
|
+
setUploadFunction(async (filePath, title) => {
|
|
675
|
+
const hostPath = translateToHostPath(filePath, conversationDir, workspacePath, conversationId);
|
|
676
|
+
await responseCtx.uploadFile(hostPath, title);
|
|
677
|
+
});
|
|
678
|
+
resetRunState(runState, responseCtx, sessionConversation, message.userName, sessionUuid);
|
|
679
|
+
const runQueue = createRunQueue(responseCtx);
|
|
680
|
+
runState.queue = runQueue.queue;
|
|
681
|
+
log.logInfo(`Context sizes - system: ${systemPrompt.length} chars, memory: ${memory.length} chars`);
|
|
682
|
+
log.logInfo(`Channels: ${platform.channels.length}, Users: ${platform.users.length}`);
|
|
683
|
+
const { userMessage, imageAttachments } = buildPromptPayload(message, workspacePath);
|
|
684
|
+
await writePromptDebugContext(conversationDir, systemPrompt, session, userMessage, imageAttachments.length);
|
|
685
|
+
return {
|
|
686
|
+
sessionConversation,
|
|
687
|
+
runQueue,
|
|
688
|
+
userMessage,
|
|
689
|
+
imageAttachments,
|
|
690
|
+
workspacePath,
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
function attachSessionEventHandlers(params) {
|
|
694
|
+
const { session, runState, model, agentConfig } = params;
|
|
496
695
|
session.subscribe(async (event) => {
|
|
497
|
-
// Skip if no active run
|
|
498
696
|
if (!runState.responseCtx || !runState.logCtx || !runState.queue)
|
|
499
697
|
return;
|
|
500
698
|
const { responseCtx, logCtx, queue, pendingTools } = runState;
|
|
501
699
|
const baseAttrs = { channel_id: logCtx.conversationId, session_id: logCtx.sessionId };
|
|
502
700
|
if (event.type === "tool_execution_start") {
|
|
503
|
-
const
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
args: agentEvent.args,
|
|
701
|
+
const args = (event.args ?? {});
|
|
702
|
+
const label = args.label || event.toolName;
|
|
703
|
+
pendingTools.set(event.toolCallId, {
|
|
704
|
+
toolName: event.toolName,
|
|
705
|
+
args: event.args,
|
|
509
706
|
startTime: Date.now(),
|
|
510
707
|
});
|
|
511
708
|
addLifecycleBreadcrumb("agent.tool.started", {
|
|
512
|
-
tool:
|
|
709
|
+
tool: event.toolName,
|
|
513
710
|
...baseAttrs,
|
|
514
711
|
});
|
|
515
|
-
log.logToolStart(logCtx,
|
|
712
|
+
log.logToolStart(logCtx, event.toolName, label, event.args);
|
|
713
|
+
return;
|
|
516
714
|
}
|
|
517
|
-
|
|
518
|
-
const
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
pendingTools.delete(agentEvent.toolCallId);
|
|
715
|
+
if (event.type === "tool_execution_end") {
|
|
716
|
+
const resultStr = extractToolResultText(event.result);
|
|
717
|
+
const pending = pendingTools.get(event.toolCallId);
|
|
718
|
+
pendingTools.delete(event.toolCallId);
|
|
522
719
|
const durationMs = pending ? Date.now() - pending.startTime : 0;
|
|
523
720
|
Sentry.metrics.count("agent.tool.calls", 1, {
|
|
524
721
|
attributes: metricAttributes({
|
|
525
|
-
tool:
|
|
526
|
-
error: String(
|
|
722
|
+
tool: event.toolName,
|
|
723
|
+
error: String(event.isError),
|
|
527
724
|
...baseAttrs,
|
|
528
725
|
}),
|
|
529
726
|
});
|
|
530
727
|
Sentry.metrics.distribution("agent.tool.duration", durationMs, {
|
|
531
728
|
unit: "millisecond",
|
|
532
729
|
attributes: metricAttributes({
|
|
533
|
-
tool:
|
|
730
|
+
tool: event.toolName,
|
|
534
731
|
...baseAttrs,
|
|
535
732
|
}),
|
|
536
733
|
});
|
|
537
734
|
addLifecycleBreadcrumb("agent.tool.completed", {
|
|
538
|
-
tool:
|
|
539
|
-
error:
|
|
735
|
+
tool: event.toolName,
|
|
736
|
+
error: event.isError,
|
|
540
737
|
duration_ms: durationMs,
|
|
541
738
|
...baseAttrs,
|
|
542
739
|
});
|
|
543
|
-
if (
|
|
544
|
-
log.logToolError(logCtx,
|
|
740
|
+
if (event.isError) {
|
|
741
|
+
log.logToolError(logCtx, event.toolName, durationMs, resultStr);
|
|
545
742
|
}
|
|
546
743
|
else {
|
|
547
|
-
log.logToolSuccess(logCtx,
|
|
744
|
+
log.logToolSuccess(logCtx, event.toolName, durationMs, resultStr);
|
|
548
745
|
}
|
|
549
|
-
if (
|
|
746
|
+
if (shouldSurfaceToolDiagnostic(event.toolName)) {
|
|
550
747
|
const toolResult = {
|
|
551
|
-
toolName:
|
|
748
|
+
toolName: event.toolName,
|
|
552
749
|
label: pending?.args ? pending.args.label : undefined,
|
|
553
750
|
args: pending?.args,
|
|
554
751
|
result: truncate(resultStr, TOOL_RESULT_DIAGNOSTIC_CAP),
|
|
555
|
-
isError:
|
|
752
|
+
isError: event.isError,
|
|
556
753
|
durationMs,
|
|
557
754
|
};
|
|
558
755
|
queue.enqueue(() => responseCtx.respondToolResult(toolResult), "tool result diagnostic");
|
|
559
756
|
}
|
|
560
|
-
if (
|
|
757
|
+
if (event.isError) {
|
|
561
758
|
queue.enqueue(() => responseCtx.respond(`_Error: ${truncate(resultStr, 200)}_`), "tool error");
|
|
562
759
|
}
|
|
760
|
+
return;
|
|
563
761
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
if (agentEvent.message.role === "assistant") {
|
|
762
|
+
if (event.type === "message_start") {
|
|
763
|
+
if (event.message.role === "assistant") {
|
|
567
764
|
runState.llmCallCount += 1;
|
|
568
765
|
addLifecycleBreadcrumb("agent.llm.call.started", {
|
|
569
766
|
call_index: runState.llmCallCount,
|
|
@@ -573,11 +770,11 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
|
|
|
573
770
|
});
|
|
574
771
|
log.logResponseStart(logCtx);
|
|
575
772
|
}
|
|
773
|
+
return;
|
|
576
774
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
const assistantMsg = agentEvent.message;
|
|
775
|
+
if (event.type === "message_end") {
|
|
776
|
+
if (event.message.role === "assistant") {
|
|
777
|
+
const assistantMsg = event.message;
|
|
581
778
|
if (assistantMsg.stopReason) {
|
|
582
779
|
runState.stopReason = assistantMsg.stopReason;
|
|
583
780
|
}
|
|
@@ -594,7 +791,6 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
|
|
|
594
791
|
runState.totalUsage.cost.cacheRead += assistantMsg.usage.cost.cacheRead;
|
|
595
792
|
runState.totalUsage.cost.cacheWrite += assistantMsg.usage.cost.cacheWrite;
|
|
596
793
|
runState.totalUsage.cost.total += assistantMsg.usage.cost.total;
|
|
597
|
-
// Per-turn LLM metrics
|
|
598
794
|
const llmAttributes = metricAttributes({
|
|
599
795
|
provider: model.provider,
|
|
600
796
|
model: agentConfig.model,
|
|
@@ -633,10 +829,9 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
|
|
|
633
829
|
cost_total_usd: assistantMsg.usage.cost.total,
|
|
634
830
|
});
|
|
635
831
|
}
|
|
636
|
-
const content = agentEvent.message.content;
|
|
637
832
|
const thinkingParts = [];
|
|
638
833
|
const textParts = [];
|
|
639
|
-
for (const part of content) {
|
|
834
|
+
for (const part of assistantMsg.content) {
|
|
640
835
|
if (part.type === "thinking") {
|
|
641
836
|
thinkingParts.push(part.thinking);
|
|
642
837
|
}
|
|
@@ -655,270 +850,160 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
|
|
|
655
850
|
queue.enqueue(() => responseCtx.respond(text), "response main");
|
|
656
851
|
}
|
|
657
852
|
}
|
|
853
|
+
return;
|
|
658
854
|
}
|
|
659
|
-
|
|
855
|
+
if (event.type === "compaction_start") {
|
|
660
856
|
log.logInfo(`Auto-compaction started (reason: ${event.reason})`);
|
|
661
857
|
queue.enqueue(() => responseCtx.respond("_Compacting context..._"), "compaction start");
|
|
858
|
+
return;
|
|
662
859
|
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
log.logInfo(`Auto-compaction complete: ${compEvent.result.tokensBefore} tokens compacted`);
|
|
860
|
+
if (event.type === "compaction_end") {
|
|
861
|
+
if (event.result) {
|
|
862
|
+
log.logInfo(`Auto-compaction complete: ${event.result.tokensBefore} tokens compacted`);
|
|
667
863
|
}
|
|
668
|
-
else if (
|
|
864
|
+
else if (event.aborted) {
|
|
669
865
|
log.logInfo("Auto-compaction aborted");
|
|
670
866
|
}
|
|
867
|
+
return;
|
|
671
868
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
queue.enqueue(() => responseCtx.respond(`_Retrying (${retryEvent.attempt}/${retryEvent.maxAttempts})..._`), "retry");
|
|
869
|
+
if (event.type === "auto_retry_start") {
|
|
870
|
+
log.logWarning(`Retrying (${event.attempt}/${event.maxAttempts})`, event.errorMessage);
|
|
871
|
+
queue.enqueue(() => responseCtx.respond(`_Retrying (${event.attempt}/${event.maxAttempts})..._`), "retry");
|
|
676
872
|
}
|
|
677
873
|
});
|
|
874
|
+
}
|
|
875
|
+
// Cap raw tool output before handing it to adapters. Bash output can be MB; without
|
|
876
|
+
// this each adapter's splitter would fan it out into many sequential platform posts.
|
|
877
|
+
const TOOL_RESULT_DIAGNOSTIC_CAP = 8000;
|
|
878
|
+
function extractToolResultText(result) {
|
|
879
|
+
if (typeof result === "string") {
|
|
880
|
+
return result;
|
|
881
|
+
}
|
|
882
|
+
if (result &&
|
|
883
|
+
typeof result === "object" &&
|
|
884
|
+
"content" in result &&
|
|
885
|
+
Array.isArray(result.content)) {
|
|
886
|
+
const content = result.content;
|
|
887
|
+
const textParts = [];
|
|
888
|
+
for (const part of content) {
|
|
889
|
+
if (part.type === "text" && part.text) {
|
|
890
|
+
textParts.push(part.text);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
if (textParts.length > 0) {
|
|
894
|
+
return textParts.join("\n");
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
return JSON.stringify(result);
|
|
898
|
+
}
|
|
899
|
+
// ============================================================================
|
|
900
|
+
// Agent runner
|
|
901
|
+
// ============================================================================
|
|
902
|
+
/**
|
|
903
|
+
* Create a new AgentRunner for a channel.
|
|
904
|
+
* Sets up the session and subscribes to events once.
|
|
905
|
+
*
|
|
906
|
+
* Runner caching is handled by the caller (channelStates in main.ts).
|
|
907
|
+
* This is a stateless factory function.
|
|
908
|
+
*/
|
|
909
|
+
export async function createRunner(sandboxConfig, sessionKey, conversationId, conversationDir, workspaceDir, sessionScope, vaultManager, provisioner) {
|
|
910
|
+
const agentConfig = loadAgentConfigForConversation(conversationDir);
|
|
911
|
+
// Initialize logger with settings from config
|
|
912
|
+
log.initLogger({
|
|
913
|
+
logFormat: agentConfig.logFormat,
|
|
914
|
+
logLevel: agentConfig.logLevel,
|
|
915
|
+
});
|
|
916
|
+
const workspaceBase = join(conversationDir, "..");
|
|
917
|
+
const { executionResolver, executor, getWorkspacePath, resolveExecutorForRun } = createRunnerExecutionContext(sandboxConfig, vaultManager, provisioner, workspaceDir, workspaceBase);
|
|
918
|
+
let workspacePath = initialWorkspacePath(sandboxConfig, workspaceBase);
|
|
919
|
+
// Create tools (per-runner, with per-runner upload function setter)
|
|
920
|
+
const { tools, setUploadFunction, setEventContext } = createMamaTools(executor, workspaceDir);
|
|
921
|
+
// Resolve model from config
|
|
922
|
+
// Use 'as any' cast because agentConfig.provider/model are plain strings,
|
|
923
|
+
// while getModel() has constrained generic types for known providers.
|
|
924
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
925
|
+
const model = getModel(agentConfig.provider, agentConfig.model);
|
|
926
|
+
// Initial system prompt (will be updated each run with fresh memory/channels/users/skills)
|
|
927
|
+
const memory = await getMemory(conversationDir);
|
|
928
|
+
const skills = loadMamaSkills(conversationDir, workspacePath);
|
|
929
|
+
const emptyPlatform = {
|
|
930
|
+
name: "chat",
|
|
931
|
+
formattingGuide: "",
|
|
932
|
+
channels: [],
|
|
933
|
+
users: [],
|
|
934
|
+
};
|
|
935
|
+
const systemPrompt = buildSystemPrompt(workspacePath, conversationId, "shared", undefined, memory, sandboxConfig, emptyPlatform, skills);
|
|
936
|
+
// Create session manager and settings manager. Top-level/private sessions
|
|
937
|
+
// use the conversation's current pointer; scoped sessions use fixed files.
|
|
938
|
+
// Platform-specific branch/fork behavior is resolved before runner creation.
|
|
939
|
+
const isThread = sessionKey.includes(":");
|
|
940
|
+
const { sessionDir, contextFile, threadRootMessage } = sessionScope;
|
|
941
|
+
const sessionManager = openManagedSession(contextFile, sessionDir, workspacePath);
|
|
942
|
+
const threadSessionName = buildThreadSessionName(threadRootMessage);
|
|
943
|
+
if (isThread && threadSessionName && sessionManager.getSessionName() !== threadSessionName) {
|
|
944
|
+
sessionManager.appendSessionInfo(threadSessionName);
|
|
945
|
+
}
|
|
946
|
+
const sessionUuid = extractSessionUuid(contextFile);
|
|
947
|
+
const settingsManager = SettingsManager.inMemory();
|
|
948
|
+
const { agent, session } = await createConfiguredAgentSession({
|
|
949
|
+
conversationId,
|
|
950
|
+
workspaceDir,
|
|
951
|
+
workspacePath,
|
|
952
|
+
systemPrompt,
|
|
953
|
+
model,
|
|
954
|
+
thinkingLevel: agentConfig.thinkingLevel,
|
|
955
|
+
tools,
|
|
956
|
+
sessionManager,
|
|
957
|
+
settingsManager,
|
|
958
|
+
});
|
|
959
|
+
// Mutable per-run state - event handler references this
|
|
960
|
+
const runState = createRunState();
|
|
961
|
+
attachSessionEventHandlers({ session, runState, model, agentConfig });
|
|
678
962
|
return {
|
|
679
963
|
async run(message, responseCtx, platform) {
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
if (executionResolver) {
|
|
685
|
-
executionResolver.refresh();
|
|
686
|
-
activeExecutor = await executionResolver.resolve({
|
|
687
|
-
platform: platform.name,
|
|
688
|
-
userId: message.userId,
|
|
689
|
-
conversationId,
|
|
690
|
-
});
|
|
691
|
-
workspacePath = getWorkspacePath();
|
|
692
|
-
}
|
|
693
|
-
// Sync messages from log.jsonl that arrived while we were offline or busy
|
|
694
|
-
// Exclude the current message (it will be added via prompt())
|
|
695
|
-
// Default sync range is 10 days (handled by syncLogToSessionManager)
|
|
696
|
-
// Thread filter ensures only messages from this session's thread are synced
|
|
697
|
-
const threadFilter = message.sessionKey.includes(":")
|
|
698
|
-
? { scope: "thread", rootTs, threadTs: message.threadTs }
|
|
699
|
-
: { scope: "top-level", rootTs };
|
|
700
|
-
const syncedCount = await syncLogToSessionManager(sessionManager, conversationDir, message.id, undefined, threadFilter);
|
|
701
|
-
if (syncedCount > 0) {
|
|
702
|
-
log.logInfo(`[${conversationId}] Synced ${syncedCount} messages from log.jsonl`);
|
|
703
|
-
}
|
|
704
|
-
// Reload messages from context.jsonl
|
|
705
|
-
// This picks up any messages synced above
|
|
706
|
-
const reloadedSession = sessionManager.buildSessionContext();
|
|
707
|
-
if (reloadedSession.messages.length > 0) {
|
|
708
|
-
agent.state.messages = reloadedSession.messages;
|
|
709
|
-
log.logInfo(`[${conversationId}] Reloaded ${reloadedSession.messages.length} messages from context`);
|
|
710
|
-
}
|
|
711
|
-
// Update system prompt with fresh memory, channel/user info, and skills
|
|
712
|
-
const memory = await getMemory(conversationDir);
|
|
713
|
-
const skills = loadMamaSkills(conversationDir, workspacePath);
|
|
714
|
-
const systemPrompt = buildSystemPrompt(workspacePath, conversationId, message.conversationKind, message.userId, memory, executor.getSandboxConfig(), platform, skills);
|
|
715
|
-
session.agent.state.systemPrompt = systemPrompt;
|
|
716
|
-
setEventContext({
|
|
717
|
-
platform: platform.name,
|
|
964
|
+
const prepared = await prepareRunContext({
|
|
965
|
+
message,
|
|
966
|
+
responseCtx,
|
|
967
|
+
platform,
|
|
718
968
|
conversationId,
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
969
|
+
conversationDir,
|
|
970
|
+
sessionUuid,
|
|
971
|
+
runState,
|
|
972
|
+
executor,
|
|
973
|
+
executionResolver,
|
|
974
|
+
resolveExecutorForRun,
|
|
975
|
+
getWorkspacePath,
|
|
976
|
+
sessionManager,
|
|
977
|
+
session,
|
|
978
|
+
agent,
|
|
979
|
+
setEventContext,
|
|
980
|
+
setUploadFunction,
|
|
981
|
+
workspacePath,
|
|
731
982
|
});
|
|
732
|
-
|
|
733
|
-
runState.responseCtx = responseCtx;
|
|
734
|
-
runState.logCtx = {
|
|
735
|
-
conversationId: sessionConversation,
|
|
736
|
-
userName: message.userName,
|
|
737
|
-
conversationName: undefined,
|
|
738
|
-
sessionId: sessionUuid,
|
|
739
|
-
};
|
|
740
|
-
runState.pendingTools.clear();
|
|
741
|
-
runState.totalUsage = {
|
|
742
|
-
input: 0,
|
|
743
|
-
output: 0,
|
|
744
|
-
cacheRead: 0,
|
|
745
|
-
cacheWrite: 0,
|
|
746
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
747
|
-
};
|
|
748
|
-
runState.llmCallCount = 0;
|
|
749
|
-
runState.stopReason = "stop";
|
|
750
|
-
runState.errorMessage = undefined;
|
|
751
|
-
// Create queue for this run
|
|
752
|
-
let queueChain = Promise.resolve();
|
|
753
|
-
runState.queue = {
|
|
754
|
-
enqueue(fn, errorContext) {
|
|
755
|
-
queueChain = queueChain.then(async () => {
|
|
756
|
-
try {
|
|
757
|
-
await fn();
|
|
758
|
-
}
|
|
759
|
-
catch (err) {
|
|
760
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
761
|
-
log.logWarning(`API error (${errorContext})`, errMsg);
|
|
762
|
-
try {
|
|
763
|
-
await responseCtx.respondDiagnostic(`Error: ${errMsg}`, { style: "error" });
|
|
764
|
-
}
|
|
765
|
-
catch {
|
|
766
|
-
// Ignore
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
});
|
|
770
|
-
},
|
|
771
|
-
};
|
|
772
|
-
// Log context info
|
|
773
|
-
log.logInfo(`Context sizes - system: ${systemPrompt.length} chars, memory: ${memory.length} chars`);
|
|
774
|
-
log.logInfo(`Channels: ${platform.channels.length}, Users: ${platform.users.length}`);
|
|
775
|
-
// Build user message with timestamp and username prefix
|
|
776
|
-
// Format: "[YYYY-MM-DD HH:MM:SS+HH:MM] [username]: message" so LLM knows when and who
|
|
777
|
-
const now = new Date();
|
|
778
|
-
const pad = (n) => n.toString().padStart(2, "0");
|
|
779
|
-
const offset = -now.getTimezoneOffset();
|
|
780
|
-
const offsetSign = offset >= 0 ? "+" : "-";
|
|
781
|
-
const offsetHours = pad(Math.floor(Math.abs(offset) / 60));
|
|
782
|
-
const offsetMins = pad(Math.abs(offset) % 60);
|
|
783
|
-
const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}${offsetSign}${offsetHours}:${offsetMins}`;
|
|
784
|
-
const threadContext = message.threadTs ? ` [in-thread:${message.threadTs}]` : "";
|
|
785
|
-
let userMessage = `[${timestamp}] [${message.userName || "unknown"}]${threadContext}: ${message.text}`;
|
|
786
|
-
const imageAttachments = [];
|
|
787
|
-
const nonImagePaths = [];
|
|
788
|
-
for (const a of message.attachments || []) {
|
|
789
|
-
// a.localPath is the path relative to the workspace.
|
|
790
|
-
const fullPath = `${workspacePath}/${a.localPath}`;
|
|
791
|
-
const mimeType = getImageMimeType(a.localPath);
|
|
792
|
-
if (mimeType && existsSync(fullPath)) {
|
|
793
|
-
try {
|
|
794
|
-
imageAttachments.push({
|
|
795
|
-
type: "image",
|
|
796
|
-
mimeType,
|
|
797
|
-
data: readFileSync(fullPath).toString("base64"),
|
|
798
|
-
});
|
|
799
|
-
}
|
|
800
|
-
catch {
|
|
801
|
-
nonImagePaths.push(fullPath);
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
else {
|
|
805
|
-
nonImagePaths.push(fullPath);
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
if (nonImagePaths.length > 0) {
|
|
809
|
-
userMessage += `\n\n<slack_attachments>\n${nonImagePaths.join("\n")}\n</slack_attachments>`;
|
|
810
|
-
}
|
|
811
|
-
// Debug: write context to last_prompt.jsonl
|
|
812
|
-
const debugContext = {
|
|
813
|
-
systemPrompt,
|
|
814
|
-
messages: session.messages,
|
|
815
|
-
newUserMessage: userMessage,
|
|
816
|
-
imageAttachmentCount: imageAttachments.length,
|
|
817
|
-
};
|
|
818
|
-
await writeFile(join(conversationDir, "last_prompt.jsonl"), JSON.stringify(debugContext, null, 2));
|
|
983
|
+
workspacePath = prepared.workspacePath;
|
|
819
984
|
addLifecycleBreadcrumb("agent.prompt.sent", {
|
|
820
985
|
provider: model.provider,
|
|
821
986
|
model: agentConfig.model,
|
|
822
|
-
channel_id: sessionConversation,
|
|
987
|
+
channel_id: prepared.sessionConversation,
|
|
823
988
|
session_id: sessionUuid,
|
|
824
989
|
attachment_count: message.attachments?.length ?? 0,
|
|
825
|
-
image_attachment_count: imageAttachments.length,
|
|
990
|
+
image_attachment_count: prepared.imageAttachments.length,
|
|
826
991
|
});
|
|
827
|
-
await session.prompt(userMessage, imageAttachments.length > 0 ? { images: imageAttachments } : undefined);
|
|
992
|
+
await session.prompt(prepared.userMessage, prepared.imageAttachments.length > 0 ? { images: prepared.imageAttachments } : undefined);
|
|
828
993
|
// Wait for queued messages
|
|
829
|
-
await
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
}
|
|
843
|
-
else {
|
|
844
|
-
// Final message update
|
|
845
|
-
const messages = session.messages;
|
|
846
|
-
const lastAssistant = messages.filter((m) => m.role === "assistant").pop();
|
|
847
|
-
const finalText = lastAssistant?.content
|
|
848
|
-
.filter((c) => c.type === "text")
|
|
849
|
-
.map((c) => c.text)
|
|
850
|
-
.join("\n") || "";
|
|
851
|
-
// Check for [SILENT] marker - delete message and thread instead of posting
|
|
852
|
-
if (finalText.trim() === "[SILENT]" || finalText.trim().startsWith("[SILENT]")) {
|
|
853
|
-
try {
|
|
854
|
-
await responseCtx.deleteResponse();
|
|
855
|
-
log.logInfo("Silent response - deleted message and thread");
|
|
856
|
-
}
|
|
857
|
-
catch (err) {
|
|
858
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
859
|
-
log.logWarning("Failed to delete message for silent response", errMsg);
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
else if (finalText.trim()) {
|
|
863
|
-
try {
|
|
864
|
-
await responseCtx.replaceResponse(finalText);
|
|
865
|
-
}
|
|
866
|
-
catch (err) {
|
|
867
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
868
|
-
log.logWarning("Failed to replace message with final text", errMsg);
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
// Log usage summary with context info
|
|
873
|
-
if (runState.totalUsage.cost.total > 0) {
|
|
874
|
-
// Get last non-aborted assistant message for context calculation
|
|
875
|
-
const messages = session.messages;
|
|
876
|
-
const lastAssistantMessage = messages
|
|
877
|
-
.slice()
|
|
878
|
-
.reverse()
|
|
879
|
-
.find((m) => m.role === "assistant" && m.stopReason !== "aborted");
|
|
880
|
-
const contextTokens = lastAssistantMessage
|
|
881
|
-
? lastAssistantMessage.usage.input +
|
|
882
|
-
lastAssistantMessage.usage.output +
|
|
883
|
-
lastAssistantMessage.usage.cacheRead +
|
|
884
|
-
lastAssistantMessage.usage.cacheWrite
|
|
885
|
-
: 0;
|
|
886
|
-
const contextWindow = model.contextWindow || 200000;
|
|
887
|
-
// Run-level Sentry metrics
|
|
888
|
-
const { totalUsage } = runState;
|
|
889
|
-
const runMetricAttributes = metricAttributes({
|
|
890
|
-
provider: model.provider,
|
|
891
|
-
model: agentConfig.model,
|
|
892
|
-
channel_id: sessionConversation,
|
|
893
|
-
session_id: sessionUuid,
|
|
894
|
-
stop_reason: runState.stopReason,
|
|
895
|
-
llm_calls: runState.llmCallCount,
|
|
896
|
-
});
|
|
897
|
-
Sentry.metrics.distribution("agent.run.tokens_in", totalUsage.input, {
|
|
898
|
-
attributes: runMetricAttributes,
|
|
899
|
-
});
|
|
900
|
-
Sentry.metrics.distribution("agent.run.tokens_out", totalUsage.output, {
|
|
901
|
-
attributes: runMetricAttributes,
|
|
902
|
-
});
|
|
903
|
-
Sentry.metrics.distribution("agent.run.cache_read", totalUsage.cacheRead, {
|
|
904
|
-
attributes: runMetricAttributes,
|
|
905
|
-
});
|
|
906
|
-
Sentry.metrics.distribution("agent.run.cache_write", totalUsage.cacheWrite, {
|
|
907
|
-
attributes: runMetricAttributes,
|
|
908
|
-
});
|
|
909
|
-
Sentry.metrics.distribution("agent.run.cost", totalUsage.cost.total, {
|
|
910
|
-
attributes: runMetricAttributes,
|
|
911
|
-
});
|
|
912
|
-
Sentry.metrics.gauge("agent.context.utilization", contextTokens / contextWindow, {
|
|
913
|
-
unit: "ratio",
|
|
914
|
-
attributes: runMetricAttributes,
|
|
915
|
-
});
|
|
916
|
-
const summary = log.logUsageSummary(runState.logCtx, runState.totalUsage, contextTokens, contextWindow);
|
|
917
|
-
if (platform.diagnostics?.showUsageSummary === true) {
|
|
918
|
-
runState.queue.enqueue(() => responseCtx.respondDiagnostic(summary, { style: "muted" }), "usage summary");
|
|
919
|
-
await queueChain;
|
|
920
|
-
}
|
|
921
|
-
}
|
|
994
|
+
await prepared.runQueue.wait();
|
|
995
|
+
await finalizeRunResponse(responseCtx, session, runState);
|
|
996
|
+
await reportUsageSummary({
|
|
997
|
+
session,
|
|
998
|
+
runState,
|
|
999
|
+
responseCtx,
|
|
1000
|
+
platform,
|
|
1001
|
+
model,
|
|
1002
|
+
agentConfig,
|
|
1003
|
+
sessionConversation: prepared.sessionConversation,
|
|
1004
|
+
sessionUuid,
|
|
1005
|
+
waitForQueue: () => prepared.runQueue.wait(),
|
|
1006
|
+
});
|
|
922
1007
|
// Clear run state
|
|
923
1008
|
runState.responseCtx = null;
|
|
924
1009
|
runState.logCtx = null;
|