@geminixiang/mama 0.2.0-beta.1 → 0.2.0-beta.3
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 +133 -78
- package/dist/adapter.d.ts +22 -10
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js.map +1 -1
- package/dist/adapters/discord/bot.d.ts +10 -7
- package/dist/adapters/discord/bot.d.ts.map +1 -1
- package/dist/adapters/discord/bot.js +228 -69
- package/dist/adapters/discord/bot.js.map +1 -1
- package/dist/adapters/discord/context.d.ts.map +1 -1
- package/dist/adapters/discord/context.js +92 -34
- package/dist/adapters/discord/context.js.map +1 -1
- package/dist/adapters/shared.d.ts +23 -0
- package/dist/adapters/shared.d.ts.map +1 -0
- package/dist/adapters/shared.js +57 -0
- package/dist/adapters/shared.js.map +1 -0
- package/dist/adapters/slack/bot.d.ts +19 -11
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +356 -96
- package/dist/adapters/slack/bot.js.map +1 -1
- package/dist/adapters/slack/branch-manager.d.ts +21 -0
- package/dist/adapters/slack/branch-manager.d.ts.map +1 -0
- package/dist/adapters/slack/branch-manager.js +96 -0
- package/dist/adapters/slack/branch-manager.js.map +1 -0
- package/dist/adapters/slack/context.d.ts.map +1 -1
- package/dist/adapters/slack/context.js +100 -67
- 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/telegram/bot.d.ts +4 -2
- package/dist/adapters/telegram/bot.d.ts.map +1 -1
- package/dist/adapters/telegram/bot.js +141 -74
- 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 +49 -109
- 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 +4 -11
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +116 -196
- package/dist/agent.js.map +1 -1
- package/dist/bindings.d.ts +1 -20
- package/dist/bindings.d.ts.map +1 -1
- package/dist/bindings.js +1 -21
- package/dist/bindings.js.map +1 -1
- package/dist/config.d.ts +9 -27
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +89 -63
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +13 -3
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +102 -18
- package/dist/context.js.map +1 -1
- package/dist/events.d.ts +18 -6
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +86 -35
- package/dist/events.js.map +1 -1
- package/dist/execution-resolver.d.ts.map +1 -1
- package/dist/execution-resolver.js +1 -3
- package/dist/execution-resolver.js.map +1 -1
- package/dist/instrument.d.ts.map +1 -1
- package/dist/instrument.js +5 -11
- package/dist/instrument.js.map +1 -1
- package/dist/{login.d.ts → login/index.d.ts} +2 -2
- package/dist/login/index.d.ts.map +1 -0
- package/dist/{login.js → login/index.js} +2 -2
- package/dist/login/index.js.map +1 -0
- package/dist/{link-server.d.ts → login/portal.d.ts} +6 -4
- package/dist/login/portal.d.ts.map +1 -0
- package/dist/login/portal.js +1453 -0
- package/dist/login/portal.js.map +1 -0
- package/dist/{link-token.d.ts → login/session.d.ts} +1 -1
- 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 +175 -119
- package/dist/main.js.map +1 -1
- package/dist/provisioner.d.ts +17 -43
- package/dist/provisioner.d.ts.map +1 -1
- package/dist/provisioner.js +84 -50
- package/dist/provisioner.js.map +1 -1
- package/dist/sandbox/host.d.ts +0 -2
- package/dist/sandbox/host.d.ts.map +1 -1
- package/dist/sandbox/host.js +1 -5
- package/dist/sandbox/host.js.map +1 -1
- package/dist/sentry.d.ts.map +1 -1
- package/dist/sentry.js +2 -0
- 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 +27 -1
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +162 -9
- 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 +9 -0
- package/dist/session-view/portal.d.ts.map +1 -0
- package/dist/session-view/portal.js +766 -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 +380 -0
- package/dist/session-view/service.js.map +1 -0
- package/dist/session-view/store.d.ts +16 -0
- package/dist/session-view/store.d.ts.map +1 -0
- package/dist/session-view/store.js +38 -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 +15 -35
- package/dist/store.js.map +1 -1
- package/dist/tools/event.d.ts +3 -0
- package/dist/tools/event.d.ts.map +1 -1
- package/dist/tools/event.js +27 -8
- package/dist/tools/event.js.map +1 -1
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +2 -2
- package/dist/tools/index.js.map +1 -1
- package/dist/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 -2
- package/dist/vault-routing.d.ts.map +1 -1
- package/dist/vault-routing.js +1 -7
- package/dist/vault-routing.js.map +1 -1
- package/package.json +1 -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
|
@@ -11,7 +11,7 @@ import { ActorExecutionResolver } from "./execution-resolver.js";
|
|
|
11
11
|
import * as log from "./log.js";
|
|
12
12
|
import { createExecutor } from "./sandbox.js";
|
|
13
13
|
import { addLifecycleBreadcrumb, metricAttributes } from "./sentry.js";
|
|
14
|
-
import {
|
|
14
|
+
import { extractSessionSuffix, extractSessionUuid, openManagedSession, } from "./session-store.js";
|
|
15
15
|
import { createMamaTools } from "./tools/index.js";
|
|
16
16
|
import * as Sentry from "@sentry/node";
|
|
17
17
|
const IMAGE_MIME_TYPES = {
|
|
@@ -24,6 +24,13 @@ const IMAGE_MIME_TYPES = {
|
|
|
24
24
|
function getImageMimeType(filename) {
|
|
25
25
|
return IMAGE_MIME_TYPES[filename.toLowerCase().split(".").pop() || ""];
|
|
26
26
|
}
|
|
27
|
+
function buildThreadSessionName(message) {
|
|
28
|
+
const text = message?.text?.trim();
|
|
29
|
+
if (!text)
|
|
30
|
+
return undefined;
|
|
31
|
+
const userLabel = message?.userName || message?.user || "unknown";
|
|
32
|
+
return `[${userLabel}]: ${text}`;
|
|
33
|
+
}
|
|
27
34
|
async function getMemory(conversationDir) {
|
|
28
35
|
const parts = [];
|
|
29
36
|
// Read workspace-level memory (shared across all conversations)
|
|
@@ -87,11 +94,12 @@ function loadMamaSkills(conversationDir, workspacePath) {
|
|
|
87
94
|
}
|
|
88
95
|
return Array.from(skillMap.values());
|
|
89
96
|
}
|
|
90
|
-
function buildSystemPrompt(workspacePath, conversationId, currentUserId, memory, sandboxConfig, platform, skills) {
|
|
97
|
+
function buildSystemPrompt(workspacePath, conversationId, conversationKind, currentUserId, memory, sandboxConfig, platform, skills) {
|
|
91
98
|
const conversationPath = `${workspacePath}/${conversationId}`;
|
|
92
99
|
const isContainer = sandboxConfig.type === "container" || sandboxConfig.type === "image";
|
|
100
|
+
const isImageSandbox = sandboxConfig.type === "image";
|
|
93
101
|
const isFirecracker = sandboxConfig.type === "firecracker";
|
|
94
|
-
// Format
|
|
102
|
+
// Format channel mappings
|
|
95
103
|
const channelMappings = platform.channels.length > 0
|
|
96
104
|
? platform.channels.map((c) => `${c.id}\t#${c.name}`).join("\n")
|
|
97
105
|
: "(no channels loaded)";
|
|
@@ -99,17 +107,22 @@ function buildSystemPrompt(workspacePath, conversationId, currentUserId, memory,
|
|
|
99
107
|
const userMappings = platform.users.length > 0
|
|
100
108
|
? platform.users.map((u) => `${u.id}\t@${u.userName}\t${u.displayName}`).join("\n")
|
|
101
109
|
: "(no users loaded)";
|
|
102
|
-
const envDescription =
|
|
103
|
-
? `You are running inside a
|
|
110
|
+
const envDescription = isImageSandbox
|
|
111
|
+
? `You are running inside a managed per-user container.
|
|
112
|
+
- Bash working directory: / (use cd or absolute paths)
|
|
113
|
+
- Install tools with the image's package manager
|
|
114
|
+
- Your changes persist for this user's container until it is recreated`
|
|
115
|
+
: isContainer
|
|
116
|
+
? `You are running inside a shared container.
|
|
104
117
|
- Bash working directory: / (use cd or absolute paths)
|
|
105
|
-
- Install tools with
|
|
118
|
+
- Install tools with the container's package manager
|
|
106
119
|
- Your changes persist across sessions`
|
|
107
|
-
|
|
108
|
-
|
|
120
|
+
: isFirecracker
|
|
121
|
+
? `You are running inside a Firecracker microVM.
|
|
109
122
|
- Bash working directory: / (use cd or absolute paths)
|
|
110
123
|
- Install tools with: apt-get install <package> (Debian-based)
|
|
111
124
|
- Your changes persist across sessions`
|
|
112
|
-
|
|
125
|
+
: `You are running directly on the host machine.
|
|
113
126
|
- Bash working directory: ${process.cwd()}
|
|
114
127
|
- Be careful with system modifications`;
|
|
115
128
|
return `You are mama, a ${platform.name} bot assistant. Be concise. No emojis.
|
|
@@ -117,8 +130,11 @@ function buildSystemPrompt(workspacePath, conversationId, currentUserId, memory,
|
|
|
117
130
|
## Context
|
|
118
131
|
- For current date/time, use: date
|
|
119
132
|
- 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
|
-
-
|
|
133
|
+
- For older human-readable history beyond your context, search \`log.jsonl\` (contains user messages and your final responses, but not tool results).
|
|
134
|
+
- Structured session history with tool results lives in \`${conversationPath}/sessions/\`.
|
|
135
|
+
- The active top-level session is selected by \`${conversationPath}/sessions/current\`, which points to a timestamped \`.jsonl\` file in the same directory.
|
|
136
|
+
- Scoped/thread sessions use fixed files at \`${conversationPath}/sessions/<scope_id>.jsonl\` (for example \`${conversationPath}/sessions/1777386320.800769.jsonl\`).
|
|
137
|
+
- 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.
|
|
122
138
|
|
|
123
139
|
${platform.formattingGuide}
|
|
124
140
|
|
|
@@ -134,11 +150,15 @@ ${envDescription}
|
|
|
134
150
|
|
|
135
151
|
## Workspace Layout
|
|
136
152
|
${workspacePath}/
|
|
137
|
-
├── MEMORY.md # Global memory (all
|
|
153
|
+
├── MEMORY.md # Global memory (all conversations)
|
|
138
154
|
├── skills/ # Global CLI tools you create
|
|
139
155
|
└── ${conversationId}/ # This conversation
|
|
140
156
|
├── MEMORY.md # Conversation-specific memory
|
|
141
|
-
├── log.jsonl #
|
|
157
|
+
├── log.jsonl # Human-readable message history (no tool results)
|
|
158
|
+
├── sessions/ # Structured session history used for context reconstruction
|
|
159
|
+
│ ├── current # Active top-level session pointer
|
|
160
|
+
│ ├── <timestamp>_<id>.jsonl # Top-level session files
|
|
161
|
+
│ └── <scope_id>.jsonl # Scoped thread/reply session files
|
|
142
162
|
├── attachments/ # User-shared files
|
|
143
163
|
├── scratch/ # Your working directory
|
|
144
164
|
└── skills/ # Conversation-specific tools
|
|
@@ -174,21 +194,19 @@ You can schedule events that wake you up at specific times or when external thin
|
|
|
174
194
|
|
|
175
195
|
**Immediate** - Triggers as soon as harness sees the file. Use in scripts/webhooks to signal external events.
|
|
176
196
|
\`\`\`json
|
|
177
|
-
{"type": "immediate", "platform": "${platform.name}", "
|
|
197
|
+
{"type": "immediate", "platform": "${platform.name}", "conversationId": "${conversationId}", "conversationKind": "${conversationKind}", "userId": "${currentUserId ?? "<requester userId>"}", "text": "New GitHub issue opened"}
|
|
178
198
|
\`\`\`
|
|
179
199
|
|
|
180
200
|
**One-shot** - Triggers once at a specific time. Use for reminders.
|
|
181
201
|
\`\`\`json
|
|
182
|
-
{"type": "one-shot", "platform": "${platform.name}", "
|
|
202
|
+
{"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
203
|
\`\`\`
|
|
184
204
|
|
|
185
205
|
**Periodic** - Triggers on a cron schedule. Use for recurring tasks.
|
|
186
206
|
\`\`\`json
|
|
187
|
-
{"type": "periodic", "platform": "${platform.name}", "
|
|
207
|
+
{"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
208
|
\`\`\`
|
|
189
209
|
|
|
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
210
|
### Cron Format
|
|
193
211
|
\`minute hour day-of-month month day-of-week\`
|
|
194
212
|
- \`0 9 * * *\` = daily at 9:00
|
|
@@ -199,23 +217,18 @@ Set \`userId\` to the platform userId of whoever asked for the event (look it up
|
|
|
199
217
|
### Timezones
|
|
200
218
|
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
219
|
|
|
202
|
-
### Platform Routing
|
|
203
|
-
Set \`platform\` to the target bot platform (\`${platform.name}\` for this conversation).
|
|
220
|
+
### Platform and Credential Routing
|
|
221
|
+
Set \`platform\` to the target bot platform (\`${platform.name}\` for this conversation). Include it explicitly to avoid ambiguity.
|
|
204
222
|
|
|
205
|
-
|
|
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.
|
|
223
|
+
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.
|
|
208
224
|
|
|
209
|
-
|
|
210
|
-
- \`platform\`: \`${platform.name}\`
|
|
211
|
-
- \`channelId\`: \`${conversationId}\`
|
|
212
|
-
- \`userId\`: \`${currentUserId ?? "unknown"}\`
|
|
225
|
+
Prefer the \`event\` tool over manually writing JSON files; it fills \`platform\`, \`conversationId\`, \`conversationKind\`, and \`userId\` for the current conversation automatically.
|
|
213
226
|
|
|
214
|
-
|
|
227
|
+
### Creating Events
|
|
215
228
|
Use unique filenames to avoid overwriting existing events. Include a timestamp or random suffix:
|
|
216
229
|
\`\`\`bash
|
|
217
230
|
cat > ${workspacePath}/events/dentist-reminder-$(date +%s).json << 'EOF'
|
|
218
|
-
{"type": "one-shot", "platform": "${platform.name}", "
|
|
231
|
+
{"type": "one-shot", "platform": "${platform.name}", "conversationId": "${conversationId}", "conversationKind": "${conversationKind}", "userId": "${currentUserId ?? "<requester userId>"}", "text": "Dentist tomorrow", "at": "2025-12-14T09:00:00+01:00"}
|
|
219
232
|
EOF
|
|
220
233
|
\`\`\`
|
|
221
234
|
Or check if file exists first before creating.
|
|
@@ -252,7 +265,7 @@ ${memory}
|
|
|
252
265
|
|
|
253
266
|
## System Configuration Log
|
|
254
267
|
Maintain ${workspacePath}/SYSTEM.md to log all environment modifications:
|
|
255
|
-
- Installed packages (
|
|
268
|
+
- Installed packages (apt install, npm install, uv pip install)
|
|
256
269
|
- Environment variables set
|
|
257
270
|
- Config files modified (~/.gitconfig, cron jobs, etc.)
|
|
258
271
|
- Skill dependencies installed
|
|
@@ -262,7 +275,8 @@ Update this file whenever you modify the environment. On fresh container, read i
|
|
|
262
275
|
## Log Queries (for older history)
|
|
263
276
|
Format: \`{"date":"...","ts":"...","user":"...","userName":"...","text":"...","isBot":false}\`
|
|
264
277
|
The log contains user messages and your final responses (not tool calls/results).
|
|
265
|
-
|
|
278
|
+
Use \`log.jsonl\` for quick grep-style history. Use \`${conversationPath}/sessions/\` when you need structured turns, tool outputs, or branch lineage.
|
|
279
|
+
${isContainer ? "Install jq: apt-get install jq" : ""}
|
|
266
280
|
${isFirecracker ? "Install jq: apt-get install jq" : ""}
|
|
267
281
|
|
|
268
282
|
\`\`\`bash
|
|
@@ -274,6 +288,10 @@ grep -i "topic" log.jsonl | jq -c '{date: .date[0:19], user: (.userName // .user
|
|
|
274
288
|
|
|
275
289
|
# Messages from specific user
|
|
276
290
|
grep '"userName":"mario"' log.jsonl | tail -20 | jq -c '{date: .date[0:19], text}'
|
|
291
|
+
|
|
292
|
+
# Inspect top-level session pointer and available session files
|
|
293
|
+
cat sessions/current
|
|
294
|
+
ls -1 sessions/
|
|
277
295
|
\`\`\`
|
|
278
296
|
|
|
279
297
|
## Tools
|
|
@@ -291,6 +309,12 @@ function truncate(text, maxLen) {
|
|
|
291
309
|
return text;
|
|
292
310
|
return `${text.substring(0, maxLen - 3)}...`;
|
|
293
311
|
}
|
|
312
|
+
// Tools whose output is interesting in the structured session log but too noisy
|
|
313
|
+
// to surface as a per-tool diagnostic to the user.
|
|
314
|
+
const QUIET_TOOLS = new Set(["read", "write", "edit"]);
|
|
315
|
+
// Cap raw tool output before handing it to adapters. Bash output can be MB; without
|
|
316
|
+
// this each adapter's splitter would fan it out into many sequential platform posts.
|
|
317
|
+
const TOOL_RESULT_DIAGNOSTIC_CAP = 8000;
|
|
294
318
|
function extractToolResultText(result) {
|
|
295
319
|
if (typeof result === "string") {
|
|
296
320
|
return result;
|
|
@@ -312,55 +336,29 @@ function extractToolResultText(result) {
|
|
|
312
336
|
}
|
|
313
337
|
return JSON.stringify(result);
|
|
314
338
|
}
|
|
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");
|
|
341
|
-
}
|
|
342
339
|
// ============================================================================
|
|
343
340
|
// Agent runner
|
|
344
341
|
// ============================================================================
|
|
345
342
|
/**
|
|
346
|
-
* Create a new AgentRunner for a
|
|
343
|
+
* Create a new AgentRunner for a channel.
|
|
347
344
|
* Sets up the session and subscribes to events once.
|
|
348
345
|
*
|
|
349
|
-
* Runner caching is handled by the caller (
|
|
346
|
+
* Runner caching is handled by the caller (channelStates in main.ts).
|
|
350
347
|
* This is a stateless factory function.
|
|
351
348
|
*/
|
|
352
|
-
export async function createRunner(sandboxConfig, sessionKey, conversationId, conversationDir, workspaceDir, vaultManager, bindingStore, provisioner
|
|
353
|
-
const agentConfig = loadAgentConfig(
|
|
349
|
+
export async function createRunner(sandboxConfig, sessionKey, conversationId, conversationDir, workspaceDir, sessionScope, vaultManager, bindingStore, provisioner) {
|
|
350
|
+
const agentConfig = loadAgentConfig(workspaceDir);
|
|
354
351
|
// Initialize logger with settings from config
|
|
355
352
|
log.initLogger({
|
|
356
353
|
logFormat: agentConfig.logFormat,
|
|
357
354
|
logLevel: agentConfig.logLevel,
|
|
358
355
|
});
|
|
359
356
|
const executionResolver = vaultManager &&
|
|
357
|
+
sandboxConfig.type !== "host" &&
|
|
360
358
|
(vaultManager.isEnabled() ||
|
|
361
359
|
!!bindingStore ||
|
|
362
|
-
sandboxConfig.type === "
|
|
363
|
-
sandboxConfig.type === "
|
|
360
|
+
sandboxConfig.type === "container" ||
|
|
361
|
+
sandboxConfig.type === "image")
|
|
364
362
|
? new ActorExecutionResolver(sandboxConfig, vaultManager, bindingStore, provisioner)
|
|
365
363
|
: undefined;
|
|
366
364
|
let activeExecutor = executionResolver !== undefined
|
|
@@ -378,7 +376,6 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
|
|
|
378
376
|
},
|
|
379
377
|
};
|
|
380
378
|
const workspaceBase = conversationDir.replace(`/${conversationId}`, "");
|
|
381
|
-
// Compute workspace path from the current executor. This may change per run.
|
|
382
379
|
const getWorkspacePath = () => executor.getWorkspacePath(workspaceBase);
|
|
383
380
|
let workspacePath = getWorkspacePath();
|
|
384
381
|
// Create tools (per-runner, with per-runner upload function setter)
|
|
@@ -392,52 +389,24 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
|
|
|
392
389
|
const memory = await getMemory(conversationDir);
|
|
393
390
|
const skills = loadMamaSkills(conversationDir, workspacePath);
|
|
394
391
|
const emptyPlatform = {
|
|
395
|
-
name: "
|
|
392
|
+
name: "chat",
|
|
396
393
|
formattingGuide: "",
|
|
397
394
|
channels: [],
|
|
398
395
|
users: [],
|
|
399
396
|
};
|
|
400
|
-
const systemPrompt = buildSystemPrompt(workspacePath, conversationId, undefined, memory, sandboxConfig, emptyPlatform, skills);
|
|
401
|
-
// Create session manager and settings manager.
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
const sessionDir = getChannelSessionDir(conversationDir);
|
|
397
|
+
const systemPrompt = buildSystemPrompt(workspacePath, conversationId, "shared", undefined, memory, sandboxConfig, emptyPlatform, skills);
|
|
398
|
+
// Create session manager and settings manager. Top-level/private sessions
|
|
399
|
+
// use the conversation's current pointer; scoped sessions use fixed files.
|
|
400
|
+
// Platform-specific branch/fork behavior is resolved before runner creation.
|
|
405
401
|
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
402
|
const rootTs = extractSessionSuffix(sessionKey);
|
|
403
|
+
const { sessionDir, contextFile, threadRootMessage } = sessionScope;
|
|
404
|
+
const sessionManager = openManagedSession(contextFile, sessionDir, conversationDir);
|
|
405
|
+
const threadSessionName = buildThreadSessionName(threadRootMessage);
|
|
406
|
+
if (isThread && threadSessionName && sessionManager.getSessionName() !== threadSessionName) {
|
|
407
|
+
sessionManager.appendSessionInfo(threadSessionName);
|
|
408
|
+
}
|
|
409
|
+
const sessionUuid = extractSessionUuid(contextFile);
|
|
441
410
|
const settingsManager = createMamaSettingsManager(join(conversationDir, ".."));
|
|
442
411
|
// Create AuthStorage and ModelRegistry
|
|
443
412
|
// Auth stored outside workspace so agent can't access it
|
|
@@ -463,7 +432,7 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
|
|
|
463
432
|
const loadedSession = sessionManager.buildSessionContext();
|
|
464
433
|
if (loadedSession.messages.length > 0) {
|
|
465
434
|
agent.state.messages = loadedSession.messages;
|
|
466
|
-
log.logInfo(`[${conversationId}] Loaded ${loadedSession.messages.length} messages from
|
|
435
|
+
log.logInfo(`[${conversationId}] Loaded ${loadedSession.messages.length} messages from context.jsonl`);
|
|
467
436
|
}
|
|
468
437
|
// Load extensions, skills, prompts, themes via DefaultResourceLoader
|
|
469
438
|
// This reads ~/.pi/agent/settings.json (packages, extensions enable/disable)
|
|
@@ -535,8 +504,6 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
|
|
|
535
504
|
...baseAttrs,
|
|
536
505
|
});
|
|
537
506
|
log.logToolStart(logCtx, agentEvent.toolName, label, agentEvent.args);
|
|
538
|
-
// Tool labels are omitted from the main message to reduce Slack noise.
|
|
539
|
-
// Tool execution details are still posted to the thread (see tool_execution_end).
|
|
540
507
|
}
|
|
541
508
|
else if (event.type === "tool_execution_end") {
|
|
542
509
|
const agentEvent = event;
|
|
@@ -570,24 +537,16 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
|
|
|
570
537
|
else {
|
|
571
538
|
log.logToolSuccess(logCtx, agentEvent.toolName, durationMs, resultStr);
|
|
572
539
|
}
|
|
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);
|
|
540
|
+
if (!QUIET_TOOLS.has(agentEvent.toolName)) {
|
|
541
|
+
const toolResult = {
|
|
542
|
+
toolName: agentEvent.toolName,
|
|
543
|
+
label: pending?.args ? pending.args.label : undefined,
|
|
544
|
+
args: pending?.args,
|
|
545
|
+
result: truncate(resultStr, TOOL_RESULT_DIAGNOSTIC_CAP),
|
|
546
|
+
isError: agentEvent.isError,
|
|
547
|
+
durationMs,
|
|
548
|
+
};
|
|
549
|
+
queue.enqueue(() => responseCtx.respondToolResult(toolResult), "tool result diagnostic");
|
|
591
550
|
}
|
|
592
551
|
if (agentEvent.isError) {
|
|
593
552
|
queue.enqueue(() => responseCtx.respond(`_Error: ${truncate(resultStr, 200)}_`), "tool error");
|
|
@@ -679,16 +638,12 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
|
|
|
679
638
|
const text = textParts.join("\n");
|
|
680
639
|
for (const thinking of thinkingParts) {
|
|
681
640
|
log.logThinking(logCtx, thinking);
|
|
682
|
-
queue.
|
|
683
|
-
queue.
|
|
641
|
+
queue.enqueue(() => responseCtx.respond(`_${thinking}_`), "thinking main");
|
|
642
|
+
queue.enqueue(() => responseCtx.respondDiagnostic(`_${thinking}_`), "thinking diagnostic");
|
|
684
643
|
}
|
|
685
644
|
if (text.trim()) {
|
|
686
645
|
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
|
-
}
|
|
646
|
+
queue.enqueue(() => responseCtx.respond(text), "response main");
|
|
692
647
|
}
|
|
693
648
|
}
|
|
694
649
|
}
|
|
@@ -711,33 +666,12 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
|
|
|
711
666
|
queue.enqueue(() => responseCtx.respond(`_Retrying (${retryEvent.attempt}/${retryEvent.maxAttempts})..._`), "retry");
|
|
712
667
|
}
|
|
713
668
|
});
|
|
714
|
-
// Message limit constant
|
|
715
|
-
const SLACK_MAX_LENGTH = 40000;
|
|
716
|
-
const splitForSlack = (text) => {
|
|
717
|
-
if (text.length <= SLACK_MAX_LENGTH)
|
|
718
|
-
return [text];
|
|
719
|
-
const parts = [];
|
|
720
|
-
let remaining = text;
|
|
721
|
-
let partNum = 1;
|
|
722
|
-
while (remaining.length > 0) {
|
|
723
|
-
const chunk = remaining.substring(0, SLACK_MAX_LENGTH - 50);
|
|
724
|
-
remaining = remaining.substring(SLACK_MAX_LENGTH - 50);
|
|
725
|
-
const suffix = remaining.length > 0 ? `\n_(continued ${partNum}...)_` : "";
|
|
726
|
-
parts.push(chunk + suffix);
|
|
727
|
-
partNum++;
|
|
728
|
-
}
|
|
729
|
-
return parts;
|
|
730
|
-
};
|
|
731
669
|
return {
|
|
732
670
|
async run(message, responseCtx, platform) {
|
|
733
671
|
// Extract conversationId from sessionKey (format: "conversationId:rootTs" or just "conversationId")
|
|
734
|
-
const
|
|
735
|
-
// Ensure
|
|
672
|
+
const sessionConversation = message.sessionKey.split(":")[0];
|
|
673
|
+
// Ensure conversation directory exists
|
|
736
674
|
await mkdir(conversationDir, { recursive: true });
|
|
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
675
|
if (executionResolver) {
|
|
742
676
|
executionResolver.refresh();
|
|
743
677
|
activeExecutor = await executionResolver.resolve({
|
|
@@ -757,35 +691,38 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
|
|
|
757
691
|
if (syncedCount > 0) {
|
|
758
692
|
log.logInfo(`[${conversationId}] Synced ${syncedCount} messages from log.jsonl`);
|
|
759
693
|
}
|
|
760
|
-
// Reload messages from
|
|
761
|
-
// This picks up any messages synced above
|
|
694
|
+
// Reload messages from context.jsonl
|
|
695
|
+
// This picks up any messages synced above
|
|
762
696
|
const reloadedSession = sessionManager.buildSessionContext();
|
|
763
697
|
if (reloadedSession.messages.length > 0) {
|
|
764
698
|
agent.state.messages = reloadedSession.messages;
|
|
765
699
|
log.logInfo(`[${conversationId}] Reloaded ${reloadedSession.messages.length} messages from context`);
|
|
766
700
|
}
|
|
767
701
|
// 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
702
|
const memory = await getMemory(conversationDir);
|
|
771
703
|
const skills = loadMamaSkills(conversationDir, workspacePath);
|
|
772
|
-
const
|
|
773
|
-
const systemPrompt = buildSystemPrompt(workspacePath, conversationId, message.userId, memory, actualSandboxConfig, platform, skills);
|
|
704
|
+
const systemPrompt = buildSystemPrompt(workspacePath, conversationId, message.conversationKind, message.userId, memory, executor.getSandboxConfig(), platform, skills);
|
|
774
705
|
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
706
|
setEventContext({
|
|
781
707
|
platform: platform.name,
|
|
782
708
|
conversationId,
|
|
709
|
+
conversationKind: message.conversationKind,
|
|
783
710
|
userId: message.userId,
|
|
711
|
+
sessionKey: message.sessionKey,
|
|
712
|
+
// For Slack scheduled events, preserve thread targeting only when the
|
|
713
|
+
// request was created inside an existing thread. Top-level reminders
|
|
714
|
+
// should come back as top-level messages.
|
|
715
|
+
threadTs: message.threadTs,
|
|
716
|
+
});
|
|
717
|
+
// Set up file upload function
|
|
718
|
+
setUploadFunction(async (filePath, title) => {
|
|
719
|
+
const hostPath = translateToHostPath(filePath, conversationDir, workspacePath, conversationId);
|
|
720
|
+
await responseCtx.uploadFile(hostPath, title);
|
|
784
721
|
});
|
|
785
722
|
// Reset per-run state
|
|
786
723
|
runState.responseCtx = responseCtx;
|
|
787
724
|
runState.logCtx = {
|
|
788
|
-
conversationId:
|
|
725
|
+
conversationId: sessionConversation,
|
|
789
726
|
userName: message.userName,
|
|
790
727
|
conversationName: undefined,
|
|
791
728
|
sessionId: sessionUuid,
|
|
@@ -813,11 +750,7 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
|
|
|
813
750
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
814
751
|
log.logWarning(`API error (${errorContext})`, errMsg);
|
|
815
752
|
try {
|
|
816
|
-
|
|
817
|
-
const errParts = splitForSlack(`_Error: ${errMsg}_`);
|
|
818
|
-
for (const part of errParts) {
|
|
819
|
-
await responseCtx.respondInThread(part);
|
|
820
|
-
}
|
|
753
|
+
await responseCtx.respondDiagnostic(`Error: ${errMsg}`, { style: "error" });
|
|
821
754
|
}
|
|
822
755
|
catch {
|
|
823
756
|
// Ignore
|
|
@@ -825,12 +758,6 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
|
|
|
825
758
|
}
|
|
826
759
|
});
|
|
827
760
|
},
|
|
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
761
|
};
|
|
835
762
|
// Log context info
|
|
836
763
|
log.logInfo(`Context sizes - system: ${systemPrompt.length} chars, memory: ${memory.length} chars`);
|
|
@@ -849,7 +776,7 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
|
|
|
849
776
|
const imageAttachments = [];
|
|
850
777
|
const nonImagePaths = [];
|
|
851
778
|
for (const a of message.attachments || []) {
|
|
852
|
-
// a.localPath is the path relative to the workspace
|
|
779
|
+
// a.localPath is the path relative to the workspace.
|
|
853
780
|
const fullPath = `${workspacePath}/${a.localPath}`;
|
|
854
781
|
const mimeType = getImageMimeType(a.localPath);
|
|
855
782
|
if (mimeType && existsSync(fullPath)) {
|
|
@@ -882,7 +809,7 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
|
|
|
882
809
|
addLifecycleBreadcrumb("agent.prompt.sent", {
|
|
883
810
|
provider: model.provider,
|
|
884
811
|
model: agentConfig.model,
|
|
885
|
-
channel_id:
|
|
812
|
+
channel_id: sessionConversation,
|
|
886
813
|
session_id: sessionUuid,
|
|
887
814
|
attachment_count: message.attachments?.length ?? 0,
|
|
888
815
|
image_attachment_count: imageAttachments.length,
|
|
@@ -894,11 +821,9 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
|
|
|
894
821
|
if (runState.stopReason === "error" && runState.errorMessage) {
|
|
895
822
|
try {
|
|
896
823
|
await responseCtx.replaceResponse("_Sorry, something went wrong_");
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
await responseCtx.respondInThread(part);
|
|
901
|
-
}
|
|
824
|
+
await responseCtx.respondDiagnostic(`Error: ${runState.errorMessage}`, {
|
|
825
|
+
style: "error",
|
|
826
|
+
});
|
|
902
827
|
}
|
|
903
828
|
catch (err) {
|
|
904
829
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -926,10 +851,7 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
|
|
|
926
851
|
}
|
|
927
852
|
else if (finalText.trim()) {
|
|
928
853
|
try {
|
|
929
|
-
|
|
930
|
-
? `${finalText.substring(0, SLACK_MAX_LENGTH - 50)}\n\n_(see thread for full response)_`
|
|
931
|
-
: finalText;
|
|
932
|
-
await responseCtx.replaceResponse(mainText);
|
|
854
|
+
await responseCtx.replaceResponse(finalText);
|
|
933
855
|
}
|
|
934
856
|
catch (err) {
|
|
935
857
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -957,7 +879,7 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
|
|
|
957
879
|
const runMetricAttributes = metricAttributes({
|
|
958
880
|
provider: model.provider,
|
|
959
881
|
model: agentConfig.model,
|
|
960
|
-
channel_id:
|
|
882
|
+
channel_id: sessionConversation,
|
|
961
883
|
session_id: sessionUuid,
|
|
962
884
|
stop_reason: runState.stopReason,
|
|
963
885
|
llm_calls: runState.llmCallCount,
|
|
@@ -982,12 +904,10 @@ export async function createRunner(sandboxConfig, sessionKey, conversationId, co
|
|
|
982
904
|
attributes: runMetricAttributes,
|
|
983
905
|
});
|
|
984
906
|
const summary = log.logUsageSummary(runState.logCtx, runState.totalUsage, contextTokens, contextWindow);
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
runState.queue.enqueue(() => responseCtx.respondInThread(part, { style: "muted" }), "usage summary");
|
|
907
|
+
if (platform.diagnostics?.showUsageSummary === true) {
|
|
908
|
+
runState.queue.enqueue(() => responseCtx.respondDiagnostic(summary, { style: "muted" }), "usage summary");
|
|
909
|
+
await queueChain;
|
|
989
910
|
}
|
|
990
|
-
await queueChain;
|
|
991
911
|
}
|
|
992
912
|
// Clear run state
|
|
993
913
|
runState.responseCtx = null;
|