@animalabs/agent-framework 0.1.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/LICENSE +21 -0
- package/README.md +233 -0
- package/dist/src/agent.d.ts +130 -0
- package/dist/src/agent.d.ts.map +1 -0
- package/dist/src/agent.js +406 -0
- package/dist/src/agent.js.map +1 -0
- package/dist/src/api/mcp-server.d.ts +37 -0
- package/dist/src/api/mcp-server.d.ts.map +1 -0
- package/dist/src/api/mcp-server.js +598 -0
- package/dist/src/api/mcp-server.js.map +1 -0
- package/dist/src/api/server.d.ts +83 -0
- package/dist/src/api/server.d.ts.map +1 -0
- package/dist/src/api/server.js +785 -0
- package/dist/src/api/server.js.map +1 -0
- package/dist/src/api/types.d.ts +395 -0
- package/dist/src/api/types.d.ts.map +1 -0
- package/dist/src/api/types.js +5 -0
- package/dist/src/api/types.js.map +1 -0
- package/dist/src/framework.d.ts +309 -0
- package/dist/src/framework.d.ts.map +1 -0
- package/dist/src/framework.js +2309 -0
- package/dist/src/framework.js.map +1 -0
- package/dist/src/gate/event-gate.d.ts +75 -0
- package/dist/src/gate/event-gate.d.ts.map +1 -0
- package/dist/src/gate/event-gate.js +503 -0
- package/dist/src/gate/event-gate.js.map +1 -0
- package/dist/src/gate/index.d.ts +3 -0
- package/dist/src/gate/index.d.ts.map +1 -0
- package/dist/src/gate/index.js +2 -0
- package/dist/src/gate/index.js.map +1 -0
- package/dist/src/gate/types.d.ts +82 -0
- package/dist/src/gate/types.d.ts.map +1 -0
- package/dist/src/gate/types.js +9 -0
- package/dist/src/gate/types.js.map +1 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +18 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/mcpl/channel-registry.d.ts +141 -0
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -0
- package/dist/src/mcpl/channel-registry.js +647 -0
- package/dist/src/mcpl/channel-registry.js.map +1 -0
- package/dist/src/mcpl/checkpoint-manager.d.ts +79 -0
- package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -0
- package/dist/src/mcpl/checkpoint-manager.js +430 -0
- package/dist/src/mcpl/checkpoint-manager.js.map +1 -0
- package/dist/src/mcpl/errors.d.ts +52 -0
- package/dist/src/mcpl/errors.d.ts.map +1 -0
- package/dist/src/mcpl/errors.js +93 -0
- package/dist/src/mcpl/errors.js.map +1 -0
- package/dist/src/mcpl/feature-set-manager.d.ts +91 -0
- package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -0
- package/dist/src/mcpl/feature-set-manager.js +250 -0
- package/dist/src/mcpl/feature-set-manager.js.map +1 -0
- package/dist/src/mcpl/hook-orchestrator.d.ts +51 -0
- package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -0
- package/dist/src/mcpl/hook-orchestrator.js +217 -0
- package/dist/src/mcpl/hook-orchestrator.js.map +1 -0
- package/dist/src/mcpl/index.d.ts +14 -0
- package/dist/src/mcpl/index.d.ts.map +1 -0
- package/dist/src/mcpl/index.js +21 -0
- package/dist/src/mcpl/index.js.map +1 -0
- package/dist/src/mcpl/inference-router.d.ts +67 -0
- package/dist/src/mcpl/inference-router.d.ts.map +1 -0
- package/dist/src/mcpl/inference-router.js +249 -0
- package/dist/src/mcpl/inference-router.js.map +1 -0
- package/dist/src/mcpl/push-handler.d.ts +60 -0
- package/dist/src/mcpl/push-handler.d.ts.map +1 -0
- package/dist/src/mcpl/push-handler.js +162 -0
- package/dist/src/mcpl/push-handler.js.map +1 -0
- package/dist/src/mcpl/scope-manager.d.ts +76 -0
- package/dist/src/mcpl/scope-manager.d.ts.map +1 -0
- package/dist/src/mcpl/scope-manager.js +213 -0
- package/dist/src/mcpl/scope-manager.js.map +1 -0
- package/dist/src/mcpl/server-connection.d.ts +174 -0
- package/dist/src/mcpl/server-connection.d.ts.map +1 -0
- package/dist/src/mcpl/server-connection.js +559 -0
- package/dist/src/mcpl/server-connection.js.map +1 -0
- package/dist/src/mcpl/server-registry.d.ts +63 -0
- package/dist/src/mcpl/server-registry.d.ts.map +1 -0
- package/dist/src/mcpl/server-registry.js +119 -0
- package/dist/src/mcpl/server-registry.js.map +1 -0
- package/dist/src/mcpl/types.d.ts +755 -0
- package/dist/src/mcpl/types.d.ts.map +1 -0
- package/dist/src/mcpl/types.js +48 -0
- package/dist/src/mcpl/types.js.map +1 -0
- package/dist/src/module-registry.d.ts +93 -0
- package/dist/src/module-registry.d.ts.map +1 -0
- package/dist/src/module-registry.js +386 -0
- package/dist/src/module-registry.js.map +1 -0
- package/dist/src/modules/api/index.d.ts +22 -0
- package/dist/src/modules/api/index.d.ts.map +1 -0
- package/dist/src/modules/api/index.js +63 -0
- package/dist/src/modules/api/index.js.map +1 -0
- package/dist/src/modules/discord/discord-js-client.d.ts +58 -0
- package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -0
- package/dist/src/modules/discord/discord-js-client.js +273 -0
- package/dist/src/modules/discord/discord-js-client.js.map +1 -0
- package/dist/src/modules/discord/index.d.ts +82 -0
- package/dist/src/modules/discord/index.d.ts.map +1 -0
- package/dist/src/modules/discord/index.js +959 -0
- package/dist/src/modules/discord/index.js.map +1 -0
- package/dist/src/modules/discord/types.d.ts +327 -0
- package/dist/src/modules/discord/types.d.ts.map +1 -0
- package/dist/src/modules/discord/types.js +5 -0
- package/dist/src/modules/discord/types.js.map +1 -0
- package/dist/src/modules/index.d.ts +11 -0
- package/dist/src/modules/index.d.ts.map +1 -0
- package/dist/src/modules/index.js +7 -0
- package/dist/src/modules/index.js.map +1 -0
- package/dist/src/modules/workspace/index.d.ts +59 -0
- package/dist/src/modules/workspace/index.d.ts.map +1 -0
- package/dist/src/modules/workspace/index.js +831 -0
- package/dist/src/modules/workspace/index.js.map +1 -0
- package/dist/src/modules/workspace/sync.d.ts +48 -0
- package/dist/src/modules/workspace/sync.d.ts.map +1 -0
- package/dist/src/modules/workspace/sync.js +253 -0
- package/dist/src/modules/workspace/sync.js.map +1 -0
- package/dist/src/modules/workspace/types.d.ts +166 -0
- package/dist/src/modules/workspace/types.d.ts.map +1 -0
- package/dist/src/modules/workspace/types.js +5 -0
- package/dist/src/modules/workspace/types.js.map +1 -0
- package/dist/src/modules/workspace/watcher.d.ts +43 -0
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -0
- package/dist/src/modules/workspace/watcher.js +120 -0
- package/dist/src/modules/workspace/watcher.js.map +1 -0
- package/dist/src/queue.d.ts +50 -0
- package/dist/src/queue.d.ts.map +1 -0
- package/dist/src/queue.js +95 -0
- package/dist/src/queue.js.map +1 -0
- package/dist/src/safe-slice.d.ts +8 -0
- package/dist/src/safe-slice.d.ts.map +1 -0
- package/dist/src/safe-slice.js +18 -0
- package/dist/src/safe-slice.js.map +1 -0
- package/dist/src/types/agent.d.ts +118 -0
- package/dist/src/types/agent.d.ts.map +1 -0
- package/dist/src/types/agent.js +2 -0
- package/dist/src/types/agent.js.map +1 -0
- package/dist/src/types/events.d.ts +220 -0
- package/dist/src/types/events.d.ts.map +1 -0
- package/dist/src/types/events.js +2 -0
- package/dist/src/types/events.js.map +1 -0
- package/dist/src/types/framework.d.ts +184 -0
- package/dist/src/types/framework.d.ts.map +1 -0
- package/dist/src/types/framework.js +2 -0
- package/dist/src/types/framework.js.map +1 -0
- package/dist/src/types/index.d.ts +7 -0
- package/dist/src/types/index.d.ts.map +1 -0
- package/dist/src/types/index.js +7 -0
- package/dist/src/types/index.js.map +1 -0
- package/dist/src/types/inference-log.d.ts +64 -0
- package/dist/src/types/inference-log.d.ts.map +1 -0
- package/dist/src/types/inference-log.js +5 -0
- package/dist/src/types/inference-log.js.map +1 -0
- package/dist/src/types/module.d.ts +302 -0
- package/dist/src/types/module.d.ts.map +1 -0
- package/dist/src/types/module.js +2 -0
- package/dist/src/types/module.js.map +1 -0
- package/dist/src/types/trace.d.ts +173 -0
- package/dist/src/types/trace.d.ts.map +1 -0
- package/dist/src/types/trace.js +2 -0
- package/dist/src/types/trace.js.map +1 -0
- package/dist/test/event-gate.test.d.ts +2 -0
- package/dist/test/event-gate.test.d.ts.map +1 -0
- package/dist/test/event-gate.test.js +535 -0
- package/dist/test/event-gate.test.js.map +1 -0
- package/dist/test/framework.test.d.ts +5 -0
- package/dist/test/framework.test.d.ts.map +1 -0
- package/dist/test/framework.test.js +738 -0
- package/dist/test/framework.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +58 -0
- package/src/agent.ts +512 -0
- package/src/api/mcp-server.ts +687 -0
- package/src/api/server.ts +1023 -0
- package/src/api/types.ts +542 -0
- package/src/framework.ts +2784 -0
- package/src/gate/event-gate.ts +623 -0
- package/src/gate/index.ts +12 -0
- package/src/gate/types.ts +112 -0
- package/src/index.ts +63 -0
- package/src/mcpl/channel-registry.ts +824 -0
- package/src/mcpl/checkpoint-manager.ts +545 -0
- package/src/mcpl/errors.ts +114 -0
- package/src/mcpl/feature-set-manager.ts +333 -0
- package/src/mcpl/hook-orchestrator.ts +281 -0
- package/src/mcpl/index.ts +134 -0
- package/src/mcpl/inference-router.ts +335 -0
- package/src/mcpl/push-handler.ts +232 -0
- package/src/mcpl/scope-manager.ts +262 -0
- package/src/mcpl/server-connection.ts +694 -0
- package/src/mcpl/server-registry.ts +144 -0
- package/src/mcpl/types.ts +991 -0
- package/src/module-registry.ts +547 -0
- package/src/modules/api/index.ts +94 -0
- package/src/modules/discord/discord-js-client.ts +353 -0
- package/src/modules/discord/index.ts +1146 -0
- package/src/modules/discord/types.ts +465 -0
- package/src/modules/index.ts +43 -0
- package/src/modules/workspace/index.ts +969 -0
- package/src/modules/workspace/sync.ts +314 -0
- package/src/modules/workspace/types.ts +202 -0
- package/src/modules/workspace/watcher.ts +142 -0
- package/src/queue.ts +106 -0
- package/src/safe-slice.ts +18 -0
- package/src/types/agent.ts +119 -0
- package/src/types/events.ts +252 -0
- package/src/types/framework.ts +211 -0
- package/src/types/index.ts +6 -0
- package/src/types/inference-log.ts +64 -0
- package/src/types/module.ts +367 -0
- package/src/types/trace.ts +196 -0
|
@@ -0,0 +1,738 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent framework integration tests
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, beforeEach } from 'node:test';
|
|
5
|
+
import assert from 'node:assert';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { AgentFramework, ProcessQueueImpl } from '../src/index.js';
|
|
10
|
+
/**
|
|
11
|
+
* Mock yielding stream for testing the streaming inference path.
|
|
12
|
+
* Takes a sequence of responses: first response starts the stream,
|
|
13
|
+
* subsequent responses resume after each tool round.
|
|
14
|
+
*/
|
|
15
|
+
class MockYieldingStream {
|
|
16
|
+
responses;
|
|
17
|
+
events = [];
|
|
18
|
+
_done = false;
|
|
19
|
+
_isWaitingForTools = false;
|
|
20
|
+
_pendingToolCallIds = [];
|
|
21
|
+
_toolDepth = 0;
|
|
22
|
+
pendingResolve = null;
|
|
23
|
+
receivedToolResults = [];
|
|
24
|
+
constructor(responses) {
|
|
25
|
+
this.responses = responses;
|
|
26
|
+
this.processResponse(0);
|
|
27
|
+
}
|
|
28
|
+
processResponse(index) {
|
|
29
|
+
const response = this.responses[index];
|
|
30
|
+
if (!response) {
|
|
31
|
+
this._done = true;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
// Emit tokens
|
|
35
|
+
const text = response.rawAssistantText;
|
|
36
|
+
if (text) {
|
|
37
|
+
this.events.push({
|
|
38
|
+
type: 'tokens',
|
|
39
|
+
content: text,
|
|
40
|
+
meta: { type: 'text', visible: true, blockIndex: 0 },
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
// Emit usage
|
|
44
|
+
if (response.usage) {
|
|
45
|
+
this.events.push({ type: 'usage', usage: response.usage });
|
|
46
|
+
}
|
|
47
|
+
// Tool calls or complete
|
|
48
|
+
if (response.toolCalls.length > 0) {
|
|
49
|
+
this._isWaitingForTools = true;
|
|
50
|
+
this._pendingToolCallIds = response.toolCalls.map((c) => c.id);
|
|
51
|
+
this.events.push({
|
|
52
|
+
type: 'tool-calls',
|
|
53
|
+
calls: response.toolCalls.map((tc) => ({
|
|
54
|
+
id: tc.id,
|
|
55
|
+
name: tc.name,
|
|
56
|
+
input: tc.input,
|
|
57
|
+
})),
|
|
58
|
+
context: {
|
|
59
|
+
rawText: '',
|
|
60
|
+
preamble: '',
|
|
61
|
+
depth: this._toolDepth,
|
|
62
|
+
previousResults: [],
|
|
63
|
+
accumulated: '',
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
this.events.push({ type: 'complete', response });
|
|
69
|
+
this._done = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
provideToolResults(results) {
|
|
73
|
+
if (!this._isWaitingForTools)
|
|
74
|
+
throw new Error('Not waiting for tools');
|
|
75
|
+
this.receivedToolResults.push(results);
|
|
76
|
+
this._isWaitingForTools = false;
|
|
77
|
+
this._pendingToolCallIds = [];
|
|
78
|
+
this._toolDepth++;
|
|
79
|
+
this.processResponse(this._toolDepth);
|
|
80
|
+
// Wake up the iterator
|
|
81
|
+
if (this.pendingResolve) {
|
|
82
|
+
const resolve = this.pendingResolve;
|
|
83
|
+
this.pendingResolve = null;
|
|
84
|
+
resolve();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
cancel() {
|
|
88
|
+
this._done = true;
|
|
89
|
+
this.events.push({ type: 'aborted', reason: 'user' });
|
|
90
|
+
if (this.pendingResolve) {
|
|
91
|
+
const resolve = this.pendingResolve;
|
|
92
|
+
this.pendingResolve = null;
|
|
93
|
+
resolve();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
get isWaitingForTools() { return this._isWaitingForTools; }
|
|
97
|
+
get pendingToolCallIds() { return [...this._pendingToolCallIds]; }
|
|
98
|
+
get toolDepth() { return this._toolDepth; }
|
|
99
|
+
async *[Symbol.asyncIterator]() {
|
|
100
|
+
while (true) {
|
|
101
|
+
while (this.events.length > 0) {
|
|
102
|
+
const event = this.events.shift();
|
|
103
|
+
yield event;
|
|
104
|
+
if (event.type === 'complete' || event.type === 'error' || event.type === 'aborted') {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (this._done)
|
|
109
|
+
return;
|
|
110
|
+
await new Promise((resolve) => { this.pendingResolve = resolve; });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function createMockResponse(content, stopReason = 'end_turn') {
|
|
115
|
+
const rawText = content
|
|
116
|
+
.filter((b) => b.type === 'text')
|
|
117
|
+
.map((b) => b.text)
|
|
118
|
+
.join('');
|
|
119
|
+
const toolCalls = content
|
|
120
|
+
.filter((b) => b.type === 'tool_use')
|
|
121
|
+
.map((b) => ({ id: b.id, name: b.name, input: b.input }));
|
|
122
|
+
return {
|
|
123
|
+
content,
|
|
124
|
+
stopReason,
|
|
125
|
+
rawAssistantText: rawText,
|
|
126
|
+
toolCalls,
|
|
127
|
+
toolResults: [],
|
|
128
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
129
|
+
details: {
|
|
130
|
+
stop: { reason: stopReason, wasTruncated: false },
|
|
131
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
132
|
+
timing: { totalDurationMs: 100, attempts: 1 },
|
|
133
|
+
model: { requested: 'test', actual: 'test', provider: 'mock' },
|
|
134
|
+
cache: { markersInRequest: 0, tokensCreated: 0, tokensRead: 0, hitRatio: 0 },
|
|
135
|
+
},
|
|
136
|
+
raw: { request: {}, response: {} },
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
class MockMembrane {
|
|
140
|
+
responses = [];
|
|
141
|
+
calls = [];
|
|
142
|
+
lastStream = null;
|
|
143
|
+
responseIndex = 0;
|
|
144
|
+
pushResponse(response) {
|
|
145
|
+
this.responses.push(response);
|
|
146
|
+
}
|
|
147
|
+
async complete(request) {
|
|
148
|
+
this.calls.push(request);
|
|
149
|
+
if (this.responseIndex >= this.responses.length) {
|
|
150
|
+
// Default response
|
|
151
|
+
return createMockResponse([{ type: 'text', text: 'Default response' }]);
|
|
152
|
+
}
|
|
153
|
+
return this.responses[this.responseIndex++];
|
|
154
|
+
}
|
|
155
|
+
streamYielding(request, _options) {
|
|
156
|
+
this.calls.push(request);
|
|
157
|
+
// All remaining queued responses feed the stream
|
|
158
|
+
const remaining = this.responses.slice(this.responseIndex);
|
|
159
|
+
this.responseIndex = this.responses.length;
|
|
160
|
+
const stream = new MockYieldingStream(remaining);
|
|
161
|
+
this.lastStream = stream;
|
|
162
|
+
return stream;
|
|
163
|
+
}
|
|
164
|
+
// Cast helper - use this when passing to framework
|
|
165
|
+
asMembrane() {
|
|
166
|
+
return this;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// ============================================================================
|
|
170
|
+
// Mock Module
|
|
171
|
+
// ============================================================================
|
|
172
|
+
class TestModule {
|
|
173
|
+
name = 'test';
|
|
174
|
+
ctx = null;
|
|
175
|
+
events = [];
|
|
176
|
+
toolCalls = [];
|
|
177
|
+
async start(ctx) {
|
|
178
|
+
this.ctx = ctx;
|
|
179
|
+
}
|
|
180
|
+
async stop() {
|
|
181
|
+
this.ctx = null;
|
|
182
|
+
}
|
|
183
|
+
getTools() {
|
|
184
|
+
return [
|
|
185
|
+
{
|
|
186
|
+
name: 'echo',
|
|
187
|
+
description: 'Echoes the input',
|
|
188
|
+
inputSchema: {
|
|
189
|
+
type: 'object',
|
|
190
|
+
properties: {
|
|
191
|
+
message: { type: 'string', description: 'Message to echo' },
|
|
192
|
+
},
|
|
193
|
+
required: ['message'],
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
];
|
|
197
|
+
}
|
|
198
|
+
async handleToolCall(call) {
|
|
199
|
+
this.toolCalls.push(call);
|
|
200
|
+
if (call.name === 'echo') {
|
|
201
|
+
const input = call.input;
|
|
202
|
+
return { success: true, data: { echoed: input.message } };
|
|
203
|
+
}
|
|
204
|
+
return { success: false, error: 'Unknown tool', isError: true };
|
|
205
|
+
}
|
|
206
|
+
async onProcess(event, _state) {
|
|
207
|
+
this.events.push(event);
|
|
208
|
+
// Handle external message by requesting inference
|
|
209
|
+
if (event.type === 'external-message') {
|
|
210
|
+
return {
|
|
211
|
+
addMessages: [
|
|
212
|
+
{
|
|
213
|
+
participant: 'User',
|
|
214
|
+
content: [{ type: 'text', text: String(event.content) }],
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
requestInference: true,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
return {};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// ============================================================================
|
|
224
|
+
// Tests
|
|
225
|
+
// ============================================================================
|
|
226
|
+
describe('ProcessQueueImpl', () => {
|
|
227
|
+
it('should push and pop events', () => {
|
|
228
|
+
const queue = new ProcessQueueImpl();
|
|
229
|
+
const event = {
|
|
230
|
+
type: 'timer-fired',
|
|
231
|
+
timerId: 'test',
|
|
232
|
+
reason: 'test',
|
|
233
|
+
};
|
|
234
|
+
queue.push(event);
|
|
235
|
+
assert.strictEqual(queue.depth, 1);
|
|
236
|
+
assert.strictEqual(queue.isEmpty, false);
|
|
237
|
+
const popped = queue.tryPop();
|
|
238
|
+
assert.deepStrictEqual(popped, event);
|
|
239
|
+
assert.strictEqual(queue.depth, 0);
|
|
240
|
+
assert.strictEqual(queue.isEmpty, true);
|
|
241
|
+
});
|
|
242
|
+
it('should return null when queue is empty', () => {
|
|
243
|
+
const queue = new ProcessQueueImpl();
|
|
244
|
+
assert.strictEqual(queue.tryPop(), null);
|
|
245
|
+
});
|
|
246
|
+
it('should peek without removing', () => {
|
|
247
|
+
const queue = new ProcessQueueImpl();
|
|
248
|
+
const event = {
|
|
249
|
+
type: 'timer-fired',
|
|
250
|
+
timerId: 'test',
|
|
251
|
+
reason: 'test',
|
|
252
|
+
};
|
|
253
|
+
queue.push(event);
|
|
254
|
+
assert.deepStrictEqual(queue.peek(), event);
|
|
255
|
+
assert.strictEqual(queue.depth, 1);
|
|
256
|
+
});
|
|
257
|
+
it('should clear all events', () => {
|
|
258
|
+
const queue = new ProcessQueueImpl();
|
|
259
|
+
queue.push({ type: 'timer-fired', timerId: '1', reason: 'test' });
|
|
260
|
+
queue.push({ type: 'timer-fired', timerId: '2', reason: 'test' });
|
|
261
|
+
queue.clear();
|
|
262
|
+
assert.strictEqual(queue.isEmpty, true);
|
|
263
|
+
});
|
|
264
|
+
it('should close and prevent further pushes', () => {
|
|
265
|
+
const queue = new ProcessQueueImpl();
|
|
266
|
+
queue.close();
|
|
267
|
+
assert.throws(() => {
|
|
268
|
+
queue.push({ type: 'timer-fired', timerId: 'test', reason: 'test' });
|
|
269
|
+
}, /Queue is closed/);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
describe('AgentFramework', () => {
|
|
273
|
+
let tempDir;
|
|
274
|
+
let membrane;
|
|
275
|
+
beforeEach(() => {
|
|
276
|
+
tempDir = mkdtempSync(join(tmpdir(), 'agent-framework-test-'));
|
|
277
|
+
membrane = new MockMembrane();
|
|
278
|
+
});
|
|
279
|
+
it('should create framework with agents and modules', async () => {
|
|
280
|
+
const testModule = new TestModule();
|
|
281
|
+
const framework = await AgentFramework.create({
|
|
282
|
+
storePath: join(tempDir, 'test.chronicle'),
|
|
283
|
+
membrane: membrane.asMembrane(),
|
|
284
|
+
agents: [
|
|
285
|
+
{
|
|
286
|
+
name: 'assistant',
|
|
287
|
+
model: 'test-model',
|
|
288
|
+
systemPrompt: 'You are a helpful assistant.',
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
modules: [testModule],
|
|
292
|
+
});
|
|
293
|
+
assert.notStrictEqual(framework.getAgent('assistant'), null);
|
|
294
|
+
assert.deepStrictEqual(framework.getAllAgents().map((a) => a.name), ['assistant']);
|
|
295
|
+
assert.notStrictEqual(testModule.ctx, null);
|
|
296
|
+
await framework.stop();
|
|
297
|
+
rmSync(tempDir, { recursive: true });
|
|
298
|
+
});
|
|
299
|
+
it('should route external messages and trigger inference', async () => {
|
|
300
|
+
const testModule = new TestModule();
|
|
301
|
+
// Set up response
|
|
302
|
+
membrane.pushResponse(createMockResponse([{ type: 'text', text: 'Hello! How can I help?' }]));
|
|
303
|
+
const framework = await AgentFramework.create({
|
|
304
|
+
storePath: join(tempDir, 'test.chronicle'),
|
|
305
|
+
membrane: membrane.asMembrane(),
|
|
306
|
+
agents: [
|
|
307
|
+
{
|
|
308
|
+
name: 'assistant',
|
|
309
|
+
model: 'test-model',
|
|
310
|
+
systemPrompt: 'You are a helpful assistant.',
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
modules: [testModule],
|
|
314
|
+
});
|
|
315
|
+
// Push an external message event
|
|
316
|
+
framework.pushEvent({
|
|
317
|
+
type: 'external-message',
|
|
318
|
+
source: 'test',
|
|
319
|
+
content: 'Hello!',
|
|
320
|
+
metadata: {},
|
|
321
|
+
});
|
|
322
|
+
// Run until idle
|
|
323
|
+
await framework.runUntilIdle();
|
|
324
|
+
// Check that module received the event
|
|
325
|
+
assert.strictEqual(testModule.events.length, 1);
|
|
326
|
+
assert.strictEqual(testModule.events[0].type, 'external-message');
|
|
327
|
+
// Check that membrane was called
|
|
328
|
+
assert.strictEqual(membrane.calls.length, 1);
|
|
329
|
+
assert.strictEqual(membrane.calls[0].system, 'You are a helpful assistant.');
|
|
330
|
+
await framework.stop();
|
|
331
|
+
rmSync(tempDir, { recursive: true });
|
|
332
|
+
});
|
|
333
|
+
it('should handle tool calls', async () => {
|
|
334
|
+
const testModule = new TestModule();
|
|
335
|
+
// First response with tool call
|
|
336
|
+
membrane.pushResponse(createMockResponse([
|
|
337
|
+
{ type: 'text', text: 'Let me echo that for you.' },
|
|
338
|
+
{
|
|
339
|
+
type: 'tool_use',
|
|
340
|
+
id: 'call_1',
|
|
341
|
+
name: 'test--echo',
|
|
342
|
+
input: { message: 'test message' },
|
|
343
|
+
},
|
|
344
|
+
], 'tool_use'));
|
|
345
|
+
// Second response after tool result
|
|
346
|
+
membrane.pushResponse(createMockResponse([
|
|
347
|
+
{ type: 'text', text: 'The echoed message is: test message' },
|
|
348
|
+
]));
|
|
349
|
+
const framework = await AgentFramework.create({
|
|
350
|
+
storePath: join(tempDir, 'test.chronicle'),
|
|
351
|
+
membrane: membrane.asMembrane(),
|
|
352
|
+
agents: [
|
|
353
|
+
{
|
|
354
|
+
name: 'assistant',
|
|
355
|
+
model: 'test-model',
|
|
356
|
+
systemPrompt: 'You are a helpful assistant.',
|
|
357
|
+
},
|
|
358
|
+
],
|
|
359
|
+
modules: [testModule],
|
|
360
|
+
});
|
|
361
|
+
// Push message and run
|
|
362
|
+
framework.pushEvent({
|
|
363
|
+
type: 'external-message',
|
|
364
|
+
source: 'test',
|
|
365
|
+
content: 'Echo this!',
|
|
366
|
+
metadata: {},
|
|
367
|
+
});
|
|
368
|
+
await framework.runUntilIdle();
|
|
369
|
+
// Check tool was called
|
|
370
|
+
assert.strictEqual(testModule.toolCalls.length, 1);
|
|
371
|
+
assert.strictEqual(testModule.toolCalls[0].name, 'echo');
|
|
372
|
+
assert.deepStrictEqual(testModule.toolCalls[0].input, { message: 'test message' });
|
|
373
|
+
// With streaming, membrane is called once (streamYielding handles tool rounds internally)
|
|
374
|
+
assert.strictEqual(membrane.calls.length, 1);
|
|
375
|
+
await framework.stop();
|
|
376
|
+
rmSync(tempDir, { recursive: true });
|
|
377
|
+
});
|
|
378
|
+
it('should emit framework events', async () => {
|
|
379
|
+
const testModule = new TestModule();
|
|
380
|
+
const events = [];
|
|
381
|
+
membrane.pushResponse(createMockResponse([{ type: 'text', text: 'Hello!' }]));
|
|
382
|
+
const framework = await AgentFramework.create({
|
|
383
|
+
storePath: join(tempDir, 'test.chronicle'),
|
|
384
|
+
membrane: membrane.asMembrane(),
|
|
385
|
+
agents: [
|
|
386
|
+
{
|
|
387
|
+
name: 'assistant',
|
|
388
|
+
model: 'test-model',
|
|
389
|
+
systemPrompt: 'Test',
|
|
390
|
+
},
|
|
391
|
+
],
|
|
392
|
+
modules: [testModule],
|
|
393
|
+
});
|
|
394
|
+
framework.onTrace((event) => {
|
|
395
|
+
events.push(event.type);
|
|
396
|
+
});
|
|
397
|
+
framework.pushEvent({
|
|
398
|
+
type: 'external-message',
|
|
399
|
+
source: 'test',
|
|
400
|
+
content: 'Hello',
|
|
401
|
+
metadata: {},
|
|
402
|
+
});
|
|
403
|
+
await framework.runUntilIdle();
|
|
404
|
+
assert.ok(events.includes('process:received'), 'Should emit process:received');
|
|
405
|
+
assert.ok(events.includes('inference:started'), 'Should emit inference:started');
|
|
406
|
+
assert.ok(events.includes('inference:completed'), 'Should emit inference:completed');
|
|
407
|
+
assert.ok(events.includes('message:added'), 'Should emit message:added');
|
|
408
|
+
await framework.stop();
|
|
409
|
+
rmSync(tempDir, { recursive: true });
|
|
410
|
+
});
|
|
411
|
+
it('should filter tools by agent allowedTools', async () => {
|
|
412
|
+
const testModule = new TestModule();
|
|
413
|
+
membrane.pushResponse(createMockResponse([
|
|
414
|
+
{ type: 'text', text: 'No tools available' },
|
|
415
|
+
]));
|
|
416
|
+
const framework = await AgentFramework.create({
|
|
417
|
+
storePath: join(tempDir, 'test.chronicle'),
|
|
418
|
+
membrane: membrane.asMembrane(),
|
|
419
|
+
agents: [
|
|
420
|
+
{
|
|
421
|
+
name: 'assistant',
|
|
422
|
+
model: 'test-model',
|
|
423
|
+
systemPrompt: 'Test',
|
|
424
|
+
allowedTools: ['nonexistent--tool'], // Won't match test--echo
|
|
425
|
+
},
|
|
426
|
+
],
|
|
427
|
+
modules: [testModule],
|
|
428
|
+
});
|
|
429
|
+
framework.pushEvent({
|
|
430
|
+
type: 'external-message',
|
|
431
|
+
source: 'test',
|
|
432
|
+
content: 'Hello',
|
|
433
|
+
metadata: {},
|
|
434
|
+
});
|
|
435
|
+
await framework.runUntilIdle();
|
|
436
|
+
// Should have no tools in the request
|
|
437
|
+
assert.strictEqual(membrane.calls[0].tools, undefined);
|
|
438
|
+
await framework.stop();
|
|
439
|
+
rmSync(tempDir, { recursive: true });
|
|
440
|
+
});
|
|
441
|
+
it('should support adding and removing modules at runtime', async () => {
|
|
442
|
+
const framework = await AgentFramework.create({
|
|
443
|
+
storePath: join(tempDir, 'test.chronicle'),
|
|
444
|
+
membrane: membrane.asMembrane(),
|
|
445
|
+
agents: [
|
|
446
|
+
{
|
|
447
|
+
name: 'assistant',
|
|
448
|
+
model: 'test-model',
|
|
449
|
+
systemPrompt: 'Test',
|
|
450
|
+
},
|
|
451
|
+
],
|
|
452
|
+
modules: [],
|
|
453
|
+
});
|
|
454
|
+
const testModule = new TestModule();
|
|
455
|
+
await framework.addModule(testModule);
|
|
456
|
+
assert.notStrictEqual(testModule.ctx, null);
|
|
457
|
+
await framework.removeModule('test');
|
|
458
|
+
assert.strictEqual(testModule.ctx, null);
|
|
459
|
+
await framework.stop();
|
|
460
|
+
rmSync(tempDir, { recursive: true });
|
|
461
|
+
});
|
|
462
|
+
it('should share store when provided', async () => {
|
|
463
|
+
// This test verifies that app-owned stores work
|
|
464
|
+
const { JsStore } = await import('@animalabs/chronicle');
|
|
465
|
+
const store = JsStore.openOrCreate({ path: join(tempDir, 'shared.chronicle') });
|
|
466
|
+
const framework = await AgentFramework.create({
|
|
467
|
+
store,
|
|
468
|
+
membrane: membrane.asMembrane(),
|
|
469
|
+
agents: [
|
|
470
|
+
{
|
|
471
|
+
name: 'assistant',
|
|
472
|
+
model: 'test-model',
|
|
473
|
+
systemPrompt: 'Test',
|
|
474
|
+
},
|
|
475
|
+
],
|
|
476
|
+
modules: [],
|
|
477
|
+
});
|
|
478
|
+
// Framework should work
|
|
479
|
+
assert.notStrictEqual(framework.getStore(), null);
|
|
480
|
+
assert.strictEqual(framework.getStore(), store);
|
|
481
|
+
await framework.stop();
|
|
482
|
+
// Store should still be open (framework doesn't own it)
|
|
483
|
+
// We can close it ourselves
|
|
484
|
+
store.close();
|
|
485
|
+
rmSync(tempDir, { recursive: true });
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
describe('Streaming lifecycle', () => {
|
|
489
|
+
let tempDir;
|
|
490
|
+
let membrane;
|
|
491
|
+
beforeEach(() => {
|
|
492
|
+
tempDir = mkdtempSync(join(tmpdir(), 'agent-framework-stream-'));
|
|
493
|
+
membrane = new MockMembrane();
|
|
494
|
+
});
|
|
495
|
+
it('should complete a simple stream without tool calls', async () => {
|
|
496
|
+
const testModule = new TestModule();
|
|
497
|
+
const events = [];
|
|
498
|
+
membrane.pushResponse(createMockResponse([{ type: 'text', text: 'Hello from stream' }]));
|
|
499
|
+
const framework = await AgentFramework.create({
|
|
500
|
+
storePath: join(tempDir, 'test.chronicle'),
|
|
501
|
+
membrane: membrane.asMembrane(),
|
|
502
|
+
agents: [{ name: 'assistant', model: 'test-model', systemPrompt: 'Test' }],
|
|
503
|
+
modules: [testModule],
|
|
504
|
+
});
|
|
505
|
+
framework.onTrace((event) => events.push(event.type));
|
|
506
|
+
framework.pushEvent({
|
|
507
|
+
type: 'external-message',
|
|
508
|
+
source: 'test',
|
|
509
|
+
content: 'Hello',
|
|
510
|
+
metadata: {},
|
|
511
|
+
});
|
|
512
|
+
await framework.runUntilIdle();
|
|
513
|
+
assert.ok(events.includes('inference:started'));
|
|
514
|
+
assert.ok(events.includes('inference:tokens'));
|
|
515
|
+
assert.ok(events.includes('inference:completed'));
|
|
516
|
+
// Agent should be idle after completion
|
|
517
|
+
const agent = framework.getAgent('assistant');
|
|
518
|
+
assert.strictEqual(agent.state.status, 'idle');
|
|
519
|
+
await framework.stop();
|
|
520
|
+
rmSync(tempDir, { recursive: true });
|
|
521
|
+
});
|
|
522
|
+
it('should handle multi-round tool calls in a single stream', async () => {
|
|
523
|
+
const testModule = new TestModule();
|
|
524
|
+
// Round 1: tool call
|
|
525
|
+
membrane.pushResponse(createMockResponse([
|
|
526
|
+
{ type: 'text', text: 'First echo.' },
|
|
527
|
+
{ type: 'tool_use', id: 'call_1', name: 'test--echo', input: { message: 'round1' } },
|
|
528
|
+
], 'tool_use'));
|
|
529
|
+
// Round 2: another tool call
|
|
530
|
+
membrane.pushResponse(createMockResponse([
|
|
531
|
+
{ type: 'text', text: 'Second echo.' },
|
|
532
|
+
{ type: 'tool_use', id: 'call_2', name: 'test--echo', input: { message: 'round2' } },
|
|
533
|
+
], 'tool_use'));
|
|
534
|
+
// Round 3: final response
|
|
535
|
+
membrane.pushResponse(createMockResponse([
|
|
536
|
+
{ type: 'text', text: 'Done with both echoes.' },
|
|
537
|
+
]));
|
|
538
|
+
const framework = await AgentFramework.create({
|
|
539
|
+
storePath: join(tempDir, 'test.chronicle'),
|
|
540
|
+
membrane: membrane.asMembrane(),
|
|
541
|
+
agents: [{ name: 'assistant', model: 'test-model', systemPrompt: 'Test' }],
|
|
542
|
+
modules: [testModule],
|
|
543
|
+
});
|
|
544
|
+
framework.pushEvent({
|
|
545
|
+
type: 'external-message',
|
|
546
|
+
source: 'test',
|
|
547
|
+
content: 'Do two echoes',
|
|
548
|
+
metadata: {},
|
|
549
|
+
});
|
|
550
|
+
await framework.runUntilIdle();
|
|
551
|
+
// Both tools were called
|
|
552
|
+
assert.strictEqual(testModule.toolCalls.length, 2);
|
|
553
|
+
assert.deepStrictEqual(testModule.toolCalls[0].input, { message: 'round1' });
|
|
554
|
+
assert.deepStrictEqual(testModule.toolCalls[1].input, { message: 'round2' });
|
|
555
|
+
// Still only one membrane call (one stream)
|
|
556
|
+
assert.strictEqual(membrane.calls.length, 1);
|
|
557
|
+
// Agent is idle
|
|
558
|
+
assert.strictEqual(framework.getAgent('assistant').state.status, 'idle');
|
|
559
|
+
await framework.stop();
|
|
560
|
+
rmSync(tempDir, { recursive: true });
|
|
561
|
+
});
|
|
562
|
+
it('should cancel active streams on stop', async () => {
|
|
563
|
+
const testModule = new TestModule();
|
|
564
|
+
// Response with a tool call but no follow-up (stream will wait forever)
|
|
565
|
+
membrane.pushResponse(createMockResponse([
|
|
566
|
+
{ type: 'tool_use', id: 'call_hang', name: 'test--echo', input: { message: 'hang' } },
|
|
567
|
+
], 'tool_use'));
|
|
568
|
+
const framework = await AgentFramework.create({
|
|
569
|
+
storePath: join(tempDir, 'test.chronicle'),
|
|
570
|
+
membrane: membrane.asMembrane(),
|
|
571
|
+
agents: [{ name: 'assistant', model: 'test-model', systemPrompt: 'Test' }],
|
|
572
|
+
modules: [testModule],
|
|
573
|
+
});
|
|
574
|
+
framework.pushEvent({
|
|
575
|
+
type: 'external-message',
|
|
576
|
+
source: 'test',
|
|
577
|
+
content: 'Hang',
|
|
578
|
+
metadata: {},
|
|
579
|
+
});
|
|
580
|
+
// Process the event and start streaming (but don't wait for idle — it won't happen)
|
|
581
|
+
// Let the stream start and dispatch the tool call
|
|
582
|
+
for (let i = 0; i < 10; i++) {
|
|
583
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
584
|
+
}
|
|
585
|
+
// stop() should cancel active streams and not hang
|
|
586
|
+
await framework.stop();
|
|
587
|
+
// Agent should be idle after cancellation
|
|
588
|
+
assert.strictEqual(framework.getAgent('assistant').state.status, 'idle');
|
|
589
|
+
rmSync(tempDir, { recursive: true });
|
|
590
|
+
});
|
|
591
|
+
it('should emit tool_calls_yielded and stream_resumed traces', async () => {
|
|
592
|
+
const testModule = new TestModule();
|
|
593
|
+
const events = [];
|
|
594
|
+
membrane.pushResponse(createMockResponse([
|
|
595
|
+
{ type: 'tool_use', id: 'call_t', name: 'test--echo', input: { message: 'trace' } },
|
|
596
|
+
], 'tool_use'));
|
|
597
|
+
membrane.pushResponse(createMockResponse([
|
|
598
|
+
{ type: 'text', text: 'Done' },
|
|
599
|
+
]));
|
|
600
|
+
const framework = await AgentFramework.create({
|
|
601
|
+
storePath: join(tempDir, 'test.chronicle'),
|
|
602
|
+
membrane: membrane.asMembrane(),
|
|
603
|
+
agents: [{ name: 'assistant', model: 'test-model', systemPrompt: 'Test' }],
|
|
604
|
+
modules: [testModule],
|
|
605
|
+
});
|
|
606
|
+
framework.onTrace((event) => events.push(event.type));
|
|
607
|
+
framework.pushEvent({
|
|
608
|
+
type: 'external-message',
|
|
609
|
+
source: 'test',
|
|
610
|
+
content: 'Trace test',
|
|
611
|
+
metadata: {},
|
|
612
|
+
});
|
|
613
|
+
await framework.runUntilIdle();
|
|
614
|
+
assert.ok(events.includes('inference:tool_calls_yielded'), 'Should emit tool_calls_yielded');
|
|
615
|
+
assert.ok(events.includes('inference:stream_resumed'), 'Should emit stream_resumed');
|
|
616
|
+
assert.ok(events.includes('inference:completed'), 'Should emit completed');
|
|
617
|
+
await framework.stop();
|
|
618
|
+
rmSync(tempDir, { recursive: true });
|
|
619
|
+
});
|
|
620
|
+
it('should convert AF tool results to Membrane format when resuming stream', async () => {
|
|
621
|
+
const testModule = new TestModule();
|
|
622
|
+
membrane.pushResponse(createMockResponse([
|
|
623
|
+
{ type: 'tool_use', id: 'call_conv', name: 'test--echo', input: { message: 'convert' } },
|
|
624
|
+
], 'tool_use'));
|
|
625
|
+
membrane.pushResponse(createMockResponse([
|
|
626
|
+
{ type: 'text', text: 'Converted' },
|
|
627
|
+
]));
|
|
628
|
+
const framework = await AgentFramework.create({
|
|
629
|
+
storePath: join(tempDir, 'test.chronicle'),
|
|
630
|
+
membrane: membrane.asMembrane(),
|
|
631
|
+
agents: [{ name: 'assistant', model: 'test-model', systemPrompt: 'Test' }],
|
|
632
|
+
modules: [testModule],
|
|
633
|
+
});
|
|
634
|
+
framework.pushEvent({
|
|
635
|
+
type: 'external-message',
|
|
636
|
+
source: 'test',
|
|
637
|
+
content: 'Convert test',
|
|
638
|
+
metadata: {},
|
|
639
|
+
});
|
|
640
|
+
await framework.runUntilIdle();
|
|
641
|
+
// Verify the mock stream received correctly converted tool results
|
|
642
|
+
const stream = membrane.lastStream;
|
|
643
|
+
assert.strictEqual(stream.receivedToolResults.length, 1);
|
|
644
|
+
const results = stream.receivedToolResults[0];
|
|
645
|
+
assert.strictEqual(results.length, 1);
|
|
646
|
+
assert.strictEqual(results[0].toolUseId, 'call_conv');
|
|
647
|
+
assert.strictEqual(results[0].content, JSON.stringify({ echoed: 'convert' }));
|
|
648
|
+
assert.strictEqual(results[0].isError, undefined);
|
|
649
|
+
await framework.stop();
|
|
650
|
+
rmSync(tempDir, { recursive: true });
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
describe('Module state persistence', () => {
|
|
654
|
+
let tempDir;
|
|
655
|
+
let membrane;
|
|
656
|
+
beforeEach(() => {
|
|
657
|
+
tempDir = mkdtempSync(join(tmpdir(), 'agent-framework-test-'));
|
|
658
|
+
membrane = new MockMembrane();
|
|
659
|
+
});
|
|
660
|
+
it('should persist and restore module state', async () => {
|
|
661
|
+
class StatefulModule {
|
|
662
|
+
name = 'stateful';
|
|
663
|
+
ctx = null;
|
|
664
|
+
async start(ctx) {
|
|
665
|
+
this.ctx = ctx;
|
|
666
|
+
// Set initial state
|
|
667
|
+
const existing = ctx.getState();
|
|
668
|
+
if (!existing) {
|
|
669
|
+
ctx.setState({ count: 1 });
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
ctx.setState({ count: existing.count + 1 });
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
async stop() { }
|
|
676
|
+
getTools() { return []; }
|
|
677
|
+
async handleToolCall() {
|
|
678
|
+
return { success: false, error: 'No tools', isError: true };
|
|
679
|
+
}
|
|
680
|
+
async onProcess() { return {}; }
|
|
681
|
+
}
|
|
682
|
+
const storePath = join(tempDir, 'state.chronicle');
|
|
683
|
+
// First run
|
|
684
|
+
const module1 = new StatefulModule();
|
|
685
|
+
const framework1 = await AgentFramework.create({
|
|
686
|
+
storePath,
|
|
687
|
+
membrane: membrane.asMembrane(),
|
|
688
|
+
agents: [{ name: 'a', model: 'm', systemPrompt: 's' }],
|
|
689
|
+
modules: [module1],
|
|
690
|
+
});
|
|
691
|
+
assert.deepStrictEqual(module1.ctx?.getState()?.count, 1);
|
|
692
|
+
await framework1.stop();
|
|
693
|
+
// Second run - state should be restored
|
|
694
|
+
const module2 = new StatefulModule();
|
|
695
|
+
const framework2 = await AgentFramework.create({
|
|
696
|
+
storePath,
|
|
697
|
+
membrane: membrane.asMembrane(),
|
|
698
|
+
agents: [{ name: 'a', model: 'm', systemPrompt: 's' }],
|
|
699
|
+
modules: [module2],
|
|
700
|
+
});
|
|
701
|
+
assert.deepStrictEqual(module2.ctx?.getState()?.count, 2);
|
|
702
|
+
await framework2.stop();
|
|
703
|
+
rmSync(tempDir, { recursive: true });
|
|
704
|
+
});
|
|
705
|
+
it('should track external IDs', async () => {
|
|
706
|
+
class IdTrackingModule {
|
|
707
|
+
name = 'idtracker';
|
|
708
|
+
ctx = null;
|
|
709
|
+
async start(ctx) {
|
|
710
|
+
this.ctx = ctx;
|
|
711
|
+
}
|
|
712
|
+
async stop() { }
|
|
713
|
+
getTools() { return []; }
|
|
714
|
+
async handleToolCall() {
|
|
715
|
+
return { success: false, error: 'No tools', isError: true };
|
|
716
|
+
}
|
|
717
|
+
async onProcess() { return {}; }
|
|
718
|
+
}
|
|
719
|
+
const module = new IdTrackingModule();
|
|
720
|
+
const framework = await AgentFramework.create({
|
|
721
|
+
storePath: join(tempDir, 'ids.chronicle'),
|
|
722
|
+
membrane: membrane.asMembrane(),
|
|
723
|
+
agents: [{ name: 'a', model: 'm', systemPrompt: 's' }],
|
|
724
|
+
modules: [module],
|
|
725
|
+
});
|
|
726
|
+
// Add a message with external ID
|
|
727
|
+
const msgId = module.ctx.addMessage('User', [{ type: 'text', text: 'Hello from Discord' }], { external: { source: 'discord', id: 'msg123' } });
|
|
728
|
+
// Should be able to look it up
|
|
729
|
+
const found = module.ctx.findMessageByExternalId('discord', 'msg123');
|
|
730
|
+
assert.strictEqual(found, msgId);
|
|
731
|
+
// Unknown ID should return null
|
|
732
|
+
const notFound = module.ctx.findMessageByExternalId('discord', 'unknown');
|
|
733
|
+
assert.strictEqual(notFound, null);
|
|
734
|
+
await framework.stop();
|
|
735
|
+
rmSync(tempDir, { recursive: true });
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
//# sourceMappingURL=framework.test.js.map
|