mixdog 0.9.44 → 0.9.46
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/package.json +5 -2
- package/scripts/agent-dispatch-abort-compose-test.mjs +31 -0
- package/scripts/agent-model-liveness-test.mjs +618 -0
- package/scripts/agent-trace-io-test.mjs +68 -4
- package/scripts/bench-run.mjs +30 -3
- package/scripts/compact-pressure-test.mjs +187 -4
- package/scripts/compact-prior-context-flatten-test.mjs +252 -0
- package/scripts/compact-trigger-migration-smoke.mjs +8 -6
- package/scripts/compacted-placeholder-scrub-test.mjs +20 -34
- package/scripts/context-mcp-metering-test.mjs +75 -0
- package/scripts/explore-prompt-policy-test.mjs +15 -16
- package/scripts/explore-timeout-cancel-test.mjs +345 -0
- package/scripts/headless-pristine-execution-test.mjs +614 -0
- package/scripts/internal-comms-smoke.mjs +15 -6
- package/scripts/live-worker-smoke.mjs +1 -1
- package/scripts/memory-core-input-test.mjs +137 -0
- package/scripts/memory-rule-contract-test.mjs +5 -5
- package/scripts/openai-oauth-refresh-race-test.mjs +120 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +26 -0
- package/scripts/parent-abort-link-test.mjs +22 -0
- package/scripts/provider-toolcall-test.mjs +22 -0
- package/scripts/reactive-compact-persist-smoke.mjs +8 -4
- package/scripts/session-bench.mjs +3 -70
- package/scripts/task-bench.mjs +0 -2
- package/scripts/terminal-bench-isolation-guards-test.mjs +102 -0
- package/scripts/tool-smoke.mjs +21 -21
- package/scripts/tool-tui-presentation-test.mjs +68 -0
- package/scripts/tui-transcript-jitter-harness-entry.jsx +91 -10
- package/src/app.mjs +28 -103
- package/src/cli.mjs +17 -13
- package/src/headless-command.mjs +139 -0
- package/src/headless-role.mjs +121 -10
- package/src/help.mjs +4 -1
- package/src/rules/agent/00-common.md +3 -3
- package/src/rules/agent/00-core.md +8 -9
- package/src/rules/agent/20-skip-protocol.md +2 -3
- package/src/rules/agent/30-explorer.md +50 -56
- package/src/rules/agent/40-cycle1-agent.md +10 -12
- package/src/rules/agent/41-cycle2-agent.md +12 -9
- package/src/rules/agent/42-cycle3-agent.md +4 -6
- package/src/rules/lead/01-general.md +5 -6
- package/src/rules/lead/02-channels.md +1 -1
- package/src/rules/lead/lead-brief.md +14 -17
- package/src/rules/lead/lead-tool.md +3 -3
- package/src/rules/shared/01-tool.md +41 -43
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +46 -10
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +44 -31
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +18 -3
- package/src/runtime/agent/orchestrator/config.mjs +96 -30
- package/src/runtime/agent/orchestrator/context/collect.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +18 -5
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +123 -3
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +12 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +148 -30
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +5 -7
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +62 -14
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +141 -19
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +12 -4
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +19 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +85 -17
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -8
- package/src/runtime/agent/orchestrator/providers/registry.mjs +47 -17
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +159 -20
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +83 -10
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +75 -7
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +4 -374
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +0 -75
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +0 -5
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +20 -3
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +32 -16
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +5 -2
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +86 -15
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +30 -27
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +1 -14
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +21 -27
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +1 -3
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +1 -51
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +26 -0
- package/src/runtime/memory/index.mjs +0 -1
- package/src/runtime/memory/lib/core-memory-store.mjs +44 -24
- package/src/runtime/memory/lib/http-router.mjs +0 -193
- package/src/runtime/memory/lib/memory-action-handlers.mjs +37 -11
- package/src/runtime/memory/tool-defs.mjs +5 -6
- package/src/runtime/shared/config.mjs +11 -34
- package/src/runtime/shared/pristine-execution-contract.json +75 -0
- package/src/runtime/shared/pristine-execution.mjs +356 -0
- package/src/runtime/shared/provider-api-key.mjs +43 -0
- package/src/runtime/shared/provider-auth-binding.mjs +21 -0
- package/src/runtime/shared/update-checker.mjs +40 -10
- package/src/session-runtime/context-status.mjs +61 -13
- package/src/session-runtime/mcp-glue.mjs +29 -2
- package/src/session-runtime/plugin-mcp.mjs +7 -0
- package/src/session-runtime/resource-api.mjs +38 -5
- package/src/session-runtime/runtime-core.mjs +5 -1
- package/src/session-runtime/session-turn-api.mjs +14 -2
- package/src/session-runtime/settings-api.mjs +5 -0
- package/src/session-runtime/tool-catalog.mjs +13 -2
- package/src/session-runtime/tool-defs.mjs +1 -3
- package/src/standalone/agent-task-status.mjs +50 -11
- package/src/standalone/agent-tool/tool-def.mjs +1 -1
- package/src/standalone/explore-tool.mjs +257 -49
- package/src/standalone/seeds.mjs +1 -0
- package/src/tui/App.jsx +26 -16
- package/src/tui/app/use-transcript-scroll.mjs +4 -3
- package/src/tui/app/use-transcript-window.mjs +12 -21
- package/src/tui/components/ContextPanel.jsx +19 -25
- package/src/tui/dist/index.mjs +89 -78
- package/src/tui/engine/agent-envelope.mjs +16 -5
- package/src/tui/engine/labels.mjs +1 -1
- package/src/workflows/default/WORKFLOW.md +21 -51
- package/src/workflows/solo/WORKFLOW.md +12 -17
- package/src/workflows/solo-review/WORKFLOW.md +0 -47
|
@@ -102,10 +102,26 @@ function firstByteCompatStreamError(label) {
|
|
|
102
102
|
return err;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
async function nextAsyncWithWatchdog(iterator, {
|
|
105
|
+
async function nextAsyncWithWatchdog(iterator, {
|
|
106
|
+
signal,
|
|
107
|
+
idleMs,
|
|
108
|
+
idleDeadlineAt,
|
|
109
|
+
idleEnabled,
|
|
110
|
+
idleLabel,
|
|
111
|
+
emittedToolCall,
|
|
112
|
+
} = {}) {
|
|
106
113
|
let idleTimer = null;
|
|
107
114
|
let idleReject = null;
|
|
108
115
|
let idleTimedOut = false;
|
|
116
|
+
let iteratorCloseRequested = false;
|
|
117
|
+
const closeIterator = () => {
|
|
118
|
+
if (iteratorCloseRequested) return;
|
|
119
|
+
iteratorCloseRequested = true;
|
|
120
|
+
try {
|
|
121
|
+
const closing = iterator?.return?.();
|
|
122
|
+
if (closing && typeof closing.catch === 'function') closing.catch(() => {});
|
|
123
|
+
} catch { /* closing must never replace the watchdog/abort error */ }
|
|
124
|
+
};
|
|
109
125
|
// Double-dispatch guard (reviewer High): if a tool call was already emitted
|
|
110
126
|
// this stream, a stall must be unsafe-to-retry so withRetry() won't replay
|
|
111
127
|
// the turn and re-run the side-effecting tool. `emittedToolCall` may be a
|
|
@@ -117,6 +133,10 @@ async function nextAsyncWithWatchdog(iterator, { signal, idleMs, idleEnabled, id
|
|
|
117
133
|
const armIdle = () => {
|
|
118
134
|
if (!idleEnabled || !(idleMs > 0)) return;
|
|
119
135
|
if (idleTimer) clearTimeout(idleTimer);
|
|
136
|
+
const deadline = Number(idleDeadlineAt);
|
|
137
|
+
const delayMs = Number.isFinite(deadline) && deadline > 0
|
|
138
|
+
? Math.max(0, deadline - Date.now())
|
|
139
|
+
: idleMs;
|
|
120
140
|
idleTimer = setTimeout(() => {
|
|
121
141
|
idleTimedOut = true;
|
|
122
142
|
// SEMANTIC idle abort: this timer is (re)armed only around waiting
|
|
@@ -125,12 +145,13 @@ async function nextAsyncWithWatchdog(iterator, { signal, idleMs, idleEnabled, id
|
|
|
125
145
|
// StreamStalledError so the retry-classifier treats it as a stream
|
|
126
146
|
// failure (owner gets notified) rather than a user cancel.
|
|
127
147
|
const e = streamStalledError(idleLabel || 'compat SSE', idleMs, { emittedToolCall: didEmitToolCall() });
|
|
148
|
+
closeIterator();
|
|
128
149
|
if (idleReject) {
|
|
129
150
|
const r = idleReject;
|
|
130
151
|
idleReject = null;
|
|
131
152
|
r(e);
|
|
132
153
|
}
|
|
133
|
-
},
|
|
154
|
+
}, delayMs);
|
|
134
155
|
if (typeof idleTimer.unref === 'function') idleTimer.unref();
|
|
135
156
|
};
|
|
136
157
|
armIdle();
|
|
@@ -139,6 +160,7 @@ async function nextAsyncWithWatchdog(iterator, { signal, idleMs, idleEnabled, id
|
|
|
139
160
|
idleReject = reject;
|
|
140
161
|
if (signal?.aborted) {
|
|
141
162
|
const reason = signal.reason;
|
|
163
|
+
closeIterator();
|
|
142
164
|
reject(reason instanceof Error ? reason : new Error('compat stream aborted'));
|
|
143
165
|
return;
|
|
144
166
|
}
|
|
@@ -146,6 +168,7 @@ async function nextAsyncWithWatchdog(iterator, { signal, idleMs, idleEnabled, id
|
|
|
146
168
|
if (signal) {
|
|
147
169
|
onAbort = () => {
|
|
148
170
|
const reason = signal.reason;
|
|
171
|
+
closeIterator();
|
|
149
172
|
reject(reason instanceof Error ? reason : new Error('compat stream aborted'));
|
|
150
173
|
};
|
|
151
174
|
signal.addEventListener('abort', onAbort, { once: true });
|
|
@@ -293,14 +316,34 @@ function isMaxOutputIncompleteReason(reason) {
|
|
|
293
316
|
return /^(?:max_output_tokens|max_tokens|length|output_token_limit)$/i.test(String(reason || '').trim());
|
|
294
317
|
}
|
|
295
318
|
|
|
296
|
-
export async function consumeCompatChatCompletionStream(stream, {
|
|
319
|
+
export async function consumeCompatChatCompletionStream(stream, {
|
|
320
|
+
signal,
|
|
321
|
+
label,
|
|
322
|
+
onStreamDelta,
|
|
323
|
+
onToolCall,
|
|
324
|
+
onTextDelta,
|
|
325
|
+
parseToolCalls,
|
|
326
|
+
knownToolNames,
|
|
327
|
+
semanticIdleTimeoutMs,
|
|
328
|
+
} = {}) {
|
|
329
|
+
// Reaching the consumer means the HTTP response/stream object exists.
|
|
330
|
+
// Record transport health without satisfying semantic model activity.
|
|
331
|
+
try { onStreamDelta?.('transport'); } catch {}
|
|
297
332
|
const iterator = stream[Symbol.asyncIterator]();
|
|
298
333
|
const firstByteTimeout = createTimeoutSignal(signal, PROVIDER_FIRST_BYTE_TIMEOUT_MS, `${label} first byte`);
|
|
299
|
-
const
|
|
334
|
+
const idleOverrideEnabled = Number.isFinite(Number(semanticIdleTimeoutMs)) && Number(semanticIdleTimeoutMs) > 0;
|
|
335
|
+
const idleEnabled = idleOverrideEnabled || PROVIDER_SSE_IDLE_WATCHDOG_ENABLED;
|
|
300
336
|
// Per-event (last-event-relative) SEMANTIC idle: nextAsyncWithWatchdog arms
|
|
301
337
|
// the timer only while awaiting the NEXT stream event, so a stream that
|
|
302
338
|
// emits some deltas then goes silent trips it within the window.
|
|
303
|
-
const idleMs =
|
|
339
|
+
const idleMs = Number.isFinite(Number(semanticIdleTimeoutMs)) && Number(semanticIdleTimeoutMs) > 0
|
|
340
|
+
? Number(semanticIdleTimeoutMs)
|
|
341
|
+
: PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS;
|
|
342
|
+
let semanticIdleDeadlineAt = 0;
|
|
343
|
+
const reportProgress = (kind) => {
|
|
344
|
+
if (kind !== 'transport') semanticIdleDeadlineAt = Date.now() + idleMs;
|
|
345
|
+
try { onStreamDelta?.(kind); } catch {}
|
|
346
|
+
};
|
|
304
347
|
let sawFirstEvent = false;
|
|
305
348
|
let content = '';
|
|
306
349
|
let reasoningContent = '';
|
|
@@ -318,6 +361,14 @@ export async function consumeCompatChatCompletionStream(stream, { signal, label,
|
|
|
318
361
|
// Fix 2: one dedupe per stream, shared by the synthetic leaked-call
|
|
319
362
|
// dispatch and every native emit so an identical (name,args) fires once.
|
|
320
363
|
const _toolDedupe = createToolCallDedupe();
|
|
364
|
+
// Persistent stream state: leaked calls dispatch eagerly, before the final
|
|
365
|
+
// native parse. If the iterator later fails this latch makes the failure
|
|
366
|
+
// unsafe-to-retry so the eager side effect cannot run twice.
|
|
367
|
+
const streamEmitState = {
|
|
368
|
+
emittedToolCallKeys: new Set(),
|
|
369
|
+
emittedToolCall: false,
|
|
370
|
+
_toolDedupe,
|
|
371
|
+
};
|
|
321
372
|
// Leaked tool-call guard: the model sometimes emits a tool call as plain
|
|
322
373
|
// text (XML `<invoke>`/`<function_calls>` or gpt-oss harmony
|
|
323
374
|
// `<|channel|>...to=functions.NAME...<|call|>`) inside `delta.content`
|
|
@@ -328,8 +379,8 @@ export async function consumeCompatChatCompletionStream(stream, { signal, label,
|
|
|
328
379
|
const leakGuard = createLeakGuard({ knownToolNames, harmony: true });
|
|
329
380
|
const dispatchLeakedCall = (recovered) => {
|
|
330
381
|
const call = synthLeakedOpenAICall(recovered);
|
|
331
|
-
|
|
332
|
-
|
|
382
|
+
emitCompatToolCallOnce(streamEmitState, call, onToolCall);
|
|
383
|
+
reportProgress('tool');
|
|
333
384
|
return call;
|
|
334
385
|
};
|
|
335
386
|
const leakedCalls = [];
|
|
@@ -337,6 +388,7 @@ export async function consumeCompatChatCompletionStream(stream, { signal, label,
|
|
|
337
388
|
const { text, calls } = leakGuard.push(delta);
|
|
338
389
|
if (text) {
|
|
339
390
|
content += text;
|
|
391
|
+
reportProgress('text');
|
|
340
392
|
if (onTextDelta) {
|
|
341
393
|
emittedText = true;
|
|
342
394
|
try { onTextDelta(text); } catch {}
|
|
@@ -348,6 +400,7 @@ export async function consumeCompatChatCompletionStream(stream, { signal, label,
|
|
|
348
400
|
const { text, calls } = leakGuard.flush();
|
|
349
401
|
if (text) {
|
|
350
402
|
content += text;
|
|
403
|
+
reportProgress('text');
|
|
351
404
|
if (onTextDelta) {
|
|
352
405
|
emittedText = true;
|
|
353
406
|
try { onTextDelta(text); } catch {}
|
|
@@ -362,21 +415,25 @@ export async function consumeCompatChatCompletionStream(stream, { signal, label,
|
|
|
362
415
|
// first-byte timer (createTimeoutSignal already chains parent).
|
|
363
416
|
signal: sawFirstEvent ? signal : firstByteTimeout.signal,
|
|
364
417
|
idleMs,
|
|
365
|
-
|
|
418
|
+
idleDeadlineAt: semanticIdleDeadlineAt,
|
|
419
|
+
idleEnabled: sawFirstEvent && idleEnabled && semanticIdleDeadlineAt > 0,
|
|
366
420
|
idleLabel: `${label} SSE idle`,
|
|
367
421
|
// A stall after a tool call has already been dispatched (native
|
|
368
422
|
// or recovered-leaked) must be unsafe-to-retry (no double-run).
|
|
369
|
-
emittedToolCall: () =>
|
|
423
|
+
emittedToolCall: () => streamEmitState.emittedToolCall || toolAcc.size > 0,
|
|
370
424
|
});
|
|
371
425
|
if (done) break;
|
|
372
426
|
if (!sawFirstEvent) {
|
|
373
427
|
sawFirstEvent = true;
|
|
374
428
|
firstByteTimeout.cleanup();
|
|
375
429
|
}
|
|
376
|
-
try { onStreamDelta?.(); } catch {}
|
|
430
|
+
try { onStreamDelta?.('transport'); } catch {}
|
|
377
431
|
if (chunk?.id) responseId = chunk.id;
|
|
378
432
|
if (chunk?.model) model = chunk.model;
|
|
379
433
|
const choice = chunk?.choices?.[0];
|
|
434
|
+
if (typeof choice?.delta?.role === 'string' && choice.delta.role) {
|
|
435
|
+
reportProgress('semantic');
|
|
436
|
+
}
|
|
380
437
|
if (choice?.delta?.content) {
|
|
381
438
|
// Live text relay (gateway): explicit assistant text delta,
|
|
382
439
|
// routed through the leaked-tool-call guard (which appends to
|
|
@@ -386,6 +443,7 @@ export async function consumeCompatChatCompletionStream(stream, { signal, label,
|
|
|
386
443
|
relayText(choice.delta.content);
|
|
387
444
|
} else {
|
|
388
445
|
content += choice.delta.content;
|
|
446
|
+
reportProgress('text');
|
|
389
447
|
if (onTextDelta) {
|
|
390
448
|
emittedText = true;
|
|
391
449
|
try { onTextDelta(choice.delta.content); } catch {}
|
|
@@ -394,6 +452,12 @@ export async function consumeCompatChatCompletionStream(stream, { signal, label,
|
|
|
394
452
|
}
|
|
395
453
|
if (typeof choice?.delta?.reasoning_content === 'string') {
|
|
396
454
|
reasoningContent += choice.delta.reasoning_content;
|
|
455
|
+
if (choice.delta.reasoning_content) {
|
|
456
|
+
reportProgress('reasoning');
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (Array.isArray(choice?.delta?.tool_calls) && choice.delta.tool_calls.length) {
|
|
460
|
+
reportProgress('tool');
|
|
397
461
|
}
|
|
398
462
|
mergeToolCallDelta(toolAcc, choice?.delta?.tool_calls, toolBucketState);
|
|
399
463
|
if (choice?.finish_reason) stopReason = choice.finish_reason;
|
|
@@ -414,7 +478,7 @@ export async function consumeCompatChatCompletionStream(stream, { signal, label,
|
|
|
414
478
|
err.pendingToolUse = toolAcc.size > 0 || leakedCalls.length > 0;
|
|
415
479
|
err.partialModel = model || undefined;
|
|
416
480
|
} catch { /* best-effort */ }
|
|
417
|
-
throw err;
|
|
481
|
+
throw markUnsafeRetryIfToolEmitted(err, streamEmitState);
|
|
418
482
|
}
|
|
419
483
|
// Partial-final recovery: on a mid-stream stall, attach the
|
|
420
484
|
// streamed partial state so the loop can accept a wedged FINAL no-tool
|
|
@@ -427,7 +491,7 @@ export async function consumeCompatChatCompletionStream(stream, { signal, label,
|
|
|
427
491
|
err.partialModel = model || undefined;
|
|
428
492
|
} catch { /* best-effort */ }
|
|
429
493
|
}
|
|
430
|
-
throw err;
|
|
494
|
+
throw markUnsafeRetryIfToolEmitted(err, streamEmitState);
|
|
431
495
|
} finally {
|
|
432
496
|
firstByteTimeout.cleanup();
|
|
433
497
|
}
|
|
@@ -450,7 +514,7 @@ export async function consumeCompatChatCompletionStream(stream, { signal, label,
|
|
|
450
514
|
err.partialModel = model || undefined;
|
|
451
515
|
} catch { /* best-effort */ }
|
|
452
516
|
}
|
|
453
|
-
throw err;
|
|
517
|
+
throw markUnsafeRetryIfToolEmitted(err, streamEmitState);
|
|
454
518
|
}
|
|
455
519
|
const message = {
|
|
456
520
|
content: content || null,
|
|
@@ -478,11 +542,10 @@ export async function consumeCompatChatCompletionStream(stream, { signal, label,
|
|
|
478
542
|
try { err.message += ` finish_reason=${stopReason}`; } catch {}
|
|
479
543
|
}
|
|
480
544
|
if (emittedText) markErrorLiveTextEmitted(err);
|
|
481
|
-
throw err;
|
|
545
|
+
throw markUnsafeRetryIfToolEmitted(err, streamEmitState);
|
|
482
546
|
}
|
|
483
547
|
if (Array.isArray(toolCalls) && toolCalls.length) {
|
|
484
|
-
const
|
|
485
|
-
for (const call of toolCalls) emitCompatToolCallOnce(emitState, call, onToolCall);
|
|
548
|
+
for (const call of toolCalls) emitCompatToolCallOnce(streamEmitState, call, onToolCall);
|
|
486
549
|
}
|
|
487
550
|
// Fold recovered leaked calls into the returned toolCalls so the dispatch
|
|
488
551
|
// loop treats them exactly like native ones. They were already emitted via
|
|
@@ -534,21 +597,30 @@ function handleCompatResponsesStreamEvent(event, state, { label, parseResponsesT
|
|
|
534
597
|
case 'response.created':
|
|
535
598
|
if (event.response?.model) state.model = event.response.model;
|
|
536
599
|
if (event.response?.id) state.responseId = event.response.id;
|
|
600
|
+
try { onStreamDelta?.('semantic'); } catch {}
|
|
537
601
|
break;
|
|
538
602
|
case 'response.output_text.delta':
|
|
539
603
|
state.sawOutput = true;
|
|
540
|
-
try { onStreamDelta?.(); } catch {}
|
|
541
604
|
// Route assistant text through the leaked-tool-call guard (appends
|
|
542
605
|
// to state.content, forwards visible text, recovers leaked calls).
|
|
543
606
|
if (relayLeakText) relayLeakText(event.delta || '');
|
|
544
607
|
else {
|
|
545
608
|
state.content += event.delta || '';
|
|
609
|
+
if (event.delta) {
|
|
610
|
+
try { onStreamDelta?.('text'); } catch {}
|
|
611
|
+
}
|
|
546
612
|
if (event.delta && onTextDelta) {
|
|
547
613
|
state.emittedText = true;
|
|
548
614
|
try { onTextDelta(event.delta); } catch {}
|
|
549
615
|
}
|
|
550
616
|
}
|
|
551
617
|
break;
|
|
618
|
+
case 'response.reasoning_text.delta':
|
|
619
|
+
case 'response.reasoning_summary_text.delta':
|
|
620
|
+
if (event.delta) {
|
|
621
|
+
try { onStreamDelta?.('reasoning'); } catch {}
|
|
622
|
+
}
|
|
623
|
+
break;
|
|
552
624
|
case 'response.output_item.added':
|
|
553
625
|
if (event.item?.type === 'function_call') {
|
|
554
626
|
state.pendingCalls.set(event.item.id || '', {
|
|
@@ -568,14 +640,14 @@ function handleCompatResponsesStreamEvent(event, state, { label, parseResponsesT
|
|
|
568
640
|
state.toolTracker?.mark(event.item);
|
|
569
641
|
state.toolInFlight = true;
|
|
570
642
|
}
|
|
571
|
-
try { onStreamDelta?.(); } catch {}
|
|
643
|
+
try { onStreamDelta?.(state.toolInFlight ? 'tool' : 'semantic'); } catch {}
|
|
572
644
|
break;
|
|
573
645
|
case 'response.function_call_arguments.delta':
|
|
574
646
|
// A tool call's args are streaming — mark tool work in-flight so a
|
|
575
647
|
// mid-args stall is NEVER accepted as a text-only partial-final.
|
|
576
648
|
state.toolTracker?.mark(null, event.item_id);
|
|
577
649
|
state.toolInFlight = true;
|
|
578
|
-
try { onStreamDelta?.(); } catch {}
|
|
650
|
+
try { onStreamDelta?.('tool'); } catch {}
|
|
579
651
|
break;
|
|
580
652
|
case 'response.custom_tool_call_input.delta':
|
|
581
653
|
// Custom-tool input streams before output_item.done records the call
|
|
@@ -583,7 +655,7 @@ function handleCompatResponsesStreamEvent(event, state, { label, parseResponsesT
|
|
|
583
655
|
// final success (otherwise a tool-bearing turn looks text-only).
|
|
584
656
|
state.toolTracker?.mark(null, event.item_id);
|
|
585
657
|
state.toolInFlight = true;
|
|
586
|
-
try { onStreamDelta?.(); } catch {}
|
|
658
|
+
try { onStreamDelta?.('tool'); } catch {}
|
|
587
659
|
break;
|
|
588
660
|
case 'response.function_call_arguments.done': {
|
|
589
661
|
const itemId = event.item_id || '';
|
|
@@ -599,7 +671,7 @@ function handleCompatResponsesStreamEvent(event, state, { label, parseResponsesT
|
|
|
599
671
|
state.toolCalls.push(call);
|
|
600
672
|
if (call.id && call.name) delete call._pendingItemId;
|
|
601
673
|
emitCompatToolCallOnce(state, call, onToolCall);
|
|
602
|
-
try { onStreamDelta?.(); } catch {}
|
|
674
|
+
try { onStreamDelta?.('tool'); } catch {}
|
|
603
675
|
break;
|
|
604
676
|
}
|
|
605
677
|
case 'response.output_item.done': {
|
|
@@ -638,7 +710,10 @@ function handleCompatResponsesStreamEvent(event, state, { label, parseResponsesT
|
|
|
638
710
|
state.toolTracker?.clear(item, item.id || '');
|
|
639
711
|
state.toolInFlight = state.pendingCalls.size > 0 || (state.toolTracker ? state.toolTracker.items.size > 0 : false);
|
|
640
712
|
}
|
|
641
|
-
|
|
713
|
+
const kind = item.type === 'reasoning'
|
|
714
|
+
? 'reasoning'
|
|
715
|
+
: (/tool|function_call|web_search_call/.test(item.type || '') ? 'tool' : 'semantic');
|
|
716
|
+
try { onStreamDelta?.(kind); } catch {}
|
|
642
717
|
break;
|
|
643
718
|
}
|
|
644
719
|
case 'response.completed': {
|
|
@@ -647,7 +722,20 @@ function handleCompatResponsesStreamEvent(event, state, { label, parseResponsesT
|
|
|
647
722
|
state.completedResponse = resp;
|
|
648
723
|
if (!state.model && resp.model) state.model = resp.model;
|
|
649
724
|
if (!state.responseId && resp.id) state.responseId = resp.id;
|
|
650
|
-
|
|
725
|
+
let reportedBundleProgress = false;
|
|
726
|
+
if (!state.content) {
|
|
727
|
+
const fallbackText = responseOutputText(resp);
|
|
728
|
+
if (fallbackText) {
|
|
729
|
+
if (relayLeakText) {
|
|
730
|
+
const result = relayLeakText(fallbackText, true);
|
|
731
|
+
reportedBundleProgress = !!(result?.text || result?.tool);
|
|
732
|
+
} else {
|
|
733
|
+
state.content = fallbackText;
|
|
734
|
+
try { onStreamDelta?.('text'); } catch {}
|
|
735
|
+
reportedBundleProgress = true;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
651
739
|
for (const item of resp.output || []) {
|
|
652
740
|
if (item?.type === 'function_call') {
|
|
653
741
|
const itemId = item.id || '';
|
|
@@ -667,13 +755,27 @@ function handleCompatResponsesStreamEvent(event, state, { label, parseResponsesT
|
|
|
667
755
|
state.toolCalls.push(call);
|
|
668
756
|
emitCompatToolCallOnce(state, call, onToolCall);
|
|
669
757
|
}
|
|
758
|
+
try { onStreamDelta?.('tool'); } catch {}
|
|
759
|
+
reportedBundleProgress = true;
|
|
670
760
|
} else if (item?.type === 'tool_search_call') {
|
|
671
761
|
pushToolSearchCall(item);
|
|
762
|
+
try { onStreamDelta?.('tool'); } catch {}
|
|
763
|
+
reportedBundleProgress = true;
|
|
672
764
|
} else if (item?.type === 'custom_tool_call') {
|
|
673
765
|
pushCustomToolCall(item);
|
|
766
|
+
try { onStreamDelta?.('tool'); } catch {}
|
|
767
|
+
reportedBundleProgress = true;
|
|
768
|
+
} else if (item?.type === 'reasoning') {
|
|
769
|
+
try { onStreamDelta?.('reasoning'); } catch {}
|
|
770
|
+
reportedBundleProgress = true;
|
|
771
|
+
} else if (item?.type === 'web_search_call') {
|
|
772
|
+
try { onStreamDelta?.('tool'); } catch {}
|
|
773
|
+
reportedBundleProgress = true;
|
|
674
774
|
}
|
|
675
775
|
}
|
|
676
|
-
|
|
776
|
+
if (!reportedBundleProgress) {
|
|
777
|
+
try { onStreamDelta?.('semantic'); } catch {}
|
|
778
|
+
}
|
|
677
779
|
break;
|
|
678
780
|
}
|
|
679
781
|
case 'response.done':
|
|
@@ -748,12 +850,17 @@ export async function consumeCompatResponsesStream(stream, {
|
|
|
748
850
|
parseResponsesToolCalls,
|
|
749
851
|
responseOutputText,
|
|
750
852
|
knownToolNames,
|
|
853
|
+
semanticIdleTimeoutMs,
|
|
751
854
|
} = {}) {
|
|
855
|
+
try { onStreamDelta?.('transport'); } catch {}
|
|
752
856
|
const iterator = stream[Symbol.asyncIterator]();
|
|
753
857
|
const firstByteTimeout = createTimeoutSignal(signal, PROVIDER_FIRST_BYTE_TIMEOUT_MS, `${label} first byte`);
|
|
754
|
-
const
|
|
858
|
+
const idleOverrideEnabled = Number.isFinite(Number(semanticIdleTimeoutMs)) && Number(semanticIdleTimeoutMs) > 0;
|
|
859
|
+
const idleEnabled = idleOverrideEnabled || PROVIDER_SSE_IDLE_WATCHDOG_ENABLED;
|
|
755
860
|
// Per-event (last-event-relative) SEMANTIC idle — see the Chat path note.
|
|
756
|
-
const idleMs =
|
|
861
|
+
const idleMs = Number.isFinite(Number(semanticIdleTimeoutMs)) && Number(semanticIdleTimeoutMs) > 0
|
|
862
|
+
? Number(semanticIdleTimeoutMs)
|
|
863
|
+
: PROVIDER_SEMANTIC_IDLE_TIMEOUT_MS;
|
|
757
864
|
const state = {
|
|
758
865
|
content: '',
|
|
759
866
|
model: '',
|
|
@@ -781,6 +888,11 @@ export async function consumeCompatResponsesStream(stream, {
|
|
|
781
888
|
// has been forwarded. A later failure is non-retryable (rendered text
|
|
782
889
|
// cannot be withdrawn; a retry would concatenate attempts).
|
|
783
890
|
emittedText: false,
|
|
891
|
+
semanticIdleDeadlineAt: 0,
|
|
892
|
+
};
|
|
893
|
+
const reportProgress = (kind) => {
|
|
894
|
+
if (kind !== 'transport') state.semanticIdleDeadlineAt = Date.now() + idleMs;
|
|
895
|
+
try { onStreamDelta?.(kind); } catch {}
|
|
784
896
|
};
|
|
785
897
|
let sawFirstEvent = false;
|
|
786
898
|
// Leaked tool-call guard for the Responses text stream. Same recovery as
|
|
@@ -792,18 +904,21 @@ export async function consumeCompatResponsesStream(stream, {
|
|
|
792
904
|
const call = synthLeakedOpenAICall(recovered);
|
|
793
905
|
emitCompatToolCallOnce(state, call, onToolCall);
|
|
794
906
|
leakedCalls.push(call);
|
|
907
|
+
reportProgress('tool');
|
|
795
908
|
};
|
|
796
909
|
const relayLeakText = leakGuard.enabled
|
|
797
|
-
? (delta) => {
|
|
798
|
-
const { text, calls } = leakGuard.push(delta);
|
|
910
|
+
? (delta, final = false) => {
|
|
911
|
+
const { text, calls } = leakGuard.push(delta, final);
|
|
799
912
|
if (text) {
|
|
800
913
|
state.content += text;
|
|
914
|
+
reportProgress('text');
|
|
801
915
|
if (onTextDelta) {
|
|
802
916
|
state.emittedText = true;
|
|
803
917
|
try { onTextDelta(text); } catch {}
|
|
804
918
|
}
|
|
805
919
|
}
|
|
806
920
|
for (const c of calls) dispatchLeakedCall(c);
|
|
921
|
+
return { text: !!text, tool: calls.length > 0 };
|
|
807
922
|
}
|
|
808
923
|
: null;
|
|
809
924
|
const flushLeak = () => {
|
|
@@ -811,6 +926,7 @@ export async function consumeCompatResponsesStream(stream, {
|
|
|
811
926
|
const { text, calls } = leakGuard.flush();
|
|
812
927
|
if (text) {
|
|
813
928
|
state.content += text;
|
|
929
|
+
reportProgress('text');
|
|
814
930
|
if (onTextDelta) {
|
|
815
931
|
state.emittedText = true;
|
|
816
932
|
try { onTextDelta(text); } catch {}
|
|
@@ -818,13 +934,14 @@ export async function consumeCompatResponsesStream(stream, {
|
|
|
818
934
|
}
|
|
819
935
|
for (const c of calls) dispatchLeakedCall(c);
|
|
820
936
|
};
|
|
821
|
-
const deps = { label, parseResponsesToolCalls, responseOutputText, onStreamDelta, onToolCall, onTextDelta, relayLeakText };
|
|
937
|
+
const deps = { label, parseResponsesToolCalls, responseOutputText, onStreamDelta: reportProgress, onToolCall, onTextDelta, relayLeakText };
|
|
822
938
|
try {
|
|
823
939
|
while (true) {
|
|
824
940
|
const { value: event, done } = await nextAsyncWithWatchdog(iterator, {
|
|
825
941
|
signal: sawFirstEvent ? signal : firstByteTimeout.signal,
|
|
826
942
|
idleMs,
|
|
827
|
-
|
|
943
|
+
idleDeadlineAt: state.semanticIdleDeadlineAt,
|
|
944
|
+
idleEnabled: sawFirstEvent && idleEnabled && state.semanticIdleDeadlineAt > 0,
|
|
828
945
|
idleLabel: `${label} SSE idle`,
|
|
829
946
|
// Unsafe-to-retry once any tool call (native or recovered-leaked)
|
|
830
947
|
// has been emitted this stream — avoid a double side-effect.
|
|
@@ -835,6 +952,7 @@ export async function consumeCompatResponsesStream(stream, {
|
|
|
835
952
|
sawFirstEvent = true;
|
|
836
953
|
firstByteTimeout.cleanup();
|
|
837
954
|
}
|
|
955
|
+
reportProgress('transport');
|
|
838
956
|
handleCompatResponsesStreamEvent(event, state, deps);
|
|
839
957
|
}
|
|
840
958
|
flushLeak();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
|
-
import {
|
|
2
|
+
import { getAgentApiKey } from '../../../shared/provider-api-key.mjs';
|
|
3
3
|
import { withRetry } from './retry-classifier.mjs';
|
|
4
4
|
import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
|
|
5
5
|
import { sendViaWebSocket } from './openai-oauth-ws.mjs';
|
|
@@ -152,13 +152,11 @@ export class OpenAICompatProvider {
|
|
|
152
152
|
}
|
|
153
153
|
reloadApiKey() {
|
|
154
154
|
try {
|
|
155
|
-
const freshConfig = loadConfig();
|
|
156
|
-
const cfg = freshConfig.providers?.[this.name];
|
|
157
155
|
const preset = PRESETS[this.name];
|
|
158
|
-
const newKey =
|
|
159
|
-
const baseURL = assertSafeBaseURL(
|
|
156
|
+
const newKey = getAgentApiKey(this.name) || this.config.apiKey;
|
|
157
|
+
const baseURL = assertSafeBaseURL(this.config.baseURL || preset?.baseURL || 'http://localhost:8080/v1', this.name);
|
|
160
158
|
if (newKey) {
|
|
161
|
-
this.config = { ...(this.config || {}),
|
|
159
|
+
this.config = { ...(this.config || {}), apiKey: newKey, baseURL };
|
|
162
160
|
this.baseURL = baseURL;
|
|
163
161
|
this.apiKey = newKey;
|
|
164
162
|
this.defaultHeaders = { ...(preset?.extraHeaders || {}), ...(this.config.extraHeaders || {}) };
|
|
@@ -180,7 +178,7 @@ export class OpenAICompatProvider {
|
|
|
180
178
|
if (err.liveTextEmitted === true || err.emittedToolCall === true || err.unsafeToRetry === true) {
|
|
181
179
|
throw err;
|
|
182
180
|
}
|
|
183
|
-
process.stderr.write(`[provider] Auth error, re-reading
|
|
181
|
+
process.stderr.write(`[provider] Auth error, re-reading provider authentication...\n`);
|
|
184
182
|
this.reloadApiKey();
|
|
185
183
|
return await this._doSend(messages, model, tools, sendOpts);
|
|
186
184
|
}
|