@animalabs/agent-framework 0.6.0 → 0.6.3
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/dist/src/agent.d.ts +4 -0
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +15 -0
- package/dist/src/agent.js.map +1 -1
- package/dist/src/framework.d.ts +235 -2
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +1141 -92
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +20 -1
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +60 -4
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +17 -1
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +34 -3
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js +4 -0
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +29 -0
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +102 -4
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +17 -1
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/module-registry.d.ts +15 -1
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +92 -4
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/types/agent.d.ts +37 -0
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +40 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +46 -0
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +51 -0
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/test/agent-cache-ttl.test.d.ts +2 -0
- package/dist/test/agent-cache-ttl.test.d.ts.map +1 -0
- package/dist/test/agent-cache-ttl.test.js +25 -0
- package/dist/test/agent-cache-ttl.test.js.map +1 -0
- package/dist/test/conversation-fork-injection-scoping.test.d.ts +2 -0
- package/dist/test/conversation-fork-injection-scoping.test.d.ts.map +1 -0
- package/dist/test/conversation-fork-injection-scoping.test.js +184 -0
- package/dist/test/conversation-fork-injection-scoping.test.js.map +1 -0
- package/dist/test/gather-context-timeout.test.d.ts +2 -0
- package/dist/test/gather-context-timeout.test.d.ts.map +1 -0
- package/dist/test/gather-context-timeout.test.js +120 -0
- package/dist/test/gather-context-timeout.test.js.map +1 -0
- package/dist/test/inference-exhaustion-rewind.test.d.ts +2 -0
- package/dist/test/inference-exhaustion-rewind.test.d.ts.map +1 -0
- package/dist/test/inference-exhaustion-rewind.test.js +181 -0
- package/dist/test/inference-exhaustion-rewind.test.js.map +1 -0
- package/dist/test/inference-failure-observability.test.js +9 -0
- package/dist/test/inference-failure-observability.test.js.map +1 -1
- package/dist/test/maintenance-timer.test.d.ts +2 -0
- package/dist/test/maintenance-timer.test.d.ts.map +1 -0
- package/dist/test/maintenance-timer.test.js +114 -0
- package/dist/test/maintenance-timer.test.js.map +1 -0
- package/dist/test/mcpl-reconnect-events.test.d.ts +2 -0
- package/dist/test/mcpl-reconnect-events.test.d.ts.map +1 -0
- package/dist/test/mcpl-reconnect-events.test.js +119 -0
- package/dist/test/mcpl-reconnect-events.test.js.map +1 -0
- package/dist/test/mcpl-reconnect-reregistration.test.d.ts +2 -0
- package/dist/test/mcpl-reconnect-reregistration.test.d.ts.map +1 -0
- package/dist/test/mcpl-reconnect-reregistration.test.js +181 -0
- package/dist/test/mcpl-reconnect-reregistration.test.js.map +1 -0
- package/dist/test/mcpl-request-timeout.test.d.ts +2 -0
- package/dist/test/mcpl-request-timeout.test.d.ts.map +1 -0
- package/dist/test/mcpl-request-timeout.test.js +123 -0
- package/dist/test/mcpl-request-timeout.test.js.map +1 -0
- package/dist/test/mcpl-runtime-lifecycle.test.d.ts +7 -0
- package/dist/test/mcpl-runtime-lifecycle.test.d.ts.map +1 -0
- package/dist/test/mcpl-runtime-lifecycle.test.js +142 -0
- package/dist/test/mcpl-runtime-lifecycle.test.js.map +1 -0
- package/dist/test/ops-alert.test.d.ts +2 -0
- package/dist/test/ops-alert.test.d.ts.map +1 -0
- package/dist/test/ops-alert.test.js +152 -0
- package/dist/test/ops-alert.test.js.map +1 -0
- package/dist/test/refusal-rewind.test.d.ts +2 -0
- package/dist/test/refusal-rewind.test.d.ts.map +1 -0
- package/dist/test/refusal-rewind.test.js +108 -0
- package/dist/test/refusal-rewind.test.js.map +1 -0
- package/dist/test/state-scaling-e2e.test.d.ts +22 -0
- package/dist/test/state-scaling-e2e.test.d.ts.map +1 -0
- package/dist/test/state-scaling-e2e.test.js +200 -0
- package/dist/test/state-scaling-e2e.test.js.map +1 -0
- package/dist/test/state-scaling.test.d.ts +12 -0
- package/dist/test/state-scaling.test.d.ts.map +1 -0
- package/dist/test/state-scaling.test.js +243 -0
- package/dist/test/state-scaling.test.js.map +1 -0
- package/dist/test/tool-result-content.test.d.ts +2 -0
- package/dist/test/tool-result-content.test.d.ts.map +1 -0
- package/dist/test/tool-result-content.test.js +37 -0
- package/dist/test/tool-result-content.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -2
- package/src/agent.ts +16 -0
- package/src/framework.ts +1339 -99
- package/src/gate/event-gate.ts +56 -4
- package/src/index.ts +10 -0
- package/src/mcpl/channel-registry.ts +34 -3
- package/src/mcpl/push-handler.ts +4 -0
- package/src/mcpl/server-connection.ts +110 -4
- package/src/mcpl/types.ts +19 -1
- package/src/module-registry.ts +113 -4
- package/src/types/agent.ts +38 -0
- package/src/types/framework.ts +44 -0
- package/src/types/module.ts +52 -0
- package/src/types/trace.ts +67 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { AgentFramework } from '../src/index.js';
|
|
7
|
+
class QueuedStrategy {
|
|
8
|
+
name = 'queued-test';
|
|
9
|
+
ticks = 0;
|
|
10
|
+
toolCounts = [];
|
|
11
|
+
checkReadiness() {
|
|
12
|
+
return { ready: this.ticks >= 2, description: 'test maintenance queued' };
|
|
13
|
+
}
|
|
14
|
+
async tick(ctx) {
|
|
15
|
+
this.toolCounts.push(ctx.tools?.length ?? 0);
|
|
16
|
+
this.ticks++;
|
|
17
|
+
}
|
|
18
|
+
select(_store, _log, _budget) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
class ToolModule {
|
|
23
|
+
name = 'maintenance-tools';
|
|
24
|
+
async start(_ctx) { }
|
|
25
|
+
async stop() { }
|
|
26
|
+
getTools() {
|
|
27
|
+
return [{
|
|
28
|
+
name: 'inspect',
|
|
29
|
+
description: 'test tool',
|
|
30
|
+
inputSchema: { type: 'object', properties: {} },
|
|
31
|
+
}];
|
|
32
|
+
}
|
|
33
|
+
async handleToolCall(_call) {
|
|
34
|
+
return { success: false, error: 'not expected', isError: true };
|
|
35
|
+
}
|
|
36
|
+
async onProcess(_event, _state) {
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const membrane = {
|
|
41
|
+
async complete(_request) {
|
|
42
|
+
throw new Error('not expected');
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
async function waitFor(predicate, timeoutMs = 1000) {
|
|
46
|
+
const deadline = Date.now() + timeoutMs;
|
|
47
|
+
while (!predicate()) {
|
|
48
|
+
if (Date.now() >= deadline)
|
|
49
|
+
throw new Error('timed out waiting for maintenance');
|
|
50
|
+
await new Promise(resolve => setTimeout(resolve, 5));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
describe('queued context maintenance timer', () => {
|
|
54
|
+
it('drains without a new message and refreshes tools before ticking', async () => {
|
|
55
|
+
const dir = mkdtempSync(join(tmpdir(), 'af-maintenance-'));
|
|
56
|
+
const strategy = new QueuedStrategy();
|
|
57
|
+
const framework = await AgentFramework.create({
|
|
58
|
+
storePath: join(dir, 'store'),
|
|
59
|
+
membrane,
|
|
60
|
+
agents: [{
|
|
61
|
+
name: 'agent',
|
|
62
|
+
model: 'test-model',
|
|
63
|
+
systemPrompt: 'test',
|
|
64
|
+
strategy,
|
|
65
|
+
allowedTools: 'all',
|
|
66
|
+
}],
|
|
67
|
+
modules: [new ToolModule()],
|
|
68
|
+
syncIntervalMs: 0,
|
|
69
|
+
maintenanceIntervalMs: 10,
|
|
70
|
+
});
|
|
71
|
+
try {
|
|
72
|
+
framework.start();
|
|
73
|
+
await waitFor(() => strategy.ticks >= 2);
|
|
74
|
+
assert.deepEqual(strategy.toolCounts, [1, 1]);
|
|
75
|
+
await waitFor(() => framework.getContextMaintenanceSnapshot().history.length === 1);
|
|
76
|
+
const snapshot = framework.getContextMaintenanceSnapshot();
|
|
77
|
+
assert.equal(snapshot.current, null);
|
|
78
|
+
assert.equal(snapshot.history[0].agents[0].ticks, 2);
|
|
79
|
+
assert.equal(snapshot.history[0].agents[0].readyAfter, true);
|
|
80
|
+
assert.deepEqual(snapshot.agents, [{ agentName: 'agent', ready: true }]);
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
await framework.stop();
|
|
84
|
+
rmSync(dir, { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
it('can be disabled explicitly', async () => {
|
|
88
|
+
const dir = mkdtempSync(join(tmpdir(), 'af-maintenance-disabled-'));
|
|
89
|
+
const strategy = new QueuedStrategy();
|
|
90
|
+
const framework = await AgentFramework.create({
|
|
91
|
+
storePath: join(dir, 'store'),
|
|
92
|
+
membrane,
|
|
93
|
+
agents: [{
|
|
94
|
+
name: 'agent',
|
|
95
|
+
model: 'test-model',
|
|
96
|
+
systemPrompt: 'test',
|
|
97
|
+
strategy,
|
|
98
|
+
}],
|
|
99
|
+
modules: [],
|
|
100
|
+
syncIntervalMs: 0,
|
|
101
|
+
maintenanceIntervalMs: 0,
|
|
102
|
+
});
|
|
103
|
+
try {
|
|
104
|
+
framework.start();
|
|
105
|
+
await new Promise(resolve => setTimeout(resolve, 40));
|
|
106
|
+
assert.equal(strategy.ticks, 0);
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
await framework.stop();
|
|
110
|
+
rmSync(dir, { recursive: true, force: true });
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
//# sourceMappingURL=maintenance-timer.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"maintenance-timer.test.js","sourceRoot":"","sources":["../../test/maintenance-timer.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcpl-reconnect-events.test.d.ts","sourceRoot":"","sources":["../../test/mcpl-reconnect-events.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* McplServerConnection reconnect lifecycle events.
|
|
3
|
+
*
|
|
4
|
+
* Covers the event contract the framework's wireMcplEvents turns into
|
|
5
|
+
* mcpl:server-* trace events:
|
|
6
|
+
* - 'connect-failed' buffered on the disconnected stub until ready()
|
|
7
|
+
* - 'reconnect-failed' per failed background attempt, with attempt ordinal
|
|
8
|
+
* - 'reconnect' on revival, with the attempt count that succeeded
|
|
9
|
+
* - 'close' with (code, signal) on unexpected child exit
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
12
|
+
import assert from 'node:assert';
|
|
13
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { McplServerConnection } from '../src/mcpl/server-connection.js';
|
|
16
|
+
// The .mjs fixture is not compiled/copied by tsc, so when this test runs from
|
|
17
|
+
// dist/test it must reach back into the source tree for it.
|
|
18
|
+
const FIXTURE = [
|
|
19
|
+
join(import.meta.dirname, 'fixtures/flaky-mcpl-server.mjs'),
|
|
20
|
+
join(import.meta.dirname, '../../test/fixtures/flaky-mcpl-server.mjs'),
|
|
21
|
+
].find((p) => existsSync(p));
|
|
22
|
+
const TMP_DIR = join(import.meta.dirname, '../.test-tmp-reconnect');
|
|
23
|
+
const FLAG_FILE = join(TMP_DIR, 'server-healthy');
|
|
24
|
+
const HOST_CAPS = {
|
|
25
|
+
version: '0.4',
|
|
26
|
+
pushEvents: true,
|
|
27
|
+
contextHooks: { beforeInference: true, afterInference: { blocking: true } },
|
|
28
|
+
featureSets: true,
|
|
29
|
+
};
|
|
30
|
+
function makeConfig(overrides) {
|
|
31
|
+
return {
|
|
32
|
+
id: 'flaky',
|
|
33
|
+
command: process.execPath,
|
|
34
|
+
args: [FIXTURE, FLAG_FILE],
|
|
35
|
+
reconnect: true,
|
|
36
|
+
reconnectIntervalMs: 50,
|
|
37
|
+
...overrides,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** Poll until `predicate` returns true or the deadline passes. */
|
|
41
|
+
async function waitFor(predicate, timeoutMs = 5000, label = 'condition') {
|
|
42
|
+
const deadline = Date.now() + timeoutMs;
|
|
43
|
+
while (!predicate()) {
|
|
44
|
+
if (Date.now() > deadline) {
|
|
45
|
+
throw new Error(`Timed out waiting for ${label}`);
|
|
46
|
+
}
|
|
47
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
let connection = null;
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
mkdirSync(TMP_DIR, { recursive: true });
|
|
53
|
+
});
|
|
54
|
+
afterEach(async () => {
|
|
55
|
+
await connection?.close();
|
|
56
|
+
connection = null;
|
|
57
|
+
if (existsSync(TMP_DIR))
|
|
58
|
+
rmSync(TMP_DIR, { recursive: true, force: true });
|
|
59
|
+
});
|
|
60
|
+
describe('McplServerConnection — reconnect lifecycle events', () => {
|
|
61
|
+
it('buffers connect-failed on the stub, emits reconnect-failed per retry, then reconnect on recovery', async () => {
|
|
62
|
+
// Flag file absent: the initial connect fails and we get a stub.
|
|
63
|
+
connection = await McplServerConnection.connectWithReconnect(makeConfig(), HOST_CAPS);
|
|
64
|
+
assert.strictEqual(connection.capabilities, null);
|
|
65
|
+
const connectFailed = [];
|
|
66
|
+
const reconnectFailed = [];
|
|
67
|
+
const reconnected = [];
|
|
68
|
+
connection.on('connect-failed', (p) => connectFailed.push(p));
|
|
69
|
+
connection.on('reconnect-failed', (p) => reconnectFailed.push(p));
|
|
70
|
+
connection.on('reconnect', (p) => reconnected.push(p));
|
|
71
|
+
// The initial failure fired before listeners existed — ready() flushes it.
|
|
72
|
+
connection.ready();
|
|
73
|
+
assert.strictEqual(connectFailed.length, 1);
|
|
74
|
+
assert.strictEqual(connectFailed[0].attempt, 0);
|
|
75
|
+
assert.match(connectFailed[0].error, /before handshake/);
|
|
76
|
+
// At least one background retry fails while the server stays down.
|
|
77
|
+
await waitFor(() => reconnectFailed.length >= 1, 5000, 'first reconnect-failed');
|
|
78
|
+
assert.strictEqual(reconnectFailed[0].attempt, 1);
|
|
79
|
+
// Bring the server up; the next attempt succeeds.
|
|
80
|
+
writeFileSync(FLAG_FILE, '');
|
|
81
|
+
await waitFor(() => reconnected.length === 1, 10_000, 'reconnect');
|
|
82
|
+
assert.ok(reconnected[0].attempts >= 2, `reconnect should report the attempt ordinal that succeeded, got ${reconnected[0].attempts}`);
|
|
83
|
+
assert.strictEqual(connection.willReconnect, true);
|
|
84
|
+
});
|
|
85
|
+
it('emits close with (code, signal) on unexpected exit, then revives with attempts=1', async () => {
|
|
86
|
+
writeFileSync(FLAG_FILE, '');
|
|
87
|
+
connection = await McplServerConnection.connect(makeConfig(), HOST_CAPS);
|
|
88
|
+
const closes = [];
|
|
89
|
+
const reconnected = [];
|
|
90
|
+
connection.on('close', (code, signal) => closes.push({ code: code ?? null, signal: signal ?? null }));
|
|
91
|
+
connection.on('reconnect', (p) => reconnected.push(p));
|
|
92
|
+
connection.ready();
|
|
93
|
+
// The fixture exits(7) when it receives tools/list — a remote-controlled
|
|
94
|
+
// crash. The pending request rejects when the process dies.
|
|
95
|
+
connection.sendToolsList().catch(() => { });
|
|
96
|
+
await waitFor(() => closes.length === 1, 5000, 'close event');
|
|
97
|
+
assert.strictEqual(closes[0].code, 7);
|
|
98
|
+
assert.strictEqual(closes[0].signal, null);
|
|
99
|
+
assert.strictEqual(connection.willReconnect, true);
|
|
100
|
+
// The server is still healthy (flag file present), so the first retry wins.
|
|
101
|
+
await waitFor(() => reconnected.length === 1, 5000, 'reconnect after crash');
|
|
102
|
+
assert.strictEqual(reconnected[0].attempts, 1);
|
|
103
|
+
});
|
|
104
|
+
it('explicit close() disables reconnect and reports willReconnect=false', async () => {
|
|
105
|
+
writeFileSync(FLAG_FILE, '');
|
|
106
|
+
connection = await McplServerConnection.connect(makeConfig(), HOST_CAPS);
|
|
107
|
+
connection.ready();
|
|
108
|
+
const reconnected = [];
|
|
109
|
+
connection.on('reconnect', (p) => reconnected.push(p));
|
|
110
|
+
assert.strictEqual(connection.willReconnect, true);
|
|
111
|
+
await connection.close();
|
|
112
|
+
assert.strictEqual(connection.willReconnect, false);
|
|
113
|
+
// No revival after an explicit close.
|
|
114
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
115
|
+
assert.strictEqual(reconnected.length, 0);
|
|
116
|
+
connection = null;
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
//# sourceMappingURL=mcpl-reconnect-events.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcpl-reconnect-events.test.js","sourceRoot":"","sources":["../../test/mcpl-reconnect-events.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcpl-reconnect-reregistration.test.d.ts","sourceRoot":"","sources":["../../test/mcpl-reconnect-reregistration.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCPL auto-reconnect must RE-REGISTER the server, not just refresh tools.
|
|
3
|
+
*
|
|
4
|
+
* Before the fix, the 'close' handler removed the server from the
|
|
5
|
+
* FeatureSetManager (and destroyed its checkpoint trees), while the
|
|
6
|
+
* 'reconnect' handler only re-listed tools. Result: after any transient
|
|
7
|
+
* crash + successful reconnect, validateInbound threw "Unknown server"
|
|
8
|
+
* forever — every push event and inference request rejected until a full
|
|
9
|
+
* host restart — and the server's durable checkpoint state was gone.
|
|
10
|
+
*
|
|
11
|
+
* These tests exercise the framework's wireMcplEvents close/reconnect
|
|
12
|
+
* handlers with the REAL FeatureSetManager / CheckpointManager / PushHandler
|
|
13
|
+
* and a fake connection (EventEmitter), asserting:
|
|
14
|
+
* 1. a push event after close→reconnect is ACCEPTED again;
|
|
15
|
+
* 2. checkpoint state SURVIVES any close (transient OR clean shutdown);
|
|
16
|
+
* 3. checkpoint state is destroyed only via disconnectMcplServer (explicit
|
|
17
|
+
* permanent removal), never by the close handler.
|
|
18
|
+
*/
|
|
19
|
+
import { test } from 'node:test';
|
|
20
|
+
import assert from 'node:assert/strict';
|
|
21
|
+
import { EventEmitter } from 'node:events';
|
|
22
|
+
import { AgentFramework } from '../src/framework.js';
|
|
23
|
+
import { FeatureSetManager } from '../src/mcpl/feature-set-manager.js';
|
|
24
|
+
import { ScopeManager } from '../src/mcpl/scope-manager.js';
|
|
25
|
+
import { CheckpointManager } from '../src/mcpl/checkpoint-manager.js';
|
|
26
|
+
import { PushHandler } from '../src/mcpl/push-handler.js';
|
|
27
|
+
/** In-memory JsStore stub — CheckpointManager only needs the state-slot API. */
|
|
28
|
+
function makeStoreStub() {
|
|
29
|
+
const slots = new Map();
|
|
30
|
+
return {
|
|
31
|
+
slots,
|
|
32
|
+
registerState: (_opts) => { },
|
|
33
|
+
setStateJson: (id, value) => { slots.set(id, structuredClone(value)); },
|
|
34
|
+
getStateJson: (id) => slots.get(id) ?? null,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const CAPABILITIES = {
|
|
38
|
+
version: '0.4',
|
|
39
|
+
featureSets: {
|
|
40
|
+
chat: { description: 'chat events', uses: ['pushEvents'] },
|
|
41
|
+
mem: { description: 'stateful memory', uses: ['tools'], hostState: true, rollback: true },
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
class FakeConnection extends EventEmitter {
|
|
45
|
+
id;
|
|
46
|
+
capabilities = CAPABILITIES;
|
|
47
|
+
willReconnect = true;
|
|
48
|
+
featureSetUpdates = [];
|
|
49
|
+
constructor(id) {
|
|
50
|
+
super();
|
|
51
|
+
this.id = id;
|
|
52
|
+
}
|
|
53
|
+
sendFeatureSetsUpdate(params) {
|
|
54
|
+
this.featureSetUpdates.push(params);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function makeHarness() {
|
|
58
|
+
const store = makeStoreStub();
|
|
59
|
+
const traces = [];
|
|
60
|
+
const pushed = [];
|
|
61
|
+
const fw = Object.create(AgentFramework.prototype);
|
|
62
|
+
fw.traceListeners = [];
|
|
63
|
+
fw.consecutiveInferenceFailures = new Map();
|
|
64
|
+
fw.exhaustionRewinds = new Map();
|
|
65
|
+
fw.agents = new Map();
|
|
66
|
+
fw.mcplTools = [];
|
|
67
|
+
fw.mcplToolRefreshInFlight = false;
|
|
68
|
+
fw.mcplToolRefreshPending = false;
|
|
69
|
+
fw.mcplServerRegistry = null; // handleToolsListChanged no-ops without it
|
|
70
|
+
fw.channelRegistry = null;
|
|
71
|
+
fw.inferenceRouter = null;
|
|
72
|
+
fw.eventGate = null;
|
|
73
|
+
fw.featureSetManager = new FeatureSetManager();
|
|
74
|
+
fw.scopeManager = new ScopeManager();
|
|
75
|
+
fw.checkpointManager = new CheckpointManager(store, (e) => traces.push(e));
|
|
76
|
+
fw.pushHandler = new PushHandler(fw.featureSetManager, (e) => pushed.push(e), (e) => traces.push(e));
|
|
77
|
+
const config = {
|
|
78
|
+
id: 'srv',
|
|
79
|
+
command: 'unused',
|
|
80
|
+
enabledFeatureSets: ['chat', 'mem'],
|
|
81
|
+
reconnect: true,
|
|
82
|
+
};
|
|
83
|
+
fw.mcplServerConfigs = new Map([[config.id, config]]);
|
|
84
|
+
const connection = new FakeConnection('srv');
|
|
85
|
+
fw.wireMcplEvents(connection);
|
|
86
|
+
fw.registerMcplServerFeatures(config, connection);
|
|
87
|
+
const sendPush = (eventId) => {
|
|
88
|
+
let result;
|
|
89
|
+
connection.emit('push-event', {
|
|
90
|
+
featureSet: 'chat',
|
|
91
|
+
eventId,
|
|
92
|
+
timestamp: new Date().toISOString(),
|
|
93
|
+
payload: { content: [{ type: 'text', text: 'hello' }] },
|
|
94
|
+
}, {
|
|
95
|
+
respond: (r) => { result = r; },
|
|
96
|
+
respondError: () => { assert.fail('push responded with a JSON-RPC error'); },
|
|
97
|
+
});
|
|
98
|
+
assert.ok(result, 'push handler must respond synchronously');
|
|
99
|
+
return result;
|
|
100
|
+
};
|
|
101
|
+
return { fw, store, connection, config, sendPush, pushed, traces };
|
|
102
|
+
}
|
|
103
|
+
test('reconnect re-registers the server: pushes are accepted again after close→reconnect', () => {
|
|
104
|
+
const { fw, connection, sendPush, pushed } = makeHarness();
|
|
105
|
+
// Sanity: initial registration accepts pushes.
|
|
106
|
+
assert.equal(sendPush('e1').accepted, true);
|
|
107
|
+
assert.equal(pushed.length, 1);
|
|
108
|
+
// Transient crash: transport closed, background reconnect pending.
|
|
109
|
+
connection.willReconnect = true;
|
|
110
|
+
connection.emit('close', null, 'SIGKILL');
|
|
111
|
+
// While down, the server is deregistered — pushes are rejected.
|
|
112
|
+
const down = sendPush('e2');
|
|
113
|
+
assert.equal(down.accepted, false);
|
|
114
|
+
assert.match(down.reason ?? '', /Unknown server/);
|
|
115
|
+
// Reconnect succeeded (fresh handshake refreshed capabilities).
|
|
116
|
+
connection.emit('reconnect', { attempts: 1 });
|
|
117
|
+
// THE regression: before the fix this stayed rejected forever.
|
|
118
|
+
const revived = sendPush('e3');
|
|
119
|
+
assert.equal(revived.accepted, true, `push after reconnect must be accepted, got: ${revived.reason}`);
|
|
120
|
+
assert.equal(pushed.length, 2);
|
|
121
|
+
// The server was told its enabled feature sets again on re-registration.
|
|
122
|
+
assert.equal(connection.featureSetUpdates.length, 2, 'featureSets/update re-sent on reconnect');
|
|
123
|
+
assert.ok(fw.featureSetManager.isEnabled('srv', 'chat'));
|
|
124
|
+
});
|
|
125
|
+
test('checkpoint state SURVIVES a transient close + reconnect', () => {
|
|
126
|
+
const { fw, store, connection } = makeHarness();
|
|
127
|
+
// Record durable checkpoint state for the stateful feature set.
|
|
128
|
+
fw.checkpointManager.recordCheckpoint('srv', 'mem', {
|
|
129
|
+
checkpoint: 'cp1',
|
|
130
|
+
data: { counter: 42 },
|
|
131
|
+
});
|
|
132
|
+
assert.equal(fw.checkpointManager.getCurrentCheckpoint('srv', 'mem'), 'cp1');
|
|
133
|
+
// Transient close: reconnect loop is active.
|
|
134
|
+
connection.willReconnect = true;
|
|
135
|
+
connection.emit('close', 1, null);
|
|
136
|
+
assert.equal(fw.checkpointManager.getCurrentCheckpoint('srv', 'mem'), 'cp1', 'a transient disconnect must NOT destroy checkpoint trees');
|
|
137
|
+
const persisted = store.slots.get('mcpl/checkpoints');
|
|
138
|
+
assert.ok(persisted.trees['srv:mem'], 'the persisted tree must not be deleted from Chronicle');
|
|
139
|
+
// Reconnect: idempotent re-registration resumes (not resets) the tree.
|
|
140
|
+
connection.emit('reconnect', { attempts: 1 });
|
|
141
|
+
assert.equal(fw.checkpointManager.getCurrentCheckpoint('srv', 'mem'), 'cp1');
|
|
142
|
+
assert.deepEqual(fw.checkpointManager.getCurrentState('srv', 'mem'), { counter: 42 });
|
|
143
|
+
});
|
|
144
|
+
test('the close handler NEVER destroys checkpoints — even with willReconnect=false', () => {
|
|
145
|
+
// A clean AgentFramework.stop() sets reconnectEnabled=false BEFORE emitting
|
|
146
|
+
// 'close', so willReconnect is false on an ordinary host restart just as much
|
|
147
|
+
// as on a permanent teardown. Gating checkpoint destruction on willReconnect
|
|
148
|
+
// would therefore erase every durable checkpoint tree on a polite restart
|
|
149
|
+
// (while a SIGKILL, whose 'close' carries willReconnect=true, preserved them).
|
|
150
|
+
// Permanent removal is owned solely by disconnectMcplServer; the close handler
|
|
151
|
+
// must leave the persisted tree intact for loadFromStore() to resume.
|
|
152
|
+
const { fw, store, connection } = makeHarness();
|
|
153
|
+
fw.checkpointManager.recordCheckpoint('srv', 'mem', { checkpoint: 'cp1', data: { x: 1 } });
|
|
154
|
+
// Even a "permanent-looking" close (reconnect disabled) must not delete state.
|
|
155
|
+
connection.willReconnect = false;
|
|
156
|
+
connection.emit('close', 0, null);
|
|
157
|
+
assert.equal(fw.checkpointManager.getCurrentCheckpoint('srv', 'mem'), 'cp1', 'the close handler must not destroy checkpoints on a clean shutdown');
|
|
158
|
+
const persisted = store.slots.get('mcpl/checkpoints');
|
|
159
|
+
assert.ok(persisted.trees['srv:mem'], 'the persisted tree survives a clean shutdown for later resume');
|
|
160
|
+
});
|
|
161
|
+
test('disconnectMcplServer destroys checkpoints even when the connection already closed transiently', async () => {
|
|
162
|
+
const { fw, connection, config } = makeHarness();
|
|
163
|
+
fw.checkpointManager.recordCheckpoint('srv', 'mem', { checkpoint: 'cp1', data: { x: 1 } });
|
|
164
|
+
// Transient close first (checkpoints preserved for the pending reconnect)…
|
|
165
|
+
connection.willReconnect = true;
|
|
166
|
+
connection.emit('close', 1, null);
|
|
167
|
+
assert.equal(fw.checkpointManager.getCurrentCheckpoint('srv', 'mem'), 'cp1');
|
|
168
|
+
// …then the operator permanently removes the server. close() on an
|
|
169
|
+
// already-closed connection emits no second 'close', so the explicit
|
|
170
|
+
// cleanup in disconnectMcplServer must handle it.
|
|
171
|
+
fw.mcplServerRegistry = {
|
|
172
|
+
removeServer: async () => { },
|
|
173
|
+
getAllServers: () => [],
|
|
174
|
+
getServer: () => null,
|
|
175
|
+
};
|
|
176
|
+
fw.mcplPrefixMap = new Map([[`mcpl--srv`, 'srv']]);
|
|
177
|
+
await fw.disconnectMcplServer(config.id);
|
|
178
|
+
assert.equal(fw.checkpointManager.getCurrentCheckpoint('srv', 'mem'), null);
|
|
179
|
+
assert.equal(fw.featureSetManager.isEnabled('srv', 'chat'), false);
|
|
180
|
+
});
|
|
181
|
+
//# sourceMappingURL=mcpl-reconnect-reregistration.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcpl-reconnect-reregistration.test.js","sourceRoot":"","sources":["../../test/mcpl-reconnect-reregistration.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcpl-request-timeout.test.d.ts","sourceRoot":"","sources":["../../test/mcpl-request-timeout.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCPL per-request timeout (6.2): a live-but-stuck server — accepts a
|
|
3
|
+
* request, never responds, never closes its transport — must not freeze the
|
|
4
|
+
* awaiting caller forever. sendRequest rejects after requestTimeoutMs with a
|
|
5
|
+
* descriptive error; the framework maps that rejection to an isError
|
|
6
|
+
* tool_result, so the agent turn completes instead of hanging.
|
|
7
|
+
*/
|
|
8
|
+
import { test, afterEach } from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import { McplServerConnection } from '../src/mcpl/server-connection.js';
|
|
11
|
+
// Stdio server: answers initialize and tools/list, but swallows tools/call —
|
|
12
|
+
// the "zulip HTML-error / event-loop-wedge" profile (alive, deaf).
|
|
13
|
+
const HUNG_SERVER = `
|
|
14
|
+
let buf = '';
|
|
15
|
+
process.stdin.on('data', (c) => {
|
|
16
|
+
buf += c.toString('utf8');
|
|
17
|
+
let i;
|
|
18
|
+
while ((i = buf.indexOf('\\n')) >= 0) {
|
|
19
|
+
const line = buf.slice(0, i); buf = buf.slice(i + 1);
|
|
20
|
+
if (!line.trim()) continue;
|
|
21
|
+
let m; try { m = JSON.parse(line); } catch { continue; }
|
|
22
|
+
const reply = (result) => process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: m.id, result }) + '\\n');
|
|
23
|
+
if (m.method === 'initialize') reply({ capabilities: {} });
|
|
24
|
+
else if (m.method === 'tools/list') reply({ tools: [{ name: 'stuck', description: 's', inputSchema: { type: 'object' } }] });
|
|
25
|
+
// tools/call: deliberately NO response, connection stays open.
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
setInterval(() => {}, 1 << 30); // stay alive
|
|
29
|
+
`;
|
|
30
|
+
const HOST_CAPS = {
|
|
31
|
+
version: '0.4',
|
|
32
|
+
pushEvents: true,
|
|
33
|
+
contextHooks: { beforeInference: true, afterInference: { blocking: true } },
|
|
34
|
+
featureSets: true,
|
|
35
|
+
};
|
|
36
|
+
function config(overrides) {
|
|
37
|
+
return {
|
|
38
|
+
id: 'hung',
|
|
39
|
+
command: process.execPath,
|
|
40
|
+
args: ['-e', HUNG_SERVER],
|
|
41
|
+
...overrides,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
let connection = null;
|
|
45
|
+
afterEach(async () => {
|
|
46
|
+
await connection?.close();
|
|
47
|
+
connection = null;
|
|
48
|
+
});
|
|
49
|
+
test('tools/call against a hung server rejects within the configured timeout', async () => {
|
|
50
|
+
connection = await McplServerConnection.connect(config({ requestTimeoutMs: 250 }), HOST_CAPS);
|
|
51
|
+
connection.ready();
|
|
52
|
+
const started = Date.now();
|
|
53
|
+
await assert.rejects(connection.sendToolsCall('stuck', {}), (err) => {
|
|
54
|
+
assert.match(err.message, /did not respond to tools\/call/);
|
|
55
|
+
assert.match(err.message, /250ms/);
|
|
56
|
+
assert.match(err.message, /hung/);
|
|
57
|
+
return true;
|
|
58
|
+
});
|
|
59
|
+
const elapsed = Date.now() - started;
|
|
60
|
+
assert.ok(elapsed >= 200 && elapsed < 5000, `rejected in ${elapsed}ms (expected ~250ms)`);
|
|
61
|
+
// The connection survives the abandoned request: a request the server DOES
|
|
62
|
+
// answer still works afterwards.
|
|
63
|
+
const list = await connection.sendToolsList();
|
|
64
|
+
assert.equal(list.tools[0]?.name, 'stuck');
|
|
65
|
+
});
|
|
66
|
+
test('a request the server answers in time is unaffected by the timeout', async () => {
|
|
67
|
+
connection = await McplServerConnection.connect(config({ requestTimeoutMs: 2000 }), HOST_CAPS);
|
|
68
|
+
connection.ready();
|
|
69
|
+
const list = await connection.sendToolsList();
|
|
70
|
+
assert.equal(list.tools.length, 1);
|
|
71
|
+
});
|
|
72
|
+
test('requestTimeoutMs: 0 disables the per-request watchdog', async () => {
|
|
73
|
+
connection = await McplServerConnection.connect(config({ requestTimeoutMs: 0 }), HOST_CAPS);
|
|
74
|
+
connection.ready();
|
|
75
|
+
let settled = false;
|
|
76
|
+
const call = connection.sendToolsCall('stuck', {}).catch(() => { }).finally(() => { settled = true; });
|
|
77
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
78
|
+
assert.equal(settled, false, 'with timeout disabled the request stays pending');
|
|
79
|
+
// Cleanup: closing the connection rejects the pending request.
|
|
80
|
+
await connection.close();
|
|
81
|
+
connection = null;
|
|
82
|
+
await call;
|
|
83
|
+
assert.equal(settled, true);
|
|
84
|
+
});
|
|
85
|
+
test('the timeout error warns the model the tool may have completed server-side', async () => {
|
|
86
|
+
connection = await McplServerConnection.connect(config({ requestTimeoutMs: 150 }), HOST_CAPS);
|
|
87
|
+
connection.ready();
|
|
88
|
+
await assert.rejects(connection.sendToolsCall('stuck', {}), (err) => {
|
|
89
|
+
// The model reads this string; it must not imply the call was cancelled.
|
|
90
|
+
assert.match(err.message, /may still have completed server-side/);
|
|
91
|
+
assert.match(err.message, /verify state before retrying/);
|
|
92
|
+
return true;
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
test('a timed-out request that later receives a response does not crash (orphaned response ignored)', async () => {
|
|
96
|
+
// Server that answers tools/call after 500ms — beyond a 100ms timeout.
|
|
97
|
+
const SLOW_SERVER = HUNG_SERVER.replace('// tools/call: deliberately NO response, connection stays open.', "else if (m.method === 'tools/call') setTimeout(() => reply({ content: [] }), 500);");
|
|
98
|
+
connection = await McplServerConnection.connect({ id: 'slow', command: process.execPath, args: ['-e', SLOW_SERVER], requestTimeoutMs: 100 }, HOST_CAPS);
|
|
99
|
+
connection.ready();
|
|
100
|
+
await assert.rejects(connection.sendToolsCall('stuck', {}), /did not respond/);
|
|
101
|
+
// Wait past the server's late response; it must be ignored, not crash.
|
|
102
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
103
|
+
const list = await connection.sendToolsList();
|
|
104
|
+
assert.equal(list.tools.length, 1);
|
|
105
|
+
});
|
|
106
|
+
test('a late STATEFUL orphaned response is surfaced via mcpl:orphaned-response (not silently dropped)', async () => {
|
|
107
|
+
// Server answers tools/call after 500ms with a checkpoint-carrying result —
|
|
108
|
+
// beyond a 100ms timeout. The host advanced no checkpoint; the server did.
|
|
109
|
+
// That divergence must be greppable, so the connection emits an event.
|
|
110
|
+
const STATEFUL_SLOW = HUNG_SERVER.replace('// tools/call: deliberately NO response, connection stays open.', "else if (m.method === 'tools/call') setTimeout(() => reply({ content: [], state: { checkpoint: 'cp-late', data: { n: 1 } } }), 500);");
|
|
111
|
+
connection = await McplServerConnection.connect({ id: 'slow-stateful', command: process.execPath, args: ['-e', STATEFUL_SLOW], requestTimeoutMs: 100 }, HOST_CAPS);
|
|
112
|
+
connection.ready();
|
|
113
|
+
const orphaned = [];
|
|
114
|
+
connection.on('orphaned-response', (info) => orphaned.push(info));
|
|
115
|
+
await assert.rejects(connection.sendToolsCall('stuck', {}), /did not respond/);
|
|
116
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
117
|
+
assert.equal(orphaned.length, 1, 'the dropped stateful response must be surfaced');
|
|
118
|
+
assert.equal(orphaned[0].hadState, true, 'the event records that state was carried');
|
|
119
|
+
// The connection still works afterwards.
|
|
120
|
+
const list = await connection.sendToolsList();
|
|
121
|
+
assert.equal(list.tools.length, 1);
|
|
122
|
+
});
|
|
123
|
+
//# sourceMappingURL=mcpl-request-timeout.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcpl-request-timeout.test.js","sourceRoot":"","sources":["../../test/mcpl-request-timeout.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime MCPL server lifecycle — connectMcplServer / disconnectMcplServer /
|
|
3
|
+
* restartMcplServer on a live framework, including lazy MCPL subsystem
|
|
4
|
+
* initialization when the framework started with zero configured servers.
|
|
5
|
+
*/
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=mcpl-runtime-lifecycle.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcpl-runtime-lifecycle.test.d.ts","sourceRoot":"","sources":["../../test/mcpl-runtime-lifecycle.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|