@geminixiang/mama 0.2.0-beta.2 → 0.2.0-beta.20
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 +156 -392
- package/dist/adapter.d.ts +31 -7
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js.map +1 -1
- package/dist/adapters/discord/bot.d.ts +10 -5
- package/dist/adapters/discord/bot.d.ts.map +1 -1
- package/dist/adapters/discord/bot.js +347 -115
- 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 +118 -25
- package/dist/adapters/discord/context.js.map +1 -1
- package/dist/adapters/shared.d.ts +91 -0
- package/dist/adapters/shared.d.ts.map +1 -0
- package/dist/adapters/shared.js +191 -0
- package/dist/adapters/shared.js.map +1 -0
- package/dist/adapters/slack/bot.d.ts +21 -22
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +530 -221
- package/dist/adapters/slack/bot.js.map +1 -1
- package/dist/adapters/slack/branch-manager.d.ts +28 -0
- package/dist/adapters/slack/branch-manager.d.ts.map +1 -0
- package/dist/adapters/slack/branch-manager.js +107 -0
- package/dist/adapters/slack/branch-manager.js.map +1 -0
- package/dist/adapters/slack/context.d.ts +4 -1
- package/dist/adapters/slack/context.d.ts.map +1 -1
- package/dist/adapters/slack/context.js +193 -75
- package/dist/adapters/slack/context.js.map +1 -1
- package/dist/adapters/slack/session.d.ts +38 -0
- package/dist/adapters/slack/session.d.ts.map +1 -0
- package/dist/adapters/slack/session.js +66 -0
- package/dist/adapters/slack/session.js.map +1 -0
- 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 +140 -153
- package/dist/adapters/telegram/bot.js.map +1 -1
- package/dist/adapters/telegram/context.d.ts +1 -1
- package/dist/adapters/telegram/context.d.ts.map +1 -1
- package/dist/adapters/telegram/context.js +74 -20
- package/dist/adapters/telegram/context.js.map +1 -1
- package/dist/agent.d.ts +13 -3
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +677 -552
- package/dist/agent.js.map +1 -1
- package/dist/commands/auto-reply.d.ts +16 -0
- package/dist/commands/auto-reply.d.ts.map +1 -0
- package/dist/commands/auto-reply.js +72 -0
- package/dist/commands/auto-reply.js.map +1 -0
- package/dist/commands/index.d.ts +5 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +18 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/login.d.ts +5 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +91 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/model.d.ts +14 -0
- package/dist/commands/model.d.ts.map +1 -0
- package/dist/commands/model.js +112 -0
- package/dist/commands/model.js.map +1 -0
- package/dist/commands/new.d.ts +9 -0
- package/dist/commands/new.d.ts.map +1 -0
- package/dist/commands/new.js +28 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/registry.d.ts +4 -0
- package/dist/commands/registry.d.ts.map +1 -0
- package/dist/commands/registry.js +9 -0
- package/dist/commands/registry.js.map +1 -0
- 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 +5 -0
- package/dist/commands/session-view.d.ts.map +1 -0
- package/dist/commands/session-view.js +62 -0
- package/dist/commands/session-view.js.map +1 -0
- package/dist/commands/types.d.ts +41 -0
- package/dist/commands/types.d.ts.map +1 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/types.js.map +1 -0
- package/dist/commands/utils.d.ts +8 -0
- package/dist/commands/utils.d.ts.map +1 -0
- package/dist/commands/utils.js +14 -0
- package/dist/commands/utils.js.map +1 -0
- package/dist/config.d.ts +45 -8
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +299 -67
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +10 -42
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +14 -127
- package/dist/context.js.map +1 -1
- package/dist/events.d.ts +2 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +148 -67
- package/dist/events.js.map +1 -1
- package/dist/execution-resolver.d.ts +10 -6
- package/dist/execution-resolver.d.ts.map +1 -1
- package/dist/execution-resolver.js +121 -21
- package/dist/execution-resolver.js.map +1 -1
- package/dist/file-guards.d.ts +9 -0
- package/dist/file-guards.d.ts.map +1 -0
- package/dist/file-guards.js +56 -0
- package/dist/file-guards.js.map +1 -0
- package/dist/fs-atomic.d.ts +10 -0
- package/dist/fs-atomic.d.ts.map +1 -0
- package/dist/fs-atomic.js +45 -0
- package/dist/fs-atomic.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/instrument.d.ts.map +1 -1
- package/dist/instrument.js +2 -3
- package/dist/instrument.js.map +1 -1
- package/dist/log.d.ts +1 -12
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +12 -143
- package/dist/log.js.map +1 -1
- package/dist/{login.d.ts → login/index.d.ts} +16 -3
- package/dist/login/index.d.ts.map +1 -0
- package/dist/{login.js → login/index.js} +94 -17
- package/dist/login/index.js.map +1 -0
- package/dist/{link-server.d.ts → login/portal.d.ts} +6 -4
- package/dist/login/portal.d.ts.map +1 -0
- package/dist/login/portal.js +1544 -0
- package/dist/login/portal.js.map +1 -0
- package/dist/login/session.d.ts +26 -0
- package/dist/login/session.d.ts.map +1 -0
- package/dist/{link-token.js → login/session.js} +10 -22
- package/dist/login/session.js.map +1 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +138 -352
- package/dist/main.js.map +1 -1
- package/dist/provisioner.d.ts +42 -11
- package/dist/provisioner.d.ts.map +1 -1
- package/dist/provisioner.js +273 -64
- package/dist/provisioner.js.map +1 -1
- package/dist/runtime/conversation-orchestrator.d.ts +40 -0
- package/dist/runtime/conversation-orchestrator.d.ts.map +1 -0
- package/dist/runtime/conversation-orchestrator.js +183 -0
- package/dist/runtime/conversation-orchestrator.js.map +1 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/session-runtime.d.ts +26 -0
- package/dist/runtime/session-runtime.d.ts.map +1 -0
- package/dist/runtime/session-runtime.js +221 -0
- package/dist/runtime/session-runtime.js.map +1 -0
- package/dist/sandbox/cloudflare.d.ts +15 -0
- package/dist/sandbox/cloudflare.d.ts.map +1 -0
- package/dist/sandbox/cloudflare.js +137 -0
- package/dist/sandbox/cloudflare.js.map +1 -0
- package/dist/sandbox/container.d.ts +2 -1
- package/dist/sandbox/container.d.ts.map +1 -1
- package/dist/sandbox/container.js +18 -2
- package/dist/sandbox/container.js.map +1 -1
- package/dist/sandbox/firecracker.d.ts +2 -1
- package/dist/sandbox/firecracker.d.ts.map +1 -1
- package/dist/sandbox/firecracker.js +6 -0
- package/dist/sandbox/firecracker.js.map +1 -1
- package/dist/sandbox/host.d.ts +2 -1
- package/dist/sandbox/host.d.ts.map +1 -1
- package/dist/sandbox/host.js +4 -0
- package/dist/sandbox/host.js.map +1 -1
- package/dist/sandbox/index.d.ts +6 -4
- package/dist/sandbox/index.d.ts.map +1 -1
- package/dist/sandbox/index.js +9 -6
- package/dist/sandbox/index.js.map +1 -1
- package/dist/sandbox/path-context.d.ts +4 -0
- package/dist/sandbox/path-context.d.ts.map +1 -0
- package/dist/sandbox/path-context.js +20 -0
- package/dist/sandbox/path-context.js.map +1 -0
- package/dist/sandbox/types.d.ts +17 -1
- package/dist/sandbox/types.d.ts.map +1 -1
- package/dist/sandbox/types.js.map +1 -1
- package/dist/sentry.d.ts +20 -1
- package/dist/sentry.d.ts.map +1 -1
- package/dist/sentry.js +58 -8
- package/dist/sentry.js.map +1 -1
- package/dist/session-policy.d.ts +13 -0
- package/dist/session-policy.d.ts.map +1 -0
- package/dist/session-policy.js +23 -0
- package/dist/session-policy.js.map +1 -0
- package/dist/session-store.d.ts +33 -2
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +179 -13
- package/dist/session-store.js.map +1 -1
- package/dist/session-view/command.d.ts +5 -0
- package/dist/session-view/command.d.ts.map +1 -0
- package/dist/session-view/command.js +11 -0
- package/dist/session-view/command.js.map +1 -0
- package/dist/session-view/portal.d.ts +16 -0
- package/dist/session-view/portal.d.ts.map +1 -0
- package/dist/session-view/portal.js +1822 -0
- package/dist/session-view/portal.js.map +1 -0
- package/dist/session-view/service.d.ts +34 -0
- package/dist/session-view/service.d.ts.map +1 -0
- package/dist/session-view/service.js +427 -0
- package/dist/session-view/service.js.map +1 -0
- package/dist/session-view/store.d.ts +18 -0
- package/dist/session-view/store.d.ts.map +1 -0
- package/dist/session-view/store.js +36 -0
- package/dist/session-view/store.js.map +1 -0
- package/dist/store.d.ts +3 -6
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +22 -48
- 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 +2 -2
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/edit.d.ts +2 -2
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/event.d.ts +42 -2
- package/dist/tools/event.d.ts.map +1 -1
- package/dist/tools/event.js +43 -9
- package/dist/tools/event.js.map +1 -1
- package/dist/tools/index.d.ts +2 -2
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +2 -2
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/read.d.ts +2 -2
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/write.d.ts +2 -2
- package/dist/tools/write.d.ts.map +1 -1
- package/dist/tools/write.js.map +1 -1
- package/dist/trigger.d.ts +31 -0
- package/dist/trigger.d.ts.map +1 -0
- package/dist/trigger.js +98 -0
- package/dist/trigger.js.map +1 -0
- package/dist/vault-routing.d.ts +2 -7
- package/dist/vault-routing.d.ts.map +1 -1
- package/dist/vault-routing.js +6 -42
- package/dist/vault-routing.js.map +1 -1
- package/dist/vault.d.ts +22 -56
- package/dist/vault.d.ts.map +1 -1
- package/dist/vault.js +155 -263
- package/dist/vault.js.map +1 -1
- package/package.json +11 -11
- package/dist/bindings.d.ts +0 -44
- package/dist/bindings.d.ts.map +0 -1
- package/dist/bindings.js +0 -74
- package/dist/bindings.js.map +0 -1
- package/dist/link-server.d.ts.map +0 -1
- package/dist/link-server.js +0 -899
- package/dist/link-server.js.map +0 -1
- package/dist/link-token.d.ts +0 -32
- package/dist/link-token.d.ts.map +0 -1
- package/dist/link-token.js.map +0 -1
- package/dist/login.d.ts.map +0 -1
- package/dist/login.js.map +0 -1
- package/dist/sandbox.d.ts +0 -2
- package/dist/sandbox.d.ts.map +0 -1
- package/dist/sandbox.js +0 -2
- package/dist/sandbox.js.map +0 -1
package/dist/agent.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
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";
|
|
8
|
-
import {
|
|
9
|
-
import { createMamaSettingsManager, syncLogToSessionManager } from "./context.js";
|
|
7
|
+
import { join, posix } from "path";
|
|
8
|
+
import { loadAgentConfigForConversation } from "./config.js";
|
|
10
9
|
import { ActorExecutionResolver } from "./execution-resolver.js";
|
|
11
10
|
import * as log from "./log.js";
|
|
12
|
-
import {
|
|
11
|
+
import { reportUserFacingError } from "./sentry.js";
|
|
12
|
+
import { createExecutor, } from "./sandbox/index.js";
|
|
13
|
+
import { createMountedRuntimePathContext } from "./sandbox/path-context.js";
|
|
13
14
|
import { addLifecycleBreadcrumb, metricAttributes } from "./sentry.js";
|
|
14
|
-
import {
|
|
15
|
+
import { extractSessionUuid, openManagedSession, } from "./session-store.js";
|
|
16
|
+
import { shouldSurfaceToolDiagnostic } from "./tool-diagnostics.js";
|
|
15
17
|
import { createMamaTools } from "./tools/index.js";
|
|
16
18
|
import * as Sentry from "@sentry/node";
|
|
17
19
|
const IMAGE_MIME_TYPES = {
|
|
@@ -24,6 +26,13 @@ const IMAGE_MIME_TYPES = {
|
|
|
24
26
|
function getImageMimeType(filename) {
|
|
25
27
|
return IMAGE_MIME_TYPES[filename.toLowerCase().split(".").pop() || ""];
|
|
26
28
|
}
|
|
29
|
+
function buildThreadSessionName(message) {
|
|
30
|
+
const text = message?.text?.trim();
|
|
31
|
+
if (!text)
|
|
32
|
+
return undefined;
|
|
33
|
+
const userLabel = message?.userName || message?.user || "unknown";
|
|
34
|
+
return `[${userLabel}]: ${text}`;
|
|
35
|
+
}
|
|
27
36
|
async function getMemory(conversationDir) {
|
|
28
37
|
const parts = [];
|
|
29
38
|
// Read workspace-level memory (shared across all conversations)
|
|
@@ -87,11 +96,89 @@ function loadMamaSkills(conversationDir, workspacePath) {
|
|
|
87
96
|
}
|
|
88
97
|
return Array.from(skillMap.values());
|
|
89
98
|
}
|
|
90
|
-
function
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
99
|
+
function buildRuntimePaths(runtimeWorkspaceRoot, conversationId) {
|
|
100
|
+
const workspaceRoot = runtimeWorkspaceRoot.replace(/\/+$/, "") || "/";
|
|
101
|
+
const conversationPath = posix.join(workspaceRoot, conversationId);
|
|
102
|
+
return {
|
|
103
|
+
workspaceRoot,
|
|
104
|
+
conversationPath,
|
|
105
|
+
scratchPath: posix.join(conversationPath, "scratch"),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function buildEnvDescription(sandboxType, workspaceRoot) {
|
|
109
|
+
switch (sandboxType) {
|
|
110
|
+
case "image":
|
|
111
|
+
return `You are running inside a managed per-user container.
|
|
112
|
+
- Runtime workspace root: ${workspaceRoot}
|
|
113
|
+
- Bash commands start in: ${workspaceRoot}
|
|
114
|
+
- Install tools with the image's package manager
|
|
115
|
+
- Your changes persist for this user's container until it is recreated`;
|
|
116
|
+
case "container":
|
|
117
|
+
return `You are running inside a shared container.
|
|
118
|
+
- Runtime workspace root: ${workspaceRoot}
|
|
119
|
+
- Bash commands start in: ${workspaceRoot}
|
|
120
|
+
- Install tools with the container's package manager
|
|
121
|
+
- Your changes persist across sessions`;
|
|
122
|
+
case "firecracker":
|
|
123
|
+
return `You are running inside a Firecracker microVM.
|
|
124
|
+
- Runtime workspace root: ${workspaceRoot}
|
|
125
|
+
- Use cd or absolute paths; project files are under ${workspaceRoot}
|
|
126
|
+
- Install tools with: apt-get install <package> (Debian-based)
|
|
127
|
+
- Your changes persist across sessions`;
|
|
128
|
+
case "cloudflare":
|
|
129
|
+
return `You are running through a Cloudflare Sandbox bridge.
|
|
130
|
+
- Runtime workspace root: ${workspaceRoot}
|
|
131
|
+
- Bash commands start in: ${workspaceRoot}
|
|
132
|
+
- Your commands run in a remote container managed by Cloudflare
|
|
133
|
+
- Important: the remote filesystem is not automatically synced back to the host workspace`;
|
|
134
|
+
default:
|
|
135
|
+
return `You are running directly on the host machine.
|
|
136
|
+
- Runtime workspace root: ${workspaceRoot}
|
|
137
|
+
- Bash commands start in: ${process.cwd()}
|
|
138
|
+
- Be careful with system modifications`;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export function buildEventFilesystemInstructions(sandboxType, workspaceRoot) {
|
|
142
|
+
if (sandboxType === "host" || sandboxType === "container" || sandboxType === "image") {
|
|
143
|
+
return `Events live in the host-side mama control plane and are mounted at \`${workspaceRoot}/events/\` in this runtime.
|
|
144
|
+
|
|
145
|
+
Prefer the \`event\` tool over manually writing JSON files; it fills \`platform\`, \`conversationId\`, \`conversationKind\`, and \`userId\` for the current conversation automatically.
|
|
146
|
+
|
|
147
|
+
### Creating Events Manually
|
|
148
|
+
Only do this when you need to create events from a script. Use unique filenames to avoid overwriting existing events. Include a timestamp or random suffix:
|
|
149
|
+
\`\`\`bash
|
|
150
|
+
cat > ${workspaceRoot}/events/dentist-reminder-$(date +%s).json << 'EOF'
|
|
151
|
+
{"type": "one-shot", "platform": "<platform>", "conversationId": "<conversationId>", "conversationKind": "<direct|shared>", "userId": "<requester userId>", "text": "Dentist tomorrow", "at": "2025-12-14T09:00:00+01:00"}
|
|
152
|
+
EOF
|
|
153
|
+
\`\`\`
|
|
154
|
+
|
|
155
|
+
### Managing Events
|
|
156
|
+
- List: \`ls ${workspaceRoot}/events/\`
|
|
157
|
+
- View: \`cat ${workspaceRoot}/events/foo.json\`
|
|
158
|
+
- Delete/cancel: \`rm ${workspaceRoot}/events/foo.json\``;
|
|
159
|
+
}
|
|
160
|
+
return `Events live in the host-side mama control plane, not necessarily in this runtime filesystem.
|
|
161
|
+
|
|
162
|
+
Use the \`event\` tool to create events. It writes to the correct host-side events directory and fills \`platform\`, \`conversationId\`, \`conversationKind\`, and \`userId\` for the current conversation automatically.
|
|
163
|
+
|
|
164
|
+
Do not create event files with bash in \`${workspaceRoot}/events/\` from this sandbox unless you have explicitly verified that path is mounted back to the host-side mama events directory.`;
|
|
165
|
+
}
|
|
166
|
+
export function resolveTriggerAttribution(message) {
|
|
167
|
+
const eventTextMatch = message.text.match(/^\[EVENT:([^:]+):/);
|
|
168
|
+
if (eventTextMatch)
|
|
169
|
+
return `[event: ${eventTextMatch[1]}]`;
|
|
170
|
+
const eventIdMatch = message.id.match(/^event:([^:]+)/);
|
|
171
|
+
if (eventIdMatch)
|
|
172
|
+
return `[event: ${eventIdMatch[1]}]`;
|
|
173
|
+
if (message.userName)
|
|
174
|
+
return `@${message.userName}`;
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
function buildSystemPrompt(workspacePath, conversationId, conversationKind, currentUserId, memory, sandboxConfig, platform, skills, isEventTrigger = false, triggerAttribution) {
|
|
178
|
+
const { workspaceRoot, conversationPath, scratchPath } = buildRuntimePaths(workspacePath, conversationId);
|
|
179
|
+
const sandboxType = sandboxConfig.type;
|
|
180
|
+
const isContainerLike = sandboxType === "container" || sandboxType === "image";
|
|
181
|
+
const isFirecracker = sandboxType === "firecracker";
|
|
95
182
|
// Format channel mappings
|
|
96
183
|
const channelMappings = platform.channels.length > 0
|
|
97
184
|
? platform.channels.map((c) => `${c.id}\t#${c.name}`).join("\n")
|
|
@@ -100,32 +187,39 @@ function buildSystemPrompt(workspacePath, conversationId, conversationKind, curr
|
|
|
100
187
|
const userMappings = platform.users.length > 0
|
|
101
188
|
? platform.users.map((u) => `${u.id}\t@${u.userName}\t${u.displayName}`).join("\n")
|
|
102
189
|
: "(no users loaded)";
|
|
103
|
-
const envDescription =
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
-
|
|
111
|
-
-
|
|
112
|
-
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
190
|
+
const envDescription = buildEnvDescription(sandboxType, workspaceRoot);
|
|
191
|
+
const eventFilesystemInstructions = buildEventFilesystemInstructions(sandboxType, workspaceRoot);
|
|
192
|
+
const eventTriggerInstructions = isEventTrigger
|
|
193
|
+
? `
|
|
194
|
+
## Event Trigger Mode
|
|
195
|
+
- You are handling a scheduled/background event, not opening a brand new chat with a stranger.
|
|
196
|
+
- Treat the incoming user message as a self-contained task prepared by an earlier run.
|
|
197
|
+
- Complete the task directly. Avoid generic greetings, self-introductions, or boilerplate offers to help.
|
|
198
|
+
- For reminders/follow-ups, prefer a short direct response that sounds like a continuation of prior intent.
|
|
199
|
+
- If the event text includes tone, brevity, or language instructions, follow them literally.
|
|
200
|
+
`
|
|
201
|
+
: "";
|
|
202
|
+
const attributionInstructions = triggerAttribution
|
|
203
|
+
? `
|
|
204
|
+
## Attribution
|
|
205
|
+
Always end your final ${platform.name} response and any GitHub issue/PR comments or descriptions you write via tools with:
|
|
206
|
+
_Triggered by ${triggerAttribution}_
|
|
207
|
+
|
|
208
|
+
Do not add this to \`[SILENT]\` responses.
|
|
209
|
+
`
|
|
210
|
+
: "";
|
|
121
211
|
return `You are mama, a ${platform.name} bot assistant. Be concise. No emojis.
|
|
122
212
|
|
|
123
213
|
## Context
|
|
124
214
|
- For current date/time, use: date
|
|
125
215
|
- You have access to previous conversation context including tool results from prior turns.
|
|
126
|
-
- For older history beyond your context, search log.jsonl (contains user messages and your final responses, but not tool results).
|
|
127
|
-
-
|
|
128
|
-
|
|
216
|
+
- For older human-readable history beyond your context, search \`log.jsonl\` (contains user messages and your final responses, but not tool results).
|
|
217
|
+
- Structured session history with tool results lives in \`${conversationPath}/sessions/\`.
|
|
218
|
+
- The active top-level session is selected by \`${conversationPath}/sessions/current\`, which points to a timestamped \`.jsonl\` file in the same directory.
|
|
219
|
+
- Scoped/thread sessions use fixed files at \`${conversationPath}/sessions/<scope_id>.jsonl\` (for example \`${conversationPath}/sessions/1777386320.800769.jsonl\`).
|
|
220
|
+
- If a user asks about something that should exist in conversation history but is not found in the current context window, do not answer "I don't know" or "I don't have that". Instead, search the thread session, top-level session, and \`log.jsonl\` before responding.
|
|
221
|
+
- User messages include a \`[in-thread:TS]\` marker when sent from within a platform thread/reply (TS is the thread or parent message identifier). Without this marker, the message is a top-level conversation message.
|
|
222
|
+
${eventTriggerInstructions}
|
|
129
223
|
${platform.formattingGuide}
|
|
130
224
|
|
|
131
225
|
## Platform IDs
|
|
@@ -137,23 +231,29 @@ When mentioning users, use <@username> format (e.g., <@mario>).
|
|
|
137
231
|
|
|
138
232
|
## Environment
|
|
139
233
|
${envDescription}
|
|
234
|
+
- Default place for clones, downloads, and experiments: ${scratchPath}
|
|
235
|
+
- Do not use host-only paths unless you are running in host mode and verified they exist.
|
|
140
236
|
|
|
141
237
|
## Workspace Layout
|
|
142
|
-
${
|
|
238
|
+
${workspaceRoot}/
|
|
143
239
|
├── MEMORY.md # Global memory (all conversations)
|
|
144
240
|
├── skills/ # Global CLI tools you create
|
|
145
241
|
└── ${conversationId}/ # This conversation
|
|
146
242
|
├── MEMORY.md # Conversation-specific memory
|
|
147
|
-
├── log.jsonl #
|
|
243
|
+
├── log.jsonl # Human-readable message history (no tool results)
|
|
244
|
+
├── sessions/ # Structured session history used for context reconstruction
|
|
245
|
+
│ ├── current # Active top-level session pointer
|
|
246
|
+
│ ├── <timestamp>_<id>.jsonl # Top-level session files
|
|
247
|
+
│ └── <scope_id>.jsonl # Scoped thread/reply session files
|
|
148
248
|
├── attachments/ # User-shared files
|
|
149
|
-
├── scratch/ #
|
|
249
|
+
├── scratch/ # Working directory for clones/downloads/experiments: ${scratchPath}
|
|
150
250
|
└── skills/ # Conversation-specific tools
|
|
151
251
|
|
|
152
252
|
## Skills (Custom CLI Tools)
|
|
153
253
|
You can create reusable CLI tools for recurring tasks (email, APIs, data processing, etc.).
|
|
154
254
|
|
|
155
255
|
### Creating Skills
|
|
156
|
-
Store in \`${
|
|
256
|
+
Store in \`${workspaceRoot}/skills/<name>/\` (global) or \`${conversationPath}/skills/<name>/\` (conversation-specific).
|
|
157
257
|
Each skill directory needs a \`SKILL.md\` with YAML frontmatter:
|
|
158
258
|
|
|
159
259
|
\`\`\`markdown
|
|
@@ -174,7 +274,8 @@ Scripts are in: {baseDir}/
|
|
|
174
274
|
${skills.length > 0 ? formatSkillsForPrompt(skills) : "(no skills installed yet)"}
|
|
175
275
|
|
|
176
276
|
## Events
|
|
177
|
-
You can schedule events that wake you up at specific times or when external things happen.
|
|
277
|
+
You can schedule events that wake you up at specific times or when external things happen.
|
|
278
|
+
${eventFilesystemInstructions}
|
|
178
279
|
|
|
179
280
|
### Event Types
|
|
180
281
|
|
|
@@ -204,25 +305,11 @@ You can schedule events that wake you up at specific times or when external thin
|
|
|
204
305
|
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}.
|
|
205
306
|
|
|
206
307
|
### Platform and Credential Routing
|
|
207
|
-
Set \`platform\` to the target bot platform (\`${platform.name}\` for this conversation).
|
|
308
|
+
Set \`platform\` to the target bot platform (\`${platform.name}\` for this conversation). Include it explicitly to avoid ambiguity.
|
|
208
309
|
|
|
209
310
|
Set \`userId\` to the platform userId of whoever asked for the event. When the event fires, tool execution routes using that user's vault selection in per-user modes. In \`container:<name>\`, events use the container's single shared vault.
|
|
210
311
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
### Creating Events
|
|
214
|
-
Use unique filenames to avoid overwriting existing events. Include a timestamp or random suffix:
|
|
215
|
-
\`\`\`bash
|
|
216
|
-
cat > ${workspacePath}/events/dentist-reminder-$(date +%s).json << 'EOF'
|
|
217
|
-
{"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"}
|
|
218
|
-
EOF
|
|
219
|
-
\`\`\`
|
|
220
|
-
Or check if file exists first before creating.
|
|
221
|
-
|
|
222
|
-
### Managing Events
|
|
223
|
-
- List: \`ls ${workspacePath}/events/\`
|
|
224
|
-
- View: \`cat ${workspacePath}/events/foo.json\`
|
|
225
|
-
- Delete/cancel: \`rm ${workspacePath}/events/foo.json\`
|
|
312
|
+
When scheduling an event, write \`text\` as a self-contained task for your future self. Include the minimum necessary context, tone, and constraints in the text itself because events do not inherit normal conversation history. Good: \`Please remind the user that break time is over and it is time to return to class. Keep it brief, in Traditional Chinese, and do not ask follow-up questions.\` Bad: \`back to class\`.
|
|
226
313
|
|
|
227
314
|
### When Events Trigger
|
|
228
315
|
You receive a message like:
|
|
@@ -242,7 +329,7 @@ Maximum 5 events can be queued. Don't create excessive immediate or periodic eve
|
|
|
242
329
|
|
|
243
330
|
## Memory
|
|
244
331
|
Write to MEMORY.md files to persist context across conversations.
|
|
245
|
-
- Global (${
|
|
332
|
+
- Global (${workspaceRoot}/MEMORY.md): skills, preferences, project info
|
|
246
333
|
- Conversation (${conversationPath}/MEMORY.md): conversation-specific decisions, ongoing work
|
|
247
334
|
Update when you learn something important or when asked to remember something.
|
|
248
335
|
|
|
@@ -250,8 +337,8 @@ Update when you learn something important or when asked to remember something.
|
|
|
250
337
|
${memory}
|
|
251
338
|
|
|
252
339
|
## System Configuration Log
|
|
253
|
-
Maintain ${
|
|
254
|
-
- Installed packages (
|
|
340
|
+
Maintain ${workspaceRoot}/SYSTEM.md to log all environment modifications:
|
|
341
|
+
- Installed packages (apt install, npm install, uv pip install)
|
|
255
342
|
- Environment variables set
|
|
256
343
|
- Config files modified (~/.gitconfig, cron jobs, etc.)
|
|
257
344
|
- Skill dependencies installed
|
|
@@ -261,9 +348,9 @@ Update this file whenever you modify the environment. On fresh container, read i
|
|
|
261
348
|
## Log Queries (for older history)
|
|
262
349
|
Format: \`{"date":"...","ts":"...","user":"...","userName":"...","text":"...","isBot":false}\`
|
|
263
350
|
The log contains user messages and your final responses (not tool calls/results).
|
|
264
|
-
|
|
265
|
-
${isFirecracker ? "Install jq: apt-get install jq" : ""}
|
|
266
|
-
|
|
351
|
+
Use \`log.jsonl\` for quick grep-style history. Use \`${conversationPath}/sessions/\` when you need structured turns, tool outputs, or branch lineage.
|
|
352
|
+
${isContainerLike || isFirecracker ? "Install jq: apt-get install jq" : ""}
|
|
353
|
+
${attributionInstructions}
|
|
267
354
|
\`\`\`bash
|
|
268
355
|
# Recent messages
|
|
269
356
|
tail -30 log.jsonl | jq -c '{date: .date[0:19], user: (.userName // .user), text}'
|
|
@@ -273,6 +360,10 @@ grep -i "topic" log.jsonl | jq -c '{date: .date[0:19], user: (.userName // .user
|
|
|
273
360
|
|
|
274
361
|
# Messages from specific user
|
|
275
362
|
grep '"userName":"mario"' log.jsonl | tail -20 | jq -c '{date: .date[0:19], text}'
|
|
363
|
+
|
|
364
|
+
# Inspect top-level session pointer and available session files
|
|
365
|
+
cat sessions/current
|
|
366
|
+
ls -1 sessions/
|
|
276
367
|
\`\`\`
|
|
277
368
|
|
|
278
369
|
## Tools
|
|
@@ -290,79 +381,24 @@ function truncate(text, maxLen) {
|
|
|
290
381
|
return text;
|
|
291
382
|
return `${text.substring(0, maxLen - 3)}...`;
|
|
292
383
|
}
|
|
293
|
-
function
|
|
294
|
-
if (
|
|
295
|
-
return
|
|
296
|
-
}
|
|
297
|
-
if (result &&
|
|
298
|
-
typeof result === "object" &&
|
|
299
|
-
"content" in result &&
|
|
300
|
-
Array.isArray(result.content)) {
|
|
301
|
-
const content = result.content;
|
|
302
|
-
const textParts = [];
|
|
303
|
-
for (const part of content) {
|
|
304
|
-
if (part.type === "text" && part.text) {
|
|
305
|
-
textParts.push(part.text);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
if (textParts.length > 0) {
|
|
309
|
-
return textParts.join("\n");
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
return JSON.stringify(result);
|
|
313
|
-
}
|
|
314
|
-
function formatToolArgsForSlack(_toolName, args) {
|
|
315
|
-
const lines = [];
|
|
316
|
-
for (const [key, value] of Object.entries(args)) {
|
|
317
|
-
if (key === "label")
|
|
318
|
-
continue;
|
|
319
|
-
if (key === "path" && typeof value === "string") {
|
|
320
|
-
const offset = args.offset;
|
|
321
|
-
const limit = args.limit;
|
|
322
|
-
if (offset !== undefined && limit !== undefined) {
|
|
323
|
-
lines.push(`${value}:${offset}-${offset + limit}`);
|
|
324
|
-
}
|
|
325
|
-
else {
|
|
326
|
-
lines.push(value);
|
|
327
|
-
}
|
|
328
|
-
continue;
|
|
329
|
-
}
|
|
330
|
-
if (key === "offset" || key === "limit")
|
|
331
|
-
continue;
|
|
332
|
-
if (typeof value === "string") {
|
|
333
|
-
lines.push(value);
|
|
334
|
-
}
|
|
335
|
-
else {
|
|
336
|
-
lines.push(JSON.stringify(value));
|
|
337
|
-
}
|
|
384
|
+
export function getUnresolvedSandboxPathContext(sandboxConfig, hostWorkspaceRoot) {
|
|
385
|
+
if (sandboxConfig.type === "image") {
|
|
386
|
+
return createMountedRuntimePathContext(hostWorkspaceRoot, "/workspace");
|
|
338
387
|
}
|
|
339
|
-
return
|
|
388
|
+
return createExecutor(sandboxConfig).getPathContext(hostWorkspaceRoot);
|
|
340
389
|
}
|
|
341
|
-
|
|
342
|
-
// Agent runner
|
|
343
|
-
// ============================================================================
|
|
344
|
-
/**
|
|
345
|
-
* Create a new AgentRunner for a channel.
|
|
346
|
-
* Sets up the session and subscribes to events once.
|
|
347
|
-
*
|
|
348
|
-
* Runner caching is handled by the caller (channelStates in main.ts).
|
|
349
|
-
* This is a stateless factory function.
|
|
350
|
-
*/
|
|
351
|
-
export async function createRunner(sandboxConfig, sessionKey, conversationId, conversationDir, workspaceDir, vaultManager, bindingStore, provisioner) {
|
|
352
|
-
const agentConfig = loadAgentConfig(workspaceDir);
|
|
353
|
-
// Initialize logger with settings from config
|
|
354
|
-
log.initLogger({
|
|
355
|
-
logFormat: agentConfig.logFormat,
|
|
356
|
-
logLevel: agentConfig.logLevel,
|
|
357
|
-
});
|
|
390
|
+
function createRunnerExecutionContext(sandboxConfig, vaultManager, provisioner, workspaceDir, hostWorkspacePath) {
|
|
358
391
|
const executionResolver = vaultManager &&
|
|
359
392
|
sandboxConfig.type !== "host" &&
|
|
360
393
|
(vaultManager.isEnabled() ||
|
|
361
|
-
!!bindingStore ||
|
|
362
394
|
sandboxConfig.type === "container" ||
|
|
363
|
-
sandboxConfig.type === "image"
|
|
364
|
-
|
|
395
|
+
sandboxConfig.type === "image" ||
|
|
396
|
+
sandboxConfig.type === "cloudflare" ||
|
|
397
|
+
sandboxConfig.type === "firecracker")
|
|
398
|
+
? new ActorExecutionResolver(sandboxConfig, vaultManager, provisioner, workspaceDir)
|
|
365
399
|
: undefined;
|
|
400
|
+
// activeExecutor is replaced at the start of each run() call when executionResolver
|
|
401
|
+
// is present, so the stable `executor` wrapper always delegates to the latest resolved value.
|
|
366
402
|
let activeExecutor = executionResolver !== undefined
|
|
367
403
|
? createExecutor({ type: "host" })
|
|
368
404
|
: createExecutor(sandboxConfig);
|
|
@@ -376,97 +412,46 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
|
|
|
376
412
|
getSandboxConfig() {
|
|
377
413
|
return activeExecutor.getSandboxConfig();
|
|
378
414
|
},
|
|
415
|
+
getPathContext(hostWorkspaceRoot) {
|
|
416
|
+
return activeExecutor.getPathContext(hostWorkspaceRoot);
|
|
417
|
+
},
|
|
379
418
|
};
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const model = getModel(agentConfig.provider, agentConfig.model);
|
|
390
|
-
// Initial system prompt (will be updated each run with fresh memory/channels/users/skills)
|
|
391
|
-
const memory = await getMemory(conversationDir);
|
|
392
|
-
const skills = loadMamaSkills(conversationDir, workspacePath);
|
|
393
|
-
const emptyPlatform = {
|
|
394
|
-
name: "slack",
|
|
395
|
-
formattingGuide: "",
|
|
396
|
-
channels: [],
|
|
397
|
-
users: [],
|
|
419
|
+
return {
|
|
420
|
+
executionResolver,
|
|
421
|
+
executor,
|
|
422
|
+
getPathContext: () => executor.getPathContext(hostWorkspacePath),
|
|
423
|
+
async resolveExecutorForRun(context) {
|
|
424
|
+
if (!executionResolver)
|
|
425
|
+
return;
|
|
426
|
+
activeExecutor = await executionResolver.resolve(context);
|
|
427
|
+
},
|
|
398
428
|
};
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
// Thread sessions use fixed files: {conversationDir}/sessions/{threadTs}.jsonl
|
|
403
|
-
const sessionDir = getChannelSessionDir(conversationDir);
|
|
404
|
-
const isThread = sessionKey.includes(":");
|
|
405
|
-
let sessionManager;
|
|
406
|
-
let contextFile;
|
|
407
|
-
if (isThread) {
|
|
408
|
-
const threadFile = getThreadSessionFile(conversationDir, sessionKey);
|
|
409
|
-
const existing = tryResolveThreadSession(threadFile);
|
|
410
|
-
if (existing) {
|
|
411
|
-
contextFile = existing;
|
|
412
|
-
sessionManager = openManagedSession(contextFile, sessionDir, conversationDir);
|
|
413
|
-
}
|
|
414
|
-
else {
|
|
415
|
-
const conversationSource = resolveChannelSessionFile(conversationDir);
|
|
416
|
-
if (conversationSource) {
|
|
417
|
-
try {
|
|
418
|
-
contextFile = forkThreadSessionFile(conversationSource, threadFile, conversationDir);
|
|
419
|
-
sessionManager = openManagedSession(contextFile, sessionDir, conversationDir);
|
|
420
|
-
}
|
|
421
|
-
catch {
|
|
422
|
-
contextFile = createManagedSessionFileAtPath(threadFile, conversationDir);
|
|
423
|
-
sessionManager = openManagedSession(contextFile, sessionDir, conversationDir);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
else {
|
|
427
|
-
contextFile = createManagedSessionFileAtPath(threadFile, conversationDir);
|
|
428
|
-
sessionManager = openManagedSession(contextFile, sessionDir, conversationDir);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
else {
|
|
433
|
-
// Direct/shared session: normal resolve
|
|
434
|
-
contextFile = resolveManagedSessionFile(sessionDir, conversationDir);
|
|
435
|
-
sessionManager = openManagedSession(contextFile, sessionDir, conversationDir);
|
|
436
|
-
}
|
|
437
|
-
const sessionUuid = extractSessionUuid(contextFile);
|
|
438
|
-
// Used for Slack thread filtering — for non-Slack platforms this is effectively a no-op
|
|
439
|
-
const rootTs = extractSessionSuffix(sessionKey);
|
|
440
|
-
const settingsManager = createMamaSettingsManager(join(conversationDir, ".."));
|
|
441
|
-
// Create AuthStorage and ModelRegistry
|
|
442
|
-
// Auth stored outside workspace so agent can't access it
|
|
429
|
+
}
|
|
430
|
+
async function createConfiguredAgentSession(params) {
|
|
431
|
+
const { conversationId, workspaceDir, runtimeWorkspaceRoot, systemPrompt, model, thinkingLevel, tools, sessionManager, settingsManager, } = params;
|
|
443
432
|
const authStorage = AuthStorage.create(join(homedir(), ".pi", "mama", "auth.json"));
|
|
444
433
|
const modelRegistry = ModelRegistry.create(authStorage);
|
|
445
|
-
// Create agent
|
|
446
434
|
const agent = new Agent({
|
|
447
435
|
initialState: {
|
|
448
436
|
systemPrompt,
|
|
449
437
|
model,
|
|
450
|
-
thinkingLevel
|
|
438
|
+
thinkingLevel,
|
|
451
439
|
tools,
|
|
452
440
|
},
|
|
453
441
|
convertToLlm,
|
|
454
442
|
getApiKey: async () => {
|
|
455
443
|
const key = await modelRegistry.getApiKeyForProvider(model.provider);
|
|
456
|
-
if (!key)
|
|
444
|
+
if (!key) {
|
|
457
445
|
throw new Error(`No API key for provider "${model.provider}". Set the appropriate environment variable or configure via auth.json`);
|
|
446
|
+
}
|
|
458
447
|
return key;
|
|
459
448
|
},
|
|
460
449
|
});
|
|
461
|
-
// Load existing messages
|
|
462
450
|
const loadedSession = sessionManager.buildSessionContext();
|
|
463
451
|
if (loadedSession.messages.length > 0) {
|
|
464
452
|
agent.state.messages = loadedSession.messages;
|
|
465
|
-
log.logInfo(`[${conversationId}]
|
|
453
|
+
log.logInfo(`[${conversationId}] Reloaded ${loadedSession.messages.length} messages from session context`);
|
|
466
454
|
}
|
|
467
|
-
// Load extensions, skills, prompts, themes via DefaultResourceLoader
|
|
468
|
-
// This reads ~/.pi/agent/settings.json (packages, extensions enable/disable)
|
|
469
|
-
// and discovers resources from standard locations + npm/git packages.
|
|
470
455
|
const resourceLoader = new DefaultResourceLoader({
|
|
471
456
|
cwd: workspaceDir,
|
|
472
457
|
agentDir: getAgentDir(),
|
|
@@ -480,121 +465,407 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
|
|
|
480
465
|
log.logWarning(`[${conversationId}] Extension load error: ${err.path}`, err.error);
|
|
481
466
|
}
|
|
482
467
|
}
|
|
483
|
-
log.logInfo(`[${conversationId}] Loaded ${extResult.extensions.length} extension(s): ${extResult.extensions.map((
|
|
468
|
+
log.logInfo(`[${conversationId}] Loaded ${extResult.extensions.length} extension(s): ${extResult.extensions.map((extension) => extension.path).join(", ")}`);
|
|
484
469
|
}
|
|
485
470
|
catch (error) {
|
|
486
471
|
log.logWarning(`[${conversationId}] Failed to load resources`, String(error));
|
|
487
472
|
}
|
|
488
473
|
const baseToolsOverride = Object.fromEntries(tools.map((tool) => [tool.name, tool]));
|
|
489
|
-
// Create AgentSession wrapper
|
|
490
474
|
const session = new AgentSession({
|
|
491
475
|
agent,
|
|
492
476
|
sessionManager,
|
|
493
477
|
settingsManager,
|
|
494
|
-
cwd:
|
|
478
|
+
cwd: runtimeWorkspaceRoot,
|
|
495
479
|
modelRegistry,
|
|
496
480
|
resourceLoader,
|
|
497
481
|
baseToolsOverride,
|
|
498
482
|
});
|
|
499
|
-
|
|
500
|
-
|
|
483
|
+
return { agent, session };
|
|
484
|
+
}
|
|
485
|
+
function createEmptyUsageTotals() {
|
|
486
|
+
return {
|
|
487
|
+
input: 0,
|
|
488
|
+
output: 0,
|
|
489
|
+
cacheRead: 0,
|
|
490
|
+
cacheWrite: 0,
|
|
491
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
function createRunState() {
|
|
495
|
+
return {
|
|
501
496
|
responseCtx: null,
|
|
502
497
|
logCtx: null,
|
|
503
498
|
queue: null,
|
|
504
499
|
pendingTools: new Map(),
|
|
505
|
-
totalUsage:
|
|
506
|
-
input: 0,
|
|
507
|
-
output: 0,
|
|
508
|
-
cacheRead: 0,
|
|
509
|
-
cacheWrite: 0,
|
|
510
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
511
|
-
},
|
|
500
|
+
totalUsage: createEmptyUsageTotals(),
|
|
512
501
|
llmCallCount: 0,
|
|
513
502
|
stopReason: "stop",
|
|
514
503
|
errorMessage: undefined,
|
|
504
|
+
reportedLlmError: false,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
function resetRunState(runState, responseCtx, sessionConversation, userName, sessionUuid) {
|
|
508
|
+
runState.responseCtx = responseCtx;
|
|
509
|
+
runState.logCtx = {
|
|
510
|
+
conversationId: sessionConversation,
|
|
511
|
+
userName,
|
|
512
|
+
conversationName: undefined,
|
|
513
|
+
sessionId: sessionUuid,
|
|
515
514
|
};
|
|
516
|
-
|
|
515
|
+
runState.pendingTools.clear();
|
|
516
|
+
runState.totalUsage = createEmptyUsageTotals();
|
|
517
|
+
runState.llmCallCount = 0;
|
|
518
|
+
runState.stopReason = "stop";
|
|
519
|
+
runState.errorMessage = undefined;
|
|
520
|
+
runState.reportedLlmError = false;
|
|
521
|
+
}
|
|
522
|
+
function createRunQueue(responseCtx) {
|
|
523
|
+
let queueChain = Promise.resolve();
|
|
524
|
+
return {
|
|
525
|
+
queue: {
|
|
526
|
+
enqueue(fn, errorContext) {
|
|
527
|
+
queueChain = queueChain.then(async () => {
|
|
528
|
+
try {
|
|
529
|
+
await fn();
|
|
530
|
+
}
|
|
531
|
+
catch (err) {
|
|
532
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
533
|
+
log.logWarning(`API error (${errorContext})`, errMsg);
|
|
534
|
+
try {
|
|
535
|
+
await responseCtx.respondDiagnostic(`Error: ${errMsg}`, { style: "error" });
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
// Ignore
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
wait: () => queueChain,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
function padTwoDigits(n) {
|
|
548
|
+
return n.toString().padStart(2, "0");
|
|
549
|
+
}
|
|
550
|
+
function formatTimestampedUserMessage(message) {
|
|
551
|
+
const now = new Date();
|
|
552
|
+
const offset = -now.getTimezoneOffset();
|
|
553
|
+
const offsetSign = offset >= 0 ? "+" : "-";
|
|
554
|
+
const offsetHours = padTwoDigits(Math.floor(Math.abs(offset) / 60));
|
|
555
|
+
const offsetMins = padTwoDigits(Math.abs(offset) % 60);
|
|
556
|
+
const timestamp = `${now.getFullYear()}-${padTwoDigits(now.getMonth() + 1)}-${padTwoDigits(now.getDate())} ` +
|
|
557
|
+
`${padTwoDigits(now.getHours())}:${padTwoDigits(now.getMinutes())}:${padTwoDigits(now.getSeconds())}` +
|
|
558
|
+
`${offsetSign}${offsetHours}:${offsetMins}`;
|
|
559
|
+
const threadContext = message.threadTs ? ` [in-thread:${message.threadTs}]` : "";
|
|
560
|
+
return `[${timestamp}] [${message.userName || "unknown"}]${threadContext}: ${message.text}`;
|
|
561
|
+
}
|
|
562
|
+
function collectMessageAttachments(message, workspacePath) {
|
|
563
|
+
const imageAttachments = [];
|
|
564
|
+
const nonImagePaths = [];
|
|
565
|
+
for (const attachment of message.attachments || []) {
|
|
566
|
+
const fullPath = `${workspacePath}/${attachment.localPath}`;
|
|
567
|
+
const mimeType = getImageMimeType(attachment.localPath);
|
|
568
|
+
if (mimeType && existsSync(fullPath)) {
|
|
569
|
+
try {
|
|
570
|
+
imageAttachments.push({
|
|
571
|
+
type: "image",
|
|
572
|
+
mimeType,
|
|
573
|
+
data: readFileSync(fullPath).toString("base64"),
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
catch {
|
|
577
|
+
nonImagePaths.push(fullPath);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
nonImagePaths.push(fullPath);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return { imageAttachments, nonImagePaths };
|
|
585
|
+
}
|
|
586
|
+
function buildPromptPayload(message, workspacePath) {
|
|
587
|
+
let userMessage = formatTimestampedUserMessage(message);
|
|
588
|
+
const { imageAttachments, nonImagePaths } = collectMessageAttachments(message, workspacePath);
|
|
589
|
+
if (nonImagePaths.length > 0) {
|
|
590
|
+
userMessage += `\n\n<slack_attachments>\n${nonImagePaths.join("\n")}\n</slack_attachments>`;
|
|
591
|
+
}
|
|
592
|
+
return { userMessage, imageAttachments };
|
|
593
|
+
}
|
|
594
|
+
async function writePromptDebugContext(conversationDir, systemPrompt, session, userMessage, imageAttachmentCount) {
|
|
595
|
+
const debugContext = {
|
|
596
|
+
systemPrompt,
|
|
597
|
+
messages: session.messages,
|
|
598
|
+
newUserMessage: userMessage,
|
|
599
|
+
imageAttachmentCount,
|
|
600
|
+
};
|
|
601
|
+
await writeFile(join(conversationDir, "last_prompt.jsonl"), JSON.stringify(debugContext, null, 2));
|
|
602
|
+
}
|
|
603
|
+
function getFinalAssistantText(session) {
|
|
604
|
+
const lastAssistant = session.messages.filter((message) => message.role === "assistant").pop();
|
|
605
|
+
return (lastAssistant?.content
|
|
606
|
+
.filter((content) => content.type === "text")
|
|
607
|
+
.map((content) => content.text)
|
|
608
|
+
.join("\n") || "");
|
|
609
|
+
}
|
|
610
|
+
export function appendTriggerAttribution(text, triggerAttribution) {
|
|
611
|
+
if (!triggerAttribution)
|
|
612
|
+
return text;
|
|
613
|
+
const suffix = `_Triggered by ${triggerAttribution}_`;
|
|
614
|
+
if (text.trimEnd().endsWith(suffix))
|
|
615
|
+
return text;
|
|
616
|
+
return `${text.trimEnd()}\n\n${suffix}`;
|
|
617
|
+
}
|
|
618
|
+
async function finalizeRunResponse(responseCtx, session, runState, options) {
|
|
619
|
+
if (runState.stopReason === "error" && runState.errorMessage) {
|
|
620
|
+
if (!runState.reportedLlmError) {
|
|
621
|
+
runState.reportedLlmError = true;
|
|
622
|
+
reportUserFacingError(new Error("LLM run completed with error stop reason"), {
|
|
623
|
+
domain: "llm",
|
|
624
|
+
surface: "assistant_response",
|
|
625
|
+
operation: "llm_turn",
|
|
626
|
+
severity: "error",
|
|
627
|
+
platform: options?.platform,
|
|
628
|
+
provider: options?.model?.provider,
|
|
629
|
+
model: options?.model?.name,
|
|
630
|
+
stopReason: runState.stopReason,
|
|
631
|
+
context: {
|
|
632
|
+
sessionConversation: options?.sessionConversation,
|
|
633
|
+
sessionUuid: options?.sessionUuid,
|
|
634
|
+
hasErrorMessage: true,
|
|
635
|
+
llmCallCount: runState.llmCallCount,
|
|
636
|
+
},
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
try {
|
|
640
|
+
await responseCtx.replaceResponse("_Sorry, something went wrong_");
|
|
641
|
+
await responseCtx.respondDiagnostic(`Error: ${runState.errorMessage}`, {
|
|
642
|
+
style: "error",
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
catch (err) {
|
|
646
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
647
|
+
log.logWarning("Failed to post error message", errMsg);
|
|
648
|
+
reportUserFacingError(err, {
|
|
649
|
+
domain: "chat_platform",
|
|
650
|
+
surface: "final_response",
|
|
651
|
+
operation: "finalize_error_response",
|
|
652
|
+
severity: "error",
|
|
653
|
+
platform: options?.platform,
|
|
654
|
+
context: {
|
|
655
|
+
sessionConversation: options?.sessionConversation,
|
|
656
|
+
sessionUuid: options?.sessionUuid,
|
|
657
|
+
stopReason: runState.stopReason,
|
|
658
|
+
},
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
const finalText = getFinalAssistantText(session);
|
|
664
|
+
if (finalText.trim() === "[SILENT]" || finalText.trim().startsWith("[SILENT]")) {
|
|
665
|
+
try {
|
|
666
|
+
await responseCtx.deleteResponse();
|
|
667
|
+
log.logInfo("Silent response - deleted message and thread");
|
|
668
|
+
}
|
|
669
|
+
catch (err) {
|
|
670
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
671
|
+
log.logWarning("Failed to delete message for silent response", errMsg);
|
|
672
|
+
}
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
if (!finalText.trim())
|
|
676
|
+
return;
|
|
677
|
+
try {
|
|
678
|
+
await responseCtx.replaceResponse(appendTriggerAttribution(finalText, options?.triggerAttribution), { createOverflowLink: options?.createOverflowLink });
|
|
679
|
+
}
|
|
680
|
+
catch (err) {
|
|
681
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
682
|
+
log.logWarning("Failed to replace message with final text", errMsg);
|
|
683
|
+
reportUserFacingError(err, {
|
|
684
|
+
domain: "chat_platform",
|
|
685
|
+
surface: "final_response",
|
|
686
|
+
operation: "replace_final_response",
|
|
687
|
+
severity: "error",
|
|
688
|
+
platform: options?.platform,
|
|
689
|
+
context: {
|
|
690
|
+
sessionConversation: options?.sessionConversation,
|
|
691
|
+
sessionUuid: options?.sessionUuid,
|
|
692
|
+
finalTextLength: finalText.length,
|
|
693
|
+
},
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
async function reportUsageSummary(ctx) {
|
|
698
|
+
const { session, runState, responseCtx, platform, model, agentConfig, sessionConversation, sessionUuid, waitForQueue, } = ctx;
|
|
699
|
+
if (runState.totalUsage.cost.total <= 0)
|
|
700
|
+
return;
|
|
701
|
+
const lastAssistantMessage = session.messages
|
|
702
|
+
.slice()
|
|
703
|
+
.toReversed()
|
|
704
|
+
.find((message) => message.role === "assistant" && message.stopReason !== "aborted");
|
|
705
|
+
const contextTokens = lastAssistantMessage
|
|
706
|
+
? lastAssistantMessage.usage.input +
|
|
707
|
+
lastAssistantMessage.usage.output +
|
|
708
|
+
lastAssistantMessage.usage.cacheRead +
|
|
709
|
+
lastAssistantMessage.usage.cacheWrite
|
|
710
|
+
: 0;
|
|
711
|
+
const contextWindow = model.contextWindow || 200000;
|
|
712
|
+
const { totalUsage } = runState;
|
|
713
|
+
const runMetricAttributes = metricAttributes({
|
|
714
|
+
provider: model.provider,
|
|
715
|
+
model: agentConfig.model,
|
|
716
|
+
channel_id: sessionConversation,
|
|
717
|
+
session_id: sessionUuid,
|
|
718
|
+
stop_reason: runState.stopReason,
|
|
719
|
+
llm_calls: runState.llmCallCount,
|
|
720
|
+
});
|
|
721
|
+
Sentry.metrics.distribution("agent.run.tokens_in", totalUsage.input, {
|
|
722
|
+
attributes: runMetricAttributes,
|
|
723
|
+
});
|
|
724
|
+
Sentry.metrics.distribution("agent.run.tokens_out", totalUsage.output, {
|
|
725
|
+
attributes: runMetricAttributes,
|
|
726
|
+
});
|
|
727
|
+
Sentry.metrics.distribution("agent.run.cache_read", totalUsage.cacheRead, {
|
|
728
|
+
attributes: runMetricAttributes,
|
|
729
|
+
});
|
|
730
|
+
Sentry.metrics.distribution("agent.run.cache_write", totalUsage.cacheWrite, {
|
|
731
|
+
attributes: runMetricAttributes,
|
|
732
|
+
});
|
|
733
|
+
Sentry.metrics.distribution("agent.run.cost", totalUsage.cost.total, {
|
|
734
|
+
attributes: runMetricAttributes,
|
|
735
|
+
});
|
|
736
|
+
Sentry.metrics.gauge("agent.context.utilization", contextTokens / contextWindow, {
|
|
737
|
+
unit: "ratio",
|
|
738
|
+
attributes: runMetricAttributes,
|
|
739
|
+
});
|
|
740
|
+
const summary = log.logUsageSummary(runState.logCtx, runState.totalUsage, contextTokens, contextWindow);
|
|
741
|
+
if (platform.diagnostics?.showUsageSummary === true) {
|
|
742
|
+
runState.queue.enqueue(() => responseCtx.respondDiagnostic(summary, { style: "muted" }), "usage summary");
|
|
743
|
+
await waitForQueue();
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
function reloadSessionMessages(sessionManager, conversationId, agent) {
|
|
747
|
+
const messages = sessionManager.buildSessionContext().messages;
|
|
748
|
+
if (messages.length > 0) {
|
|
749
|
+
agent.state.messages = messages;
|
|
750
|
+
log.logInfo(`[${conversationId}] Reloaded ${messages.length} messages from context`);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
async function prepareRunContext(params) {
|
|
754
|
+
const { message, responseCtx, platform, conversationId, conversationDir, sessionUuid, runState, executor, executionResolver, resolveExecutorForRun, getPathContext, sessionManager, session, agent, setEventContext, setUploadFunction, } = params;
|
|
755
|
+
let pathContext = params.pathContext;
|
|
756
|
+
const sessionConversation = message.sessionKey.split(":")[0];
|
|
757
|
+
await mkdir(join(conversationDir, "scratch"), { recursive: true });
|
|
758
|
+
if (executionResolver) {
|
|
759
|
+
await resolveExecutorForRun({
|
|
760
|
+
platform: platform.name,
|
|
761
|
+
userId: message.userId,
|
|
762
|
+
conversationId,
|
|
763
|
+
});
|
|
764
|
+
pathContext = getPathContext();
|
|
765
|
+
}
|
|
766
|
+
reloadSessionMessages(sessionManager, conversationId, agent);
|
|
767
|
+
const memory = await getMemory(conversationDir);
|
|
768
|
+
const skills = loadMamaSkills(conversationDir, pathContext.runtimeWorkspaceRoot);
|
|
769
|
+
const triggerAttribution = resolveTriggerAttribution(message);
|
|
770
|
+
const systemPrompt = buildSystemPrompt(pathContext.runtimeWorkspaceRoot, conversationId, message.conversationKind, message.userId, memory, executor.getSandboxConfig(), platform, skills, message.id.startsWith("event:"), triggerAttribution);
|
|
771
|
+
session.agent.state.systemPrompt = systemPrompt;
|
|
772
|
+
setEventContext({
|
|
773
|
+
platform: platform.name,
|
|
774
|
+
conversationId,
|
|
775
|
+
conversationKind: message.conversationKind,
|
|
776
|
+
userId: message.userId,
|
|
777
|
+
});
|
|
778
|
+
setUploadFunction(async (filePath, title) => {
|
|
779
|
+
const hostPath = translateRuntimePathToHost(filePath, pathContext);
|
|
780
|
+
await responseCtx.uploadFile(hostPath, title);
|
|
781
|
+
});
|
|
782
|
+
resetRunState(runState, responseCtx, sessionConversation, message.userName, sessionUuid);
|
|
783
|
+
const runQueue = createRunQueue(responseCtx);
|
|
784
|
+
runState.queue = runQueue.queue;
|
|
785
|
+
log.logInfo(`Context sizes - system: ${systemPrompt.length} chars, memory: ${memory.length} chars`);
|
|
786
|
+
log.logInfo(`Channels: ${platform.channels.length}, Users: ${platform.users.length}`);
|
|
787
|
+
const { userMessage, imageAttachments } = buildPromptPayload(message, pathContext.runtimeWorkspaceRoot);
|
|
788
|
+
await writePromptDebugContext(conversationDir, systemPrompt, session, userMessage, imageAttachments.length);
|
|
789
|
+
return {
|
|
790
|
+
sessionConversation,
|
|
791
|
+
runQueue,
|
|
792
|
+
userMessage,
|
|
793
|
+
imageAttachments,
|
|
794
|
+
triggerAttribution,
|
|
795
|
+
pathContext,
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
function attachSessionEventHandlers(params) {
|
|
799
|
+
const { session, runState, model, agentConfig } = params;
|
|
517
800
|
session.subscribe(async (event) => {
|
|
518
|
-
// Skip if no active run
|
|
519
801
|
if (!runState.responseCtx || !runState.logCtx || !runState.queue)
|
|
520
802
|
return;
|
|
521
803
|
const { responseCtx, logCtx, queue, pendingTools } = runState;
|
|
522
804
|
const baseAttrs = { channel_id: logCtx.conversationId, session_id: logCtx.sessionId };
|
|
523
805
|
if (event.type === "tool_execution_start") {
|
|
524
|
-
const
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
args: agentEvent.args,
|
|
806
|
+
const args = (event.args ?? {});
|
|
807
|
+
const label = args.label || event.toolName;
|
|
808
|
+
pendingTools.set(event.toolCallId, {
|
|
809
|
+
toolName: event.toolName,
|
|
810
|
+
args: event.args,
|
|
530
811
|
startTime: Date.now(),
|
|
531
812
|
});
|
|
532
813
|
addLifecycleBreadcrumb("agent.tool.started", {
|
|
533
|
-
tool:
|
|
814
|
+
tool: event.toolName,
|
|
534
815
|
...baseAttrs,
|
|
535
816
|
});
|
|
536
|
-
log.logToolStart(logCtx,
|
|
537
|
-
|
|
538
|
-
// Tool execution details are still posted to the thread (see tool_execution_end).
|
|
817
|
+
log.logToolStart(logCtx, event.toolName, label, event.args);
|
|
818
|
+
return;
|
|
539
819
|
}
|
|
540
|
-
|
|
541
|
-
const
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
pendingTools.delete(agentEvent.toolCallId);
|
|
820
|
+
if (event.type === "tool_execution_end") {
|
|
821
|
+
const resultStr = extractToolResultText(event.result);
|
|
822
|
+
const pending = pendingTools.get(event.toolCallId);
|
|
823
|
+
pendingTools.delete(event.toolCallId);
|
|
545
824
|
const durationMs = pending ? Date.now() - pending.startTime : 0;
|
|
546
825
|
Sentry.metrics.count("agent.tool.calls", 1, {
|
|
547
826
|
attributes: metricAttributes({
|
|
548
|
-
tool:
|
|
549
|
-
error: String(
|
|
827
|
+
tool: event.toolName,
|
|
828
|
+
error: String(event.isError),
|
|
550
829
|
...baseAttrs,
|
|
551
830
|
}),
|
|
552
831
|
});
|
|
553
832
|
Sentry.metrics.distribution("agent.tool.duration", durationMs, {
|
|
554
833
|
unit: "millisecond",
|
|
555
834
|
attributes: metricAttributes({
|
|
556
|
-
tool:
|
|
835
|
+
tool: event.toolName,
|
|
557
836
|
...baseAttrs,
|
|
558
837
|
}),
|
|
559
838
|
});
|
|
560
839
|
addLifecycleBreadcrumb("agent.tool.completed", {
|
|
561
|
-
tool:
|
|
562
|
-
error:
|
|
840
|
+
tool: event.toolName,
|
|
841
|
+
error: event.isError,
|
|
563
842
|
duration_ms: durationMs,
|
|
564
843
|
...baseAttrs,
|
|
565
844
|
});
|
|
566
|
-
if (
|
|
567
|
-
log.logToolError(logCtx,
|
|
845
|
+
if (event.isError) {
|
|
846
|
+
log.logToolError(logCtx, event.toolName, durationMs, resultStr);
|
|
568
847
|
}
|
|
569
848
|
else {
|
|
570
|
-
log.logToolSuccess(logCtx,
|
|
849
|
+
log.logToolSuccess(logCtx, event.toolName, durationMs, resultStr);
|
|
571
850
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
if (argsFormatted)
|
|
583
|
-
threadMessage += `\`\`\`\n${argsFormatted}\n\`\`\`\n`;
|
|
584
|
-
threadMessage += `*Result:*\n\`\`\`\n${resultStr}\n\`\`\``;
|
|
585
|
-
// Only post thread details for tools with meaningful output (bash, attach).
|
|
586
|
-
// Skip read/write/edit to reduce Slack noise — their results are in the log.
|
|
587
|
-
const quietTools = new Set(["read", "write", "edit"]);
|
|
588
|
-
if (!quietTools.has(agentEvent.toolName)) {
|
|
589
|
-
queue.enqueueMessage(threadMessage, "thread", "tool result thread", false);
|
|
851
|
+
if (shouldSurfaceToolDiagnostic(event.toolName)) {
|
|
852
|
+
const toolResult = {
|
|
853
|
+
toolName: event.toolName,
|
|
854
|
+
label: pending?.args ? pending.args.label : undefined,
|
|
855
|
+
args: pending?.args,
|
|
856
|
+
result: truncate(resultStr, TOOL_RESULT_DIAGNOSTIC_CAP),
|
|
857
|
+
isError: event.isError,
|
|
858
|
+
durationMs,
|
|
859
|
+
};
|
|
860
|
+
queue.enqueue(() => responseCtx.respondToolResult(toolResult), "tool result diagnostic");
|
|
590
861
|
}
|
|
591
|
-
if (
|
|
862
|
+
if (event.isError && shouldSurfaceToolDiagnostic(event.toolName)) {
|
|
592
863
|
queue.enqueue(() => responseCtx.respond(`_Error: ${truncate(resultStr, 200)}_`), "tool error");
|
|
593
864
|
}
|
|
865
|
+
return;
|
|
594
866
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
if (agentEvent.message.role === "assistant") {
|
|
867
|
+
if (event.type === "message_start") {
|
|
868
|
+
if (event.message.role === "assistant") {
|
|
598
869
|
runState.llmCallCount += 1;
|
|
599
870
|
addLifecycleBreadcrumb("agent.llm.call.started", {
|
|
600
871
|
call_index: runState.llmCallCount,
|
|
@@ -604,11 +875,11 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
|
|
|
604
875
|
});
|
|
605
876
|
log.logResponseStart(logCtx);
|
|
606
877
|
}
|
|
878
|
+
return;
|
|
607
879
|
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
const assistantMsg = agentEvent.message;
|
|
880
|
+
if (event.type === "message_end") {
|
|
881
|
+
if (event.message.role === "assistant") {
|
|
882
|
+
const assistantMsg = event.message;
|
|
612
883
|
if (assistantMsg.stopReason) {
|
|
613
884
|
runState.stopReason = assistantMsg.stopReason;
|
|
614
885
|
}
|
|
@@ -625,7 +896,6 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
|
|
|
625
896
|
runState.totalUsage.cost.cacheRead += assistantMsg.usage.cost.cacheRead;
|
|
626
897
|
runState.totalUsage.cost.cacheWrite += assistantMsg.usage.cost.cacheWrite;
|
|
627
898
|
runState.totalUsage.cost.total += assistantMsg.usage.cost.total;
|
|
628
|
-
// Per-turn LLM metrics
|
|
629
899
|
const llmAttributes = metricAttributes({
|
|
630
900
|
provider: model.provider,
|
|
631
901
|
model: agentConfig.model,
|
|
@@ -664,10 +934,9 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
|
|
|
664
934
|
cost_total_usd: assistantMsg.usage.cost.total,
|
|
665
935
|
});
|
|
666
936
|
}
|
|
667
|
-
const content = agentEvent.message.content;
|
|
668
937
|
const thinkingParts = [];
|
|
669
938
|
const textParts = [];
|
|
670
|
-
for (const part of content) {
|
|
939
|
+
for (const part of assistantMsg.content) {
|
|
671
940
|
if (part.type === "thinking") {
|
|
672
941
|
thinkingParts.push(part.thinking);
|
|
673
942
|
}
|
|
@@ -678,310 +947,175 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
|
|
|
678
947
|
const text = textParts.join("\n");
|
|
679
948
|
for (const thinking of thinkingParts) {
|
|
680
949
|
log.logThinking(logCtx, thinking);
|
|
681
|
-
queue.
|
|
682
|
-
queue.
|
|
950
|
+
queue.enqueue(() => responseCtx.respond(`_${thinking}_`), "thinking main");
|
|
951
|
+
queue.enqueue(() => responseCtx.respondDiagnostic(`_${thinking}_`), "thinking diagnostic");
|
|
683
952
|
}
|
|
684
953
|
if (text.trim()) {
|
|
685
954
|
log.logResponse(logCtx, text);
|
|
686
|
-
queue.
|
|
687
|
-
// Only overflow to thread for texts that will be truncated in main
|
|
688
|
-
if (text.length > SLACK_MAX_LENGTH) {
|
|
689
|
-
queue.enqueueMessage(text, "thread", "response thread", false);
|
|
690
|
-
}
|
|
955
|
+
queue.enqueue(() => responseCtx.respond(text), "response main");
|
|
691
956
|
}
|
|
692
957
|
}
|
|
958
|
+
return;
|
|
693
959
|
}
|
|
694
|
-
|
|
960
|
+
if (event.type === "compaction_start") {
|
|
695
961
|
log.logInfo(`Auto-compaction started (reason: ${event.reason})`);
|
|
696
962
|
queue.enqueue(() => responseCtx.respond("_Compacting context..._"), "compaction start");
|
|
963
|
+
return;
|
|
697
964
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
log.logInfo(`Auto-compaction complete: ${compEvent.result.tokensBefore} tokens compacted`);
|
|
965
|
+
if (event.type === "compaction_end") {
|
|
966
|
+
if (event.result) {
|
|
967
|
+
log.logInfo(`Auto-compaction complete: ${event.result.tokensBefore} tokens compacted`);
|
|
702
968
|
}
|
|
703
|
-
else if (
|
|
969
|
+
else if (event.aborted) {
|
|
704
970
|
log.logInfo("Auto-compaction aborted");
|
|
705
971
|
}
|
|
972
|
+
return;
|
|
706
973
|
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
queue.enqueue(() => responseCtx.respond(`_Retrying (${retryEvent.attempt}/${retryEvent.maxAttempts})..._`), "retry");
|
|
974
|
+
if (event.type === "auto_retry_start") {
|
|
975
|
+
log.logWarning(`Retrying (${event.attempt}/${event.maxAttempts})`, event.errorMessage);
|
|
976
|
+
queue.enqueue(() => responseCtx.respond(`_Retrying (${event.attempt}/${event.maxAttempts})..._`), "retry");
|
|
711
977
|
}
|
|
712
978
|
});
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
979
|
+
}
|
|
980
|
+
// Cap raw tool output before handing it to adapters. Bash output can be MB; without
|
|
981
|
+
// this each adapter's splitter would fan it out into many sequential platform posts.
|
|
982
|
+
const TOOL_RESULT_DIAGNOSTIC_CAP = 8000;
|
|
983
|
+
function extractToolResultText(result) {
|
|
984
|
+
if (typeof result === "string") {
|
|
985
|
+
return result;
|
|
986
|
+
}
|
|
987
|
+
if (result &&
|
|
988
|
+
typeof result === "object" &&
|
|
989
|
+
"content" in result &&
|
|
990
|
+
Array.isArray(result.content)) {
|
|
991
|
+
const content = result.content;
|
|
992
|
+
const textParts = [];
|
|
993
|
+
for (const part of content) {
|
|
994
|
+
if (part.type === "text" && part.text) {
|
|
995
|
+
textParts.push(part.text);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
if (textParts.length > 0) {
|
|
999
|
+
return textParts.join("\n");
|
|
727
1000
|
}
|
|
728
|
-
|
|
1001
|
+
}
|
|
1002
|
+
return JSON.stringify(result);
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Create a new AgentRunner for a channel.
|
|
1006
|
+
* Sets up the session and subscribes to events once.
|
|
1007
|
+
*
|
|
1008
|
+
* Runner caching is handled by the caller (channelStates in main.ts).
|
|
1009
|
+
* This is a stateless factory function.
|
|
1010
|
+
*/
|
|
1011
|
+
export async function createRunner(sandboxConfig, sessionKey, conversationId, conversationDir, workspaceDir, sessionScope, vaultManager, provisioner, sessionView) {
|
|
1012
|
+
const agentConfig = loadAgentConfigForConversation(conversationDir);
|
|
1013
|
+
const workspaceBase = join(conversationDir, "..");
|
|
1014
|
+
const { executionResolver, executor, getPathContext, resolveExecutorForRun } = createRunnerExecutionContext(sandboxConfig, vaultManager, provisioner, workspaceDir, workspaceBase);
|
|
1015
|
+
let pathContext = getUnresolvedSandboxPathContext(sandboxConfig, workspaceBase);
|
|
1016
|
+
// Create tools (per-runner, with per-runner upload function setter)
|
|
1017
|
+
const { tools, setUploadFunction, setEventContext } = createMamaTools(executor, workspaceDir);
|
|
1018
|
+
// Resolve model from config
|
|
1019
|
+
// Use 'as any' cast because agentConfig.provider/model are plain strings,
|
|
1020
|
+
// while getModel() has constrained generic types for known providers.
|
|
1021
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1022
|
+
const model = getModel(agentConfig.provider, agentConfig.model);
|
|
1023
|
+
// Initial system prompt (will be updated each run with fresh memory/channels/users/skills)
|
|
1024
|
+
const memory = await getMemory(conversationDir);
|
|
1025
|
+
const skills = loadMamaSkills(conversationDir, pathContext.runtimeWorkspaceRoot);
|
|
1026
|
+
const emptyPlatform = {
|
|
1027
|
+
name: "chat",
|
|
1028
|
+
formattingGuide: "",
|
|
1029
|
+
channels: [],
|
|
1030
|
+
users: [],
|
|
729
1031
|
};
|
|
1032
|
+
const systemPrompt = buildSystemPrompt(pathContext.runtimeWorkspaceRoot, conversationId, "shared", undefined, memory, sandboxConfig, emptyPlatform, skills);
|
|
1033
|
+
// Create session manager and settings manager. Top-level/private sessions
|
|
1034
|
+
// use the conversation's current pointer; scoped sessions use fixed files.
|
|
1035
|
+
// Platform-specific branch/fork behavior is resolved before runner creation.
|
|
1036
|
+
const isThread = sessionKey.includes(":");
|
|
1037
|
+
const { sessionDir, contextFile, threadRootMessage } = sessionScope;
|
|
1038
|
+
const sessionManager = openManagedSession(contextFile, sessionDir, pathContext.runtimeWorkspaceRoot);
|
|
1039
|
+
const threadSessionName = buildThreadSessionName(threadRootMessage);
|
|
1040
|
+
if (isThread && threadSessionName && sessionManager.getSessionName() !== threadSessionName) {
|
|
1041
|
+
sessionManager.appendSessionInfo(threadSessionName);
|
|
1042
|
+
}
|
|
1043
|
+
const sessionUuid = extractSessionUuid(contextFile);
|
|
1044
|
+
const settingsManager = SettingsManager.inMemory();
|
|
1045
|
+
const { agent, session } = await createConfiguredAgentSession({
|
|
1046
|
+
conversationId,
|
|
1047
|
+
workspaceDir,
|
|
1048
|
+
runtimeWorkspaceRoot: pathContext.runtimeWorkspaceRoot,
|
|
1049
|
+
systemPrompt,
|
|
1050
|
+
model,
|
|
1051
|
+
thinkingLevel: agentConfig.thinkingLevel,
|
|
1052
|
+
tools,
|
|
1053
|
+
sessionManager,
|
|
1054
|
+
settingsManager,
|
|
1055
|
+
});
|
|
1056
|
+
// Mutable per-run state - event handler references this
|
|
1057
|
+
const runState = createRunState();
|
|
1058
|
+
attachSessionEventHandlers({ session, runState, model, agentConfig });
|
|
730
1059
|
return {
|
|
731
1060
|
async run(message, responseCtx, platform) {
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
if (executionResolver) {
|
|
737
|
-
executionResolver.refresh();
|
|
738
|
-
activeExecutor = await executionResolver.resolve({
|
|
739
|
-
platform: platform.name,
|
|
740
|
-
userId: message.userId,
|
|
741
|
-
});
|
|
742
|
-
workspacePath = getWorkspacePath();
|
|
743
|
-
}
|
|
744
|
-
// Sync messages from log.jsonl that arrived while we were offline or busy
|
|
745
|
-
// Exclude the current message (it will be added via prompt())
|
|
746
|
-
// Default sync range is 10 days (handled by syncLogToSessionManager)
|
|
747
|
-
// Thread filter ensures only messages from this session's thread are synced
|
|
748
|
-
const threadFilter = message.sessionKey.includes(":")
|
|
749
|
-
? { scope: "thread", rootTs, threadTs: message.threadTs }
|
|
750
|
-
: { scope: "top-level", rootTs };
|
|
751
|
-
const syncedCount = await syncLogToSessionManager(sessionManager, conversationDir, message.id, undefined, threadFilter);
|
|
752
|
-
if (syncedCount > 0) {
|
|
753
|
-
log.logInfo(`[${conversationId}] Synced ${syncedCount} messages from log.jsonl`);
|
|
754
|
-
}
|
|
755
|
-
// Reload messages from context.jsonl
|
|
756
|
-
// This picks up any messages synced above
|
|
757
|
-
const reloadedSession = sessionManager.buildSessionContext();
|
|
758
|
-
if (reloadedSession.messages.length > 0) {
|
|
759
|
-
agent.state.messages = reloadedSession.messages;
|
|
760
|
-
log.logInfo(`[${conversationId}] Reloaded ${reloadedSession.messages.length} messages from context`);
|
|
761
|
-
}
|
|
762
|
-
// Update system prompt with fresh memory, channel/user info, and skills
|
|
763
|
-
const memory = await getMemory(conversationDir);
|
|
764
|
-
const skills = loadMamaSkills(conversationDir, workspacePath);
|
|
765
|
-
const systemPrompt = buildSystemPrompt(workspacePath, conversationId, message.conversationKind, message.userId, memory, executor.getSandboxConfig(), platform, skills);
|
|
766
|
-
session.agent.state.systemPrompt = systemPrompt;
|
|
767
|
-
setEventContext({
|
|
768
|
-
platform: platform.name,
|
|
1061
|
+
const prepared = await prepareRunContext({
|
|
1062
|
+
message,
|
|
1063
|
+
responseCtx,
|
|
1064
|
+
platform,
|
|
769
1065
|
conversationId,
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
1066
|
+
conversationDir,
|
|
1067
|
+
sessionUuid,
|
|
1068
|
+
runState,
|
|
1069
|
+
executor,
|
|
1070
|
+
executionResolver,
|
|
1071
|
+
resolveExecutorForRun,
|
|
1072
|
+
getPathContext,
|
|
1073
|
+
sessionManager,
|
|
1074
|
+
session,
|
|
1075
|
+
agent,
|
|
1076
|
+
setEventContext,
|
|
1077
|
+
setUploadFunction,
|
|
1078
|
+
pathContext,
|
|
777
1079
|
});
|
|
778
|
-
|
|
779
|
-
runState.responseCtx = responseCtx;
|
|
780
|
-
runState.logCtx = {
|
|
781
|
-
conversationId: sessionConversation,
|
|
782
|
-
userName: message.userName,
|
|
783
|
-
conversationName: undefined,
|
|
784
|
-
sessionId: sessionUuid,
|
|
785
|
-
};
|
|
786
|
-
runState.pendingTools.clear();
|
|
787
|
-
runState.totalUsage = {
|
|
788
|
-
input: 0,
|
|
789
|
-
output: 0,
|
|
790
|
-
cacheRead: 0,
|
|
791
|
-
cacheWrite: 0,
|
|
792
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
793
|
-
};
|
|
794
|
-
runState.llmCallCount = 0;
|
|
795
|
-
runState.stopReason = "stop";
|
|
796
|
-
runState.errorMessage = undefined;
|
|
797
|
-
// Create queue for this run
|
|
798
|
-
let queueChain = Promise.resolve();
|
|
799
|
-
runState.queue = {
|
|
800
|
-
enqueue(fn, errorContext) {
|
|
801
|
-
queueChain = queueChain.then(async () => {
|
|
802
|
-
try {
|
|
803
|
-
await fn();
|
|
804
|
-
}
|
|
805
|
-
catch (err) {
|
|
806
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
807
|
-
log.logWarning(`API error (${errorContext})`, errMsg);
|
|
808
|
-
try {
|
|
809
|
-
// Split long error messages to avoid msg_too_long
|
|
810
|
-
const errParts = splitForSlack(`_Error: ${errMsg}_`);
|
|
811
|
-
for (const part of errParts) {
|
|
812
|
-
await responseCtx.respondInThread(part);
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
catch {
|
|
816
|
-
// Ignore
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
});
|
|
820
|
-
},
|
|
821
|
-
enqueueMessage(text, target, errorContext, _doLog = true) {
|
|
822
|
-
const parts = splitForSlack(text);
|
|
823
|
-
for (const part of parts) {
|
|
824
|
-
this.enqueue(() => target === "main" ? responseCtx.respond(part) : responseCtx.respondInThread(part), errorContext);
|
|
825
|
-
}
|
|
826
|
-
},
|
|
827
|
-
};
|
|
828
|
-
// Log context info
|
|
829
|
-
log.logInfo(`Context sizes - system: ${systemPrompt.length} chars, memory: ${memory.length} chars`);
|
|
830
|
-
log.logInfo(`Channels: ${platform.channels.length}, Users: ${platform.users.length}`);
|
|
831
|
-
// Build user message with timestamp and username prefix
|
|
832
|
-
// Format: "[YYYY-MM-DD HH:MM:SS+HH:MM] [username]: message" so LLM knows when and who
|
|
833
|
-
const now = new Date();
|
|
834
|
-
const pad = (n) => n.toString().padStart(2, "0");
|
|
835
|
-
const offset = -now.getTimezoneOffset();
|
|
836
|
-
const offsetSign = offset >= 0 ? "+" : "-";
|
|
837
|
-
const offsetHours = pad(Math.floor(Math.abs(offset) / 60));
|
|
838
|
-
const offsetMins = pad(Math.abs(offset) % 60);
|
|
839
|
-
const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}${offsetSign}${offsetHours}:${offsetMins}`;
|
|
840
|
-
const threadContext = message.threadTs ? ` [in-thread:${message.threadTs}]` : "";
|
|
841
|
-
let userMessage = `[${timestamp}] [${message.userName || "unknown"}]${threadContext}: ${message.text}`;
|
|
842
|
-
const imageAttachments = [];
|
|
843
|
-
const nonImagePaths = [];
|
|
844
|
-
for (const a of message.attachments || []) {
|
|
845
|
-
// a.localPath is the path relative to the workspace.
|
|
846
|
-
const fullPath = `${workspacePath}/${a.localPath}`;
|
|
847
|
-
const mimeType = getImageMimeType(a.localPath);
|
|
848
|
-
if (mimeType && existsSync(fullPath)) {
|
|
849
|
-
try {
|
|
850
|
-
imageAttachments.push({
|
|
851
|
-
type: "image",
|
|
852
|
-
mimeType,
|
|
853
|
-
data: readFileSync(fullPath).toString("base64"),
|
|
854
|
-
});
|
|
855
|
-
}
|
|
856
|
-
catch {
|
|
857
|
-
nonImagePaths.push(fullPath);
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
else {
|
|
861
|
-
nonImagePaths.push(fullPath);
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
if (nonImagePaths.length > 0) {
|
|
865
|
-
userMessage += `\n\n<slack_attachments>\n${nonImagePaths.join("\n")}\n</slack_attachments>`;
|
|
866
|
-
}
|
|
867
|
-
// Debug: write context to last_prompt.jsonl
|
|
868
|
-
const debugContext = {
|
|
869
|
-
systemPrompt,
|
|
870
|
-
messages: session.messages,
|
|
871
|
-
newUserMessage: userMessage,
|
|
872
|
-
imageAttachmentCount: imageAttachments.length,
|
|
873
|
-
};
|
|
874
|
-
await writeFile(join(conversationDir, "last_prompt.jsonl"), JSON.stringify(debugContext, null, 2));
|
|
1080
|
+
pathContext = prepared.pathContext;
|
|
875
1081
|
addLifecycleBreadcrumb("agent.prompt.sent", {
|
|
876
1082
|
provider: model.provider,
|
|
877
1083
|
model: agentConfig.model,
|
|
878
|
-
channel_id: sessionConversation,
|
|
1084
|
+
channel_id: prepared.sessionConversation,
|
|
879
1085
|
session_id: sessionUuid,
|
|
880
1086
|
attachment_count: message.attachments?.length ?? 0,
|
|
881
|
-
image_attachment_count: imageAttachments.length,
|
|
1087
|
+
image_attachment_count: prepared.imageAttachments.length,
|
|
882
1088
|
});
|
|
883
|
-
await session.prompt(userMessage, imageAttachments.length > 0 ? { images: imageAttachments } : undefined);
|
|
1089
|
+
await session.prompt(prepared.userMessage, prepared.imageAttachments.length > 0 ? { images: prepared.imageAttachments } : undefined);
|
|
884
1090
|
// Wait for queued messages
|
|
885
|
-
await
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
for (const part of errorParts) {
|
|
893
|
-
await responseCtx.respondInThread(part);
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
catch (err) {
|
|
897
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
898
|
-
log.logWarning("Failed to post error message", errMsg);
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
else {
|
|
902
|
-
// Final message update
|
|
903
|
-
const messages = session.messages;
|
|
904
|
-
const lastAssistant = messages.filter((m) => m.role === "assistant").pop();
|
|
905
|
-
const finalText = lastAssistant?.content
|
|
906
|
-
.filter((c) => c.type === "text")
|
|
907
|
-
.map((c) => c.text)
|
|
908
|
-
.join("\n") || "";
|
|
909
|
-
// Check for [SILENT] marker - delete message and thread instead of posting
|
|
910
|
-
if (finalText.trim() === "[SILENT]" || finalText.trim().startsWith("[SILENT]")) {
|
|
911
|
-
try {
|
|
912
|
-
await responseCtx.deleteResponse();
|
|
913
|
-
log.logInfo("Silent response - deleted message and thread");
|
|
914
|
-
}
|
|
915
|
-
catch (err) {
|
|
916
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
917
|
-
log.logWarning("Failed to delete message for silent response", errMsg);
|
|
918
|
-
}
|
|
1091
|
+
await prepared.runQueue.wait();
|
|
1092
|
+
const sessionViewTokenStore = sessionView?.tokenStore;
|
|
1093
|
+
const sessionViewPortalBaseUrl = sessionView?.portalBaseUrl;
|
|
1094
|
+
const createOverflowLink = sessionViewTokenStore && sessionViewPortalBaseUrl
|
|
1095
|
+
? () => {
|
|
1096
|
+
const token = sessionViewTokenStore.create(platform.name, message.userId, conversationId, message.sessionKey, contextFile, message.userName);
|
|
1097
|
+
return `${sessionViewPortalBaseUrl}/session?token=${token.token}`;
|
|
919
1098
|
}
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
.find((m) => m.role === "assistant" && m.stopReason !== "aborted");
|
|
941
|
-
const contextTokens = lastAssistantMessage
|
|
942
|
-
? lastAssistantMessage.usage.input +
|
|
943
|
-
lastAssistantMessage.usage.output +
|
|
944
|
-
lastAssistantMessage.usage.cacheRead +
|
|
945
|
-
lastAssistantMessage.usage.cacheWrite
|
|
946
|
-
: 0;
|
|
947
|
-
const contextWindow = model.contextWindow || 200000;
|
|
948
|
-
// Run-level Sentry metrics
|
|
949
|
-
const { totalUsage } = runState;
|
|
950
|
-
const runMetricAttributes = metricAttributes({
|
|
951
|
-
provider: model.provider,
|
|
952
|
-
model: agentConfig.model,
|
|
953
|
-
channel_id: sessionConversation,
|
|
954
|
-
session_id: sessionUuid,
|
|
955
|
-
stop_reason: runState.stopReason,
|
|
956
|
-
llm_calls: runState.llmCallCount,
|
|
957
|
-
});
|
|
958
|
-
Sentry.metrics.distribution("agent.run.tokens_in", totalUsage.input, {
|
|
959
|
-
attributes: runMetricAttributes,
|
|
960
|
-
});
|
|
961
|
-
Sentry.metrics.distribution("agent.run.tokens_out", totalUsage.output, {
|
|
962
|
-
attributes: runMetricAttributes,
|
|
963
|
-
});
|
|
964
|
-
Sentry.metrics.distribution("agent.run.cache_read", totalUsage.cacheRead, {
|
|
965
|
-
attributes: runMetricAttributes,
|
|
966
|
-
});
|
|
967
|
-
Sentry.metrics.distribution("agent.run.cache_write", totalUsage.cacheWrite, {
|
|
968
|
-
attributes: runMetricAttributes,
|
|
969
|
-
});
|
|
970
|
-
Sentry.metrics.distribution("agent.run.cost", totalUsage.cost.total, {
|
|
971
|
-
attributes: runMetricAttributes,
|
|
972
|
-
});
|
|
973
|
-
Sentry.metrics.gauge("agent.context.utilization", contextTokens / contextWindow, {
|
|
974
|
-
unit: "ratio",
|
|
975
|
-
attributes: runMetricAttributes,
|
|
976
|
-
});
|
|
977
|
-
const summary = log.logUsageSummary(runState.logCtx, runState.totalUsage, contextTokens, contextWindow);
|
|
978
|
-
// Split long summaries to avoid msg_too_long
|
|
979
|
-
const summaryParts = splitForSlack(summary);
|
|
980
|
-
for (const part of summaryParts) {
|
|
981
|
-
runState.queue.enqueue(() => responseCtx.respondInThread(part, { style: "muted" }), "usage summary");
|
|
982
|
-
}
|
|
983
|
-
await queueChain;
|
|
984
|
-
}
|
|
1099
|
+
: undefined;
|
|
1100
|
+
await finalizeRunResponse(responseCtx, session, runState, {
|
|
1101
|
+
triggerAttribution: prepared.triggerAttribution,
|
|
1102
|
+
createOverflowLink,
|
|
1103
|
+
platform: platform.name,
|
|
1104
|
+
model,
|
|
1105
|
+
sessionConversation: prepared.sessionConversation,
|
|
1106
|
+
sessionUuid,
|
|
1107
|
+
});
|
|
1108
|
+
await reportUsageSummary({
|
|
1109
|
+
session,
|
|
1110
|
+
runState,
|
|
1111
|
+
responseCtx,
|
|
1112
|
+
platform,
|
|
1113
|
+
model,
|
|
1114
|
+
agentConfig,
|
|
1115
|
+
sessionConversation: prepared.sessionConversation,
|
|
1116
|
+
sessionUuid,
|
|
1117
|
+
waitForQueue: () => prepared.runQueue.wait(),
|
|
1118
|
+
});
|
|
985
1119
|
// Clear run state
|
|
986
1120
|
runState.responseCtx = null;
|
|
987
1121
|
runState.logCtx = null;
|
|
@@ -1006,19 +1140,10 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
|
|
|
1006
1140
|
},
|
|
1007
1141
|
};
|
|
1008
1142
|
}
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
function
|
|
1013
|
-
|
|
1014
|
-
const prefix = `/workspace/${conversationId}/`;
|
|
1015
|
-
if (containerPath.startsWith(prefix)) {
|
|
1016
|
-
return join(conversationDir, containerPath.slice(prefix.length));
|
|
1017
|
-
}
|
|
1018
|
-
if (containerPath.startsWith("/workspace/")) {
|
|
1019
|
-
return join(conversationDir, "..", containerPath.slice("/workspace/".length));
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
return containerPath;
|
|
1143
|
+
export function translateRuntimePathToHost(runtimePath, pathContext) {
|
|
1144
|
+
return pathContext.runtimeToHostPath?.(runtimePath) ?? runtimePath;
|
|
1145
|
+
}
|
|
1146
|
+
export function buildInitialPathContextForTest(sandboxConfig, hostWorkspaceRoot) {
|
|
1147
|
+
return getUnresolvedSandboxPathContext(sandboxConfig, hostWorkspaceRoot);
|
|
1023
1148
|
}
|
|
1024
1149
|
//# sourceMappingURL=agent.js.map
|