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