mixdog 0.9.46 → 0.9.49
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 +14 -4
- package/scripts/agent-parallel-smoke.mjs +4 -1
- package/scripts/agent-route-batch-test.mjs +40 -0
- package/scripts/code-graph-aggregate-cwd-test.mjs +154 -0
- package/scripts/code-graph-description-contract.mjs +185 -0
- package/scripts/code-graph-disk-hit-test.mjs +55 -0
- package/scripts/code-graph-dispatch-test.mjs +96 -0
- package/scripts/compact-pressure-test.mjs +40 -0
- package/scripts/deferred-tool-loading-test.mjs +233 -0
- package/scripts/execution-completion-dedup-test.mjs +48 -0
- package/scripts/explore-prompt-policy-test.mjs +88 -3
- package/scripts/live-worker-smoke.mjs +68 -16
- package/scripts/memory-core-input-test.mjs +33 -13
- package/scripts/native-edit-wire-test.mjs +152 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +294 -16
- package/scripts/patch-binary-cache-test.mjs +181 -0
- package/scripts/prompt-immediate-render-test.mjs +89 -0
- package/scripts/provider-toolcall-test.mjs +280 -15
- package/scripts/shell-failure-diagnostics-test.mjs +211 -0
- package/scripts/statusline-quota-hysteresis-test.mjs +26 -1
- package/scripts/streaming-tail-window-test.mjs +29 -0
- package/scripts/tool-failures.mjs +21 -3
- package/scripts/tool-smoke.mjs +263 -38
- package/scripts/tool-tui-presentation-test.mjs +17 -1
- package/scripts/tui-perf-run.ps1 +26 -0
- package/scripts/tui-transcript-perf-test.mjs +7 -1
- package/scripts/verify-release-assets-test.mjs +647 -0
- package/scripts/verify-release-assets.mjs +293 -0
- package/scripts/windows-hide-spawn-options-test.mjs +19 -0
- package/src/cli.mjs +1 -0
- package/src/defaults/cycle3-review-prompt.md +10 -4
- package/src/defaults/memory-promote-prompt.md +4 -3
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +16 -5
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +35 -11
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +35 -11
- package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +28 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +34 -34
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +45 -38
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +36 -11
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +10 -6
- package/src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs +4 -3
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +4 -3
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +16 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +0 -1
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +13 -8
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +30 -16
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +25 -8
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +69 -4
- package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +5 -4
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +12 -12
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +12 -7
- package/src/runtime/agent/orchestrator/tools/patch-binary-fetcher.mjs +77 -18
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +99 -24
- package/src/runtime/memory/lib/memory-action-handlers.mjs +21 -11
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +13 -5
- package/src/runtime/memory/lib/memory-cycle2.mjs +8 -5
- package/src/runtime/memory/lib/memory-cycle3.mjs +41 -6
- package/src/runtime/memory/lib/query-handlers.mjs +49 -6
- package/src/runtime/memory/lib/recall-format.mjs +21 -6
- package/src/runtime/memory/tool-defs.mjs +2 -3
- package/src/session-runtime/context-status.mjs +25 -16
- package/src/session-runtime/model-route-api.mjs +2 -0
- package/src/session-runtime/runtime-core.mjs +2 -2
- package/src/session-runtime/session-turn-api.mjs +4 -1
- package/src/session-runtime/tool-catalog.mjs +113 -19
- package/src/session-runtime/tool-defs.mjs +1 -0
- package/src/standalone/agent-tool/helpers.mjs +25 -6
- package/src/standalone/agent-tool.mjs +75 -41
- package/src/tui/App.jsx +4 -0
- package/src/tui/app/input-parsers.mjs +8 -9
- package/src/tui/components/Markdown.jsx +6 -1
- package/src/tui/components/Message.jsx +11 -2
- package/src/tui/components/PromptInput.jsx +19 -21
- package/src/tui/components/Spinner.jsx +4 -4
- package/src/tui/components/StatusLine.jsx +6 -6
- package/src/tui/components/TranscriptItem.jsx +2 -2
- package/src/tui/components/prompt-input/immediate-render.mjs +47 -0
- package/src/tui/components/tool-execution/surface-detail.mjs +14 -9
- package/src/tui/dist/index.mjs +130 -45
- package/src/tui/engine/agent-job-feed.mjs +21 -2
- package/src/tui/engine/turn.mjs +12 -1
- package/src/tui/markdown/measure-rendered-rows.mjs +1 -1
- package/src/tui/markdown/streaming-markdown.mjs +20 -0
- package/src/ui/statusline.mjs +5 -5
- package/src/vendor/statusline/src/gateway/session-routes.mjs +22 -10
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import {
|
|
4
|
+
mkdirSync, mkdtempSync, rmSync, writeFileSync,
|
|
5
|
+
} from 'node:fs';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import test from 'node:test';
|
|
9
|
+
import {
|
|
10
|
+
ensurePatchBinary,
|
|
11
|
+
findCachedPatchBinary,
|
|
12
|
+
} from '../src/runtime/agent/orchestrator/tools/patch-binary-fetcher.mjs';
|
|
13
|
+
import {
|
|
14
|
+
ensureNativePatchBinaryAvailable,
|
|
15
|
+
nativePatchBinPath,
|
|
16
|
+
} from '../src/runtime/agent/orchestrator/tools/patch/native-server.mjs';
|
|
17
|
+
|
|
18
|
+
const pkey = `${process.platform === 'win32' ? 'win32' : process.platform}-${process.arch}`;
|
|
19
|
+
const suffix = process.platform === 'win32' ? '.exe' : '';
|
|
20
|
+
|
|
21
|
+
function sha256(body) {
|
|
22
|
+
return createHash('sha256').update(body).digest('hex');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function makeManifest(version, body, url = `https://github.com/tribgames/mixdog/releases/download/patch-v${version}/mixdog-patch-${pkey}${suffix}`) {
|
|
26
|
+
return {
|
|
27
|
+
version,
|
|
28
|
+
assets: { [pkey]: { url, sha256: sha256(body) } },
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function fixture(t) {
|
|
33
|
+
const dataDir = mkdtempSync(join(tmpdir(), 'mixdog-patch-cache-'));
|
|
34
|
+
const binDir = join(dataDir, 'patch-bin');
|
|
35
|
+
mkdirSync(binDir, { recursive: true });
|
|
36
|
+
t.after(() => rmSync(dataDir, { recursive: true, force: true }));
|
|
37
|
+
return {
|
|
38
|
+
dataDir,
|
|
39
|
+
binDir,
|
|
40
|
+
bin(version) {
|
|
41
|
+
return join(binDir, `mixdog-patch-${version}${suffix}`);
|
|
42
|
+
},
|
|
43
|
+
localBin: join(dataDir, 'local-build', `mixdog-patch${suffix}`),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function nativeOptions(f, bundledManifest, download) {
|
|
48
|
+
return {
|
|
49
|
+
dataDir: f.dataDir,
|
|
50
|
+
defaultBin: f.localBin,
|
|
51
|
+
fetcherOptions: { bundledManifest, download },
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
test('stale cached manifest and binary cannot bypass native resolution', async (t) => {
|
|
56
|
+
const f = fixture(t);
|
|
57
|
+
const oldBody = Buffer.from('old binary');
|
|
58
|
+
const bundledBody = Buffer.from('bundled binary');
|
|
59
|
+
const bundledManifest = makeManifest('2.0.0', bundledBody);
|
|
60
|
+
writeFileSync(join(f.binDir, 'manifest.json'), JSON.stringify(makeManifest('1.0.0', oldBody)));
|
|
61
|
+
writeFileSync(f.bin('1.0.0'), oldBody);
|
|
62
|
+
const options = nativeOptions(f, bundledManifest, async (_url, destination) => {
|
|
63
|
+
writeFileSync(destination, bundledBody);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
assert.equal(findCachedPatchBinary(f.dataDir, { bundledManifest }), null);
|
|
67
|
+
assert.equal(nativePatchBinPath(options), f.localBin);
|
|
68
|
+
assert.equal(await ensureNativePatchBinaryAvailable(options), f.bin('2.0.0'));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('wrong-hash binary cannot bypass native resolution', async (t) => {
|
|
72
|
+
const f = fixture(t);
|
|
73
|
+
const expectedBody = Buffer.from('expected binary');
|
|
74
|
+
const manifest = makeManifest('2.0.0', expectedBody);
|
|
75
|
+
writeFileSync(f.bin('2.0.0'), 'wrong binary');
|
|
76
|
+
const options = nativeOptions(f, manifest, async (_url, destination) => {
|
|
77
|
+
writeFileSync(destination, expectedBody);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
assert.equal(findCachedPatchBinary(f.dataDir, { bundledManifest: manifest }), null);
|
|
81
|
+
assert.equal(nativePatchBinPath(options), f.localBin);
|
|
82
|
+
assert.equal(await ensureNativePatchBinaryAvailable(options), f.bin('2.0.0'));
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('exact bundled filename with the correct hash is selected', (t) => {
|
|
86
|
+
const f = fixture(t);
|
|
87
|
+
const body = Buffer.from('correct binary');
|
|
88
|
+
const manifest = makeManifest('2.0.0', body);
|
|
89
|
+
writeFileSync(f.bin('2.0.0'), body);
|
|
90
|
+
|
|
91
|
+
assert.equal(findCachedPatchBinary(f.dataDir, { bundledManifest: manifest }), f.bin('2.0.0'));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('upgrade downloads the bundled version through the network-free seam', async (t) => {
|
|
95
|
+
const f = fixture(t);
|
|
96
|
+
const oldBody = Buffer.from('old binary');
|
|
97
|
+
const newBody = Buffer.from('new binary');
|
|
98
|
+
const oldManifest = makeManifest('1.0.0', oldBody);
|
|
99
|
+
const bundledManifest = makeManifest('2.0.0', newBody);
|
|
100
|
+
writeFileSync(join(f.binDir, 'manifest.json'), JSON.stringify(oldManifest));
|
|
101
|
+
writeFileSync(f.bin('1.0.0'), oldBody);
|
|
102
|
+
let downloads = 0;
|
|
103
|
+
|
|
104
|
+
const selected = await ensurePatchBinary(f.dataDir, {
|
|
105
|
+
bundledManifest,
|
|
106
|
+
download: async (url, destination) => {
|
|
107
|
+
downloads += 1;
|
|
108
|
+
assert.equal(url, bundledManifest.assets[pkey].url);
|
|
109
|
+
writeFileSync(destination, newBody);
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
assert.equal(downloads, 1);
|
|
114
|
+
assert.equal(selected, f.bin('2.0.0'));
|
|
115
|
+
assert.equal(findCachedPatchBinary(f.dataDir, { bundledManifest }), selected);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('only a trusted, strictly newer valid cached manifest may advance policy', (t) => {
|
|
119
|
+
const f = fixture(t);
|
|
120
|
+
const bundledBody = Buffer.from('bundled binary');
|
|
121
|
+
const newerBody = Buffer.from('newer binary');
|
|
122
|
+
const bundledManifest = makeManifest('2.0.0', bundledBody);
|
|
123
|
+
const newerManifest = makeManifest('2.1.0', newerBody);
|
|
124
|
+
writeFileSync(join(f.binDir, 'manifest.json'), JSON.stringify(newerManifest));
|
|
125
|
+
writeFileSync(f.bin('2.1.0'), newerBody);
|
|
126
|
+
assert.equal(findCachedPatchBinary(f.dataDir, { bundledManifest }), f.bin('2.1.0'));
|
|
127
|
+
|
|
128
|
+
newerManifest.assets[pkey].url = 'https://example.com/untrusted';
|
|
129
|
+
writeFileSync(join(f.binDir, 'manifest.json'), JSON.stringify(newerManifest));
|
|
130
|
+
writeFileSync(f.bin('2.0.0'), bundledBody);
|
|
131
|
+
assert.equal(findCachedPatchBinary(f.dataDir, { bundledManifest }), f.bin('2.0.0'));
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('equal-version cached manifest cannot replace bundled policy', (t) => {
|
|
135
|
+
const f = fixture(t);
|
|
136
|
+
const bundledBody = Buffer.from('bundled binary');
|
|
137
|
+
const bundledManifest = makeManifest('2.0.0', bundledBody);
|
|
138
|
+
const equalManifest = makeManifest('2.0.0', Buffer.from('different binary'));
|
|
139
|
+
writeFileSync(join(f.binDir, 'manifest.json'), JSON.stringify(equalManifest));
|
|
140
|
+
writeFileSync(f.bin('2.0.0'), bundledBody);
|
|
141
|
+
|
|
142
|
+
assert.equal(findCachedPatchBinary(f.dataDir, { bundledManifest }), f.bin('2.0.0'));
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
for (const [label, badUrl] of [
|
|
146
|
+
['wrong release tag', `https://github.com/tribgames/mixdog/releases/download/patch-v9.9.9/mixdog-patch-${pkey}${suffix}`],
|
|
147
|
+
['wrong platform filename', `https://github.com/tribgames/mixdog/releases/download/patch-v2.1.0/mixdog-patch-wrong-platform${suffix}`],
|
|
148
|
+
]) {
|
|
149
|
+
test(`newer cached manifest with ${label} cannot replace bundled policy`, (t) => {
|
|
150
|
+
const f = fixture(t);
|
|
151
|
+
const bundledBody = Buffer.from('bundled binary');
|
|
152
|
+
const newerBody = Buffer.from('newer binary');
|
|
153
|
+
const bundledManifest = makeManifest('2.0.0', bundledBody);
|
|
154
|
+
const newerManifest = makeManifest('2.1.0', newerBody, badUrl);
|
|
155
|
+
writeFileSync(join(f.binDir, 'manifest.json'), JSON.stringify(newerManifest));
|
|
156
|
+
writeFileSync(f.bin('2.0.0'), bundledBody);
|
|
157
|
+
writeFileSync(f.bin('2.1.0'), newerBody);
|
|
158
|
+
|
|
159
|
+
assert.equal(findCachedPatchBinary(f.dataDir, { bundledManifest }), f.bin('2.0.0'));
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
test('explicit native override and local cargo path retain precedence', (t) => {
|
|
164
|
+
const f = fixture(t);
|
|
165
|
+
const body = Buffer.from('cached binary');
|
|
166
|
+
const bundledManifest = makeManifest('2.0.0', body);
|
|
167
|
+
writeFileSync(f.bin('2.0.0'), body);
|
|
168
|
+
mkdirSync(join(f.dataDir, 'local-build'), { recursive: true });
|
|
169
|
+
writeFileSync(f.localBin, 'local binary');
|
|
170
|
+
const options = nativeOptions(f, bundledManifest);
|
|
171
|
+
|
|
172
|
+
assert.equal(nativePatchBinPath(options), f.localBin);
|
|
173
|
+
const previous = process.env.MIXDOG_PATCH_NATIVE_BIN;
|
|
174
|
+
process.env.MIXDOG_PATCH_NATIVE_BIN = join(f.dataDir, 'explicit-binary');
|
|
175
|
+
try {
|
|
176
|
+
assert.equal(nativePatchBinPath(options), process.env.MIXDOG_PATCH_NATIVE_BIN);
|
|
177
|
+
} finally {
|
|
178
|
+
if (previous === undefined) delete process.env.MIXDOG_PATCH_NATIVE_BIN;
|
|
179
|
+
else process.env.MIXDOG_PATCH_NATIVE_BIN = previous;
|
|
180
|
+
}
|
|
181
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
cancelPromptImmediateFlush,
|
|
6
|
+
schedulePromptImmediateFlush,
|
|
7
|
+
} from '../src/tui/components/prompt-input/immediate-render.mjs';
|
|
8
|
+
|
|
9
|
+
test('busy prompt input cancels a pending immediate render and schedules nothing', () => {
|
|
10
|
+
const throttle = { lastAt: 90, timer: 7 };
|
|
11
|
+
const cleared = [];
|
|
12
|
+
let queued = 0;
|
|
13
|
+
let flushed = 0;
|
|
14
|
+
|
|
15
|
+
const scheduled = schedulePromptImmediateFlush({
|
|
16
|
+
throttle,
|
|
17
|
+
isSuppressed: true,
|
|
18
|
+
flush: () => { flushed += 1; },
|
|
19
|
+
now: () => 100,
|
|
20
|
+
enqueue: () => { queued += 1; },
|
|
21
|
+
clearTimer: (timer) => cleared.push(timer),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
assert.equal(scheduled, false);
|
|
25
|
+
assert.equal(throttle.timer, null);
|
|
26
|
+
assert.deepEqual(cleared, [7]);
|
|
27
|
+
assert.equal(queued, 0);
|
|
28
|
+
assert.equal(flushed, 0);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('idle prompt input keeps one leading and one coalesced trailing immediate render', () => {
|
|
32
|
+
const throttle = { lastAt: 0, timer: null };
|
|
33
|
+
let current = 100;
|
|
34
|
+
const queued = [];
|
|
35
|
+
const timers = [];
|
|
36
|
+
let flushed = 0;
|
|
37
|
+
const options = {
|
|
38
|
+
throttle,
|
|
39
|
+
isSuppressed: false,
|
|
40
|
+
flush: () => { flushed += 1; },
|
|
41
|
+
now: () => current,
|
|
42
|
+
enqueue: (callback) => queued.push(callback),
|
|
43
|
+
setTimer: (callback, delay) => {
|
|
44
|
+
timers.push({ callback, delay });
|
|
45
|
+
return timers.length;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
assert.equal(schedulePromptImmediateFlush(options), true);
|
|
50
|
+
assert.equal(queued.length, 1);
|
|
51
|
+
queued.shift()();
|
|
52
|
+
assert.equal(flushed, 1);
|
|
53
|
+
|
|
54
|
+
current = 105;
|
|
55
|
+
assert.equal(schedulePromptImmediateFlush(options), true);
|
|
56
|
+
current = 106;
|
|
57
|
+
assert.equal(schedulePromptImmediateFlush(options), false);
|
|
58
|
+
assert.equal(timers.length, 1);
|
|
59
|
+
assert.equal(timers[0].delay, 11);
|
|
60
|
+
|
|
61
|
+
current = 116;
|
|
62
|
+
timers[0].callback();
|
|
63
|
+
assert.equal(flushed, 2);
|
|
64
|
+
assert.equal(throttle.timer, null);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('a trailing immediate render is dropped if the turn becomes busy', () => {
|
|
68
|
+
const throttle = { lastAt: 100, timer: null };
|
|
69
|
+
let busy = false;
|
|
70
|
+
let timerCallback;
|
|
71
|
+
let flushed = 0;
|
|
72
|
+
|
|
73
|
+
schedulePromptImmediateFlush({
|
|
74
|
+
throttle,
|
|
75
|
+
isSuppressed: () => busy,
|
|
76
|
+
flush: () => { flushed += 1; },
|
|
77
|
+
now: () => 105,
|
|
78
|
+
setTimer: (callback) => {
|
|
79
|
+
timerCallback = callback;
|
|
80
|
+
return 1;
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
busy = true;
|
|
84
|
+
timerCallback();
|
|
85
|
+
|
|
86
|
+
assert.equal(flushed, 0);
|
|
87
|
+
assert.equal(throttle.timer, null);
|
|
88
|
+
assert.equal(cancelPromptImmediateFlush(throttle), false);
|
|
89
|
+
});
|
|
@@ -53,7 +53,9 @@ import {
|
|
|
53
53
|
} from '../src/runtime/agent/orchestrator/providers/gemini-cache.mjs';
|
|
54
54
|
import { parseSSEStream as anthropicParseSSEStream } from '../src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs';
|
|
55
55
|
import { _buildRequestBodyForCacheSmoke } from '../src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs';
|
|
56
|
+
import { _test as _anthropicApiKeyTest } from '../src/runtime/agent/orchestrator/providers/anthropic.mjs';
|
|
56
57
|
import { _toAnthropicMessagesForTest } from '../src/runtime/agent/orchestrator/providers/anthropic.mjs';
|
|
58
|
+
import { _test as _anthropicOAuthTest } from '../src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs';
|
|
57
59
|
import {
|
|
58
60
|
EFFORT_BETA_HEADER,
|
|
59
61
|
LEGACY_EFFORT_BUDGET,
|
|
@@ -66,9 +68,191 @@ import { buildAnthropicBetaHeaders } from '../src/runtime/agent/orchestrator/pro
|
|
|
66
68
|
import { PATCH_TOOL_DEFS } from '../src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs';
|
|
67
69
|
import { sendViaHttpSse } from '../src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs';
|
|
68
70
|
import { buildRequestBody as buildOpenAIOAuthRequestBody } from '../src/runtime/agent/orchestrator/providers/openai-oauth.mjs';
|
|
71
|
+
import { _convertMessagesToResponsesInputForTest } from '../src/runtime/agent/orchestrator/providers/openai-oauth.mjs';
|
|
69
72
|
|
|
70
73
|
// --- Helpers ---------------------------------------------------------------
|
|
71
74
|
|
|
75
|
+
test('native deferred history normalizes per provider without leaking OpenAI variants to xAI', () => {
|
|
76
|
+
const nativePayload = {
|
|
77
|
+
provider: 'openai-oauth',
|
|
78
|
+
toolReferences: ['mcp__demo__ping'],
|
|
79
|
+
openaiTools: [{
|
|
80
|
+
type: 'function',
|
|
81
|
+
name: 'mcp__demo__ping',
|
|
82
|
+
defer_loading: true,
|
|
83
|
+
parameters: { type: 'object', properties: {} },
|
|
84
|
+
}],
|
|
85
|
+
};
|
|
86
|
+
const history = [
|
|
87
|
+
{ role: 'user', content: 'load it' },
|
|
88
|
+
{
|
|
89
|
+
role: 'assistant',
|
|
90
|
+
content: '',
|
|
91
|
+
toolCalls: [{ id: 'load-1', name: 'load_tool', arguments: { names: ['mcp__demo__ping'] }, nativeType: 'tool_search_call' }],
|
|
92
|
+
},
|
|
93
|
+
{ role: 'tool', toolCallId: 'load-1', content: 'Loaded', nativeToolSearch: nativePayload },
|
|
94
|
+
];
|
|
95
|
+
const openai = _convertMessagesToResponsesInputForTest(history);
|
|
96
|
+
assert.equal(openai[1].type, 'tool_search_call');
|
|
97
|
+
assert.equal(openai[2].type, 'tool_search_output');
|
|
98
|
+
assert.equal(openai[2].tools[0].name, 'mcp__demo__ping');
|
|
99
|
+
|
|
100
|
+
const xai = _toXaiResponsesInputForTest(history, {
|
|
101
|
+
xaiResponses: { previousResponseId: 'resp-1', seenMessageCount: 0, model: 'grok-4' },
|
|
102
|
+
}, { model: 'grok-4' }).input;
|
|
103
|
+
assert.equal(xai.some((item) => item.type === 'tool_search_call' || item.type === 'tool_search_output'), false);
|
|
104
|
+
assert.equal(xai[1].type, 'function_call');
|
|
105
|
+
assert.equal(xai[2].type, 'function_call_output');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('Anthropic native deferred result retains tool_reference history and defer_loading declarations', () => {
|
|
109
|
+
const base = [{ name: 'load_tool', description: 'loader', inputSchema: { type: 'object', properties: {} } }];
|
|
110
|
+
const deferred = { name: 'mcp__demo__ping', description: 'ping', inputSchema: { type: 'object', properties: {} } };
|
|
111
|
+
const session = {
|
|
112
|
+
deferredNativeTools: true,
|
|
113
|
+
deferredToolCatalog: [...base, deferred],
|
|
114
|
+
};
|
|
115
|
+
const firstBody = _buildRequestBodyForCacheSmoke(
|
|
116
|
+
[{ role: 'user', content: 'find ping' }],
|
|
117
|
+
'claude-sonnet-4-6',
|
|
118
|
+
base,
|
|
119
|
+
{ session },
|
|
120
|
+
);
|
|
121
|
+
assert.equal(firstBody.tools.some((tool) => tool.name === 'mcp__demo__ping'), false);
|
|
122
|
+
assert.deepEqual(_anthropicApiKeyTest.deferredAnthropicTools(base, [], { session }), []);
|
|
123
|
+
const history = [
|
|
124
|
+
{
|
|
125
|
+
role: 'assistant',
|
|
126
|
+
content: '',
|
|
127
|
+
toolCalls: [{ id: 'load-a1', name: 'load_tool', arguments: { names: ['mcp__demo__ping'] } }],
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
role: 'tool',
|
|
131
|
+
toolCallId: 'load-a1',
|
|
132
|
+
content: 'Loaded',
|
|
133
|
+
nativeToolSearch: {
|
|
134
|
+
provider: 'anthropic-oauth',
|
|
135
|
+
toolReferences: ['mcp__demo__ping'],
|
|
136
|
+
openaiTools: [],
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
const body = _buildRequestBodyForCacheSmoke(history, 'claude-sonnet-4-6', base, { session });
|
|
141
|
+
const result = body.messages.flatMap((message) => Array.isArray(message.content) ? message.content : [])
|
|
142
|
+
.find((block) => block.type === 'tool_result');
|
|
143
|
+
assert.deepEqual(result.content, [{ type: 'tool_reference', tool_name: 'mcp__demo__ping' }]);
|
|
144
|
+
assert.equal(body.tools.find((tool) => tool.name === 'mcp__demo__ping')?.defer_loading, true);
|
|
145
|
+
const apiKeyHistory = history.map((message) => (
|
|
146
|
+
message.nativeToolSearch
|
|
147
|
+
? { ...message, nativeToolSearch: { ...message.nativeToolSearch, provider: 'anthropic' } }
|
|
148
|
+
: message
|
|
149
|
+
));
|
|
150
|
+
const apiKeyDiscovered = _anthropicApiKeyTest.deferredAnthropicTools(base, apiKeyHistory, { session });
|
|
151
|
+
assert.equal(apiKeyDiscovered.find((tool) => tool.name === 'mcp__demo__ping')?.deferLoading, true);
|
|
152
|
+
session.deferredDiscoveredTools = ['mcp__demo__ping'];
|
|
153
|
+
const compacted = _buildRequestBodyForCacheSmoke(
|
|
154
|
+
[{ role: 'user', content: 'continue after compact' }],
|
|
155
|
+
'claude-sonnet-4-6',
|
|
156
|
+
base,
|
|
157
|
+
{ session },
|
|
158
|
+
);
|
|
159
|
+
assert.equal(compacted.tools.find((tool) => tool.name === 'mcp__demo__ping')?.defer_loading, true);
|
|
160
|
+
const apiKeyCompacted = _anthropicApiKeyTest.deferredAnthropicTools(base, [], { session });
|
|
161
|
+
assert.equal(apiKeyCompacted.find((tool) => tool.name === 'mcp__demo__ping')?.deferLoading, true);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('Anthropic API-key and OAuth preserve root properties across compound schemas', () => {
|
|
165
|
+
const properties = {
|
|
166
|
+
pattern: { type: 'string' },
|
|
167
|
+
path: { type: 'string' },
|
|
168
|
+
};
|
|
169
|
+
const branches = [
|
|
170
|
+
{ required: ['pattern'] },
|
|
171
|
+
{ required: ['path'] },
|
|
172
|
+
];
|
|
173
|
+
for (const compoundKey of ['oneOf', 'anyOf', 'allOf']) {
|
|
174
|
+
const schema = { type: 'object', properties, [compoundKey]: branches };
|
|
175
|
+
const expected = { type: 'object', properties };
|
|
176
|
+
const apiKey = _anthropicApiKeyTest.sanitizeInputSchema(schema, 'grep');
|
|
177
|
+
const oauth = _anthropicOAuthTest.sanitizeInputSchema(schema, 'grep');
|
|
178
|
+
|
|
179
|
+
assert.deepEqual(apiKey, expected, `API-key ${compoundKey}`);
|
|
180
|
+
assert.deepEqual(oauth, expected, `OAuth ${compoundKey}`);
|
|
181
|
+
assert.deepEqual(apiKey, oauth, `${compoundKey} parity`);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('OpenAI native provider-tag switches keep tool_search call/output paired', () => {
|
|
186
|
+
const history = [
|
|
187
|
+
{
|
|
188
|
+
role: 'assistant',
|
|
189
|
+
content: '',
|
|
190
|
+
toolCalls: [{ id: 'native-pair', name: 'load_tool', arguments: {}, nativeType: 'tool_search_call' }],
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
role: 'tool',
|
|
194
|
+
toolCallId: 'native-pair',
|
|
195
|
+
content: 'Loaded',
|
|
196
|
+
nativeToolSearch: {
|
|
197
|
+
provider: 'openai',
|
|
198
|
+
toolReferences: ['read'],
|
|
199
|
+
openaiTools: [{ type: 'function', name: 'read', parameters: { type: 'object', properties: {} } }],
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
];
|
|
203
|
+
const oauth = _convertMessagesToResponsesInputForTest(history, { nativeToolSearchProvider: 'openai-oauth' });
|
|
204
|
+
assert.deepEqual(oauth.map((item) => item.type), ['tool_search_call', 'tool_search_output']);
|
|
205
|
+
history[1].nativeToolSearch.provider = 'openai-oauth';
|
|
206
|
+
const direct = _convertMessagesToResponsesInputForTest(history, { nativeToolSearchProvider: 'openai' });
|
|
207
|
+
assert.deepEqual(direct.map((item) => item.type), ['tool_search_call', 'tool_search_output']);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('Anthropic native provider-tag switches preserve tool_reference and loaded schema bidirectionally', () => {
|
|
211
|
+
const base = [{ name: 'load_tool', description: 'loader', inputSchema: { type: 'object', properties: {} } }];
|
|
212
|
+
const deferred = { name: 'mcp__demo__ping', description: 'ping', inputSchema: { type: 'object', properties: {} } };
|
|
213
|
+
const session = {
|
|
214
|
+
deferredNativeTools: true,
|
|
215
|
+
deferredToolCatalog: [...base, deferred],
|
|
216
|
+
};
|
|
217
|
+
const history = (provider) => [
|
|
218
|
+
{
|
|
219
|
+
role: 'assistant',
|
|
220
|
+
content: '',
|
|
221
|
+
toolCalls: [{ id: 'anthropic-family-pair', name: 'load_tool', arguments: { names: ['mcp__demo__ping'] } }],
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
role: 'tool',
|
|
225
|
+
toolCallId: 'anthropic-family-pair',
|
|
226
|
+
content: 'Loaded deferred tools: mcp__demo__ping',
|
|
227
|
+
nativeToolSearch: {
|
|
228
|
+
provider,
|
|
229
|
+
toolReferences: ['mcp__demo__ping'],
|
|
230
|
+
openaiTools: [],
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
];
|
|
234
|
+
const apiKeyToOauth = _buildRequestBodyForCacheSmoke(
|
|
235
|
+
history('anthropic'),
|
|
236
|
+
'claude-sonnet-4-6',
|
|
237
|
+
base,
|
|
238
|
+
{ session },
|
|
239
|
+
);
|
|
240
|
+
const oauthResult = apiKeyToOauth.messages
|
|
241
|
+
.flatMap((message) => Array.isArray(message.content) ? message.content : [])
|
|
242
|
+
.find((block) => block.type === 'tool_result');
|
|
243
|
+
assert.deepEqual(oauthResult.content, [{ type: 'tool_reference', tool_name: 'mcp__demo__ping' }]);
|
|
244
|
+
assert.equal(apiKeyToOauth.tools.find((tool) => tool.name === 'mcp__demo__ping')?.defer_loading, true);
|
|
245
|
+
|
|
246
|
+
const oauthHistory = history('anthropic-oauth');
|
|
247
|
+
const oauthToApiKeyMessages = _toAnthropicMessagesForTest(oauthHistory);
|
|
248
|
+
const apiKeyResult = oauthToApiKeyMessages
|
|
249
|
+
.flatMap((message) => Array.isArray(message.content) ? message.content : [])
|
|
250
|
+
.find((block) => block.type === 'tool_result');
|
|
251
|
+
assert.deepEqual(apiKeyResult.content, [{ type: 'tool_reference', tool_name: 'mcp__demo__ping' }]);
|
|
252
|
+
const apiKeyTools = _anthropicApiKeyTest.deferredAnthropicTools(base, oauthHistory, { session });
|
|
253
|
+
assert.equal(apiKeyTools.find((tool) => tool.name === 'mcp__demo__ping')?.deferLoading, true);
|
|
254
|
+
});
|
|
255
|
+
|
|
72
256
|
// Wraps an array of Anthropic SSE event objects in a minimal Response-like
|
|
73
257
|
// shape exposing the single `body.getReader()` API that parseSSEStream uses.
|
|
74
258
|
// Each event becomes a `data: <json>` SSE frame, preceded by its `event:` line.
|
|
@@ -500,7 +684,7 @@ test('openai-compat/xai Responses: first Grok request after provider switch drop
|
|
|
500
684
|
assert.deepEqual(input.map((item) => item.role), ['system', 'user', 'user']);
|
|
501
685
|
});
|
|
502
686
|
|
|
503
|
-
test('openai-oauth Responses: load_tool
|
|
687
|
+
test('openai-oauth Responses: load_tool uses native tool_search history', () => {
|
|
504
688
|
const loadTool = {
|
|
505
689
|
name: 'load_tool',
|
|
506
690
|
description: 'load tools',
|
|
@@ -517,19 +701,51 @@ test('openai-oauth Responses: load_tool schema and history stay function_call',
|
|
|
517
701
|
content: '',
|
|
518
702
|
toolCalls: [{ id: 'call_load_1', name: 'tool_search', arguments: { names: ['read'] }, nativeType: 'tool_search_call' }],
|
|
519
703
|
},
|
|
520
|
-
{
|
|
704
|
+
{
|
|
705
|
+
role: 'tool',
|
|
706
|
+
toolCallId: 'call_load_1',
|
|
707
|
+
content: 'Loaded deferred tools: read',
|
|
708
|
+
nativeToolSearch: {
|
|
709
|
+
provider: 'openai-oauth',
|
|
710
|
+
toolReferences: ['read'],
|
|
711
|
+
openaiTools: [{
|
|
712
|
+
type: 'function',
|
|
713
|
+
name: 'read',
|
|
714
|
+
description: 'read',
|
|
715
|
+
defer_loading: true,
|
|
716
|
+
parameters: { type: 'object', properties: {} },
|
|
717
|
+
}],
|
|
718
|
+
},
|
|
719
|
+
},
|
|
521
720
|
], 'gpt-5.5', [loadTool], {});
|
|
522
|
-
assert.equal(JSON.stringify(body).includes('tool_search'),
|
|
523
|
-
assert.deepEqual(body.tools
|
|
524
|
-
type: '
|
|
525
|
-
|
|
721
|
+
assert.equal(JSON.stringify(body).includes('tool_search'), true);
|
|
722
|
+
assert.deepEqual(body.tools, [{
|
|
723
|
+
type: 'tool_search',
|
|
724
|
+
execution: 'client',
|
|
526
725
|
description: loadTool.description,
|
|
527
726
|
parameters: loadTool.inputSchema,
|
|
727
|
+
}]);
|
|
728
|
+
const call = body.input.find((item) => item.type === 'tool_search_call');
|
|
729
|
+
assert.deepEqual(call, {
|
|
730
|
+
type: 'tool_search_call',
|
|
731
|
+
call_id: 'call_load_1',
|
|
732
|
+
execution: 'client',
|
|
733
|
+
arguments: { names: ['read'] },
|
|
734
|
+
});
|
|
735
|
+
const output = body.input.find((item) => item.type === 'tool_search_output' && item.call_id === 'call_load_1');
|
|
736
|
+
assert.deepEqual(output, {
|
|
737
|
+
type: 'tool_search_output',
|
|
738
|
+
call_id: 'call_load_1',
|
|
739
|
+
status: 'completed',
|
|
740
|
+
execution: 'client',
|
|
741
|
+
tools: [{
|
|
742
|
+
type: 'function',
|
|
743
|
+
name: 'read',
|
|
744
|
+
description: 'read',
|
|
745
|
+
defer_loading: true,
|
|
746
|
+
parameters: { type: 'object', properties: {} },
|
|
747
|
+
}],
|
|
528
748
|
});
|
|
529
|
-
const call = body.input.find((item) => item.type === 'function_call' && item.name === 'load_tool');
|
|
530
|
-
assert.equal(call.call_id, 'call_load_1');
|
|
531
|
-
assert.deepEqual(JSON.parse(call.arguments), { names: ['read'] });
|
|
532
|
-
assert.equal(body.input.some((item) => item.type === 'function_call_output' && item.call_id === 'call_load_1'), true);
|
|
533
749
|
});
|
|
534
750
|
|
|
535
751
|
test('openai-compat/xai Responses: custom_tool_call history replays as function_call', () => {
|
|
@@ -1432,7 +1648,7 @@ test('anthropic effort: sonnet-4-6 uses output_config + effort beta, not thinkin
|
|
|
1432
1648
|
assert.ok(beta.includes(EFFORT_BETA_HEADER));
|
|
1433
1649
|
});
|
|
1434
1650
|
|
|
1435
|
-
test('anthropic-oauth:
|
|
1651
|
+
test('anthropic-oauth: foreign native references normalize to ordinary tool_result after provider switches', () => {
|
|
1436
1652
|
const body = _buildRequestBodyForCacheSmoke(
|
|
1437
1653
|
[
|
|
1438
1654
|
{ role: 'user', content: 'load a tool' },
|
|
@@ -1445,7 +1661,7 @@ test('anthropic-oauth: load_tool native references replay as ordinary tool_resul
|
|
|
1445
1661
|
role: 'tool',
|
|
1446
1662
|
toolCallId: 'toolu_load_1',
|
|
1447
1663
|
content: '{"loaded":["read"]}',
|
|
1448
|
-
nativeToolSearch: { toolReferences: ['read'] },
|
|
1664
|
+
nativeToolSearch: { provider: 'openai-oauth', toolReferences: ['read'] },
|
|
1449
1665
|
},
|
|
1450
1666
|
],
|
|
1451
1667
|
'claude-opus-4-8',
|
|
@@ -1617,6 +1833,55 @@ test('openai oauth ws delta: warmup generate:false does not force request_proper
|
|
|
1617
1833
|
}
|
|
1618
1834
|
});
|
|
1619
1835
|
|
|
1836
|
+
test('openai oauth ws delta: native tool_search output replays with canonical fields', () => {
|
|
1837
|
+
const prevTransport = process.env.MIXDOG_OAI_TRANSPORT;
|
|
1838
|
+
try {
|
|
1839
|
+
process.env.MIXDOG_OAI_TRANSPORT = 'ws-delta';
|
|
1840
|
+
const baseTools = [{
|
|
1841
|
+
type: 'tool_search',
|
|
1842
|
+
execution: 'client',
|
|
1843
|
+
description: 'load tools',
|
|
1844
|
+
parameters: { type: 'object', properties: {} },
|
|
1845
|
+
}];
|
|
1846
|
+
const output = {
|
|
1847
|
+
type: 'tool_search_output',
|
|
1848
|
+
call_id: 'call_load_1',
|
|
1849
|
+
status: 'completed',
|
|
1850
|
+
execution: 'client',
|
|
1851
|
+
tools: [{ type: 'function', name: 'read', parameters: { type: 'object', properties: {} } }],
|
|
1852
|
+
};
|
|
1853
|
+
const body = {
|
|
1854
|
+
model: 'gpt-5.5',
|
|
1855
|
+
instructions: 'sys',
|
|
1856
|
+
tools: baseTools,
|
|
1857
|
+
prompt_cache_key: 'cache-key',
|
|
1858
|
+
input: [
|
|
1859
|
+
{ type: 'message', role: 'user', content: 'load read' },
|
|
1860
|
+
{ type: 'tool_search_call', call_id: 'call_load_1', execution: 'client', arguments: { names: ['read'] } },
|
|
1861
|
+
output,
|
|
1862
|
+
{ type: 'message', role: 'user', content: 'use read' },
|
|
1863
|
+
],
|
|
1864
|
+
};
|
|
1865
|
+
const delta = _computeDelta({
|
|
1866
|
+
entry: {
|
|
1867
|
+
lastRequestSansInput: _stableStringify(_sansInput(body)),
|
|
1868
|
+
lastResponseId: 'resp_prev',
|
|
1869
|
+
lastRequestInput: [body.input[0]],
|
|
1870
|
+
lastResponseItems: [body.input[1]],
|
|
1871
|
+
},
|
|
1872
|
+
body,
|
|
1873
|
+
});
|
|
1874
|
+
assert.equal(delta.mode, 'delta');
|
|
1875
|
+
assert.equal(delta.frame.prompt_cache_key, 'cache-key');
|
|
1876
|
+
assert.deepEqual(delta.frame.tools, baseTools);
|
|
1877
|
+
assert.equal(delta.frame.instructions, 'sys');
|
|
1878
|
+
assert.deepEqual(delta.frame.input, [output, body.input[3]]);
|
|
1879
|
+
} finally {
|
|
1880
|
+
if (prevTransport == null) delete process.env.MIXDOG_OAI_TRANSPORT;
|
|
1881
|
+
else process.env.MIXDOG_OAI_TRANSPORT = prevTransport;
|
|
1882
|
+
}
|
|
1883
|
+
});
|
|
1884
|
+
|
|
1620
1885
|
test('canonical frame: full-frame builder leads with type and preserves codex body key order', () => {
|
|
1621
1886
|
const body = {
|
|
1622
1887
|
model: 'gpt-5.5',
|
|
@@ -1639,7 +1904,7 @@ test('canonical frame: full-frame builder leads with type and preserves codex bo
|
|
|
1639
1904
|
assert.equal(frame.previous_response_id, undefined);
|
|
1640
1905
|
});
|
|
1641
1906
|
|
|
1642
|
-
test('canonical frame: delta insert keeps key order
|
|
1907
|
+
test('canonical frame: delta insert keeps key order and chained instructions, overrides input', () => {
|
|
1643
1908
|
const body = {
|
|
1644
1909
|
model: 'gpt-5.5',
|
|
1645
1910
|
instructions: 'sys',
|
|
@@ -1648,8 +1913,8 @@ test('canonical frame: delta insert keeps key order, drops chained instructions,
|
|
|
1648
1913
|
text: { verbosity: 'low' },
|
|
1649
1914
|
};
|
|
1650
1915
|
const delta = _buildResponseCreateFrame(body, { previousResponseId: 'resp_prev', inputOverride: [{ b: 2 }] });
|
|
1651
|
-
assert.deepEqual(Object.keys(delta), ['type', 'model', 'previous_response_id', 'input', 'tool_choice', 'text']);
|
|
1652
|
-
assert.equal(delta.instructions,
|
|
1916
|
+
assert.deepEqual(Object.keys(delta), ['type', 'model', 'instructions', 'previous_response_id', 'input', 'tool_choice', 'text']);
|
|
1917
|
+
assert.equal(delta.instructions, 'sys');
|
|
1653
1918
|
assert.equal(delta.previous_response_id, 'resp_prev');
|
|
1654
1919
|
assert.deepEqual(delta.input, [{ b: 2 }]);
|
|
1655
1920
|
// Empty instructions is also dropped in delta mode (server resolves via prev id).
|