@esankhan3/anvil-agent-core 0.2.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__/fetch-pool.integration.test.d.ts +15 -0
- package/dist/__tests__/fetch-pool.integration.test.d.ts.map +1 -0
- package/dist/__tests__/fetch-pool.integration.test.js +109 -0
- package/dist/__tests__/fetch-pool.integration.test.js.map +1 -0
- package/dist/__tests__/fetch-pool.test.d.ts +15 -0
- package/dist/__tests__/fetch-pool.test.d.ts.map +1 -0
- package/dist/__tests__/fetch-pool.test.js +115 -0
- package/dist/__tests__/fetch-pool.test.js.map +1 -0
- 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/adk-anthropic-llm.d.ts.map +1 -1
- package/dist/adk-anthropic-llm.js +23 -10
- package/dist/adk-anthropic-llm.js.map +1 -1
- package/dist/agent/session/adapter.d.ts +25 -0
- package/dist/agent/session/adapter.d.ts.map +1 -1
- package/dist/agent/session/adapter.js +4 -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 +30 -2
- 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 +32 -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/fetch-pool.d.ts +47 -0
- package/dist/fetch-pool.d.ts.map +1 -0
- package/dist/fetch-pool.js +132 -0
- package/dist/fetch-pool.js.map +1 -0
- package/dist/gemini-adapter.d.ts.map +1 -1
- package/dist/gemini-adapter.js +65 -6
- package/dist/gemini-adapter.js.map +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/ollama-adapter.d.ts.map +1 -1
- package/dist/ollama-adapter.js +145 -15
- package/dist/ollama-adapter.js.map +1 -1
- package/dist/openrouter-adapter.d.ts.map +1 -1
- package/dist/openrouter-adapter.js +296 -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/provider-liveness.d.ts.map +1 -1
- package/dist/provider-liveness.js +17 -6
- package/dist/provider-liveness.js.map +1 -1
- 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/local-executor.d.ts.map +1 -1
- package/dist/router/local-executor.js +19 -6
- package/dist/router/local-executor.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/single-shot.d.ts.map +1 -1
- package/dist/single-shot.js +26 -1
- package/dist/single-shot.js.map +1 -1
- package/dist/tools/builtin.d.ts +19 -0
- package/dist/tools/builtin.d.ts.map +1 -1
- package/dist/tools/builtin.js +51 -0
- package/dist/tools/builtin.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 +4 -3
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,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration test — proves the heal-on-failure fix actually works.
|
|
3
|
+
*
|
|
4
|
+
* Without the fix, a poisoned pool fails every subsequent fetch with the
|
|
5
|
+
* same TypeError until the process restarts. With the fix:
|
|
6
|
+
* - call 1 succeeds against a live server
|
|
7
|
+
* - server dies → call 2 fails (TypeError: fetch failed)
|
|
8
|
+
* - recycleFetchPoolOnFailure() runs
|
|
9
|
+
* - server comes back → call 3 succeeds against the healed pool
|
|
10
|
+
*
|
|
11
|
+
* This is the test the plan in docs/FETCH-POOL-MANAGEMENT-PLAN.md §6.2
|
|
12
|
+
* specifies as the canary for the entire fix.
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=fetch-pool.integration.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch-pool.integration.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/fetch-pool.integration.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration test — proves the heal-on-failure fix actually works.
|
|
3
|
+
*
|
|
4
|
+
* Without the fix, a poisoned pool fails every subsequent fetch with the
|
|
5
|
+
* same TypeError until the process restarts. With the fix:
|
|
6
|
+
* - call 1 succeeds against a live server
|
|
7
|
+
* - server dies → call 2 fails (TypeError: fetch failed)
|
|
8
|
+
* - recycleFetchPoolOnFailure() runs
|
|
9
|
+
* - server comes back → call 3 succeeds against the healed pool
|
|
10
|
+
*
|
|
11
|
+
* This is the test the plan in docs/FETCH-POOL-MANAGEMENT-PLAN.md §6.2
|
|
12
|
+
* specifies as the canary for the entire fix.
|
|
13
|
+
*/
|
|
14
|
+
import { describe, it, before, after, beforeEach } from 'node:test';
|
|
15
|
+
import assert from 'node:assert/strict';
|
|
16
|
+
import http from 'node:http';
|
|
17
|
+
import { getFetchPool, recycleFetchPoolOnFailure, getPoolMetrics, resetAllPools, } from '../fetch-pool.js';
|
|
18
|
+
describe('fetch-pool integration — heal-on-failure end-to-end', () => {
|
|
19
|
+
let server = null;
|
|
20
|
+
let port = 0;
|
|
21
|
+
async function startServer() {
|
|
22
|
+
server = http.createServer((req, res) => {
|
|
23
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
24
|
+
res.end(JSON.stringify({ ok: true, path: req.url }));
|
|
25
|
+
});
|
|
26
|
+
await new Promise((resolve) => {
|
|
27
|
+
server.listen(port, '127.0.0.1', () => {
|
|
28
|
+
port = server.address().port;
|
|
29
|
+
resolve();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
async function stopServer() {
|
|
34
|
+
if (!server)
|
|
35
|
+
return;
|
|
36
|
+
await new Promise((resolve) => {
|
|
37
|
+
server.close(() => resolve());
|
|
38
|
+
// Force-close any sockets the pool may be holding so the close()
|
|
39
|
+
// callback fires promptly.
|
|
40
|
+
server.closeAllConnections?.();
|
|
41
|
+
});
|
|
42
|
+
server = null;
|
|
43
|
+
}
|
|
44
|
+
before(async () => {
|
|
45
|
+
await startServer();
|
|
46
|
+
});
|
|
47
|
+
beforeEach(async () => {
|
|
48
|
+
await resetAllPools();
|
|
49
|
+
});
|
|
50
|
+
after(async () => {
|
|
51
|
+
await stopServer();
|
|
52
|
+
await resetAllPools();
|
|
53
|
+
});
|
|
54
|
+
it('healed pool succeeds after a network failure poisons the old pool', async () => {
|
|
55
|
+
const url = `http://127.0.0.1:${port}/v1/messages`;
|
|
56
|
+
// 1. First call succeeds against a live server.
|
|
57
|
+
const r1 = await fetch(url, {
|
|
58
|
+
// @ts-expect-error — undici dispatcher accepted by Node fetch at runtime
|
|
59
|
+
dispatcher: getFetchPool('anthropic'),
|
|
60
|
+
});
|
|
61
|
+
assert.strictEqual(r1.status, 200);
|
|
62
|
+
await r1.text();
|
|
63
|
+
// 2. Kill the server. The pool now holds zombie sockets pointed at a
|
|
64
|
+
// dead listener — every subsequent fetch through the SAME dispatcher
|
|
65
|
+
// fails with `TypeError: fetch failed`.
|
|
66
|
+
await stopServer();
|
|
67
|
+
let caught = null;
|
|
68
|
+
try {
|
|
69
|
+
await fetch(url, {
|
|
70
|
+
// @ts-expect-error — undici dispatcher accepted by Node fetch at runtime
|
|
71
|
+
dispatcher: getFetchPool('anthropic'),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
caught = err;
|
|
76
|
+
}
|
|
77
|
+
assert.ok(caught instanceof Error, 'expected fetch to throw with server down');
|
|
78
|
+
assert.match(caught.message + ' ' + (caught.cause instanceof Error ? caught.cause.message : ''), /fetch failed|ECONNREFUSED|ECONNRESET|socket/i);
|
|
79
|
+
// 3. Recycle the pool — this is what the adapter's catch block does
|
|
80
|
+
// in production.
|
|
81
|
+
await recycleFetchPoolOnFailure('anthropic', caught);
|
|
82
|
+
const metrics = getPoolMetrics().find((m) => m.provider === 'anthropic');
|
|
83
|
+
assert.strictEqual(metrics?.recycleCount, 1, 'pool should have been recycled');
|
|
84
|
+
// 4. Bring the server back on the same port.
|
|
85
|
+
await startServer();
|
|
86
|
+
// 5. The next fetch must succeed. Without the recycle, the same zombie
|
|
87
|
+
// socket would be re-used and this would fail again.
|
|
88
|
+
const r3 = await fetch(url, {
|
|
89
|
+
// @ts-expect-error — undici dispatcher accepted by Node fetch at runtime
|
|
90
|
+
dispatcher: getFetchPool('anthropic'),
|
|
91
|
+
});
|
|
92
|
+
assert.strictEqual(r3.status, 200, 'healed pool must serve the next request');
|
|
93
|
+
const body = (await r3.json());
|
|
94
|
+
assert.strictEqual(body.ok, true);
|
|
95
|
+
});
|
|
96
|
+
it('one provider failing does not recycle a sibling provider', async () => {
|
|
97
|
+
// Touch both pools so they exist.
|
|
98
|
+
getFetchPool('anthropic');
|
|
99
|
+
getFetchPool('gemini');
|
|
100
|
+
// Poison only one.
|
|
101
|
+
await recycleFetchPoolOnFailure('anthropic', new TypeError('fetch failed'));
|
|
102
|
+
const metrics = getPoolMetrics();
|
|
103
|
+
const anthropic = metrics.find((m) => m.provider === 'anthropic');
|
|
104
|
+
const gemini = metrics.find((m) => m.provider === 'gemini');
|
|
105
|
+
assert.strictEqual(anthropic?.recycleCount, 1);
|
|
106
|
+
assert.strictEqual(gemini?.recycleCount, 0, 'sibling provider must be untouched');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
//# sourceMappingURL=fetch-pool.integration.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch-pool.integration.test.js","sourceRoot":"","sources":["../../src/__tests__/fetch-pool.integration.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for fetch-pool — per-provider dispatcher with heal-on-failure.
|
|
3
|
+
*
|
|
4
|
+
* Covers the eight contracts from FETCH-POOL-MANAGEMENT-PLAN.md §6.1:
|
|
5
|
+
* a) Lazy construction returns a stable instance per provider
|
|
6
|
+
* b) Per-provider isolation — different providers get different agents
|
|
7
|
+
* c) Bounded options applied at construction
|
|
8
|
+
* d) Recycle on poisoned-error pattern swaps the agent
|
|
9
|
+
* e) No recycle on non-poisoned errors (e.g. 400, 401, 503)
|
|
10
|
+
* f) Concurrent recycles coalesce to one bump
|
|
11
|
+
* g) Metrics shape is well-formed
|
|
12
|
+
* h) resetAllPools clears state cleanly
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=fetch-pool.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch-pool.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/fetch-pool.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for fetch-pool — per-provider dispatcher with heal-on-failure.
|
|
3
|
+
*
|
|
4
|
+
* Covers the eight contracts from FETCH-POOL-MANAGEMENT-PLAN.md §6.1:
|
|
5
|
+
* a) Lazy construction returns a stable instance per provider
|
|
6
|
+
* b) Per-provider isolation — different providers get different agents
|
|
7
|
+
* c) Bounded options applied at construction
|
|
8
|
+
* d) Recycle on poisoned-error pattern swaps the agent
|
|
9
|
+
* e) No recycle on non-poisoned errors (e.g. 400, 401, 503)
|
|
10
|
+
* f) Concurrent recycles coalesce to one bump
|
|
11
|
+
* g) Metrics shape is well-formed
|
|
12
|
+
* h) resetAllPools clears state cleanly
|
|
13
|
+
*/
|
|
14
|
+
import { describe, it, beforeEach } from 'node:test';
|
|
15
|
+
import assert from 'node:assert/strict';
|
|
16
|
+
import { Agent } from 'undici';
|
|
17
|
+
import { getFetchPool, recycleFetchPoolOnFailure, getPoolMetrics, resetAllPools, } from '../fetch-pool.js';
|
|
18
|
+
describe('fetch-pool', () => {
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
await resetAllPools();
|
|
21
|
+
});
|
|
22
|
+
it('a) lazy construction returns the same Agent on repeat calls', () => {
|
|
23
|
+
const a1 = getFetchPool('anthropic');
|
|
24
|
+
const a2 = getFetchPool('anthropic');
|
|
25
|
+
assert.ok(a1 instanceof Agent);
|
|
26
|
+
assert.strictEqual(a1, a2, 'second call should return same instance');
|
|
27
|
+
});
|
|
28
|
+
it('b) per-provider isolation — separate Agents per provider', () => {
|
|
29
|
+
const anthropic = getFetchPool('anthropic');
|
|
30
|
+
const gemini = getFetchPool('gemini');
|
|
31
|
+
const opencode = getFetchPool('opencode');
|
|
32
|
+
assert.notStrictEqual(anthropic, gemini);
|
|
33
|
+
assert.notStrictEqual(anthropic, opencode);
|
|
34
|
+
assert.notStrictEqual(gemini, opencode);
|
|
35
|
+
});
|
|
36
|
+
it('c) constructed Agent is bounded — close() resolves without hanging', async () => {
|
|
37
|
+
// Indirect bounded-keep-alive test: a fresh pool with no in-flight work
|
|
38
|
+
// closes synchronously-ish. If construction picked up unbounded defaults,
|
|
39
|
+
// tests for d/f would intermittently hang on the close() inside recycle.
|
|
40
|
+
const agent = getFetchPool('openai');
|
|
41
|
+
const closed = agent.close();
|
|
42
|
+
await Promise.race([
|
|
43
|
+
closed,
|
|
44
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('close hung beyond 500ms')), 500)),
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
it('d) recycle swaps the Agent when the error matches a poisoned pattern', async () => {
|
|
48
|
+
const before = getFetchPool('opencode');
|
|
49
|
+
await recycleFetchPoolOnFailure('opencode', new TypeError('fetch failed'));
|
|
50
|
+
const after = getFetchPool('opencode');
|
|
51
|
+
assert.notStrictEqual(after, before, 'agent should have been swapped');
|
|
52
|
+
const metrics = getPoolMetrics().find((m) => m.provider === 'opencode');
|
|
53
|
+
assert.ok(metrics);
|
|
54
|
+
assert.strictEqual(metrics.recycleCount, 1);
|
|
55
|
+
assert.ok(metrics.lastRecycleAt);
|
|
56
|
+
assert.match(metrics.lastRecycleReason ?? '', /fetch failed/);
|
|
57
|
+
});
|
|
58
|
+
it('e) does not recycle on non-poisoned errors (real upstream status)', async () => {
|
|
59
|
+
const before = getFetchPool('gemini');
|
|
60
|
+
await recycleFetchPoolOnFailure('gemini', new Error('400 Bad Request'));
|
|
61
|
+
await recycleFetchPoolOnFailure('gemini', new Error('503 Service Unavailable'));
|
|
62
|
+
await recycleFetchPoolOnFailure('gemini', new Error('quota exceeded'));
|
|
63
|
+
const after = getFetchPool('gemini');
|
|
64
|
+
assert.strictEqual(after, before, 'agent must not change on non-network errors');
|
|
65
|
+
const metrics = getPoolMetrics().find((m) => m.provider === 'gemini');
|
|
66
|
+
assert.strictEqual(metrics?.recycleCount, 0);
|
|
67
|
+
});
|
|
68
|
+
it('f) concurrent recycles coalesce — recycleCount increments once', async () => {
|
|
69
|
+
getFetchPool('ollama');
|
|
70
|
+
const err = new TypeError('fetch failed');
|
|
71
|
+
const all = await Promise.all(Array.from({ length: 10 }, () => recycleFetchPoolOnFailure('ollama', err)));
|
|
72
|
+
assert.strictEqual(all.length, 10);
|
|
73
|
+
const metrics = getPoolMetrics().find((m) => m.provider === 'ollama');
|
|
74
|
+
assert.strictEqual(metrics?.recycleCount, 1, 'must coalesce to one recycle');
|
|
75
|
+
});
|
|
76
|
+
it('g) getPoolMetrics returns well-shaped entries', async () => {
|
|
77
|
+
getFetchPool('anthropic');
|
|
78
|
+
getFetchPool('openrouter');
|
|
79
|
+
const metrics = getPoolMetrics();
|
|
80
|
+
assert.ok(Array.isArray(metrics));
|
|
81
|
+
assert.strictEqual(metrics.length, 2);
|
|
82
|
+
for (const m of metrics) {
|
|
83
|
+
assert.ok(['anthropic', 'openrouter'].includes(m.provider));
|
|
84
|
+
assert.strictEqual(typeof m.createdAt, 'string');
|
|
85
|
+
assert.match(m.createdAt, /\dT\d/, 'ISO timestamp');
|
|
86
|
+
assert.strictEqual(typeof m.recycleCount, 'number');
|
|
87
|
+
assert.strictEqual(typeof m.active, 'boolean');
|
|
88
|
+
assert.strictEqual(m.active, true, 'idle pool is active');
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
it('h) resetAllPools drops all state', async () => {
|
|
92
|
+
getFetchPool('anthropic');
|
|
93
|
+
getFetchPool('gemini');
|
|
94
|
+
assert.strictEqual(getPoolMetrics().length, 2);
|
|
95
|
+
await resetAllPools();
|
|
96
|
+
assert.strictEqual(getPoolMetrics().length, 0);
|
|
97
|
+
// Subsequent get reconstructs cleanly
|
|
98
|
+
const fresh = getFetchPool('anthropic');
|
|
99
|
+
assert.ok(fresh instanceof Agent);
|
|
100
|
+
assert.strictEqual(getPoolMetrics().length, 1);
|
|
101
|
+
});
|
|
102
|
+
it('recycle reads ECONNRESET from err.cause', async () => {
|
|
103
|
+
const before = getFetchPool('anthropic');
|
|
104
|
+
// Node's fetch wraps the underlying socket error in `.cause`.
|
|
105
|
+
const err = new TypeError('fetch failed', {
|
|
106
|
+
cause: new Error('read ECONNRESET'),
|
|
107
|
+
});
|
|
108
|
+
await recycleFetchPoolOnFailure('anthropic', err);
|
|
109
|
+
const after = getFetchPool('anthropic');
|
|
110
|
+
assert.notStrictEqual(after, before);
|
|
111
|
+
const m = getPoolMetrics().find((p) => p.provider === 'anthropic');
|
|
112
|
+
assert.strictEqual(m?.recycleCount, 1);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
//# sourceMappingURL=fetch-pool.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch-pool.test.js","sourceRoot":"","sources":["../../src/__tests__/fetch-pool.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -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"}
|