@dxos/compute 0.10.0 → 0.11.1
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/lib/AgentService.mjs +16 -0
- package/dist/lib/AgentService.mjs.map +1 -0
- package/dist/lib/StorageService.mjs +59 -0
- package/dist/lib/StorageService.mjs.map +1 -0
- package/dist/lib/chunk-OperationHandlerSet.mjs +660 -0
- package/dist/lib/chunk-OperationHandlerSet.mjs.map +1 -0
- package/dist/lib/chunk-definitions.mjs +41 -0
- package/dist/lib/chunk-definitions.mjs.map +1 -0
- package/dist/lib/chunk-rolldown-runtime.mjs +13 -0
- package/dist/lib/index.mjs +1576 -0
- package/dist/lib/index.mjs.map +1 -0
- package/dist/lib/testing.mjs +43 -0
- package/dist/lib/testing.mjs.map +1 -0
- package/dist/types/src/AgentIdentity.d.ts +27 -0
- package/dist/types/src/AgentIdentity.d.ts.map +1 -0
- package/dist/types/src/AgentService.d.ts +10 -2
- package/dist/types/src/AgentService.d.ts.map +1 -1
- package/dist/types/src/Cancellation.d.ts +19 -0
- package/dist/types/src/Cancellation.d.ts.map +1 -0
- package/dist/types/src/Credential.d.ts +41 -0
- package/dist/types/src/Credential.d.ts.map +1 -1
- package/dist/types/src/Operation.d.ts +79 -1
- package/dist/types/src/Operation.d.ts.map +1 -1
- package/dist/types/src/Process.d.ts +15 -3
- package/dist/types/src/Process.d.ts.map +1 -1
- package/dist/types/src/Trace.d.ts +55 -3
- package/dist/types/src/Trace.d.ts.map +1 -1
- package/dist/types/src/Trace.test.d.ts +2 -0
- package/dist/types/src/Trace.test.d.ts.map +1 -0
- package/dist/types/src/errors.d.ts +35 -1
- package/dist/types/src/errors.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +3 -6
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/testing/index.d.ts +1 -0
- package/dist/types/src/testing/index.d.ts.map +1 -1
- package/dist/types/src/testing/operation.d.ts +5 -0
- package/dist/types/src/testing/operation.d.ts.map +1 -0
- package/dist/types/src/{Instructions.d.ts → types/Instructions.d.ts} +14 -1
- package/dist/types/src/types/Instructions.d.ts.map +1 -0
- package/dist/types/src/types/Instructions.test.d.ts +2 -0
- package/dist/types/src/types/Instructions.test.d.ts.map +1 -0
- package/dist/types/src/types/Project.d.ts +53 -0
- package/dist/types/src/types/Project.d.ts.map +1 -0
- package/dist/types/src/types/Project.test.d.ts +2 -0
- package/dist/types/src/types/Project.test.d.ts.map +1 -0
- package/dist/types/src/types/Routine.d.ts +42 -0
- package/dist/types/src/types/Routine.d.ts.map +1 -0
- package/dist/types/src/types/Routine.test.d.ts +2 -0
- package/dist/types/src/types/Routine.test.d.ts.map +1 -0
- package/dist/types/src/types/Script.d.ts.map +1 -0
- package/dist/types/src/{Skill.d.ts → types/Skill.d.ts} +1 -1
- package/dist/types/src/types/Skill.d.ts.map +1 -0
- package/dist/types/src/{Template.d.ts → types/Template.d.ts} +2 -2
- package/dist/types/src/types/Template.d.ts.map +1 -0
- package/dist/types/src/types/Template.test.d.ts.map +1 -0
- package/dist/types/src/types/Trigger.d.ts +249 -0
- package/dist/types/src/types/Trigger.d.ts.map +1 -0
- package/dist/types/src/{TriggerEvent.d.ts → types/TriggerEvent.d.ts} +19 -6
- package/dist/types/src/types/TriggerEvent.d.ts.map +1 -0
- package/dist/types/src/types/index.d.ts +9 -0
- package/dist/types/src/types/index.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +18 -17
- package/src/AgentIdentity.ts +35 -0
- package/src/AgentService.ts +10 -2
- package/src/Cancellation.ts +33 -0
- package/src/Credential.ts +52 -0
- package/src/Operation.ts +92 -2
- package/src/Process.ts +21 -4
- package/src/Trace.test.ts +155 -0
- package/src/Trace.ts +207 -6
- package/src/errors.ts +9 -5
- package/src/index.ts +3 -6
- package/src/testing/index.ts +1 -0
- package/src/testing/operation.ts +21 -0
- package/src/types/Instructions.test.ts +18 -0
- package/src/{Instructions.ts → types/Instructions.ts} +22 -1
- package/src/types/Project.test.ts +52 -0
- package/src/types/Project.ts +99 -0
- package/src/types/Routine.test.ts +18 -0
- package/src/types/Routine.ts +85 -0
- package/src/{Skill.ts → types/Skill.ts} +2 -2
- package/src/{Template.test.ts → types/Template.test.ts} +2 -2
- package/src/{Template.ts → types/Template.ts} +2 -2
- package/src/{Trigger.ts → types/Trigger.ts} +126 -8
- package/src/{TriggerEvent.ts → types/TriggerEvent.ts} +23 -5
- package/src/types/index.ts +12 -0
- package/dist/lib/neutral/AgentService.mjs +0 -12
- package/dist/lib/neutral/AgentService.mjs.map +0 -7
- package/dist/lib/neutral/StorageService.mjs +0 -22
- package/dist/lib/neutral/StorageService.mjs.map +0 -7
- package/dist/lib/neutral/chunk-2ZN64ZS7.mjs +0 -25
- package/dist/lib/neutral/chunk-2ZN64ZS7.mjs.map +0 -7
- package/dist/lib/neutral/chunk-5VAXF6AH.mjs +0 -53
- package/dist/lib/neutral/chunk-5VAXF6AH.mjs.map +0 -7
- package/dist/lib/neutral/chunk-EVROJER7.mjs +0 -285
- package/dist/lib/neutral/chunk-EVROJER7.mjs.map +0 -7
- package/dist/lib/neutral/chunk-J5LGTIGS.mjs +0 -10
- package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +0 -7
- package/dist/lib/neutral/chunk-PF7XP7XP.mjs +0 -62
- package/dist/lib/neutral/chunk-PF7XP7XP.mjs.map +0 -7
- package/dist/lib/neutral/chunk-SJ37RWHU.mjs +0 -21
- package/dist/lib/neutral/chunk-SJ37RWHU.mjs.map +0 -7
- package/dist/lib/neutral/chunk-VSAZPWSJ.mjs +0 -25
- package/dist/lib/neutral/chunk-VSAZPWSJ.mjs.map +0 -7
- package/dist/lib/neutral/chunk-WEO4FCQY.mjs +0 -17
- package/dist/lib/neutral/chunk-WEO4FCQY.mjs.map +0 -7
- package/dist/lib/neutral/chunk-Z3Y7FQGN.mjs +0 -174
- package/dist/lib/neutral/chunk-Z3Y7FQGN.mjs.map +0 -7
- package/dist/lib/neutral/fib-5XIYECL5.mjs +0 -10
- package/dist/lib/neutral/fib-5XIYECL5.mjs.map +0 -7
- package/dist/lib/neutral/index.mjs +0 -1232
- package/dist/lib/neutral/index.mjs.map +0 -7
- package/dist/lib/neutral/meta.json +0 -1
- package/dist/lib/neutral/reply-GCBRCUVM.mjs +0 -10
- package/dist/lib/neutral/reply-GCBRCUVM.mjs.map +0 -7
- package/dist/lib/neutral/sleep-XR6CL6W3.mjs +0 -10
- package/dist/lib/neutral/sleep-XR6CL6W3.mjs.map +0 -7
- package/dist/lib/neutral/testing/index.mjs +0 -32
- package/dist/lib/neutral/testing/index.mjs.map +0 -7
- package/dist/types/src/Instructions.d.ts.map +0 -1
- package/dist/types/src/Script.d.ts.map +0 -1
- package/dist/types/src/Skill.d.ts.map +0 -1
- package/dist/types/src/Template.d.ts.map +0 -1
- package/dist/types/src/Template.test.d.ts.map +0 -1
- package/dist/types/src/Trigger.d.ts +0 -157
- package/dist/types/src/Trigger.d.ts.map +0 -1
- package/dist/types/src/TriggerEvent.d.ts.map +0 -1
- /package/dist/types/src/{Script.d.ts → types/Script.d.ts} +0 -0
- /package/dist/types/src/{Template.test.d.ts → types/Template.test.d.ts} +0 -0
- /package/src/{Script.ts → types/Script.ts} +0 -0
package/src/Trace.ts
CHANGED
|
@@ -10,9 +10,10 @@ import * as Layer from 'effect/Layer';
|
|
|
10
10
|
import * as Schema from 'effect/Schema';
|
|
11
11
|
|
|
12
12
|
import { Annotation, DXN, Obj, Ref, Type } from '@dxos/echo';
|
|
13
|
+
import { EID } from '@dxos/keys';
|
|
13
14
|
import { log } from '@dxos/log';
|
|
14
15
|
|
|
15
|
-
import * as Trigger from './Trigger';
|
|
16
|
+
import * as Trigger from './types/Trigger';
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Writes ephemeral or persistent events to the trace.
|
|
@@ -70,6 +71,7 @@ export const Event = Schema.Struct({
|
|
|
70
71
|
data: Schema.Unknown, // Type-specific payload;
|
|
71
72
|
});
|
|
72
73
|
export type Event = Schema.Schema.Type<typeof Event>;
|
|
74
|
+
|
|
73
75
|
/**
|
|
74
76
|
* Checks if an event is of a given type.
|
|
75
77
|
*/
|
|
@@ -132,6 +134,7 @@ export const Meta = Schema.Struct({
|
|
|
132
134
|
runtimeName: Schema.optional(RuntimeName),
|
|
133
135
|
});
|
|
134
136
|
export type Meta = Schema.Schema.Type<typeof Meta>;
|
|
137
|
+
|
|
135
138
|
/**
|
|
136
139
|
* Checks if a runtime is an edge runtime.
|
|
137
140
|
*/
|
|
@@ -145,17 +148,18 @@ export const isEdgeRuntime = (name: RuntimeName): boolean =>
|
|
|
145
148
|
*/
|
|
146
149
|
export const MessageData = Schema.Struct({
|
|
147
150
|
meta: Meta,
|
|
148
|
-
|
|
149
151
|
isEphemeral: Schema.Boolean,
|
|
150
152
|
events: Schema.Array(Event),
|
|
151
153
|
});
|
|
152
154
|
export type MessageData = Schema.Schema.Type<typeof MessageData>;
|
|
155
|
+
|
|
153
156
|
export class Message extends Type.makeObject<Message>(DXN.make('org.dxos.type.traceMessage', '0.1.0'))(
|
|
154
157
|
MessageData.pipe(
|
|
155
158
|
Annotation.IconAnnotation.set({ icon: 'ph--note--regular', hue: 'rose' }),
|
|
156
159
|
Annotation.HiddenAnnotation.set(true),
|
|
157
160
|
),
|
|
158
161
|
) {}
|
|
162
|
+
|
|
159
163
|
/**
|
|
160
164
|
* Flattened representation of a signle event in a trace message.
|
|
161
165
|
* Events are stored in batched messages for efficiency, but flat representation is more convenient for consumption.
|
|
@@ -176,6 +180,185 @@ export const flatten = (message: Message): FlatEvent[] => {
|
|
|
176
180
|
}));
|
|
177
181
|
};
|
|
178
182
|
|
|
183
|
+
//
|
|
184
|
+
// Swarm broadcast (DX-1125).
|
|
185
|
+
//
|
|
186
|
+
// Ephemeral trace messages produced on a remote runtime are broadcast over the space swarm and
|
|
187
|
+
// projected into the client's progress UI. The publisher tags each message with a flat key:value list
|
|
188
|
+
// derived from its meta; subscribers register a coarse tag (OR match at the swarm) and re-apply the
|
|
189
|
+
// exact filter (AND) client-side.
|
|
190
|
+
//
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* `google.protobuf.Any` type URL for a trace message broadcast over the swarm.
|
|
194
|
+
*/
|
|
195
|
+
export const TRACE_MESSAGE_TYPE_URL = 'dxos.compute.TraceMessage';
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Declarative, serializable filter over trace messages. All present fields are ANDed. `type` matches
|
|
199
|
+
* against event types within the message; the rest match against {@link Meta} fields. Refs
|
|
200
|
+
* (`conversation`, `trigger`) are matched by their DXN string.
|
|
201
|
+
*/
|
|
202
|
+
export interface Filter {
|
|
203
|
+
readonly type?: string;
|
|
204
|
+
readonly pid?: string;
|
|
205
|
+
readonly parentPid?: string;
|
|
206
|
+
readonly conversation?: string;
|
|
207
|
+
readonly trigger?: string;
|
|
208
|
+
readonly space?: string;
|
|
209
|
+
readonly runtimeName?: string;
|
|
210
|
+
readonly toolCallId?: string;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Derive the flat broadcast tag list for a message (DX-1125). The publisher emits all applicable tags;
|
|
215
|
+
* subscribers match a subset (logical OR).
|
|
216
|
+
*/
|
|
217
|
+
export const messageToTags = (message: Pick<MessageData, 'meta' | 'events'>): string[] => {
|
|
218
|
+
const { meta } = message;
|
|
219
|
+
const tags: string[] = [];
|
|
220
|
+
for (const event of message.events) {
|
|
221
|
+
tags.push(`type:${event.type}`);
|
|
222
|
+
}
|
|
223
|
+
if (meta.pid) {
|
|
224
|
+
tags.push(`pid:${meta.pid}`);
|
|
225
|
+
}
|
|
226
|
+
if (meta.parentPid) {
|
|
227
|
+
tags.push(`parentPid:${meta.parentPid}`);
|
|
228
|
+
}
|
|
229
|
+
if (meta.conversation) {
|
|
230
|
+
tags.push(`conversation:${meta.conversation.uri.toString()}`);
|
|
231
|
+
}
|
|
232
|
+
if (meta.trigger) {
|
|
233
|
+
tags.push(`trigger:${meta.trigger.uri.toString()}`);
|
|
234
|
+
}
|
|
235
|
+
if (meta.space) {
|
|
236
|
+
tags.push(`space:${meta.space}`);
|
|
237
|
+
}
|
|
238
|
+
if (meta.runtimeName) {
|
|
239
|
+
tags.push(`runtime:${meta.runtimeName}`);
|
|
240
|
+
}
|
|
241
|
+
if (meta.toolCallId) {
|
|
242
|
+
tags.push(`toolCall:${meta.toolCallId}`);
|
|
243
|
+
}
|
|
244
|
+
return tags;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Pick the coarse subscription tag for a filter (DX-1125). The swarm cuts traffic to a superset of the
|
|
249
|
+
* filter using its most selective present dimension; the client re-applies the exact filter.
|
|
250
|
+
*/
|
|
251
|
+
export const subscriptionTagForFilter = (filter: Filter): string | undefined => {
|
|
252
|
+
if (filter.conversation !== undefined) {
|
|
253
|
+
return `conversation:${filter.conversation}`;
|
|
254
|
+
}
|
|
255
|
+
if (filter.trigger !== undefined) {
|
|
256
|
+
return `trigger:${filter.trigger}`;
|
|
257
|
+
}
|
|
258
|
+
if (filter.parentPid !== undefined) {
|
|
259
|
+
return `parentPid:${filter.parentPid}`;
|
|
260
|
+
}
|
|
261
|
+
if (filter.pid !== undefined) {
|
|
262
|
+
return `pid:${filter.pid}`;
|
|
263
|
+
}
|
|
264
|
+
if (filter.space !== undefined) {
|
|
265
|
+
return `space:${filter.space}`;
|
|
266
|
+
}
|
|
267
|
+
if (filter.type !== undefined) {
|
|
268
|
+
return `type:${filter.type}`;
|
|
269
|
+
}
|
|
270
|
+
return undefined;
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Exact (AND) filter match applied client-side after the coarse swarm subscription.
|
|
275
|
+
*/
|
|
276
|
+
export const matchesFilter = (message: Pick<MessageData, 'meta' | 'events'>, filter: Filter): boolean => {
|
|
277
|
+
const { meta } = message;
|
|
278
|
+
if (filter.type !== undefined && !message.events.some((event) => event.type === filter.type)) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
if (filter.pid !== undefined && meta.pid !== filter.pid) {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
if (filter.parentPid !== undefined && meta.parentPid !== filter.parentPid) {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
if (filter.conversation !== undefined && meta.conversation?.uri.toString() !== filter.conversation) {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
if (filter.trigger !== undefined && meta.trigger?.uri.toString() !== filter.trigger) {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
if (filter.space !== undefined && meta.space !== filter.space) {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
if (filter.runtimeName !== undefined && meta.runtimeName !== filter.runtimeName) {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
if (filter.toolCallId !== undefined && meta.toolCallId !== filter.toolCallId) {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
return true;
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Wire-safe subset of {@link Meta} for broadcast. Ref fields (`conversation`, `trigger`) are not
|
|
307
|
+
* carried in the payload — they are represented only as envelope tags — so the decoded message never
|
|
308
|
+
* attempts ref resolution on the consumer.
|
|
309
|
+
*/
|
|
310
|
+
const encodeMetaForWire = (meta: Meta): Meta => ({
|
|
311
|
+
pid: meta.pid,
|
|
312
|
+
parentPid: meta.parentPid,
|
|
313
|
+
processName: meta.processName,
|
|
314
|
+
space: meta.space,
|
|
315
|
+
toolCallId: meta.toolCallId,
|
|
316
|
+
runtimeName: meta.runtimeName,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Encode a trace message for the broadcast envelope's `google.protobuf.Any.value` (DX-1125).
|
|
321
|
+
* The wire format is UTF-8 JSON of the message data (ref fields dropped — see {@link encodeMetaForWire}).
|
|
322
|
+
*/
|
|
323
|
+
export const encodeTraceMessage = (message: Pick<MessageData, 'meta' | 'isEphemeral' | 'events'>): Uint8Array =>
|
|
324
|
+
new TextEncoder().encode(
|
|
325
|
+
JSON.stringify({
|
|
326
|
+
meta: encodeMetaForWire(message.meta),
|
|
327
|
+
isEphemeral: message.isEphemeral,
|
|
328
|
+
events: message.events,
|
|
329
|
+
}),
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Decode a broadcast trace message back into a {@link Message} object (DX-1125). The wire payload
|
|
334
|
+
* drops both ref meta fields (see {@link encodeMetaForWire}), so `tags` — the broadcast envelope's
|
|
335
|
+
* tag list — is required to restore them: `trigger` addresses work for cancellation, and
|
|
336
|
+
* `conversation` is what {@link matchesFilter} compares, so a subscription filtered by it matches
|
|
337
|
+
* nothing without this. Restored refs are address-only (`.uri`); they are never resolved.
|
|
338
|
+
*/
|
|
339
|
+
export const decodeTraceMessage = (bytes: Uint8Array, tags?: readonly string[]): Message => {
|
|
340
|
+
const parsed = JSON.parse(new TextDecoder().decode(bytes)) as Partial<MessageData>;
|
|
341
|
+
const meta = parsed.meta ?? {};
|
|
342
|
+
const conversation = meta.conversation ?? refFromTags<Obj.Unknown>(tags, 'conversation:');
|
|
343
|
+
const trigger = meta.trigger ?? refFromTags<Trigger.Trigger>(tags, 'trigger:');
|
|
344
|
+
return Obj.make(Message, {
|
|
345
|
+
meta: {
|
|
346
|
+
...meta,
|
|
347
|
+
...(conversation ? { conversation } : {}),
|
|
348
|
+
...(trigger ? { trigger } : {}),
|
|
349
|
+
},
|
|
350
|
+
isEphemeral: parsed.isEphemeral ?? true,
|
|
351
|
+
events: parsed.events ?? [],
|
|
352
|
+
});
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
/** The ref carried on a broadcast envelope's `<prefix><uri>` tag, when present and parseable. */
|
|
356
|
+
const refFromTags = <T>(tags: readonly string[] | undefined, prefix: string): Ref.Ref<T> | undefined => {
|
|
357
|
+
const uri = tags?.find((tag) => tag.startsWith(prefix))?.slice(prefix.length);
|
|
358
|
+
const eid = uri !== undefined ? EID.tryParse(uri) : undefined;
|
|
359
|
+
return eid !== undefined ? Ref.fromURI(eid) : undefined;
|
|
360
|
+
};
|
|
361
|
+
|
|
179
362
|
/**
|
|
180
363
|
* Sink for complete trace messages.
|
|
181
364
|
*/
|
|
@@ -352,8 +535,24 @@ export const OperationOutput = EventType('operation.output', {
|
|
|
352
535
|
*/
|
|
353
536
|
export const StatusUpdate = EventType('status.update', {
|
|
354
537
|
schema: Schema.Struct({
|
|
355
|
-
/** Human-readable status message
|
|
356
|
-
message: Schema.String,
|
|
538
|
+
/** Human-readable status message.. */
|
|
539
|
+
message: Schema.optional(Schema.String),
|
|
540
|
+
|
|
541
|
+
progress: Schema.optional(
|
|
542
|
+
Schema.Struct({
|
|
543
|
+
/** Progress key. See {@link ProgressRegistry.key}. */
|
|
544
|
+
key: Schema.String,
|
|
545
|
+
|
|
546
|
+
/* Progress current item index. */
|
|
547
|
+
current: Schema.optional(Schema.Number),
|
|
548
|
+
|
|
549
|
+
/** Progress total item count. */
|
|
550
|
+
total: Schema.optional(Schema.Number),
|
|
551
|
+
|
|
552
|
+
/** Progress estimate of remaining time (ms). */
|
|
553
|
+
estimate: Schema.optional(Schema.Number),
|
|
554
|
+
}),
|
|
555
|
+
),
|
|
357
556
|
}),
|
|
358
557
|
isEphemeral: true,
|
|
359
558
|
});
|
|
@@ -361,5 +560,7 @@ export const StatusUpdate = EventType('status.update', {
|
|
|
361
560
|
/**
|
|
362
561
|
* Emit the current human-readable execution status to the trace.
|
|
363
562
|
*/
|
|
364
|
-
export const emitStatus: (
|
|
365
|
-
|
|
563
|
+
export const emitStatus: (
|
|
564
|
+
messageOrData: string | PayloadType<typeof StatusUpdate>,
|
|
565
|
+
) => Effect.Effect<void, never, TraceService> = (message) =>
|
|
566
|
+
write(StatusUpdate, typeof message === 'string' ? { message } : message);
|
package/src/errors.ts
CHANGED
|
@@ -4,16 +4,20 @@
|
|
|
4
4
|
|
|
5
5
|
import { BaseError, type BaseErrorOptions } from '@dxos/errors';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Raised when a function wants to yield to the scheduler to not exceed platform limits.
|
|
9
|
+
* The scheduler will re-run the function after the yield.
|
|
10
|
+
* It is assumed that the function can be retried with the same input.
|
|
11
|
+
* The function might have produced side-effects on the first run.
|
|
12
|
+
*/
|
|
13
|
+
export class RunAgainError extends BaseError.extend('RunAgainError', 'Run again') {}
|
|
14
|
+
|
|
7
15
|
// Errors from @dxos/operation.
|
|
8
16
|
|
|
9
17
|
export class InvokerNotInitializedError extends BaseError.extend(
|
|
10
18
|
'InvokerNotInitializedError',
|
|
11
19
|
'Invoker not initialized',
|
|
12
|
-
) {
|
|
13
|
-
constructor() {
|
|
14
|
-
super();
|
|
15
|
-
}
|
|
16
|
-
}
|
|
20
|
+
) {}
|
|
17
21
|
|
|
18
22
|
export class NoHandlerError extends BaseError.extend('NoHandlerError', 'No handler found for operation. ') {
|
|
19
23
|
constructor(operationKey: string) {
|
package/src/index.ts
CHANGED
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
export * from './errors';
|
|
6
|
+
export * from './types';
|
|
6
7
|
|
|
8
|
+
export * as AgentIdentity from './AgentIdentity';
|
|
7
9
|
export * as AgentService from './AgentService';
|
|
8
|
-
export * as
|
|
10
|
+
export * as Cancellation from './Cancellation';
|
|
9
11
|
export * as Credential from './Credential';
|
|
10
12
|
export * as Header from './Header';
|
|
11
|
-
export * as Instructions from './Instructions';
|
|
12
13
|
export * as LayerSpec from './LayerSpec';
|
|
13
14
|
export * as McpServer from './McpServer';
|
|
14
15
|
export * as Operation from './Operation';
|
|
@@ -16,10 +17,6 @@ export * as OperationHandlerSet from './OperationHandlerSet';
|
|
|
16
17
|
export * as OperationRegistry from './OperationRegistry';
|
|
17
18
|
export * as Process from './Process';
|
|
18
19
|
export * as Runnable from './Runnable';
|
|
19
|
-
export * as Script from './Script';
|
|
20
20
|
export * as ServiceResolver from './ServiceResolver';
|
|
21
21
|
export * as StorageService from './StorageService';
|
|
22
|
-
export * as Template from './Template';
|
|
23
22
|
export * as Trace from './Trace';
|
|
24
|
-
export * as Trigger from './Trigger';
|
|
25
|
-
export * as TriggerEvent from './TriggerEvent';
|
package/src/testing/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ export { Fibonacci, Reply, Sleep } from './definitions';
|
|
|
8
8
|
export { default as FibonacciHandler } from './fib';
|
|
9
9
|
export { default as ReplyHandler } from './reply';
|
|
10
10
|
export { default as SleepHandler } from './sleep';
|
|
11
|
+
export * from './operation';
|
|
11
12
|
|
|
12
13
|
export const ExampleHandlers = OperationHandlerSet.lazy(
|
|
13
14
|
() => import('./fib'),
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Effect from 'effect/Effect';
|
|
6
|
+
import * as Layer from 'effect/Layer';
|
|
7
|
+
|
|
8
|
+
import * as Operation from '../Operation';
|
|
9
|
+
|
|
10
|
+
const operationServiceStub: Operation.OperationService = {
|
|
11
|
+
invoke: () => Effect.die('operationServiceLayerNoop: invoke is not implemented.'),
|
|
12
|
+
schedule: () => Effect.die('operationServiceLayerNoop: schedule is not implemented.'),
|
|
13
|
+
invokePromise: () =>
|
|
14
|
+
Promise.resolve({ error: new Error('operationServiceLayerNoop: invokePromise is not implemented.') }),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** Noop `Operation.Service` for tests that must satisfy the type without invoking it. */
|
|
18
|
+
export const operationServiceLayerNoop: Layer.Layer<Operation.Service> = Layer.succeed(
|
|
19
|
+
Operation.Service,
|
|
20
|
+
operationServiceStub,
|
|
21
|
+
);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import * as Instructions from './Instructions';
|
|
8
|
+
|
|
9
|
+
describe('Instructions', () => {
|
|
10
|
+
test('commands round-trip through make', ({ expect }) => {
|
|
11
|
+
const instructions = Instructions.make({
|
|
12
|
+
name: 'test',
|
|
13
|
+
commands: [{ sentinel: '$track', description: 'Track a follow-up', prompt: 'Append the item to TASKS.md.' }],
|
|
14
|
+
});
|
|
15
|
+
expect(instructions.commands?.length).toBe(1);
|
|
16
|
+
expect(instructions.commands?.[0].sentinel).toBe('$track');
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -11,6 +11,14 @@ import { Text } from '@dxos/schema';
|
|
|
11
11
|
|
|
12
12
|
import * as Skill from './Skill';
|
|
13
13
|
|
|
14
|
+
/** A sentinel command the model recognizes in chat (e.g. `$track <text>`). */
|
|
15
|
+
export const Command = Schema.Struct({
|
|
16
|
+
sentinel: Schema.String.annotations({ description: 'Token that invokes the command (e.g. "$track").' }),
|
|
17
|
+
description: Schema.optional(Schema.String),
|
|
18
|
+
prompt: Schema.String.annotations({ description: 'What the model should do when the sentinel appears.' }),
|
|
19
|
+
});
|
|
20
|
+
export type Command = Schema.Schema.Type<typeof Command>;
|
|
21
|
+
|
|
14
22
|
/**
|
|
15
23
|
* Prompt-based operation.
|
|
16
24
|
* May reference skills and additional context.
|
|
@@ -37,6 +45,8 @@ export class Instructions extends Type.makeObject<Instructions>(DXN.make('org.dx
|
|
|
37
45
|
* executes a routine through the agent prompt, not only triggered automations.
|
|
38
46
|
*/
|
|
39
47
|
objects: Schema.Array(Ref.Ref(Obj.Unknown)).pipe(Schema.annotations({ title: 'Objects' }), Schema.optional),
|
|
48
|
+
/** Sentinel commands available to chat sessions running with these instructions. */
|
|
49
|
+
commands: Schema.Array(Command).pipe(Schema.annotations({ title: 'Commands' }), Schema.optional),
|
|
40
50
|
}).pipe(
|
|
41
51
|
Annotation.LabelAnnotation.set(['name']),
|
|
42
52
|
Annotation.IconAnnotation.set({ icon: 'ph--scroll--regular', hue: 'sky' }),
|
|
@@ -52,10 +62,20 @@ export type MakeProps = {
|
|
|
52
62
|
text?: string;
|
|
53
63
|
skills?: Ref.Ref<Skill.Skill>[];
|
|
54
64
|
objects?: Ref.Ref<Obj.Unknown>[];
|
|
65
|
+
commands?: Command[];
|
|
55
66
|
};
|
|
56
67
|
|
|
57
68
|
/** Creates an Instructions object with an owned Markdown `text` body (parented so it cascades and deep-clones). */
|
|
58
|
-
export const make = ({
|
|
69
|
+
export const make = ({
|
|
70
|
+
name,
|
|
71
|
+
description,
|
|
72
|
+
input,
|
|
73
|
+
output,
|
|
74
|
+
text,
|
|
75
|
+
skills = [],
|
|
76
|
+
objects,
|
|
77
|
+
commands,
|
|
78
|
+
}: MakeProps): Instructions => {
|
|
59
79
|
const body = Text.make({ content: text ?? '' });
|
|
60
80
|
const instructions = Obj.make(Instructions, {
|
|
61
81
|
name,
|
|
@@ -65,6 +85,7 @@ export const make = ({ name, description, input, output, text, skills = [], obje
|
|
|
65
85
|
text: Ref.make(body),
|
|
66
86
|
skills,
|
|
67
87
|
objects,
|
|
88
|
+
commands,
|
|
68
89
|
});
|
|
69
90
|
// The body is owned by the instructions: it cascade-deletes with it and is cloned alongside it under
|
|
70
91
|
// `Obj.clone(..., { deep: 'parent' })`.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Schema from 'effect/Schema';
|
|
6
|
+
import { describe, test } from 'vitest';
|
|
7
|
+
|
|
8
|
+
import { DXN, Obj, Ref, Type, URI } from '@dxos/echo';
|
|
9
|
+
|
|
10
|
+
import * as Instructions from './Instructions';
|
|
11
|
+
import * as Project from './Project';
|
|
12
|
+
|
|
13
|
+
// Stand-in `Obj.Unknown` type for context objects, mirroring the ad-hoc test types used in
|
|
14
|
+
// `AiContext.test.ts` — no database needed since `Ref.make` inlines the target.
|
|
15
|
+
const TestObject = Type.makeObject(DXN.make('org.dxos.type.testObject', '0.1.0'))(Schema.Struct({}));
|
|
16
|
+
|
|
17
|
+
describe('Project', () => {
|
|
18
|
+
test('typename, version, and defaults', ({ expect }) => {
|
|
19
|
+
expect(Type.getTypename(Project.Project)).toBe('org.dxos.type.project');
|
|
20
|
+
expect(Type.getVersion(Project.Project)).toBe('0.3.0');
|
|
21
|
+
const project = Project.make({ name: 'test' });
|
|
22
|
+
expect(Obj.instanceOf(Project.Project, project)).toBe(true);
|
|
23
|
+
expect(project.routines).toEqual([]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('contextBindings exposes skills and objects, not the instructions object itself', ({ expect }) => {
|
|
27
|
+
const skillRef = Ref.fromURI(URI.make('dxn:echo:@:skill-1'));
|
|
28
|
+
const doc = Obj.make(TestObject, {});
|
|
29
|
+
const instructions = Instructions.make({ text: 'Test', skills: [skillRef], objects: [Ref.make(doc)] });
|
|
30
|
+
const project = Project.make({ name: 'test' });
|
|
31
|
+
Obj.setParent(instructions, project);
|
|
32
|
+
Obj.update(project, (project) => {
|
|
33
|
+
project.instructions = Ref.make(instructions);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const bindings = Project.contextBindings(project);
|
|
37
|
+
expect(bindings.skills.map((ref) => ref.uri)).toEqual([skillRef.uri]);
|
|
38
|
+
// Instructions text reaches the prompt via Chat.instructions, not a binding.
|
|
39
|
+
expect(bindings.objects.map((ref) => ref.uri)).toEqual([Ref.make(doc).uri]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('contextBindings returns empty bindings when instructions is unresolved', ({ expect }) => {
|
|
43
|
+
const project = Project.make({ name: 'test' });
|
|
44
|
+
Obj.update(project, (project) => {
|
|
45
|
+
project.instructions = Ref.fromURI(URI.make('dxn:echo:@:instructions-1'));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const bindings = Project.contextBindings(project);
|
|
49
|
+
expect(bindings.skills).toEqual([]);
|
|
50
|
+
expect(bindings.objects).toEqual([]);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
// @import-as-namespace
|
|
6
|
+
|
|
7
|
+
import * as Schema from 'effect/Schema';
|
|
8
|
+
|
|
9
|
+
import { Annotation, Collection, DXN, Obj, Ref, Type } from '@dxos/echo';
|
|
10
|
+
import { FormInlineAnnotation, LabelAnnotation } from '@dxos/echo/Annotation';
|
|
11
|
+
import { Outline, TaskSet } from '@dxos/types';
|
|
12
|
+
|
|
13
|
+
import * as Instructions from './Instructions';
|
|
14
|
+
import * as Routine from './Routine';
|
|
15
|
+
import type * as Skill from './Skill';
|
|
16
|
+
|
|
17
|
+
/** Lightweight inline goal: per-item status and addressability without document ceremony. */
|
|
18
|
+
export const Goal = Schema.Struct({
|
|
19
|
+
id: Schema.String,
|
|
20
|
+
text: Schema.String,
|
|
21
|
+
status: Schema.optional(Schema.Literal('open', 'met', 'dropped')),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export type Goal = Schema.Schema.Type<typeof Goal>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A user-facing container for interactive, long-running work: instructions (skills + commands),
|
|
28
|
+
* routines, artifacts, and AI chat sessions in project context. Successor to `Topic`.
|
|
29
|
+
* Chats and agents attach via relations/queries (assistant-toolkit depends on compute, so no typed refs here).
|
|
30
|
+
*/
|
|
31
|
+
export class Project extends Type.makeObject<Project>(DXN.make('org.dxos.type.project', '0.3.0'))(
|
|
32
|
+
Schema.Struct({
|
|
33
|
+
name: Schema.optional(Schema.String),
|
|
34
|
+
description: Schema.optional(Schema.String),
|
|
35
|
+
|
|
36
|
+
/** Owned agent instructions (created + parented at the plugin layer). */
|
|
37
|
+
instructions: Schema.optional(Ref.Ref(Instructions.Instructions).pipe(FormInlineAnnotation.set(true))),
|
|
38
|
+
|
|
39
|
+
/** Routines created within the scope of this project. */
|
|
40
|
+
routines: Schema.Array(Ref.Ref(Routine.Routine)),
|
|
41
|
+
|
|
42
|
+
/** Owned collection of artifacts (documents, outliners, tables, ...) managed by the project. */
|
|
43
|
+
artifacts: Schema.optional(Ref.Ref(Collection.Collection)),
|
|
44
|
+
|
|
45
|
+
/** What done means for this project. */
|
|
46
|
+
goals: Schema.optional(Schema.Array(Goal)),
|
|
47
|
+
|
|
48
|
+
/** Ad hoc markdown checklist — the scratch surface; project chats write into it. */
|
|
49
|
+
outline: Schema.optional(Ref.Ref(Outline.Outline)),
|
|
50
|
+
|
|
51
|
+
/** Owned (or adopted synced) task container; membership is the ECHO parent edge. */
|
|
52
|
+
taskSet: Schema.optional(Ref.Ref(TaskSet.TaskSet)),
|
|
53
|
+
}).pipe(
|
|
54
|
+
Schema.annotations({ title: 'Project' }),
|
|
55
|
+
LabelAnnotation.set(['name']),
|
|
56
|
+
Annotation.IconAnnotation.set({ icon: 'ph--stack--regular', hue: 'amber' }),
|
|
57
|
+
),
|
|
58
|
+
) {}
|
|
59
|
+
|
|
60
|
+
/** Factory wrapper around `Obj.make` for {@link Project}. */
|
|
61
|
+
export const make = (
|
|
62
|
+
props: Omit<Partial<Obj.MakeProps<typeof Project>>, 'routines'> & {
|
|
63
|
+
routines?: ReadonlyArray<Ref.Ref<Routine.Routine>>;
|
|
64
|
+
} = {},
|
|
65
|
+
): Project => Obj.make(Project, { ...props, routines: props.routines ?? [] });
|
|
66
|
+
|
|
67
|
+
/** Bindings a chat session should receive when running in a project's context. */
|
|
68
|
+
export type ContextBindings = {
|
|
69
|
+
skills: Ref.Ref<Skill.Skill>[];
|
|
70
|
+
objects: Ref.Ref<Obj.Unknown>[];
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Bindings a chat session should receive when running in this project's context: the instructions'
|
|
75
|
+
* skills and context objects. The instructions object itself is NOT bound — its text/commands reach
|
|
76
|
+
* the system prompt through the chat's `instructions` ref, and a bound Instructions object would
|
|
77
|
+
* render as an inert context stub. Requires the instructions ref to be resolved (`.target`);
|
|
78
|
+
* unresolved refs contribute nothing.
|
|
79
|
+
*/
|
|
80
|
+
export const contextBindings = (project: Project): ContextBindings => {
|
|
81
|
+
const instructionsRef = project.instructions;
|
|
82
|
+
// `.target` throws when the ref has neither an inlined target nor a resolver attached
|
|
83
|
+
// (e.g. an unresolved ref outside a database context); guard with `isAvailable` first.
|
|
84
|
+
if (!instructionsRef?.isAvailable) {
|
|
85
|
+
return { skills: [], objects: [] };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const instructions = instructionsRef.target;
|
|
89
|
+
if (!instructions) {
|
|
90
|
+
return { skills: [], objects: [] };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Refs are kept as-is (never re-wrapped via `Ref.make`): bindings persist to the conversation feed,
|
|
94
|
+
// and re-wrapping would drop the registry DXN of skills that have no space-DB identity.
|
|
95
|
+
return {
|
|
96
|
+
skills: [...instructions.skills],
|
|
97
|
+
objects: [...(instructions.objects ?? [])],
|
|
98
|
+
};
|
|
99
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { Obj, Type } from '@dxos/echo';
|
|
8
|
+
|
|
9
|
+
import * as Routine from './Routine';
|
|
10
|
+
|
|
11
|
+
describe('Routine', () => {
|
|
12
|
+
test('typename and factory', ({ expect }) => {
|
|
13
|
+
expect(Type.getTypename(Routine.Routine)).toBe('org.dxos.type.routine');
|
|
14
|
+
const routine = Routine.make({ name: 'test' });
|
|
15
|
+
expect(Obj.instanceOf(Routine.Routine, routine)).toBe(true);
|
|
16
|
+
expect(routine.triggers).toEqual([]);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
// @import-as-namespace
|
|
6
|
+
|
|
7
|
+
import * as Schema from 'effect/Schema';
|
|
8
|
+
|
|
9
|
+
import { Annotation, DXN, Obj, Ref, Type } from '@dxos/echo';
|
|
10
|
+
import { LabelAnnotation } from '@dxos/echo/internal';
|
|
11
|
+
|
|
12
|
+
import type * as Operation from '../Operation';
|
|
13
|
+
import * as Runnable from '../Runnable';
|
|
14
|
+
import * as Instructions from './Instructions';
|
|
15
|
+
import * as Trigger from './Trigger';
|
|
16
|
+
|
|
17
|
+
const Kinds = ['runnable', 'instructions'] as const;
|
|
18
|
+
export const Kind = Schema.Literal(...Kinds);
|
|
19
|
+
export type Kind = (typeof Kinds)[number];
|
|
20
|
+
|
|
21
|
+
const RunnableSpec = Schema.Struct({
|
|
22
|
+
kind: Schema.Literal('runnable'),
|
|
23
|
+
runnable: Ref.Ref(Runnable.Runnable),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const InstructionsSpec = Schema.Struct({
|
|
27
|
+
kind: Schema.Literal('instructions'),
|
|
28
|
+
instructions: Ref.Ref(Instructions.Instructions),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const RoutineSpec = Schema.Union(RunnableSpec, InstructionsSpec);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* User-facing routine: a thin aggregate of an action (`runnable`) and the triggers that fire it.
|
|
35
|
+
* App-level only — EDGE stays unaware of it (triggers point directly at the runnable).
|
|
36
|
+
*/
|
|
37
|
+
export class Routine extends Type.makeObject<Routine>(DXN.make('org.dxos.type.routine', '0.2.0'))(
|
|
38
|
+
Schema.Struct({
|
|
39
|
+
name: Schema.String.pipe(Schema.optional),
|
|
40
|
+
description: Schema.String.pipe(Schema.optional),
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The action to run: either an Operation (`spec.runnable`, bound directly) or the routine's own owned
|
|
44
|
+
* Instructions (`spec.instructions`). For an Operation action the trigger's `runnable` points at this
|
|
45
|
+
* Operation. For an Instructions action `spec.instructions` is the owned Instructions object (the operation
|
|
46
|
+
* is implicitly the static RunInstructions, so no separate operation ref is stored), and the trigger's
|
|
47
|
+
* `runnable` is RunInstructions with this instructions bound as its input.
|
|
48
|
+
*/
|
|
49
|
+
// TODO(burdon): Change to Array? Or handle that case with a ComputeGraph runnable.
|
|
50
|
+
spec: RoutineSpec.pipe(Schema.optional),
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Explicit membership, bi-directional with `trigger.runnable → runnable`. Required (not derived by query)
|
|
54
|
+
* because the runnable may be a shared registry operation referenced by multiple automations, which would
|
|
55
|
+
* conflate triggers. MVP enforces length <= 1.
|
|
56
|
+
*/
|
|
57
|
+
triggers: Schema.Array(Ref.Ref(Trigger.Trigger)),
|
|
58
|
+
}).pipe(
|
|
59
|
+
LabelAnnotation.set(['name']),
|
|
60
|
+
Annotation.IconAnnotation.set({ icon: 'ph--lightning--regular', hue: 'amber' }),
|
|
61
|
+
),
|
|
62
|
+
) {}
|
|
63
|
+
|
|
64
|
+
/** Returns true when value is a Routine object. */
|
|
65
|
+
export const instanceOf = (value: unknown): value is Routine => Obj.instanceOf(Routine, value);
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The owned Instructions ref of an instructions-action routine, or undefined for an operation action.
|
|
69
|
+
* Classification is explicit in `spec.kind`, so this needs no `.target` dereference; callers resolve the ref
|
|
70
|
+
* reactively (`useObject`) or asynchronously (`Database.load`) as appropriate.
|
|
71
|
+
*/
|
|
72
|
+
export const instructionsRef = (routine: Pick<Routine, 'spec'>): Ref.Ref<Instructions.Instructions> | undefined =>
|
|
73
|
+
routine.spec?.kind === 'instructions' ? routine.spec.instructions : undefined;
|
|
74
|
+
|
|
75
|
+
// Return type de-aliased to `Operation.PersistentOperation` (what `Runnable.Runnable` aliases) so this
|
|
76
|
+
// exported symbol's declaration names it directly — the reference that lets `tsc` emit the module's
|
|
77
|
+
// `.d.ts` (avoids TS2742) without keeping an otherwise-unused `Operation` import.
|
|
78
|
+
/** The Operation (runnable) ref of an operation-action routine, or undefined for an instructions action. */
|
|
79
|
+
export const runnableRef = (routine: Pick<Routine, 'spec'>): Ref.Ref<Operation.PersistentOperation> | undefined =>
|
|
80
|
+
routine.spec?.kind === 'runnable' ? routine.spec.runnable : undefined;
|
|
81
|
+
|
|
82
|
+
/** Factory wrapper around `Obj.make` for {@link Routine}. Trigger wiring lives in plugin-routine (`makeRoutine`). */
|
|
83
|
+
export const make = (
|
|
84
|
+
props: Omit<Obj.MakeProps<typeof Routine>, 'triggers'> & { triggers?: ReadonlyArray<Ref.Ref<Trigger.Trigger>> },
|
|
85
|
+
): Routine => Obj.make(Routine, { ...props, triggers: props.triggers ?? [] });
|