@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.
package/src/errors.ts ADDED
@@ -0,0 +1,154 @@
1
+ import type { ActivityId } from './types';
2
+
3
+ export type IntelligenceErrorCode =
4
+ | 'intelligence-invalid-request'
5
+ | 'intelligence-session-provider-mismatch'
6
+ | 'intelligence-capacity-exceeded'
7
+ | 'intelligence-activity-not-found'
8
+ | 'intelligence-provider-failed'
9
+ | 'intelligence-output-overflow'
10
+ | 'intelligence-closed';
11
+
12
+ export interface IntelligenceErrorDetailMap {
13
+ readonly 'intelligence-invalid-request': { readonly field: 'input'; readonly reason: string };
14
+ readonly 'intelligence-session-provider-mismatch': {
15
+ readonly expectedProviderId: string;
16
+ readonly receivedProviderId: string;
17
+ };
18
+ readonly 'intelligence-capacity-exceeded': { readonly limit: number };
19
+ readonly 'intelligence-activity-not-found': { readonly activityId: ActivityId };
20
+ readonly 'intelligence-provider-failed': { readonly providerId: string; readonly cause: unknown };
21
+ readonly 'intelligence-output-overflow': {
22
+ readonly activityId: ActivityId;
23
+ readonly bound: 'output-chars' | 'pending-events';
24
+ readonly limit: number;
25
+ };
26
+ readonly 'intelligence-closed': Readonly<Record<string, never>>;
27
+ }
28
+
29
+ export type IntelligenceErrorDetailFor<C extends IntelligenceErrorCode> =
30
+ IntelligenceErrorDetailMap[C];
31
+
32
+ const policy = {
33
+ 'intelligence-invalid-request': {
34
+ expected: 'activity input must be a non-empty string within the configured bound',
35
+ hint: 'validate and bound player input before submitting the activity',
36
+ },
37
+ 'intelligence-session-provider-mismatch': {
38
+ expected: 'a session reference must be resumed by the provider that created it',
39
+ hint: 'discard the incompatible session or select its original provider',
40
+ },
41
+ 'intelligence-capacity-exceeded': {
42
+ expected: 'active intelligence work must remain within the configured concurrency bound',
43
+ hint: 'wait for an activity to finish or raise the explicit provider capacity',
44
+ },
45
+ 'intelligence-activity-not-found': {
46
+ expected: 'the activity must still be running when cancellation is requested',
47
+ hint: 'ignore an already observed terminal activity or retain the correct ActivityId',
48
+ },
49
+ 'intelligence-provider-failed': {
50
+ expected: 'the selected provider must complete or report a structured failure',
51
+ hint: 'inspect detail.cause and provider configuration, then retry or select another provider',
52
+ },
53
+ 'intelligence-output-overflow': {
54
+ expected: 'incremental and final output must remain within configured queue and text bounds',
55
+ hint: 'consume events every frame or raise the explicit bound for this application',
56
+ },
57
+ 'intelligence-closed': {
58
+ expected: 'the intelligence service must be open for submit and cancel operations',
59
+ hint: 'create a new service after its Cordis realm or owner has been disposed',
60
+ },
61
+ } satisfies Record<IntelligenceErrorCode, { readonly expected: string; readonly hint: string }>;
62
+
63
+ export const INTELLIGENCE_EXPECTED = Object.fromEntries(
64
+ Object.entries(policy).map(([code, value]) => [code, value.expected]),
65
+ ) as Readonly<Record<IntelligenceErrorCode, string>>;
66
+
67
+ export const INTELLIGENCE_ERROR_HINTS = Object.fromEntries(
68
+ Object.entries(policy).map(([code, value]) => [code, value.hint]),
69
+ ) as Readonly<Record<IntelligenceErrorCode, string>>;
70
+
71
+ class IntelligenceErrorClass extends Error {
72
+ readonly code: IntelligenceErrorCode;
73
+ readonly expected: string;
74
+ readonly hint: string;
75
+ readonly detail: IntelligenceErrorDetailFor<IntelligenceErrorCode>;
76
+
77
+ constructor(args: {
78
+ code: IntelligenceErrorCode;
79
+ detail: IntelligenceErrorDetailFor<IntelligenceErrorCode>;
80
+ }) {
81
+ const selected = policy[args.code];
82
+ super(
83
+ `[IntelligenceError ${args.code}] expected: ${selected.expected}; hint: ${selected.hint}`,
84
+ );
85
+ this.name = 'IntelligenceError';
86
+ this.code = args.code;
87
+ this.expected = selected.expected;
88
+ this.hint = selected.hint;
89
+ this.detail = args.detail;
90
+ }
91
+ }
92
+
93
+ type IntelligenceErrorVariant<C extends IntelligenceErrorCode> = IntelligenceErrorClass & {
94
+ readonly code: C;
95
+ readonly detail: IntelligenceErrorDetailFor<C>;
96
+ };
97
+
98
+ export type IntelligenceError = {
99
+ [C in IntelligenceErrorCode]: IntelligenceErrorVariant<C>;
100
+ }[IntelligenceErrorCode];
101
+
102
+ interface IntelligenceErrorConstructor {
103
+ new <C extends IntelligenceErrorCode>(args: {
104
+ code: C;
105
+ detail: IntelligenceErrorDetailFor<C>;
106
+ }): IntelligenceErrorVariant<C>;
107
+ readonly prototype: IntelligenceErrorClass;
108
+ }
109
+
110
+ export const IntelligenceError: IntelligenceErrorConstructor =
111
+ IntelligenceErrorClass as unknown as IntelligenceErrorConstructor;
112
+
113
+ type PodDetailMap = Omit<IntelligenceErrorDetailMap, 'intelligence-provider-failed'> & {
114
+ readonly 'intelligence-provider-failed': { readonly providerId: string; readonly cause: string };
115
+ };
116
+
117
+ export type IntelligenceFailure = {
118
+ [C in IntelligenceErrorCode]: {
119
+ readonly code: C;
120
+ readonly expected: string;
121
+ readonly hint: string;
122
+ readonly detail: PodDetailMap[C];
123
+ };
124
+ }[IntelligenceErrorCode];
125
+
126
+ export function intelligenceFailure(error: IntelligenceError): IntelligenceFailure {
127
+ if (error.code === 'intelligence-provider-failed') {
128
+ return {
129
+ code: error.code,
130
+ expected: error.expected,
131
+ hint: error.hint,
132
+ detail: {
133
+ providerId: error.detail.providerId,
134
+ cause:
135
+ error.detail.cause instanceof Error
136
+ ? error.detail.cause.message
137
+ : String(error.detail.cause),
138
+ },
139
+ };
140
+ }
141
+ return {
142
+ code: error.code,
143
+ expected: error.expected,
144
+ hint: error.hint,
145
+ detail: error.detail,
146
+ } as IntelligenceFailure;
147
+ }
148
+
149
+ export function providerError(providerId: string, cause: unknown): IntelligenceError {
150
+ return new IntelligenceError({
151
+ code: 'intelligence-provider-failed',
152
+ detail: { providerId, cause },
153
+ });
154
+ }
package/src/index.ts ADDED
@@ -0,0 +1,36 @@
1
+ export {
2
+ INTELLIGENCE_ERROR_HINTS,
3
+ INTELLIGENCE_EXPECTED,
4
+ IntelligenceError,
5
+ type IntelligenceErrorCode,
6
+ type IntelligenceErrorDetailFor,
7
+ type IntelligenceFailure,
8
+ intelligenceFailure,
9
+ providerError,
10
+ } from './errors';
11
+ export { intelligencePlugin } from './plugin';
12
+ export { createIntelligenceRuntime, IntelligenceRuntime } from './runtime';
13
+ export {
14
+ bindIntelligencePort,
15
+ createIntelligencePortClient,
16
+ type IntelligenceHostCommand,
17
+ type IntelligenceMessagePort,
18
+ type IntelligencePortBinding,
19
+ IntelligencePortClient,
20
+ type IntelligenceRealmMessage,
21
+ } from './transport';
22
+ export {
23
+ type ActivityEvent,
24
+ type ActivityId,
25
+ type ActivityRef,
26
+ type ActivityRequest,
27
+ type ActivitySink,
28
+ type ActivitySubmission,
29
+ activityId,
30
+ DEFAULT_INTELLIGENCE_LIMITS,
31
+ type IntelligenceLimits,
32
+ type IntelligenceProvider,
33
+ type IntelligenceRuntimeOptions,
34
+ type IntelligenceService,
35
+ type SessionRef,
36
+ } from './types';
package/src/plugin.ts ADDED
@@ -0,0 +1,20 @@
1
+ import type { Plugin } from '@forgeax/engine-plugin';
2
+ import type { IntelligenceService } from './types';
3
+
4
+ declare module '@forgeax/engine-plugin' {
5
+ interface EngineContextServices {
6
+ intelligence?: IntelligenceService;
7
+ }
8
+ }
9
+
10
+ /** Install one optional intelligence service and bind its lifetime to this Cordis Fiber. */
11
+ export function intelligencePlugin(service: IntelligenceService): Plugin {
12
+ return {
13
+ name: 'intelligence',
14
+ provide: 'intelligence',
15
+ apply(ctx) {
16
+ ctx.provide('intelligence', service);
17
+ ctx.effect(() => () => service.close(), 'intelligence/service');
18
+ },
19
+ };
20
+ }
package/src/runtime.ts ADDED
@@ -0,0 +1,297 @@
1
+ import { err, ok, type Result } from '@forgeax/engine-types';
2
+ import { IntelligenceError, intelligenceFailure, providerError } from './errors';
3
+ import {
4
+ type ActivityEvent,
5
+ type ActivityId,
6
+ type ActivityRef,
7
+ type ActivityRequest,
8
+ type ActivitySink,
9
+ type ActivitySubmission,
10
+ activityId,
11
+ DEFAULT_INTELLIGENCE_LIMITS,
12
+ type IntelligenceLimits,
13
+ type IntelligenceProvider,
14
+ type IntelligenceRuntimeOptions,
15
+ type IntelligenceService,
16
+ } from './types';
17
+
18
+ interface ActivityRecord {
19
+ readonly ref: ActivityRef;
20
+ readonly events: ActivityEvent[];
21
+ sequence: number;
22
+ outputChars: number;
23
+ terminal: boolean;
24
+ }
25
+
26
+ let fallbackIdentity = 0;
27
+
28
+ function nextIdentity(prefix: string): string {
29
+ const uuid = globalThis.crypto?.randomUUID?.();
30
+ if (uuid !== undefined) return `${prefix}-${uuid}`;
31
+ fallbackIdentity += 1;
32
+ return `${prefix}-${Date.now().toString(36)}-${fallbackIdentity.toString(36)}`;
33
+ }
34
+
35
+ export function resolveIntelligenceLimits(
36
+ overrides: Partial<IntelligenceLimits> | undefined,
37
+ ): IntelligenceLimits {
38
+ const limits = { ...DEFAULT_INTELLIGENCE_LIMITS, ...overrides };
39
+ for (const [name, value] of Object.entries(limits)) {
40
+ if (!Number.isInteger(value) || value <= 0) {
41
+ throw new RangeError(`${name} must be a positive integer`);
42
+ }
43
+ }
44
+ if (limits.maxPendingEventsPerActivity < 2) {
45
+ throw new RangeError(
46
+ 'maxPendingEventsPerActivity must reserve at least one data and terminal event',
47
+ );
48
+ }
49
+ return limits;
50
+ }
51
+
52
+ export class IntelligenceRuntime implements IntelligenceService {
53
+ readonly providerId: string;
54
+ readonly limits: IntelligenceLimits;
55
+ private readonly records = new Map<ActivityId, ActivityRecord>();
56
+ private readonly createActivityId: () => ActivityId;
57
+ private readonly createSessionId: () => string;
58
+ private closed = false;
59
+ private closeTask: Promise<void> | undefined;
60
+
61
+ constructor(
62
+ private readonly provider: IntelligenceProvider,
63
+ options: IntelligenceRuntimeOptions = {},
64
+ ) {
65
+ this.providerId = provider.id;
66
+ this.limits = resolveIntelligenceLimits(options.limits);
67
+ this.createActivityId =
68
+ options.createActivityId ?? (() => activityId(nextIdentity('activity')));
69
+ this.createSessionId = options.createSessionId ?? (() => nextIdentity('session'));
70
+ }
71
+
72
+ submit(request: ActivityRequest): Result<ActivityRef, IntelligenceError> {
73
+ const validated = this.validateRequest(request);
74
+ if (!validated.ok) return validated;
75
+ const ref: ActivityRef = {
76
+ id: this.createActivityId(),
77
+ session: request.session ?? { providerId: this.providerId, id: this.createSessionId() },
78
+ };
79
+ const accepted = this.accept({ ...ref, input: request.input });
80
+ return accepted.ok ? ok(ref) : accepted;
81
+ }
82
+
83
+ /** Accept an already identified request from a realm transport. */
84
+ accept(submission: ActivitySubmission): Result<void, IntelligenceError> {
85
+ const validated = this.validateRequest({
86
+ input: submission.input,
87
+ session: submission.session,
88
+ });
89
+ if (!validated.ok) return validated;
90
+ if (this.records.has(submission.id)) {
91
+ return err(
92
+ providerError(this.providerId, `duplicate activity identity: ${String(submission.id)}`),
93
+ );
94
+ }
95
+ if (this.runningCount >= this.limits.maxConcurrentActivities) {
96
+ return err(
97
+ new IntelligenceError({
98
+ code: 'intelligence-capacity-exceeded',
99
+ detail: { limit: this.limits.maxConcurrentActivities },
100
+ }),
101
+ );
102
+ }
103
+ const record: ActivityRecord = {
104
+ ref: { id: submission.id, session: submission.session },
105
+ events: [],
106
+ sequence: 0,
107
+ outputChars: 0,
108
+ terminal: false,
109
+ };
110
+ this.records.set(submission.id, record);
111
+ let started: Result<void, IntelligenceError>;
112
+ try {
113
+ started = this.provider.start(submission, this.createSink(record));
114
+ } catch (cause) {
115
+ this.records.delete(submission.id);
116
+ return err(providerError(this.providerId, cause));
117
+ }
118
+ if (!started.ok) {
119
+ this.records.delete(submission.id);
120
+ return started;
121
+ }
122
+ return ok(undefined);
123
+ }
124
+
125
+ poll(maxEvents = this.limits.maxPollEvents): readonly ActivityEvent[] {
126
+ if (!Number.isInteger(maxEvents) || maxEvents <= 0) return [];
127
+ const bounded = Math.min(maxEvents, this.limits.maxPollEvents);
128
+ const events: ActivityEvent[] = [];
129
+ for (const [id, record] of this.records) {
130
+ while (record.events.length > 0 && events.length < bounded) {
131
+ const event = record.events.shift();
132
+ if (event !== undefined) events.push(event);
133
+ }
134
+ if (record.terminal && record.events.length === 0) this.records.delete(id);
135
+ if (events.length === bounded) break;
136
+ }
137
+ return events;
138
+ }
139
+
140
+ cancel(activity: ActivityId): Result<void, IntelligenceError> {
141
+ if (this.closed) return err(this.closedError());
142
+ const record = this.records.get(activity);
143
+ if (record === undefined || record.terminal) {
144
+ return err(
145
+ new IntelligenceError({
146
+ code: 'intelligence-activity-not-found',
147
+ detail: { activityId: activity },
148
+ }),
149
+ );
150
+ }
151
+ try {
152
+ return this.provider.cancel(activity);
153
+ } catch (cause) {
154
+ return err(providerError(this.providerId, cause));
155
+ }
156
+ }
157
+
158
+ close(): Promise<void> {
159
+ this.closeTask ??= this.performClose();
160
+ return this.closeTask;
161
+ }
162
+
163
+ private async performClose(): Promise<void> {
164
+ this.closed = true;
165
+ try {
166
+ await this.provider.close();
167
+ } catch {
168
+ // Disposal is terminal; provider failure must not strand the realm transport.
169
+ }
170
+ this.records.clear();
171
+ }
172
+
173
+ private get runningCount(): number {
174
+ let count = 0;
175
+ for (const record of this.records.values()) if (!record.terminal) count += 1;
176
+ return count;
177
+ }
178
+
179
+ private validateRequest(request: ActivityRequest): Result<void, IntelligenceError> {
180
+ if (this.closed) return err(this.closedError());
181
+ if (request.input.length === 0 || request.input.length > this.limits.maxInputChars) {
182
+ return err(
183
+ new IntelligenceError({
184
+ code: 'intelligence-invalid-request',
185
+ detail: {
186
+ field: 'input',
187
+ reason:
188
+ request.input.length === 0
189
+ ? 'input is empty'
190
+ : `input exceeds ${this.limits.maxInputChars} characters`,
191
+ },
192
+ }),
193
+ );
194
+ }
195
+ if (request.session !== undefined && request.session.providerId !== this.providerId) {
196
+ return err(
197
+ new IntelligenceError({
198
+ code: 'intelligence-session-provider-mismatch',
199
+ detail: {
200
+ expectedProviderId: this.providerId,
201
+ receivedProviderId: request.session.providerId,
202
+ },
203
+ }),
204
+ );
205
+ }
206
+ return ok(undefined);
207
+ }
208
+
209
+ private createSink(record: ActivityRecord): ActivitySink {
210
+ return {
211
+ text: (text) => {
212
+ if (this.closed || record.terminal || text.length === 0) return;
213
+ if (record.outputChars + text.length > this.limits.maxOutputChars) {
214
+ this.overflow(record, 'output-chars', this.limits.maxOutputChars);
215
+ return;
216
+ }
217
+ if (record.events.length >= this.limits.maxPendingEventsPerActivity - 1) {
218
+ this.overflow(record, 'pending-events', this.limits.maxPendingEventsPerActivity);
219
+ return;
220
+ }
221
+ record.outputChars += text.length;
222
+ this.push(record, { type: 'text-delta', text });
223
+ },
224
+ complete: (output) => {
225
+ if (this.closed || record.terminal) return;
226
+ if (output.length > this.limits.maxOutputChars) {
227
+ this.overflow(record, 'output-chars', this.limits.maxOutputChars);
228
+ return;
229
+ }
230
+ record.terminal = true;
231
+ this.push(record, { type: 'completed', session: record.ref.session, output });
232
+ },
233
+ fail: (cause) => {
234
+ if (this.closed || record.terminal) return;
235
+ record.terminal = true;
236
+ const error = providerError(this.providerId, cause);
237
+ this.push(record, { type: 'failed', error: intelligenceFailure(error) });
238
+ },
239
+ cancelled: () => {
240
+ if (this.closed || record.terminal) return;
241
+ record.terminal = true;
242
+ this.push(record, { type: 'cancelled' });
243
+ },
244
+ };
245
+ }
246
+
247
+ private overflow(
248
+ record: ActivityRecord,
249
+ bound: 'output-chars' | 'pending-events',
250
+ limit: number,
251
+ ): void {
252
+ if (record.terminal) return;
253
+ record.terminal = true;
254
+ const error = new IntelligenceError({
255
+ code: 'intelligence-output-overflow',
256
+ detail: { activityId: record.ref.id, bound, limit },
257
+ });
258
+ if (record.events.length >= this.limits.maxPendingEventsPerActivity) record.events.pop();
259
+ this.push(record, { type: 'failed', error: intelligenceFailure(error) });
260
+ try {
261
+ this.provider.cancel(record.ref.id);
262
+ } catch {
263
+ // The overflow terminal already owns the activity outcome.
264
+ }
265
+ }
266
+
267
+ private push(
268
+ record: ActivityRecord,
269
+ event:
270
+ | { readonly type: 'text-delta'; readonly text: string }
271
+ | {
272
+ readonly type: 'completed';
273
+ readonly session: ActivityRef['session'];
274
+ readonly output: string;
275
+ }
276
+ | { readonly type: 'failed'; readonly error: ReturnType<typeof intelligenceFailure> }
277
+ | { readonly type: 'cancelled' },
278
+ ): void {
279
+ record.sequence += 1;
280
+ record.events.push({
281
+ ...event,
282
+ activityId: record.ref.id,
283
+ sequence: record.sequence,
284
+ } as ActivityEvent);
285
+ }
286
+
287
+ private closedError(): IntelligenceError {
288
+ return new IntelligenceError({ code: 'intelligence-closed', detail: {} });
289
+ }
290
+ }
291
+
292
+ export function createIntelligenceRuntime(
293
+ provider: IntelligenceProvider,
294
+ options: IntelligenceRuntimeOptions = {},
295
+ ): IntelligenceRuntime {
296
+ return new IntelligenceRuntime(provider, options);
297
+ }