@esankhan3/anvil-agent-core 0.3.0 → 0.4.0
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 +1 -1
- package/dist/__tests__/gemini-adapter-recording.test.d.ts +11 -0
- package/dist/__tests__/gemini-adapter-recording.test.d.ts.map +1 -0
- package/dist/__tests__/gemini-adapter-recording.test.js +108 -0
- package/dist/__tests__/gemini-adapter-recording.test.js.map +1 -0
- package/dist/__tests__/legacy-adapter-language-model.test.d.ts +11 -0
- package/dist/__tests__/legacy-adapter-language-model.test.d.ts.map +1 -0
- package/dist/__tests__/legacy-adapter-language-model.test.js +102 -0
- package/dist/__tests__/legacy-adapter-language-model.test.js.map +1 -0
- package/dist/__tests__/ollama-adapter-recording.test.d.ts +15 -0
- package/dist/__tests__/ollama-adapter-recording.test.d.ts.map +1 -0
- package/dist/__tests__/ollama-adapter-recording.test.js +128 -0
- package/dist/__tests__/ollama-adapter-recording.test.js.map +1 -0
- package/dist/__tests__/openrouter-prefill.integration.test.d.ts +19 -0
- package/dist/__tests__/openrouter-prefill.integration.test.d.ts.map +1 -0
- package/dist/__tests__/openrouter-prefill.integration.test.js +410 -0
- package/dist/__tests__/openrouter-prefill.integration.test.js.map +1 -0
- package/dist/__tests__/prefill-truncate.test.d.ts +8 -0
- package/dist/__tests__/prefill-truncate.test.d.ts.map +1 -0
- package/dist/__tests__/prefill-truncate.test.js +75 -0
- package/dist/__tests__/prefill-truncate.test.js.map +1 -0
- package/dist/__tests__/router-circuit-breaker.test.js +1 -1
- package/dist/__tests__/router-circuit-breaker.test.js.map +1 -1
- package/dist/__tests__/router-classify-unified.test.d.ts +12 -0
- package/dist/__tests__/router-classify-unified.test.d.ts.map +1 -0
- package/dist/__tests__/router-classify-unified.test.js +108 -0
- package/dist/__tests__/router-classify-unified.test.js.map +1 -0
- package/dist/__tests__/router-fallback.test.js +1 -1
- package/dist/__tests__/router-fallback.test.js.map +1 -1
- package/dist/__tests__/router-model-registry.test.js +24 -0
- package/dist/__tests__/router-model-registry.test.js.map +1 -1
- package/dist/__tests__/router-retry.test.js +3 -5
- package/dist/__tests__/router-retry.test.js.map +1 -1
- package/dist/__tests__/router-runagent.test.d.ts +14 -0
- package/dist/__tests__/router-runagent.test.d.ts.map +1 -0
- package/dist/__tests__/router-runagent.test.js +95 -0
- package/dist/__tests__/router-runagent.test.js.map +1 -0
- package/dist/__tests__/router-scaffold.test.js +2 -1
- package/dist/__tests__/router-scaffold.test.js.map +1 -1
- package/dist/__tests__/router-spend-ledger.test.js +1 -1
- package/dist/__tests__/router-spend-ledger.test.js.map +1 -1
- package/dist/__tests__/router-stream-fallback.test.d.ts +13 -0
- package/dist/__tests__/router-stream-fallback.test.d.ts.map +1 -0
- package/dist/__tests__/router-stream-fallback.test.js +129 -0
- package/dist/__tests__/router-stream-fallback.test.js.map +1 -0
- package/dist/__tests__/router-telemetry.test.js +3 -1
- package/dist/__tests__/router-telemetry.test.js.map +1 -1
- package/dist/__tests__/util/mock-upstream.d.ts +77 -0
- package/dist/__tests__/util/mock-upstream.d.ts.map +1 -0
- package/dist/__tests__/util/mock-upstream.js +219 -0
- package/dist/__tests__/util/mock-upstream.js.map +1 -0
- package/dist/adk-adapter.d.ts.map +1 -1
- package/dist/adk-adapter.js +51 -0
- package/dist/adk-adapter.js.map +1 -1
- package/dist/agent/session/adapter.d.ts +15 -0
- package/dist/agent/session/adapter.d.ts.map +1 -1
- package/dist/agent/session/adapter.js +3 -0
- package/dist/agent/session/adapter.js.map +1 -1
- package/dist/agent/session/index.d.ts +1 -0
- package/dist/agent/session/index.d.ts.map +1 -1
- package/dist/agent/session/index.js +1 -0
- package/dist/agent/session/index.js.map +1 -1
- package/dist/agent/session/language-model-bridge.d.ts.map +1 -1
- package/dist/agent/session/language-model-bridge.js +22 -0
- package/dist/agent/session/language-model-bridge.js.map +1 -1
- package/dist/agent/session/legacy-adapter-language-model.d.ts +27 -0
- package/dist/agent/session/legacy-adapter-language-model.d.ts.map +1 -0
- package/dist/agent/session/legacy-adapter-language-model.js +221 -0
- package/dist/agent/session/legacy-adapter-language-model.js.map +1 -0
- package/dist/agent/session/types.d.ts +20 -0
- package/dist/agent/session/types.d.ts.map +1 -1
- package/dist/agent/session/types.js.map +1 -1
- package/dist/claude-adapter.d.ts.map +1 -1
- package/dist/claude-adapter.js +27 -0
- package/dist/claude-adapter.js.map +1 -1
- package/dist/gemini-adapter.d.ts.map +1 -1
- package/dist/gemini-adapter.js +46 -0
- package/dist/gemini-adapter.js.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/ollama-adapter.d.ts.map +1 -1
- package/dist/ollama-adapter.js +118 -8
- package/dist/ollama-adapter.js.map +1 -1
- package/dist/openrouter-adapter.d.ts.map +1 -1
- package/dist/openrouter-adapter.js +289 -15
- package/dist/openrouter-adapter.js.map +1 -1
- package/dist/prefill/strip.d.ts +49 -0
- package/dist/prefill/strip.d.ts.map +1 -0
- package/dist/prefill/strip.js +93 -0
- package/dist/prefill/strip.js.map +1 -0
- package/dist/prefill/translate.d.ts +55 -0
- package/dist/prefill/translate.d.ts.map +1 -0
- package/dist/prefill/translate.js +162 -0
- package/dist/prefill/translate.js.map +1 -0
- package/dist/prefill/truncate.d.ts +62 -0
- package/dist/prefill/truncate.d.ts.map +1 -0
- package/dist/prefill/truncate.js +77 -0
- package/dist/prefill/truncate.js.map +1 -0
- package/dist/router/errors.d.ts +24 -7
- package/dist/router/errors.d.ts.map +1 -1
- package/dist/router/errors.js +71 -14
- package/dist/router/errors.js.map +1 -1
- package/dist/router/index.d.ts +3 -2
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +2 -1
- package/dist/router/index.js.map +1 -1
- package/dist/router/model-registry.d.ts +22 -0
- package/dist/router/model-registry.d.ts.map +1 -1
- package/dist/router/model-registry.js +64 -1
- package/dist/router/model-registry.js.map +1 -1
- package/dist/router/provider-registry-resolver.d.ts +25 -0
- package/dist/router/provider-registry-resolver.d.ts.map +1 -0
- package/dist/router/provider-registry-resolver.js +84 -0
- package/dist/router/provider-registry-resolver.js.map +1 -0
- package/dist/router/retry.d.ts +3 -0
- package/dist/router/retry.d.ts.map +1 -1
- package/dist/router/retry.js +4 -1
- package/dist/router/retry.js.map +1 -1
- package/dist/router/router.d.ts +79 -1
- package/dist/router/router.d.ts.map +1 -1
- package/dist/router/router.js +256 -2
- package/dist/router/router.js.map +1 -1
- package/dist/router/types.d.ts +1 -1
- package/dist/router/types.d.ts.map +1 -1
- package/dist/router/types.js +1 -0
- package/dist/router/types.js.map +1 -1
- package/dist/turn-recorder/__tests__/turn-recorder.test.d.ts +20 -0
- package/dist/turn-recorder/__tests__/turn-recorder.test.d.ts.map +1 -0
- package/dist/turn-recorder/__tests__/turn-recorder.test.js +282 -0
- package/dist/turn-recorder/__tests__/turn-recorder.test.js.map +1 -0
- package/dist/turn-recorder/hash.d.ts +10 -0
- package/dist/turn-recorder/hash.d.ts.map +1 -0
- package/dist/turn-recorder/hash.js +35 -0
- package/dist/turn-recorder/hash.js.map +1 -0
- package/dist/turn-recorder/index.d.ts +119 -0
- package/dist/turn-recorder/index.d.ts.map +1 -0
- package/dist/turn-recorder/index.js +326 -0
- package/dist/turn-recorder/index.js.map +1 -0
- package/dist/turn-recorder/types.d.ts +227 -0
- package/dist/turn-recorder/types.d.ts.map +1 -0
- package/dist/turn-recorder/types.js +20 -0
- package/dist/turn-recorder/types.js.map +1 -0
- package/dist/types.d.ts +35 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -106,7 +106,7 @@ All adapters share the same Anvil Stream Format (NDJSON), the same
|
|
|
106
106
|
↑
|
|
107
107
|
ProviderRegistry
|
|
108
108
|
↑
|
|
109
|
-
LlmRouter (retries, fallbacks, rate limit)
|
|
109
|
+
LlmRouter (runAgent/invoke: backoff retries, fallbacks, circuit breaker, rate limit)
|
|
110
110
|
↑
|
|
111
111
|
Telemetry (OTel spans + cost)
|
|
112
112
|
↑
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §H4 — cross-vendor turn recording for the Gemini single-shot adapter.
|
|
3
|
+
*
|
|
4
|
+
* Verifies the adapter (a) records `turn:0:assistant-start` with the
|
|
5
|
+
* user prompt and `turn:0:assistant-end` with the streamed text on the
|
|
6
|
+
* live path, and (b) honors a recorded turn on replay by skipping the
|
|
7
|
+
* upstream fetch entirely. Mocks `globalThis.fetch` with a fake Gemini
|
|
8
|
+
* SSE stream; the recorder runs over a test-double EffectRuntime.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=gemini-adapter-recording.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gemini-adapter-recording.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/gemini-adapter-recording.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §H4 — cross-vendor turn recording for the Gemini single-shot adapter.
|
|
3
|
+
*
|
|
4
|
+
* Verifies the adapter (a) records `turn:0:assistant-start` with the
|
|
5
|
+
* user prompt and `turn:0:assistant-end` with the streamed text on the
|
|
6
|
+
* live path, and (b) honors a recorded turn on replay by skipping the
|
|
7
|
+
* upstream fetch entirely. Mocks `globalThis.fetch` with a fake Gemini
|
|
8
|
+
* SSE stream; the recorder runs over a test-double EffectRuntime.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, before, after } from 'node:test';
|
|
11
|
+
import assert from 'node:assert/strict';
|
|
12
|
+
import { GeminiAdapter } from '../gemini-adapter.js';
|
|
13
|
+
import { TurnRecorder } from '../turn-recorder/index.js';
|
|
14
|
+
function geminiSse(text) {
|
|
15
|
+
const body = `data: ${JSON.stringify({ candidates: [{ content: { parts: [{ text }] } }] })}\n` +
|
|
16
|
+
`data: ${JSON.stringify({
|
|
17
|
+
candidates: [{ content: { parts: [{ text: '' }] } }],
|
|
18
|
+
usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 5 },
|
|
19
|
+
})}\n`;
|
|
20
|
+
return new Response(body, { status: 200, headers: { 'Content-Type': 'text/event-stream' } });
|
|
21
|
+
}
|
|
22
|
+
class StubWritable {
|
|
23
|
+
chunks = [];
|
|
24
|
+
write(chunk) {
|
|
25
|
+
this.chunks.push(typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk));
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
end() { }
|
|
29
|
+
on() { }
|
|
30
|
+
}
|
|
31
|
+
/** EffectRuntime double; `seed[name]` makes peekRecorded return a replay. */
|
|
32
|
+
function fakeRuntime(seed = {}) {
|
|
33
|
+
const log = [];
|
|
34
|
+
const runtime = {
|
|
35
|
+
async effect(name, fn, opts) {
|
|
36
|
+
if (name in seed) {
|
|
37
|
+
log.push({ name, opts, payload: seed[name] });
|
|
38
|
+
return seed[name];
|
|
39
|
+
}
|
|
40
|
+
const payload = await fn();
|
|
41
|
+
log.push({ name, opts, payload });
|
|
42
|
+
return payload;
|
|
43
|
+
},
|
|
44
|
+
peekRecorded(name) {
|
|
45
|
+
return (name in seed ? seed[name] : undefined);
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
return { runtime, log };
|
|
49
|
+
}
|
|
50
|
+
const baseConfig = {
|
|
51
|
+
userPrompt: 'summarise the repo',
|
|
52
|
+
model: 'gemini-2.5-flash',
|
|
53
|
+
workingDir: '/tmp',
|
|
54
|
+
stage: 'build',
|
|
55
|
+
persona: 'engineer',
|
|
56
|
+
sessionId: 'run-gem',
|
|
57
|
+
};
|
|
58
|
+
let originalFetch;
|
|
59
|
+
let originalKey;
|
|
60
|
+
before(() => {
|
|
61
|
+
originalFetch = globalThis.fetch;
|
|
62
|
+
originalKey = process.env.GEMINI_API_KEY;
|
|
63
|
+
process.env.GEMINI_API_KEY = 'test-key';
|
|
64
|
+
});
|
|
65
|
+
after(() => {
|
|
66
|
+
globalThis.fetch = originalFetch;
|
|
67
|
+
if (originalKey === undefined)
|
|
68
|
+
delete process.env.GEMINI_API_KEY;
|
|
69
|
+
else
|
|
70
|
+
process.env.GEMINI_API_KEY = originalKey;
|
|
71
|
+
});
|
|
72
|
+
describe('GeminiAdapter — H4 turn recording', () => {
|
|
73
|
+
it('records assistant-start (with userPrompt) and assistant-end (with text) on the live path', async () => {
|
|
74
|
+
globalThis.fetch = (async () => geminiSse('hello from gemini'));
|
|
75
|
+
const { runtime, log } = fakeRuntime();
|
|
76
|
+
const recorder = new TurnRecorder({ runtime, partialSink: () => { }, runId: 'run-gem', stepId: 'build' });
|
|
77
|
+
const r = await new GeminiAdapter().run({ ...baseConfig, turnRecorder: recorder }, new StubWritable());
|
|
78
|
+
assert.equal(r.output, 'hello from gemini');
|
|
79
|
+
const start = log.find((e) => e.name === 'turn:0:assistant-start');
|
|
80
|
+
assert.ok(start, 'assistant-start must be recorded');
|
|
81
|
+
assert.equal(start.payload.userPrompt, 'summarise the repo');
|
|
82
|
+
const end = log.find((e) => e.name === 'turn:0:assistant-end');
|
|
83
|
+
assert.ok(end, 'assistant-end must be recorded');
|
|
84
|
+
assert.equal(end.payload.text, 'hello from gemini');
|
|
85
|
+
assert.equal(end.payload.provider, 'gemini');
|
|
86
|
+
});
|
|
87
|
+
it('skips the upstream fetch on replay and returns the recorded turn', async () => {
|
|
88
|
+
let fetched = false;
|
|
89
|
+
globalThis.fetch = (async () => { fetched = true; return geminiSse('SHOULD NOT RUN'); });
|
|
90
|
+
// Seed a recorded assistant-end → startTurn returns `replayed`.
|
|
91
|
+
const { runtime } = fakeRuntime({
|
|
92
|
+
'turn:0:assistant-end': {
|
|
93
|
+
text: 'recorded answer',
|
|
94
|
+
stopReason: 'end_turn',
|
|
95
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
96
|
+
provenance: { segments: [{ model: 'gemini-2.5-flash', provider: 'gemini', range: [0, 15], source: 'live' }] },
|
|
97
|
+
historyDelta: [],
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
const recorder = new TurnRecorder({ runtime, partialSink: () => { }, runId: 'run-gem', stepId: 'build' });
|
|
101
|
+
const r = await new GeminiAdapter().run({ ...baseConfig, turnRecorder: recorder }, new StubWritable());
|
|
102
|
+
assert.equal(fetched, false, 'replay must NOT hit the upstream');
|
|
103
|
+
assert.equal(r.output, 'recorded answer');
|
|
104
|
+
assert.equal(r.inputTokens, 10);
|
|
105
|
+
assert.equal(r.outputTokens, 5);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
//# sourceMappingURL=gemini-adapter-recording.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gemini-adapter-recording.test.js","sourceRoot":"","sources":["../../src/__tests__/gemini-adapter-recording.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 1 — `legacyAdapterToLanguageModel` shim.
|
|
3
|
+
*
|
|
4
|
+
* A fake `ModelAdapter` writes known Anvil Stream Format NDJSON to the sink
|
|
5
|
+
* and resolves a `ModelAdapterResult`. We assert the shim turns that push
|
|
6
|
+
* stream into the pull `LanguageModel` surface correctly: `invokeStream`
|
|
7
|
+
* yields the right `StreamEvent`s, `invoke` assembles the `InvokeResult`, and
|
|
8
|
+
* a rejected `run()` surfaces as a throw the unified classifier understands.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=legacy-adapter-language-model.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"legacy-adapter-language-model.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/legacy-adapter-language-model.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 1 — `legacyAdapterToLanguageModel` shim.
|
|
3
|
+
*
|
|
4
|
+
* A fake `ModelAdapter` writes known Anvil Stream Format NDJSON to the sink
|
|
5
|
+
* and resolves a `ModelAdapterResult`. We assert the shim turns that push
|
|
6
|
+
* stream into the pull `LanguageModel` surface correctly: `invokeStream`
|
|
7
|
+
* yields the right `StreamEvent`s, `invoke` assembles the `InvokeResult`, and
|
|
8
|
+
* a rejected `run()` surfaces as a throw the unified classifier understands.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it } from 'node:test';
|
|
11
|
+
import assert from 'node:assert/strict';
|
|
12
|
+
import { legacyAdapterToLanguageModel } from '../agent/session/legacy-adapter-language-model.js';
|
|
13
|
+
import { emitContent, emitToolUse } from '../stream-format.js';
|
|
14
|
+
import { UpstreamError } from '../upstream-error.js';
|
|
15
|
+
import { classifyError } from '../router/errors.js';
|
|
16
|
+
class FakeAdapter {
|
|
17
|
+
script;
|
|
18
|
+
provider = 'openrouter';
|
|
19
|
+
capabilities = { streaming: true };
|
|
20
|
+
constructor(script) {
|
|
21
|
+
this.script = script;
|
|
22
|
+
}
|
|
23
|
+
supportsModel() { return true; }
|
|
24
|
+
getModelPricing() { return null; }
|
|
25
|
+
async checkAvailability() { return { available: true }; }
|
|
26
|
+
run(_config, output) {
|
|
27
|
+
return this.script(output);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const OPTS = {
|
|
31
|
+
model: 'some-model',
|
|
32
|
+
messages: [{ role: 'user', content: 'hi' }],
|
|
33
|
+
};
|
|
34
|
+
function fakeResult(over) {
|
|
35
|
+
return {
|
|
36
|
+
output: '', inputTokens: 0, outputTokens: 0, costUsd: 0, durationMs: 0,
|
|
37
|
+
provider: 'openrouter', model: 'some-model', ...over,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async function collect(stream) {
|
|
41
|
+
const out = [];
|
|
42
|
+
for await (const ev of stream)
|
|
43
|
+
out.push(ev);
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
describe('legacyAdapterToLanguageModel — invoke()', () => {
|
|
47
|
+
it('assembles InvokeResult from streamed text + resolved result', async () => {
|
|
48
|
+
const adapter = new FakeAdapter(async (out) => {
|
|
49
|
+
emitContent(out, 'Hello ');
|
|
50
|
+
emitContent(out, 'world');
|
|
51
|
+
return fakeResult({ output: 'Hello world', inputTokens: 10, outputTokens: 5, costUsd: 0.001, durationMs: 120, stopReason: 'end_turn' });
|
|
52
|
+
});
|
|
53
|
+
const lm = legacyAdapterToLanguageModel(adapter);
|
|
54
|
+
const res = await lm.invoke(OPTS);
|
|
55
|
+
assert.equal(res.text, 'Hello world');
|
|
56
|
+
assert.equal(res.usage.inputTokens, 10);
|
|
57
|
+
assert.equal(res.usage.outputTokens, 5);
|
|
58
|
+
assert.equal(res.costUsd, 0.001);
|
|
59
|
+
assert.equal(res.durationMs, 120);
|
|
60
|
+
assert.equal(res.finishReason, 'end');
|
|
61
|
+
assert.equal(res.provider, 'openrouter');
|
|
62
|
+
assert.deepEqual(res.toolCalls, []);
|
|
63
|
+
});
|
|
64
|
+
it('surfaces tool_use blocks as structured toolCalls + tool-use finish', async () => {
|
|
65
|
+
const adapter = new FakeAdapter(async (out) => {
|
|
66
|
+
emitToolUse(out, 'read_file', { path: 'a.ts' }, 't1');
|
|
67
|
+
return fakeResult({ output: '', stopReason: 'tool_use' });
|
|
68
|
+
});
|
|
69
|
+
const res = await legacyAdapterToLanguageModel(adapter).invoke(OPTS);
|
|
70
|
+
assert.equal(res.toolCalls.length, 1);
|
|
71
|
+
assert.equal(res.toolCalls[0].name, 'read_file');
|
|
72
|
+
assert.equal(res.toolCalls[0].id, 't1');
|
|
73
|
+
assert.deepEqual(res.toolCalls[0].arguments, { path: 'a.ts' });
|
|
74
|
+
assert.equal(res.finishReason, 'tool-use');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
describe('legacyAdapterToLanguageModel — invokeStream()', () => {
|
|
78
|
+
it('yields text-delta events then usage then finish', async () => {
|
|
79
|
+
const adapter = new FakeAdapter(async (out) => {
|
|
80
|
+
emitContent(out, 'A');
|
|
81
|
+
emitContent(out, 'B');
|
|
82
|
+
return fakeResult({ output: 'AB', inputTokens: 3, outputTokens: 2, stopReason: 'end_turn' });
|
|
83
|
+
});
|
|
84
|
+
const events = await collect(legacyAdapterToLanguageModel(adapter).invokeStream(OPTS));
|
|
85
|
+
const kinds = events.map((e) => e.type);
|
|
86
|
+
assert.deepEqual(kinds, ['text-delta', 'text-delta', 'usage', 'finish']);
|
|
87
|
+
assert.equal(events[0].text, 'A');
|
|
88
|
+
assert.equal(events[3].reason, 'end');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
describe('legacyAdapterToLanguageModel — error propagation', () => {
|
|
92
|
+
it('rethrows a rejected run() so the unified classifier can act on it', async () => {
|
|
93
|
+
const adapter = new FakeAdapter(async () => {
|
|
94
|
+
throw new UpstreamError(0, 'fetch failed: fetch failed', { provider: 'opencode', retryable: true });
|
|
95
|
+
});
|
|
96
|
+
await assert.rejects(() => legacyAdapterToLanguageModel(adapter).invoke(OPTS), (err) => {
|
|
97
|
+
assert.equal(classifyError(err), 'timeout');
|
|
98
|
+
return true;
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
//# sourceMappingURL=legacy-adapter-language-model.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"legacy-adapter-language-model.test.js","sourceRoot":"","sources":["../../src/__tests__/legacy-adapter-language-model.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §H4 — turn recording + replay-equivalence for the Ollama AGENTIC adapter.
|
|
3
|
+
*
|
|
4
|
+
* The agentic port records per-turn assistant-start / tool_use / tool_result /
|
|
5
|
+
* assistant-end and, crucially, HONORS `replayed` on a same-runId resume so the
|
|
6
|
+
* recorded effects re-issue in order WITHOUT re-calling the upstream or
|
|
7
|
+
* re-executing tools. This is the guarantee that prevents a crash-resume from
|
|
8
|
+
* tripping a DeterminismViolation under FO1's runId reuse.
|
|
9
|
+
*
|
|
10
|
+
* Test 1 records a 2-turn (tool-call → text) run and asserts the effect log.
|
|
11
|
+
* Test 2 replays that exact log and asserts ZERO upstream fetches + ZERO tool
|
|
12
|
+
* executions, with byte-identical output.
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=ollama-adapter-recording.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ollama-adapter-recording.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/ollama-adapter-recording.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §H4 — turn recording + replay-equivalence for the Ollama AGENTIC adapter.
|
|
3
|
+
*
|
|
4
|
+
* The agentic port records per-turn assistant-start / tool_use / tool_result /
|
|
5
|
+
* assistant-end and, crucially, HONORS `replayed` on a same-runId resume so the
|
|
6
|
+
* recorded effects re-issue in order WITHOUT re-calling the upstream or
|
|
7
|
+
* re-executing tools. This is the guarantee that prevents a crash-resume from
|
|
8
|
+
* tripping a DeterminismViolation under FO1's runId reuse.
|
|
9
|
+
*
|
|
10
|
+
* Test 1 records a 2-turn (tool-call → text) run and asserts the effect log.
|
|
11
|
+
* Test 2 replays that exact log and asserts ZERO upstream fetches + ZERO tool
|
|
12
|
+
* executions, with byte-identical output.
|
|
13
|
+
*/
|
|
14
|
+
import { describe, it, before, after } from 'node:test';
|
|
15
|
+
import assert from 'node:assert/strict';
|
|
16
|
+
import { OllamaAdapter } from '../ollama-adapter.js';
|
|
17
|
+
import { TurnRecorder } from '../turn-recorder/index.js';
|
|
18
|
+
function ollamaText(text) {
|
|
19
|
+
return new Response([
|
|
20
|
+
JSON.stringify({ message: { content: text } }),
|
|
21
|
+
JSON.stringify({ done: true, prompt_eval_count: 5, eval_count: 7, total_duration: 1_000_000 }),
|
|
22
|
+
].join('\n') + '\n', { status: 200 });
|
|
23
|
+
}
|
|
24
|
+
function ollamaToolCall(name, args) {
|
|
25
|
+
return new Response(JSON.stringify({
|
|
26
|
+
done: true, prompt_eval_count: 3, eval_count: 2, total_duration: 500_000,
|
|
27
|
+
message: { role: 'assistant', content: '', tool_calls: [{ function: { name, arguments: args } }] },
|
|
28
|
+
}) + '\n', { status: 200 });
|
|
29
|
+
}
|
|
30
|
+
class StubWritable {
|
|
31
|
+
write() { return true; }
|
|
32
|
+
end() { }
|
|
33
|
+
on() { }
|
|
34
|
+
}
|
|
35
|
+
class FakeExecutor {
|
|
36
|
+
handlers;
|
|
37
|
+
calls = [];
|
|
38
|
+
constructor(handlers) {
|
|
39
|
+
this.handlers = handlers;
|
|
40
|
+
}
|
|
41
|
+
listSchemas() {
|
|
42
|
+
return Object.keys(this.handlers).map((name) => ({
|
|
43
|
+
name, description: `${name} tool`, inputSchema: { type: 'object', properties: {} },
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
async execute(call) {
|
|
47
|
+
this.calls.push(call);
|
|
48
|
+
return this.handlers[call.name]();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Record (seed empty) or replay (seed populated) fake EffectRuntime. */
|
|
52
|
+
function fakeRuntime(seed = {}) {
|
|
53
|
+
const log = [];
|
|
54
|
+
const runtime = {
|
|
55
|
+
async effect(name, fn, _opts) {
|
|
56
|
+
if (name in seed) {
|
|
57
|
+
log.push({ name, payload: seed[name] });
|
|
58
|
+
return seed[name];
|
|
59
|
+
}
|
|
60
|
+
const payload = await fn();
|
|
61
|
+
log.push({ name, payload });
|
|
62
|
+
return payload;
|
|
63
|
+
},
|
|
64
|
+
peekRecorded(name) {
|
|
65
|
+
return (name in seed ? seed[name] : undefined);
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
return { runtime, log };
|
|
69
|
+
}
|
|
70
|
+
const baseConfig = {
|
|
71
|
+
userPrompt: 'list the files then finish',
|
|
72
|
+
model: 'qwen3:14b',
|
|
73
|
+
workingDir: '/tmp',
|
|
74
|
+
stage: 'build',
|
|
75
|
+
persona: 'engineer',
|
|
76
|
+
sessionId: 'run-oll',
|
|
77
|
+
};
|
|
78
|
+
let originalFetch;
|
|
79
|
+
before(() => { originalFetch = globalThis.fetch; });
|
|
80
|
+
after(() => { globalThis.fetch = originalFetch; });
|
|
81
|
+
describe('OllamaAdapter — H4 recording + replay-equivalence', () => {
|
|
82
|
+
// Shared between the two tests: test 1 records into `recordedLog`.
|
|
83
|
+
let recordedLog = [];
|
|
84
|
+
let liveFetchCount = 0;
|
|
85
|
+
it('records assistant-start/tool_use/tool_result/assistant-end across a tool-call run', async () => {
|
|
86
|
+
liveFetchCount = 0;
|
|
87
|
+
globalThis.fetch = (async () => {
|
|
88
|
+
liveFetchCount += 1;
|
|
89
|
+
return liveFetchCount === 1 ? ollamaToolCall('list', {}) : ollamaText('done');
|
|
90
|
+
});
|
|
91
|
+
const fx = new FakeExecutor({ list: () => ({ content: 'a.ts\nb.ts', isError: false }) });
|
|
92
|
+
const { runtime, log } = fakeRuntime();
|
|
93
|
+
const recorder = new TurnRecorder({ runtime, partialSink: () => { }, runId: 'run-oll', stepId: 'build' });
|
|
94
|
+
const r = await new OllamaAdapter().run({ ...baseConfig, toolExecutor: fx, turnRecorder: recorder }, new StubWritable());
|
|
95
|
+
assert.equal(r.output, 'done');
|
|
96
|
+
assert.equal(liveFetchCount, 2, 'two upstream turns on the live path');
|
|
97
|
+
assert.equal(fx.calls.length, 1, 'tool executed once on the live path');
|
|
98
|
+
const names = log.map((e) => e.name);
|
|
99
|
+
assert.ok(names.includes('turn:0:assistant-start'));
|
|
100
|
+
assert.ok(names.includes('turn:0:tool_use:0'));
|
|
101
|
+
assert.ok(names.includes('turn:0:tool_result:0'));
|
|
102
|
+
assert.ok(names.includes('turn:0:assistant-end'));
|
|
103
|
+
assert.ok(names.includes('turn:1:assistant-start'));
|
|
104
|
+
assert.ok(names.includes('turn:1:assistant-end'));
|
|
105
|
+
const start0 = log.find((e) => e.name === 'turn:0:assistant-start');
|
|
106
|
+
assert.equal(start0.payload.userPrompt, 'list the files then finish');
|
|
107
|
+
const end0 = log.find((e) => e.name === 'turn:0:assistant-end');
|
|
108
|
+
assert.equal(end0.payload.stopReason, 'tool_use');
|
|
109
|
+
assert.ok(Array.isArray(end0.payload.historyDelta));
|
|
110
|
+
recordedLog = log;
|
|
111
|
+
});
|
|
112
|
+
it('replays the recorded run with ZERO upstream fetches and ZERO tool executions', async () => {
|
|
113
|
+
// Build the replay seed from test 1's effect log.
|
|
114
|
+
const seed = {};
|
|
115
|
+
for (const e of recordedLog)
|
|
116
|
+
seed[e.name] = e.payload;
|
|
117
|
+
let replayFetchCount = 0;
|
|
118
|
+
globalThis.fetch = (async () => { replayFetchCount += 1; return ollamaText('SHOULD NOT RUN'); });
|
|
119
|
+
const fx = new FakeExecutor({ list: () => { throw new Error('tool exec must NOT run on replay'); } });
|
|
120
|
+
const { runtime } = fakeRuntime(seed);
|
|
121
|
+
const recorder = new TurnRecorder({ runtime, partialSink: () => { }, runId: 'run-oll', stepId: 'build' });
|
|
122
|
+
const r = await new OllamaAdapter().run({ ...baseConfig, toolExecutor: fx, turnRecorder: recorder }, new StubWritable());
|
|
123
|
+
assert.equal(replayFetchCount, 0, 'replay must NOT hit the upstream');
|
|
124
|
+
assert.equal(fx.calls.length, 0, 'replay must NOT re-execute tools');
|
|
125
|
+
assert.equal(r.output, 'done', 'replay reproduces the recorded output byte-for-byte');
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
//# sourceMappingURL=ollama-adapter-recording.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ollama-adapter-recording.test.js","sourceRoot":"","sources":["../../src/__tests__/ollama-adapter-recording.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for the H2 turn-level durable resume machinery in
|
|
3
|
+
* the OpenRouter adapter (v2 ADR §2.1 / §2.3 / §2.5), driven against a
|
|
4
|
+
* real HTTP server (`MockUpstream`) that can cut its SSE stream at a
|
|
5
|
+
* chosen frame to simulate a model dying mid-output.
|
|
6
|
+
*
|
|
7
|
+
* Proves:
|
|
8
|
+
* 1. Burn mid-SSE → the partial sink receives the EXACT assistant
|
|
9
|
+
* text streamed before the cut (not an empty string). This is the
|
|
10
|
+
* whole point of surfacing the accumulator out of consumeSSE.
|
|
11
|
+
* 2. Mid-`tool_use` truncation (§2.1.1) → a tool_call whose streamed
|
|
12
|
+
* args are incomplete JSON is NOT counted in `toolUsesEmitted`.
|
|
13
|
+
* 3. Prefill (§2.3) → when config.prefill is present, the adapter's
|
|
14
|
+
* outbound request carries the re-presented tool_use/tool_result
|
|
15
|
+
* pair AND a trailing assistant message with the partial text, so
|
|
16
|
+
* the new model continues from the exact stopping point.
|
|
17
|
+
*/
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=openrouter-prefill.integration.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openrouter-prefill.integration.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/openrouter-prefill.integration.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG"}
|