mixdog 0.9.36 → 0.9.37
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 +3 -2
- package/scripts/abort-recovery-test.mjs +118 -0
- package/scripts/compact-smoke.mjs +7 -7
- package/scripts/explore-bench-tmp.mjs +19 -0
- package/scripts/explore-bench.mjs +36 -6
- package/scripts/explore-prompt-policy-test.mjs +27 -0
- package/scripts/ingest-pure-conversation-smoke.mjs +11 -11
- package/scripts/openai-ws-early-settle-test.mjs +176 -0
- package/scripts/output-style-smoke.mjs +17 -17
- package/scripts/provider-toolcall-test.mjs +333 -14
- package/scripts/recall-bench-cases.json +3 -3
- package/scripts/recall-bench.mjs +1 -1
- package/scripts/recall-quality-cases.json +4 -4
- package/scripts/recall-usecase-cases.json +2 -2
- package/scripts/session-bench.mjs +13 -13
- package/scripts/steering-drain-buckets-test.mjs +72 -11
- package/scripts/submit-commandbusy-race-test.mjs +114 -0
- package/scripts/tool-smoke.mjs +83 -10
- package/src/app.mjs +2 -1
- package/src/defaults/cycle3-review-prompt.md +9 -0
- package/src/defaults/skills/setup/SKILL.md +93 -293
- package/src/lib/rules-builder.cjs +3 -2
- package/src/output-styles/default.md +2 -2
- package/src/output-styles/extreme-minimal.md +1 -1
- package/src/output-styles/minimal.md +1 -1
- package/src/output-styles/simple.md +2 -3
- package/src/rules/agent/30-explorer.md +15 -7
- package/src/rules/lead/01-general.md +4 -0
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +15 -3
- package/src/runtime/agent/orchestrator/config.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +35 -5
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +27 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +36 -37
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +72 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +26 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +12 -28
- package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +18 -15
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +4 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +113 -2
- package/src/runtime/agent/orchestrator/providers/registry.mjs +44 -5
- package/src/runtime/agent/orchestrator/providers/tool-stream-state.mjs +78 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +57 -31
- package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +8 -6
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +28 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +1 -3
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +15 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +26 -17
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +24 -16
- package/src/runtime/agent/orchestrator/session/result-classification.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +5 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +18 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +35 -20
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +77 -31
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +25 -3
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +9 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +73 -25
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +17 -7
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +14 -12
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +56 -6
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -0
- package/src/runtime/channels/lib/worker-main.mjs +5 -0
- package/src/runtime/memory/lib/core-memory-store.mjs +104 -0
- package/src/runtime/memory/lib/ko-morph.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle3.mjs +64 -4
- package/src/runtime/memory/lib/memory-text-utils.mjs +4 -4
- package/src/runtime/memory/lib/recall-format.mjs +3 -3
- package/src/runtime/shared/child-spawn-gate.mjs +15 -0
- package/src/runtime/shared/config.mjs +98 -12
- package/src/runtime/shared/process-listener-headroom.mjs +12 -0
- package/src/session-runtime/cwd-plugins.mjs +6 -3
- package/src/session-runtime/model-route-api.mjs +50 -2
- package/src/session-runtime/provider-auth-api.mjs +8 -1
- package/src/session-runtime/resource-api.mjs +22 -0
- package/src/session-runtime/runtime-core.mjs +13 -2
- package/src/session-runtime/settings-api.mjs +11 -2
- package/src/standalone/agent-tool.mjs +15 -9
- package/src/standalone/explore-tool.mjs +4 -1
- package/src/tui/App.jsx +6 -5
- package/src/tui/app/transcript-window.mjs +60 -10
- package/src/tui/app/use-transcript-window.mjs +2 -1
- package/src/tui/components/ContextPanel.jsx +4 -1
- package/src/tui/components/ToolExecution.jsx +15 -12
- package/src/tui/components/TranscriptItem.jsx +1 -1
- package/src/tui/components/tool-execution/surface-detail.mjs +8 -8
- package/src/tui/dist/index.mjs +467 -151
- package/src/tui/engine/agent-job-feed.mjs +26 -6
- package/src/tui/engine/notification-plan.mjs +3 -3
- package/src/tui/engine/render-timing.mjs +104 -8
- package/src/tui/engine/session-api.mjs +58 -3
- package/src/tui/engine/session-flow.mjs +78 -28
- package/src/tui/engine/tool-card-results.mjs +28 -13
- package/src/tui/engine/turn.mjs +232 -156
- package/src/tui/engine.mjs +17 -8
- package/src/tui/index.jsx +10 -1
- package/src/workflows/default/WORKFLOW.md +8 -4
- package/src/workflows/solo/WORKFLOW.md +8 -4
|
@@ -13,7 +13,7 @@ function assert(condition, message) {
|
|
|
13
13
|
if (!condition) throw new Error(message);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
const DEFAULT_REPORT_LABELS = new Set(['
|
|
16
|
+
const DEFAULT_REPORT_LABELS = new Set(['\uBC14\uB010 \uC810', '\uD655\uC778\uD55C \uAC83', '\uB0A8\uC740 \uB9AC\uC2A4\uD06C/\uB2E4\uC74C \uB2E8\uACC4', '\uB2E4\uC74C \uB2E8\uACC4']);
|
|
17
17
|
|
|
18
18
|
function assertCleanOutput(name, value, { maxLines = 3, maxBullets = 3, allowedLabels = new Set() } = {}) {
|
|
19
19
|
const text = String(value || '').trim();
|
|
@@ -28,7 +28,7 @@ function assertCleanOutput(name, value, { maxLines = 3, maxBullets = 3, allowedL
|
|
|
28
28
|
}
|
|
29
29
|
assert(!/\b(?:tool trace|session metadata|model metadata|searched-path list)\b/i.test(text), `${name}: tool/meta trace found`);
|
|
30
30
|
assert(!/\b(?:Mapped|Searched|Read|Called) for \d/i.test(text), `${name}: timing/tool status found`);
|
|
31
|
-
assert(!/^(
|
|
31
|
+
assert(!/^(?:\uB124|\uC608|\uB9DE\uC544\uC694|\uC88B\uC544\uC694|\uC54C\uACA0\uC2B5\uB2C8\uB2E4|Sure|Okay|Understood)[,.\s]/i.test(text), `${name}: acknowledgment preface found`);
|
|
32
32
|
assert(!/(.+)(?:\n|\s{2,})\1/.test(text), `${name}: repeated conclusion found`);
|
|
33
33
|
|
|
34
34
|
const lines = text.split(/\r?\n/).filter((line) => line.trim());
|
|
@@ -41,7 +41,7 @@ function assertCleanOutput(name, value, { maxLines = 3, maxBullets = 3, allowedL
|
|
|
41
41
|
assert(lines.length <= maxLines, `${name}: too many lines`);
|
|
42
42
|
|
|
43
43
|
const withoutCode = text.replace(/`[^`]*`/g, '');
|
|
44
|
-
const sentenceMarks = withoutCode.match(/[.!?。!?]
|
|
44
|
+
const sentenceMarks = withoutCode.match(/[.!?。!?]|\uB2E4\.|\uC694\./g) || [];
|
|
45
45
|
assert(sentenceMarks.length <= 2, `${name}: too many sentences`);
|
|
46
46
|
}
|
|
47
47
|
|
|
@@ -52,7 +52,7 @@ for (const required of [
|
|
|
52
52
|
'Mixdog default — the most detailed style',
|
|
53
53
|
'State conclusions, not reasoning',
|
|
54
54
|
'Use labels such as',
|
|
55
|
-
'
|
|
55
|
+
'`\uBC14\uB010 \uC810`, `\uD655\uC778\uD55C \uAC83`,',
|
|
56
56
|
'Synthesize agent or retrieval results',
|
|
57
57
|
'Do not hide blockers',
|
|
58
58
|
]) {
|
|
@@ -97,7 +97,7 @@ try {
|
|
|
97
97
|
|
|
98
98
|
mkdirSync(join(dataDir, 'output-styles'), { recursive: true });
|
|
99
99
|
writeFileSync(join(dataDir, 'mixdog-config.json'), JSON.stringify({
|
|
100
|
-
agent: { profile: { title: '
|
|
100
|
+
agent: { profile: { title: '\uD64D\uAE38\uB3D9\uB2D8', language: 'system' } },
|
|
101
101
|
outputStyle: 'custom-smoke',
|
|
102
102
|
}));
|
|
103
103
|
writeFileSync(join(dataDir, 'output-styles', 'custom-smoke.md'), '---\nname: custom-smoke\n---\n\n# Custom Output Style\n\ncustom smoke style\n');
|
|
@@ -105,7 +105,7 @@ try {
|
|
|
105
105
|
assert(customRules.includes('# Custom Output Style'), 'configured outputStyle must select custom style');
|
|
106
106
|
assert(!customRules.includes('Mixdog default — the most detailed of the three styles'), 'custom outputStyle should not append default style');
|
|
107
107
|
const profileMeta = rulesBuilder.buildLeadMetaContent({ PLUGIN_ROOT: join(root, 'src'), DATA_DIR: dataDir });
|
|
108
|
-
assert(profileMeta.includes('Use "
|
|
108
|
+
assert(profileMeta.includes('Use "\uD64D\uAE38\uB3D9\uB2D8" when directly addressing the user'), 'profile title must inject into Lead BP3 meta');
|
|
109
109
|
assert(profileMeta.includes('do not repeat it in routine progress updates or pre-tool preambles'), 'profile title must not encourage title in preambles');
|
|
110
110
|
assert(/Default user-facing response language from system locale/.test(profileMeta), 'system profile language must resolve from system locale');
|
|
111
111
|
assert(profileMeta.includes('pre-tool preambles (even single-line)'), 'profile language must cover pre-tool preambles');
|
|
@@ -115,26 +115,26 @@ try {
|
|
|
115
115
|
|
|
116
116
|
// assertCleanOutput encodes the Simple style compact contract (not Default, which may be longer).
|
|
117
117
|
const goodOutputs = {
|
|
118
|
-
explanation: 'Simple
|
|
119
|
-
implementation: '-
|
|
120
|
-
crowded: '-
|
|
121
|
-
blocker: '`scripts/output-style-smoke.mjs
|
|
122
|
-
semicolon: '
|
|
118
|
+
explanation: 'Simple \uC2A4\uD0C0\uC77C\uC740 \uACB0\uACFC \uD55C \uC904\uACFC \uADFC\uAC70 \uD55C \uAC00\uC9C0\uB85C \uB05D\uB0B4\uACE0, \uCD5C\uC885 handoff\uB9CC \uC9E7\uC740 bullet \uB77C\uBCA8\uC744 \uC501\uB2C8\uB2E4.',
|
|
119
|
+
implementation: '- \uBC14\uB010 \uC810: `scripts/output-style-smoke.mjs`\uC758 default/simple \uBB38\uC790\uC5F4 \uAC80\uC99D\uC744 \uD604\uC7AC \uD504\uB9AC\uC14B\uC5D0 \uB9DE\uCDC4\uC2B5\uB2C8\uB2E4.\n- \uD655\uC778\uD55C \uAC83: `node scripts/output-style-smoke.mjs`\uB97C \uC2E4\uD589\uD588\uC2B5\uB2C8\uB2E4.',
|
|
120
|
+
crowded: '- \uBC14\uB010 \uC810: compact guardrail\uC740 Simple handoff\uC6A9 controlled detail\uC744 \uAC80\uC99D\uD569\uB2C8\uB2E4.\n- \uD655\uC778\uD55C \uAC83: bad \uC0D8\uD50C \uAC70\uBD80 \uADDC\uCE59\uC740 \uADF8\uB300\uB85C\uC785\uB2C8\uB2E4.',
|
|
121
|
+
blocker: '`scripts/output-style-smoke.mjs`\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC5B4 \uBCC0\uACBD\uC774 \uB9C9\uD614\uC2B5\uB2C8\uB2E4.',
|
|
122
|
+
semicolon: '\uC2A4\uBAA8\uD06C \uC2A4\uD06C\uB9BD\uD2B8\uB97C \uAC31\uC2E0\uD588\uACE0; Simple \uACC4\uC57D\uC5D0 \uB9DE\uB294 \uC608\uC2DC \uC751\uB2F5\uB9CC \uD1B5\uACFC\uD569\uB2C8\uB2E4.',
|
|
123
123
|
};
|
|
124
124
|
for (const [name, output] of Object.entries(goodOutputs)) assertCleanOutput(name, output, { allowedLabels: DEFAULT_REPORT_LABELS });
|
|
125
125
|
|
|
126
126
|
const badOutputs = {
|
|
127
127
|
heading: '## Summary\nDone.',
|
|
128
128
|
label: 'Changed: updated default style.',
|
|
129
|
-
koreanLabel: '
|
|
129
|
+
koreanLabel: '\uBCC0\uACBD: default \uC2A4\uD0C0\uC77C\uC744 \uC218\uC815\uD588\uC2B5\uB2C8\uB2E4.\n\uAC80\uC99D: \uC2A4\uBAA8\uD06C\uB97C \uD1B5\uACFC\uD588\uC2B5\uB2C8\uB2E4.',
|
|
130
130
|
numbered: '1. Updated style\n2. Ran smoke',
|
|
131
131
|
paragraphs: 'Done.\n\nDone again.',
|
|
132
132
|
nested: '- Updated\n - Nested detail',
|
|
133
|
-
ack: '
|
|
134
|
-
timing: 'Mapped for 2m 32s\n\ndefault
|
|
135
|
-
tooManySentences: '
|
|
136
|
-
mixed: '
|
|
137
|
-
tooManyBullets: '-
|
|
133
|
+
ack: '\uB124, default \uC2A4\uD0C0\uC77C\uC744 \uC218\uC815\uD588\uC2B5\uB2C8\uB2E4.',
|
|
134
|
+
timing: 'Mapped for 2m 32s\n\ndefault \uC2A4\uD0C0\uC77C\uC744 \uC218\uC815\uD588\uC2B5\uB2C8\uB2E4.',
|
|
135
|
+
tooManySentences: '\uC218\uC815\uD588\uC2B5\uB2C8\uB2E4. \uAC80\uC99D\uD588\uC2B5\uB2C8\uB2E4. \uBCF4\uACE0\uD588\uC2B5\uB2C8\uB2E4.',
|
|
136
|
+
mixed: '\uC218\uC815\uD588\uC2B5\uB2C8\uB2E4.\n- \uAC80\uC99D\uD588\uC2B5\uB2C8\uB2E4.',
|
|
137
|
+
tooManyBullets: '- \uBC14\uB010 \uC810: A\n- \uD655\uC778\uD55C \uAC83: B\n- \uB2E4\uC74C \uB2E8\uACC4: C\n- \uB0A8\uC740 \uB9AC\uC2A4\uD06C/\uB2E4\uC74C \uB2E8\uACC4: D',
|
|
138
138
|
};
|
|
139
139
|
for (const [name, output] of Object.entries(badOutputs)) {
|
|
140
140
|
let failed = false;
|
|
@@ -62,6 +62,8 @@ import {
|
|
|
62
62
|
} from '../src/runtime/agent/orchestrator/providers/anthropic-effort.mjs';
|
|
63
63
|
import { buildAnthropicBetaHeaders } from '../src/runtime/agent/orchestrator/providers/anthropic-betas.mjs';
|
|
64
64
|
import { PATCH_TOOL_DEFS } from '../src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs';
|
|
65
|
+
import { sendViaHttpSse } from '../src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs';
|
|
66
|
+
import { buildRequestBody as buildOpenAIOAuthRequestBody } from '../src/runtime/agent/orchestrator/providers/openai-oauth.mjs';
|
|
65
67
|
|
|
66
68
|
// --- Helpers ---------------------------------------------------------------
|
|
67
69
|
|
|
@@ -100,6 +102,33 @@ function compatResponsesEventStream(events) {
|
|
|
100
102
|
};
|
|
101
103
|
}
|
|
102
104
|
|
|
105
|
+
// Minimal 200-OK Response-like shape for the HTTP/SSE Responses path: frames
|
|
106
|
+
// each event as `event:<type>\ndata:<json>\n\n`, delivered synchronously so the
|
|
107
|
+
// semantic-idle watchdog never arms during the test.
|
|
108
|
+
function httpSseResponse(events) {
|
|
109
|
+
const encoder = new TextEncoder();
|
|
110
|
+
const chunks = events.map((e) => encoder.encode(`event: ${e.type}\ndata: ${JSON.stringify(e)}\n\n`));
|
|
111
|
+
let i = 0;
|
|
112
|
+
return {
|
|
113
|
+
status: 200,
|
|
114
|
+
ok: true,
|
|
115
|
+
headers: new Map(),
|
|
116
|
+
body: {
|
|
117
|
+
getReader() {
|
|
118
|
+
return {
|
|
119
|
+
read() {
|
|
120
|
+
return i < chunks.length
|
|
121
|
+
? Promise.resolve({ done: false, value: chunks[i++] })
|
|
122
|
+
: Promise.resolve({ done: true, value: undefined });
|
|
123
|
+
},
|
|
124
|
+
cancel() { return Promise.resolve(); },
|
|
125
|
+
releaseLock() {},
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
103
132
|
// === 1. openai-compat ======================================================
|
|
104
133
|
// Chat path: parseToolCalls(choice, label) openai-compat.mjs:957
|
|
105
134
|
// Responses path: parseResponsesToolCalls(response,...) openai-compat.mjs:972
|
|
@@ -220,6 +249,126 @@ test('openai-compat/xai Responses stream: response.completed salvages deferred f
|
|
|
220
249
|
assert.deepEqual(captured, [{ id: 'fc_1', name: 'read', arguments: { path: 'a' } }]);
|
|
221
250
|
});
|
|
222
251
|
|
|
252
|
+
// Missing-terminal + partial-tool gate (shared tool-stream-state.mjs tracker):
|
|
253
|
+
// a Responses stream that streams a CUSTOM tool call's input then truncates
|
|
254
|
+
// before response.output_item.done / response.completed. The call never lands
|
|
255
|
+
// in pendingCalls or toolCalls, so the truncation error must be gated as
|
|
256
|
+
// pendingToolUse via the shared active tool-item tracker (activeToolItems),
|
|
257
|
+
// NOT accepted as a text-only partial-final that would drop the in-flight tool.
|
|
258
|
+
test('openai-compat/xai Responses stream: mid custom-tool-input truncation is gated pendingToolUse via shared tracker', async () => {
|
|
259
|
+
const rejected = await consumeCompatResponsesStream(compatResponsesEventStream([
|
|
260
|
+
{ type: 'response.created', response: { id: 'resp_ct', model: 'grok' } },
|
|
261
|
+
{ type: 'response.output_text.delta', delta: 'partial ' },
|
|
262
|
+
{ type: 'response.output_item.added', item: { type: 'custom_tool_call', id: 'ct_item_1' } },
|
|
263
|
+
{ type: 'response.custom_tool_call_input.delta', item_id: 'ct_item_1', delta: '{"x":' },
|
|
264
|
+
// stream truncates here: no output_item.done, no response.completed.
|
|
265
|
+
]), {
|
|
266
|
+
label: 'test',
|
|
267
|
+
parseResponsesToolCalls: compatParseResponsesToolCalls,
|
|
268
|
+
responseOutputText: () => '',
|
|
269
|
+
onTextDelta: () => {},
|
|
270
|
+
}).then(() => null, (e) => e);
|
|
271
|
+
assert.ok(rejected, 'expected the truncated stream to reject');
|
|
272
|
+
assert.equal(rejected.streamStalled, true);
|
|
273
|
+
// The in-flight custom tool must gate partial-final success even though it
|
|
274
|
+
// never reached pendingCalls/toolCalls — the active tracker carries it.
|
|
275
|
+
assert.equal(rejected.pendingToolUse, true);
|
|
276
|
+
assert.equal(rejected.partialContent, 'partial ');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// Precision half of the same tracker: once the tool call fully COMPLETES
|
|
280
|
+
// (output_item.done clears the active item) and text keeps streaming, a later
|
|
281
|
+
// truncation is a plain text-only partial — the tracker having been cleared is
|
|
282
|
+
// what lets pendingToolUse fall back to the real emit/pending state.
|
|
283
|
+
test('openai-compat/xai Responses stream: completed tool then trailing-text truncation clears active tracker', async () => {
|
|
284
|
+
let emitted = 0;
|
|
285
|
+
const rejected = await consumeCompatResponsesStream(compatResponsesEventStream([
|
|
286
|
+
{ type: 'response.created', response: { id: 'resp_done', model: 'grok' } },
|
|
287
|
+
{ type: 'response.output_item.added', item: { type: 'custom_tool_call', id: 'ct_done_1' } },
|
|
288
|
+
{ type: 'response.custom_tool_call_input.delta', item_id: 'ct_done_1', delta: '{"x":1}' },
|
|
289
|
+
{ type: 'response.output_item.done', item: { type: 'custom_tool_call', id: 'ct_done_1', name: 'load_tool', input: '{"x":1}' } },
|
|
290
|
+
{ type: 'response.output_text.delta', delta: 'after tool ' },
|
|
291
|
+
// truncates before response.completed.
|
|
292
|
+
]), {
|
|
293
|
+
label: 'test',
|
|
294
|
+
parseResponsesToolCalls: compatParseResponsesToolCalls,
|
|
295
|
+
responseOutputText: () => '',
|
|
296
|
+
onTextDelta: () => {},
|
|
297
|
+
onToolCall: () => { emitted += 1; },
|
|
298
|
+
}).then(() => null, (e) => e);
|
|
299
|
+
assert.ok(rejected, 'expected the truncated stream to reject');
|
|
300
|
+
assert.equal(rejected.streamStalled, true);
|
|
301
|
+
// A tool WAS emitted this turn, so the turn is still unsafe/tool-bearing —
|
|
302
|
+
// pendingToolUse stays true off the emit state, not a stale active latch.
|
|
303
|
+
assert.ok(emitted >= 1);
|
|
304
|
+
assert.equal(rejected.pendingToolUse, true);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Reviewer fix: function output_item.done must delete the pendingCalls itemId
|
|
308
|
+
// before recomputing toolInFlight — otherwise a fully-completed function call
|
|
309
|
+
// keeps pendingCalls.size > 0 forever, and a later max-output cutoff on trailing
|
|
310
|
+
// text is misclassified as a truncated tool-in-flight stall instead of a clean
|
|
311
|
+
// length completion.
|
|
312
|
+
test('openai-compat/xai Responses stream: completed function call clears pendingCalls so max-output cutoff is clean length', async () => {
|
|
313
|
+
const out = await consumeCompatResponsesStream(compatResponsesEventStream([
|
|
314
|
+
{ type: 'response.created', response: { id: 'resp_len', model: 'grok' } },
|
|
315
|
+
{ type: 'response.output_item.added', item: { type: 'function_call', id: 'fi_len', name: 'read', call_id: 'fc_len' } },
|
|
316
|
+
{ type: 'response.function_call_arguments.done', item_id: 'fi_len', arguments: '{"path":"a"}' },
|
|
317
|
+
{ type: 'response.output_item.done', item: { type: 'function_call', id: 'fi_len', call_id: 'fc_len', name: 'read', arguments: '{"path":"a"}' } },
|
|
318
|
+
{ type: 'response.incomplete', response: { incomplete_details: { reason: 'max_output_tokens' } } },
|
|
319
|
+
]), {
|
|
320
|
+
label: 'test',
|
|
321
|
+
parseResponsesToolCalls: compatParseResponsesToolCalls,
|
|
322
|
+
responseOutputText: () => '',
|
|
323
|
+
});
|
|
324
|
+
// The completed call drained pendingCalls + the active tracker, so the
|
|
325
|
+
// max-output cutoff is a clean length truncation (no in-flight-tool stall).
|
|
326
|
+
assert.equal(out.stopReason, 'length');
|
|
327
|
+
assert.deepEqual(out.toolCalls, [{ id: 'fc_len', name: 'read', arguments: { path: 'a' } }]);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// Reviewer fix (HTTP/SSE): a max_output_tokens cutoff while a function call is
|
|
331
|
+
// still in flight (added + partial args, no output_item.done) must NOT mark a
|
|
332
|
+
// clean length completion — the tool arguments were truncated. Mirror compat:
|
|
333
|
+
// throw a stream-stalled pendingToolUse error so the loop gates/retries.
|
|
334
|
+
test('openai-oauth HTTP/SSE Responses: max-output cutoff with function call in flight → stream-stalled pendingToolUse', async () => {
|
|
335
|
+
const rejected = await sendViaHttpSse({
|
|
336
|
+
auth: { type: 'openai-direct', apiKey: 'k' },
|
|
337
|
+
body: { model: 'gpt', tools: [] },
|
|
338
|
+
useModel: 'gpt',
|
|
339
|
+
fetchFn: async () => httpSseResponse([
|
|
340
|
+
{ type: 'response.created', response: { id: 'r', model: 'gpt' } },
|
|
341
|
+
{ type: 'response.output_item.added', item: { type: 'function_call', id: 'fi_1', name: 'read', call_id: 'fc_1' } },
|
|
342
|
+
{ type: 'response.function_call_arguments.delta', item_id: 'fi_1', delta: '{"path":' },
|
|
343
|
+
{ type: 'response.incomplete', response: { incomplete_details: { reason: 'max_output_tokens' } } },
|
|
344
|
+
]),
|
|
345
|
+
}).then(() => null, (e) => e);
|
|
346
|
+
assert.ok(rejected, 'expected the truncated-tool cutoff to reject');
|
|
347
|
+
assert.equal(rejected.streamStalled, true);
|
|
348
|
+
assert.equal(rejected.pendingToolUse, true);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// Reviewer fix (HTTP/SSE): function output_item.done must delete pendingCalls[id]
|
|
352
|
+
// before recomputing _toolInFlight — otherwise the completed call keeps
|
|
353
|
+
// pendingCalls.size > 0 and a later max-output cutoff is misread as a truncated
|
|
354
|
+
// tool. A fully-completed call before the cutoff must be a clean length result.
|
|
355
|
+
test('openai-oauth HTTP/SSE Responses: completed function call clears pendingCalls so max-output cutoff is clean length', async () => {
|
|
356
|
+
const out = await sendViaHttpSse({
|
|
357
|
+
auth: { type: 'openai-direct', apiKey: 'k' },
|
|
358
|
+
body: { model: 'gpt', tools: [] },
|
|
359
|
+
useModel: 'gpt',
|
|
360
|
+
fetchFn: async () => httpSseResponse([
|
|
361
|
+
{ type: 'response.created', response: { id: 'r', model: 'gpt' } },
|
|
362
|
+
{ type: 'response.output_item.added', item: { type: 'function_call', id: 'fi_1', name: 'read', call_id: 'fc_1' } },
|
|
363
|
+
{ type: 'response.function_call_arguments.done', item_id: 'fi_1', arguments: '{"path":"a"}' },
|
|
364
|
+
{ type: 'response.output_item.done', item: { type: 'function_call', id: 'fi_1', call_id: 'fc_1', name: 'read', arguments: '{"path":"a"}' } },
|
|
365
|
+
{ type: 'response.incomplete', response: { incomplete_details: { reason: 'max_output_tokens' } } },
|
|
366
|
+
]),
|
|
367
|
+
});
|
|
368
|
+
assert.equal(out.stopReason, 'length');
|
|
369
|
+
assert.deepEqual(out.toolCalls, [{ id: 'fc_1', name: 'read', arguments: { path: 'a' } }]);
|
|
370
|
+
});
|
|
371
|
+
|
|
223
372
|
test('openai-compat/xai Responses: freeform apply_patch downgrades to function schema', () => {
|
|
224
373
|
const tools = _toResponsesToolsForTest(PATCH_TOOL_DEFS);
|
|
225
374
|
const patch = tools.find((tool) => tool.name === 'apply_patch');
|
|
@@ -229,6 +378,135 @@ test('openai-compat/xai Responses: freeform apply_patch downgrades to function s
|
|
|
229
378
|
assert.deepEqual(patch.parameters?.required, ['patch']);
|
|
230
379
|
});
|
|
231
380
|
|
|
381
|
+
test('openai-compat/xai Responses: load_tool downgrades from tool_search to function schema', () => {
|
|
382
|
+
const loadTool = {
|
|
383
|
+
name: 'load_tool',
|
|
384
|
+
description: 'load tools',
|
|
385
|
+
inputSchema: {
|
|
386
|
+
type: 'object',
|
|
387
|
+
properties: { names: { type: 'array', items: { type: 'string' } } },
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
const [xaiTool] = _toResponsesToolsForTest([loadTool], { provider: 'xai' });
|
|
391
|
+
assert.equal(xaiTool.type, 'function');
|
|
392
|
+
assert.equal(xaiTool.name, 'load_tool');
|
|
393
|
+
assert.equal(xaiTool.execution, undefined);
|
|
394
|
+
assert.equal(xaiTool.parameters, loadTool.inputSchema);
|
|
395
|
+
|
|
396
|
+
const [openaiTool] = _toResponsesToolsForTest([loadTool], { provider: 'openai' });
|
|
397
|
+
assert.equal(openaiTool.type, 'tool_search');
|
|
398
|
+
assert.equal(openaiTool.execution, 'client');
|
|
399
|
+
assert.equal(openaiTool.name, undefined);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
test('openai-compat/xai Responses: load_tool history replays as function_call', () => {
|
|
403
|
+
const { input } = _toXaiResponsesInputForTest([
|
|
404
|
+
{ role: 'user', content: 'load a tool' },
|
|
405
|
+
{
|
|
406
|
+
role: 'assistant',
|
|
407
|
+
content: '',
|
|
408
|
+
toolCalls: [{ id: 'call_load_1', name: 'load_tool', arguments: { names: ['read'] }, nativeType: 'tool_search_call' }],
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
role: 'tool',
|
|
412
|
+
toolCallId: 'call_load_1',
|
|
413
|
+
content: '{}',
|
|
414
|
+
nativeToolSearch: { openaiTools: [{ name: 'read' }] },
|
|
415
|
+
},
|
|
416
|
+
], {
|
|
417
|
+
xaiResponses: {
|
|
418
|
+
previousResponseId: 'resp_same',
|
|
419
|
+
seenMessageCount: 0,
|
|
420
|
+
model: 'grok-4.5',
|
|
421
|
+
},
|
|
422
|
+
}, { model: 'grok-4.5' });
|
|
423
|
+
assert.equal(input.some((item) => item.type === 'tool_search_call'), false);
|
|
424
|
+
assert.equal(input.some((item) => item.type === 'tool_search_output'), false);
|
|
425
|
+
const call = input.find((item) => item.type === 'function_call' && item.name === 'load_tool');
|
|
426
|
+
assert.equal(call.call_id, 'call_load_1');
|
|
427
|
+
assert.deepEqual(JSON.parse(call.arguments), { names: ['read'] });
|
|
428
|
+
assert.equal(input.some((item) => item.type === 'function_call_output' && item.call_id === 'call_load_1'), true);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test('openai-compat/xai Responses: model switch drops prior tool transcript history', () => {
|
|
432
|
+
const { input, previousResponseId, continuationResetReason } = _toXaiResponsesInputForTest([
|
|
433
|
+
{ role: 'system', content: 'sys' },
|
|
434
|
+
{ role: 'user', content: 'before switch' },
|
|
435
|
+
{
|
|
436
|
+
role: 'assistant',
|
|
437
|
+
content: '',
|
|
438
|
+
toolCalls: [{ id: 'call_load_1', name: 'load_tool', arguments: { names: ['read'] }, nativeType: 'tool_search_call' }],
|
|
439
|
+
},
|
|
440
|
+
{ role: 'tool', toolCallId: 'call_load_1', content: '{"loaded":["read"]}' },
|
|
441
|
+
{ role: 'user', content: 'after switch' },
|
|
442
|
+
], {
|
|
443
|
+
xaiResponses: {
|
|
444
|
+
previousResponseId: 'resp_old',
|
|
445
|
+
seenMessageCount: 4,
|
|
446
|
+
model: 'grok-4.20',
|
|
447
|
+
},
|
|
448
|
+
}, { model: 'grok-4.5' });
|
|
449
|
+
assert.equal(previousResponseId, null);
|
|
450
|
+
assert.equal(continuationResetReason, 'model_changed');
|
|
451
|
+
const serialized = JSON.stringify(input);
|
|
452
|
+
assert.equal(serialized.includes('tool_search'), false);
|
|
453
|
+
assert.equal(serialized.includes('function_call'), false);
|
|
454
|
+
assert.equal(serialized.includes('function_call_output'), false);
|
|
455
|
+
assert.deepEqual(input.map((item) => item.role), ['system', 'user', 'user']);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test('openai-compat/xai Responses: first Grok request after provider switch drops prior tool transcript history', () => {
|
|
459
|
+
const { input, previousResponseId, continuationResetReason } = _toXaiResponsesInputForTest([
|
|
460
|
+
{ role: 'system', content: 'sys' },
|
|
461
|
+
{ role: 'user', content: 'before switch' },
|
|
462
|
+
{
|
|
463
|
+
role: 'assistant',
|
|
464
|
+
content: '',
|
|
465
|
+
toolCalls: [{ id: 'call_patch_1', name: 'apply_patch', arguments: { patch: '*** Begin Patch\n*** End Patch\n' } }],
|
|
466
|
+
},
|
|
467
|
+
{ role: 'tool', toolCallId: 'call_patch_1', content: 'OK' },
|
|
468
|
+
{ role: 'user', content: 'after switch' },
|
|
469
|
+
], {}, { model: 'grok-4.5' });
|
|
470
|
+
assert.equal(previousResponseId, null);
|
|
471
|
+
assert.equal(continuationResetReason, null);
|
|
472
|
+
const serialized = JSON.stringify(input);
|
|
473
|
+
assert.equal(serialized.includes('function_call'), false);
|
|
474
|
+
assert.equal(serialized.includes('function_call_output'), false);
|
|
475
|
+
assert.deepEqual(input.map((item) => item.role), ['system', 'user', 'user']);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
test('openai-oauth Responses: load_tool schema and history stay function_call', () => {
|
|
479
|
+
const loadTool = {
|
|
480
|
+
name: 'load_tool',
|
|
481
|
+
description: 'load tools',
|
|
482
|
+
inputSchema: {
|
|
483
|
+
type: 'object',
|
|
484
|
+
properties: { names: { type: 'array', items: { type: 'string' } } },
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
const body = buildOpenAIOAuthRequestBody([
|
|
488
|
+
{ role: 'system', content: 'sys' },
|
|
489
|
+
{ role: 'user', content: 'load a tool' },
|
|
490
|
+
{
|
|
491
|
+
role: 'assistant',
|
|
492
|
+
content: '',
|
|
493
|
+
toolCalls: [{ id: 'call_load_1', name: 'tool_search', arguments: { names: ['read'] }, nativeType: 'tool_search_call' }],
|
|
494
|
+
},
|
|
495
|
+
{ role: 'tool', toolCallId: 'call_load_1', content: '{}' },
|
|
496
|
+
], 'gpt-5.5', [loadTool], {});
|
|
497
|
+
assert.equal(JSON.stringify(body).includes('tool_search'), false);
|
|
498
|
+
assert.deepEqual(body.tools?.[0], {
|
|
499
|
+
type: 'function',
|
|
500
|
+
name: 'load_tool',
|
|
501
|
+
description: loadTool.description,
|
|
502
|
+
parameters: loadTool.inputSchema,
|
|
503
|
+
});
|
|
504
|
+
const call = body.input.find((item) => item.type === 'function_call' && item.name === 'load_tool');
|
|
505
|
+
assert.equal(call.call_id, 'call_load_1');
|
|
506
|
+
assert.deepEqual(JSON.parse(call.arguments), { names: ['read'] });
|
|
507
|
+
assert.equal(body.input.some((item) => item.type === 'function_call_output' && item.call_id === 'call_load_1'), true);
|
|
508
|
+
});
|
|
509
|
+
|
|
232
510
|
test('openai-compat/xai Responses: custom_tool_call history replays as function_call', () => {
|
|
233
511
|
const rawPatch = '*** Begin Patch\n*** Add File: xai-history.txt\n+ok\n*** End Patch\n';
|
|
234
512
|
const { input } = _toXaiResponsesInputForTest([
|
|
@@ -239,7 +517,13 @@ test('openai-compat/xai Responses: custom_tool_call history replays as function_
|
|
|
239
517
|
toolCalls: [{ id: 'call_patch_1', name: 'apply_patch', arguments: { patch: rawPatch }, nativeType: 'custom_tool_call' }],
|
|
240
518
|
},
|
|
241
519
|
{ role: 'tool', toolCallId: 'call_patch_1', content: 'OK' },
|
|
242
|
-
],
|
|
520
|
+
], {
|
|
521
|
+
xaiResponses: {
|
|
522
|
+
previousResponseId: 'resp_same',
|
|
523
|
+
seenMessageCount: 0,
|
|
524
|
+
model: 'grok-composer-2.5-fast',
|
|
525
|
+
},
|
|
526
|
+
}, { model: 'grok-composer-2.5-fast' });
|
|
243
527
|
assert.equal(input.some((item) => item.type === 'custom_tool_call'), false);
|
|
244
528
|
assert.equal(input.some((item) => item.type === 'custom_tool_call_output'), false);
|
|
245
529
|
const call = input.find((item) => item.type === 'function_call' && item.name === 'apply_patch');
|
|
@@ -1092,6 +1376,35 @@ test('anthropic effort: sonnet-4-6 uses output_config + effort beta, not thinkin
|
|
|
1092
1376
|
assert.ok(beta.includes(EFFORT_BETA_HEADER));
|
|
1093
1377
|
});
|
|
1094
1378
|
|
|
1379
|
+
test('anthropic-oauth: load_tool native references replay as ordinary tool_result after model switches', () => {
|
|
1380
|
+
const body = _buildRequestBodyForCacheSmoke(
|
|
1381
|
+
[
|
|
1382
|
+
{ role: 'user', content: 'load a tool' },
|
|
1383
|
+
{
|
|
1384
|
+
role: 'assistant',
|
|
1385
|
+
content: '',
|
|
1386
|
+
toolCalls: [{ id: 'toolu_load_1', name: 'load_tool', arguments: { names: ['read'] } }],
|
|
1387
|
+
},
|
|
1388
|
+
{
|
|
1389
|
+
role: 'tool',
|
|
1390
|
+
toolCallId: 'toolu_load_1',
|
|
1391
|
+
content: '{"loaded":["read"]}',
|
|
1392
|
+
nativeToolSearch: { toolReferences: ['read'] },
|
|
1393
|
+
},
|
|
1394
|
+
],
|
|
1395
|
+
'claude-opus-4-8',
|
|
1396
|
+
[],
|
|
1397
|
+
{ effort: 'high' },
|
|
1398
|
+
);
|
|
1399
|
+
const serialized = JSON.stringify(body.messages);
|
|
1400
|
+
assert.equal(serialized.includes('tool_reference'), false);
|
|
1401
|
+
const toolResult = body.messages
|
|
1402
|
+
.flatMap((message) => Array.isArray(message.content) ? message.content : [])
|
|
1403
|
+
.find((block) => block?.type === 'tool_result' && block.tool_use_id === 'toolu_load_1');
|
|
1404
|
+
assert.ok(toolResult);
|
|
1405
|
+
assert.equal(toolResult.content, '{"loaded":["read"]}');
|
|
1406
|
+
});
|
|
1407
|
+
|
|
1095
1408
|
test('anthropic effort: legacy sonnet-4-5 maps effort to thinking budget', () => {
|
|
1096
1409
|
const model = 'claude-sonnet-4-5-20250514';
|
|
1097
1410
|
const body = _buildRequestBodyForCacheSmoke(
|
|
@@ -1270,7 +1583,7 @@ test('canonical frame: full-frame builder leads with type and preserves codex bo
|
|
|
1270
1583
|
assert.equal(frame.previous_response_id, undefined);
|
|
1271
1584
|
});
|
|
1272
1585
|
|
|
1273
|
-
test('canonical frame: delta insert keeps key order, drops
|
|
1586
|
+
test('canonical frame: delta insert keeps key order, drops chained instructions, overrides input', () => {
|
|
1274
1587
|
const body = {
|
|
1275
1588
|
model: 'gpt-5.5',
|
|
1276
1589
|
instructions: 'sys',
|
|
@@ -1279,10 +1592,11 @@ test('canonical frame: delta insert keeps key order, drops empty instructions, o
|
|
|
1279
1592
|
text: { verbosity: 'low' },
|
|
1280
1593
|
};
|
|
1281
1594
|
const delta = _buildResponseCreateFrame(body, { previousResponseId: 'resp_prev', inputOverride: [{ b: 2 }] });
|
|
1282
|
-
assert.deepEqual(Object.keys(delta), ['type', 'model', '
|
|
1595
|
+
assert.deepEqual(Object.keys(delta), ['type', 'model', 'previous_response_id', 'input', 'tool_choice', 'text']);
|
|
1596
|
+
assert.equal(delta.instructions, undefined);
|
|
1283
1597
|
assert.equal(delta.previous_response_id, 'resp_prev');
|
|
1284
1598
|
assert.deepEqual(delta.input, [{ b: 2 }]);
|
|
1285
|
-
// Empty instructions is dropped in delta mode (server resolves via prev id).
|
|
1599
|
+
// Empty instructions is also dropped in delta mode (server resolves via prev id).
|
|
1286
1600
|
const noInstr = _buildResponseCreateFrame({ ...body, instructions: '' }, { previousResponseId: 'resp_prev', inputOverride: [] });
|
|
1287
1601
|
assert.deepEqual(Object.keys(noInstr), ['type', 'model', 'previous_response_id', 'input', 'tool_choice', 'text']);
|
|
1288
1602
|
});
|
|
@@ -1328,11 +1642,11 @@ import {
|
|
|
1328
1642
|
FULL_RESPONSES_TRANSPORT_CAPS,
|
|
1329
1643
|
} from '../src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs';
|
|
1330
1644
|
|
|
1331
|
-
test('transport policy: default (no env) is
|
|
1645
|
+
test('transport policy: default (no env) is auto WS-first / refs continuation ON / HTTP fallback ON', () => {
|
|
1332
1646
|
const p = resolveOpenAiTransportPolicy({});
|
|
1333
|
-
assert.equal(p.mode, '
|
|
1647
|
+
assert.equal(p.mode, 'auto');
|
|
1334
1648
|
assert.equal(p.transport, 'ws');
|
|
1335
|
-
assert.equal(p.allowHttpFallback,
|
|
1649
|
+
assert.equal(p.allowHttpFallback, true);
|
|
1336
1650
|
assert.deepEqual(p.delta, { force: false, refs: true, optIn: true });
|
|
1337
1651
|
});
|
|
1338
1652
|
|
|
@@ -1364,10 +1678,11 @@ test('transport policy: http-sse forces the HTTP transport with delta OFF', () =
|
|
|
1364
1678
|
assert.equal(p.delta.optIn, false);
|
|
1365
1679
|
});
|
|
1366
1680
|
|
|
1367
|
-
test('transport policy: unknown MIXDOG_OAI_TRANSPORT falls back to default
|
|
1681
|
+
test('transport policy: unknown MIXDOG_OAI_TRANSPORT falls back to default auto', () => {
|
|
1368
1682
|
const p = resolveOpenAiTransportPolicy({ MIXDOG_OAI_TRANSPORT: 'quantum' });
|
|
1369
|
-
assert.equal(p.mode, '
|
|
1683
|
+
assert.equal(p.mode, 'auto');
|
|
1370
1684
|
assert.equal(p.transport, 'ws');
|
|
1685
|
+
assert.equal(p.allowHttpFallback, true);
|
|
1371
1686
|
});
|
|
1372
1687
|
|
|
1373
1688
|
test('response frame builder can omit transport-only stream/background for codex warmup parity', () => {
|
|
@@ -1383,7 +1698,7 @@ test('transport policy: mode token aliases normalize', () => {
|
|
|
1383
1698
|
assert.equal(_normalizeTransportMode(' ws delta '), 'ws-delta');
|
|
1384
1699
|
assert.equal(_normalizeTransportMode('http/sse'), 'http-sse');
|
|
1385
1700
|
assert.equal(_normalizeTransportMode('sse'), 'http-sse');
|
|
1386
|
-
assert.equal(_normalizeTransportMode('auto'), '
|
|
1701
|
+
assert.equal(_normalizeTransportMode('auto'), 'auto');
|
|
1387
1702
|
assert.equal(_normalizeTransportMode('official'), null);
|
|
1388
1703
|
assert.equal(_normalizeTransportMode(''), null);
|
|
1389
1704
|
assert.equal(_normalizeTransportMode('bogus'), null);
|
|
@@ -1472,17 +1787,19 @@ test('responses transport policy: openai direct caps match oauth (both full)', (
|
|
|
1472
1787
|
assert.deepEqual(direct.delta, { force: false, refs: true, optIn: true });
|
|
1473
1788
|
});
|
|
1474
1789
|
|
|
1475
|
-
test('responses transport policy: xai auto
|
|
1790
|
+
test('responses transport policy: xai auto is WS-first with HTTP fallback', () => {
|
|
1476
1791
|
const p = resolveResponsesTransportPolicy({}, RESPONSES_TRANSPORT_CAPABILITIES.xai);
|
|
1477
|
-
assert.equal(p.mode, '
|
|
1792
|
+
assert.equal(p.mode, 'auto');
|
|
1478
1793
|
assert.equal(p.transport, 'ws');
|
|
1479
|
-
assert.equal(p.allowHttpFallback,
|
|
1794
|
+
assert.equal(p.allowHttpFallback, true);
|
|
1480
1795
|
assert.deepEqual(p.delta, { force: false, refs: true, optIn: true });
|
|
1481
1796
|
});
|
|
1482
1797
|
|
|
1483
|
-
test('responses transport policy: explicit auto is
|
|
1798
|
+
test('responses transport policy: explicit auto is WS-first with HTTP fallback', () => {
|
|
1484
1799
|
const p = resolveResponsesTransportPolicy({ MIXDOG_OAI_TRANSPORT: 'auto' }, RESPONSES_TRANSPORT_CAPABILITIES.xai);
|
|
1800
|
+
assert.equal(p.mode, 'auto');
|
|
1485
1801
|
assert.equal(p.transport, 'ws');
|
|
1802
|
+
assert.equal(p.allowHttpFallback, true);
|
|
1486
1803
|
assert.deepEqual(p.delta, { force: false, refs: true, optIn: true });
|
|
1487
1804
|
});
|
|
1488
1805
|
|
|
@@ -1570,11 +1887,13 @@ test('grok-oauth: all Grok models inherit global transport; explicit setting is
|
|
|
1570
1887
|
test('responses transport policy: _gateTransportMode down-shifts per capability', () => {
|
|
1571
1888
|
// delta unsupported → ws-delta collapses to ws-full; others pass through.
|
|
1572
1889
|
const noDelta = { ws: true, http: true, delta: false };
|
|
1890
|
+
assert.equal(_gateTransportMode('auto', noDelta), 'ws-full');
|
|
1573
1891
|
assert.equal(_gateTransportMode('ws-delta', noDelta), 'ws-full');
|
|
1574
1892
|
assert.equal(_gateTransportMode('ws-full', noDelta), 'ws-full');
|
|
1575
1893
|
assert.equal(_gateTransportMode('http-sse', noDelta), 'http-sse');
|
|
1576
1894
|
// WS unsupported → WS modes prefer HTTP.
|
|
1577
1895
|
const httpOnly = { ws: false, http: true, delta: false };
|
|
1896
|
+
assert.equal(_gateTransportMode('auto', httpOnly), 'http-sse');
|
|
1578
1897
|
assert.equal(_gateTransportMode('ws-full', httpOnly), 'http-sse');
|
|
1579
1898
|
assert.equal(_gateTransportMode('ws-delta', httpOnly), 'http-sse');
|
|
1580
1899
|
// HTTP unsupported → http-sse prefers full-frame WS.
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
{ "id": "period-24h", "label": "period window 24h", "args": { "period": "24h", "limit": 10 }, "expect": "browse" },
|
|
5
5
|
{ "id": "period-7d", "label": "period window 7d", "args": { "period": "7d", "limit": 10 }, "expect": "browse" },
|
|
6
6
|
{ "id": "scope-all", "label": "all-scope query", "args": { "query": "recall", "projectScope": "all", "limit": 10 }, "expect": "results" },
|
|
7
|
-
{ "id": "recent-dryrun-delta", "label": "recent-work topical: dryrun delta sum", "args": { "query": "
|
|
8
|
-
{ "id": "recent-remote-singleton", "label": "recent-work topical: remote singleton seat", "args": { "query": "remote
|
|
9
|
-
{ "id": "recency-today", "label": "query+period recency: today's work (24h)", "args": { "query": "
|
|
7
|
+
{ "id": "recent-dryrun-delta", "label": "recent-work topical: dryrun delta sum", "args": { "query": "\uB4DC\uB77C\uC774\uB7F0 \uB378\uD0C0 \uD569\uC0B0", "limit": 10 }, "expect": { "kind": "results", "topNContains": ["\uB4DC\uB77C\uC774\uB7F0"], "topN": 5 } },
|
|
8
|
+
{ "id": "recent-remote-singleton", "label": "recent-work topical: remote singleton seat", "args": { "query": "remote \uC2F1\uAE00\uD1A4 \uC88C\uC11D", "limit": 10 }, "expect": { "kind": "results", "topNContains": ["\uC2F1\uAE00\uD1A4"], "topN": 5 } },
|
|
9
|
+
{ "id": "recency-today", "label": "query+period recency: today's work (24h)", "args": { "query": "\uC624\uB298 \uC9C4\uD589\uD55C \uC791\uC5C5", "period": "24h", "limit": 10 }, "expect": { "kind": "results", "recencyOrdered": true } },
|
|
10
10
|
{ "id": "xlang-embedding-crash", "label": "cross-language: embedding worker crash (strict topN)", "args": { "query": "embedding worker crash", "limit": 10 }, "expect": { "kind": "results", "topNContains": ["embedding"], "topN": 3 } }
|
|
11
11
|
]
|
package/scripts/recall-bench.mjs
CHANGED
|
@@ -31,7 +31,7 @@ function hasFlag(name) { return process.argv.includes(`--${name}`); }
|
|
|
31
31
|
const WARN_LATENCY_MS = 3000;
|
|
32
32
|
|
|
33
33
|
const DEFAULT_CASES = [
|
|
34
|
-
{ id: 'kw-ko', label: 'keyword query (ko)', args: { query: '
|
|
34
|
+
{ id: 'kw-ko', label: 'keyword query (ko)', args: { query: '\uBA54\uBAA8\uB9AC \uC7AC\uD604' }, expect: 'results' },
|
|
35
35
|
{ id: 'kw-en', label: 'keyword query (en)', args: { query: 'memory recall pipeline' }, expect: 'results' },
|
|
36
36
|
{ id: 'short-1tok', label: 'short 1-token query', args: { query: 'recall' }, expect: 'results' },
|
|
37
37
|
{ id: 'short-2tok', label: 'short 2-token query', args: { query: 'cycle1 drain' }, expect: 'results' },
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
[
|
|
2
|
-
{ "id": "q-kr-recall-phrase", "label": "Korean distinctive phrase: persistent memory", "args": { "query": "
|
|
2
|
+
{ "id": "q-kr-recall-phrase", "label": "Korean distinctive phrase: persistent memory", "args": { "query": "\uC601\uC18D \uBA54\uBAA8\uB9AC" }, "expect": { "kind": "results", "topNContains": ["\uC601\uC18D \uBA54\uBAA8\uB9AC"], "topN": 5 } },
|
|
3
3
|
{ "id": "q-en-recall-pipeline", "label": "English distinctive phrase: recall fast-track builder", "args": { "query": "buildRecallFastTrackQuery" }, "expect": { "kind": "results", "topNContains": ["buildRecallFastTrackQuery"], "topN": 5 } },
|
|
4
4
|
{ "id": "q-cycle1-drain", "label": "cycle1 ECONNRESET topic", "args": { "query": "cycle1 ECONNRESET" }, "expect": { "kind": "results", "topNContains": ["cycle1", "ECONNRESET"], "topN": 5 } },
|
|
5
5
|
{ "id": "q-recall-general", "label": "bare recall keyword", "args": { "query": "recall" }, "expect": { "kind": "results", "topNContains": ["recall"], "topN": 5 } },
|
|
6
|
-
{ "id": "q-uc4-topic-kr", "label": "UC4 topic query with Korean qualifier", "args": { "query": "recall period last
|
|
7
|
-
{ "id": "q-recall-improve-kr", "label": "cache improvement topic", "args": { "query": "
|
|
8
|
-
{ "id": "q-fanout-recall-cache", "label": "fan-out array query cache+recall", "args": { "query": ["
|
|
6
|
+
{ "id": "q-uc4-topic-kr", "label": "UC4 topic query with Korean qualifier", "args": { "query": "recall period last \uAC1C\uC120", "limit": 8 }, "expect": { "kind": "results", "topNContains": ["\uAC1C\uC120"], "topN": 5 } },
|
|
7
|
+
{ "id": "q-recall-improve-kr", "label": "cache improvement topic", "args": { "query": "\uCE90\uC2DC \uAC1C\uC120" }, "expect": { "kind": "results", "topNContains": ["\uAC1C\uC120"], "topN": 5 } },
|
|
8
|
+
{ "id": "q-fanout-recall-cache", "label": "fan-out array query cache+recall", "args": { "query": ["\uCE90\uC2DC \uAC1C\uC120", "recall"], "limit": 5 }, "expect": { "kind": "results", "topNContains": ["\uAC1C\uC120", "recall"], "topN": 10 } },
|
|
9
9
|
{ "id": "q-short-2tok-cycle", "label": "short 2-token cycle1 query", "args": { "query": "cycle1 drain", "limit": 8 }, "expect": { "kind": "results", "topNContains": ["cycle1"], "topN": 3 } },
|
|
10
10
|
{ "id": "q-recall-scope-project", "label": "project-scoped recall query", "args": { "query": "recall", "cwd": "C:\\Project\\mixdog" , "limit": 10 }, "expect": { "kind": "results", "topNContains": ["recall"], "topN": 5 } }
|
|
11
11
|
]
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
{ "id": "uc3-tod-dated", "label": "UC3 dated time window 2026-07-03 09:00~12:00", "args": { "period": "2026-07-03 09:00~12:00", "limit": 10 }, "expect": "browse" },
|
|
9
9
|
{ "id": "uc3-date", "label": "UC3 specific date 2026-07-02", "args": { "period": "2026-07-02", "limit": 10 }, "expect": "browse" },
|
|
10
10
|
{ "id": "uc3-range", "label": "UC3 date range 2026-07-01~2026-07-03", "args": { "period": "2026-07-01~2026-07-03", "limit": 10 }, "expect": "browse" },
|
|
11
|
-
{ "id": "uc4-topic", "label": "UC4 topic query", "args": { "query": "recall period last
|
|
11
|
+
{ "id": "uc4-topic", "label": "UC4 topic query", "args": { "query": "recall period last \uAC1C\uC120", "limit": 8 }, "expect": "results" },
|
|
12
12
|
{ "id": "uc4-topic-time", "label": "UC4 topic + time window", "args": { "query": "recall", "period": "24h", "limit": 8 }, "expect": "results" },
|
|
13
13
|
{ "id": "uc4-topic-tod", "label": "UC4 topic + time-of-day", "args": { "query": "recall", "period": "12:00~15:00", "limit": 8 }, "expect": "results" },
|
|
14
|
-
{ "id": "uc4-fanout", "label": "UC4 fan-out array query", "args": { "query": ["recall
|
|
14
|
+
{ "id": "uc4-fanout", "label": "UC4 fan-out array query", "args": { "query": ["recall \uAC1C\uC120", "battle balance"], "limit": 5 }, "expect": "results" },
|
|
15
15
|
{ "id": "uc5-yesterday", "label": "UC5 calendar yesterday", "args": { "period": "yesterday", "limit": 10 }, "expect": "browse" },
|
|
16
16
|
{ "id": "uc5-thisweek", "label": "UC5 calendar this_week", "args": { "period": "this_week", "limit": 10 }, "expect": "browse" },
|
|
17
17
|
{ "id": "uc6-category", "label": "UC6 category decision 7d", "args": { "period": "7d", "category": "decision", "limit": 10 }, "expect": "browse" }
|