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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mixdog",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.46",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Standalone mixdog coding-agent CLI/TUI workspace.",
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"diff": "^9.0.0",
|
|
95
95
|
"discord.js": "^14.26.4",
|
|
96
96
|
"ink": "^7.1.0",
|
|
97
|
-
"jsdom": "^
|
|
97
|
+
"jsdom": "^27.0.0",
|
|
98
98
|
"kiwi-nlp": "^0.21.0",
|
|
99
99
|
"marked": "^14.1.4",
|
|
100
100
|
"node-cron": "^4.5.0",
|
|
@@ -115,6 +115,9 @@
|
|
|
115
115
|
},
|
|
116
116
|
"@discordjs/rest": {
|
|
117
117
|
"undici": "6.27.0"
|
|
118
|
+
},
|
|
119
|
+
"onnxruntime-node": {
|
|
120
|
+
"global-agent": "^4.1.3"
|
|
118
121
|
}
|
|
119
122
|
},
|
|
120
123
|
"devDependencies": {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { composeAgentDispatchAbortSignal } from '../src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs';
|
|
5
|
+
|
|
6
|
+
test('explore cancellation survives an active idle watchdog composite link', () => {
|
|
7
|
+
const factory = new AbortController();
|
|
8
|
+
const explore = new AbortController();
|
|
9
|
+
const idleWatchdog = new AbortController();
|
|
10
|
+
const linked = composeAgentDispatchAbortSignal([
|
|
11
|
+
factory.signal,
|
|
12
|
+
explore.signal,
|
|
13
|
+
idleWatchdog.signal,
|
|
14
|
+
]);
|
|
15
|
+
const reason = new Error('explore ESC');
|
|
16
|
+
explore.abort(reason);
|
|
17
|
+
assert.equal(linked.signal.aborted, true);
|
|
18
|
+
assert.equal(linked.signal.reason, reason, 'per-call explore reason reaches the one session link');
|
|
19
|
+
assert.equal(idleWatchdog.signal.aborted, false, 'watchdog remains independently active');
|
|
20
|
+
linked.dispose();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('an already-aborted parent reason is retained before the askSession controller swap', () => {
|
|
24
|
+
const factory = new AbortController();
|
|
25
|
+
const reason = new Error('factory canceled first');
|
|
26
|
+
factory.abort(reason);
|
|
27
|
+
const linked = composeAgentDispatchAbortSignal([factory.signal, new AbortController().signal]);
|
|
28
|
+
assert.equal(linked.signal.aborted, true);
|
|
29
|
+
assert.equal(linked.signal.reason, reason);
|
|
30
|
+
linked.dispose();
|
|
31
|
+
});
|
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { EventEmitter } from 'node:events';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
_clearSessionRuntime,
|
|
8
|
+
_getRuntimeEntry,
|
|
9
|
+
getSessionProgressSnapshot,
|
|
10
|
+
markSessionAskStart,
|
|
11
|
+
markSessionStreamDelta,
|
|
12
|
+
} from '../src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs';
|
|
13
|
+
import {
|
|
14
|
+
evaluateAgentWatchdogAbort,
|
|
15
|
+
resolveEffectiveToolRunningCeilingMs,
|
|
16
|
+
} from '../src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs';
|
|
17
|
+
import {
|
|
18
|
+
buildAgentTaskProgressFields,
|
|
19
|
+
formatAgentWatchdogSummary,
|
|
20
|
+
} from '../src/standalone/agent-task-status.mjs';
|
|
21
|
+
import {
|
|
22
|
+
consumeCompatChatCompletionStream,
|
|
23
|
+
consumeCompatResponsesStream,
|
|
24
|
+
} from '../src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs';
|
|
25
|
+
import { parseSSEStream } from '../src/runtime/agent/orchestrator/providers/anthropic-sse.mjs';
|
|
26
|
+
import { _streamResponse } from '../src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs';
|
|
27
|
+
import { sendViaHttpSse } from '../src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs';
|
|
28
|
+
import { shouldFallbackTransport } from '../src/runtime/agent/orchestrator/providers/retry-classifier.mjs';
|
|
29
|
+
|
|
30
|
+
const policy = {
|
|
31
|
+
firstTransportMs: 120_000,
|
|
32
|
+
firstSemanticMs: 600_000,
|
|
33
|
+
idleStaleMs: 600_000,
|
|
34
|
+
toolRunningMs: 600_000,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function seededSnapshot(id, elapsedMs) {
|
|
38
|
+
markSessionAskStart(id);
|
|
39
|
+
const entry = _getRuntimeEntry(id);
|
|
40
|
+
const startedAt = Date.now() - elapsedMs;
|
|
41
|
+
entry.askStartedAt = startedAt;
|
|
42
|
+
entry.modelRequestStartedAt = startedAt;
|
|
43
|
+
entry.lastProgressAt = startedAt;
|
|
44
|
+
return { entry, snapshot: () => getSessionProgressSnapshot(id) };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
test('independent first transport and semantic deadlines', async (t) => {
|
|
48
|
+
const id = `liveness-deadlines-${Date.now()}`;
|
|
49
|
+
t.after(() => _clearSessionRuntime(id));
|
|
50
|
+
const { entry, snapshot } = seededSnapshot(id, 121_000);
|
|
51
|
+
let err = evaluateAgentWatchdogAbort(snapshot(), Date.now(), policy);
|
|
52
|
+
assert.match(err?.message || '', /first transport stale \(120000ms\)/);
|
|
53
|
+
|
|
54
|
+
entry.lastTransportAt = Date.now();
|
|
55
|
+
err = evaluateAgentWatchdogAbort(snapshot(), entry.modelRequestStartedAt + 599_999, policy);
|
|
56
|
+
assert.equal(err, null, 'keepalive transport must not switch/reset the semantic deadline');
|
|
57
|
+
err = evaluateAgentWatchdogAbort(snapshot(), entry.modelRequestStartedAt + 600_001, policy);
|
|
58
|
+
assert.match(err?.message || '', /first semantic response stale \(600000ms\)/);
|
|
59
|
+
|
|
60
|
+
await markSessionStreamDelta(id, 'reasoning');
|
|
61
|
+
assert.equal(evaluateAgentWatchdogAbort(snapshot(), entry.modelRequestStartedAt + 700_000, policy), null);
|
|
62
|
+
assert.equal(snapshot().hasVisibleProgress, false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('status labels distinguish transport, semantic, reasoning, text, tool, and true stall', async (t) => {
|
|
66
|
+
const id = `liveness-status-${Date.now()}`;
|
|
67
|
+
t.after(() => _clearSessionRuntime(id));
|
|
68
|
+
const { entry, snapshot } = seededSnapshot(id, 10_000);
|
|
69
|
+
const fields = () => buildAgentTaskProgressFields({
|
|
70
|
+
now: Date.now(),
|
|
71
|
+
runtimeStage: entry.stage,
|
|
72
|
+
snapshot: snapshot(),
|
|
73
|
+
runtime: entry,
|
|
74
|
+
policy,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
assert.equal(fields().last_progress, 'awaiting model transport');
|
|
78
|
+
assert.equal(fields().watchdog, 'armed transport=120s');
|
|
79
|
+
await markSessionStreamDelta(id, 'transport');
|
|
80
|
+
assert.equal(fields().last_progress, 'transport active; awaiting first model event');
|
|
81
|
+
assert.equal(fields().watchdog, 'armed semantic=600s');
|
|
82
|
+
await markSessionStreamDelta(id, 'semantic');
|
|
83
|
+
assert.equal(fields().last_progress, 'model active (no visible output yet)');
|
|
84
|
+
await markSessionStreamDelta(id, 'reasoning');
|
|
85
|
+
assert.equal(fields().last_progress, 'model reasoning (hidden; no visible output yet)');
|
|
86
|
+
assert.equal(fields().diagnostic, 'hidden reasoning active; no visible output yet');
|
|
87
|
+
await markSessionStreamDelta(id, 'text');
|
|
88
|
+
assert.equal(fields().last_progress, 'visible model text');
|
|
89
|
+
await markSessionStreamDelta(id, 'reasoning');
|
|
90
|
+
assert.equal(fields().last_progress, 'model reasoning (hidden; visible output previously emitted)');
|
|
91
|
+
await markSessionStreamDelta(id, 'tool');
|
|
92
|
+
entry.lastVisibleTextAt = null;
|
|
93
|
+
assert.equal(fields().last_progress, 'tool protocol progress');
|
|
94
|
+
assert.equal(formatAgentWatchdogSummary(policy), 'armed transport=120s semantic=600s idle=600s tool=600s');
|
|
95
|
+
|
|
96
|
+
entry.firstSemanticAt = null;
|
|
97
|
+
entry.lastSemanticAt = null;
|
|
98
|
+
entry.lastTransportAt = Date.now();
|
|
99
|
+
entry.modelRequestStartedAt = Date.now() - 600_001;
|
|
100
|
+
assert.match(fields().diagnostic, /^stale: first semantic response stale/);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('tool-running status uses the same effective self-deadline ceiling as the watchdog', () => {
|
|
104
|
+
const snapshot = {
|
|
105
|
+
stage: 'tool_running',
|
|
106
|
+
toolStartedAt: 1_000,
|
|
107
|
+
currentTool: 'shell',
|
|
108
|
+
toolSelfDeadlineMs: 900_000,
|
|
109
|
+
lastProgressAt: 1_000,
|
|
110
|
+
};
|
|
111
|
+
const toolPolicy = { ...policy, idleStaleMs: 0 };
|
|
112
|
+
assert.equal(resolveEffectiveToolRunningCeilingMs(snapshot, toolPolicy), 960_000);
|
|
113
|
+
assert.equal(formatAgentWatchdogSummary(toolPolicy, snapshot), 'armed tool=960s');
|
|
114
|
+
assert.equal(evaluateAgentWatchdogAbort(snapshot, 961_000, toolPolicy), null);
|
|
115
|
+
assert.match(
|
|
116
|
+
evaluateAgentWatchdogAbort(snapshot, 961_001, toolPolicy)?.message || '',
|
|
117
|
+
/tool running stale \(960000ms\)/,
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
function asyncEvents(events) {
|
|
122
|
+
return { async *[Symbol.asyncIterator]() { for (const event of events) yield event; } };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function heartbeatOnlyStream(firstEvent, heartbeatEvent, intervalMs = 10) {
|
|
126
|
+
return {
|
|
127
|
+
async *[Symbol.asyncIterator]() {
|
|
128
|
+
yield firstEvent;
|
|
129
|
+
while (true) {
|
|
130
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
131
|
+
yield heartbeatEvent;
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function closablePendingStream(firstEvent, { closeError = null } = {}) {
|
|
138
|
+
let delivered = false;
|
|
139
|
+
let returnCalls = 0;
|
|
140
|
+
const iterator = {
|
|
141
|
+
next() {
|
|
142
|
+
if (!delivered) {
|
|
143
|
+
delivered = true;
|
|
144
|
+
return Promise.resolve({ done: false, value: firstEvent });
|
|
145
|
+
}
|
|
146
|
+
return new Promise(() => {});
|
|
147
|
+
},
|
|
148
|
+
return() {
|
|
149
|
+
returnCalls += 1;
|
|
150
|
+
if (closeError) return Promise.reject(closeError);
|
|
151
|
+
return Promise.resolve({ done: true });
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
return {
|
|
155
|
+
stream: { [Symbol.asyncIterator]: () => iterator },
|
|
156
|
+
returnCalls: () => returnCalls,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function assertCompatIteratorCloses({ kind, mode }) {
|
|
161
|
+
const firstEvent = kind === 'chat'
|
|
162
|
+
? { choices: [{ delta: { role: 'assistant' } }] }
|
|
163
|
+
: { type: 'response.created', response: { id: 'r1', model: 'gpt' } };
|
|
164
|
+
const closeError = new Error(`${kind}-${mode} close failure`);
|
|
165
|
+
const pending = closablePendingStream(firstEvent, { closeError });
|
|
166
|
+
const controller = new AbortController();
|
|
167
|
+
const originalAbort = new Error(`${kind} original abort`);
|
|
168
|
+
const options = {
|
|
169
|
+
label: `${kind}-${mode}`,
|
|
170
|
+
signal: controller.signal,
|
|
171
|
+
semanticIdleTimeoutMs: 25,
|
|
172
|
+
...(kind === 'responses'
|
|
173
|
+
? { parseResponsesToolCalls: () => undefined, responseOutputText: () => '' }
|
|
174
|
+
: {}),
|
|
175
|
+
};
|
|
176
|
+
const consume = kind === 'chat'
|
|
177
|
+
? consumeCompatChatCompletionStream
|
|
178
|
+
: consumeCompatResponsesStream;
|
|
179
|
+
const keepAlive = setInterval(() => {}, 10);
|
|
180
|
+
try {
|
|
181
|
+
const run = consume(pending.stream, options);
|
|
182
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
183
|
+
if (mode === 'abort') controller.abort(originalAbort);
|
|
184
|
+
if (mode === 'abort') {
|
|
185
|
+
await assert.rejects(run, (err) => err === originalAbort);
|
|
186
|
+
} else {
|
|
187
|
+
await assert.rejects(run, (err) => err?.name === 'StreamStalledError');
|
|
188
|
+
}
|
|
189
|
+
assert.equal(pending.returnCalls(), 1, `${kind} ${mode} must close iterator once`);
|
|
190
|
+
} finally {
|
|
191
|
+
clearInterval(keepAlive);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
test('compat Chat/Responses watchdog and abort rejection close iterators without masking errors', async () => {
|
|
196
|
+
for (const kind of ['chat', 'responses']) {
|
|
197
|
+
for (const mode of ['abort', 'watchdog']) {
|
|
198
|
+
await assertCompatIteratorCloses({ kind, mode });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('recovered Chat tool persists eager-dispatch safety across a later stream failure', async () => {
|
|
204
|
+
const original = new Error('compat stream failed after leaked tool dispatch');
|
|
205
|
+
let step = 0;
|
|
206
|
+
const stream = {
|
|
207
|
+
[Symbol.asyncIterator]() {
|
|
208
|
+
return {
|
|
209
|
+
next() {
|
|
210
|
+
step += 1;
|
|
211
|
+
if (step === 1) {
|
|
212
|
+
return Promise.resolve({
|
|
213
|
+
done: false,
|
|
214
|
+
value: { choices: [{ delta: { content: '<invoke name="read"><parameter name="path">a.txt</parameter></invoke>' } }] },
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
return Promise.reject(original);
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
const calls = [];
|
|
223
|
+
const rejected = await consumeCompatChatCompletionStream(stream, {
|
|
224
|
+
label: 'leaked-tool-failure',
|
|
225
|
+
knownToolNames: new Set(['read']),
|
|
226
|
+
onToolCall: (call) => calls.push(call),
|
|
227
|
+
parseToolCalls: () => undefined,
|
|
228
|
+
}).then(() => null, (err) => err);
|
|
229
|
+
assert.equal(rejected, original);
|
|
230
|
+
assert.equal(calls.length, 1);
|
|
231
|
+
assert.equal(calls[0].name, 'read');
|
|
232
|
+
assert.equal(rejected.emittedToolCall, true);
|
|
233
|
+
assert.equal(rejected.unsafeToRetry, true);
|
|
234
|
+
assert.equal(shouldFallbackTransport(rejected, { enabled: true }), false);
|
|
235
|
+
|
|
236
|
+
const truncatedCalls = [];
|
|
237
|
+
const truncated = await consumeCompatChatCompletionStream(asyncEvents([
|
|
238
|
+
{ choices: [{ delta: { content: '<invoke name="read"><parameter name="path">b.txt</parameter></invoke>' } }] },
|
|
239
|
+
]), {
|
|
240
|
+
label: 'leaked-tool-truncation',
|
|
241
|
+
knownToolNames: new Set(['read']),
|
|
242
|
+
onToolCall: (call) => truncatedCalls.push(call),
|
|
243
|
+
parseToolCalls: () => undefined,
|
|
244
|
+
}).then(() => null, (err) => err);
|
|
245
|
+
assert.equal(truncatedCalls.length, 1);
|
|
246
|
+
assert.equal(truncated.emittedToolCall, true);
|
|
247
|
+
assert.equal(truncated.unsafeToRetry, true);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('compat Chat/Responses metadata heartbeats stay transport-only and cannot rearm semantic idle', async () => {
|
|
251
|
+
const chatKinds = [];
|
|
252
|
+
await assert.rejects(
|
|
253
|
+
consumeCompatChatCompletionStream(
|
|
254
|
+
heartbeatOnlyStream(
|
|
255
|
+
{ choices: [{ delta: { role: 'assistant' } }] },
|
|
256
|
+
{ id: 'meta', model: 'gpt', choices: [] },
|
|
257
|
+
),
|
|
258
|
+
{
|
|
259
|
+
label: 'chat-heartbeat',
|
|
260
|
+
semanticIdleTimeoutMs: 45,
|
|
261
|
+
onStreamDelta: (kind) => chatKinds.push(kind),
|
|
262
|
+
},
|
|
263
|
+
),
|
|
264
|
+
(err) => err?.name === 'StreamStalledError',
|
|
265
|
+
);
|
|
266
|
+
assert.deepEqual(chatKinds.filter((kind) => kind !== 'transport'), ['semantic']);
|
|
267
|
+
const chatSemanticIndex = chatKinds.indexOf('semantic');
|
|
268
|
+
assert.ok(chatSemanticIndex >= 0);
|
|
269
|
+
assert.ok(chatKinds.slice(chatSemanticIndex + 1).every((kind) => kind === 'transport'));
|
|
270
|
+
|
|
271
|
+
const responseKinds = [];
|
|
272
|
+
await assert.rejects(
|
|
273
|
+
consumeCompatResponsesStream(
|
|
274
|
+
heartbeatOnlyStream(
|
|
275
|
+
{ type: 'response.created', response: { id: 'r1', model: 'gpt' } },
|
|
276
|
+
{ type: 'response.in_progress', response: { id: 'r1' } },
|
|
277
|
+
),
|
|
278
|
+
{
|
|
279
|
+
label: 'responses-heartbeat',
|
|
280
|
+
semanticIdleTimeoutMs: 45,
|
|
281
|
+
onStreamDelta: (kind) => responseKinds.push(kind),
|
|
282
|
+
parseResponsesToolCalls: () => undefined,
|
|
283
|
+
responseOutputText: () => '',
|
|
284
|
+
},
|
|
285
|
+
),
|
|
286
|
+
(err) => err?.name === 'StreamStalledError',
|
|
287
|
+
);
|
|
288
|
+
assert.deepEqual(responseKinds.filter((kind) => kind !== 'transport'), ['semantic']);
|
|
289
|
+
const responseSemanticIndex = responseKinds.indexOf('semantic');
|
|
290
|
+
assert.ok(responseSemanticIndex >= 0);
|
|
291
|
+
assert.ok(responseKinds.slice(responseSemanticIndex + 1).every((kind) => kind === 'transport'));
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test('OpenAI compat chat and Responses classify semantic progress without exposing reasoning', async () => {
|
|
295
|
+
const chatKinds = [];
|
|
296
|
+
const chat = await consumeCompatChatCompletionStream(asyncEvents([
|
|
297
|
+
{ choices: [{ delta: { reasoning_content: 'secret' } }] },
|
|
298
|
+
{ choices: [{ delta: { content: 'hello' } }] },
|
|
299
|
+
{ choices: [{ delta: { tool_calls: [{ index: 0, id: 'call_1', function: { name: 'read', arguments: '{}' } }] }, finish_reason: 'tool_calls' }] },
|
|
300
|
+
]), {
|
|
301
|
+
label: 'test',
|
|
302
|
+
onStreamDelta: (kind) => chatKinds.push(kind),
|
|
303
|
+
parseToolCalls: () => [{ id: 'call_1', name: 'read', arguments: {} }],
|
|
304
|
+
});
|
|
305
|
+
assert.ok(chatKinds.includes('transport'));
|
|
306
|
+
assert.ok(chatKinds.includes('reasoning'));
|
|
307
|
+
assert.ok(chatKinds.includes('text'));
|
|
308
|
+
assert.ok(chatKinds.includes('tool'));
|
|
309
|
+
assert.equal(chat.content, 'hello');
|
|
310
|
+
assert.equal(chat.reasoningContent, 'secret');
|
|
311
|
+
|
|
312
|
+
const responseKinds = [];
|
|
313
|
+
await consumeCompatResponsesStream(asyncEvents([
|
|
314
|
+
{ type: 'response.created', response: { id: 'r1', model: 'gpt' } },
|
|
315
|
+
{ type: 'response.reasoning_text.delta', delta: 'secret' },
|
|
316
|
+
{ type: 'response.output_text.delta', delta: 'done' },
|
|
317
|
+
{ type: 'response.completed', response: { id: 'r1', model: 'gpt', output: [] } },
|
|
318
|
+
]), {
|
|
319
|
+
label: 'test',
|
|
320
|
+
onStreamDelta: (kind) => responseKinds.push(kind),
|
|
321
|
+
parseResponsesToolCalls: () => undefined,
|
|
322
|
+
responseOutputText: () => '',
|
|
323
|
+
});
|
|
324
|
+
assert.deepEqual(new Set(responseKinds), new Set(['transport', 'semantic', 'reasoning', 'text']));
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
const encoder = new TextEncoder();
|
|
328
|
+
function anthropicResponse(events) {
|
|
329
|
+
const chunks = events.map((event) => encoder.encode(
|
|
330
|
+
`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`,
|
|
331
|
+
));
|
|
332
|
+
let index = 0;
|
|
333
|
+
return { body: { getReader: () => ({
|
|
334
|
+
read: async () => index < chunks.length
|
|
335
|
+
? { done: false, value: chunks[index++] }
|
|
336
|
+
: { done: true },
|
|
337
|
+
cancel: async () => {},
|
|
338
|
+
releaseLock: () => {},
|
|
339
|
+
}) } };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
test('Anthropic classifies message_start, reasoning, text, and tool protocol with transport separate', async () => {
|
|
343
|
+
const kinds = [];
|
|
344
|
+
await parseSSEStream(anthropicResponse([
|
|
345
|
+
{ type: 'message_start', message: { model: 'claude', usage: {} } },
|
|
346
|
+
{ type: 'content_block_start', index: 0, content_block: { type: 'thinking' } },
|
|
347
|
+
{ type: 'content_block_delta', index: 0, delta: { type: 'thinking_delta', thinking: 'secret' } },
|
|
348
|
+
{ type: 'content_block_start', index: 1, content_block: { type: 'text' } },
|
|
349
|
+
{ type: 'content_block_delta', index: 1, delta: { type: 'text_delta', text: 'hello' } },
|
|
350
|
+
{ type: 'content_block_start', index: 2, content_block: { type: 'tool_use', id: 't1', name: 'read' } },
|
|
351
|
+
{ type: 'content_block_delta', index: 2, delta: { type: 'input_json_delta', partial_json: '{}' } },
|
|
352
|
+
{ type: 'content_block_stop', index: 2 },
|
|
353
|
+
{ type: 'message_delta', delta: { stop_reason: 'tool_use' }, usage: {} },
|
|
354
|
+
]), null, () => {}, (kind) => kinds.push(kind), () => {}, {}, null);
|
|
355
|
+
assert.ok(kinds.includes('transport'));
|
|
356
|
+
assert.ok(kinds.includes('semantic'));
|
|
357
|
+
assert.ok(kinds.includes('reasoning'));
|
|
358
|
+
assert.ok(kinds.includes('text'));
|
|
359
|
+
assert.ok(kinds.includes('tool'));
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test('Anthropic redacted_thinking is private reasoning and never visible text', async () => {
|
|
363
|
+
const kinds = [];
|
|
364
|
+
const visible = [];
|
|
365
|
+
const result = await parseSSEStream(anthropicResponse([
|
|
366
|
+
{ type: 'message_start', message: { model: 'claude', usage: {} } },
|
|
367
|
+
{ type: 'content_block_start', index: 0, content_block: { type: 'redacted_thinking', data: 'opaque-secret' } },
|
|
368
|
+
{ type: 'content_block_stop', index: 0 },
|
|
369
|
+
{ type: 'message_delta', delta: { stop_reason: 'end_turn' }, usage: {} },
|
|
370
|
+
{ type: 'message_stop' },
|
|
371
|
+
]), null, () => {}, (kind) => kinds.push(kind), () => {}, {}, (text) => visible.push(text));
|
|
372
|
+
assert.ok(kinds.includes('reasoning'));
|
|
373
|
+
assert.equal(kinds.includes('text'), false);
|
|
374
|
+
assert.deepEqual(visible, []);
|
|
375
|
+
assert.equal(result.content, '');
|
|
376
|
+
assert.deepEqual(result.thinkingBlocks, [{ type: 'redacted_thinking', data: 'opaque-secret' }]);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
class FakeSocket extends EventEmitter {
|
|
380
|
+
constructor() { super(); this.readyState = 1; }
|
|
381
|
+
close() {}
|
|
382
|
+
ping() {}
|
|
383
|
+
feed(events) { for (const event of events) this.emit('message', JSON.stringify(event)); }
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function openAiHttpResponse(events) {
|
|
387
|
+
const chunks = events.map((event) => encoder.encode(
|
|
388
|
+
`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`,
|
|
389
|
+
));
|
|
390
|
+
let index = 0;
|
|
391
|
+
return {
|
|
392
|
+
ok: true,
|
|
393
|
+
status: 200,
|
|
394
|
+
headers: new Map(),
|
|
395
|
+
body: { getReader: () => ({
|
|
396
|
+
read: async () => index < chunks.length
|
|
397
|
+
? { done: false, value: chunks[index++] }
|
|
398
|
+
: { done: true },
|
|
399
|
+
cancel: async () => {},
|
|
400
|
+
releaseLock: () => {},
|
|
401
|
+
}) },
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function wedgedOpenAiHttpResponse() {
|
|
406
|
+
return {
|
|
407
|
+
ok: true,
|
|
408
|
+
status: 200,
|
|
409
|
+
headers: new Map(),
|
|
410
|
+
body: { getReader: () => ({
|
|
411
|
+
read: () => new Promise(() => {}),
|
|
412
|
+
// Deliberately never settles: sendViaHttpSse must reject its own
|
|
413
|
+
// pending read race rather than trusting cancel().
|
|
414
|
+
cancel: () => new Promise(() => {}),
|
|
415
|
+
releaseLock: () => {},
|
|
416
|
+
}) },
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
test('OAuth HTTP SSE actively rejects a wedged reader on external and semantic aborts', async () => {
|
|
421
|
+
const keepAlive = setInterval(() => {}, 10);
|
|
422
|
+
try {
|
|
423
|
+
const startedAt = Date.now();
|
|
424
|
+
const external = new AbortController();
|
|
425
|
+
const externalReason = new Error('reviewer external abort');
|
|
426
|
+
const externalRun = sendViaHttpSse({
|
|
427
|
+
auth: { type: 'openai-direct', apiKey: 'test' },
|
|
428
|
+
body: {},
|
|
429
|
+
opts: {},
|
|
430
|
+
externalSignal: external.signal,
|
|
431
|
+
useModel: 'gpt',
|
|
432
|
+
fetchFn: async () => wedgedOpenAiHttpResponse(),
|
|
433
|
+
});
|
|
434
|
+
setTimeout(() => external.abort(externalReason), 10);
|
|
435
|
+
await assert.rejects(externalRun, (err) => err === externalReason);
|
|
436
|
+
|
|
437
|
+
const semanticRun = sendViaHttpSse({
|
|
438
|
+
auth: { type: 'openai-direct', apiKey: 'test' },
|
|
439
|
+
body: {},
|
|
440
|
+
opts: { _semanticIdleTimeoutMs: 25 },
|
|
441
|
+
useModel: 'gpt',
|
|
442
|
+
fetchFn: async () => wedgedOpenAiHttpResponse(),
|
|
443
|
+
});
|
|
444
|
+
await assert.rejects(semanticRun, (err) => (
|
|
445
|
+
err?.name === 'StreamStalledError'
|
|
446
|
+
&& err?.streamStalled === true
|
|
447
|
+
));
|
|
448
|
+
assert.ok(Date.now() - startedAt < 1_000, 'wedged reads must reject near their abort deadlines');
|
|
449
|
+
} finally {
|
|
450
|
+
clearInterval(keepAlive);
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
test('completion bundles report actual reasoning/text/tool kinds across compat, WS, and OAuth SSE', async () => {
|
|
455
|
+
const output = [
|
|
456
|
+
{ type: 'reasoning', id: 'rs_1', encrypted_content: 'opaque', summary: [] },
|
|
457
|
+
{ type: 'message', content: [{ type: 'output_text', text: 'final' }] },
|
|
458
|
+
{ type: 'function_call', id: 'fc_1', call_id: 'call_1', name: 'read', arguments: '{}' },
|
|
459
|
+
];
|
|
460
|
+
|
|
461
|
+
const compatKinds = [];
|
|
462
|
+
await consumeCompatResponsesStream(asyncEvents([
|
|
463
|
+
{ type: 'response.created', response: { id: 'r1', model: 'gpt' } },
|
|
464
|
+
{ type: 'response.completed', response: { id: 'r1', model: 'gpt', status: 'completed', output } },
|
|
465
|
+
]), {
|
|
466
|
+
label: 'bundle-compat',
|
|
467
|
+
onStreamDelta: (kind) => compatKinds.push(kind),
|
|
468
|
+
parseResponsesToolCalls: () => undefined,
|
|
469
|
+
responseOutputText: () => 'final',
|
|
470
|
+
});
|
|
471
|
+
for (const kind of ['reasoning', 'text', 'tool']) assert.ok(compatKinds.includes(kind), `compat missing ${kind}`);
|
|
472
|
+
|
|
473
|
+
const wsKinds = [];
|
|
474
|
+
const socket = new FakeSocket();
|
|
475
|
+
const wsRun = _streamResponse({
|
|
476
|
+
entry: { socket },
|
|
477
|
+
state: {},
|
|
478
|
+
onStreamDelta: (kind) => wsKinds.push(kind),
|
|
479
|
+
_timeouts: { interChunkMs: 5_000, preResponseCreatedMs: 5_000, firstMeaningfulMs: 5_000 },
|
|
480
|
+
});
|
|
481
|
+
socket.feed([
|
|
482
|
+
{ type: 'response.created', response: { id: 'r1', model: 'gpt' } },
|
|
483
|
+
{ type: 'response.completed', response: { id: 'r1', model: 'gpt', output } },
|
|
484
|
+
]);
|
|
485
|
+
await wsRun;
|
|
486
|
+
for (const kind of ['reasoning', 'text', 'tool']) assert.ok(wsKinds.includes(kind), `ws missing ${kind}`);
|
|
487
|
+
|
|
488
|
+
const oauthKinds = [];
|
|
489
|
+
await sendViaHttpSse({
|
|
490
|
+
auth: { type: 'openai-direct', apiKey: 'test' },
|
|
491
|
+
body: {},
|
|
492
|
+
opts: {},
|
|
493
|
+
useModel: 'gpt',
|
|
494
|
+
onStreamDelta: (kind) => oauthKinds.push(kind),
|
|
495
|
+
fetchFn: async () => openAiHttpResponse([
|
|
496
|
+
{ type: 'response.created', response: { id: 'r1', model: 'gpt' } },
|
|
497
|
+
{ type: 'response.completed', response: { id: 'r1', model: 'gpt', status: 'completed', output } },
|
|
498
|
+
]),
|
|
499
|
+
});
|
|
500
|
+
for (const kind of ['reasoning', 'text', 'tool']) assert.ok(oauthKinds.includes(kind), `oauth missing ${kind}`);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
const PURE_LEAK = '<invoke name="read"><parameter name="path">a.txt</parameter></invoke>';
|
|
504
|
+
|
|
505
|
+
test('synthesized leaked tools report tool progress without visible-text progress', async () => {
|
|
506
|
+
const compatKinds = [];
|
|
507
|
+
const compatVisible = [];
|
|
508
|
+
await consumeCompatChatCompletionStream(asyncEvents([
|
|
509
|
+
{ choices: [{ delta: { content: PURE_LEAK } }] },
|
|
510
|
+
{ choices: [{ delta: {}, finish_reason: 'stop' }] },
|
|
511
|
+
]), {
|
|
512
|
+
label: 'leak-compat',
|
|
513
|
+
knownToolNames: new Set(['read']),
|
|
514
|
+
onStreamDelta: (kind) => compatKinds.push(kind),
|
|
515
|
+
onTextDelta: (text) => compatVisible.push(text),
|
|
516
|
+
parseToolCalls: () => undefined,
|
|
517
|
+
});
|
|
518
|
+
assert.ok(compatKinds.includes('tool'));
|
|
519
|
+
assert.equal(compatKinds.includes('text'), false);
|
|
520
|
+
assert.deepEqual(compatVisible, []);
|
|
521
|
+
|
|
522
|
+
const compatResponseKinds = [];
|
|
523
|
+
const compatResponseVisible = [];
|
|
524
|
+
await consumeCompatResponsesStream(asyncEvents([
|
|
525
|
+
{ type: 'response.created', response: { id: 'r1', model: 'gpt' } },
|
|
526
|
+
{ type: 'response.output_text.delta', delta: PURE_LEAK },
|
|
527
|
+
{ type: 'response.completed', response: { id: 'r1', model: 'gpt', status: 'completed', output: [] } },
|
|
528
|
+
]), {
|
|
529
|
+
label: 'leak-compat-responses',
|
|
530
|
+
knownToolNames: new Set(['read']),
|
|
531
|
+
onStreamDelta: (kind) => compatResponseKinds.push(kind),
|
|
532
|
+
onTextDelta: (text) => compatResponseVisible.push(text),
|
|
533
|
+
parseResponsesToolCalls: () => undefined,
|
|
534
|
+
responseOutputText: () => '',
|
|
535
|
+
});
|
|
536
|
+
assert.ok(compatResponseKinds.includes('tool'));
|
|
537
|
+
assert.equal(compatResponseKinds.includes('text'), false);
|
|
538
|
+
assert.deepEqual(compatResponseVisible, []);
|
|
539
|
+
|
|
540
|
+
const anthropicKinds = [];
|
|
541
|
+
const anthropicVisible = [];
|
|
542
|
+
await parseSSEStream(anthropicResponse([
|
|
543
|
+
{ type: 'message_start', message: { model: 'claude', usage: {} } },
|
|
544
|
+
{ type: 'content_block_start', index: 0, content_block: { type: 'text' } },
|
|
545
|
+
{ type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: PURE_LEAK } },
|
|
546
|
+
{ type: 'content_block_stop', index: 0 },
|
|
547
|
+
{ type: 'message_delta', delta: { stop_reason: 'end_turn' }, usage: {} },
|
|
548
|
+
{ type: 'message_stop' },
|
|
549
|
+
]), null, () => {}, (kind) => anthropicKinds.push(kind), () => {}, {}, (text) => anthropicVisible.push(text), new Set(['read']));
|
|
550
|
+
assert.ok(anthropicKinds.includes('tool'));
|
|
551
|
+
assert.equal(anthropicKinds.includes('text'), false);
|
|
552
|
+
assert.deepEqual(anthropicVisible, []);
|
|
553
|
+
|
|
554
|
+
const wsKinds = [];
|
|
555
|
+
const wsVisible = [];
|
|
556
|
+
const socket = new FakeSocket();
|
|
557
|
+
const wsRun = _streamResponse({
|
|
558
|
+
entry: { socket },
|
|
559
|
+
state: {},
|
|
560
|
+
knownToolNames: new Set(['read']),
|
|
561
|
+
onStreamDelta: (kind) => wsKinds.push(kind),
|
|
562
|
+
onTextDelta: (text) => wsVisible.push(text),
|
|
563
|
+
_timeouts: { interChunkMs: 5_000, preResponseCreatedMs: 5_000, firstMeaningfulMs: 5_000 },
|
|
564
|
+
});
|
|
565
|
+
socket.feed([
|
|
566
|
+
{ type: 'response.created', response: { id: 'r1', model: 'gpt' } },
|
|
567
|
+
{ type: 'response.output_text.delta', delta: PURE_LEAK },
|
|
568
|
+
{ type: 'response.completed', response: { id: 'r1', model: 'gpt', output: [] } },
|
|
569
|
+
]);
|
|
570
|
+
await wsRun;
|
|
571
|
+
assert.ok(wsKinds.includes('tool'));
|
|
572
|
+
assert.equal(wsKinds.includes('text'), false);
|
|
573
|
+
assert.deepEqual(wsVisible, []);
|
|
574
|
+
|
|
575
|
+
const oauthKinds = [];
|
|
576
|
+
const oauthVisible = [];
|
|
577
|
+
await sendViaHttpSse({
|
|
578
|
+
auth: { type: 'openai-direct', apiKey: 'test' },
|
|
579
|
+
body: { tools: [{ name: 'read' }] },
|
|
580
|
+
opts: {},
|
|
581
|
+
useModel: 'gpt',
|
|
582
|
+
onStreamDelta: (kind) => oauthKinds.push(kind),
|
|
583
|
+
onTextDelta: (text) => oauthVisible.push(text),
|
|
584
|
+
fetchFn: async () => openAiHttpResponse([
|
|
585
|
+
{ type: 'response.created', response: { id: 'r1', model: 'gpt' } },
|
|
586
|
+
{ type: 'response.output_text.delta', delta: PURE_LEAK },
|
|
587
|
+
{ type: 'response.completed', response: { id: 'r1', model: 'gpt', status: 'completed', output: [] } },
|
|
588
|
+
]),
|
|
589
|
+
});
|
|
590
|
+
assert.ok(oauthKinds.includes('tool'));
|
|
591
|
+
assert.equal(oauthKinds.includes('text'), false);
|
|
592
|
+
assert.deepEqual(oauthVisible, []);
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
test('OpenAI WS reports transport plus semantic/reasoning/text classifications', async () => {
|
|
596
|
+
const id = `liveness-ws-${Date.now()}`;
|
|
597
|
+
const kinds = [];
|
|
598
|
+
const socket = new FakeSocket();
|
|
599
|
+
markSessionAskStart(id);
|
|
600
|
+
const p = _streamResponse({
|
|
601
|
+
entry: { socket },
|
|
602
|
+
state: { sessionId: id },
|
|
603
|
+
onStreamDelta: (kind) => kinds.push(kind),
|
|
604
|
+
_timeouts: { interChunkMs: 5_000, preResponseCreatedMs: 5_000, firstMeaningfulMs: 5_000 },
|
|
605
|
+
});
|
|
606
|
+
socket.feed([
|
|
607
|
+
{ type: 'response.created', response: { id: 'r1', model: 'gpt' } },
|
|
608
|
+
{ type: 'response.reasoning_text.delta', delta: 'secret' },
|
|
609
|
+
{ type: 'response.output_text.delta', delta: 'hello' },
|
|
610
|
+
{ type: 'response.completed', response: { id: 'r1', model: 'gpt', output: [] } },
|
|
611
|
+
]);
|
|
612
|
+
await p;
|
|
613
|
+
assert.ok(_getRuntimeEntry(id)?.lastTransportAt);
|
|
614
|
+
assert.ok(kinds.includes('semantic'));
|
|
615
|
+
assert.ok(kinds.includes('reasoning'));
|
|
616
|
+
assert.ok(kinds.includes('text'));
|
|
617
|
+
_clearSessionRuntime(id);
|
|
618
|
+
});
|