@forgeax/engine-intelligence 0.1.2
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 +202 -0
- package/README.md +36 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/intelligence-error-code-owner.test-d.d.ts +33 -0
- package/dist/__tests__/intelligence-error-code-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/runtime.test.d.ts +2 -0
- package/dist/__tests__/runtime.test.d.ts.map +1 -0
- package/dist/__tests__/transport.test.d.ts +2 -0
- package/dist/__tests__/transport.test.d.ts.map +1 -0
- package/dist/errors.d.ts +74 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +538 -0
- package/dist/index.mjs.map +1 -0
- package/dist/plugin.d.ts +10 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/runtime.d.ts +30 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/transport.d.ts +61 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/types.d.ts +82 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +56 -0
- package/src/__tests__/intelligence-error-code-owner.test-d.ts +143 -0
- package/src/__tests__/runtime.test.ts +504 -0
- package/src/__tests__/transport.test.ts +130 -0
- package/src/errors.ts +147 -0
- package/src/index.ts +36 -0
- package/src/plugin.ts +20 -0
- package/src/runtime.ts +293 -0
- package/src/transport.ts +282 -0
- package/src/types.ts +104 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
import { err, ok, type Result } from '@forgeax/engine-types';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { IntelligenceError } from '../errors';
|
|
4
|
+
import { createIntelligenceRuntime } from '../runtime';
|
|
5
|
+
import {
|
|
6
|
+
type ActivityId,
|
|
7
|
+
type ActivitySink,
|
|
8
|
+
type ActivitySubmission,
|
|
9
|
+
activityId,
|
|
10
|
+
type IntelligenceProvider,
|
|
11
|
+
} from '../types';
|
|
12
|
+
|
|
13
|
+
function controlledProvider(id = 'test.provider') {
|
|
14
|
+
const sinks = new Map<ActivityId, ActivitySink>();
|
|
15
|
+
let closed = false;
|
|
16
|
+
const provider: IntelligenceProvider = {
|
|
17
|
+
id,
|
|
18
|
+
start(submission, sink) {
|
|
19
|
+
sinks.set(submission.id, sink);
|
|
20
|
+
return ok(undefined);
|
|
21
|
+
},
|
|
22
|
+
cancel(activity) {
|
|
23
|
+
const sink = sinks.get(activity);
|
|
24
|
+
if (sink === undefined) {
|
|
25
|
+
return err(
|
|
26
|
+
new IntelligenceError({
|
|
27
|
+
code: 'intelligence-activity-not-found',
|
|
28
|
+
detail: { activityId: activity },
|
|
29
|
+
}),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
sinks.delete(activity);
|
|
33
|
+
sink.cancelled();
|
|
34
|
+
return ok(undefined);
|
|
35
|
+
},
|
|
36
|
+
async close() {
|
|
37
|
+
closed = true;
|
|
38
|
+
for (const sink of sinks.values()) sink.cancelled();
|
|
39
|
+
sinks.clear();
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
provider,
|
|
44
|
+
sinks,
|
|
45
|
+
get closed() {
|
|
46
|
+
return closed;
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('IntelligenceRuntime', () => {
|
|
52
|
+
it('delivers ordered bounded deltas and a terminal result without awaiting in poll', () => {
|
|
53
|
+
const controlled = controlledProvider();
|
|
54
|
+
const runtime = createIntelligenceRuntime(controlled.provider, {
|
|
55
|
+
createActivityId: () => activityId('activity-a'),
|
|
56
|
+
createSessionId: () => 'session-a',
|
|
57
|
+
});
|
|
58
|
+
const started = runtime.submit({ input: 'hello' });
|
|
59
|
+
expect(started.ok).toBe(true);
|
|
60
|
+
if (!started.ok) return;
|
|
61
|
+
const sink = controlled.sinks.get(started.value.id);
|
|
62
|
+
expect(sink).toBeDefined();
|
|
63
|
+
sink?.text('hel');
|
|
64
|
+
sink?.text('lo');
|
|
65
|
+
sink?.complete('hello');
|
|
66
|
+
|
|
67
|
+
expect(runtime.poll()).toEqual([
|
|
68
|
+
{ type: 'text-delta', activityId: activityId('activity-a'), sequence: 1, text: 'hel' },
|
|
69
|
+
{ type: 'text-delta', activityId: activityId('activity-a'), sequence: 2, text: 'lo' },
|
|
70
|
+
{
|
|
71
|
+
type: 'completed',
|
|
72
|
+
activityId: activityId('activity-a'),
|
|
73
|
+
sequence: 3,
|
|
74
|
+
session: { providerId: 'test.provider', id: 'session-a' },
|
|
75
|
+
output: 'hello',
|
|
76
|
+
},
|
|
77
|
+
]);
|
|
78
|
+
expect(runtime.poll()).toEqual([]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('rejects cross-provider session reuse before provider dispatch', () => {
|
|
82
|
+
const controlled = controlledProvider();
|
|
83
|
+
const runtime = createIntelligenceRuntime(controlled.provider);
|
|
84
|
+
const result = runtime.submit({
|
|
85
|
+
input: 'resume',
|
|
86
|
+
session: { providerId: 'another.provider', id: 'session-a' },
|
|
87
|
+
});
|
|
88
|
+
expect(result.ok).toBe(false);
|
|
89
|
+
if (result.ok) return;
|
|
90
|
+
expect(result.error.code).toBe('intelligence-session-provider-mismatch');
|
|
91
|
+
expect(controlled.sinks.size).toBe(0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('turns queue overflow into one structured terminal failure and cancels the provider', () => {
|
|
95
|
+
const controlled = controlledProvider();
|
|
96
|
+
const runtime = createIntelligenceRuntime(controlled.provider, {
|
|
97
|
+
limits: { maxPendingEventsPerActivity: 3 },
|
|
98
|
+
createActivityId: () => activityId('overflow'),
|
|
99
|
+
});
|
|
100
|
+
const started = runtime.submit({ input: 'overflow' });
|
|
101
|
+
if (!started.ok) throw started.error;
|
|
102
|
+
const sink = controlled.sinks.get(started.value.id);
|
|
103
|
+
sink?.text('one');
|
|
104
|
+
sink?.text('two');
|
|
105
|
+
sink?.text('three');
|
|
106
|
+
|
|
107
|
+
const events = runtime.poll();
|
|
108
|
+
expect(events).toHaveLength(3);
|
|
109
|
+
expect(events[2]?.type).toBe('failed');
|
|
110
|
+
if (events[2]?.type === 'failed') {
|
|
111
|
+
expect(events[2].error.code).toBe('intelligence-output-overflow');
|
|
112
|
+
}
|
|
113
|
+
expect(controlled.sinks.size).toBe(0);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('preserves the provider error category and cause projection', () => {
|
|
117
|
+
const controlled = controlledProvider();
|
|
118
|
+
const runtime = createIntelligenceRuntime(controlled.provider, {
|
|
119
|
+
createActivityId: () => activityId('failed'),
|
|
120
|
+
});
|
|
121
|
+
const started = runtime.submit({ input: 'fail' });
|
|
122
|
+
if (!started.ok) throw started.error;
|
|
123
|
+
controlled.sinks.get(started.value.id)?.fail(new Error('provider exploded'));
|
|
124
|
+
const event = runtime.poll()[0];
|
|
125
|
+
expect(event?.type).toBe('failed');
|
|
126
|
+
if (event?.type === 'failed') {
|
|
127
|
+
expect(event.error.code).toBe('intelligence-provider-failed');
|
|
128
|
+
if (event.error.code === 'intelligence-provider-failed') {
|
|
129
|
+
expect(event.error.detail.cause).toBe('provider exploded');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('closes the provider and rejects later work', async () => {
|
|
135
|
+
const controlled = controlledProvider();
|
|
136
|
+
const runtime = createIntelligenceRuntime(controlled.provider);
|
|
137
|
+
await runtime.close();
|
|
138
|
+
expect(controlled.closed).toBe(true);
|
|
139
|
+
const result = runtime.submit({ input: 'late' });
|
|
140
|
+
expect(result.ok).toBe(false);
|
|
141
|
+
if (!result.ok) expect(result.error.code).toBe('intelligence-closed');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('accepts a transport-owned identity and provider session', () => {
|
|
145
|
+
const controlled = controlledProvider();
|
|
146
|
+
const runtime = createIntelligenceRuntime(controlled.provider);
|
|
147
|
+
const submission: ActivitySubmission = {
|
|
148
|
+
id: activityId('transport-owned'),
|
|
149
|
+
session: { providerId: 'test.provider', id: 'saved-session' },
|
|
150
|
+
input: 'next turn',
|
|
151
|
+
};
|
|
152
|
+
const result: Result<void, IntelligenceError> = runtime.accept(submission);
|
|
153
|
+
expect(result.ok).toBe(true);
|
|
154
|
+
expect(controlled.sinks.has(submission.id)).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('contains synchronous provider start throws and permits same-runtime retry', () => {
|
|
158
|
+
const siblingId = activityId('sibling');
|
|
159
|
+
const failedId = activityId('failed');
|
|
160
|
+
const spareId = activityId('spare');
|
|
161
|
+
const failedSession = { providerId: 'test.provider', id: 'failed-session' };
|
|
162
|
+
const ids = [siblingId, failedId, spareId, failedId];
|
|
163
|
+
const sinks = new Map<ActivityId, ActivitySink>();
|
|
164
|
+
const starts = new Map<ActivityId, number>();
|
|
165
|
+
let throwOnFailedStart = true;
|
|
166
|
+
let cancelCalls = 0;
|
|
167
|
+
let closeCalls = 0;
|
|
168
|
+
const provider: IntelligenceProvider = {
|
|
169
|
+
id: 'test.provider',
|
|
170
|
+
start(submission, sink) {
|
|
171
|
+
starts.set(submission.id, (starts.get(submission.id) ?? 0) + 1);
|
|
172
|
+
if (submission.id === failedId && throwOnFailedStart) {
|
|
173
|
+
throw new Error('sentinel provider start failure');
|
|
174
|
+
}
|
|
175
|
+
sinks.set(submission.id, sink);
|
|
176
|
+
return ok(undefined);
|
|
177
|
+
},
|
|
178
|
+
cancel() {
|
|
179
|
+
cancelCalls += 1;
|
|
180
|
+
return ok(undefined);
|
|
181
|
+
},
|
|
182
|
+
async close() {
|
|
183
|
+
closeCalls += 1;
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
const runtime = createIntelligenceRuntime(provider, {
|
|
187
|
+
limits: { maxConcurrentActivities: 2 },
|
|
188
|
+
createActivityId: () => {
|
|
189
|
+
const id = ids.shift();
|
|
190
|
+
if (id === undefined) throw new Error('test activity identity exhausted');
|
|
191
|
+
return id;
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const sibling = runtime.submit({ input: 'sibling' });
|
|
196
|
+
expect(sibling.ok).toBe(true);
|
|
197
|
+
if (!sibling.ok) return;
|
|
198
|
+
const siblingSink = sinks.get(sibling.value.id);
|
|
199
|
+
expect(siblingSink).toBeDefined();
|
|
200
|
+
siblingSink?.text('sibling');
|
|
201
|
+
expect(runtime.poll()).toEqual([
|
|
202
|
+
{ type: 'text-delta', activityId: siblingId, sequence: 1, text: 'sibling' },
|
|
203
|
+
]);
|
|
204
|
+
|
|
205
|
+
let refused: ReturnType<typeof runtime.submit> | undefined;
|
|
206
|
+
let rawThrow: unknown;
|
|
207
|
+
try {
|
|
208
|
+
refused = runtime.submit({ input: 'throw', session: failedSession });
|
|
209
|
+
} catch (error) {
|
|
210
|
+
rawThrow = error;
|
|
211
|
+
}
|
|
212
|
+
expect(rawThrow).toBeUndefined();
|
|
213
|
+
expect(refused?.ok).toBe(false);
|
|
214
|
+
if (refused === undefined || refused.ok) return;
|
|
215
|
+
expect(refused.error.code).toBe('intelligence-provider-failed');
|
|
216
|
+
if (refused.error.code === 'intelligence-provider-failed') {
|
|
217
|
+
expect(refused.error.detail.providerId).toBe(provider.id);
|
|
218
|
+
expect(refused.error.detail.cause).toBeInstanceOf(Error);
|
|
219
|
+
expect(refused.error.message).not.toContain('sentinel provider start failure');
|
|
220
|
+
expect(refused.error.hint).not.toContain('sentinel provider start failure');
|
|
221
|
+
}
|
|
222
|
+
expect(runtime.poll()).toEqual([]);
|
|
223
|
+
expect(sinks.has(failedId)).toBe(false);
|
|
224
|
+
expect(cancelCalls).toBe(0);
|
|
225
|
+
expect(closeCalls).toBe(0);
|
|
226
|
+
|
|
227
|
+
const spare = runtime.submit({ input: 'spare' });
|
|
228
|
+
expect(spare.ok).toBe(true);
|
|
229
|
+
if (!spare.ok) return;
|
|
230
|
+
sinks.get(spare.value.id)?.complete('spare');
|
|
231
|
+
expect(runtime.poll()).toEqual([
|
|
232
|
+
{
|
|
233
|
+
type: 'completed',
|
|
234
|
+
activityId: spareId,
|
|
235
|
+
sequence: 1,
|
|
236
|
+
session: spare.value.session,
|
|
237
|
+
output: 'spare',
|
|
238
|
+
},
|
|
239
|
+
]);
|
|
240
|
+
|
|
241
|
+
throwOnFailedStart = false;
|
|
242
|
+
const retry = runtime.submit({ input: 'retry', session: failedSession });
|
|
243
|
+
expect(retry.ok).toBe(true);
|
|
244
|
+
if (!retry.ok) return;
|
|
245
|
+
expect(retry.value.id).toBe(failedId);
|
|
246
|
+
expect(retry.value.session).toBe(failedSession);
|
|
247
|
+
siblingSink?.complete('sibling-done');
|
|
248
|
+
sinks.get(retry.value.id)?.complete('retry-done');
|
|
249
|
+
expect(runtime.poll()).toEqual([
|
|
250
|
+
{
|
|
251
|
+
type: 'completed',
|
|
252
|
+
activityId: siblingId,
|
|
253
|
+
sequence: 2,
|
|
254
|
+
session: sibling.value.session,
|
|
255
|
+
output: 'sibling-done',
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
type: 'completed',
|
|
259
|
+
activityId: failedId,
|
|
260
|
+
sequence: 1,
|
|
261
|
+
session: failedSession,
|
|
262
|
+
output: 'retry-done',
|
|
263
|
+
},
|
|
264
|
+
]);
|
|
265
|
+
expect(runtime.poll()).toEqual([]);
|
|
266
|
+
expect(starts.get(siblingId)).toBe(1);
|
|
267
|
+
expect(starts.get(failedId)).toBe(2);
|
|
268
|
+
expect(starts.get(spareId)).toBe(1);
|
|
269
|
+
expect(cancelCalls).toBe(0);
|
|
270
|
+
|
|
271
|
+
const close = runtime.close();
|
|
272
|
+
expect(close).toBe(runtime.close());
|
|
273
|
+
return close.then(() => {
|
|
274
|
+
expect(closeCalls).toBe(1);
|
|
275
|
+
expect(runtime.poll()).toEqual([]);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('contains synchronous provider cancel throws and preserves the same Activity for retry', () => {
|
|
280
|
+
const siblingId = activityId('cancel-sibling');
|
|
281
|
+
const failedId = activityId('cancel-failed');
|
|
282
|
+
const spareId = activityId('cancel-spare');
|
|
283
|
+
const failedSession = { providerId: 'cancel.throw.provider', id: 'failed-session' };
|
|
284
|
+
const ids = [siblingId, failedId, spareId];
|
|
285
|
+
const sinks = new Map<ActivityId, ActivitySink>();
|
|
286
|
+
const starts = new Map<ActivityId, number>();
|
|
287
|
+
const cancelCalls: ActivityId[] = [];
|
|
288
|
+
const sentinel = new Error('sentinel provider cancel failure');
|
|
289
|
+
let throwOnFailedCancel = true;
|
|
290
|
+
const provider: IntelligenceProvider = {
|
|
291
|
+
id: 'cancel.throw.provider',
|
|
292
|
+
start(submission, sink) {
|
|
293
|
+
starts.set(submission.id, (starts.get(submission.id) ?? 0) + 1);
|
|
294
|
+
sinks.set(submission.id, sink);
|
|
295
|
+
return ok(undefined);
|
|
296
|
+
},
|
|
297
|
+
cancel(activity) {
|
|
298
|
+
cancelCalls.push(activity);
|
|
299
|
+
if (activity === failedId && throwOnFailedCancel) throw sentinel;
|
|
300
|
+
const sink = sinks.get(activity);
|
|
301
|
+
if (sink === undefined) {
|
|
302
|
+
return err(
|
|
303
|
+
new IntelligenceError({
|
|
304
|
+
code: 'intelligence-activity-not-found',
|
|
305
|
+
detail: { activityId: activity },
|
|
306
|
+
}),
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
sinks.delete(activity);
|
|
310
|
+
sink.cancelled();
|
|
311
|
+
return ok(undefined);
|
|
312
|
+
},
|
|
313
|
+
async close() {},
|
|
314
|
+
};
|
|
315
|
+
const runtime = createIntelligenceRuntime(provider, {
|
|
316
|
+
limits: { maxConcurrentActivities: 2 },
|
|
317
|
+
createActivityId: () => {
|
|
318
|
+
const id = ids.shift();
|
|
319
|
+
if (id === undefined) throw new Error('test activity identity exhausted');
|
|
320
|
+
return id;
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const sibling = runtime.submit({ input: 'sibling' });
|
|
325
|
+
const failed = runtime.submit({ input: 'cancel', session: failedSession });
|
|
326
|
+
expect(sibling.ok).toBe(true);
|
|
327
|
+
expect(failed.ok).toBe(true);
|
|
328
|
+
if (!sibling.ok || !failed.ok) return;
|
|
329
|
+
expect(runtime.poll()).toEqual([]);
|
|
330
|
+
|
|
331
|
+
let rawThrow: unknown;
|
|
332
|
+
let refused: ReturnType<typeof runtime.cancel> | undefined;
|
|
333
|
+
try {
|
|
334
|
+
refused = runtime.cancel(failed.value.id);
|
|
335
|
+
} catch (error) {
|
|
336
|
+
rawThrow = error;
|
|
337
|
+
}
|
|
338
|
+
expect(rawThrow).toBeUndefined();
|
|
339
|
+
expect(refused?.ok).toBe(false);
|
|
340
|
+
if (refused === undefined || refused.ok) return;
|
|
341
|
+
expect(refused.error.code).toBe('intelligence-provider-failed');
|
|
342
|
+
if (refused.error.code === 'intelligence-provider-failed') {
|
|
343
|
+
expect(refused.error.detail.providerId).toBe(provider.id);
|
|
344
|
+
expect(refused.error.detail.cause).toBe(sentinel);
|
|
345
|
+
expect(refused.error.message).not.toContain(sentinel.message);
|
|
346
|
+
expect(refused.error.hint).not.toContain(sentinel.message);
|
|
347
|
+
}
|
|
348
|
+
expect(cancelCalls).toEqual([failedId]);
|
|
349
|
+
expect(runtime.poll()).toEqual([]);
|
|
350
|
+
|
|
351
|
+
const spare = runtime.submit({ input: 'spare' });
|
|
352
|
+
expect(spare.ok).toBe(false);
|
|
353
|
+
if (!spare.ok) expect(spare.error.code).toBe('intelligence-capacity-exceeded');
|
|
354
|
+
expect(starts.has(spareId)).toBe(false);
|
|
355
|
+
|
|
356
|
+
const siblingSink = sinks.get(sibling.value.id);
|
|
357
|
+
expect(siblingSink).toBeDefined();
|
|
358
|
+
siblingSink?.text('healthy');
|
|
359
|
+
expect(runtime.poll()).toEqual([
|
|
360
|
+
{ type: 'text-delta', activityId: siblingId, sequence: 1, text: 'healthy' },
|
|
361
|
+
]);
|
|
362
|
+
|
|
363
|
+
throwOnFailedCancel = false;
|
|
364
|
+
const retried = runtime.cancel(failed.value.id);
|
|
365
|
+
expect(retried.ok).toBe(true);
|
|
366
|
+
expect(cancelCalls).toEqual([failedId, failedId]);
|
|
367
|
+
expect(runtime.poll()).toEqual([{ type: 'cancelled', activityId: failedId, sequence: 1 }]);
|
|
368
|
+
const afterCancel = runtime.cancel(failed.value.id);
|
|
369
|
+
expect(afterCancel.ok).toBe(false);
|
|
370
|
+
if (!afterCancel.ok) expect(afterCancel.error.code).toBe('intelligence-activity-not-found');
|
|
371
|
+
|
|
372
|
+
siblingSink?.complete('healthy-done');
|
|
373
|
+
expect(runtime.poll()).toEqual([
|
|
374
|
+
{
|
|
375
|
+
type: 'completed',
|
|
376
|
+
activityId: siblingId,
|
|
377
|
+
sequence: 2,
|
|
378
|
+
session: sibling.value.session,
|
|
379
|
+
output: 'healthy-done',
|
|
380
|
+
},
|
|
381
|
+
]);
|
|
382
|
+
expect(starts.get(siblingId)).toBe(1);
|
|
383
|
+
expect(starts.get(failedId)).toBe(1);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it('contains a throwing overflow cancellation and preserves provider Result errors', () => {
|
|
387
|
+
const siblingId = activityId('overflow-sibling');
|
|
388
|
+
const returnedId = activityId('returned-cancel-error');
|
|
389
|
+
const overflowId = activityId('overflow-throw');
|
|
390
|
+
const ids = [siblingId, returnedId, overflowId];
|
|
391
|
+
const sinks = new Map<ActivityId, ActivitySink>();
|
|
392
|
+
const cancelCalls: ActivityId[] = [];
|
|
393
|
+
const overflowSentinel = new Error('sentinel overflow cancel failure');
|
|
394
|
+
const returnedError = new IntelligenceError({
|
|
395
|
+
code: 'intelligence-provider-failed',
|
|
396
|
+
detail: { providerId: 'inner.provider', cause: 'provider returned error' },
|
|
397
|
+
});
|
|
398
|
+
const returnedResult = err(returnedError);
|
|
399
|
+
const provider: IntelligenceProvider = {
|
|
400
|
+
id: 'overflow.throw.provider',
|
|
401
|
+
start(submission, sink) {
|
|
402
|
+
sinks.set(submission.id, sink);
|
|
403
|
+
return ok(undefined);
|
|
404
|
+
},
|
|
405
|
+
cancel(activity) {
|
|
406
|
+
cancelCalls.push(activity);
|
|
407
|
+
if (activity === returnedId) return returnedResult;
|
|
408
|
+
if (activity === overflowId) throw overflowSentinel;
|
|
409
|
+
const sink = sinks.get(activity);
|
|
410
|
+
if (sink === undefined) {
|
|
411
|
+
return err(
|
|
412
|
+
new IntelligenceError({
|
|
413
|
+
code: 'intelligence-activity-not-found',
|
|
414
|
+
detail: { activityId: activity },
|
|
415
|
+
}),
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
sinks.delete(activity);
|
|
419
|
+
sink.cancelled();
|
|
420
|
+
return ok(undefined);
|
|
421
|
+
},
|
|
422
|
+
async close() {},
|
|
423
|
+
};
|
|
424
|
+
const runtime = createIntelligenceRuntime(provider, {
|
|
425
|
+
limits: { maxPendingEventsPerActivity: 2, maxConcurrentActivities: 3 },
|
|
426
|
+
createActivityId: () => {
|
|
427
|
+
const id = ids.shift();
|
|
428
|
+
if (id === undefined) throw new Error('test activity identity exhausted');
|
|
429
|
+
return id;
|
|
430
|
+
},
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
const sibling = runtime.submit({ input: 'sibling' });
|
|
434
|
+
const returned = runtime.submit({ input: 'returned' });
|
|
435
|
+
const overflow = runtime.submit({ input: 'overflow' });
|
|
436
|
+
expect(sibling.ok).toBe(true);
|
|
437
|
+
expect(returned.ok).toBe(true);
|
|
438
|
+
expect(overflow.ok).toBe(true);
|
|
439
|
+
if (!sibling.ok || !returned.ok || !overflow.ok) return;
|
|
440
|
+
|
|
441
|
+
const returnedCancel = runtime.cancel(returned.value.id);
|
|
442
|
+
expect(returnedCancel).toBe(returnedResult);
|
|
443
|
+
expect(runtime.poll()).toEqual([]);
|
|
444
|
+
sinks.get(returned.value.id)?.complete('returned-retry');
|
|
445
|
+
expect(runtime.poll()).toEqual([
|
|
446
|
+
{
|
|
447
|
+
type: 'completed',
|
|
448
|
+
activityId: returnedId,
|
|
449
|
+
sequence: 1,
|
|
450
|
+
session: returned.value.session,
|
|
451
|
+
output: 'returned-retry',
|
|
452
|
+
},
|
|
453
|
+
]);
|
|
454
|
+
|
|
455
|
+
const overflowSink = sinks.get(overflow.value.id);
|
|
456
|
+
expect(overflowSink).toBeDefined();
|
|
457
|
+
overflowSink?.text('first');
|
|
458
|
+
let rawThrow: unknown;
|
|
459
|
+
try {
|
|
460
|
+
overflowSink?.text('second');
|
|
461
|
+
} catch (error) {
|
|
462
|
+
rawThrow = error;
|
|
463
|
+
}
|
|
464
|
+
expect(rawThrow).toBeUndefined();
|
|
465
|
+
const events = runtime.poll();
|
|
466
|
+
expect(events).toHaveLength(2);
|
|
467
|
+
expect(events.filter((event) => event.type === 'failed')).toHaveLength(1);
|
|
468
|
+
expect(events[0]).toEqual({
|
|
469
|
+
type: 'text-delta',
|
|
470
|
+
activityId: overflowId,
|
|
471
|
+
sequence: 1,
|
|
472
|
+
text: 'first',
|
|
473
|
+
});
|
|
474
|
+
const overflowEvent = events[1];
|
|
475
|
+
expect(overflowEvent?.type).toBe('failed');
|
|
476
|
+
if (overflowEvent?.type === 'failed') {
|
|
477
|
+
expect(overflowEvent.error.code).toBe('intelligence-output-overflow');
|
|
478
|
+
if (overflowEvent.error.code === 'intelligence-output-overflow') {
|
|
479
|
+
expect(overflowEvent.error.detail).toEqual({
|
|
480
|
+
activityId: overflowId,
|
|
481
|
+
bound: 'pending-events',
|
|
482
|
+
limit: 2,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
expect(cancelCalls).toEqual([returnedId, overflowId]);
|
|
487
|
+
|
|
488
|
+
const siblingSink = sinks.get(sibling.value.id);
|
|
489
|
+
siblingSink?.text('sibling-alive');
|
|
490
|
+
expect(runtime.poll()).toEqual([
|
|
491
|
+
{ type: 'text-delta', activityId: siblingId, sequence: 1, text: 'sibling-alive' },
|
|
492
|
+
]);
|
|
493
|
+
siblingSink?.complete('sibling-done');
|
|
494
|
+
expect(runtime.poll()).toEqual([
|
|
495
|
+
{
|
|
496
|
+
type: 'completed',
|
|
497
|
+
activityId: siblingId,
|
|
498
|
+
sequence: 2,
|
|
499
|
+
session: sibling.value.session,
|
|
500
|
+
output: 'sibling-done',
|
|
501
|
+
},
|
|
502
|
+
]);
|
|
503
|
+
});
|
|
504
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { ok } from '@forgeax/engine-types';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { createIntelligenceRuntime } from '../runtime';
|
|
4
|
+
import {
|
|
5
|
+
bindIntelligencePort,
|
|
6
|
+
createIntelligencePortClient,
|
|
7
|
+
type IntelligenceMessagePort,
|
|
8
|
+
} from '../transport';
|
|
9
|
+
import { type ActivitySink, activityId, type IntelligenceProvider } from '../types';
|
|
10
|
+
|
|
11
|
+
describe('intelligence MessagePort boundary', () => {
|
|
12
|
+
it('moves only structured-cloneable POD across a Worker-style channel', async () => {
|
|
13
|
+
const sinks: ActivitySink[] = [];
|
|
14
|
+
let providerClosed = false;
|
|
15
|
+
const provider: IntelligenceProvider = {
|
|
16
|
+
id: 'port.provider',
|
|
17
|
+
start(_submission, sink) {
|
|
18
|
+
sinks.push(sink);
|
|
19
|
+
return ok(undefined);
|
|
20
|
+
},
|
|
21
|
+
cancel() {
|
|
22
|
+
sinks[0]?.cancelled();
|
|
23
|
+
return ok(undefined);
|
|
24
|
+
},
|
|
25
|
+
async close() {
|
|
26
|
+
providerClosed = true;
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
const runtime = createIntelligenceRuntime(provider);
|
|
30
|
+
const channel = new MessageChannel();
|
|
31
|
+
const host = bindIntelligencePort(channel.port1 as unknown as IntelligenceMessagePort, runtime);
|
|
32
|
+
const client = createIntelligencePortClient(
|
|
33
|
+
'port.provider',
|
|
34
|
+
channel.port2 as unknown as IntelligenceMessagePort,
|
|
35
|
+
{
|
|
36
|
+
createActivityId: () => activityId('worker-activity'),
|
|
37
|
+
createSessionId: () => 'worker-session',
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const started = client.submit({ input: 'hello worker' });
|
|
42
|
+
expect(started.ok).toBe(true);
|
|
43
|
+
for (let attempt = 0; attempt < 10 && sinks.length === 0; attempt += 1) {
|
|
44
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
45
|
+
}
|
|
46
|
+
expect(sinks).toHaveLength(1);
|
|
47
|
+
sinks[0]?.text('hello ');
|
|
48
|
+
sinks[0]?.complete('hello worker');
|
|
49
|
+
const received = [] as ReturnType<typeof client.poll>[number][];
|
|
50
|
+
for (let attempt = 0; attempt < 10 && received.length === 0; attempt += 1) {
|
|
51
|
+
received.push(...client.poll());
|
|
52
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
53
|
+
}
|
|
54
|
+
expect(structuredClone(received)).toEqual([
|
|
55
|
+
{
|
|
56
|
+
type: 'text-delta',
|
|
57
|
+
activityId: 'worker-activity',
|
|
58
|
+
sequence: 1,
|
|
59
|
+
text: 'hello ',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
type: 'completed',
|
|
63
|
+
activityId: 'worker-activity',
|
|
64
|
+
sequence: 2,
|
|
65
|
+
session: { providerId: 'port.provider', id: 'worker-session' },
|
|
66
|
+
output: 'hello worker',
|
|
67
|
+
},
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
await client.close();
|
|
71
|
+
expect(providerClosed).toBe(true);
|
|
72
|
+
await host.close();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('bounds realm submissions and releases capacity after a terminal event', async () => {
|
|
76
|
+
const sinks = new Map<string, ActivitySink>();
|
|
77
|
+
const provider: IntelligenceProvider = {
|
|
78
|
+
id: 'bounded.provider',
|
|
79
|
+
start(submission, sink) {
|
|
80
|
+
sinks.set(submission.id, sink);
|
|
81
|
+
return ok(undefined);
|
|
82
|
+
},
|
|
83
|
+
cancel(id) {
|
|
84
|
+
sinks.get(id)?.cancelled();
|
|
85
|
+
sinks.delete(id);
|
|
86
|
+
return ok(undefined);
|
|
87
|
+
},
|
|
88
|
+
async close() {},
|
|
89
|
+
};
|
|
90
|
+
const runtime = createIntelligenceRuntime(provider, {
|
|
91
|
+
limits: { maxConcurrentActivities: 1 },
|
|
92
|
+
});
|
|
93
|
+
const channel = new MessageChannel();
|
|
94
|
+
const host = bindIntelligencePort(channel.port1 as unknown as IntelligenceMessagePort, runtime);
|
|
95
|
+
let identity = 0;
|
|
96
|
+
const client = createIntelligencePortClient(
|
|
97
|
+
'bounded.provider',
|
|
98
|
+
channel.port2 as unknown as IntelligenceMessagePort,
|
|
99
|
+
{
|
|
100
|
+
limits: { maxConcurrentActivities: 1 },
|
|
101
|
+
createActivityId: () => {
|
|
102
|
+
identity += 1;
|
|
103
|
+
return activityId(`bounded-${identity}`);
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const first = client.submit({ input: 'first' });
|
|
109
|
+
expect(first.ok).toBe(true);
|
|
110
|
+
const second = client.submit({ input: 'second' });
|
|
111
|
+
expect(second.ok).toBe(false);
|
|
112
|
+
if (!second.ok) expect(second.error.code).toBe('intelligence-capacity-exceeded');
|
|
113
|
+
|
|
114
|
+
client.poll();
|
|
115
|
+
client.poll();
|
|
116
|
+
for (let attempt = 0; attempt < 10 && sinks.size === 0; attempt += 1) {
|
|
117
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
118
|
+
}
|
|
119
|
+
if (!first.ok) return;
|
|
120
|
+
sinks.get(first.value.id)?.complete('done');
|
|
121
|
+
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
122
|
+
client.poll();
|
|
123
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
124
|
+
}
|
|
125
|
+
expect(client.submit({ input: 'after terminal' }).ok).toBe(true);
|
|
126
|
+
|
|
127
|
+
await client.close();
|
|
128
|
+
await host.close();
|
|
129
|
+
});
|
|
130
|
+
});
|