mixdog 0.9.87 → 0.9.89
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 +13 -7
- package/scripts/tool-failures.mjs +60 -24
- package/src/agents/heavy-worker/AGENT.md +3 -4
- package/src/agents/worker/AGENT.md +1 -2
- package/src/cli.mjs +8 -0
- package/src/defaults/cycle3-review-prompt.md +4 -4
- package/src/rules/agent/00-core.md +3 -0
- package/src/rules/agent/42-cycle3-agent.md +5 -6
- package/src/rules/lead/01-general.md +2 -0
- package/src/rules/shared/01-tool.md +29 -35
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +3 -51
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +59 -0
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -2
- package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +67 -0
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +57 -4
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +2 -3
- package/src/runtime/agent/orchestrator/config.mjs +29 -11
- package/src/runtime/agent/orchestrator/context/collect.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-agents.mjs +1 -1
- package/src/runtime/agent/orchestrator/mcp/client.mjs +0 -24
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +25 -17
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +321 -22
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +35 -22
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +28 -23
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +10 -2
- package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +0 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +0 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +2 -5
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-native-blocks.mjs +27 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +23 -0
- package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +329 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +23 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +107 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +4 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +124 -12
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +19 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +110 -75
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +96 -113
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +160 -11
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +18 -1
- package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +147 -117
- package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +88 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +50 -8
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +25 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +47 -6
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +485 -24
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +100 -2
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +80 -14
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +142 -2
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +128 -32
- package/src/runtime/agent/orchestrator/session/manager.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +77 -4
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +70 -39
- package/src/runtime/agent/orchestrator/session/store/fs-probe.mjs +91 -0
- package/src/runtime/agent/orchestrator/session/store/listing.mjs +141 -64
- package/src/runtime/agent/orchestrator/session/store/live-state.mjs +253 -0
- package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +211 -28
- package/src/runtime/agent/orchestrator/session/store/save-fault.mjs +313 -0
- package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +622 -53
- package/src/runtime/agent/orchestrator/session/store/serialize.mjs +43 -10
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +53 -8
- package/src/runtime/agent/orchestrator/session/store/summary-rebuild-worker.mjs +20 -0
- package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +113 -9
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +476 -52
- package/src/runtime/agent/orchestrator/session/store.mjs +721 -196
- package/src/runtime/agent/orchestrator/session/token-native.mjs +2 -4
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +4 -6
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +146 -90
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +6 -0
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +93 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +17 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +85 -5
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +52 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +54 -51
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +14 -15
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +132 -8
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +86 -3
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +210 -38
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +23 -1
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +87 -19
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -8
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +12 -2
- package/src/runtime/media/lanes.mjs +2 -2
- package/src/runtime/media/renditions.mjs +101 -17
- package/src/runtime/media/renditions.test.mjs +54 -0
- package/src/runtime/media/store.mjs +111 -43
- package/src/runtime/media/store.test.mjs +53 -11
- package/src/runtime/memory/index.mjs +26 -50
- package/src/runtime/memory/lib/core-memory-candidates.mjs +2 -7
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -9
- package/src/runtime/memory/lib/cycle-scheduler.mjs +10 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +3 -3
- package/src/runtime/memory/lib/embedding-worker.mjs +60 -15
- package/src/runtime/memory/lib/http-router.mjs +4 -0
- package/src/runtime/memory/lib/ko-morph.mjs +49 -6
- package/src/runtime/memory/lib/memory-action-handlers.mjs +21 -18
- package/src/runtime/memory/lib/memory-cycle2.mjs +3 -3
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -3
- package/src/runtime/memory/lib/memory-recall-store.mjs +24 -0
- package/src/runtime/memory/lib/query-handlers.mjs +26 -28
- package/src/runtime/memory/tool-defs.mjs +3 -3
- package/src/runtime/shared/atomic-file.mjs +5 -1
- package/src/runtime/shared/child-guardian.mjs +73 -2
- package/src/runtime/shared/provider-api-key.mjs +22 -5
- package/src/runtime/shared/tool-execution-contract.mjs +53 -0
- package/src/session-runtime/config-helpers.mjs +6 -5
- package/src/session-runtime/lifecycle-api.mjs +16 -4
- package/src/session-runtime/media-api.mjs +31 -16
- package/src/session-runtime/prewarm.mjs +2 -24
- package/src/session-runtime/runtime-core.mjs +33 -15
- package/src/session-runtime/runtime-tunables.mjs +0 -3
- package/src/session-runtime/session-lifecycle.mjs +0 -6
- package/src/session-runtime/session-title.mjs +228 -0
- package/src/session-runtime/session-turn-api.mjs +11 -7
- package/src/session-runtime/workflow-agents-api.mjs +16 -0
- package/src/session-runtime/workflow.mjs +7 -5
- package/src/standalone/agent-tool/job-views.mjs +90 -36
- package/src/standalone/agent-tool/spawn-flow.mjs +25 -216
- package/src/standalone/agent-tool/worker-index.mjs +8 -0
- package/src/standalone/agent-tool/worker-rows.mjs +2 -1
- package/src/standalone/agent-tool.mjs +17 -17
- package/src/standalone/agent-watchdog-registry.mjs +19 -2
- package/src/standalone/channel-daemon.mjs +8 -0
- package/src/standalone/explore-tool.mjs +1 -1
- package/src/standalone/memory-runtime-proxy.mjs +4 -7
- package/src/standalone/projects.mjs +4 -1
- package/src/standalone/usage-dashboard.mjs +25 -3
- package/src/tui/App.jsx +5 -3
- package/src/tui/app/resume-picker.mjs +7 -2
- package/src/tui/app/route-pickers.mjs +11 -117
- package/src/tui/app/slash-dispatch.mjs +13 -3
- package/src/tui/app/transcript-row-estimate.mjs +1 -1
- package/src/tui/app/transcript-window.mjs +17 -93
- package/src/tui/app/use-transcript-scroll.mjs +32 -2
- package/src/tui/app/use-transcript-window.mjs +64 -114
- package/src/tui/components/Markdown.jsx +19 -2
- package/src/tui/components/Message.jsx +8 -3
- package/src/tui/components/ToolExecution.jsx +7 -3
- package/src/tui/dist/index.mjs +243 -223
- package/src/tui/engine/live-share.mjs +15 -5
- package/src/tui/engine/render-timing.mjs +2 -2
- package/src/tui/engine/session-api-ext.mjs +74 -4
- package/src/tui/engine/session-flow.mjs +3 -1
- package/src/tui/engine.mjs +2 -2
- package/src/tui/hooks/useSharedTick.mjs +0 -2
- package/src/tui/index.jsx +5 -5
- package/src/ui/statusline-agents.mjs +44 -11
- package/src/vendor/statusline/bin/statusline-route.mjs +26 -3
- package/src/workflows/default/WORKFLOW.md +3 -3
- package/src/workflows/solo/WORKFLOW.md +5 -3
- package/src/runtime/media/index.mjs +0 -18
- package/src/runtime/memory/lib/embedding-warmup.mjs +0 -68
- package/src/standalone/agent-shard/shard-child.mjs +0 -300
- package/src/standalone/agent-shard/shard-pool.mjs +0 -443
|
@@ -80,13 +80,15 @@ function normalizeGeminiSdkStreamError(err, label) {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
// CC-rule safety stamp for Gemini stream failures (provider-stall audit):
|
|
83
|
-
// once text has been
|
|
84
|
-
// dispatched, replaying
|
|
83
|
+
// once text has actually been RELAYED to the live gateway or a leaked tool
|
|
84
|
+
// call was dispatched, replaying would double-render/double-execute — the
|
|
85
85
|
// outer withRetry() in gemini.mjs wraps the WHOLE stream, and a bare
|
|
86
86
|
// EGEMINITIMEOUT classifies transient, so without these markers a mid-stream
|
|
87
|
-
// stall after visible output was silently retried.
|
|
88
|
-
//
|
|
89
|
-
//
|
|
87
|
+
// stall after visible output was silently retried. Text that was only
|
|
88
|
+
// BUFFERED (no onTextDelta sink, or still held inside the leak guard) was
|
|
89
|
+
// never shown, so it is not a replay boundary. Relayed-text stalls also gain
|
|
90
|
+
// streamStalled + partialContent so the loop's partial-final path can keep
|
|
91
|
+
// the streamed output instead of dropping the turn.
|
|
90
92
|
function stampGeminiStreamFailure(err, {
|
|
91
93
|
relayedText = '',
|
|
92
94
|
textLeakGuard = null,
|
|
@@ -95,7 +97,7 @@ function stampGeminiStreamFailure(err, {
|
|
|
95
97
|
} = {}) {
|
|
96
98
|
if (!err || typeof err !== 'object') return err;
|
|
97
99
|
const leaked = (textLeakGuard?.getLeakedToolCalls?.() || []).length > 0;
|
|
98
|
-
const finalizedText = textLeakGuard?.
|
|
100
|
+
const finalizedText = textLeakGuard?.getRelayedText?.();
|
|
99
101
|
const visibleText = typeof finalizedText === 'string' ? finalizedText : relayedText;
|
|
100
102
|
const visible = visibleText.length > 0;
|
|
101
103
|
// Native functionCall chunks are only dispatched after successful stream
|
|
@@ -205,10 +207,13 @@ export function geminiChunkText(chunk) {
|
|
|
205
207
|
return text;
|
|
206
208
|
}
|
|
207
209
|
|
|
210
|
+
// Returns the text actually RELAYED to the live sink ('' when it was only
|
|
211
|
+
// buffered), so callers can track visible output rather than raw stream text.
|
|
208
212
|
function relayGeminiStreamText(t, { onTextDelta, textLeakGuard }) {
|
|
209
|
-
if (!t) return;
|
|
210
|
-
if (textLeakGuard) textLeakGuard.feedText(t);
|
|
211
|
-
|
|
213
|
+
if (!t) return '';
|
|
214
|
+
if (textLeakGuard) { textLeakGuard.feedText(t); return ''; }
|
|
215
|
+
if (onTextDelta) { try { onTextDelta(t); } catch {} return t; }
|
|
216
|
+
return '';
|
|
212
217
|
}
|
|
213
218
|
|
|
214
219
|
/**
|
|
@@ -223,7 +228,7 @@ export function createGeminiTextLeakGuard({ knownToolNames, onTextDelta, onToolC
|
|
|
223
228
|
const _enabled = _knownTools.size > 0;
|
|
224
229
|
const _isKnownTool = (name) => _knownTools.has(name);
|
|
225
230
|
let leakBuffer = '';
|
|
226
|
-
let
|
|
231
|
+
let relayedText = '';
|
|
227
232
|
const leakedCalls = [];
|
|
228
233
|
const dispatchedFingerprints = new Set();
|
|
229
234
|
|
|
@@ -259,11 +264,12 @@ export function createGeminiTextLeakGuard({ knownToolNames, onTextDelta, onToolC
|
|
|
259
264
|
if (!leakBuffer && !final) return;
|
|
260
265
|
const { emit, calls, rest } = scanLeakedToolCalls(leakBuffer, { isKnownTool: _isKnownTool, final });
|
|
261
266
|
leakBuffer = rest;
|
|
267
|
+
// Only text handed to the live sink counts as relayed/visible. Without
|
|
268
|
+
// an onTextDelta sink the scrubbed text is buffered for the final
|
|
269
|
+
// result and never shown, so it must not deny a replay.
|
|
262
270
|
if (emit && onTextDelta) {
|
|
263
|
-
|
|
271
|
+
relayedText += emit;
|
|
264
272
|
try { onTextDelta(emit); } catch {}
|
|
265
|
-
} else if (emit) {
|
|
266
|
-
visibleText += emit;
|
|
267
273
|
}
|
|
268
274
|
for (const c of calls) dispatchLeakedCall(c);
|
|
269
275
|
};
|
|
@@ -273,8 +279,10 @@ export function createGeminiTextLeakGuard({ knownToolNames, onTextDelta, onToolC
|
|
|
273
279
|
feedText(text) {
|
|
274
280
|
if (!text) return;
|
|
275
281
|
if (!_enabled) {
|
|
276
|
-
|
|
277
|
-
|
|
282
|
+
if (onTextDelta) {
|
|
283
|
+
relayedText += text;
|
|
284
|
+
try { onTextDelta(text); } catch {}
|
|
285
|
+
}
|
|
278
286
|
return;
|
|
279
287
|
}
|
|
280
288
|
leakBuffer += text;
|
|
@@ -304,8 +312,8 @@ export function createGeminiTextLeakGuard({ knownToolNames, onTextDelta, onToolC
|
|
|
304
312
|
getLeakedToolCalls() {
|
|
305
313
|
return leakedCalls.length ? [...leakedCalls] : [];
|
|
306
314
|
},
|
|
307
|
-
|
|
308
|
-
return
|
|
315
|
+
getRelayedText() {
|
|
316
|
+
return relayedText;
|
|
309
317
|
},
|
|
310
318
|
};
|
|
311
319
|
}
|
|
@@ -422,8 +430,7 @@ export async function consumeGeminiRestStreamResponse(response, { signal, onStre
|
|
|
422
430
|
if (!sawFunctionCall && geminiChunkHasFunctionCall(parsed)) sawFunctionCall = true;
|
|
423
431
|
if (onTextDelta || textLeakGuard) {
|
|
424
432
|
const t = geminiChunkText(parsed);
|
|
425
|
-
|
|
426
|
-
relayGeminiStreamText(t, { onTextDelta, textLeakGuard });
|
|
433
|
+
relayedText += relayGeminiStreamText(t, { onTextDelta, textLeakGuard });
|
|
427
434
|
}
|
|
428
435
|
}
|
|
429
436
|
}
|
|
@@ -443,8 +450,7 @@ export async function consumeGeminiRestStreamResponse(response, { signal, onStre
|
|
|
443
450
|
if (!sawFunctionCall && geminiChunkHasFunctionCall(parsed)) sawFunctionCall = true;
|
|
444
451
|
if (onTextDelta || textLeakGuard) {
|
|
445
452
|
const t = geminiChunkText(parsed);
|
|
446
|
-
|
|
447
|
-
relayGeminiStreamText(t, { onTextDelta, textLeakGuard });
|
|
453
|
+
relayedText += relayGeminiStreamText(t, { onTextDelta, textLeakGuard });
|
|
448
454
|
}
|
|
449
455
|
} catch (cause) {
|
|
450
456
|
throw geminiStreamCorruptionError(`${label} corrupt SSE tail JSON`, cause);
|
|
@@ -675,8 +681,7 @@ export async function consumeGeminiSdkStream(streamResult, {
|
|
|
675
681
|
if (!sawFunctionCall && geminiChunkHasFunctionCall(step.value)) sawFunctionCall = true;
|
|
676
682
|
if (onTextDelta || textLeakGuard) {
|
|
677
683
|
const t = geminiChunkText(step.value);
|
|
678
|
-
|
|
679
|
-
relayGeminiStreamText(t, { onTextDelta, textLeakGuard });
|
|
684
|
+
relayedText += relayGeminiStreamText(t, { onTextDelta, textLeakGuard });
|
|
680
685
|
}
|
|
681
686
|
}
|
|
682
687
|
if (idleTimedOut) {
|
|
@@ -75,6 +75,11 @@ function geminiRestError(res, text, label) {
|
|
|
75
75
|
err.status = res.status;
|
|
76
76
|
err.httpStatus = res.status;
|
|
77
77
|
err.headers = res.headers;
|
|
78
|
+
// Initial-response failure: the request was rejected before any sample was
|
|
79
|
+
// produced, so withRetry's typed rules decide: a known transient status
|
|
80
|
+
// retries with bounded backoff (Retry-After honored), anything unknown or
|
|
81
|
+
// deterministic surfaces as an error.
|
|
82
|
+
err.initialResponseError = true;
|
|
78
83
|
if (detail) {
|
|
79
84
|
err.error = detail;
|
|
80
85
|
err.data = payload;
|
|
@@ -486,8 +491,11 @@ export class GeminiProvider {
|
|
|
486
491
|
try {
|
|
487
492
|
return await this._doSend(messages, model, tools, sendOpts);
|
|
488
493
|
} catch (err) {
|
|
489
|
-
|
|
490
|
-
|
|
494
|
+
// Credential reload + reissue requires a TYPED 401: message text is
|
|
495
|
+
// not evidence, and a typed 403 (permission/quota decision) is not
|
|
496
|
+
// fixed by re-reading the key, so it surfaces unchanged.
|
|
497
|
+
const status = Number(err?.status || err?.httpStatus || err?.response?.status || 0);
|
|
498
|
+
if (status === 401) {
|
|
491
499
|
if (err.liveTextEmitted === true || err.emittedToolCall === true || err.unsafeToRetry === true) {
|
|
492
500
|
throw err;
|
|
493
501
|
}
|
|
@@ -26,7 +26,6 @@ import { sanitizeModelList } from './model-list-sanitize.mjs';
|
|
|
26
26
|
import { makeModelCache } from './model-cache.mjs';
|
|
27
27
|
import { OpenAICompatProvider } from './openai-compat.mjs';
|
|
28
28
|
import { createTimeoutSignal } from '../stall-policy.mjs';
|
|
29
|
-
import { populateHttpStatusFromMessage } from './retry-classifier.mjs';
|
|
30
29
|
import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
|
|
31
30
|
import { normalizeGrokToolSchemas } from './lib/grok-tool-schema.mjs';
|
|
32
31
|
|
|
@@ -26,7 +26,6 @@ import { sanitizeModelList } from './model-list-sanitize.mjs';
|
|
|
26
26
|
import { makeModelCache } from './model-cache.mjs';
|
|
27
27
|
import { OpenAICompatProvider } from './openai-compat.mjs';
|
|
28
28
|
import { createTimeoutSignal } from '../stall-policy.mjs';
|
|
29
|
-
import { populateHttpStatusFromMessage } from './retry-classifier.mjs';
|
|
30
29
|
import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
|
|
31
30
|
import { normalizeGrokToolSchemas } from './lib/grok-tool-schema.mjs';
|
|
32
31
|
|
|
@@ -25,7 +25,6 @@ import { sanitizeModelList } from './model-list-sanitize.mjs';
|
|
|
25
25
|
import { makeModelCache } from './model-cache.mjs';
|
|
26
26
|
import { OpenAICompatProvider } from './openai-compat.mjs';
|
|
27
27
|
import { createTimeoutSignal } from '../stall-policy.mjs';
|
|
28
|
-
import { populateHttpStatusFromMessage } from './retry-classifier.mjs';
|
|
29
28
|
import { getLlmDispatcher, preconnect } from '../../../shared/llm/http-agent.mjs';
|
|
30
29
|
import { normalizeGrokToolSchemas } from './lib/grok-tool-schema.mjs';
|
|
31
30
|
|
|
@@ -311,10 +310,8 @@ export class GrokOAuthProvider {
|
|
|
311
310
|
return await inner._doSend(messages, useModel, grokTools, sendOpts);
|
|
312
311
|
} catch (err) {
|
|
313
312
|
// Refresh-and-retry on a server-rejected OAuth session (401/403).
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
// ad-hoc string matching.
|
|
317
|
-
populateHttpStatusFromMessage(err);
|
|
313
|
+
// TYPED status only: a rejection whose only evidence is message
|
|
314
|
+
// text is never treated as an auth failure worth re-issuing.
|
|
318
315
|
const rejectedStatus = Number(err?.httpStatus || err?.status);
|
|
319
316
|
if (rejectedStatus === 401 || rejectedStatus === 403) {
|
|
320
317
|
// A stream-level rejection after text/tool dispatch cannot be safely
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* anthropic-native-blocks.mjs — single source of truth for Anthropic NATIVE
|
|
3
|
+
* (server-side) tool block types.
|
|
4
|
+
*
|
|
5
|
+
* Anthropic executes these itself and returns BOTH the CALL block
|
|
6
|
+
* (`server_tool_use` / `mcp_tool_use`) and its RESULT block
|
|
7
|
+
* (`web_search_tool_result`, …). They are never dispatched to the agent loop,
|
|
8
|
+
* so they only survive a continuation turn (`stop_reason:'pause_turn'`) when
|
|
9
|
+
* the provider boundary preserves them verbatim, in original block order — a
|
|
10
|
+
* result block is valid ONLY immediately after the call block that produced
|
|
11
|
+
* it. Shared by the SSE parser (streaming) and the non-streaming normalizer.
|
|
12
|
+
*/
|
|
13
|
+
export const NATIVE_SERVER_TOOL_CALL_BLOCK_TYPES = new Set(['server_tool_use', 'mcp_tool_use']);
|
|
14
|
+
|
|
15
|
+
export const NATIVE_SERVER_TOOL_RESULT_BLOCK_TYPES = new Set([
|
|
16
|
+
'web_search_tool_result',
|
|
17
|
+
'web_fetch_tool_result',
|
|
18
|
+
'code_execution_tool_result',
|
|
19
|
+
'bash_code_execution_tool_result',
|
|
20
|
+
'text_editor_code_execution_tool_result',
|
|
21
|
+
'mcp_tool_result',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
export function isNativeServerToolBlockType(type) {
|
|
25
|
+
return NATIVE_SERVER_TOOL_CALL_BLOCK_TYPES.has(type)
|
|
26
|
+
|| NATIVE_SERVER_TOOL_RESULT_BLOCK_TYPES.has(type);
|
|
27
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { providerNativeToolPrefixCount } from '../../../../../session-runtime/provider-request-tools.mjs';
|
|
2
|
+
import { isNativeServerToolBlockType } from './anthropic-native-blocks.mjs';
|
|
2
3
|
|
|
3
4
|
export const ANTHROPIC_CACHE_TTL_STABLE = { type: 'ephemeral', ttl: '1h' };
|
|
4
5
|
export const ANTHROPIC_CACHE_TTL_VOLATILE = { type: 'ephemeral' };
|
|
@@ -55,6 +56,11 @@ export function clampAnthropicThinkingBudget(value, maxTokens) {
|
|
|
55
56
|
return Math.max(1024, Math.min(desired, ceiling));
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
// Assistant blocks that are replayable verbatim alongside native server-tool
|
|
60
|
+
// blocks. `text` is handled separately (empty text blocks are dropped, exactly
|
|
61
|
+
// like parseSSEStream's assistantBlocks list).
|
|
62
|
+
const REPLAYABLE_ASSISTANT_BLOCK_TYPES = new Set(['thinking', 'redacted_thinking', 'tool_use']);
|
|
63
|
+
|
|
58
64
|
export function normalizeAnthropicNonStreamingResponse(message, fallbackModel = '') {
|
|
59
65
|
const blocks = Array.isArray(message?.content) ? message.content : [];
|
|
60
66
|
const text = blocks
|
|
@@ -71,6 +77,22 @@ export function normalizeAnthropicNonStreamingResponse(message, fallbackModel =
|
|
|
71
77
|
const thinkingBlocks = blocks.filter((block) => (
|
|
72
78
|
block?.type === 'thinking' || block?.type === 'redacted_thinking'
|
|
73
79
|
));
|
|
80
|
+
// Anthropic NATIVE server-tool turns (web search / web fetch / code
|
|
81
|
+
// execution / native MCP) cannot be rebuilt from the flattened
|
|
82
|
+
// text/toolCalls/thinking projections above: `server_tool_use` and its
|
|
83
|
+
// `*_tool_result` are opaque and order-bound (a result block is valid ONLY
|
|
84
|
+
// right after its call block). Keep the ordered raw list so the loop can
|
|
85
|
+
// replay the paused turn verbatim on the `pause_turn` continuation —
|
|
86
|
+
// mirror of parseSSEStream's assistantBlocks, including its
|
|
87
|
+
// absent-for-ordinary-turns rule, so plain text/thinking/tool_use
|
|
88
|
+
// responses keep the existing lowering and never double-replay.
|
|
89
|
+
const nativeAssistantBlocks = blocks.some((block) => isNativeServerToolBlockType(block?.type))
|
|
90
|
+
? blocks.filter((block) => block && typeof block === 'object' && (
|
|
91
|
+
isNativeServerToolBlockType(block.type)
|
|
92
|
+
|| REPLAYABLE_ASSISTANT_BLOCK_TYPES.has(block.type)
|
|
93
|
+
|| (block.type === 'text' && typeof block.text === 'string' && block.text.length > 0)
|
|
94
|
+
))
|
|
95
|
+
: [];
|
|
74
96
|
const usage = message?.usage || {};
|
|
75
97
|
const input = Number(usage.input_tokens) || 0;
|
|
76
98
|
const cacheRead = Number(usage.cache_read_input_tokens) || 0;
|
|
@@ -83,6 +105,7 @@ export function normalizeAnthropicNonStreamingResponse(message, fallbackModel =
|
|
|
83
105
|
hasThinkingContent: thinkingBlocks.length > 0,
|
|
84
106
|
contentBlockTypes: blocks.map((block) => block?.type).filter(Boolean),
|
|
85
107
|
thinkingBlocks: thinkingBlocks.length ? thinkingBlocks : undefined,
|
|
108
|
+
assistantBlocks: nativeAssistantBlocks.length ? nativeAssistantBlocks : undefined,
|
|
86
109
|
usage: {
|
|
87
110
|
inputTokens: input,
|
|
88
111
|
outputTokens: Number(usage.output_tokens) || 0,
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stream-outcome.mjs — canonical provider stream outcome / safety contract.
|
|
3
|
+
*
|
|
4
|
+
* ONE record describes how a provider stream ended, across every transport:
|
|
5
|
+
* OpenAI Responses WS, OpenAI Responses HTTP/SSE, Anthropic API-key SSE,
|
|
6
|
+
* Anthropic OAuth SSE and its non-streaming fallback, and any adapter that
|
|
7
|
+
* only attaches partial state to a thrown error.
|
|
8
|
+
*
|
|
9
|
+
* Historically each consumer (retry classifier, transport fallback,
|
|
10
|
+
* send-with-recovery, ask-session persistence) re-derived safety from a
|
|
11
|
+
* DIFFERENT subset of scattered per-provider flags (liveTextEmitted,
|
|
12
|
+
* emittedText, emittedToolCall, toolCallEmitted, partialToolCall,
|
|
13
|
+
* emittedThinking, emittedReasoning, startedToolCall, partialToolCalls,
|
|
14
|
+
* pendingToolUse, unsafeToRetry, sawCompleted, ...). A provider that set only
|
|
15
|
+
* SOME of them (Anthropic's stall error carries partialContent but neither
|
|
16
|
+
* liveTextEmitted nor unsafeToRetry) silently fell through the gaps: streamed
|
|
17
|
+
* text was dropped from history, or an ambiguous end-of-stream was treated as
|
|
18
|
+
* a clean success / a replayable request.
|
|
19
|
+
*
|
|
20
|
+
* ── Schema (canonical record, frozen) ───────────────────────────────────────
|
|
21
|
+
* version 1
|
|
22
|
+
* provider string|null informational
|
|
23
|
+
* transport 'ws'|'http-sse'|'sse'|'non-streaming'|'unknown'
|
|
24
|
+
* terminalObserved bool provider signalled end-of-turn (response.completed
|
|
25
|
+
* / response.done / message_stop / explicit finish
|
|
26
|
+
* reason / early-settle on a complete tool call)
|
|
27
|
+
* continuation bool stream ended WITHOUT a terminal signal (stall,
|
|
28
|
+
* truncation, abnormal close, EOF mid-message)
|
|
29
|
+
* textEmitted bool non-empty text RELAYED to the client (visible)
|
|
30
|
+
* textObservedChars int assistant text accumulated by the adapter,
|
|
31
|
+
* whether or not it was relayed
|
|
32
|
+
* reasoningEmitted bool thinking/reasoning deltas or blocks exposed
|
|
33
|
+
* toolCallsStarted bool tool input began streaming (args may be partial)
|
|
34
|
+
* toolCallsComplete int fully parsed tool calls
|
|
35
|
+
* toolCallsDispatched int tool calls handed to onToolCall (side effect may
|
|
36
|
+
* already have run)
|
|
37
|
+
* dispatchAmbiguous bool dispatch count was INFERRED, not reported
|
|
38
|
+
* pendingToolInput bool a tool_use/function_call input never completed
|
|
39
|
+
* userAbort bool caller cancelled
|
|
40
|
+
* stallObserved bool watchdog/idle stall produced this outcome
|
|
41
|
+
* truncatedStream bool message_start-without-message_stop class
|
|
42
|
+
*
|
|
43
|
+
* ── Derived invariants ──────────────────────────────────────────────────────
|
|
44
|
+
* terminalObserved clears continuation ONLY when the turn was not declared
|
|
45
|
+
* a same-user-turn continuation. A terminal sample frame may still say
|
|
46
|
+
* `end_turn=false` (Responses) / continuationDeclared: the turn is NOT over,
|
|
47
|
+
* so continuation stays true and successEligible stays false.
|
|
48
|
+
* visibleOutput = textEmitted || reasoningEmitted
|
|
49
|
+
* observedOutput = visibleOutput || textObservedChars > 0
|
|
50
|
+
* || toolCallsComplete > 0 || toolCallsDispatched > 0
|
|
51
|
+
* sideEffectDispatched = toolCallsDispatched > 0
|
|
52
|
+
* replayUnsafe = the ONE architecture-specific deny MixDog adds on top
|
|
53
|
+
* of the Codex retry rules, because it streams UI text
|
|
54
|
+
* and dispatches tools eagerly:
|
|
55
|
+
* visibleOutput || sideEffectDispatched
|
|
56
|
+
* || dispatchAmbiguous || unsafe marker
|
|
57
|
+
* replaySafe = !replayUnsafe && !userAbort
|
|
58
|
+
* successEligible = terminalObserved && !continuation && !pendingToolInput
|
|
59
|
+
*
|
|
60
|
+
* This record does NOT decide retries on its own. Whether a failure is retried
|
|
61
|
+
* at all is the TYPED question owned by retry-classifier.mjs (known transient
|
|
62
|
+
* status / errno / close code → bounded backoff + transport fallback; unknown
|
|
63
|
+
* or untyped → error). `replaySafe` only answers "would re-issuing this turn
|
|
64
|
+
* duplicate something the user already saw or a side effect that already ran".
|
|
65
|
+
* Buffered-but-never-relayed text and a tool input that never completed (and
|
|
66
|
+
* therefore never dispatched) are NOT replay denials; `observedOutput` remains
|
|
67
|
+
* the signal for partial-failure PERSISTENCE, not for replay permission.
|
|
68
|
+
* An absent or ambiguous terminal can never be promoted to success
|
|
69
|
+
* (successEligible=false): an explicit terminal stream event is required.
|
|
70
|
+
*
|
|
71
|
+
* Compatibility: stampStreamOutcome() writes the canonical record AND the
|
|
72
|
+
* historical alias flags (never clearing one that is already true), so every
|
|
73
|
+
* legacy reader keeps working unchanged.
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
export const STREAM_OUTCOME_VERSION = 1
|
|
77
|
+
|
|
78
|
+
export const STREAM_TRANSPORTS = Object.freeze({
|
|
79
|
+
WS: 'ws',
|
|
80
|
+
HTTP_SSE: 'http-sse',
|
|
81
|
+
SSE: 'sse',
|
|
82
|
+
NON_STREAMING: 'non-streaming',
|
|
83
|
+
UNKNOWN: 'unknown',
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const TRUE = (v) => v === true
|
|
87
|
+
const COUNT = (v) => {
|
|
88
|
+
const n = Number(v)
|
|
89
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function _isAbortLike(source) {
|
|
93
|
+
const name = String(source?.name || '')
|
|
94
|
+
return name === 'AbortError' || name === 'APIUserAbortError' || source?.code === 'ABORT_ERR'
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Raw (pre-derivation) signal read of ONE source object (error or midState). */
|
|
98
|
+
function _signalsOf(source) {
|
|
99
|
+
if (!source || (typeof source !== 'object' && typeof source !== 'function')) return null
|
|
100
|
+
|
|
101
|
+
const partialContent = typeof source.partialContent === 'string' ? source.partialContent : ''
|
|
102
|
+
const partialToolCalls = Array.isArray(source.partialToolCalls) ? source.partialToolCalls.length : 0
|
|
103
|
+
const explicitComplete = COUNT(source.toolCallsComplete)
|
|
104
|
+
const toolCallsComplete = Math.max(partialToolCalls, explicitComplete)
|
|
105
|
+
|
|
106
|
+
const textEmitted = TRUE(source.textEmitted) || TRUE(source.liveTextEmitted) || TRUE(source.emittedText)
|
|
107
|
+
const textObservedChars = Math.max(
|
|
108
|
+
COUNT(source.textObservedChars),
|
|
109
|
+
COUNT(source.emittedTextChars),
|
|
110
|
+
partialContent.trim().length > 0 ? partialContent.length : 0,
|
|
111
|
+
)
|
|
112
|
+
const reasoningEmitted = TRUE(source.reasoningEmitted) || TRUE(source.emittedThinking)
|
|
113
|
+
|| TRUE(source.emittedReasoning) || TRUE(source.partialReasoningEmitted)
|
|
114
|
+
// NOTE: `partialHasThinking` (an extended-thinking block EXISTED, possibly
|
|
115
|
+
// empty / signature-only) is deliberately NOT read here: a thinking block
|
|
116
|
+
// the user never saw is not exposed reasoning.
|
|
117
|
+
|
|
118
|
+
// `pendingToolUse` alone is NOT "started": a tool_use whose input JSON never
|
|
119
|
+
// completed was never dispatched, so re-requesting stays idempotent (the
|
|
120
|
+
// documented truncated-stream retry). Only explicit partial-args markers
|
|
121
|
+
// count as exposure.
|
|
122
|
+
const toolCallsStarted = TRUE(source.toolCallsStarted) || TRUE(source.partialToolCall)
|
|
123
|
+
|| TRUE(source.startedToolCall) || TRUE(source.partialToolCallStarted)
|
|
124
|
+
|| toolCallsComplete > 0
|
|
125
|
+
|
|
126
|
+
const dispatchReported = Object.prototype.hasOwnProperty.call(source, 'toolCallsDispatched')
|
|
127
|
+
const dispatchFlag = TRUE(source.emittedToolCall) || TRUE(source.toolCallEmitted)
|
|
128
|
+
let toolCallsDispatched = dispatchReported ? COUNT(source.toolCallsDispatched) : 0
|
|
129
|
+
let dispatchAmbiguous = false
|
|
130
|
+
if (!dispatchReported) {
|
|
131
|
+
if (dispatchFlag) {
|
|
132
|
+
toolCallsDispatched = Math.max(1, toolCallsComplete)
|
|
133
|
+
dispatchAmbiguous = toolCallsComplete === 0
|
|
134
|
+
} else if (toolCallsComplete > 0) {
|
|
135
|
+
// Fail closed: every streaming adapter dispatches eagerly at
|
|
136
|
+
// content_block_stop / function_call_arguments.done, so a complete tool
|
|
137
|
+
// call with no dispatch report must be assumed to have side effects.
|
|
138
|
+
toolCallsDispatched = toolCallsComplete
|
|
139
|
+
dispatchAmbiguous = true
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const truncatedStream = TRUE(source.truncatedStream) || source.code === 'TRUNCATED_STREAM'
|
|
144
|
+
const stallObserved = TRUE(source.streamStalled) || source.code === 'ESTREAMSTALL'
|
|
145
|
+
|| String(source.name || '') === 'StreamStalledError'
|
|
146
|
+
|| String(source.name || '') === 'StreamStalledAbortError'
|
|
147
|
+
|| String(source.name || '') === 'AgentStallAbortError'
|
|
148
|
+
|| String(source.watchdogAbort || '') !== ''
|
|
149
|
+
|
|
150
|
+
const terminalObserved = TRUE(source.terminalObserved) || TRUE(source.sawCompleted)
|
|
151
|
+
|| TRUE(source.completed)
|
|
152
|
+
|| (TRUE(source.providerIncomplete) && !!source.finishReason)
|
|
153
|
+
const streamStarted = TRUE(source.sawMessageStart) || TRUE(source.sawResponseCreated)
|
|
154
|
+
|| textEmitted || reasoningEmitted || textObservedChars > 0 || toolCallsStarted
|
|
155
|
+
// Protocol distinction: a TERMINAL sample frame can still declare that the
|
|
156
|
+
// same user turn continues (Responses `end_turn=false`). That is a
|
|
157
|
+
// continuation, not a finished turn.
|
|
158
|
+
const declaredContinuation = source.endTurn === false || TRUE(source.continuationDeclared)
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
provider: source.provider ?? source.providerName ?? null,
|
|
162
|
+
transport: typeof source.transport === 'string' ? source.transport : null,
|
|
163
|
+
terminalObserved,
|
|
164
|
+
continuation: !terminalObserved
|
|
165
|
+
&& (truncatedStream || stallObserved || streamStarted || TRUE(source.continuation)),
|
|
166
|
+
declaredContinuation,
|
|
167
|
+
textEmitted,
|
|
168
|
+
textObservedChars,
|
|
169
|
+
reasoningEmitted,
|
|
170
|
+
toolCallsStarted,
|
|
171
|
+
toolCallsComplete,
|
|
172
|
+
toolCallsDispatched,
|
|
173
|
+
// Monotonic across canonical round-trips: an already-recorded ambiguity is
|
|
174
|
+
// never downgraded by a later read that happens to see a dispatch count.
|
|
175
|
+
dispatchAmbiguous: TRUE(source.dispatchAmbiguous) || dispatchAmbiguous,
|
|
176
|
+
pendingToolInput: TRUE(source.pendingToolInput) || TRUE(source.pendingToolUse),
|
|
177
|
+
userAbort: TRUE(source.userAbort) || _isAbortLike(source),
|
|
178
|
+
stallObserved,
|
|
179
|
+
truncatedStream,
|
|
180
|
+
unsafeMarker: TRUE(source.unsafeToRetry),
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const EMPTY_SIGNALS = {
|
|
185
|
+
provider: null, transport: null,
|
|
186
|
+
terminalObserved: false, continuation: false, declaredContinuation: false,
|
|
187
|
+
textEmitted: false, textObservedChars: 0, reasoningEmitted: false,
|
|
188
|
+
toolCallsStarted: false, toolCallsComplete: 0, toolCallsDispatched: 0,
|
|
189
|
+
dispatchAmbiguous: false, pendingToolInput: false, userAbort: false,
|
|
190
|
+
stallObserved: false, truncatedStream: false, unsafeMarker: false,
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function _merge(a, b) {
|
|
194
|
+
if (!b) return a
|
|
195
|
+
if (!a) return b
|
|
196
|
+
return {
|
|
197
|
+
provider: a.provider || b.provider || null,
|
|
198
|
+
transport: a.transport || b.transport || null,
|
|
199
|
+
terminalObserved: a.terminalObserved || b.terminalObserved,
|
|
200
|
+
continuation: a.continuation || b.continuation,
|
|
201
|
+
declaredContinuation: a.declaredContinuation || b.declaredContinuation,
|
|
202
|
+
textEmitted: a.textEmitted || b.textEmitted,
|
|
203
|
+
textObservedChars: Math.max(a.textObservedChars, b.textObservedChars),
|
|
204
|
+
reasoningEmitted: a.reasoningEmitted || b.reasoningEmitted,
|
|
205
|
+
toolCallsStarted: a.toolCallsStarted || b.toolCallsStarted,
|
|
206
|
+
toolCallsComplete: Math.max(a.toolCallsComplete, b.toolCallsComplete),
|
|
207
|
+
toolCallsDispatched: Math.max(a.toolCallsDispatched, b.toolCallsDispatched),
|
|
208
|
+
dispatchAmbiguous: a.dispatchAmbiguous || b.dispatchAmbiguous,
|
|
209
|
+
pendingToolInput: a.pendingToolInput || b.pendingToolInput,
|
|
210
|
+
userAbort: a.userAbort || b.userAbort,
|
|
211
|
+
stallObserved: a.stallObserved || b.stallObserved,
|
|
212
|
+
truncatedStream: a.truncatedStream || b.truncatedStream,
|
|
213
|
+
unsafeMarker: a.unsafeMarker || b.unsafeMarker,
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function _finalize(signals) {
|
|
218
|
+
const s = signals || EMPTY_SIGNALS
|
|
219
|
+
const terminalObserved = s.terminalObserved === true
|
|
220
|
+
const visibleOutput = s.textEmitted || s.reasoningEmitted
|
|
221
|
+
const sideEffectDispatched = s.toolCallsDispatched > 0
|
|
222
|
+
const observedOutput = visibleOutput || s.textObservedChars > 0
|
|
223
|
+
|| s.toolCallsComplete > 0 || sideEffectDispatched
|
|
224
|
+
// The architecture-specific deny: a replay would duplicate output the user
|
|
225
|
+
// already saw, or re-run a tool that was already handed off (including the
|
|
226
|
+
// ambiguous case where a complete tool call carries no dispatch report).
|
|
227
|
+
const replayUnsafe = visibleOutput || sideEffectDispatched
|
|
228
|
+
|| s.dispatchAmbiguous === true || s.unsafeMarker === true
|
|
229
|
+
const replaySafe = !replayUnsafe && s.userAbort !== true
|
|
230
|
+
// A terminal frame that declared `end_turn=false` keeps the turn open.
|
|
231
|
+
const continuation = s.declaredContinuation === true
|
|
232
|
+
? true
|
|
233
|
+
: (terminalObserved ? false : s.continuation === true)
|
|
234
|
+
return Object.freeze({
|
|
235
|
+
version: STREAM_OUTCOME_VERSION,
|
|
236
|
+
provider: s.provider || null,
|
|
237
|
+
transport: s.transport || STREAM_TRANSPORTS.UNKNOWN,
|
|
238
|
+
terminalObserved,
|
|
239
|
+
continuation,
|
|
240
|
+
declaredContinuation: s.declaredContinuation === true,
|
|
241
|
+
textEmitted: s.textEmitted === true,
|
|
242
|
+
textObservedChars: s.textObservedChars | 0,
|
|
243
|
+
reasoningEmitted: s.reasoningEmitted === true,
|
|
244
|
+
toolCallsStarted: s.toolCallsStarted === true,
|
|
245
|
+
toolCallsComplete: s.toolCallsComplete | 0,
|
|
246
|
+
toolCallsDispatched: s.toolCallsDispatched | 0,
|
|
247
|
+
dispatchAmbiguous: s.dispatchAmbiguous === true,
|
|
248
|
+
pendingToolInput: s.pendingToolInput === true,
|
|
249
|
+
userAbort: s.userAbort === true,
|
|
250
|
+
stallObserved: s.stallObserved === true,
|
|
251
|
+
truncatedStream: s.truncatedStream === true,
|
|
252
|
+
// Derived verdicts.
|
|
253
|
+
visibleOutput,
|
|
254
|
+
observedOutput,
|
|
255
|
+
sideEffectDispatched,
|
|
256
|
+
replayUnsafe,
|
|
257
|
+
replaySafe,
|
|
258
|
+
successEligible: terminalObserved && !continuation && s.pendingToolInput !== true,
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Normalize any number of sources (thrown error, provider midState, explicit
|
|
264
|
+
* hint object) into the canonical record. Sources are OR/max merged; an
|
|
265
|
+
* already-canonical `source.streamOutcome` is merged in as well.
|
|
266
|
+
*/
|
|
267
|
+
export function readStreamOutcome(...sources) {
|
|
268
|
+
let signals = null
|
|
269
|
+
for (const source of sources) {
|
|
270
|
+
if (!source) continue
|
|
271
|
+
const existing = source.streamOutcome
|
|
272
|
+
if (existing && existing.version === STREAM_OUTCOME_VERSION) {
|
|
273
|
+
signals = _merge(signals, _signalsOf(existing))
|
|
274
|
+
}
|
|
275
|
+
signals = _merge(signals, _signalsOf(source))
|
|
276
|
+
}
|
|
277
|
+
return _finalize(signals || EMPTY_SIGNALS)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Alias kept for callers that read a provider midState directly. */
|
|
281
|
+
export const streamOutcomeFromState = readStreamOutcome
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Attach the canonical record to `target` (usually a thrown error) and mirror
|
|
285
|
+
* it onto the historical alias flags. Aliases are only ever SET, never
|
|
286
|
+
* cleared, so a provider that already stamped one keeps it. Text that was
|
|
287
|
+
* merely observed (buffered, never relayed) does NOT set liveTextEmitted:
|
|
288
|
+
* only relayed text is a visibility/no-replay boundary.
|
|
289
|
+
*/
|
|
290
|
+
export function stampStreamOutcome(target, ...sources) {
|
|
291
|
+
if (!target || (typeof target !== 'object' && typeof target !== 'function')) return target
|
|
292
|
+
const outcome = readStreamOutcome(target, ...sources)
|
|
293
|
+
try {
|
|
294
|
+
target.streamOutcome = outcome
|
|
295
|
+
if (outcome.textEmitted) { target.liveTextEmitted = true; target.emittedText = true }
|
|
296
|
+
if (outcome.reasoningEmitted) target.emittedThinking = true
|
|
297
|
+
if (outcome.toolCallsStarted) target.partialToolCall = true
|
|
298
|
+
if (outcome.sideEffectDispatched) { target.emittedToolCall = true; target.toolCallEmitted = true }
|
|
299
|
+
if (outcome.pendingToolInput) target.pendingToolUse = true
|
|
300
|
+
if (outcome.truncatedStream) target.truncatedStream = true
|
|
301
|
+
if (outcome.replayUnsafe) target.unsafeToRetry = true
|
|
302
|
+
} catch { /* frozen/exotic targets keep the returned record only */ }
|
|
303
|
+
return outcome
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** True when re-issuing the turn would not duplicate exposure or side effects. */
|
|
307
|
+
export function isReplaySafe(source) {
|
|
308
|
+
return readStreamOutcome(source).replaySafe
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** True when there is positive evidence that a replay would duplicate output. */
|
|
312
|
+
export function isReplayUnsafe(source) {
|
|
313
|
+
return readStreamOutcome(source).replayUnsafe
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** True when the outcome may be promoted to a normal successful response. */
|
|
317
|
+
export function canPromoteToSuccess(source) {
|
|
318
|
+
return readStreamOutcome(source).successEligible
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** True when anything the model produced was observed (visible or buffered). */
|
|
322
|
+
export function hasObservedOutput(source) {
|
|
323
|
+
return readStreamOutcome(source).observedOutput
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** True when a tool call was handed off and may already have run. */
|
|
327
|
+
export function hasDispatchedToolCalls(source) {
|
|
328
|
+
return readStreamOutcome(source).sideEffectDispatched
|
|
329
|
+
}
|
|
@@ -236,6 +236,18 @@ function normalizeOpenAICodexResetCredits(data, accountId = '') {
|
|
|
236
236
|
const availableRows = credits.filter((credit) => credit.status === 'available');
|
|
237
237
|
if (explicitCount === null && !credits.length) return null;
|
|
238
238
|
const availableCount = Math.max(0, Math.floor(explicitCount ?? availableRows.length));
|
|
239
|
+
const availableCredits = availableRows
|
|
240
|
+
.map((credit) => ({
|
|
241
|
+
...(Number.isFinite(credit.expiresAt) && credit.expiresAt > 0
|
|
242
|
+
? { expiresAt: credit.expiresAt }
|
|
243
|
+
: {}),
|
|
244
|
+
...(Number.isFinite(credit.grantedAt) && credit.grantedAt > 0
|
|
245
|
+
? { grantedAt: credit.grantedAt }
|
|
246
|
+
: {}),
|
|
247
|
+
}))
|
|
248
|
+
.sort((left, right) =>
|
|
249
|
+
(left.expiresAt ?? Number.POSITIVE_INFINITY)
|
|
250
|
+
- (right.expiresAt ?? Number.POSITIVE_INFINITY));
|
|
239
251
|
const expiryCandidates = availableRows
|
|
240
252
|
.map((credit) => credit.expiresAt)
|
|
241
253
|
.filter((value) => Number.isFinite(value) && value > 0);
|
|
@@ -249,6 +261,7 @@ function normalizeOpenAICodexResetCredits(data, accountId = '') {
|
|
|
249
261
|
})).digest('hex')}`;
|
|
250
262
|
return {
|
|
251
263
|
availableCount,
|
|
264
|
+
availableCredits,
|
|
252
265
|
...(nextExpiresAt ? { nextExpiresAt } : {}),
|
|
253
266
|
offerRevision,
|
|
254
267
|
};
|
|
@@ -600,7 +613,16 @@ async function fetchOpenAICodexUsage(providerObj) {
|
|
|
600
613
|
if (!res.ok) throw new Error(`openai-oauth usage ${res.status}`);
|
|
601
614
|
const data = await res.json();
|
|
602
615
|
const usage = normalizeOpenAIWhamUsage(data);
|
|
603
|
-
|
|
616
|
+
// Orca parity: Codex may include the authoritative available count directly
|
|
617
|
+
// in /wham/usage while the detail endpoint is unavailable. Keep that count
|
|
618
|
+
// usable (including a scoped offer revision), then let the dedicated endpoint
|
|
619
|
+
// replace it when richer expiry details arrive.
|
|
620
|
+
const embeddedResetCredits = normalizeOpenAICodexResetCredits(
|
|
621
|
+
data?.rate_limit_reset_credits,
|
|
622
|
+
auth.accountId,
|
|
623
|
+
);
|
|
624
|
+
const resolvedResetCredits = resetCredits || embeddedResetCredits;
|
|
625
|
+
return usage && resolvedResetCredits ? { ...usage, resetCredits: resolvedResetCredits } : usage;
|
|
604
626
|
}
|
|
605
627
|
|
|
606
628
|
async function fetchAnthropicUsage(providerObj) {
|