agents-bridge 0.1.0 → 0.1.2
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/LICENSE +21 -21
- package/README.md +162 -131
- package/bin/agent-bridge.mjs +11 -11
- package/config.example.json +22 -55
- package/config.multi-agent.example.json +67 -0
- package/dist/agents/agent-resolver.d.ts +12 -0
- package/dist/agents/agent-resolver.d.ts.map +1 -0
- package/dist/agents/agent-resolver.js +114 -0
- package/dist/agents/agent-resolver.js.map +1 -0
- package/dist/agents/agent-router.d.ts +8 -0
- package/dist/agents/agent-router.d.ts.map +1 -0
- package/dist/agents/agent-router.js +72 -0
- package/dist/agents/agent-router.js.map +1 -0
- package/dist/agents/cli-backends.d.ts +20 -27
- package/dist/agents/cli-backends.d.ts.map +1 -1
- package/dist/agents/cli-backends.js +29 -4
- package/dist/agents/cli-backends.js.map +1 -1
- package/dist/agents/cli-output.d.ts +35 -0
- package/dist/agents/cli-output.d.ts.map +1 -0
- package/dist/agents/cli-output.js +277 -0
- package/dist/agents/cli-output.js.map +1 -0
- package/dist/agents/cli-output.test.d.ts +2 -0
- package/dist/agents/cli-output.test.d.ts.map +1 -0
- package/dist/agents/cli-output.test.js +56 -0
- package/dist/agents/cli-output.test.js.map +1 -0
- package/dist/agents/cli-runner.d.ts +23 -0
- package/dist/agents/cli-runner.d.ts.map +1 -1
- package/dist/agents/cli-runner.js +244 -153
- package/dist/agents/cli-runner.js.map +1 -1
- package/dist/agents/cli-runner.test.d.ts +2 -0
- package/dist/agents/cli-runner.test.d.ts.map +1 -0
- package/dist/agents/cli-runner.test.js +119 -0
- package/dist/agents/cli-runner.test.js.map +1 -0
- package/dist/agents/cli-session-store.d.ts +47 -0
- package/dist/agents/cli-session-store.d.ts.map +1 -1
- package/dist/agents/cli-session-store.js +72 -0
- package/dist/agents/cli-session-store.js.map +1 -1
- package/dist/agents/cli-session-store.test.d.ts +2 -0
- package/dist/agents/cli-session-store.test.d.ts.map +1 -0
- package/dist/agents/cli-session-store.test.js +30 -0
- package/dist/agents/cli-session-store.test.js.map +1 -0
- package/dist/agents/codex-app-server-runtime.d.ts +72 -0
- package/dist/agents/codex-app-server-runtime.d.ts.map +1 -0
- package/dist/agents/codex-app-server-runtime.js +772 -0
- package/dist/agents/codex-app-server-runtime.js.map +1 -0
- package/dist/agents/codex-app-server-runtime.test.d.ts +2 -0
- package/dist/agents/codex-app-server-runtime.test.d.ts.map +1 -0
- package/dist/agents/codex-app-server-runtime.test.js +164 -0
- package/dist/agents/codex-app-server-runtime.test.js.map +1 -0
- package/dist/agents/model-selection.d.ts +3 -2
- package/dist/agents/model-selection.js +1 -1
- package/dist/channels/botlink.d.ts +2 -0
- package/dist/channels/botlink.js +73 -60
- package/dist/channels/botlink.js.map +1 -1
- package/dist/cli/cmd-doctor.d.ts.map +1 -1
- package/dist/cli/cmd-doctor.js +41 -5
- package/dist/cli/cmd-doctor.js.map +1 -1
- package/dist/cli/cmd-logs.js +1 -1
- package/dist/cli/cmd-logs.js.map +1 -1
- package/dist/cli/cmd-onboard.d.ts.map +1 -1
- package/dist/cli/cmd-onboard.js +16 -10
- package/dist/cli/cmd-onboard.js.map +1 -1
- package/dist/cli/cmd-start.d.ts.map +1 -1
- package/dist/cli/cmd-start.js +205 -32
- package/dist/cli/cmd-start.js.map +1 -1
- package/dist/cli/run-main.js +27 -27
- package/dist/config/agent-dirs.d.ts +12 -0
- package/dist/config/agent-dirs.d.ts.map +1 -1
- package/dist/config/agent-dirs.js +43 -2
- package/dist/config/agent-dirs.js.map +1 -1
- package/dist/config/config.js +1 -1
- package/dist/config/config.js.map +1 -1
- package/dist/config/merge-config.d.ts.map +1 -1
- package/dist/config/merge-config.js +16 -4
- package/dist/config/merge-config.js.map +1 -1
- package/dist/config/paths.d.ts +4 -0
- package/dist/config/paths.d.ts.map +1 -1
- package/dist/config/paths.js +7 -0
- package/dist/config/paths.js.map +1 -1
- package/dist/config/zod-schema.d.ts +2158 -239
- package/dist/config/zod-schema.d.ts.map +1 -1
- package/dist/config/zod-schema.js +100 -1
- package/dist/config/zod-schema.js.map +1 -1
- package/dist/entry.d.ts +1 -1
- package/dist/logger.d.ts +1 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +28 -5
- package/dist/logger.js.map +1 -1
- package/dist/types/index.d.ts +130 -3
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +55 -54
- package/dist/console-encoding.d.ts +0 -6
- package/dist/console-encoding.d.ts.map +0 -1
- package/dist/console-encoding.js +0 -35
- package/dist/console-encoding.js.map +0 -1
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { CLI_FRESH_WATCHDOG_DEFAULTS, CLI_RESUME_WATCHDOG_DEFAULTS, CLI_WATCHDOG_MIN_TIMEOUT_MS, normalizeProviderId, resolveCliBackendConfig, } from './cli-backends.js';
|
|
4
|
-
import {
|
|
4
|
+
import { createCliOutputProbe, isSessionExpiredError, parseCliOutput, } from './cli-output.js';
|
|
5
|
+
import { CodexAppServerRuntime, } from './codex-app-server-runtime.js';
|
|
6
|
+
import { CliSessionStore, parseAgentSessionKey } from './cli-session-store.js';
|
|
5
7
|
const CLI_RUN_QUEUES = new Map();
|
|
6
8
|
const DEFAULT_TIMEOUT_MS = 15 * 60 * 1_000;
|
|
7
|
-
const SESSION_EXPIRED_PATTERNS = [
|
|
8
|
-
/session expired/i,
|
|
9
|
-
/invalid session/i,
|
|
10
|
-
/conversation not found/i,
|
|
11
|
-
/thread not found/i,
|
|
12
|
-
/resume session not found/i,
|
|
13
|
-
];
|
|
14
9
|
class CliExecutionError extends Error {
|
|
15
10
|
reason;
|
|
16
11
|
stdout;
|
|
@@ -27,28 +22,41 @@ class CliExecutionError extends Error {
|
|
|
27
22
|
}
|
|
28
23
|
export class LocalCliTurnExecutor {
|
|
29
24
|
options;
|
|
25
|
+
codexRuntime = null;
|
|
30
26
|
constructor(options) {
|
|
31
27
|
this.options = options;
|
|
32
28
|
}
|
|
33
29
|
async run(params) {
|
|
34
30
|
const callbacks = params.callbacks ?? {};
|
|
35
31
|
const providerId = normalizeProviderId(params.provider);
|
|
36
|
-
const backendResolved = resolveCliBackendConfig(providerId, this.options.config);
|
|
32
|
+
const backendResolved = resolveCliBackendConfig(providerId, this.options.config, params.cliBackends);
|
|
37
33
|
if (!backendResolved) {
|
|
38
34
|
throw new Error(`Unknown CLI backend: ${params.provider}`);
|
|
39
35
|
}
|
|
40
36
|
const sessionStore = this.options.sessionStore ?? new CliSessionStore();
|
|
41
37
|
const envelope = params.envelope;
|
|
38
|
+
let existingSessionEntry = await sessionStore.get(envelope.sessionId);
|
|
39
|
+
const parsedAgentSession = parseAgentSessionKey(envelope.sessionId);
|
|
40
|
+
if (!existingSessionEntry && parsedAgentSession) {
|
|
41
|
+
existingSessionEntry = await sessionStore.getWithLegacyFallback(envelope.sessionId, parsedAgentSession.originalKey);
|
|
42
|
+
}
|
|
42
43
|
const sessionEntry = await sessionStore.ensureSession(envelope.sessionId, {
|
|
43
44
|
provider: providerId,
|
|
44
45
|
model: params.model,
|
|
45
46
|
});
|
|
46
47
|
const existingCliSessionId = await sessionStore.getCliSessionId(envelope.sessionId, providerId);
|
|
48
|
+
if (providerId === 'codex-cli' && !existingCliSessionId && existingSessionEntry?.provider === providerId) {
|
|
49
|
+
this.options.logger.warn({
|
|
50
|
+
provider: providerId,
|
|
51
|
+
sessionId: envelope.sessionId,
|
|
52
|
+
runtimeSessionId: sessionEntry.runtimeSessionId,
|
|
53
|
+
}, 'No persisted Codex thread id found for existing botlink session; next turn will start fresh');
|
|
54
|
+
}
|
|
47
55
|
await this.emitProcessing(callbacks);
|
|
48
56
|
const queueKey = backendResolved.config.serialize === false
|
|
49
57
|
? `${backendResolved.id}:${randomUUID()}`
|
|
50
58
|
: `${backendResolved.id}:${envelope.sessionId}`;
|
|
51
|
-
return enqueueCliRun(queueKey, async () => {
|
|
59
|
+
return enqueueCliRun(queueKey, async ({ queueWaitMs }) => {
|
|
52
60
|
try {
|
|
53
61
|
const result = await this.executeTurn({
|
|
54
62
|
envelope,
|
|
@@ -59,13 +67,25 @@ export class LocalCliTurnExecutor {
|
|
|
59
67
|
systemPrompt: params.systemPrompt,
|
|
60
68
|
backend: backendResolved.config,
|
|
61
69
|
cliSessionId: existingCliSessionId,
|
|
70
|
+
queueWaitMs,
|
|
62
71
|
runtimeSessionId: sessionEntry.runtimeSessionId,
|
|
72
|
+
agentEnv: params.agentEnv,
|
|
73
|
+
agentClearEnv: params.agentClearEnv,
|
|
63
74
|
});
|
|
64
75
|
if (result.cliSessionId) {
|
|
65
76
|
await sessionStore.setCliSessionId(envelope.sessionId, providerId, result.cliSessionId, {
|
|
66
77
|
model: params.model,
|
|
67
78
|
});
|
|
68
79
|
}
|
|
80
|
+
else if (providerId === 'codex-cli') {
|
|
81
|
+
this.options.logger.warn({
|
|
82
|
+
provider: providerId,
|
|
83
|
+
sessionId: envelope.sessionId,
|
|
84
|
+
runtimeSessionId: sessionEntry.runtimeSessionId,
|
|
85
|
+
isResume: result.isResume,
|
|
86
|
+
diagnostics: result.diagnostics,
|
|
87
|
+
}, 'Codex turn completed without a persisted thread id; future turns will remain fresh');
|
|
88
|
+
}
|
|
69
89
|
else {
|
|
70
90
|
await sessionStore.ensureSession(envelope.sessionId, {
|
|
71
91
|
provider: providerId,
|
|
@@ -77,8 +97,8 @@ export class LocalCliTurnExecutor {
|
|
|
77
97
|
catch (error) {
|
|
78
98
|
if (existingCliSessionId &&
|
|
79
99
|
isSessionExpiredError(error) &&
|
|
80
|
-
|
|
81
|
-
|
|
100
|
+
(providerId === 'codex-cli' ||
|
|
101
|
+
Boolean(backendResolved.config.resumeArgs && backendResolved.config.resumeArgs.length > 0))) {
|
|
82
102
|
this.options.logger.warn({ provider: providerId, sessionId: envelope.sessionId }, 'CLI session expired, clearing provider mapping and retrying fresh turn');
|
|
83
103
|
await sessionStore.clearCliSessionId(envelope.sessionId, providerId, { model: params.model });
|
|
84
104
|
const retried = await this.executeTurn({
|
|
@@ -90,7 +110,10 @@ export class LocalCliTurnExecutor {
|
|
|
90
110
|
systemPrompt: params.systemPrompt,
|
|
91
111
|
backend: backendResolved.config,
|
|
92
112
|
cliSessionId: undefined,
|
|
113
|
+
queueWaitMs,
|
|
93
114
|
runtimeSessionId: sessionEntry.runtimeSessionId,
|
|
115
|
+
agentEnv: params.agentEnv,
|
|
116
|
+
agentClearEnv: params.agentClearEnv,
|
|
94
117
|
});
|
|
95
118
|
if (retried.cliSessionId) {
|
|
96
119
|
await sessionStore.setCliSessionId(envelope.sessionId, providerId, retried.cliSessionId, {
|
|
@@ -107,6 +130,65 @@ export class LocalCliTurnExecutor {
|
|
|
107
130
|
await callbacks.onProcessing?.();
|
|
108
131
|
}
|
|
109
132
|
async executeTurn(params) {
|
|
133
|
+
if (params.providerId === 'codex-cli' && params.backend.runtimeMode === 'app-server') {
|
|
134
|
+
return this.executeCodexAppServerTurn(params);
|
|
135
|
+
}
|
|
136
|
+
return this.executeCliExecTurn(params);
|
|
137
|
+
}
|
|
138
|
+
async teardown() {
|
|
139
|
+
await this.codexRuntime?.teardown();
|
|
140
|
+
}
|
|
141
|
+
getCodexRuntime(backend) {
|
|
142
|
+
if (!this.codexRuntime) {
|
|
143
|
+
this.codexRuntime = this.options.createCodexRuntime?.(backend) ?? new CodexAppServerRuntime({
|
|
144
|
+
backend,
|
|
145
|
+
logger: this.options.logger,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return this.codexRuntime;
|
|
149
|
+
}
|
|
150
|
+
async executeCodexAppServerTurn(params) {
|
|
151
|
+
const normalizedModel = normalizeCliModel(params.model, params.backend);
|
|
152
|
+
const existingCliSessionId = params.cliSessionId?.trim() || undefined;
|
|
153
|
+
const isResume = Boolean(existingCliSessionId);
|
|
154
|
+
const promptPayload = buildPromptPayload(params.envelope, params.backend, true);
|
|
155
|
+
this.options.logger.info({
|
|
156
|
+
provider: params.providerId,
|
|
157
|
+
model: normalizedModel,
|
|
158
|
+
sessionId: params.envelope.sessionId,
|
|
159
|
+
cliSessionId: existingCliSessionId,
|
|
160
|
+
runtimeSessionId: params.runtimeSessionId,
|
|
161
|
+
isResume,
|
|
162
|
+
runtimeMode: 'app-server',
|
|
163
|
+
}, 'Starting Codex app-server turn');
|
|
164
|
+
const runtime = this.getCodexRuntime(params.backend);
|
|
165
|
+
const result = await runtime.run({
|
|
166
|
+
sessionId: params.envelope.sessionId,
|
|
167
|
+
runtimeSessionId: params.runtimeSessionId,
|
|
168
|
+
existingThreadId: existingCliSessionId,
|
|
169
|
+
model: normalizedModel,
|
|
170
|
+
workspaceDir: params.workspaceDir,
|
|
171
|
+
timeoutMs: params.timeoutMs,
|
|
172
|
+
noProgressTimeoutMs: resolveCliNoOutputTimeoutMs(params.backend, params.timeoutMs, isResume),
|
|
173
|
+
systemPrompt: resolveAppServerSystemPrompt(params.backend, params.systemPrompt, !existingCliSessionId),
|
|
174
|
+
prompt: promptPayload.prompt,
|
|
175
|
+
imagePaths: promptPayload.imagePaths,
|
|
176
|
+
});
|
|
177
|
+
return {
|
|
178
|
+
text: result.text,
|
|
179
|
+
provider: params.providerId,
|
|
180
|
+
model: params.model,
|
|
181
|
+
cliSessionId: result.cliSessionId,
|
|
182
|
+
isResume: result.isResume,
|
|
183
|
+
usage: result.usage,
|
|
184
|
+
diagnostics: {
|
|
185
|
+
...result.diagnostics,
|
|
186
|
+
queueWaitMs: params.queueWaitMs,
|
|
187
|
+
totalElapsedMs: result.diagnostics.totalElapsedMs + params.queueWaitMs,
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
async executeCliExecTurn(params) {
|
|
110
192
|
const normalizedModel = normalizeCliModel(params.model, params.backend);
|
|
111
193
|
const existingCliSessionId = params.cliSessionId?.trim() || undefined;
|
|
112
194
|
const useResume = Boolean(existingCliSessionId &&
|
|
@@ -116,7 +198,7 @@ export class LocalCliTurnExecutor {
|
|
|
116
198
|
backend: params.backend,
|
|
117
199
|
cliSessionId: existingCliSessionId,
|
|
118
200
|
});
|
|
119
|
-
const promptPayload = buildPromptPayload(params.envelope, params.backend);
|
|
201
|
+
const promptPayload = buildPromptPayload(params.envelope, params.backend, Boolean(params.backend.imageArg));
|
|
120
202
|
const promptInput = resolvePromptInput({
|
|
121
203
|
backend: params.backend,
|
|
122
204
|
prompt: promptPayload.prompt,
|
|
@@ -148,10 +230,18 @@ export class LocalCliTurnExecutor {
|
|
|
148
230
|
runtimeSessionId: params.runtimeSessionId,
|
|
149
231
|
isResume: useResume,
|
|
150
232
|
}, 'Starting CLI turn');
|
|
151
|
-
|
|
233
|
+
// Build env per design: process.env → backend.env → agent.env → clear
|
|
234
|
+
const env = { ...process.env, ...params.backend.env, ...(params.agentEnv ?? {}) };
|
|
152
235
|
for (const key of params.backend.clearEnv ?? []) {
|
|
153
236
|
delete env[key];
|
|
154
237
|
}
|
|
238
|
+
for (const key of params.agentClearEnv ?? []) {
|
|
239
|
+
delete env[key];
|
|
240
|
+
}
|
|
241
|
+
const probe = createCliOutputProbe({
|
|
242
|
+
backend: params.backend,
|
|
243
|
+
mode: outputMode,
|
|
244
|
+
});
|
|
155
245
|
const output = await executeCliCommand({
|
|
156
246
|
command: params.backend.command,
|
|
157
247
|
args,
|
|
@@ -160,22 +250,56 @@ export class LocalCliTurnExecutor {
|
|
|
160
250
|
stdin: promptInput.stdin,
|
|
161
251
|
timeoutMs: params.timeoutMs,
|
|
162
252
|
noOutputTimeoutMs: resolveCliNoOutputTimeoutMs(params.backend, params.timeoutMs, useResume),
|
|
253
|
+
onStdoutChunk(chunk, atMs) {
|
|
254
|
+
probe.onStdout(chunk, atMs);
|
|
255
|
+
},
|
|
256
|
+
onStderrChunk(chunk, atMs) {
|
|
257
|
+
probe.onStderr(chunk, atMs);
|
|
258
|
+
},
|
|
163
259
|
});
|
|
260
|
+
const probeSnapshot = probe.snapshot();
|
|
164
261
|
const parsed = parseCliOutput(output.stdout.trim(), outputMode, params.backend) ?? {
|
|
165
|
-
text: output.stdout.trim(),
|
|
262
|
+
text: probeSnapshot.lastText ?? output.stdout.trim(),
|
|
263
|
+
cliSessionId: probeSnapshot.cliSessionId,
|
|
264
|
+
usage: probeSnapshot.usage,
|
|
166
265
|
};
|
|
167
|
-
const cliSessionId = parsed.cliSessionId ?? existingCliSessionId ?? sessionIdToSend.sessionId;
|
|
266
|
+
const cliSessionId = parsed.cliSessionId ?? probeSnapshot.cliSessionId ?? existingCliSessionId ?? sessionIdToSend.sessionId;
|
|
267
|
+
const diagnostics = buildRunDiagnostics({
|
|
268
|
+
queueWaitMs: params.queueWaitMs,
|
|
269
|
+
commandElapsedMs: output.elapsedMs,
|
|
270
|
+
outputMode,
|
|
271
|
+
stdout: output.stdout,
|
|
272
|
+
stderr: output.stderr,
|
|
273
|
+
cliSessionId,
|
|
274
|
+
existingCliSessionId,
|
|
275
|
+
runtimeSessionId: sessionIdToSend.sessionId,
|
|
276
|
+
probe: probeSnapshot,
|
|
277
|
+
});
|
|
278
|
+
if (params.providerId === 'codex-cli' && !cliSessionId) {
|
|
279
|
+
this.options.logger.warn({
|
|
280
|
+
provider: params.providerId,
|
|
281
|
+
sessionId: params.envelope.sessionId,
|
|
282
|
+
runtimeSessionId: params.runtimeSessionId,
|
|
283
|
+
isResume: useResume,
|
|
284
|
+
diagnostics,
|
|
285
|
+
stdoutPreview: trimOutputPreview(output.stdout),
|
|
286
|
+
stderrPreview: trimOutputPreview(output.stderr),
|
|
287
|
+
}, 'Unable to extract Codex thread id from CLI output');
|
|
288
|
+
}
|
|
289
|
+
const text = parsed.text.trim() || probeSnapshot.lastText?.trim() || output.stdout.trim();
|
|
168
290
|
return {
|
|
169
|
-
text
|
|
291
|
+
text,
|
|
170
292
|
provider: params.providerId,
|
|
171
293
|
model: params.model,
|
|
172
294
|
cliSessionId,
|
|
173
295
|
isResume: useResume,
|
|
174
296
|
usage: parsed.usage,
|
|
297
|
+
diagnostics,
|
|
175
298
|
};
|
|
176
299
|
}
|
|
177
300
|
}
|
|
178
301
|
async function enqueueCliRun(key, task) {
|
|
302
|
+
const queuedAt = Date.now();
|
|
179
303
|
const previous = CLI_RUN_QUEUES.get(key) ?? Promise.resolve();
|
|
180
304
|
let release;
|
|
181
305
|
const current = new Promise((resolve) => {
|
|
@@ -185,7 +309,9 @@ async function enqueueCliRun(key, task) {
|
|
|
185
309
|
CLI_RUN_QUEUES.set(key, tail);
|
|
186
310
|
await previous.catch(() => undefined);
|
|
187
311
|
try {
|
|
188
|
-
return await task(
|
|
312
|
+
return await task({
|
|
313
|
+
queueWaitMs: Date.now() - queuedAt,
|
|
314
|
+
});
|
|
189
315
|
}
|
|
190
316
|
finally {
|
|
191
317
|
release();
|
|
@@ -216,6 +342,20 @@ function resolveSystemPrompt(backend, systemPrompt, isNewSession, useResume) {
|
|
|
216
342
|
}
|
|
217
343
|
return trimmed;
|
|
218
344
|
}
|
|
345
|
+
function resolveAppServerSystemPrompt(backend, systemPrompt, isNewThread) {
|
|
346
|
+
const trimmed = systemPrompt?.trim();
|
|
347
|
+
if (!trimmed) {
|
|
348
|
+
return undefined;
|
|
349
|
+
}
|
|
350
|
+
const when = backend.systemPromptWhen ?? 'first';
|
|
351
|
+
if (when === 'never') {
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
354
|
+
if (when === 'first' && !isNewThread) {
|
|
355
|
+
return undefined;
|
|
356
|
+
}
|
|
357
|
+
return trimmed;
|
|
358
|
+
}
|
|
219
359
|
function resolveSessionIdToSend(params) {
|
|
220
360
|
const mode = params.backend.sessionMode ?? 'always';
|
|
221
361
|
const existing = params.cliSessionId?.trim();
|
|
@@ -240,7 +380,7 @@ function resolvePromptInput(params) {
|
|
|
240
380
|
}
|
|
241
381
|
return { promptArg: params.prompt };
|
|
242
382
|
}
|
|
243
|
-
function buildPromptPayload(envelope, backend) {
|
|
383
|
+
function buildPromptPayload(envelope, backend, useSeparateImageInputs = Boolean(backend.imageArg)) {
|
|
244
384
|
const imagePaths = envelope.content.attachments
|
|
245
385
|
.filter((attachment) => attachment.type === 'photo' && attachment.localPath)
|
|
246
386
|
.map((attachment) => attachment.localPath);
|
|
@@ -249,14 +389,14 @@ function buildPromptPayload(envelope, backend) {
|
|
|
249
389
|
return `Attachment: ${attachment.localPath}${name}`;
|
|
250
390
|
});
|
|
251
391
|
const promptParts = [envelope.content.text.trim()];
|
|
252
|
-
if (!
|
|
392
|
+
if (!useSeparateImageInputs && imagePaths.length > 0) {
|
|
253
393
|
promptParts.push(...imagePaths.map((imagePath) => `Image: ${imagePath}`));
|
|
254
394
|
}
|
|
255
395
|
const nonImageFiles = fileLines.filter((line) => !imagePaths.some((imagePath) => line.includes(imagePath)));
|
|
256
396
|
promptParts.push(...nonImageFiles);
|
|
257
397
|
return {
|
|
258
398
|
prompt: promptParts.filter(Boolean).join('\n\n').trim(),
|
|
259
|
-
imagePaths:
|
|
399
|
+
imagePaths: useSeparateImageInputs && imagePaths.length > 0 ? imagePaths : undefined,
|
|
260
400
|
};
|
|
261
401
|
}
|
|
262
402
|
function buildCliArgs(params) {
|
|
@@ -309,10 +449,11 @@ function resolveCliNoOutputTimeoutMs(backend, timeoutMs, useResume) {
|
|
|
309
449
|
}
|
|
310
450
|
async function executeCliCommand(params) {
|
|
311
451
|
return new Promise((resolve, reject) => {
|
|
312
|
-
|
|
313
|
-
|
|
452
|
+
const stdoutChunks = [];
|
|
453
|
+
const stderrChunks = [];
|
|
314
454
|
let settled = false;
|
|
315
455
|
let timedOutReason = null;
|
|
456
|
+
const startedAt = Date.now();
|
|
316
457
|
const child = spawn(params.command, params.args, {
|
|
317
458
|
cwd: params.cwd,
|
|
318
459
|
env: params.env,
|
|
@@ -330,7 +471,11 @@ async function executeCliCommand(params) {
|
|
|
330
471
|
reject(error);
|
|
331
472
|
return;
|
|
332
473
|
}
|
|
333
|
-
resolve(result ?? {
|
|
474
|
+
resolve(result ?? {
|
|
475
|
+
stdout: decodeCliOutput(Buffer.concat(stdoutChunks)),
|
|
476
|
+
stderr: decodeCliOutput(Buffer.concat(stderrChunks)),
|
|
477
|
+
elapsedMs: Date.now() - startedAt,
|
|
478
|
+
});
|
|
334
479
|
};
|
|
335
480
|
const armNoOutputTimer = () => {
|
|
336
481
|
clearTimeout(noOutputTimer);
|
|
@@ -346,21 +491,26 @@ async function executeCliCommand(params) {
|
|
|
346
491
|
let noOutputTimer = setTimeout(() => undefined, params.noOutputTimeoutMs);
|
|
347
492
|
armNoOutputTimer();
|
|
348
493
|
child.on('error', (error) => {
|
|
349
|
-
finish(new CliExecutionError(error.message, 'spawn-error',
|
|
494
|
+
finish(new CliExecutionError(error.message, 'spawn-error', decodeCliOutput(Buffer.concat(stdoutChunks)), decodeCliOutput(Buffer.concat(stderrChunks))));
|
|
350
495
|
});
|
|
351
|
-
child.stdout?.setEncoding('utf8');
|
|
352
|
-
child.stderr?.setEncoding('utf8');
|
|
353
496
|
child.stdout?.on('data', (chunk) => {
|
|
354
|
-
|
|
497
|
+
const buffer = typeof chunk === 'string' ? Buffer.from(chunk) : chunk;
|
|
498
|
+
stdoutChunks.push(buffer);
|
|
499
|
+
params.onStdoutChunk?.(decodeCliOutput(buffer), Date.now() - startedAt);
|
|
355
500
|
armNoOutputTimer();
|
|
356
501
|
});
|
|
357
502
|
child.stderr?.on('data', (chunk) => {
|
|
358
|
-
|
|
503
|
+
const buffer = typeof chunk === 'string' ? Buffer.from(chunk) : chunk;
|
|
504
|
+
stderrChunks.push(buffer);
|
|
505
|
+
params.onStderrChunk?.(decodeCliOutput(buffer), Date.now() - startedAt);
|
|
359
506
|
armNoOutputTimer();
|
|
360
507
|
});
|
|
361
508
|
child.on('close', (code) => {
|
|
509
|
+
const stdout = decodeCliOutput(Buffer.concat(stdoutChunks));
|
|
510
|
+
const stderr = decodeCliOutput(Buffer.concat(stderrChunks));
|
|
362
511
|
const trimmedStdout = stdout.trim();
|
|
363
512
|
const trimmedStderr = stderr.trim();
|
|
513
|
+
const elapsedMs = Date.now() - startedAt;
|
|
364
514
|
if (timedOutReason) {
|
|
365
515
|
const timeoutMessage = timedOutReason === 'overall-timeout'
|
|
366
516
|
? `CLI exceeded timeout (${Math.round(params.timeoutMs / 1000)}s) and was terminated.`
|
|
@@ -372,7 +522,7 @@ async function executeCliCommand(params) {
|
|
|
372
522
|
finish(new CliExecutionError(trimmedStderr || trimmedStdout || `CLI failed with exit code ${code ?? 'unknown'}.`, 'exit', trimmedStdout, trimmedStderr, code));
|
|
373
523
|
return;
|
|
374
524
|
}
|
|
375
|
-
finish(undefined, { stdout: trimmedStdout, stderr: trimmedStderr });
|
|
525
|
+
finish(undefined, { stdout: trimmedStdout, stderr: trimmedStderr, elapsedMs });
|
|
376
526
|
});
|
|
377
527
|
if (params.stdin !== undefined && child.stdin) {
|
|
378
528
|
child.stdin.write(params.stdin);
|
|
@@ -380,145 +530,86 @@ async function executeCliCommand(params) {
|
|
|
380
530
|
child.stdin?.end();
|
|
381
531
|
});
|
|
382
532
|
}
|
|
383
|
-
function
|
|
384
|
-
if (
|
|
385
|
-
return
|
|
533
|
+
function decodeCliOutput(buffer) {
|
|
534
|
+
if (buffer.length === 0) {
|
|
535
|
+
return '';
|
|
536
|
+
}
|
|
537
|
+
const utf8 = buffer.toString('utf8');
|
|
538
|
+
if (!shouldTryWindowsLegacyDecode(utf8)) {
|
|
539
|
+
return utf8;
|
|
386
540
|
}
|
|
387
|
-
|
|
388
|
-
|
|
541
|
+
for (const encoding of ['gb18030', 'gbk']) {
|
|
542
|
+
const decoded = tryDecodeWithEncoding(buffer, encoding);
|
|
543
|
+
if (decoded && decoded !== utf8) {
|
|
544
|
+
return decoded;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return utf8;
|
|
548
|
+
}
|
|
549
|
+
function shouldTryWindowsLegacyDecode(decoded) {
|
|
550
|
+
if (process.platform !== 'win32') {
|
|
551
|
+
return false;
|
|
389
552
|
}
|
|
390
|
-
|
|
391
|
-
|
|
553
|
+
const replacementCount = (decoded.match(/\uFFFD/g) ?? []).length;
|
|
554
|
+
if (replacementCount > 0) {
|
|
555
|
+
return true;
|
|
392
556
|
}
|
|
393
|
-
|
|
557
|
+
// Common mojibake markers when UTF-8/Chinese text is decoded with the wrong code page.
|
|
558
|
+
return ['鈥', '銆', '锛', '鏈', '鍦', '浠', '闂'].some((marker) => decoded.includes(marker));
|
|
394
559
|
}
|
|
395
|
-
function
|
|
560
|
+
function tryDecodeWithEncoding(buffer, encoding) {
|
|
396
561
|
try {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
return null;
|
|
400
|
-
}
|
|
401
|
-
return {
|
|
402
|
-
text: (collectText(parsed.message) ||
|
|
403
|
-
collectText(parsed.content) ||
|
|
404
|
-
collectText(parsed.result) ||
|
|
405
|
-
collectText(parsed)).trim(),
|
|
406
|
-
cliSessionId: pickCliSessionId(parsed, backend),
|
|
407
|
-
usage: isRecord(parsed.usage) ? toUsage(parsed.usage) : undefined,
|
|
408
|
-
};
|
|
562
|
+
// TextDecoder coverage depends on the Node/ICU build; guard runtime support.
|
|
563
|
+
return new TextDecoder(encoding, { fatal: false }).decode(buffer);
|
|
409
564
|
}
|
|
410
565
|
catch {
|
|
411
566
|
return null;
|
|
412
567
|
}
|
|
413
568
|
}
|
|
414
|
-
function
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
.
|
|
418
|
-
|
|
419
|
-
if (lines.length === 0) {
|
|
420
|
-
return null;
|
|
421
|
-
}
|
|
422
|
-
let cliSessionId;
|
|
423
|
-
let usage;
|
|
424
|
-
let lastText = '';
|
|
425
|
-
for (const line of lines) {
|
|
426
|
-
try {
|
|
427
|
-
const parsed = JSON.parse(line);
|
|
428
|
-
if (!isRecord(parsed)) {
|
|
429
|
-
continue;
|
|
430
|
-
}
|
|
431
|
-
cliSessionId = cliSessionId ?? pickCliSessionId(parsed, backend) ?? getString(parsed.thread_id);
|
|
432
|
-
if (isRecord(parsed.usage)) {
|
|
433
|
-
usage = toUsage(parsed.usage) ?? usage;
|
|
434
|
-
}
|
|
435
|
-
const candidate = (collectText(parsed.item) ||
|
|
436
|
-
collectText(parsed.message) ||
|
|
437
|
-
collectText(parsed.delta) ||
|
|
438
|
-
collectText(parsed.result)).trim();
|
|
439
|
-
if (candidate) {
|
|
440
|
-
lastText = candidate;
|
|
441
|
-
}
|
|
569
|
+
function buildRunDiagnostics(params) {
|
|
570
|
+
let cliSessionIdSource;
|
|
571
|
+
if (params.cliSessionId) {
|
|
572
|
+
if (params.probe.cliSessionId && params.cliSessionId === params.probe.cliSessionId) {
|
|
573
|
+
cliSessionIdSource = 'stream';
|
|
442
574
|
}
|
|
443
|
-
|
|
444
|
-
|
|
575
|
+
else if (params.existingCliSessionId && params.cliSessionId === params.existingCliSessionId) {
|
|
576
|
+
cliSessionIdSource = 'existing';
|
|
445
577
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
return null;
|
|
449
|
-
}
|
|
450
|
-
return { text: lastText, cliSessionId, usage };
|
|
451
|
-
}
|
|
452
|
-
function pickCliSessionId(parsed, backend) {
|
|
453
|
-
const fields = backend.sessionIdFields ?? ['session_id', 'sessionId', 'conversation_id', 'conversationId'];
|
|
454
|
-
for (const field of fields) {
|
|
455
|
-
const value = getString(parsed[field]);
|
|
456
|
-
if (value) {
|
|
457
|
-
return value;
|
|
578
|
+
else if (params.runtimeSessionId && params.cliSessionId === params.runtimeSessionId) {
|
|
579
|
+
cliSessionIdSource = 'runtime';
|
|
458
580
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
}
|
|
462
|
-
function collectText(value) {
|
|
463
|
-
if (!value) {
|
|
464
|
-
return '';
|
|
465
|
-
}
|
|
466
|
-
if (typeof value === 'string') {
|
|
467
|
-
return value;
|
|
468
|
-
}
|
|
469
|
-
if (Array.isArray(value)) {
|
|
470
|
-
return value.map((entry) => collectText(entry)).filter(Boolean).join('\n');
|
|
471
|
-
}
|
|
472
|
-
if (!isRecord(value)) {
|
|
473
|
-
return '';
|
|
474
|
-
}
|
|
475
|
-
if (typeof value.text === 'string') {
|
|
476
|
-
return value.text;
|
|
477
|
-
}
|
|
478
|
-
if (typeof value.content === 'string') {
|
|
479
|
-
return value.content;
|
|
480
|
-
}
|
|
481
|
-
if (Array.isArray(value.content)) {
|
|
482
|
-
return value.content.map((entry) => collectText(entry)).filter(Boolean).join('\n');
|
|
483
|
-
}
|
|
484
|
-
if (isRecord(value.message)) {
|
|
485
|
-
return collectText(value.message);
|
|
486
|
-
}
|
|
487
|
-
if (isRecord(value.item)) {
|
|
488
|
-
return collectText(value.item);
|
|
489
|
-
}
|
|
490
|
-
return '';
|
|
491
|
-
}
|
|
492
|
-
function toUsage(raw) {
|
|
493
|
-
const pick = (...keys) => {
|
|
494
|
-
for (const key of keys) {
|
|
495
|
-
const value = raw[key];
|
|
496
|
-
if (typeof value === 'number' && value >= 0) {
|
|
497
|
-
return value;
|
|
498
|
-
}
|
|
581
|
+
else {
|
|
582
|
+
cliSessionIdSource = 'parsed';
|
|
499
583
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
584
|
+
}
|
|
585
|
+
return {
|
|
586
|
+
queueWaitMs: params.queueWaitMs,
|
|
587
|
+
commandElapsedMs: params.commandElapsedMs,
|
|
588
|
+
totalElapsedMs: params.queueWaitMs + params.commandElapsedMs,
|
|
589
|
+
outputMode: params.outputMode,
|
|
590
|
+
runtimeMode: 'exec',
|
|
591
|
+
hadStdout: Boolean(params.stdout),
|
|
592
|
+
hadStderr: Boolean(params.stderr),
|
|
593
|
+
firstStdoutMs: params.probe.firstStdoutMs,
|
|
594
|
+
firstStderrMs: params.probe.firstStderrMs,
|
|
595
|
+
threadStartedMs: params.probe.threadStartedMs,
|
|
596
|
+
firstAgentMessageMs: params.probe.firstAgentMessageMs,
|
|
597
|
+
turnCompletedMs: params.probe.turnCompletedMs,
|
|
598
|
+
reconnectCount: params.probe.reconnectCount,
|
|
599
|
+
fellBackToHttps: params.probe.fellBackToHttps,
|
|
600
|
+
threadOperation: params.existingCliSessionId ? 'resume' : 'start',
|
|
601
|
+
threadOperationElapsedMs: params.probe.threadStartedMs,
|
|
602
|
+
cliSessionIdSource,
|
|
508
603
|
};
|
|
509
|
-
|
|
604
|
+
}
|
|
605
|
+
function trimOutputPreview(raw, limit = 1200) {
|
|
606
|
+
const normalized = raw.trim();
|
|
607
|
+
if (!normalized) {
|
|
510
608
|
return undefined;
|
|
511
609
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
return SESSION_EXPIRED_PATTERNS.some((pattern) => pattern.test(message));
|
|
517
|
-
}
|
|
518
|
-
function isRecord(value) {
|
|
519
|
-
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
520
|
-
}
|
|
521
|
-
function getString(value) {
|
|
522
|
-
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
610
|
+
if (normalized.length <= limit) {
|
|
611
|
+
return normalized;
|
|
612
|
+
}
|
|
613
|
+
return `${normalized.slice(0, limit)}...`;
|
|
523
614
|
}
|
|
524
615
|
//# sourceMappingURL=cli-runner.js.map
|