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