mixdog 0.8.1 → 0.9.1
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 +47 -23
- package/package.json +34 -28
- package/scripts/_test-folder-dialog.mjs +30 -0
- package/scripts/agent-parallel-smoke.mjs +388 -0
- package/scripts/agent-tag-reuse-smoke.mjs +183 -0
- package/scripts/background-task-meta-smoke.mjs +38 -0
- package/scripts/boot-smoke.mjs +52 -9
- package/scripts/build-runtime-linux.sh +348 -0
- package/scripts/build-runtime-macos.sh +217 -0
- package/scripts/build-runtime-windows.ps1 +242 -0
- package/scripts/compact-active-turn-test.mjs +68 -0
- package/scripts/compact-smoke.mjs +859 -129
- package/scripts/compact-trigger-migration-smoke.mjs +187 -0
- package/scripts/fix-brief-fn.mjs +35 -0
- package/scripts/fix-format-tool-surface.mjs +24 -0
- package/scripts/fix-tool-exec-visible.mjs +42 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/hook-bus-test.mjs +330 -0
- package/scripts/lead-workflow-smoke.mjs +33 -39
- package/scripts/live-worker-smoke.mjs +43 -37
- package/scripts/llm-trace-summary.mjs +315 -0
- package/scripts/memory-meta-concurrency-test.mjs +20 -0
- package/scripts/output-style-smoke.mjs +56 -15
- package/scripts/parent-abort-link-test.mjs +44 -0
- package/scripts/patch-agent-brief.mjs +48 -0
- package/scripts/patch-app.mjs +21 -0
- package/scripts/patch-app2.mjs +18 -0
- package/scripts/patch-dist-brief.mjs +96 -0
- package/scripts/patch-tool-exec.mjs +70 -0
- package/scripts/pretool-ask-runtime-test.mjs +54 -0
- package/scripts/provider-toolcall-test.mjs +376 -0
- package/scripts/reactive-compact-persist-smoke.mjs +124 -0
- package/scripts/sanitize-tool-pairs-test.mjs +260 -0
- package/scripts/session-context-bench.mjs +205 -33
- package/scripts/session-ingest-smoke.mjs +177 -0
- package/scripts/set-effort-config-test.mjs +41 -0
- package/scripts/smoke-runtime-negative.ps1 +106 -0
- package/scripts/smoke-runtime-negative.sh +97 -0
- package/scripts/smoke.mjs +25 -0
- package/scripts/tool-result-hook-test.mjs +48 -0
- package/scripts/tool-smoke.mjs +1222 -90
- package/scripts/toolcall-args-test.mjs +150 -0
- package/scripts/tui-background-failure-smoke.mjs +73 -0
- package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
- package/src/agents/debugger/AGENT.md +8 -0
- package/src/agents/explore/AGENT.md +4 -0
- package/src/agents/heavy-worker/AGENT.md +9 -3
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +8 -0
- package/src/agents/scheduler-task/AGENT.md +12 -0
- package/src/agents/scheduler-task/agent.json +6 -0
- package/src/agents/webhook-handler/AGENT.md +12 -0
- package/src/agents/webhook-handler/agent.json +6 -0
- package/src/agents/worker/AGENT.md +9 -3
- package/src/app.mjs +77 -3
- package/src/defaults/hidden-roles.json +17 -12
- package/src/headless-role.mjs +117 -0
- package/src/help.mjs +30 -0
- package/src/hooks/lib/permission-evaluator.cjs +11 -475
- package/src/lib/keychain-cjs.cjs +9 -1
- package/src/lib/mixdog-debug.cjs +0 -29
- package/src/lib/plugin-paths.cjs +1 -7
- package/src/lib/rules-builder.cjs +242 -98
- package/src/lib/text-utils.cjs +1 -1
- package/src/mixdog-session-runtime.mjs +2211 -446
- package/src/output-styles/default.md +12 -28
- package/src/output-styles/extreme-simple.md +9 -6
- package/src/output-styles/simple.md +22 -9
- package/src/repl.mjs +108 -59
- package/src/rules/agent/00-common.md +15 -0
- package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
- package/src/rules/agent/30-explorer.md +22 -0
- package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
- package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
- package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
- package/src/rules/lead/01-general.md +9 -5
- package/src/rules/lead/04-workflow.md +51 -12
- package/src/rules/lead/lead-tool.md +6 -0
- package/src/rules/shared/01-tool.md +12 -1
- package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
- package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
- package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
- package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
- package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
- package/src/runtime/agent/orchestrator/config.mjs +174 -55
- package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
- package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
- package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
- package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +380 -268
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +131 -93
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
- package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +203 -71
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +512 -312
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -518
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +289 -11
- package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
- package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
- package/src/runtime/agent/orchestrator/session/loop.mjs +840 -612
- package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
- package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +35 -8
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -8
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -53
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
- package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
- package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
- package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
- package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
- package/src/runtime/channels/index.mjs +149 -51
- package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
- package/src/runtime/channels/lib/config.mjs +0 -1
- package/src/runtime/channels/lib/drop-trace.mjs +1 -1
- package/src/runtime/channels/lib/executor.mjs +0 -3
- package/src/runtime/channels/lib/memory-client.mjs +0 -38
- package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
- package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
- package/src/runtime/channels/lib/scheduler.mjs +18 -14
- package/src/runtime/channels/lib/session-discovery.mjs +3 -6
- package/src/runtime/channels/lib/tool-format.mjs +0 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +4 -12
- package/src/runtime/channels/lib/webhook.mjs +1 -1
- package/src/runtime/channels/tool-defs.mjs +29 -29
- package/src/runtime/lib/keychain-cjs.cjs +0 -1
- package/src/runtime/memory/data/runtime-manifest.json +6 -7
- package/src/runtime/memory/index.mjs +519 -118
- package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
- package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
- package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
- package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
- package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
- package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
- package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
- package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -2
- package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
- package/src/runtime/memory/lib/memory.mjs +88 -9
- package/src/runtime/memory/lib/model-profile.mjs +1 -1
- package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
- package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +80 -21
- package/src/runtime/memory/lib/session-ingest.mjs +196 -0
- package/src/runtime/memory/lib/trace-store.mjs +96 -51
- package/src/runtime/memory/tool-defs.mjs +46 -37
- package/src/runtime/search/index.mjs +104 -473
- package/src/runtime/search/lib/config.mjs +0 -4
- package/src/runtime/search/lib/state.mjs +1 -15
- package/src/runtime/search/lib/web-tools.mjs +44 -25
- package/src/runtime/search/tool-defs.mjs +16 -23
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +4 -3
- package/src/runtime/shared/background-tasks.mjs +122 -11
- package/src/runtime/shared/child-spawn-gate.mjs +139 -0
- package/src/runtime/shared/config.mjs +7 -4
- package/src/runtime/shared/err-text.mjs +131 -4
- package/src/runtime/shared/llm/cost.mjs +2 -2
- package/src/runtime/shared/llm/http-agent.mjs +23 -7
- package/src/runtime/shared/llm/index.mjs +34 -11
- package/src/runtime/shared/llm/usage-log.mjs +4 -4
- package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
- package/src/runtime/shared/singleton-owner.mjs +104 -0
- package/src/runtime/shared/tool-execution-contract.mjs +199 -20
- package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
- package/src/runtime/shared/tool-surface.mjs +624 -95
- package/src/runtime/shared/user-data-guard.mjs +0 -2
- package/src/standalone/agent-task-status.mjs +203 -0
- package/src/standalone/agent-task-status.test.mjs +76 -0
- package/src/standalone/agent-tool.mjs +1913 -0
- package/src/standalone/channel-worker.mjs +370 -14
- package/src/standalone/explore-tool.mjs +165 -70
- package/src/standalone/folder-dialog.mjs +314 -0
- package/src/standalone/hook-bus.mjs +898 -22
- package/src/standalone/memory-runtime-proxy.mjs +320 -0
- package/src/standalone/projects.mjs +226 -0
- package/src/standalone/provider-admin.mjs +41 -24
- package/src/standalone/seeds.mjs +1 -78
- package/src/standalone/usage-dashboard.mjs +96 -8
- package/src/tui/App.jsx +4800 -2140
- package/src/tui/components/AnsiText.jsx +39 -28
- package/src/tui/components/ContextPanel.jsx +14 -4
- package/src/tui/components/Markdown.jsx +43 -77
- package/src/tui/components/MarkdownTable.jsx +9 -184
- package/src/tui/components/Message.jsx +28 -11
- package/src/tui/components/Picker.jsx +95 -56
- package/src/tui/components/PromptInput.jsx +428 -239
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +27 -21
- package/src/tui/components/Spinner.jsx +67 -38
- package/src/tui/components/StatusLine.jsx +606 -38
- package/src/tui/components/TextEntryPanel.jsx +128 -9
- package/src/tui/components/ToolExecution.jsx +597 -362
- package/src/tui/components/TurnDone.jsx +3 -3
- package/src/tui/components/UsagePanel.jsx +3 -5
- package/src/tui/components/tool-output-format.mjs +499 -0
- package/src/tui/components/tool-output-format.test.mjs +312 -0
- package/src/tui/dist/index.mjs +9192 -2399
- package/src/tui/engine-runtime-notification.test.mjs +115 -0
- package/src/tui/engine-tool-result-text.test.mjs +75 -0
- package/src/tui/engine.mjs +1455 -279
- package/src/tui/figures.mjs +21 -40
- package/src/tui/index.jsx +75 -31
- package/src/tui/input-editing.mjs +25 -0
- package/src/tui/markdown/format-token.mjs +671 -69
- package/src/tui/markdown/format-token.test.mjs +312 -0
- package/src/tui/markdown/render-ansi.mjs +94 -0
- package/src/tui/markdown/render-ansi.test.mjs +108 -0
- package/src/tui/markdown/stream-fence.mjs +34 -0
- package/src/tui/markdown/stream-fence.test.mjs +26 -0
- package/src/tui/markdown/table-layout.mjs +250 -0
- package/src/tui/paste-attachments.mjs +0 -7
- package/src/tui/spinner-verbs.mjs +1 -2
- package/src/tui/statusline-ansi-bridge.mjs +172 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
- package/src/tui/theme.mjs +756 -24
- package/src/tui/time-format.mjs +1 -1
- package/src/tui/transcript-tool-failures.mjs +67 -0
- package/src/tui/transcript-tool-failures.test.mjs +111 -0
- package/src/ui/ansi.mjs +1 -2
- package/src/ui/markdown.mjs +85 -26
- package/src/ui/markdown.test.mjs +70 -0
- package/src/ui/model-display.mjs +121 -0
- package/src/ui/session-stats.mjs +44 -0
- package/src/ui/statusline-context-label.test.mjs +15 -0
- package/src/ui/statusline.mjs +386 -178
- package/src/ui/tool-card.mjs +2 -13
- package/src/vendor/statusline/bin/statusline-lib.mjs +1 -620
- package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
- package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
- package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
- package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
- package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
- package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
- package/src/workflows/default/WORKFLOW.md +12 -5
- package/src/workflows/default/workflow.json +0 -1
- package/src/workflows/solo/WORKFLOW.md +15 -0
- package/src/workflows/solo/workflow.json +7 -0
- package/vendor/ink/build/ink.js +54 -8
- package/vendor/ink/build/output.js +6 -1
- package/src/agents/scheduler-task.md +0 -3
- package/src/agents/web-researcher/AGENT.md +0 -3
- package/src/agents/web-researcher/agent.json +0 -6
- package/src/agents/webhook-handler.md +0 -3
- package/src/hooks/lib/permission-rules.cjs +0 -170
- package/src/hooks/lib/settings-loader.cjs +0 -112
- package/src/lib/hook-pipe-path.cjs +0 -10
- package/src/rules/bridge/00-common.md +0 -5
- package/src/rules/bridge/30-explorer.md +0 -4
- package/src/rules/lead/00-tool-lead.md +0 -5
- package/src/rules/shared/00-language.md +0 -3
- package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
- package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
- package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
- package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
- package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
- package/src/standalone/bridge-tool.mjs +0 -1414
- package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Regression tests pinning the cross-provider "native tool_call extraction"
|
|
3
|
+
// contract: when a provider's native parser is fed a well-formed tool_call
|
|
4
|
+
// payload, it MUST surface the call in our canonical toolCalls shape
|
|
5
|
+
// ({ id, name, arguments }). Synthetic inputs fed directly to the exported parser, asserting the
|
|
6
|
+
// resulting outcome. No network, no model. Each provider also gets one
|
|
7
|
+
// negative case (no native tool_call → undefined / empty).
|
|
8
|
+
//
|
|
9
|
+
// Parser entry points (file:line at authoring time) and sharing notes are
|
|
10
|
+
// documented inline per provider block below.
|
|
11
|
+
import test from 'node:test';
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
_toResponsesToolsForTest,
|
|
16
|
+
_toXaiResponsesInputForTest,
|
|
17
|
+
parseToolCalls as compatParseToolCalls,
|
|
18
|
+
parseResponsesToolCalls as compatParseResponsesToolCalls,
|
|
19
|
+
} from '../src/runtime/agent/orchestrator/providers/openai-compat.mjs';
|
|
20
|
+
import {
|
|
21
|
+
consumeCompatResponsesStream,
|
|
22
|
+
isInvalidToolArgsMarker,
|
|
23
|
+
} from '../src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs';
|
|
24
|
+
import { parseToolCalls as geminiParseToolCalls } from '../src/runtime/agent/orchestrator/providers/gemini.mjs';
|
|
25
|
+
import { parseSSEStream as anthropicParseSSEStream } from '../src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs';
|
|
26
|
+
import { PATCH_TOOL_DEFS } from '../src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs';
|
|
27
|
+
|
|
28
|
+
// --- Helpers ---------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
// Wraps an array of Anthropic SSE event objects in a minimal Response-like
|
|
31
|
+
// shape exposing the single `body.getReader()` API that parseSSEStream uses.
|
|
32
|
+
// Each event becomes a `data: <json>` SSE frame, preceded by its `event:` line.
|
|
33
|
+
function anthropicSseResponse(events) {
|
|
34
|
+
const encoder = new TextEncoder();
|
|
35
|
+
const frames = events.map((e) => {
|
|
36
|
+
const type = e.type || 'message';
|
|
37
|
+
return `event: ${type}\ndata: ${JSON.stringify(e)}\n\n`;
|
|
38
|
+
});
|
|
39
|
+
const chunks = frames.map((f) => encoder.encode(f));
|
|
40
|
+
let i = 0;
|
|
41
|
+
return {
|
|
42
|
+
body: {
|
|
43
|
+
getReader() {
|
|
44
|
+
return {
|
|
45
|
+
read() {
|
|
46
|
+
if (i < chunks.length) return Promise.resolve({ done: false, value: chunks[i++] });
|
|
47
|
+
return Promise.resolve({ done: true, value: undefined });
|
|
48
|
+
},
|
|
49
|
+
cancel() { return Promise.resolve(); },
|
|
50
|
+
releaseLock() {},
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function compatResponsesEventStream(events) {
|
|
58
|
+
return {
|
|
59
|
+
async *[Symbol.asyncIterator]() {
|
|
60
|
+
for (const event of events) yield event;
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// === 1. openai-compat ======================================================
|
|
66
|
+
// Chat path: parseToolCalls(choice, label) openai-compat.mjs:957
|
|
67
|
+
// Responses path: parseResponsesToolCalls(response,...) openai-compat.mjs:972
|
|
68
|
+
// Both exported (added `export` keyword only).
|
|
69
|
+
|
|
70
|
+
test('openai-compat (chat): native tool_calls → canonical toolCalls', () => {
|
|
71
|
+
const choice = {
|
|
72
|
+
message: {
|
|
73
|
+
tool_calls: [{
|
|
74
|
+
id: 'call_1',
|
|
75
|
+
type: 'function',
|
|
76
|
+
function: { name: 'grep', arguments: '{"pattern":"x"}' },
|
|
77
|
+
}],
|
|
78
|
+
},
|
|
79
|
+
finish_reason: 'tool_calls',
|
|
80
|
+
};
|
|
81
|
+
const out = compatParseToolCalls(choice, 'test');
|
|
82
|
+
assert.deepEqual(out, [{ id: 'call_1', name: 'grep', arguments: { pattern: 'x' } }]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('openai-compat (chat): no tool_calls → undefined', () => {
|
|
86
|
+
assert.equal(compatParseToolCalls({ message: { content: 'hi' }, finish_reason: 'stop' }, 'test'), undefined);
|
|
87
|
+
assert.equal(compatParseToolCalls({ message: { tool_calls: [] } }, 'test'), undefined);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('openai-compat (responses): native function_call → canonical toolCalls', () => {
|
|
91
|
+
const response = {
|
|
92
|
+
status: 'completed',
|
|
93
|
+
output: [{
|
|
94
|
+
type: 'function_call',
|
|
95
|
+
call_id: 'fc_1',
|
|
96
|
+
name: 'read',
|
|
97
|
+
arguments: '{"path":"a"}',
|
|
98
|
+
}],
|
|
99
|
+
};
|
|
100
|
+
const out = compatParseResponsesToolCalls(response, 'test');
|
|
101
|
+
assert.deepEqual(out, [{ id: 'fc_1', name: 'read', arguments: { path: 'a' } }]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('openai-compat (responses): no function_call → undefined', () => {
|
|
105
|
+
assert.equal(compatParseResponsesToolCalls({ status: 'completed', output: [] }, 'test'), undefined);
|
|
106
|
+
assert.equal(compatParseResponsesToolCalls({ output: [{ type: 'message', content: [] }] }, 'test'), undefined);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Native convergence: a completed function tool_call whose arguments JSON is
|
|
110
|
+
// malformed must NOT make the provider layer throw or swallow to {}. The parse
|
|
111
|
+
// failure rides through on the call's `arguments` slot as an invalid-args
|
|
112
|
+
// marker so the dispatch loop can return an is_error tool_result and let the
|
|
113
|
+
// model self-correct in the same turn.
|
|
114
|
+
test('openai-compat (chat): malformed tool_calls args → invalid-args marker (provider does not throw)', () => {
|
|
115
|
+
const choice = {
|
|
116
|
+
message: {
|
|
117
|
+
tool_calls: [{
|
|
118
|
+
id: 'call_bad',
|
|
119
|
+
type: 'function',
|
|
120
|
+
function: { name: 'grep', arguments: '{"pattern": dispatchAiWrapped}' },
|
|
121
|
+
}],
|
|
122
|
+
},
|
|
123
|
+
finish_reason: 'tool_calls',
|
|
124
|
+
};
|
|
125
|
+
const out = compatParseToolCalls(choice, 'test');
|
|
126
|
+
assert.equal(Array.isArray(out), true);
|
|
127
|
+
assert.equal(out.length, 1);
|
|
128
|
+
assert.equal(out[0].id, 'call_bad');
|
|
129
|
+
assert.equal(out[0].name, 'grep');
|
|
130
|
+
assert.equal(isInvalidToolArgsMarker(out[0].arguments), true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('openai-compat (responses): malformed function_call args → invalid-args marker (no throw)', () => {
|
|
134
|
+
const response = {
|
|
135
|
+
status: 'completed',
|
|
136
|
+
output: [{
|
|
137
|
+
type: 'function_call',
|
|
138
|
+
call_id: 'fc_bad',
|
|
139
|
+
name: 'read',
|
|
140
|
+
arguments: '{path:}',
|
|
141
|
+
}],
|
|
142
|
+
};
|
|
143
|
+
const out = compatParseResponsesToolCalls(response, 'test');
|
|
144
|
+
assert.equal(Array.isArray(out), true);
|
|
145
|
+
assert.equal(out.length, 1);
|
|
146
|
+
assert.equal(out[0].id, 'fc_bad');
|
|
147
|
+
assert.equal(out[0].name, 'read');
|
|
148
|
+
assert.equal(isInvalidToolArgsMarker(out[0].arguments), true);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('openai-compat/xai Responses stream: response.completed salvages deferred function_call id/name', async () => {
|
|
152
|
+
const captured = [];
|
|
153
|
+
const out = await consumeCompatResponsesStream(compatResponsesEventStream([
|
|
154
|
+
{ type: 'response.created', response: { id: 'resp_1', model: 'grok' } },
|
|
155
|
+
{
|
|
156
|
+
type: 'response.function_call_arguments.done',
|
|
157
|
+
item_id: 'fc_item_1',
|
|
158
|
+
arguments: '{"path":"a"}',
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
type: 'response.completed',
|
|
162
|
+
response: {
|
|
163
|
+
id: 'resp_1',
|
|
164
|
+
model: 'grok',
|
|
165
|
+
status: 'completed',
|
|
166
|
+
output: [{
|
|
167
|
+
type: 'function_call',
|
|
168
|
+
id: 'fc_item_1',
|
|
169
|
+
call_id: 'fc_1',
|
|
170
|
+
name: 'read',
|
|
171
|
+
arguments: '{"path":"a"}',
|
|
172
|
+
}],
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
]), {
|
|
176
|
+
label: 'test',
|
|
177
|
+
parseResponsesToolCalls: compatParseResponsesToolCalls,
|
|
178
|
+
responseOutputText: () => '',
|
|
179
|
+
onToolCall: (call) => captured.push(call),
|
|
180
|
+
});
|
|
181
|
+
assert.deepEqual(out.toolCalls, [{ id: 'fc_1', name: 'read', arguments: { path: 'a' } }]);
|
|
182
|
+
assert.deepEqual(captured, [{ id: 'fc_1', name: 'read', arguments: { path: 'a' } }]);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('openai-compat/xai Responses: freeform apply_patch downgrades to function schema', () => {
|
|
186
|
+
const tools = _toResponsesToolsForTest(PATCH_TOOL_DEFS);
|
|
187
|
+
const patch = tools.find((tool) => tool.name === 'apply_patch');
|
|
188
|
+
assert.equal(patch.type, 'function');
|
|
189
|
+
assert.equal(patch.format, undefined);
|
|
190
|
+
assert.equal(patch.parameters?.properties?.patch?.type, 'string');
|
|
191
|
+
assert.deepEqual(patch.parameters?.required, ['patch']);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('openai-compat/xai Responses: custom_tool_call history replays as function_call', () => {
|
|
195
|
+
const rawPatch = '*** Begin Patch\n*** Add File: xai-history.txt\n+ok\n*** End Patch\n';
|
|
196
|
+
const { input } = _toXaiResponsesInputForTest([
|
|
197
|
+
{ role: 'user', content: 'patch please' },
|
|
198
|
+
{
|
|
199
|
+
role: 'assistant',
|
|
200
|
+
content: '',
|
|
201
|
+
toolCalls: [{ id: 'call_patch_1', name: 'apply_patch', arguments: { patch: rawPatch }, nativeType: 'custom_tool_call' }],
|
|
202
|
+
},
|
|
203
|
+
{ role: 'tool', toolCallId: 'call_patch_1', content: 'OK' },
|
|
204
|
+
], null, { model: 'grok-composer-2.5-fast' });
|
|
205
|
+
assert.equal(input.some((item) => item.type === 'custom_tool_call'), false);
|
|
206
|
+
assert.equal(input.some((item) => item.type === 'custom_tool_call_output'), false);
|
|
207
|
+
const call = input.find((item) => item.type === 'function_call' && item.name === 'apply_patch');
|
|
208
|
+
assert.equal(call.call_id, 'call_patch_1');
|
|
209
|
+
assert.deepEqual(JSON.parse(call.arguments), { patch: rawPatch });
|
|
210
|
+
const output = input.find((item) => item.type === 'function_call_output');
|
|
211
|
+
assert.equal(output.call_id, 'call_patch_1');
|
|
212
|
+
assert.equal(output.output, 'OK');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// === 2. gemini =============================================================
|
|
216
|
+
// parseToolCalls(parts) gemini.mjs:946 (exported — `export` keyword only).
|
|
217
|
+
// id is a content hash → assert the `gemini_` prefix, not the exact value.
|
|
218
|
+
|
|
219
|
+
test('gemini: native functionCall parts → canonical toolCalls (hashed id)', () => {
|
|
220
|
+
const parts = [{ functionCall: { name: 'read', args: { path: 'a' } } }];
|
|
221
|
+
const out = geminiParseToolCalls(parts);
|
|
222
|
+
assert.equal(Array.isArray(out), true);
|
|
223
|
+
assert.equal(out.length, 1);
|
|
224
|
+
assert.equal(out[0].name, 'read');
|
|
225
|
+
assert.deepEqual(out[0].arguments, { path: 'a' });
|
|
226
|
+
assert.match(out[0].id, /^gemini_/);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('gemini: no functionCall parts → undefined', () => {
|
|
230
|
+
assert.equal(geminiParseToolCalls([{ text: 'hello' }]), undefined);
|
|
231
|
+
assert.equal(geminiParseToolCalls([]), undefined);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// === 3. anthropic / anthropic-oauth ========================================
|
|
235
|
+
// tool_use block parser lives in anthropic-oauth.mjs:936 parseSSEStream
|
|
236
|
+
// (content_block_start/delta/stop → toolCalls.push). anthropic.mjs has NO
|
|
237
|
+
// independent tool_use parser: it imports and reuses the SAME parseSSEStream
|
|
238
|
+
// from anthropic-oauth.mjs (anthropic.mjs:12). So a single test covers both
|
|
239
|
+
// providers — shared parser, no duplicate test needed.
|
|
240
|
+
|
|
241
|
+
test('anthropic(-oauth): streamed tool_use block → canonical toolCalls', async () => {
|
|
242
|
+
const events = [
|
|
243
|
+
{ type: 'message_start', message: { model: 'claude', usage: { input_tokens: 1 } } },
|
|
244
|
+
{ type: 'content_block_start', index: 0, content_block: { type: 'tool_use', id: 'toolu_1', name: 'shell' } },
|
|
245
|
+
{ type: 'content_block_delta', index: 0, delta: { type: 'input_json_delta', partial_json: '{"command":"ls"}' } },
|
|
246
|
+
{ type: 'content_block_stop', index: 0 },
|
|
247
|
+
{ type: 'message_delta', delta: { stop_reason: 'tool_use' }, usage: { output_tokens: 1 } },
|
|
248
|
+
{ type: 'message_stop' },
|
|
249
|
+
];
|
|
250
|
+
const captured = [];
|
|
251
|
+
const result = await anthropicParseSSEStream(
|
|
252
|
+
anthropicSseResponse(events),
|
|
253
|
+
null, // signal
|
|
254
|
+
() => {}, // abortStream
|
|
255
|
+
() => {}, // onStreamDelta
|
|
256
|
+
(call) => captured.push(call), // onToolCall
|
|
257
|
+
{}, // state
|
|
258
|
+
null, // onTextDelta
|
|
259
|
+
);
|
|
260
|
+
assert.deepEqual(result.toolCalls, [{ id: 'toolu_1', name: 'shell', arguments: { command: 'ls' } }]);
|
|
261
|
+
// Eager dispatch fired the same call exactly once.
|
|
262
|
+
assert.deepEqual(captured, [{ id: 'toolu_1', name: 'shell', arguments: { command: 'ls' } }]);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test('anthropic(-oauth): malformed streamed tool_use args → invalid-args marker, not {} dispatch', async () => {
|
|
266
|
+
const events = [
|
|
267
|
+
{ type: 'message_start', message: { model: 'claude', usage: { input_tokens: 1 } } },
|
|
268
|
+
{ type: 'content_block_start', index: 0, content_block: { type: 'tool_use', id: 'toolu_bad', name: 'shell' } },
|
|
269
|
+
{ type: 'content_block_delta', index: 0, delta: { type: 'input_json_delta', partial_json: '{"command": dispatchAiWrapped}' } },
|
|
270
|
+
{ type: 'content_block_stop', index: 0 },
|
|
271
|
+
{ type: 'message_delta', delta: { stop_reason: 'tool_use' }, usage: { output_tokens: 1 } },
|
|
272
|
+
{ type: 'message_stop' },
|
|
273
|
+
];
|
|
274
|
+
const captured = [];
|
|
275
|
+
const result = await anthropicParseSSEStream(
|
|
276
|
+
anthropicSseResponse(events),
|
|
277
|
+
null,
|
|
278
|
+
() => {},
|
|
279
|
+
() => {},
|
|
280
|
+
(call) => captured.push(call),
|
|
281
|
+
{},
|
|
282
|
+
null,
|
|
283
|
+
);
|
|
284
|
+
assert.equal(isInvalidToolArgsMarker(result.toolCalls[0].arguments), true);
|
|
285
|
+
assert.equal(result.toolCalls[0].arguments.__rawArguments, '{"command": dispatchAiWrapped}');
|
|
286
|
+
assert.equal(isInvalidToolArgsMarker(captured[0].arguments), true);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test('anthropic(-oauth): text-only stream → no toolCalls', async () => {
|
|
290
|
+
const events = [
|
|
291
|
+
{ type: 'message_start', message: { model: 'claude', usage: { input_tokens: 1 } } },
|
|
292
|
+
{ type: 'content_block_start', index: 0, content_block: { type: 'text' } },
|
|
293
|
+
{ type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: 'hello' } },
|
|
294
|
+
{ type: 'content_block_stop', index: 0 },
|
|
295
|
+
{ type: 'message_delta', delta: { stop_reason: 'end_turn' }, usage: { output_tokens: 1 } },
|
|
296
|
+
{ type: 'message_stop' },
|
|
297
|
+
];
|
|
298
|
+
const result = await anthropicParseSSEStream(
|
|
299
|
+
anthropicSseResponse(events),
|
|
300
|
+
null, () => {}, () => {}, () => {}, {}, null,
|
|
301
|
+
);
|
|
302
|
+
assert.equal(result.toolCalls, undefined);
|
|
303
|
+
assert.equal(result.content, 'hello');
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// === 4. openai-oauth / openai-oauth-ws =====================================
|
|
307
|
+
// openai-oauth (HTTP/SSE) and openai-oauth-ws (WebSocket) both consume the
|
|
308
|
+
// Responses event stream inside large stateful stream loops, NOT a standalone
|
|
309
|
+
// parser. The HTTP path's handleEvent is a private closure inside
|
|
310
|
+
// sendViaHttpSse (openai-oauth.mjs:1038) and cannot be exported without
|
|
311
|
+
// extracting it (forbidden: no logic change). The WS path's _streamResponse
|
|
312
|
+
// (openai-oauth-ws.mjs:1190) IS exported but requires a live `entry.socket`
|
|
313
|
+
// EventEmitter and resolves only on response.completed — driving it needs a
|
|
314
|
+
// full fake-socket test rig, well beyond "inject synthetic input to a parser".
|
|
315
|
+
//
|
|
316
|
+
// Their canonical Responses function_call shape (call_id/name/arguments) and
|
|
317
|
+
// custom_tool_call handling are the SAME wire contract already asserted via
|
|
318
|
+
// openai-compat's parseResponsesToolCalls above, and the shared
|
|
319
|
+
// customToolCallFromResponseItem helper (custom-tool-wire.mjs) is imported by
|
|
320
|
+
// all three. We add a focused unit test for that shared custom-tool helper so
|
|
321
|
+
// the OAuth custom_tool_call extraction path has explicit coverage; the
|
|
322
|
+
// function_call path is covered by the openai-compat Responses test.
|
|
323
|
+
|
|
324
|
+
import { customToolCallFromResponseItem } from '../src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs';
|
|
325
|
+
import { parseToolSearchArgs } from '../src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs';
|
|
326
|
+
|
|
327
|
+
test('openai-oauth (shared custom-tool-wire): custom_tool_call item → canonical call', () => {
|
|
328
|
+
const item = { type: 'custom_tool_call', call_id: 'ctc_1', name: 'apply_patch', input: '*** patch ***' };
|
|
329
|
+
const call = customToolCallFromResponseItem(item);
|
|
330
|
+
assert.equal(call.id, 'ctc_1');
|
|
331
|
+
assert.equal(call.name, 'apply_patch');
|
|
332
|
+
assert.deepEqual(call.arguments, { patch: '*** patch ***' });
|
|
333
|
+
assert.equal(call.nativeType, 'custom_tool_call');
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test('openai-oauth (shared custom-tool-wire): non custom_tool_call → null', () => {
|
|
337
|
+
assert.equal(customToolCallFromResponseItem({ type: 'function_call' }), null);
|
|
338
|
+
assert.equal(customToolCallFromResponseItem(null), null);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// WS tool_search_call.arguments parse policy (parseToolSearchArgs, module-scope
|
|
342
|
+
// export of openai-oauth-ws.mjs). Native convergence: malformed non-empty JSON
|
|
343
|
+
// becomes an invalid-args marker so the dispatch loop blocks execution and
|
|
344
|
+
// returns an is_error tool_result — NOT a silent {} that would dispatch
|
|
345
|
+
// tool_search with empty arguments. Empty/whitespace/object inputs keep their
|
|
346
|
+
// prior, correct behavior.
|
|
347
|
+
test('openai-oauth-ws (tool_search): malformed args string → invalid-args marker (not {})', () => {
|
|
348
|
+
const out = parseToolSearchArgs('{"query": dispatchAiWrapped}');
|
|
349
|
+
assert.equal(isInvalidToolArgsMarker(out), true);
|
|
350
|
+
assert.equal(out.__invalidToolArgs, true);
|
|
351
|
+
assert.equal(out.__rawArguments, '{"query": dispatchAiWrapped}');
|
|
352
|
+
assert.equal(typeof out.__parseError, 'string');
|
|
353
|
+
assert.ok(out.__parseError.length > 0);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test('openai-oauth-ws (tool_search): valid args / object / empty preserved', () => {
|
|
357
|
+
// valid JSON string → parsed object
|
|
358
|
+
assert.deepEqual(parseToolSearchArgs('{"query":"x"}'), { query: 'x' });
|
|
359
|
+
// already an object → passthrough
|
|
360
|
+
const obj = { query: 'y' };
|
|
361
|
+
assert.equal(parseToolSearchArgs(obj), obj);
|
|
362
|
+
// empty / whitespace / null / non-string → {} (no args, not a marker)
|
|
363
|
+
assert.deepEqual(parseToolSearchArgs(''), {});
|
|
364
|
+
assert.deepEqual(parseToolSearchArgs(' '), {});
|
|
365
|
+
assert.deepEqual(parseToolSearchArgs(null), {});
|
|
366
|
+
assert.deepEqual(parseToolSearchArgs(undefined), {});
|
|
367
|
+
assert.equal(isInvalidToolArgsMarker(parseToolSearchArgs('')), false);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// === 5. grok-oauth =========================================================
|
|
371
|
+
// grok-oauth has NO independent tool_call parser. GrokOAuthProvider delegates
|
|
372
|
+
// all request shaping AND response parsing to an inner OpenAICompatProvider
|
|
373
|
+
// constructed as `new OpenAICompatProvider('xai', ...)` (grok-oauth.mjs:668).
|
|
374
|
+
// Its tool_call extraction therefore goes through the exact
|
|
375
|
+
// parseToolCalls / parseResponsesToolCalls already asserted in block 1 — no
|
|
376
|
+
// duplicate test. (Documented as shared in the report.)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdtempSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { estimateMessagesTokens } from '../src/runtime/agent/orchestrator/session/context-utils.mjs';
|
|
6
|
+
import { SUMMARY_PREFIX } from '../src/runtime/agent/orchestrator/session/compact.mjs';
|
|
7
|
+
|
|
8
|
+
process.env.MIXDOG_AGENT_TRACE_DISABLE = '1';
|
|
9
|
+
process.env.MIXDOG_DATA_DIR = mkdtempSync(join(tmpdir(), 'mixdog-reactive-compact-persist-'));
|
|
10
|
+
|
|
11
|
+
function assert(condition, message) {
|
|
12
|
+
if (!condition) throw new Error(message);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { initProviders, getProvider } = await import('../src/runtime/agent/orchestrator/providers/registry.mjs');
|
|
16
|
+
const { saveSessionAsync, loadSession } = await import('../src/runtime/agent/orchestrator/session/store.mjs');
|
|
17
|
+
|
|
18
|
+
await initProviders({ 'openai-oauth': { enabled: true } });
|
|
19
|
+
const provider = getProvider('openai-oauth');
|
|
20
|
+
if (!provider || typeof provider.send !== 'function') {
|
|
21
|
+
process.stdout.write('reactive-compact-persist smoke skipped (openai-oauth unavailable)\n');
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let mainSendCount = 0;
|
|
26
|
+
let compactSendCount = 0;
|
|
27
|
+
provider.send = async (_sentMessages, _model, _tools, opts = {}) => {
|
|
28
|
+
if (String(opts?.sessionId || '').endsWith(':compact')) {
|
|
29
|
+
compactSendCount += 1;
|
|
30
|
+
return {
|
|
31
|
+
content: [
|
|
32
|
+
'## Goal',
|
|
33
|
+
'- continue reactive persist smoke',
|
|
34
|
+
'',
|
|
35
|
+
'## Constraints & Preferences',
|
|
36
|
+
'- (none)',
|
|
37
|
+
'',
|
|
38
|
+
'## Progress',
|
|
39
|
+
'### Done',
|
|
40
|
+
'- older turn summarized',
|
|
41
|
+
'',
|
|
42
|
+
'### In Progress',
|
|
43
|
+
'- (none)',
|
|
44
|
+
'',
|
|
45
|
+
'### Blocked',
|
|
46
|
+
'- (none)',
|
|
47
|
+
'',
|
|
48
|
+
'## Key Decisions',
|
|
49
|
+
'- (none)',
|
|
50
|
+
'',
|
|
51
|
+
'## Next Steps',
|
|
52
|
+
'- continue',
|
|
53
|
+
'',
|
|
54
|
+
'## Critical Context',
|
|
55
|
+
'- manager.mjs',
|
|
56
|
+
'',
|
|
57
|
+
'## Relevant Files',
|
|
58
|
+
'- src/runtime/agent/orchestrator/session/manager.mjs: askSession overflow persist',
|
|
59
|
+
].join('\n'),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
mainSendCount += 1;
|
|
63
|
+
throw new Error('input tokens exceeds the context window');
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const messages = [{ role: 'system', content: 'system rules stay mandatory' }];
|
|
67
|
+
let overflowIndex = 0;
|
|
68
|
+
while (estimateMessagesTokens(messages) + 512 < 8_800) {
|
|
69
|
+
messages.push({ role: 'user', content: `older overflow request ${overflowIndex}: ${'important detail '.repeat(90)}` });
|
|
70
|
+
messages.push({ role: 'assistant', content: `older overflow answer ${overflowIndex}: ${'implementation note '.repeat(90)}` });
|
|
71
|
+
overflowIndex += 1;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const sessionId = `sess_reactive_compact_persist_${process.pid}_${Date.now()}`;
|
|
75
|
+
const session = {
|
|
76
|
+
id: sessionId,
|
|
77
|
+
provider: 'openai-oauth',
|
|
78
|
+
model: 'reactive-compact-persist-smoke',
|
|
79
|
+
messages,
|
|
80
|
+
tools: [],
|
|
81
|
+
generation: 0,
|
|
82
|
+
closed: false,
|
|
83
|
+
contextWindow: 12_000,
|
|
84
|
+
rawContextWindow: 12_000,
|
|
85
|
+
compactBoundaryTokens: 12_000,
|
|
86
|
+
compaction: { auto: true, semantic: true, type: 1, compactType: 1, lastStage: 'compacting' },
|
|
87
|
+
cwd: process.cwd(),
|
|
88
|
+
sessionStartMetaInjected: true,
|
|
89
|
+
providerState: { xaiResponses: { previousResponseId: 'stale-after-compact' } },
|
|
90
|
+
};
|
|
91
|
+
await saveSessionAsync(session, { expectedGeneration: 0 });
|
|
92
|
+
const beforeCount = messages.length;
|
|
93
|
+
|
|
94
|
+
const { askSession } = await import('../src/runtime/agent/orchestrator/session/manager.mjs');
|
|
95
|
+
|
|
96
|
+
let threw = false;
|
|
97
|
+
let thrownCode = null;
|
|
98
|
+
try {
|
|
99
|
+
await askSession(sessionId, 'current overflow task must stay verbatim', null, null, process.cwd());
|
|
100
|
+
} catch (err) {
|
|
101
|
+
threw = true;
|
|
102
|
+
thrownCode = err?.code || null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
assert(threw, 'askSession should surface overflow error');
|
|
106
|
+
assert(thrownCode === 'AGENT_CONTEXT_OVERFLOW', `expected AGENT_CONTEXT_OVERFLOW, got ${thrownCode}`);
|
|
107
|
+
assert(mainSendCount === 2, `expected 2 main sends after reactive retry, got ${mainSendCount}`);
|
|
108
|
+
assert(compactSendCount === 1, `expected 1 compact send, got ${compactSendCount}`);
|
|
109
|
+
|
|
110
|
+
const reloaded = loadSession(sessionId);
|
|
111
|
+
assert(reloaded, 'session should reload from store after overflow');
|
|
112
|
+
assert((reloaded.messages || []).length < beforeCount + 1, 'persisted transcript should reflect compaction shrink');
|
|
113
|
+
const summary = (reloaded.messages || []).find(
|
|
114
|
+
(m) => m?.role === 'user' && typeof m.content === 'string' && m.content.startsWith(SUMMARY_PREFIX),
|
|
115
|
+
);
|
|
116
|
+
assert(summary, 'compacted transcript with SUMMARY_PREFIX should persist after overflow error');
|
|
117
|
+
assert(
|
|
118
|
+
(reloaded.messages || []).some((m) => m?.role === 'user' && String(m.content).includes('current overflow task must stay verbatim')),
|
|
119
|
+
'current user turn should remain in persisted transcript',
|
|
120
|
+
);
|
|
121
|
+
assert(reloaded.compaction?.lastStage !== 'compacting', 'compaction lastStage should not remain stuck on compacting');
|
|
122
|
+
assert(reloaded.providerState === undefined, 'providerState should clear after reactive compact persist');
|
|
123
|
+
|
|
124
|
+
process.stdout.write('reactive-compact-persist smoke passed ✓\n');
|