amalgm 0.0.0 → 0.0.1
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 +37 -1
- package/bin/amalgm.js +8 -2
- package/lib/auth-store.js +223 -0
- package/lib/cli.js +1000 -0
- package/lib/paths.js +30 -0
- package/lib/supervisor.js +467 -0
- package/lib/tunnel-chat.js +328 -0
- package/lib/tunnel-events.js +499 -0
- package/package.json +29 -3
- package/runtime/README.md +4 -0
- package/runtime/lib/chatInput.js +306 -0
- package/runtime/lib/harnesses.js +988 -0
- package/runtime/lib/local/amalgmStore.js +128 -0
- package/runtime/lib/local/credentialResolver.js +425 -0
- package/runtime/lib/mcpApps/registry.js +619 -0
- package/runtime/package.json +5 -0
- package/runtime/scripts/amalgm-mcp/agents/rest.js +165 -0
- package/runtime/scripts/amalgm-mcp/agents/store.js +153 -0
- package/runtime/scripts/amalgm-mcp/agents/talk.js +1156 -0
- package/runtime/scripts/amalgm-mcp/agents/tools.js +210 -0
- package/runtime/scripts/amalgm-mcp/artifacts/advertise.js +132 -0
- package/runtime/scripts/amalgm-mcp/artifacts/rest.js +103 -0
- package/runtime/scripts/amalgm-mcp/artifacts/store.js +141 -0
- package/runtime/scripts/amalgm-mcp/artifacts/supervisor.js +402 -0
- package/runtime/scripts/amalgm-mcp/artifacts/tools.js +176 -0
- package/runtime/scripts/amalgm-mcp/browser/page.js +637 -0
- package/runtime/scripts/amalgm-mcp/browser/tools.js +688 -0
- package/runtime/scripts/amalgm-mcp/config.js +138 -0
- package/runtime/scripts/amalgm-mcp/credentials/rest.js +45 -0
- package/runtime/scripts/amalgm-mcp/deps.js +40 -0
- package/runtime/scripts/amalgm-mcp/email/inbound.js +215 -0
- package/runtime/scripts/amalgm-mcp/events/executor.js +179 -0
- package/runtime/scripts/amalgm-mcp/events/ingress.js +113 -0
- package/runtime/scripts/amalgm-mcp/events/matcher.js +125 -0
- package/runtime/scripts/amalgm-mcp/events/rest.js +200 -0
- package/runtime/scripts/amalgm-mcp/events/ring-buffer.js +19 -0
- package/runtime/scripts/amalgm-mcp/events/store.js +98 -0
- package/runtime/scripts/amalgm-mcp/events/tools.js +306 -0
- package/runtime/scripts/amalgm-mcp/events/webhook-url.js +28 -0
- package/runtime/scripts/amalgm-mcp/fs/rest.js +293 -0
- package/runtime/scripts/amalgm-mcp/index.js +100 -0
- package/runtime/scripts/amalgm-mcp/lib/chat-runner.js +167 -0
- package/runtime/scripts/amalgm-mcp/lib/email-md.js +288 -0
- package/runtime/scripts/amalgm-mcp/lib/mcp-resolver.js +63 -0
- package/runtime/scripts/amalgm-mcp/lib/prefs.js +393 -0
- package/runtime/scripts/amalgm-mcp/lib/storage.js +92 -0
- package/runtime/scripts/amalgm-mcp/lib/supabase.js +118 -0
- package/runtime/scripts/amalgm-mcp/lib/tool-result.js +177 -0
- package/runtime/scripts/amalgm-mcp/local/rest.js +80 -0
- package/runtime/scripts/amalgm-mcp/mcp-connections/rest.js +151 -0
- package/runtime/scripts/amalgm-mcp/notify/index.js +107 -0
- package/runtime/scripts/amalgm-mcp/server/http.js +335 -0
- package/runtime/scripts/amalgm-mcp/server/mcp.js +116 -0
- package/runtime/scripts/amalgm-mcp/slack/inbound.js +200 -0
- package/runtime/scripts/amalgm-mcp/tasks/executor.js +225 -0
- package/runtime/scripts/amalgm-mcp/tasks/rest.js +110 -0
- package/runtime/scripts/amalgm-mcp/tasks/schedule-normalization.js +85 -0
- package/runtime/scripts/amalgm-mcp/tasks/scheduler.js +105 -0
- package/runtime/scripts/amalgm-mcp/tasks/store.js +139 -0
- package/runtime/scripts/amalgm-mcp/tasks/tools.js +391 -0
- package/runtime/scripts/amalgm-mcp/user-api-keys/rest.js +105 -0
- package/runtime/scripts/amalgm-mcp/workspace/rest.js +389 -0
- package/runtime/scripts/chat-core/adapters/claude.js +163 -0
- package/runtime/scripts/chat-core/adapters/codex.js +313 -0
- package/runtime/scripts/chat-core/adapters/opencode.js +412 -0
- package/runtime/scripts/chat-core/auth.js +177 -0
- package/runtime/scripts/chat-core/contract.js +326 -0
- package/runtime/scripts/chat-core/credentials/store.js +212 -0
- package/runtime/scripts/chat-core/egress.js +87 -0
- package/runtime/scripts/chat-core/engine.js +195 -0
- package/runtime/scripts/chat-core/event-schema.js +231 -0
- package/runtime/scripts/chat-core/events.js +190 -0
- package/runtime/scripts/chat-core/index.js +11 -0
- package/runtime/scripts/chat-core/input.js +50 -0
- package/runtime/scripts/chat-core/normalizers/claude.js +450 -0
- package/runtime/scripts/chat-core/normalizers/codex.js +380 -0
- package/runtime/scripts/chat-core/normalizers/normalizer_spec.md +259 -0
- package/runtime/scripts/chat-core/normalizers/opencode.js +552 -0
- package/runtime/scripts/chat-core/normalizers/tool_contract.md +123 -0
- package/runtime/scripts/chat-core/normalizers/usage_contract.md +304 -0
- package/runtime/scripts/chat-core/parts.js +253 -0
- package/runtime/scripts/chat-core/recorder.js +65 -0
- package/runtime/scripts/chat-core/runtime.js +86 -0
- package/runtime/scripts/chat-core/server.js +163 -0
- package/runtime/scripts/chat-core/sse.js +196 -0
- package/runtime/scripts/chat-core/stores.js +100 -0
- package/runtime/scripts/chat-core/tool-display.js +149 -0
- package/runtime/scripts/chat-core/tool-shape.js +143 -0
- package/runtime/scripts/chat-core/tooling/mcp-bundle.js +161 -0
- package/runtime/scripts/chat-core/tooling/mcp-relay.js +97 -0
- package/runtime/scripts/chat-core/tooling/native-binaries.js +608 -0
- package/runtime/scripts/chat-core/tooling/system-prompt.js +20 -0
- package/runtime/scripts/chat-core/usage.js +343 -0
- package/runtime/scripts/chat-server/config.js +110 -0
- package/runtime/scripts/chat-server/db.js +529 -0
- package/runtime/scripts/chat-server/index.js +33 -0
- package/runtime/scripts/chat-server/model-catalog.js +327 -0
- package/runtime/scripts/chat-server.js +75 -0
- package/runtime/scripts/credential-adapter.js +129 -0
- package/runtime/scripts/fs-watcher.js +888 -0
- package/runtime/scripts/local-gateway.js +852 -0
- package/runtime/scripts/platform-context.txt +246 -0
- package/runtime/scripts/port-monitor.js +175 -0
- package/runtime/scripts/proxy-token-store.js +162 -0
- package/runtime/scripts/runtime-auth.js +163 -0
- package/runtime/scripts/test-claude-code-models.js +87 -0
- package/runtime/tsconfig.json +15 -0
|
@@ -0,0 +1,1156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* talk_to_agent — multi-turn agent conversations.
|
|
3
|
+
*
|
|
4
|
+
* Delegates to the local chat-server. On the first turn it creates a Supabase
|
|
5
|
+
* session row (if configured) so the UI can pick it up. On follow-up turns
|
|
6
|
+
* (conversation_id/session_id provided), chat-core reuses the frozen runtime session
|
|
7
|
+
* while it is still alive.
|
|
8
|
+
*
|
|
9
|
+
* Conversation lines are additionally appended to
|
|
10
|
+
* ~/.amalgm/agent-convos/{sessionId}.jsonl as a local trace.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const crypto = require('crypto');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
|
|
17
|
+
const { AMALGM_COMPUTER_ID, AMALGM_USER_ID, DEFAULT_CWD, STORAGE_DIR } = require('../config');
|
|
18
|
+
const { runThroughChatServer } = require('../lib/chat-runner');
|
|
19
|
+
const {
|
|
20
|
+
hasSupabase,
|
|
21
|
+
supabaseInsert,
|
|
22
|
+
supabasePatch,
|
|
23
|
+
supabaseSelect,
|
|
24
|
+
} = require('../lib/supabase');
|
|
25
|
+
const {
|
|
26
|
+
hydrateModelPreferences,
|
|
27
|
+
getSelectedModel,
|
|
28
|
+
resolveModelSelection,
|
|
29
|
+
DEFAULT_SELECTED_MODELS,
|
|
30
|
+
} = require('../lib/prefs');
|
|
31
|
+
const { textResult, errorResult } = require('../lib/tool-result');
|
|
32
|
+
const { ensureDir } = require('../lib/storage');
|
|
33
|
+
const {
|
|
34
|
+
appendAgentConvoLog,
|
|
35
|
+
normalizeConversationId,
|
|
36
|
+
resolveAgentByNameOrId,
|
|
37
|
+
} = require('./store');
|
|
38
|
+
const {
|
|
39
|
+
chatInputToLegacyFields,
|
|
40
|
+
getChatInputText,
|
|
41
|
+
normalizeChatInput,
|
|
42
|
+
} = require('../../../lib/chatInput');
|
|
43
|
+
const credentialAdapter = require('../../credential-adapter');
|
|
44
|
+
const { buildLocalMcpServerConfigs } = require('../lib/mcp-resolver');
|
|
45
|
+
|
|
46
|
+
const MAX_INLINE_MESSAGE_CHARS = Number(
|
|
47
|
+
process.env.AMALGM_TALK_TO_AGENT_MAX_INLINE_CHARS
|
|
48
|
+
|| process.env.AMALGM_TALK_TO_AGENT_MAX_MESSAGE_CHARS
|
|
49
|
+
|| 2800,
|
|
50
|
+
);
|
|
51
|
+
const AGENT_TO_AGENT_PROMPT =
|
|
52
|
+
'Agent-to-agent call: keep visible assistant text answer-focused. Do not narrate tool-use intent, pre-tool status, or private planning in plain text; use tools silently and report results when useful.';
|
|
53
|
+
const AGENT_CONTEXT_DIR = path.join(STORAGE_DIR, 'agent-context');
|
|
54
|
+
const DEFAULT_FOREGROUND_TIMEOUT_MS = Number(
|
|
55
|
+
process.env.AMALGM_TALK_TO_AGENT_FOREGROUND_TIMEOUT_MS || 120_000,
|
|
56
|
+
);
|
|
57
|
+
const MAX_FOREGROUND_TIMEOUT_MS = Number(
|
|
58
|
+
process.env.AMALGM_TALK_TO_AGENT_MAX_FOREGROUND_TIMEOUT_MS || 300_000,
|
|
59
|
+
);
|
|
60
|
+
const HEARTBEAT_INTERVAL_MS = Number(
|
|
61
|
+
process.env.AMALGM_TALK_TO_AGENT_HEARTBEAT_MS || 15_000,
|
|
62
|
+
);
|
|
63
|
+
const ACTIVE_RESPONSE_PREVIEW_CHARS = Number(
|
|
64
|
+
process.env.AMALGM_TALK_TO_AGENT_ACTIVE_PREVIEW_CHARS || 20_000,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
function isObject(value) {
|
|
68
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizeStringList(values) {
|
|
72
|
+
if (!Array.isArray(values)) return [];
|
|
73
|
+
return [...new Set(values
|
|
74
|
+
.filter((value) => typeof value === 'string')
|
|
75
|
+
.map((value) => value.trim())
|
|
76
|
+
.filter(Boolean))];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function safeContextName(value) {
|
|
80
|
+
return String(value || 'message')
|
|
81
|
+
.replace(/[^A-Za-z0-9_.-]+/g, '-')
|
|
82
|
+
.replace(/^-+|-+$/g, '')
|
|
83
|
+
.slice(0, 80) || 'message';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function writeLongMessageContextFile(sessionId, messageId, text) {
|
|
87
|
+
const dir = path.join(AGENT_CONTEXT_DIR, safeContextName(sessionId));
|
|
88
|
+
ensureDir(dir);
|
|
89
|
+
const file = path.join(dir, `${safeContextName(messageId)}.md`);
|
|
90
|
+
const body = [
|
|
91
|
+
'# Agent-to-Agent Message',
|
|
92
|
+
'',
|
|
93
|
+
`Session: ${sessionId}`,
|
|
94
|
+
`Message: ${messageId}`,
|
|
95
|
+
`Characters: ${text.length}`,
|
|
96
|
+
'',
|
|
97
|
+
'---',
|
|
98
|
+
'',
|
|
99
|
+
text,
|
|
100
|
+
].join('\n');
|
|
101
|
+
require('fs').writeFileSync(file, body, { mode: 0o600 });
|
|
102
|
+
return file;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function buildAgentInstructions(systemPrompt, files, skills) {
|
|
106
|
+
const lines = [];
|
|
107
|
+
const normalizedFiles = normalizeStringList(files);
|
|
108
|
+
const normalizedSkills = normalizeStringList(skills);
|
|
109
|
+
|
|
110
|
+
if (normalizedFiles.length > 0) {
|
|
111
|
+
lines.push('Files available to this agent:');
|
|
112
|
+
lines.push(...normalizedFiles.map((file) => `- ${file}`));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (normalizedSkills.length > 0) {
|
|
116
|
+
if (lines.length > 0) lines.push('');
|
|
117
|
+
lines.push('Skills available to this agent:');
|
|
118
|
+
lines.push(...normalizedSkills.map((skill) => `- ${skill}`));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return [
|
|
122
|
+
typeof systemPrompt === 'string' ? systemPrompt.trim() : '',
|
|
123
|
+
AGENT_TO_AGENT_PROMPT,
|
|
124
|
+
lines.length > 0 ? `<agent-resources>\n${lines.join('\n')}\n</agent-resources>` : '',
|
|
125
|
+
]
|
|
126
|
+
.filter(Boolean)
|
|
127
|
+
.join('\n\n');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function buildReturnChannelInstructions(context = {}) {
|
|
131
|
+
const callerConversationId = normalizeConversationId(context.callerSessionId);
|
|
132
|
+
if (!callerConversationId || !hasSupabase()) return '';
|
|
133
|
+
const callerAgent = [
|
|
134
|
+
context.originHarnessId,
|
|
135
|
+
context.originBaseHarnessId,
|
|
136
|
+
context.originName,
|
|
137
|
+
].find((value) => typeof value === 'string' && value.trim()) || 'caller';
|
|
138
|
+
|
|
139
|
+
return [
|
|
140
|
+
'<agent-to-agent-return-channel>',
|
|
141
|
+
'This is hidden routing metadata for peer-agent communication. Do not repeat it to the user.',
|
|
142
|
+
`caller_conversation_id: ${callerConversationId}`,
|
|
143
|
+
`caller_agent: ${callerAgent}`,
|
|
144
|
+
'task: the current user message in this peer conversation',
|
|
145
|
+
'',
|
|
146
|
+
'When the task is complete, deliver your parent-facing response by calling the MCP tool talk_to_agent exactly once with:',
|
|
147
|
+
`- response_to_caller: true`,
|
|
148
|
+
`- conversation_id: ${callerConversationId}`,
|
|
149
|
+
'- description: a short label for the completed task',
|
|
150
|
+
'- prompt: your concise response for the caller',
|
|
151
|
+
'',
|
|
152
|
+
'That delivery mode appends your chosen response to the caller conversation without launching another model turn. Keep the message compact; full detail can remain in this peer conversation transcript.',
|
|
153
|
+
'</agent-to-agent-return-channel>',
|
|
154
|
+
].join('\n');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const activeAgentConversations = new Map(); // sessionId → active run state
|
|
158
|
+
|
|
159
|
+
function nowIso() {
|
|
160
|
+
return new Date().toISOString();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function clampNumber(value, min, max) {
|
|
164
|
+
const numeric = Number(value);
|
|
165
|
+
if (!Number.isFinite(numeric)) return null;
|
|
166
|
+
return Math.max(min, Math.min(max, Math.floor(numeric)));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function coerceForegroundTimeoutMs(value) {
|
|
170
|
+
if (value === null || value === undefined || value === '') {
|
|
171
|
+
const configured = clampNumber(DEFAULT_FOREGROUND_TIMEOUT_MS, 0, MAX_FOREGROUND_TIMEOUT_MS);
|
|
172
|
+
return configured === null ? 120_000 : configured;
|
|
173
|
+
}
|
|
174
|
+
const numeric = Number(value);
|
|
175
|
+
if (!Number.isFinite(numeric)) {
|
|
176
|
+
const configured = clampNumber(DEFAULT_FOREGROUND_TIMEOUT_MS, 0, MAX_FOREGROUND_TIMEOUT_MS);
|
|
177
|
+
return configured === null ? 120_000 : configured;
|
|
178
|
+
}
|
|
179
|
+
if (numeric <= 0) return 0;
|
|
180
|
+
return clampNumber(numeric, 1_000, MAX_FOREGROUND_TIMEOUT_MS) || 120_000;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function clampActiveResponsePreview(text) {
|
|
184
|
+
const clean = String(text || '');
|
|
185
|
+
if (!clean || ACTIVE_RESPONSE_PREVIEW_CHARS <= 0) return '';
|
|
186
|
+
if (clean.length <= ACTIVE_RESPONSE_PREVIEW_CHARS) return clean;
|
|
187
|
+
return `${clean.slice(0, ACTIVE_RESPONSE_PREVIEW_CHARS).trimEnd()}\n\n... (streaming preview truncated; wait for completion or increase limit)`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function includeMetricsFlag(value) {
|
|
191
|
+
return value === true || String(value || '').toLowerCase() === 'true';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function appendStatusLog(sessionId, event, fields = {}) {
|
|
195
|
+
appendAgentConvoLog(sessionId, {
|
|
196
|
+
role: 'status',
|
|
197
|
+
event,
|
|
198
|
+
timestamp: nowIso(),
|
|
199
|
+
...fields,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function updateActiveConversation(sessionId, patch = {}) {
|
|
204
|
+
const current = activeAgentConversations.get(sessionId);
|
|
205
|
+
if (!current) return null;
|
|
206
|
+
const next = {
|
|
207
|
+
...current,
|
|
208
|
+
...patch,
|
|
209
|
+
updatedAt: nowIso(),
|
|
210
|
+
};
|
|
211
|
+
activeAgentConversations.set(sessionId, next);
|
|
212
|
+
return next;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function summarizeActiveConversation(sessionId) {
|
|
216
|
+
const state = activeAgentConversations.get(sessionId);
|
|
217
|
+
if (!state) return null;
|
|
218
|
+
const {
|
|
219
|
+
abortController,
|
|
220
|
+
...safeState
|
|
221
|
+
} = state;
|
|
222
|
+
return safeState;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function metricSummary(metrics) {
|
|
226
|
+
if (!metrics) return null;
|
|
227
|
+
return {
|
|
228
|
+
textChunks: metrics.textChunks || 0,
|
|
229
|
+
reasoningChunks: metrics.reasoningChunks || 0,
|
|
230
|
+
toolEvents: metrics.toolEvents || 0,
|
|
231
|
+
usageEvents: metrics.usageEvents || 0,
|
|
232
|
+
sawComplete: metrics.sawComplete === true,
|
|
233
|
+
sawError: metrics.sawError === true,
|
|
234
|
+
wallTimeMs: metrics.wallTimeMs || 0,
|
|
235
|
+
toolCallCount: Array.isArray(metrics.toolCalls) ? metrics.toolCalls.length : 0,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function responsePayload({
|
|
240
|
+
agent,
|
|
241
|
+
description,
|
|
242
|
+
sessionId,
|
|
243
|
+
runId,
|
|
244
|
+
status,
|
|
245
|
+
isNewConversation,
|
|
246
|
+
longMessageContext,
|
|
247
|
+
outputText,
|
|
248
|
+
stopReason,
|
|
249
|
+
usage,
|
|
250
|
+
metrics,
|
|
251
|
+
warnings,
|
|
252
|
+
recovery,
|
|
253
|
+
includeMetrics,
|
|
254
|
+
extra = {},
|
|
255
|
+
}) {
|
|
256
|
+
return {
|
|
257
|
+
agent: agent.name,
|
|
258
|
+
agent_id: agent.id,
|
|
259
|
+
description,
|
|
260
|
+
conversation_id: sessionId,
|
|
261
|
+
session_id: sessionId,
|
|
262
|
+
run_id: runId,
|
|
263
|
+
status,
|
|
264
|
+
...(outputText !== undefined ? { response: outputText } : {}),
|
|
265
|
+
is_new_conversation: isNewConversation,
|
|
266
|
+
...(stopReason !== undefined ? { stop_reason: stopReason } : {}),
|
|
267
|
+
usage: usage || null,
|
|
268
|
+
metric_summary: metricSummary(metrics),
|
|
269
|
+
...(includeMetrics ? { metrics: metrics || null } : {}),
|
|
270
|
+
warnings: warnings || [],
|
|
271
|
+
long_message: longMessageContext,
|
|
272
|
+
active: summarizeActiveConversation(sessionId),
|
|
273
|
+
recovery,
|
|
274
|
+
...extra,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function agentErrorResult({
|
|
279
|
+
agent,
|
|
280
|
+
description,
|
|
281
|
+
sessionId,
|
|
282
|
+
runId,
|
|
283
|
+
isNewConversation,
|
|
284
|
+
longMessageContext,
|
|
285
|
+
error,
|
|
286
|
+
status = 'error',
|
|
287
|
+
includeMetrics,
|
|
288
|
+
}) {
|
|
289
|
+
return errorResult(
|
|
290
|
+
JSON.stringify(
|
|
291
|
+
responsePayload({
|
|
292
|
+
agent,
|
|
293
|
+
description,
|
|
294
|
+
sessionId,
|
|
295
|
+
runId,
|
|
296
|
+
status,
|
|
297
|
+
isNewConversation,
|
|
298
|
+
longMessageContext,
|
|
299
|
+
warnings: [error],
|
|
300
|
+
recovery:
|
|
301
|
+
'Use agents_get_conversation with this conversation_id/run_id to inspect progress and recover any later transcript.',
|
|
302
|
+
includeMetrics,
|
|
303
|
+
extra: { error },
|
|
304
|
+
}),
|
|
305
|
+
null,
|
|
306
|
+
2,
|
|
307
|
+
),
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function summarizeStreamEvent(event) {
|
|
312
|
+
if (!event || typeof event !== 'object') return null;
|
|
313
|
+
if (event._type === 'update' && (event.sessionUpdate === 'tool_call' || event.sessionUpdate === 'tool_call_update')) {
|
|
314
|
+
return {
|
|
315
|
+
type: event.sessionUpdate,
|
|
316
|
+
toolCallId: event.toolCallId || null,
|
|
317
|
+
title: event.title || null,
|
|
318
|
+
kind: event.kind || null,
|
|
319
|
+
status: event.status || null,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
if (event._type === 'update' && event.sessionUpdate === 'agent_message_chunk') {
|
|
323
|
+
return { type: 'agent_message_chunk' };
|
|
324
|
+
}
|
|
325
|
+
if (event._type === 'update' && event.sessionUpdate === 'agent_thought_chunk') {
|
|
326
|
+
return { type: 'agent_thought_chunk' };
|
|
327
|
+
}
|
|
328
|
+
if (event._type === 'update' && event.sessionUpdate === 'usage_update') {
|
|
329
|
+
return { type: 'usage_update' };
|
|
330
|
+
}
|
|
331
|
+
if (event._type === 'complete') {
|
|
332
|
+
return { type: 'complete', stopReason: event.stopReason || null };
|
|
333
|
+
}
|
|
334
|
+
if (event._type === 'error') {
|
|
335
|
+
return { type: 'error', message: event.message || null };
|
|
336
|
+
}
|
|
337
|
+
return { type: event._type || 'unknown' };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function coerceAuthMethodForHarness(harnessId, authMethod) {
|
|
341
|
+
const supported = credentialAdapter.SUPPORTED_AUTH_BY_HARNESS?.[harnessId] || ['amalgm'];
|
|
342
|
+
return supported.includes(authMethod) ? authMethod : supported[0] || 'amalgm';
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function appendAsyncCompletionToCallerSession({
|
|
346
|
+
callerSessionId,
|
|
347
|
+
agent,
|
|
348
|
+
agentSessionId,
|
|
349
|
+
runId,
|
|
350
|
+
description,
|
|
351
|
+
stopReason,
|
|
352
|
+
warnings,
|
|
353
|
+
}) {
|
|
354
|
+
if (!callerSessionId || !hasSupabase()) return;
|
|
355
|
+
const text = [
|
|
356
|
+
`Agent completed: ${description || agent.name}`,
|
|
357
|
+
'',
|
|
358
|
+
`Agent: ${agent.name}`,
|
|
359
|
+
`Conversation: ${agentSessionId}`,
|
|
360
|
+
`Run: ${runId}`,
|
|
361
|
+
stopReason ? `Stop reason: ${stopReason}` : '',
|
|
362
|
+
warnings?.length ? `Warnings: ${warnings.join(' ')}` : '',
|
|
363
|
+
'',
|
|
364
|
+
'The peer response is stored in its conversation transcript.',
|
|
365
|
+
'Use agents_get_conversation with this conversation_id to read it, or talk_to_agent with this conversation_id to continue.',
|
|
366
|
+
].filter((line) => line !== '').join('\n');
|
|
367
|
+
|
|
368
|
+
await supabaseInsert('messages', {
|
|
369
|
+
id: crypto.randomUUID(),
|
|
370
|
+
session_id: callerSessionId,
|
|
371
|
+
role: 'assistant',
|
|
372
|
+
parts: [{ type: 'text', text }],
|
|
373
|
+
content_plain: text,
|
|
374
|
+
origin: 'agent',
|
|
375
|
+
origin_id: agentSessionId,
|
|
376
|
+
metadata: {
|
|
377
|
+
callback: true,
|
|
378
|
+
agentId: agent.id,
|
|
379
|
+
agentName: agent.name,
|
|
380
|
+
agentSessionId,
|
|
381
|
+
runId,
|
|
382
|
+
description,
|
|
383
|
+
},
|
|
384
|
+
}).catch(() => {});
|
|
385
|
+
await supabasePatch('sessions', 'id', callerSessionId, {
|
|
386
|
+
last_message_at: new Date().toISOString(),
|
|
387
|
+
new_messages: true,
|
|
388
|
+
}).catch(() => {});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function deliverResponseToCallerSession({
|
|
392
|
+
targetSessionId,
|
|
393
|
+
sourceSessionId,
|
|
394
|
+
description,
|
|
395
|
+
text,
|
|
396
|
+
}) {
|
|
397
|
+
if (!hasSupabase()) {
|
|
398
|
+
return errorResult('response_to_caller requires a Supabase-backed caller conversation.');
|
|
399
|
+
}
|
|
400
|
+
if (!targetSessionId) {
|
|
401
|
+
return errorResult('conversation_id is required when response_to_caller=true.');
|
|
402
|
+
}
|
|
403
|
+
if (!sourceSessionId) {
|
|
404
|
+
return errorResult('response_to_caller can only be used from inside a running peer agent conversation.');
|
|
405
|
+
}
|
|
406
|
+
if (!text || !String(text).trim()) {
|
|
407
|
+
return errorResult('prompt is required when response_to_caller=true.');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const targetRows = await supabaseSelect(
|
|
411
|
+
'sessions',
|
|
412
|
+
`id=eq.${targetSessionId}&user_id=eq.${AMALGM_USER_ID}&select=id,title,harness,metadata`,
|
|
413
|
+
);
|
|
414
|
+
const targetSession = Array.isArray(targetRows) ? targetRows[0] : null;
|
|
415
|
+
if (!targetSession) {
|
|
416
|
+
return errorResult('Caller conversation was not found or is not owned by this user.');
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const sourceRows = await supabaseSelect(
|
|
420
|
+
'sessions',
|
|
421
|
+
`id=eq.${sourceSessionId}&user_id=eq.${AMALGM_USER_ID}&select=id,title,harness,origin,origin_id,metadata`,
|
|
422
|
+
);
|
|
423
|
+
const sourceSession = Array.isArray(sourceRows) ? sourceRows[0] : null;
|
|
424
|
+
if (!sourceSession) {
|
|
425
|
+
return errorResult('Peer conversation was not found or is not owned by this user.');
|
|
426
|
+
}
|
|
427
|
+
if (sourceSession.origin !== 'agent' || sourceSession.origin_id !== targetSessionId) {
|
|
428
|
+
return errorResult('Peer conversation is not linked to the requested caller conversation.');
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const messageText = String(text).trim();
|
|
432
|
+
const deliveredAt = nowIso();
|
|
433
|
+
const sourceMetadata = isObject(sourceSession.metadata) ? sourceSession.metadata : {};
|
|
434
|
+
const sourceAgentId = sourceMetadata.agentId || sourceSession.harness || null;
|
|
435
|
+
const sourceAgentName = sourceMetadata.agentName || sourceSession.title || sourceSession.harness || 'Peer agent';
|
|
436
|
+
|
|
437
|
+
const inserted = await supabaseInsert('messages', {
|
|
438
|
+
id: crypto.randomUUID(),
|
|
439
|
+
session_id: targetSessionId,
|
|
440
|
+
role: 'assistant',
|
|
441
|
+
parts: [{ type: 'text', text: messageText }],
|
|
442
|
+
content_plain: messageText,
|
|
443
|
+
origin: 'agent',
|
|
444
|
+
origin_id: sourceSessionId,
|
|
445
|
+
metadata: {
|
|
446
|
+
responseToCaller: true,
|
|
447
|
+
sourceConversationId: sourceSessionId,
|
|
448
|
+
sourceAgentId,
|
|
449
|
+
sourceAgentName,
|
|
450
|
+
sourceHarness: sourceSession.harness || null,
|
|
451
|
+
description,
|
|
452
|
+
},
|
|
453
|
+
});
|
|
454
|
+
if (!inserted) {
|
|
455
|
+
return errorResult('Failed to deliver response to caller conversation.');
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
await supabasePatch('sessions', 'id', targetSessionId, {
|
|
459
|
+
last_message_at: deliveredAt,
|
|
460
|
+
new_messages: true,
|
|
461
|
+
}).catch(() => {});
|
|
462
|
+
|
|
463
|
+
updateActiveConversation(sourceSessionId, {
|
|
464
|
+
callerResponseDelivered: true,
|
|
465
|
+
callerResponseDeliveredAt: deliveredAt,
|
|
466
|
+
callerResponseTarget: targetSessionId,
|
|
467
|
+
callerResponseText: messageText,
|
|
468
|
+
callerResponseDescription: description || null,
|
|
469
|
+
});
|
|
470
|
+
appendStatusLog(sourceSessionId, 'caller_response_delivered', {
|
|
471
|
+
targetConversationId: targetSessionId,
|
|
472
|
+
description,
|
|
473
|
+
messageChars: messageText.length,
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
return textResult(JSON.stringify({
|
|
477
|
+
status: 'delivered',
|
|
478
|
+
response_to_caller: true,
|
|
479
|
+
conversation_id: targetSessionId,
|
|
480
|
+
session_id: targetSessionId,
|
|
481
|
+
source_conversation_id: sourceSessionId,
|
|
482
|
+
description,
|
|
483
|
+
}, null, 2));
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async function handleTalkToAgent(args, context = {}) {
|
|
487
|
+
const {
|
|
488
|
+
agent: requestedAgent,
|
|
489
|
+
agent_id,
|
|
490
|
+
description,
|
|
491
|
+
prompt,
|
|
492
|
+
message,
|
|
493
|
+
conversation_id,
|
|
494
|
+
session_id,
|
|
495
|
+
cwd,
|
|
496
|
+
chatInput,
|
|
497
|
+
mode,
|
|
498
|
+
run_in_background,
|
|
499
|
+
response_to_caller,
|
|
500
|
+
timeout_ms,
|
|
501
|
+
include_metrics,
|
|
502
|
+
isolation,
|
|
503
|
+
} = args || {};
|
|
504
|
+
const taskPrompt = prompt || message;
|
|
505
|
+
if (!taskPrompt && !chatInput) {
|
|
506
|
+
return errorResult('prompt is required.');
|
|
507
|
+
}
|
|
508
|
+
const requestedConversationId = conversation_id || session_id;
|
|
509
|
+
const normalizedConversationId = requestedConversationId
|
|
510
|
+
? normalizeConversationId(requestedConversationId)
|
|
511
|
+
: null;
|
|
512
|
+
if (requestedConversationId && !normalizedConversationId) {
|
|
513
|
+
return errorResult('conversation_id must be a UUID.');
|
|
514
|
+
}
|
|
515
|
+
const taskDescription =
|
|
516
|
+
typeof description === 'string' && description.trim()
|
|
517
|
+
? description.trim()
|
|
518
|
+
: String(taskPrompt || '').trim().split(/\s+/).slice(0, 5).join(' ') || 'Peer response';
|
|
519
|
+
const responseToCaller =
|
|
520
|
+
response_to_caller === true || String(response_to_caller || '').toLowerCase() === 'true';
|
|
521
|
+
|
|
522
|
+
if (responseToCaller) {
|
|
523
|
+
return deliverResponseToCallerSession({
|
|
524
|
+
targetSessionId: normalizedConversationId,
|
|
525
|
+
sourceSessionId: normalizeConversationId(context.callerSessionId),
|
|
526
|
+
description: taskDescription,
|
|
527
|
+
text: taskPrompt,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
await hydrateModelPreferences();
|
|
532
|
+
const agentLookup = requestedAgent || agent_id || 'Codex';
|
|
533
|
+
const agent = resolveAgentByNameOrId(agentLookup);
|
|
534
|
+
if (!agent) {
|
|
535
|
+
return errorResult(
|
|
536
|
+
`Agent not found: ${agentLookup}. Use agents_list to see available agents.`,
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
const isNewConversation = !normalizedConversationId;
|
|
540
|
+
const sessionId = normalizedConversationId || crypto.randomUUID();
|
|
541
|
+
const userMessageId = crypto.randomUUID();
|
|
542
|
+
const assistantMessageId = crypto.randomUUID();
|
|
543
|
+
const runId = assistantMessageId;
|
|
544
|
+
const asyncMode = run_in_background === true || String(mode || '').toLowerCase() === 'async';
|
|
545
|
+
const includeMetrics = includeMetricsFlag(include_metrics);
|
|
546
|
+
const foregroundTimeoutMs = coerceForegroundTimeoutMs(timeout_ms);
|
|
547
|
+
|
|
548
|
+
// Chat-server agent ID derives from the agent's base harness.
|
|
549
|
+
const agentIdMap = { claude_code: 'claude', codex: 'codex', opencode: 'opencode', pi: 'pi', amp: 'amp', cursor: 'cursor' };
|
|
550
|
+
const chatAgentId = agentIdMap[agent.baseHarnessId] || 'claude';
|
|
551
|
+
const defaultAuthMethod = coerceAuthMethodForHarness(
|
|
552
|
+
agent.baseHarnessId,
|
|
553
|
+
agent.authMethod
|
|
554
|
+
|| (credentialAdapter.VALID_HARNESS_IDS.includes(agent.baseHarnessId)
|
|
555
|
+
? credentialAdapter.getPersistedAuthMode(agent.baseHarnessId)
|
|
556
|
+
: 'amalgm'),
|
|
557
|
+
);
|
|
558
|
+
const agentFiles = normalizeStringList(agent.files);
|
|
559
|
+
const agentSkills = normalizeStringList(agent.skills);
|
|
560
|
+
const agentMcpAppIds = normalizeStringList(agent.mcp?.appIds ?? agent.mcpAppIds);
|
|
561
|
+
const baseAgentSystemPrompt = buildAgentInstructions(
|
|
562
|
+
agent.systemPrompt,
|
|
563
|
+
agentFiles,
|
|
564
|
+
agentSkills,
|
|
565
|
+
);
|
|
566
|
+
const returnChannelInstructions = buildReturnChannelInstructions(context);
|
|
567
|
+
const agentSystemPrompt = [
|
|
568
|
+
baseAgentSystemPrompt,
|
|
569
|
+
returnChannelInstructions,
|
|
570
|
+
].filter(Boolean).join('\n\n');
|
|
571
|
+
const fallbackModelId = agent.builtin
|
|
572
|
+
? agent.baseModelId
|
|
573
|
+
: agent.baseModelId || DEFAULT_SELECTED_MODELS[agent.baseHarnessId] || null;
|
|
574
|
+
const normalizedChatInput = normalizeChatInput({
|
|
575
|
+
...(isObject(chatInput) ? chatInput : {}),
|
|
576
|
+
agent: {
|
|
577
|
+
...((isObject(chatInput) && isObject(chatInput.agent)) ? chatInput.agent : {}),
|
|
578
|
+
harness:
|
|
579
|
+
((isObject(chatInput) && isObject(chatInput.agent) && chatInput.agent.harness) || null)
|
|
580
|
+
|| agent.baseHarnessId,
|
|
581
|
+
model:
|
|
582
|
+
((isObject(chatInput) && isObject(chatInput.agent) && chatInput.agent.model) || null)
|
|
583
|
+
|| fallbackModelId,
|
|
584
|
+
authMethod: coerceAuthMethodForHarness(
|
|
585
|
+
agent.baseHarnessId,
|
|
586
|
+
((isObject(chatInput) && isObject(chatInput.agent) && chatInput.agent.authMethod) || null)
|
|
587
|
+
|| defaultAuthMethod,
|
|
588
|
+
),
|
|
589
|
+
customAgentId: agent.builtin ? null : agent.id,
|
|
590
|
+
systemPrompt:
|
|
591
|
+
((isObject(chatInput) && isObject(chatInput.agent) && chatInput.agent.systemPrompt) || null)
|
|
592
|
+
|| agentSystemPrompt
|
|
593
|
+
|| null,
|
|
594
|
+
},
|
|
595
|
+
tools: {
|
|
596
|
+
...((isObject(chatInput) && isObject(chatInput.tools)) ? chatInput.tools : {}),
|
|
597
|
+
mcpAppIds: normalizeStringList([
|
|
598
|
+
...agentMcpAppIds,
|
|
599
|
+
...((((isObject(chatInput) && isObject(chatInput.tools) && Array.isArray(chatInput.tools.mcpAppIds))
|
|
600
|
+
? chatInput.tools.mcpAppIds
|
|
601
|
+
: []))),
|
|
602
|
+
]),
|
|
603
|
+
},
|
|
604
|
+
execution: {
|
|
605
|
+
...((isObject(chatInput) && isObject(chatInput.execution)) ? chatInput.execution : {}),
|
|
606
|
+
cwd:
|
|
607
|
+
((isObject(chatInput) && isObject(chatInput.execution) && chatInput.execution.cwd) || null)
|
|
608
|
+
|| cwd
|
|
609
|
+
|| DEFAULT_CWD,
|
|
610
|
+
},
|
|
611
|
+
}, {
|
|
612
|
+
prompt: taskPrompt,
|
|
613
|
+
harness: agent.baseHarnessId,
|
|
614
|
+
modelId: fallbackModelId,
|
|
615
|
+
authMethod: defaultAuthMethod,
|
|
616
|
+
cwd,
|
|
617
|
+
systemPrompt: agentSystemPrompt,
|
|
618
|
+
});
|
|
619
|
+
const legacyChatFields = chatInputToLegacyFields(normalizedChatInput, {
|
|
620
|
+
prompt: taskPrompt,
|
|
621
|
+
harness: agent.baseHarnessId,
|
|
622
|
+
modelId: fallbackModelId,
|
|
623
|
+
authMethod: defaultAuthMethod,
|
|
624
|
+
cwd,
|
|
625
|
+
systemPrompt: agentSystemPrompt,
|
|
626
|
+
});
|
|
627
|
+
let userPromptText = getChatInputText(normalizedChatInput, taskPrompt) || legacyChatFields.prompt;
|
|
628
|
+
let longMessageContext = null;
|
|
629
|
+
if (MAX_INLINE_MESSAGE_CHARS > 0 && userPromptText.length > MAX_INLINE_MESSAGE_CHARS) {
|
|
630
|
+
const contextPath = writeLongMessageContextFile(sessionId, userMessageId, userPromptText);
|
|
631
|
+
longMessageContext = {
|
|
632
|
+
path: contextPath,
|
|
633
|
+
chars: userPromptText.length,
|
|
634
|
+
threshold: MAX_INLINE_MESSAGE_CHARS,
|
|
635
|
+
};
|
|
636
|
+
const contextPrompt = [
|
|
637
|
+
`The caller sent a long agent-to-agent message (${userPromptText.length} characters).`,
|
|
638
|
+
`It is stored in a local context file: ${contextPath}`,
|
|
639
|
+
'',
|
|
640
|
+
'Read that file fully before responding. Treat it as the caller\'s message for this turn.',
|
|
641
|
+
'Do not mention private tool-use narration; answer the request after reading the file.',
|
|
642
|
+
].join('\n');
|
|
643
|
+
legacyChatFields.prompt = contextPrompt;
|
|
644
|
+
normalizedChatInput.parts = [{ type: 'text', text: contextPrompt }];
|
|
645
|
+
normalizedChatInput.files = [
|
|
646
|
+
...(Array.isArray(normalizedChatInput.files) ? normalizedChatInput.files : []),
|
|
647
|
+
{ path: contextPath, name: 'agent-to-agent-message.md' },
|
|
648
|
+
];
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
console.log(
|
|
652
|
+
`[AmalgmMCP:Agent] ${isNewConversation ? 'Starting' : 'Continuing'} conversation with ${agent.name} (${agent.id}), session: ${sessionId}`,
|
|
653
|
+
);
|
|
654
|
+
|
|
655
|
+
appendAgentConvoLog(sessionId, {
|
|
656
|
+
role: 'user',
|
|
657
|
+
agentId: agent.id,
|
|
658
|
+
runId,
|
|
659
|
+
description: taskDescription,
|
|
660
|
+
isolation: isolation || null,
|
|
661
|
+
message: userPromptText,
|
|
662
|
+
timestamp: new Date().toISOString(),
|
|
663
|
+
isNewConversation,
|
|
664
|
+
...(longMessageContext ? { longMessageContext } : {}),
|
|
665
|
+
});
|
|
666
|
+
appendStatusLog(sessionId, 'queued', {
|
|
667
|
+
agentId: agent.id,
|
|
668
|
+
agentName: agent.name,
|
|
669
|
+
runId,
|
|
670
|
+
description: taskDescription,
|
|
671
|
+
asyncMode,
|
|
672
|
+
foregroundTimeoutMs: asyncMode ? 0 : foregroundTimeoutMs,
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
if (isNewConversation && hasSupabase()) {
|
|
676
|
+
try {
|
|
677
|
+
const callerSessionId = context.callerSessionId || null;
|
|
678
|
+
let parentOrigin =
|
|
679
|
+
context.originName || context.originHarnessId || context.originBaseHarnessId
|
|
680
|
+
? {
|
|
681
|
+
originName: context.originName || undefined,
|
|
682
|
+
originHarnessId: context.originHarnessId || undefined,
|
|
683
|
+
originBaseHarnessId: context.originBaseHarnessId || undefined,
|
|
684
|
+
}
|
|
685
|
+
: null;
|
|
686
|
+
|
|
687
|
+
if (callerSessionId && !parentOrigin) {
|
|
688
|
+
const rows = await supabaseSelect(
|
|
689
|
+
'sessions',
|
|
690
|
+
`id=eq.${callerSessionId}&user_id=eq.${AMALGM_USER_ID}&select=id,title,harness,metadata`,
|
|
691
|
+
);
|
|
692
|
+
const parentSession = Array.isArray(rows) ? rows[0] : null;
|
|
693
|
+
if (parentSession) {
|
|
694
|
+
parentOrigin = {
|
|
695
|
+
originName: parentSession.title || undefined,
|
|
696
|
+
originHarnessId: parentSession.harness || undefined,
|
|
697
|
+
originBaseHarnessId: parentSession.metadata?.baseHarnessId || undefined,
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
await supabaseInsert('sessions', {
|
|
703
|
+
id: sessionId,
|
|
704
|
+
user_id: AMALGM_USER_ID,
|
|
705
|
+
computer_id: AMALGM_COMPUTER_ID,
|
|
706
|
+
container_id: os.hostname() || 'agent-conversation',
|
|
707
|
+
harness: agent.id,
|
|
708
|
+
title: taskDescription || 'New Chat',
|
|
709
|
+
status: 'in-progress',
|
|
710
|
+
origin: 'agent',
|
|
711
|
+
origin_id: callerSessionId,
|
|
712
|
+
metadata: {
|
|
713
|
+
agentId: agent.id,
|
|
714
|
+
agentName: agent.name,
|
|
715
|
+
taskDescription,
|
|
716
|
+
baseHarnessId: agent.baseHarnessId,
|
|
717
|
+
baseModelId: agent.builtin
|
|
718
|
+
? getSelectedModel(agent.baseHarnessId) || agent.baseModelId
|
|
719
|
+
: agent.baseModelId ||
|
|
720
|
+
getSelectedModel(agent.baseHarnessId) ||
|
|
721
|
+
DEFAULT_SELECTED_MODELS[agent.baseHarnessId],
|
|
722
|
+
isAgentConversation: true,
|
|
723
|
+
builtin: !!agent.builtin,
|
|
724
|
+
systemPrompt: baseAgentSystemPrompt || '',
|
|
725
|
+
...(returnChannelInstructions
|
|
726
|
+
? {
|
|
727
|
+
returnChannel: {
|
|
728
|
+
deliveryTool: 'talk_to_agent',
|
|
729
|
+
mode: 'response_to_caller',
|
|
730
|
+
callerConversationId: normalizeConversationId(context.callerSessionId),
|
|
731
|
+
},
|
|
732
|
+
}
|
|
733
|
+
: {}),
|
|
734
|
+
files: agentFiles,
|
|
735
|
+
skills: agentSkills,
|
|
736
|
+
mcpAppIds: agentMcpAppIds,
|
|
737
|
+
...(parentOrigin || {}),
|
|
738
|
+
},
|
|
739
|
+
});
|
|
740
|
+
} catch (err) {
|
|
741
|
+
console.error('[AmalgmMCP:Agent] DB session creation failed:', err.message);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const abortController = new AbortController();
|
|
746
|
+
activeAgentConversations.set(sessionId, {
|
|
747
|
+
agentId: agent.id,
|
|
748
|
+
agentName: agent.name,
|
|
749
|
+
runId,
|
|
750
|
+
description: taskDescription,
|
|
751
|
+
status: 'queued',
|
|
752
|
+
startedAt: nowIso(),
|
|
753
|
+
updatedAt: nowIso(),
|
|
754
|
+
lastEventAt: null,
|
|
755
|
+
counters: {
|
|
756
|
+
textChunks: 0,
|
|
757
|
+
reasoningChunks: 0,
|
|
758
|
+
toolEvents: 0,
|
|
759
|
+
usageEvents: 0,
|
|
760
|
+
heartbeats: 0,
|
|
761
|
+
},
|
|
762
|
+
abortController,
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
try {
|
|
766
|
+
const { modelId: uiModelId, cliModel, reasoningEffort } = resolveModelSelection(
|
|
767
|
+
agent.baseHarnessId,
|
|
768
|
+
legacyChatFields.modelId || fallbackModelId,
|
|
769
|
+
);
|
|
770
|
+
const mcpServers = await buildLocalMcpServerConfigs(legacyChatFields.mcpAppIds || []);
|
|
771
|
+
|
|
772
|
+
const chatPayload = {
|
|
773
|
+
chatInput: normalizedChatInput,
|
|
774
|
+
prompt: legacyChatFields.prompt,
|
|
775
|
+
userId: AMALGM_USER_ID,
|
|
776
|
+
computerId: AMALGM_COMPUTER_ID,
|
|
777
|
+
codeSessionId: sessionId,
|
|
778
|
+
assistantMessageId,
|
|
779
|
+
userMessageId,
|
|
780
|
+
userParts: legacyChatFields.userParts,
|
|
781
|
+
agentId: chatAgentId,
|
|
782
|
+
modelId: uiModelId,
|
|
783
|
+
cliModel,
|
|
784
|
+
...(reasoningEffort ? { reasoningEffort } : {}),
|
|
785
|
+
cwd: legacyChatFields.cwd || DEFAULT_CWD,
|
|
786
|
+
authMethod: legacyChatFields.authMethod || defaultAuthMethod,
|
|
787
|
+
mcpServers,
|
|
788
|
+
...(legacyChatFields.systemPrompt ? { systemPrompt: legacyChatFields.systemPrompt } : {}),
|
|
789
|
+
...(longMessageContext ? { longMessageContext } : {}),
|
|
790
|
+
...(!isNewConversation ? { resumeSessionId: sessionId } : {}),
|
|
791
|
+
...(context.callerSessionId
|
|
792
|
+
? { origin: 'agent', originId: context.callerSessionId }
|
|
793
|
+
: {}),
|
|
794
|
+
...(context.originName ? { originName: context.originName } : {}),
|
|
795
|
+
...(context.originHarnessId ? { originHarnessId: context.originHarnessId } : {}),
|
|
796
|
+
...(context.originBaseHarnessId
|
|
797
|
+
? { originBaseHarnessId: context.originBaseHarnessId }
|
|
798
|
+
: {}),
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
let notifyCallerOnCompletion = asyncMode;
|
|
802
|
+
const finishTurn = async () => {
|
|
803
|
+
let heartbeatTimer = null;
|
|
804
|
+
const startedAt = Date.now();
|
|
805
|
+
const seenLifecycle = {
|
|
806
|
+
text: false,
|
|
807
|
+
reasoning: false,
|
|
808
|
+
};
|
|
809
|
+
try {
|
|
810
|
+
updateActiveConversation(sessionId, { status: 'starting' });
|
|
811
|
+
appendStatusLog(sessionId, 'harness_started', {
|
|
812
|
+
agentId: agent.id,
|
|
813
|
+
agentName: agent.name,
|
|
814
|
+
runId,
|
|
815
|
+
description: taskDescription,
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
if (HEARTBEAT_INTERVAL_MS > 0) {
|
|
819
|
+
heartbeatTimer = setInterval(() => {
|
|
820
|
+
const active = updateActiveConversation(sessionId, {
|
|
821
|
+
status: 'running',
|
|
822
|
+
counters: {
|
|
823
|
+
...(summarizeActiveConversation(sessionId)?.counters || {}),
|
|
824
|
+
heartbeats:
|
|
825
|
+
((summarizeActiveConversation(sessionId)?.counters || {}).heartbeats || 0) + 1,
|
|
826
|
+
},
|
|
827
|
+
});
|
|
828
|
+
appendStatusLog(sessionId, 'still_running', {
|
|
829
|
+
agentId: agent.id,
|
|
830
|
+
agentName: agent.name,
|
|
831
|
+
runId,
|
|
832
|
+
description: taskDescription,
|
|
833
|
+
elapsedMs: Date.now() - startedAt,
|
|
834
|
+
active,
|
|
835
|
+
});
|
|
836
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
837
|
+
if (typeof heartbeatTimer.unref === 'function') heartbeatTimer.unref();
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const { outputText, stopReason, usage, metrics } = await runThroughChatServer(
|
|
841
|
+
chatPayload,
|
|
842
|
+
abortController.signal,
|
|
843
|
+
{
|
|
844
|
+
onEvent(event, eventMetrics) {
|
|
845
|
+
const summary = summarizeStreamEvent(event);
|
|
846
|
+
if (!summary) return;
|
|
847
|
+
const counters = {
|
|
848
|
+
textChunks: eventMetrics.textChunks || 0,
|
|
849
|
+
reasoningChunks: eventMetrics.reasoningChunks || 0,
|
|
850
|
+
toolEvents: eventMetrics.toolEvents || 0,
|
|
851
|
+
usageEvents: eventMetrics.usageEvents || 0,
|
|
852
|
+
heartbeats:
|
|
853
|
+
(summarizeActiveConversation(sessionId)?.counters || {}).heartbeats || 0,
|
|
854
|
+
};
|
|
855
|
+
updateActiveConversation(sessionId, {
|
|
856
|
+
status: summary.type === 'complete' ? 'finalizing' : 'running',
|
|
857
|
+
lastEventAt: nowIso(),
|
|
858
|
+
counters,
|
|
859
|
+
...(summary.type === 'agent_message_chunk'
|
|
860
|
+
? {
|
|
861
|
+
responsePreview: clampActiveResponsePreview(eventMetrics.outputText),
|
|
862
|
+
responsePreviewChars: String(eventMetrics.outputText || '').length,
|
|
863
|
+
}
|
|
864
|
+
: {}),
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
if (summary.type === 'tool_call' || summary.type === 'tool_call_update') {
|
|
868
|
+
appendStatusLog(sessionId, 'tool_event', {
|
|
869
|
+
agentId: agent.id,
|
|
870
|
+
agentName: agent.name,
|
|
871
|
+
runId,
|
|
872
|
+
description: taskDescription,
|
|
873
|
+
streamEvent: summary,
|
|
874
|
+
});
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
if (summary.type === 'agent_message_chunk' && !seenLifecycle.text) {
|
|
878
|
+
seenLifecycle.text = true;
|
|
879
|
+
appendStatusLog(sessionId, 'agent_text_started', {
|
|
880
|
+
agentId: agent.id,
|
|
881
|
+
agentName: agent.name,
|
|
882
|
+
runId,
|
|
883
|
+
description: taskDescription,
|
|
884
|
+
});
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
if (summary.type === 'agent_thought_chunk' && !seenLifecycle.reasoning) {
|
|
888
|
+
seenLifecycle.reasoning = true;
|
|
889
|
+
appendStatusLog(sessionId, 'agent_reasoning_started', {
|
|
890
|
+
agentId: agent.id,
|
|
891
|
+
agentName: agent.name,
|
|
892
|
+
runId,
|
|
893
|
+
description: taskDescription,
|
|
894
|
+
});
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
if (summary.type === 'complete' || summary.type === 'error') {
|
|
898
|
+
appendStatusLog(sessionId, summary.type === 'complete' ? 'stream_complete' : 'stream_error', {
|
|
899
|
+
agentId: agent.id,
|
|
900
|
+
agentName: agent.name,
|
|
901
|
+
runId,
|
|
902
|
+
description: taskDescription,
|
|
903
|
+
streamEvent: summary,
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
},
|
|
907
|
+
},
|
|
908
|
+
);
|
|
909
|
+
const activeAfterTurn = summarizeActiveConversation(sessionId);
|
|
910
|
+
const deliveredResponseText = String(activeAfterTurn?.callerResponseText || '').trim();
|
|
911
|
+
const finalOutputText = String(outputText || '').trim() ? outputText : deliveredResponseText;
|
|
912
|
+
const warnings = [];
|
|
913
|
+
if (!String(finalOutputText || '').trim()) {
|
|
914
|
+
warnings.push(
|
|
915
|
+
metrics?.toolEvents > 0
|
|
916
|
+
? 'Agent completed without visible text after tool activity; check the session log or workspace diff before retrying.'
|
|
917
|
+
: 'Agent completed with an empty text response; retry this same session if you need an answer.',
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
if (metrics && metrics.sawComplete === false) {
|
|
921
|
+
warnings.push('Chat stream ended without an explicit completion frame.');
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
appendAgentConvoLog(sessionId, {
|
|
925
|
+
role: 'assistant',
|
|
926
|
+
agentId: agent.id,
|
|
927
|
+
agentName: agent.name,
|
|
928
|
+
runId,
|
|
929
|
+
description: taskDescription,
|
|
930
|
+
message: finalOutputText,
|
|
931
|
+
timestamp: new Date().toISOString(),
|
|
932
|
+
stopReason,
|
|
933
|
+
usage,
|
|
934
|
+
metrics,
|
|
935
|
+
warnings,
|
|
936
|
+
});
|
|
937
|
+
updateActiveConversation(sessionId, {
|
|
938
|
+
status: 'completed',
|
|
939
|
+
completedAt: nowIso(),
|
|
940
|
+
counters: {
|
|
941
|
+
textChunks: metrics?.textChunks || 0,
|
|
942
|
+
reasoningChunks: metrics?.reasoningChunks || 0,
|
|
943
|
+
toolEvents: metrics?.toolEvents || 0,
|
|
944
|
+
usageEvents: metrics?.usageEvents || 0,
|
|
945
|
+
heartbeats:
|
|
946
|
+
(summarizeActiveConversation(sessionId)?.counters || {}).heartbeats || 0,
|
|
947
|
+
},
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
console.log(
|
|
951
|
+
`[AmalgmMCP:Agent] ${agent.name} responded (${finalOutputText.length} chars, session: ${sessionId})`,
|
|
952
|
+
);
|
|
953
|
+
|
|
954
|
+
if (
|
|
955
|
+
notifyCallerOnCompletion &&
|
|
956
|
+
!summarizeActiveConversation(sessionId)?.callerResponseDelivered
|
|
957
|
+
) {
|
|
958
|
+
await appendAsyncCompletionToCallerSession({
|
|
959
|
+
callerSessionId: context.callerSessionId,
|
|
960
|
+
agent,
|
|
961
|
+
agentSessionId: sessionId,
|
|
962
|
+
runId,
|
|
963
|
+
description: taskDescription,
|
|
964
|
+
stopReason,
|
|
965
|
+
warnings,
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
return textResult(
|
|
970
|
+
JSON.stringify(
|
|
971
|
+
responsePayload({
|
|
972
|
+
agent,
|
|
973
|
+
description: taskDescription,
|
|
974
|
+
sessionId,
|
|
975
|
+
runId,
|
|
976
|
+
status: 'completed',
|
|
977
|
+
isNewConversation,
|
|
978
|
+
longMessageContext,
|
|
979
|
+
outputText: finalOutputText,
|
|
980
|
+
stopReason,
|
|
981
|
+
usage,
|
|
982
|
+
metrics,
|
|
983
|
+
warnings,
|
|
984
|
+
recovery:
|
|
985
|
+
'Use agents_get_conversation with this conversation_id if the caller times out or needs the local transcript.',
|
|
986
|
+
includeMetrics,
|
|
987
|
+
extra: deliveredResponseText && !String(outputText || '').trim()
|
|
988
|
+
? { response_source: 'return_channel' }
|
|
989
|
+
: {},
|
|
990
|
+
}),
|
|
991
|
+
null,
|
|
992
|
+
2,
|
|
993
|
+
),
|
|
994
|
+
);
|
|
995
|
+
} catch (err) {
|
|
996
|
+
updateActiveConversation(sessionId, {
|
|
997
|
+
status: err.name === 'AbortError' ? 'cancelled' : 'error',
|
|
998
|
+
error: err.message,
|
|
999
|
+
completedAt: nowIso(),
|
|
1000
|
+
});
|
|
1001
|
+
appendAgentConvoLog(sessionId, {
|
|
1002
|
+
role: 'error',
|
|
1003
|
+
agentId: agent.id,
|
|
1004
|
+
runId,
|
|
1005
|
+
description: taskDescription,
|
|
1006
|
+
error: err.message,
|
|
1007
|
+
timestamp: new Date().toISOString(),
|
|
1008
|
+
});
|
|
1009
|
+
if (err.name === 'AbortError') {
|
|
1010
|
+
return agentErrorResult({
|
|
1011
|
+
agent,
|
|
1012
|
+
description: taskDescription,
|
|
1013
|
+
sessionId,
|
|
1014
|
+
runId,
|
|
1015
|
+
isNewConversation,
|
|
1016
|
+
longMessageContext,
|
|
1017
|
+
error: `Conversation with ${agent.name} was cancelled.`,
|
|
1018
|
+
status: 'cancelled',
|
|
1019
|
+
includeMetrics,
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
return agentErrorResult({
|
|
1023
|
+
agent,
|
|
1024
|
+
description: taskDescription,
|
|
1025
|
+
sessionId,
|
|
1026
|
+
runId,
|
|
1027
|
+
isNewConversation,
|
|
1028
|
+
longMessageContext,
|
|
1029
|
+
error: `Failed to talk to ${agent.name}: ${err.message}`,
|
|
1030
|
+
includeMetrics,
|
|
1031
|
+
});
|
|
1032
|
+
} finally {
|
|
1033
|
+
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
1034
|
+
activeAgentConversations.delete(sessionId);
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
|
|
1038
|
+
const turnPromise = finishTurn();
|
|
1039
|
+
|
|
1040
|
+
if (asyncMode) {
|
|
1041
|
+
turnPromise.catch((err) => {
|
|
1042
|
+
appendAgentConvoLog(sessionId, {
|
|
1043
|
+
role: 'error',
|
|
1044
|
+
agentId: agent.id,
|
|
1045
|
+
runId,
|
|
1046
|
+
description: taskDescription,
|
|
1047
|
+
error: err.message,
|
|
1048
|
+
timestamp: new Date().toISOString(),
|
|
1049
|
+
});
|
|
1050
|
+
activeAgentConversations.delete(sessionId);
|
|
1051
|
+
});
|
|
1052
|
+
return textResult(
|
|
1053
|
+
JSON.stringify(
|
|
1054
|
+
responsePayload({
|
|
1055
|
+
agent,
|
|
1056
|
+
description: taskDescription,
|
|
1057
|
+
sessionId,
|
|
1058
|
+
runId,
|
|
1059
|
+
status: 'running',
|
|
1060
|
+
isNewConversation,
|
|
1061
|
+
longMessageContext,
|
|
1062
|
+
recovery:
|
|
1063
|
+
'Use agents_get_conversation with this conversation_id to poll for the assistant response.',
|
|
1064
|
+
includeMetrics,
|
|
1065
|
+
}),
|
|
1066
|
+
null,
|
|
1067
|
+
2,
|
|
1068
|
+
),
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
if (foregroundTimeoutMs > 0) {
|
|
1073
|
+
const timeoutResult = await Promise.race([
|
|
1074
|
+
turnPromise.then((result) => ({ type: 'done', result })),
|
|
1075
|
+
new Promise((resolve) => setTimeout(() => resolve({ type: 'timeout' }), foregroundTimeoutMs)),
|
|
1076
|
+
]);
|
|
1077
|
+
if (timeoutResult.type === 'done') {
|
|
1078
|
+
return timeoutResult.result;
|
|
1079
|
+
}
|
|
1080
|
+
notifyCallerOnCompletion = true;
|
|
1081
|
+
appendStatusLog(sessionId, 'foreground_wait_timeout', {
|
|
1082
|
+
agentId: agent.id,
|
|
1083
|
+
agentName: agent.name,
|
|
1084
|
+
runId,
|
|
1085
|
+
description: taskDescription,
|
|
1086
|
+
timeoutMs: foregroundTimeoutMs,
|
|
1087
|
+
active: summarizeActiveConversation(sessionId),
|
|
1088
|
+
});
|
|
1089
|
+
updateActiveConversation(sessionId, { status: 'running' });
|
|
1090
|
+
return textResult(
|
|
1091
|
+
JSON.stringify(
|
|
1092
|
+
responsePayload({
|
|
1093
|
+
agent,
|
|
1094
|
+
description: taskDescription,
|
|
1095
|
+
sessionId,
|
|
1096
|
+
runId,
|
|
1097
|
+
status: 'timed_out',
|
|
1098
|
+
isNewConversation,
|
|
1099
|
+
longMessageContext,
|
|
1100
|
+
recovery:
|
|
1101
|
+
'The agent is still running. Use agents_get_conversation with this conversation_id/run_id to poll; a short completion notification will also be posted back to the caller session when available.',
|
|
1102
|
+
includeMetrics,
|
|
1103
|
+
extra: {
|
|
1104
|
+
timeout_ms: foregroundTimeoutMs,
|
|
1105
|
+
},
|
|
1106
|
+
}),
|
|
1107
|
+
null,
|
|
1108
|
+
2,
|
|
1109
|
+
),
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
return await turnPromise;
|
|
1114
|
+
} catch (err) {
|
|
1115
|
+
appendAgentConvoLog(sessionId, {
|
|
1116
|
+
role: 'error',
|
|
1117
|
+
agentId: agent?.id || agentLookup,
|
|
1118
|
+
runId,
|
|
1119
|
+
description: taskDescription,
|
|
1120
|
+
error: err.message,
|
|
1121
|
+
timestamp: new Date().toISOString(),
|
|
1122
|
+
});
|
|
1123
|
+
activeAgentConversations.delete(sessionId);
|
|
1124
|
+
if (err.name === 'AbortError') {
|
|
1125
|
+
return agentErrorResult({
|
|
1126
|
+
agent,
|
|
1127
|
+
description: taskDescription,
|
|
1128
|
+
sessionId,
|
|
1129
|
+
runId,
|
|
1130
|
+
isNewConversation,
|
|
1131
|
+
longMessageContext,
|
|
1132
|
+
error: `Conversation with ${agent.name} was cancelled.`,
|
|
1133
|
+
status: 'cancelled',
|
|
1134
|
+
includeMetrics,
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
return agentErrorResult({
|
|
1138
|
+
agent,
|
|
1139
|
+
description: taskDescription,
|
|
1140
|
+
sessionId,
|
|
1141
|
+
runId,
|
|
1142
|
+
isNewConversation,
|
|
1143
|
+
longMessageContext,
|
|
1144
|
+
error: `Failed to talk to ${agent.name}: ${err.message}`,
|
|
1145
|
+
includeMetrics,
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
module.exports = {
|
|
1151
|
+
handleTalkToAgent,
|
|
1152
|
+
activeAgentConversations,
|
|
1153
|
+
summarizeActiveConversation,
|
|
1154
|
+
MAX_INLINE_MESSAGE_CHARS,
|
|
1155
|
+
DEFAULT_FOREGROUND_TIMEOUT_MS,
|
|
1156
|
+
};
|