mixdog 0.9.47 → 0.9.50
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/README.md +6 -6
- package/package.json +19 -9
- 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/embedding-worker-exit-test.mjs +76 -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/smoke-loop.mjs +9 -3
- package/scripts/smoke.mjs +5 -106
- 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 +281 -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/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/embedding-provider.mjs +3 -2
- package/src/runtime/memory/lib/memory-action-handlers.mjs +21 -11
- package/src/runtime/memory/tool-defs.mjs +1 -2
- 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
|
@@ -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).
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { chmodSync, mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join, resolve } from 'node:path';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { classifyToolFailure } from '../src/runtime/agent/orchestrator/agent-trace-format.mjs';
|
|
8
|
+
import { ExecResult, execShellCommand } from '../src/runtime/agent/orchestrator/tools/shell-command.mjs';
|
|
9
|
+
import { _composeShellFailure, _shellFailureStatus } from '../src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs';
|
|
10
|
+
|
|
11
|
+
test('shell trace classification uses only the leading status marker', () => {
|
|
12
|
+
assert.equal(classifyToolFailure(
|
|
13
|
+
'Error: [shell-run-failed] [exit code: 1]\n\ncommand timed out while parsing an aborted field',
|
|
14
|
+
'shell',
|
|
15
|
+
), 'command-exit');
|
|
16
|
+
assert.equal(classifyToolFailure(
|
|
17
|
+
'Error: [shell-run-failed] [signal: SIGKILL]\n\n(no output)',
|
|
18
|
+
'shell',
|
|
19
|
+
), 'process/signal');
|
|
20
|
+
assert.equal(classifyToolFailure(
|
|
21
|
+
'Error: [shell-run-failed] [timeout: 500ms signal: SIGTERM cause: timeout]',
|
|
22
|
+
'shell',
|
|
23
|
+
), 'timeout/abort');
|
|
24
|
+
assert.equal(classifyToolFailure(
|
|
25
|
+
'Error: [shell-run-failed] [signal: SIGTERM cause: cancellation]',
|
|
26
|
+
'shell',
|
|
27
|
+
), 'timeout/abort');
|
|
28
|
+
assert.equal(classifyToolFailure(
|
|
29
|
+
'Error: [shell-run-failed] [signal: SIGKILL cause: output-limit]',
|
|
30
|
+
'shell',
|
|
31
|
+
), 'runtime/failure');
|
|
32
|
+
assert.equal(classifyToolFailure(
|
|
33
|
+
'⚠️ destructive command warning\nError: [shell-run-failed] [signal: SIGKILL]',
|
|
34
|
+
'shell',
|
|
35
|
+
), 'process/signal');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('shell failure rendering preserves actual signals and runtime kill causes', () => {
|
|
39
|
+
const status = (opts) => _shellFailureStatus(new ExecResult({
|
|
40
|
+
stdout: '', stderr: '', exitCode: null, taskId: 'test', ...opts,
|
|
41
|
+
}), 500).statusDetail;
|
|
42
|
+
assert.match(status({ signal: 'SIGKILL' }), /^\[signal: SIGKILL\]$/);
|
|
43
|
+
assert.match(status({ signal: 'SIGTERM', killed: true, killCause: 'cancellation' }),
|
|
44
|
+
/^\[signal: SIGTERM cause: cancellation\]$/);
|
|
45
|
+
assert.match(status({ signal: 'SIGTERM', killed: true, timedOut: true, killCause: 'timeout' }),
|
|
46
|
+
/^\[timeout: 500ms signal: SIGTERM cause: timeout\]/);
|
|
47
|
+
assert.match(status({
|
|
48
|
+
killed: true,
|
|
49
|
+
killCause: 'output-capture-error',
|
|
50
|
+
outputCaptureError: new Error('disk full'),
|
|
51
|
+
}), /^\[output capture failed cause: output-capture-error signal: SIGKILL\]$/);
|
|
52
|
+
assert.match(status({ signal: 'SIGKILL', killed: true, killCause: 'output-limit' }),
|
|
53
|
+
/^\[signal: SIGKILL cause: output-limit\]$/);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('WMIC rewrite note follows the leading shell failure marker', () => {
|
|
57
|
+
const rendered = _composeShellFailure(
|
|
58
|
+
'[shell-run-failed] [exit code: 1]',
|
|
59
|
+
'Error: ',
|
|
60
|
+
'[auto-rewrite: deprecated wmic process query -> PowerShell; timeout capped at 30000ms]',
|
|
61
|
+
'(no output)',
|
|
62
|
+
);
|
|
63
|
+
assert.match(rendered, /^Error: \[shell-run-failed\] \[exit code: 1\]\n\[auto-rewrite:/);
|
|
64
|
+
assert.equal(classifyToolFailure(rendered, 'shell'), 'command-exit');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
async function withoutUnhandledProcessFailure(run) {
|
|
68
|
+
const uncaught = [];
|
|
69
|
+
const rejected = [];
|
|
70
|
+
const onUncaught = (err) => uncaught.push(err);
|
|
71
|
+
const onRejected = (err) => rejected.push(err);
|
|
72
|
+
process.on('uncaughtException', onUncaught);
|
|
73
|
+
process.on('unhandledRejection', onRejected);
|
|
74
|
+
try {
|
|
75
|
+
const result = await run();
|
|
76
|
+
await new Promise((resolveTurn) => setImmediate(resolveTurn));
|
|
77
|
+
assert.deepEqual(uncaught, [], `unexpected uncaught error: ${uncaught[0]?.stack || uncaught[0]}`);
|
|
78
|
+
assert.deepEqual(rejected, [], `unexpected unhandled rejection: ${rejected[0]?.stack || rejected[0]}`);
|
|
79
|
+
return result;
|
|
80
|
+
} finally {
|
|
81
|
+
process.removeListener('uncaughtException', onUncaught);
|
|
82
|
+
process.removeListener('unhandledRejection', onRejected);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function assertSpawnToolFailure(result) {
|
|
87
|
+
assert.equal(result.failurePhase, 'tool');
|
|
88
|
+
assert.equal(result.failureReason, 'spawn failed');
|
|
89
|
+
const status = _shellFailureStatus(result, 1000);
|
|
90
|
+
assert.equal(status.shellToolFailed, true);
|
|
91
|
+
const rendered = _composeShellFailure(
|
|
92
|
+
`[shell-tool-failed] ${status.statusDetail}`,
|
|
93
|
+
'Error: ',
|
|
94
|
+
'',
|
|
95
|
+
result.stderr,
|
|
96
|
+
);
|
|
97
|
+
assert.match(rendered, /^Error: \[shell-tool-failed\] \[spawn failed\]/);
|
|
98
|
+
assert.equal(classifyToolFailure(rendered, 'shell'), 'tool-call/failure');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
test('asynchronous ENOENT spawn errors remain shell tool failures', async () => {
|
|
102
|
+
const missing = await withoutUnhandledProcessFailure(() => execShellCommand({
|
|
103
|
+
shell: join(tmpdir(), `mixdog-missing-shell-${process.pid}`),
|
|
104
|
+
shellArg: '-c',
|
|
105
|
+
command: 'echo unreachable',
|
|
106
|
+
env: process.env,
|
|
107
|
+
cwd: process.cwd(),
|
|
108
|
+
timeoutMs: 1000,
|
|
109
|
+
}));
|
|
110
|
+
assertSpawnToolFailure(missing);
|
|
111
|
+
assert.match(missing.stderr, /ENOENT|not found/i);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('asynchronous EACCES spawn errors remain shell tool failures', async (t) => {
|
|
115
|
+
if (process.platform === 'win32') return t.skip('executable-bit case is POSIX-only');
|
|
116
|
+
const dir = mkdtempSync(join(tmpdir(), 'mixdog-eacces-shell-'));
|
|
117
|
+
try {
|
|
118
|
+
const denied = join(dir, 'denied.sh');
|
|
119
|
+
writeFileSync(denied, '#!/bin/sh\necho unreachable\n');
|
|
120
|
+
chmodSync(denied, 0o600);
|
|
121
|
+
const result = await withoutUnhandledProcessFailure(() => execShellCommand({
|
|
122
|
+
shell: denied,
|
|
123
|
+
shellArg: '-c',
|
|
124
|
+
command: 'echo unreachable',
|
|
125
|
+
env: process.env,
|
|
126
|
+
cwd: process.cwd(),
|
|
127
|
+
timeoutMs: 1000,
|
|
128
|
+
}));
|
|
129
|
+
assertSpawnToolFailure(result);
|
|
130
|
+
assert.match(result.stderr, /EACCES|permission denied/i);
|
|
131
|
+
} finally {
|
|
132
|
+
rmSync(dir, { recursive: true, force: true });
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('execShellCommand carries cancellation cause alongside process signal', async () => {
|
|
137
|
+
const controller = new AbortController();
|
|
138
|
+
const isWindows = process.platform === 'win32';
|
|
139
|
+
const promise = execShellCommand({
|
|
140
|
+
shell: isWindows ? (process.env.ComSpec || 'cmd.exe') : '/bin/sh',
|
|
141
|
+
shellArg: isWindows ? '/c' : '-c',
|
|
142
|
+
command: isWindows ? 'ping 127.0.0.1 -n 20 > nul' : 'sleep 10',
|
|
143
|
+
env: process.env,
|
|
144
|
+
cwd: process.cwd(),
|
|
145
|
+
timeoutMs: 5000,
|
|
146
|
+
abortSignal: controller.signal,
|
|
147
|
+
backgroundOnTimeout: false,
|
|
148
|
+
});
|
|
149
|
+
setTimeout(() => controller.abort(), 100);
|
|
150
|
+
const result = await promise;
|
|
151
|
+
assert.equal(result.killed, true);
|
|
152
|
+
assert.equal(result.killCause, 'cancellation');
|
|
153
|
+
assert.ok(result.signal || process.platform === 'win32');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('cancellation racing with auto-background adoption is returned as cancelled', async () => {
|
|
157
|
+
let abortReads = 0;
|
|
158
|
+
const racingSignal = {
|
|
159
|
+
get aborted() { abortReads += 1; return abortReads >= 2; },
|
|
160
|
+
addEventListener() {},
|
|
161
|
+
removeEventListener() {},
|
|
162
|
+
};
|
|
163
|
+
const isWindows = process.platform === 'win32';
|
|
164
|
+
const result = await execShellCommand({
|
|
165
|
+
shell: isWindows ? (process.env.ComSpec || 'cmd.exe') : '/bin/sh',
|
|
166
|
+
shellArg: isWindows ? '/c' : '-c',
|
|
167
|
+
command: isWindows ? 'ping 127.0.0.1 -n 20 > nul' : 'sleep 10',
|
|
168
|
+
env: process.env,
|
|
169
|
+
cwd: process.cwd(),
|
|
170
|
+
timeoutMs: 5000,
|
|
171
|
+
abortSignal: racingSignal,
|
|
172
|
+
autoBackgroundMs: 25,
|
|
173
|
+
backgroundOnTimeout: false,
|
|
174
|
+
});
|
|
175
|
+
assert.equal(result.backgrounded, false);
|
|
176
|
+
assert.equal(result.killed, true);
|
|
177
|
+
assert.equal(result.killCause, 'cancellation');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('tool-failures separates actionable totals while retaining command-exit rows', () => {
|
|
181
|
+
const dir = mkdtempSync(join(tmpdir(), 'mixdog-tool-failures-test-'));
|
|
182
|
+
try {
|
|
183
|
+
const history = join(dir, 'history');
|
|
184
|
+
mkdirSync(history);
|
|
185
|
+
const rows = [
|
|
186
|
+
{ ts: 1, tool_name: 'shell', category: 'process/signal', error_first_line: 'SIGKILL' },
|
|
187
|
+
{ ts: 2, tool_name: 'shell', category: 'runtime/failure', error_first_line: 'capture guard' },
|
|
188
|
+
...Array.from({ length: 45 }, (_, index) => ({
|
|
189
|
+
ts: index + 3,
|
|
190
|
+
tool_name: 'shell',
|
|
191
|
+
category: 'command-exit',
|
|
192
|
+
error_first_line: `exit ${index}`,
|
|
193
|
+
})),
|
|
194
|
+
];
|
|
195
|
+
writeFileSync(join(history, 'tool-failures.jsonl'), `${rows.map(JSON.stringify).join('\n')}\n`);
|
|
196
|
+
const script = resolve('scripts/tool-failures.mjs');
|
|
197
|
+
const text = spawnSync(process.execPath, [script, '--data-dir', dir, '--limit', '2'], { encoding: 'utf8' });
|
|
198
|
+
assert.equal(text.status, 0, text.stderr);
|
|
199
|
+
assert.match(text.stdout, /actionable failures: 2\/2 shown/);
|
|
200
|
+
assert.match(text.stdout, /command exits: 2\/45 shown \(retained\)/);
|
|
201
|
+
assert.equal((text.stdout.match(/^- /gm) || []).length, 4);
|
|
202
|
+
const json = spawnSync(process.execPath, [script, '--data-dir', dir, '--limit', '2', '--json'], { encoding: 'utf8' });
|
|
203
|
+
assert.equal(json.status, 0, json.stderr);
|
|
204
|
+
const report = JSON.parse(json.stdout);
|
|
205
|
+
assert.deepEqual(report.actionable_failures, { shown: 2, matched: 2 });
|
|
206
|
+
assert.deepEqual(report.command_exits, { shown: 2, matched: 45 });
|
|
207
|
+
assert.equal(report.rows.length, 4);
|
|
208
|
+
} finally {
|
|
209
|
+
rmSync(dir, { recursive: true, force: true });
|
|
210
|
+
}
|
|
211
|
+
});
|
package/scripts/smoke-loop.mjs
CHANGED
|
@@ -58,7 +58,7 @@ function runNode(args, label, timeoutMs = 180_000) {
|
|
|
58
58
|
function runSmokeAll(iteration) {
|
|
59
59
|
let totalMs = 0;
|
|
60
60
|
const steps = [];
|
|
61
|
-
for (const script of ['scripts/
|
|
61
|
+
for (const script of ['scripts/smoke.mjs', 'scripts/boot-smoke.mjs']) {
|
|
62
62
|
const result = runNode([script], `${script} iteration ${iteration}`);
|
|
63
63
|
totalMs += result.ms;
|
|
64
64
|
steps.push({
|
|
@@ -161,8 +161,14 @@ while (Date.now() < deadline && iteration < maxIterations) {
|
|
|
161
161
|
const iterStartedAt = Date.now();
|
|
162
162
|
const smoke = runSmokeAll(iteration);
|
|
163
163
|
smokeTimes.push(smoke.ms);
|
|
164
|
-
const failure = runNode(['scripts/tool-failures.mjs', '--since', since, '--limit', '1'], `failures iteration ${iteration}`, 60_000);
|
|
165
|
-
|
|
164
|
+
const failure = runNode(['scripts/tool-failures.mjs', '--since', since, '--limit', '1', '--json'], `failures iteration ${iteration}`, 60_000);
|
|
165
|
+
let failureSummary;
|
|
166
|
+
try {
|
|
167
|
+
failureSummary = JSON.parse(failure.stdout);
|
|
168
|
+
} catch {
|
|
169
|
+
throw new Error(`tool failures returned malformed JSON:\n${failure.stdout}`);
|
|
170
|
+
}
|
|
171
|
+
if (failureSummary?.actionable_failures?.matched !== 0) {
|
|
166
172
|
throw new Error(`tool failures appeared after loop start:\n${failure.stdout}`);
|
|
167
173
|
}
|
|
168
174
|
const currentRss = rssMb();
|