@agent-assistant/core 0.1.0 → 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/dist/core.d.ts +11 -0
- package/dist/core.js +272 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/types.d.ts +72 -0
- package/dist/types.js +1 -0
- package/package.json +1 -1
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AssistantDefinition, AssistantRuntime, RelayInboundAdapter, RelayOutboundAdapter } from './types.js';
|
|
2
|
+
export declare class AssistantDefinitionError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
export declare class OutboundEventError extends Error {
|
|
6
|
+
constructor(message: string);
|
|
7
|
+
}
|
|
8
|
+
export declare function createAssistant(definition: AssistantDefinition, adapters: {
|
|
9
|
+
inbound: RelayInboundAdapter;
|
|
10
|
+
outbound: RelayOutboundAdapter;
|
|
11
|
+
}): AssistantRuntime;
|
package/dist/core.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
const DEFAULT_HANDLER_TIMEOUT_MS = 30_000;
|
|
2
|
+
const DEFAULT_MAX_CONCURRENT_HANDLERS = 10;
|
|
3
|
+
const STOP_DRAIN_TIMEOUT_MS = 30_000;
|
|
4
|
+
export class AssistantDefinitionError extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'AssistantDefinitionError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export class OutboundEventError extends Error {
|
|
11
|
+
constructor(message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'OutboundEventError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function freezeDefinition(definition) {
|
|
17
|
+
const frozenCapabilities = Object.freeze({ ...definition.capabilities });
|
|
18
|
+
const frozenHooks = definition.hooks ? Object.freeze({ ...definition.hooks }) : undefined;
|
|
19
|
+
const frozenConstraints = definition.constraints
|
|
20
|
+
? Object.freeze({ ...definition.constraints })
|
|
21
|
+
: undefined;
|
|
22
|
+
const frozenTraits = definition.traits ? Object.freeze(definition.traits) : undefined;
|
|
23
|
+
return Object.freeze({
|
|
24
|
+
...definition,
|
|
25
|
+
capabilities: frozenCapabilities,
|
|
26
|
+
hooks: frozenHooks,
|
|
27
|
+
constraints: frozenConstraints,
|
|
28
|
+
traits: frozenTraits,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function createContextLogger(context) {
|
|
32
|
+
const baseFields = {
|
|
33
|
+
messageId: context.messageId,
|
|
34
|
+
capability: context.capability,
|
|
35
|
+
surfaceId: context.surfaceId,
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
info(message, fields = {}) {
|
|
39
|
+
console.info(message, { ...baseFields, ...fields });
|
|
40
|
+
},
|
|
41
|
+
warn(message, fields = {}) {
|
|
42
|
+
console.warn(message, { ...baseFields, ...fields });
|
|
43
|
+
},
|
|
44
|
+
error(message, fields = {}) {
|
|
45
|
+
console.error(message, { ...baseFields, ...fields });
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function createDeferred() {
|
|
50
|
+
let resolve = () => { };
|
|
51
|
+
const promise = new Promise((innerResolve) => {
|
|
52
|
+
resolve = innerResolve;
|
|
53
|
+
});
|
|
54
|
+
return { promise, resolve };
|
|
55
|
+
}
|
|
56
|
+
function createTimeoutError(message, timeoutMs) {
|
|
57
|
+
return new Error(`Capability '${message.capability}' timed out after ${timeoutMs}ms for message '${message.id}'`);
|
|
58
|
+
}
|
|
59
|
+
function validateDefinition(definition) {
|
|
60
|
+
if (typeof definition.id !== 'string' || definition.id.trim().length === 0) {
|
|
61
|
+
throw new AssistantDefinitionError("Assistant definition requires a non-empty 'id'");
|
|
62
|
+
}
|
|
63
|
+
if (typeof definition.name !== 'string' || definition.name.trim().length === 0) {
|
|
64
|
+
throw new AssistantDefinitionError("Assistant definition requires a non-empty 'name'");
|
|
65
|
+
}
|
|
66
|
+
if (definition.capabilities === null ||
|
|
67
|
+
typeof definition.capabilities !== 'object' ||
|
|
68
|
+
Array.isArray(definition.capabilities)) {
|
|
69
|
+
throw new AssistantDefinitionError("Assistant definition requires a capabilities object");
|
|
70
|
+
}
|
|
71
|
+
const entries = Object.entries(definition.capabilities);
|
|
72
|
+
if (entries.length === 0) {
|
|
73
|
+
throw new AssistantDefinitionError('Assistant definition requires at least one capability');
|
|
74
|
+
}
|
|
75
|
+
for (const [capability, handler] of entries) {
|
|
76
|
+
if (typeof handler !== 'function') {
|
|
77
|
+
throw new AssistantDefinitionError(`Capability '${capability}' must be a function handler`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function withTimeout(handler, message, context, timeoutMs) {
|
|
82
|
+
let timeoutHandle;
|
|
83
|
+
try {
|
|
84
|
+
await Promise.race([
|
|
85
|
+
Promise.resolve(handler(message, context)),
|
|
86
|
+
new Promise((_, reject) => {
|
|
87
|
+
timeoutHandle = setTimeout(() => {
|
|
88
|
+
reject(createTimeoutError(message, timeoutMs));
|
|
89
|
+
}, timeoutMs);
|
|
90
|
+
}),
|
|
91
|
+
]);
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
if (timeoutHandle) {
|
|
95
|
+
clearTimeout(timeoutHandle);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function resolveAttachedSurfaces(sessionSubsystem, sessionId) {
|
|
100
|
+
const session = 'getSession' in sessionSubsystem
|
|
101
|
+
? await sessionSubsystem.getSession(sessionId)
|
|
102
|
+
: await sessionSubsystem.get(sessionId);
|
|
103
|
+
if (!session) {
|
|
104
|
+
throw new Error(`Session '${sessionId}' could not be resolved for fanout`);
|
|
105
|
+
}
|
|
106
|
+
return Array.isArray(session.attachedSurfaces) ? [...session.attachedSurfaces] : [];
|
|
107
|
+
}
|
|
108
|
+
export function createAssistant(definition, adapters) {
|
|
109
|
+
validateDefinition(definition);
|
|
110
|
+
const frozenDefinition = freezeDefinition(definition);
|
|
111
|
+
const capabilityMap = new Map(Object.entries(frozenDefinition.capabilities));
|
|
112
|
+
const subsystems = new Map();
|
|
113
|
+
const pendingDispatches = [];
|
|
114
|
+
let lifecycleState = 'created';
|
|
115
|
+
let inFlightCount = 0;
|
|
116
|
+
let startedAt = null;
|
|
117
|
+
let drainWaiter = null;
|
|
118
|
+
const constraints = {
|
|
119
|
+
handlerTimeoutMs: frozenDefinition.constraints?.handlerTimeoutMs ?? DEFAULT_HANDLER_TIMEOUT_MS,
|
|
120
|
+
maxConcurrentHandlers: frozenDefinition.constraints?.maxConcurrentHandlers ?? DEFAULT_MAX_CONCURRENT_HANDLERS,
|
|
121
|
+
};
|
|
122
|
+
const runtime = {
|
|
123
|
+
definition: frozenDefinition,
|
|
124
|
+
async emit(event) {
|
|
125
|
+
if (!event.surfaceId && !event.sessionId) {
|
|
126
|
+
throw new OutboundEventError("Outbound event requires either 'surfaceId' or 'sessionId'");
|
|
127
|
+
}
|
|
128
|
+
if (event.surfaceId) {
|
|
129
|
+
await adapters.outbound.send(event);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const sessionSubsystem = runtime.get('sessions');
|
|
133
|
+
const surfaceIds = await resolveAttachedSurfaces(sessionSubsystem, event.sessionId);
|
|
134
|
+
if (adapters.outbound.fanout) {
|
|
135
|
+
await adapters.outbound.fanout(event, surfaceIds);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
for (const surfaceId of surfaceIds) {
|
|
139
|
+
await adapters.outbound.send({ ...event, surfaceId });
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
async dispatch(message) {
|
|
143
|
+
if (lifecycleState !== 'started') {
|
|
144
|
+
throw new Error('Assistant runtime must be started before dispatching messages');
|
|
145
|
+
}
|
|
146
|
+
return new Promise((resolve, reject) => {
|
|
147
|
+
pendingDispatches.push({ message, resolve, reject });
|
|
148
|
+
runNext();
|
|
149
|
+
});
|
|
150
|
+
},
|
|
151
|
+
register(name, subsystem) {
|
|
152
|
+
subsystems.set(name, subsystem);
|
|
153
|
+
return runtime;
|
|
154
|
+
},
|
|
155
|
+
get(name) {
|
|
156
|
+
if (!subsystems.has(name)) {
|
|
157
|
+
throw new Error(`Subsystem '${name}' is not registered`);
|
|
158
|
+
}
|
|
159
|
+
return subsystems.get(name);
|
|
160
|
+
},
|
|
161
|
+
status() {
|
|
162
|
+
return {
|
|
163
|
+
ready: lifecycleState === 'started',
|
|
164
|
+
startedAt,
|
|
165
|
+
registeredSubsystems: [...subsystems.keys()],
|
|
166
|
+
registeredCapabilities: [...capabilityMap.keys()],
|
|
167
|
+
inFlightHandlers: inFlightCount,
|
|
168
|
+
};
|
|
169
|
+
},
|
|
170
|
+
async start() {
|
|
171
|
+
if (lifecycleState === 'started') {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (lifecycleState === 'stopped') {
|
|
175
|
+
throw new Error('Assistant runtime cannot be restarted after stop()');
|
|
176
|
+
}
|
|
177
|
+
lifecycleState = 'started';
|
|
178
|
+
startedAt = new Date().toISOString();
|
|
179
|
+
adapters.inbound.onMessage(inboundHandler);
|
|
180
|
+
await frozenDefinition.hooks?.onStart?.(runtime);
|
|
181
|
+
},
|
|
182
|
+
async stop() {
|
|
183
|
+
if (lifecycleState === 'stopped') {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const wasStarted = lifecycleState === 'started';
|
|
187
|
+
lifecycleState = 'stopped';
|
|
188
|
+
if (wasStarted) {
|
|
189
|
+
adapters.inbound.offMessage(inboundHandler);
|
|
190
|
+
await waitForDrain();
|
|
191
|
+
await frozenDefinition.hooks?.onStop?.(runtime);
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
const inboundHandler = (message) => {
|
|
196
|
+
void runtime.dispatch(message);
|
|
197
|
+
};
|
|
198
|
+
function maybeResolveDrainWaiter() {
|
|
199
|
+
if (inFlightCount === 0 && drainWaiter) {
|
|
200
|
+
drainWaiter.resolve();
|
|
201
|
+
drainWaiter = null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async function waitForDrain() {
|
|
205
|
+
if (inFlightCount === 0) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
drainWaiter ??= createDeferred();
|
|
209
|
+
await Promise.race([
|
|
210
|
+
drainWaiter.promise,
|
|
211
|
+
new Promise((_, reject) => {
|
|
212
|
+
setTimeout(() => {
|
|
213
|
+
reject(new Error(`Timed out waiting ${STOP_DRAIN_TIMEOUT_MS}ms for in-flight handlers to drain`));
|
|
214
|
+
}, STOP_DRAIN_TIMEOUT_MS);
|
|
215
|
+
}),
|
|
216
|
+
]);
|
|
217
|
+
}
|
|
218
|
+
function runNext() {
|
|
219
|
+
while (lifecycleState === 'started' &&
|
|
220
|
+
inFlightCount < constraints.maxConcurrentHandlers &&
|
|
221
|
+
pendingDispatches.length > 0) {
|
|
222
|
+
const nextDispatch = pendingDispatches.shift();
|
|
223
|
+
if (!nextDispatch) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
inFlightCount += 1;
|
|
227
|
+
void executeDispatch(nextDispatch);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async function executeDispatch(dispatchJob) {
|
|
231
|
+
const { message, resolve, reject } = dispatchJob;
|
|
232
|
+
try {
|
|
233
|
+
const shouldProcess = await frozenDefinition.hooks?.onMessage?.(message);
|
|
234
|
+
if (shouldProcess === false) {
|
|
235
|
+
resolve();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const handler = capabilityMap.get(message.capability);
|
|
239
|
+
if (!handler) {
|
|
240
|
+
frozenDefinition.hooks?.onError?.(new Error(`No capability registered for '${message.capability}'`), message);
|
|
241
|
+
resolve();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const context = {
|
|
245
|
+
runtime,
|
|
246
|
+
log: createContextLogger({
|
|
247
|
+
messageId: message.id,
|
|
248
|
+
capability: message.capability,
|
|
249
|
+
surfaceId: message.surfaceId,
|
|
250
|
+
}),
|
|
251
|
+
};
|
|
252
|
+
try {
|
|
253
|
+
await withTimeout(handler, message, context, constraints.handlerTimeoutMs);
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
frozenDefinition.hooks?.onError?.(error instanceof Error ? error : new Error(String(error)), message);
|
|
257
|
+
}
|
|
258
|
+
resolve();
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
const wrappedError = error instanceof Error ? error : new Error(String(error));
|
|
262
|
+
frozenDefinition.hooks?.onError?.(wrappedError, message);
|
|
263
|
+
reject(wrappedError);
|
|
264
|
+
}
|
|
265
|
+
finally {
|
|
266
|
+
inFlightCount -= 1;
|
|
267
|
+
maybeResolveDrainWaiter();
|
|
268
|
+
runNext();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return runtime;
|
|
272
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { AssistantDefinitionError, OutboundEventError, createAssistant } from './core.js';
|
|
2
|
+
export type { AssistantDefinition, AssistantHooks, AssistantRuntime, CapabilityContext, CapabilityHandler, ContextLogger, InboundMessage, OutboundEvent, RelayInboundAdapter, RelayOutboundAdapter, RuntimeConstraints, RuntimeStatus, } from './types.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { AssistantDefinitionError, OutboundEventError, createAssistant } from './core.js';
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { TraitsProvider } from '@agent-assistant/traits';
|
|
2
|
+
export interface InboundMessage {
|
|
3
|
+
id: string;
|
|
4
|
+
surfaceId: string;
|
|
5
|
+
sessionId?: string;
|
|
6
|
+
userId: string;
|
|
7
|
+
workspaceId?: string;
|
|
8
|
+
text: string;
|
|
9
|
+
raw: unknown;
|
|
10
|
+
receivedAt: string;
|
|
11
|
+
capability: string;
|
|
12
|
+
}
|
|
13
|
+
export interface OutboundEvent {
|
|
14
|
+
surfaceId?: string;
|
|
15
|
+
sessionId?: string;
|
|
16
|
+
text: string;
|
|
17
|
+
format?: unknown;
|
|
18
|
+
}
|
|
19
|
+
export interface RuntimeStatus {
|
|
20
|
+
ready: boolean;
|
|
21
|
+
startedAt: string | null;
|
|
22
|
+
registeredSubsystems: string[];
|
|
23
|
+
registeredCapabilities: string[];
|
|
24
|
+
inFlightHandlers: number;
|
|
25
|
+
}
|
|
26
|
+
export interface RuntimeConstraints {
|
|
27
|
+
handlerTimeoutMs?: number;
|
|
28
|
+
maxConcurrentHandlers?: number;
|
|
29
|
+
}
|
|
30
|
+
export interface RelayInboundAdapter {
|
|
31
|
+
onMessage(handler: (message: InboundMessage) => void): void;
|
|
32
|
+
offMessage(handler: (message: InboundMessage) => void): void;
|
|
33
|
+
}
|
|
34
|
+
export interface RelayOutboundAdapter {
|
|
35
|
+
send(event: OutboundEvent): Promise<void>;
|
|
36
|
+
fanout?(event: OutboundEvent, attachedSurfaceIds: string[]): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
export interface ContextLogger {
|
|
39
|
+
info(message: string, fields?: Record<string, unknown>): void;
|
|
40
|
+
warn(message: string, fields?: Record<string, unknown>): void;
|
|
41
|
+
error(message: string, fields?: Record<string, unknown>): void;
|
|
42
|
+
}
|
|
43
|
+
export interface AssistantRuntime {
|
|
44
|
+
readonly definition: Readonly<AssistantDefinition>;
|
|
45
|
+
emit(event: OutboundEvent): Promise<void>;
|
|
46
|
+
dispatch(message: InboundMessage): Promise<void>;
|
|
47
|
+
register<T>(name: string, subsystem: T): AssistantRuntime;
|
|
48
|
+
get<T>(name: string): T;
|
|
49
|
+
status(): RuntimeStatus;
|
|
50
|
+
start(): Promise<void>;
|
|
51
|
+
stop(): Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
export interface CapabilityContext {
|
|
54
|
+
runtime: AssistantRuntime;
|
|
55
|
+
log: ContextLogger;
|
|
56
|
+
}
|
|
57
|
+
export type CapabilityHandler = (message: InboundMessage, context: CapabilityContext) => Promise<void> | void;
|
|
58
|
+
export interface AssistantHooks {
|
|
59
|
+
onStart?: (runtime: AssistantRuntime) => Promise<void> | void;
|
|
60
|
+
onStop?: (runtime: AssistantRuntime) => Promise<void> | void;
|
|
61
|
+
onMessage?: (message: InboundMessage) => boolean | Promise<boolean>;
|
|
62
|
+
onError?: (error: Error, message: InboundMessage) => void;
|
|
63
|
+
}
|
|
64
|
+
export interface AssistantDefinition {
|
|
65
|
+
id: string;
|
|
66
|
+
name: string;
|
|
67
|
+
description?: string;
|
|
68
|
+
traits?: TraitsProvider;
|
|
69
|
+
capabilities: Record<string, CapabilityHandler>;
|
|
70
|
+
hooks?: AssistantHooks;
|
|
71
|
+
constraints?: RuntimeConstraints;
|
|
72
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|