@forgeax/engine-intelligence 0.0.0-dev.8d955ade1c79

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.
@@ -0,0 +1,282 @@
1
+ import { err, ok, type Result } from '@forgeax/engine-types';
2
+ import { IntelligenceError } from './errors';
3
+ import { type IntelligenceRuntime, resolveIntelligenceLimits } from './runtime';
4
+ import {
5
+ type ActivityEvent,
6
+ type ActivityId,
7
+ type ActivityRef,
8
+ type ActivityRequest,
9
+ type ActivitySubmission,
10
+ activityId,
11
+ type IntelligenceLimits,
12
+ type IntelligenceRuntimeOptions,
13
+ type IntelligenceService,
14
+ } from './types';
15
+
16
+ export type IntelligenceHostCommand =
17
+ | { readonly kind: 'intelligence-submit'; readonly submission: ActivitySubmission }
18
+ | { readonly kind: 'intelligence-poll'; readonly maxEvents: number }
19
+ | { readonly kind: 'intelligence-cancel'; readonly activityId: ActivityId }
20
+ | { readonly kind: 'intelligence-close' };
21
+
22
+ export type IntelligenceRealmMessage =
23
+ | { readonly kind: 'intelligence-events'; readonly events: readonly ActivityEvent[] }
24
+ | { readonly kind: 'intelligence-closed' }
25
+ | {
26
+ readonly kind: 'intelligence-rejected';
27
+ readonly activityId: ActivityId;
28
+ readonly error: import('./errors').IntelligenceFailure;
29
+ };
30
+
31
+ export interface IntelligenceMessagePort {
32
+ postMessage(message: IntelligenceHostCommand | IntelligenceRealmMessage): void;
33
+ addEventListener(
34
+ type: 'message',
35
+ listener: (event: MessageEvent<IntelligenceHostCommand | IntelligenceRealmMessage>) => void,
36
+ ): void;
37
+ removeEventListener(
38
+ type: 'message',
39
+ listener: (event: MessageEvent<IntelligenceHostCommand | IntelligenceRealmMessage>) => void,
40
+ ): void;
41
+ start?(): void;
42
+ close(): void;
43
+ }
44
+
45
+ export interface IntelligencePortBinding {
46
+ close(): Promise<void>;
47
+ }
48
+
49
+ /** Bind a Host-owned runtime to the realm side of a MessageChannel. */
50
+ export function bindIntelligencePort(
51
+ port: IntelligenceMessagePort,
52
+ runtime: IntelligenceRuntime,
53
+ ): IntelligencePortBinding {
54
+ let closed = false;
55
+ const listener = (
56
+ event: MessageEvent<IntelligenceHostCommand | IntelligenceRealmMessage>,
57
+ ): void => {
58
+ const message = event.data;
59
+ if (message.kind === 'intelligence-submit') {
60
+ const accepted = runtime.accept(message.submission);
61
+ if (!accepted.ok) {
62
+ const failure = importFailure(accepted.error);
63
+ port.postMessage({
64
+ kind: 'intelligence-rejected',
65
+ activityId: message.submission.id,
66
+ error: failure,
67
+ });
68
+ }
69
+ return;
70
+ }
71
+ if (message.kind === 'intelligence-poll') {
72
+ port.postMessage({ kind: 'intelligence-events', events: runtime.poll(message.maxEvents) });
73
+ return;
74
+ }
75
+ if (message.kind === 'intelligence-cancel') {
76
+ runtime.cancel(message.activityId);
77
+ return;
78
+ }
79
+ if (message.kind === 'intelligence-close') void close(true);
80
+ };
81
+ const close = async (notifyRealm = false): Promise<void> => {
82
+ if (closed) return;
83
+ closed = true;
84
+ port.removeEventListener('message', listener);
85
+ await runtime.close();
86
+ if (notifyRealm) {
87
+ port.postMessage({ kind: 'intelligence-closed' });
88
+ setTimeout(() => port.close(), 0);
89
+ } else {
90
+ port.close();
91
+ }
92
+ };
93
+ port.addEventListener('message', listener);
94
+ port.start?.();
95
+ return { close };
96
+ }
97
+
98
+ function importFailure(error: import('./errors').IntelligenceError) {
99
+ if (error.code === 'intelligence-provider-failed') {
100
+ return {
101
+ code: error.code,
102
+ expected: error.expected,
103
+ hint: error.hint,
104
+ detail: {
105
+ providerId: error.detail.providerId,
106
+ cause:
107
+ error.detail.cause instanceof Error
108
+ ? error.detail.cause.message
109
+ : String(error.detail.cause),
110
+ },
111
+ } as const;
112
+ }
113
+ return {
114
+ code: error.code,
115
+ expected: error.expected,
116
+ hint: error.hint,
117
+ detail: error.detail,
118
+ } as import('./errors').IntelligenceFailure;
119
+ }
120
+
121
+ let portIdentity = 0;
122
+
123
+ function nextPortIdentity(prefix: string): string {
124
+ const uuid = globalThis.crypto?.randomUUID?.();
125
+ if (uuid !== undefined) return `${prefix}-${uuid}`;
126
+ portIdentity += 1;
127
+ return `${prefix}-${portIdentity}`;
128
+ }
129
+
130
+ /** Worker/main-realm client. Polling is request/response and never awaits a Host provider. */
131
+ export class IntelligencePortClient implements IntelligenceService {
132
+ readonly limits: IntelligenceLimits;
133
+ private readonly received: ActivityEvent[] = [];
134
+ private readonly active = new Set<ActivityId>();
135
+ private readonly rejected = new Map<
136
+ ActivityId,
137
+ IntelligenceRealmMessage & { kind: 'intelligence-rejected' }
138
+ >();
139
+ private readonly createActivityId: () => ActivityId;
140
+ private readonly createSessionId: () => string;
141
+ private readonly listener: (
142
+ event: MessageEvent<IntelligenceHostCommand | IntelligenceRealmMessage>,
143
+ ) => void;
144
+ private closed = false;
145
+ private pollPending = false;
146
+ private closeTask: Promise<void> | undefined;
147
+ private closeResolve: (() => void) | undefined;
148
+
149
+ constructor(
150
+ readonly providerId: string,
151
+ private readonly port: IntelligenceMessagePort,
152
+ options: IntelligenceRuntimeOptions = {},
153
+ ) {
154
+ this.limits = resolveIntelligenceLimits(options.limits);
155
+ this.createActivityId =
156
+ options.createActivityId ?? (() => activityId(nextPortIdentity('activity')));
157
+ this.createSessionId = options.createSessionId ?? (() => nextPortIdentity('session'));
158
+ this.listener = (event) => {
159
+ const message = event.data;
160
+ if (message.kind === 'intelligence-events') {
161
+ this.pollPending = false;
162
+ for (const item of message.events) {
163
+ this.received.push(item);
164
+ if (item.type !== 'text-delta') this.active.delete(item.activityId);
165
+ }
166
+ } else if (message.kind === 'intelligence-rejected') {
167
+ this.active.delete(message.activityId);
168
+ this.rejected.set(message.activityId, message);
169
+ } else if (message.kind === 'intelligence-closed') {
170
+ this.port.removeEventListener('message', this.listener);
171
+ this.received.length = 0;
172
+ this.active.clear();
173
+ this.rejected.clear();
174
+ this.port.close();
175
+ this.closeResolve?.();
176
+ this.closeResolve = undefined;
177
+ }
178
+ };
179
+ port.addEventListener('message', this.listener);
180
+ port.start?.();
181
+ }
182
+
183
+ submit(request: ActivityRequest): Result<ActivityRef, IntelligenceError> {
184
+ if (this.closed) return err(new IntelligenceError({ code: 'intelligence-closed', detail: {} }));
185
+ if (request.input.length === 0 || request.input.length > this.limits.maxInputChars) {
186
+ return err(
187
+ new IntelligenceError({
188
+ code: 'intelligence-invalid-request',
189
+ detail: {
190
+ field: 'input',
191
+ reason:
192
+ request.input.length === 0
193
+ ? 'input is empty'
194
+ : `input exceeds ${this.limits.maxInputChars} characters`,
195
+ },
196
+ }),
197
+ );
198
+ }
199
+ if (request.session !== undefined && request.session.providerId !== this.providerId) {
200
+ return err(
201
+ new IntelligenceError({
202
+ code: 'intelligence-session-provider-mismatch',
203
+ detail: {
204
+ expectedProviderId: this.providerId,
205
+ receivedProviderId: request.session.providerId,
206
+ },
207
+ }),
208
+ );
209
+ }
210
+ if (this.active.size >= this.limits.maxConcurrentActivities) {
211
+ return err(
212
+ new IntelligenceError({
213
+ code: 'intelligence-capacity-exceeded',
214
+ detail: { limit: this.limits.maxConcurrentActivities },
215
+ }),
216
+ );
217
+ }
218
+ const ref: ActivityRef = {
219
+ id: this.createActivityId(),
220
+ session: request.session ?? { providerId: this.providerId, id: this.createSessionId() },
221
+ };
222
+ this.active.add(ref.id);
223
+ this.port.postMessage({
224
+ kind: 'intelligence-submit',
225
+ submission: { ...ref, input: request.input },
226
+ });
227
+ return ok(ref);
228
+ }
229
+
230
+ poll(maxEvents = this.limits.maxPollEvents): readonly ActivityEvent[] {
231
+ if (this.closed || !Number.isInteger(maxEvents) || maxEvents <= 0) return [];
232
+ const count = Math.min(maxEvents, this.limits.maxPollEvents);
233
+ const events = this.received.splice(0, count);
234
+ for (const [id, rejection] of this.rejected) {
235
+ if (events.length >= count) break;
236
+ this.rejected.delete(id);
237
+ events.push({
238
+ type: 'failed',
239
+ activityId: id,
240
+ sequence: 1,
241
+ error: rejection.error,
242
+ });
243
+ }
244
+ if (!this.pollPending) {
245
+ this.pollPending = true;
246
+ this.port.postMessage({ kind: 'intelligence-poll', maxEvents: count });
247
+ }
248
+ return events;
249
+ }
250
+
251
+ cancel(id: ActivityId): Result<void, IntelligenceError> {
252
+ if (this.closed) return err(new IntelligenceError({ code: 'intelligence-closed', detail: {} }));
253
+ if (!this.active.has(id)) {
254
+ return err(
255
+ new IntelligenceError({
256
+ code: 'intelligence-activity-not-found',
257
+ detail: { activityId: id },
258
+ }),
259
+ );
260
+ }
261
+ this.port.postMessage({ kind: 'intelligence-cancel', activityId: id });
262
+ return ok(undefined);
263
+ }
264
+
265
+ close(): Promise<void> {
266
+ if (this.closeTask !== undefined) return this.closeTask;
267
+ this.closed = true;
268
+ this.closeTask = new Promise((resolve) => {
269
+ this.closeResolve = resolve;
270
+ this.port.postMessage({ kind: 'intelligence-close' });
271
+ });
272
+ return this.closeTask;
273
+ }
274
+ }
275
+
276
+ export function createIntelligencePortClient(
277
+ providerId: string,
278
+ port: IntelligenceMessagePort,
279
+ options: IntelligenceRuntimeOptions = {},
280
+ ): IntelligencePortClient {
281
+ return new IntelligencePortClient(providerId, port, options);
282
+ }
package/src/types.ts ADDED
@@ -0,0 +1,104 @@
1
+ import type { Result } from '@forgeax/engine-types';
2
+ import type { IntelligenceError, IntelligenceFailure } from './errors';
3
+
4
+ declare const activityIdBrand: unique symbol;
5
+
6
+ /** Opaque identity of one bounded asynchronous operation. */
7
+ export type ActivityId = string & { readonly [activityIdBrand]: true };
8
+
9
+ /** Provider-scoped durable conversation identity. */
10
+ export interface SessionRef {
11
+ readonly providerId: string;
12
+ readonly id: string;
13
+ }
14
+
15
+ export interface ActivityRef {
16
+ readonly id: ActivityId;
17
+ readonly session: SessionRef;
18
+ }
19
+
20
+ export interface ActivityRequest {
21
+ readonly input: string;
22
+ readonly session?: SessionRef;
23
+ }
24
+
25
+ /** Fully identified request used by realm transports and provider dispatch. */
26
+ export interface ActivitySubmission extends ActivityRef {
27
+ readonly input: string;
28
+ }
29
+
30
+ export type ActivityEvent =
31
+ | {
32
+ readonly type: 'text-delta';
33
+ readonly activityId: ActivityId;
34
+ readonly sequence: number;
35
+ readonly text: string;
36
+ }
37
+ | {
38
+ readonly type: 'completed';
39
+ readonly activityId: ActivityId;
40
+ readonly sequence: number;
41
+ readonly session: SessionRef;
42
+ readonly output: string;
43
+ }
44
+ | {
45
+ readonly type: 'failed';
46
+ readonly activityId: ActivityId;
47
+ readonly sequence: number;
48
+ readonly error: IntelligenceFailure;
49
+ }
50
+ | {
51
+ readonly type: 'cancelled';
52
+ readonly activityId: ActivityId;
53
+ readonly sequence: number;
54
+ };
55
+
56
+ export interface ActivitySink {
57
+ text(text: string): void;
58
+ complete(output: string): void;
59
+ fail(cause: unknown): void;
60
+ cancelled(): void;
61
+ }
62
+
63
+ /** Provider implementation boundary. It never receives World or Renderer authority. */
64
+ export interface IntelligenceProvider {
65
+ readonly id: string;
66
+ start(submission: ActivitySubmission, sink: ActivitySink): Result<void, IntelligenceError>;
67
+ cancel(activityId: ActivityId): Result<void, IntelligenceError>;
68
+ close(): Promise<void>;
69
+ }
70
+
71
+ /** Frame-safe consumer surface. No method waits for provider work. */
72
+ export interface IntelligenceService {
73
+ readonly providerId: string;
74
+ submit(request: ActivityRequest): Result<ActivityRef, IntelligenceError>;
75
+ poll(maxEvents?: number): readonly ActivityEvent[];
76
+ cancel(activityId: ActivityId): Result<void, IntelligenceError>;
77
+ close(): Promise<void>;
78
+ }
79
+
80
+ export interface IntelligenceLimits {
81
+ readonly maxInputChars: number;
82
+ readonly maxOutputChars: number;
83
+ readonly maxConcurrentActivities: number;
84
+ readonly maxPendingEventsPerActivity: number;
85
+ readonly maxPollEvents: number;
86
+ }
87
+
88
+ export const DEFAULT_INTELLIGENCE_LIMITS: IntelligenceLimits = {
89
+ maxInputChars: 16_384,
90
+ maxOutputChars: 65_536,
91
+ maxConcurrentActivities: 8,
92
+ maxPendingEventsPerActivity: 256,
93
+ maxPollEvents: 64,
94
+ };
95
+
96
+ export interface IntelligenceRuntimeOptions {
97
+ readonly limits?: Partial<IntelligenceLimits>;
98
+ readonly createActivityId?: () => ActivityId;
99
+ readonly createSessionId?: () => string;
100
+ }
101
+
102
+ export function activityId(value: string): ActivityId {
103
+ return value as ActivityId;
104
+ }