@dxos/compute 0.8.4-main.fd6878d → 0.9.0

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.
Files changed (181) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +36 -1
  3. package/dist/lib/neutral/StorageService.mjs +22 -0
  4. package/dist/lib/neutral/chunk-2B2KSJT6.mjs +17 -0
  5. package/dist/lib/neutral/chunk-2B2KSJT6.mjs.map +7 -0
  6. package/dist/lib/neutral/chunk-AFBNVFAY.mjs +25 -0
  7. package/dist/lib/neutral/chunk-AFBNVFAY.mjs.map +7 -0
  8. package/dist/lib/neutral/chunk-J5LGTIGS.mjs +10 -0
  9. package/dist/lib/neutral/chunk-JAU4E4EK.mjs +53 -0
  10. package/dist/lib/neutral/chunk-JAU4E4EK.mjs.map +7 -0
  11. package/dist/lib/neutral/chunk-MEPE4HHX.mjs +21 -0
  12. package/dist/lib/neutral/chunk-MEPE4HHX.mjs.map +7 -0
  13. package/dist/lib/neutral/chunk-QZS4G6PE.mjs +240 -0
  14. package/dist/lib/neutral/chunk-QZS4G6PE.mjs.map +7 -0
  15. package/dist/lib/neutral/chunk-XEVFBI6S.mjs +62 -0
  16. package/dist/lib/neutral/chunk-XEVFBI6S.mjs.map +7 -0
  17. package/dist/lib/neutral/chunk-Z3Y7FQGN.mjs +174 -0
  18. package/dist/lib/neutral/chunk-Z3Y7FQGN.mjs.map +7 -0
  19. package/dist/lib/neutral/fib-HBDUGQW7.mjs +10 -0
  20. package/dist/lib/neutral/fib-HBDUGQW7.mjs.map +7 -0
  21. package/dist/lib/neutral/index.mjs +1183 -0
  22. package/dist/lib/neutral/index.mjs.map +7 -0
  23. package/dist/lib/neutral/meta.json +1 -0
  24. package/dist/lib/neutral/reply-RJIAJ6FV.mjs +10 -0
  25. package/dist/lib/neutral/reply-RJIAJ6FV.mjs.map +7 -0
  26. package/dist/lib/neutral/sleep-3T2CZ7RE.mjs +10 -0
  27. package/dist/lib/neutral/sleep-3T2CZ7RE.mjs.map +7 -0
  28. package/dist/lib/neutral/testing/index.mjs +32 -0
  29. package/dist/lib/neutral/testing/index.mjs.map +7 -0
  30. package/dist/types/src/Blueprint.d.ts +182 -0
  31. package/dist/types/src/Blueprint.d.ts.map +1 -0
  32. package/dist/types/src/Credential.d.ts +27 -0
  33. package/dist/types/src/Credential.d.ts.map +1 -0
  34. package/dist/types/src/Header.d.ts +9 -0
  35. package/dist/types/src/Header.d.ts.map +1 -0
  36. package/dist/types/src/Header.test.d.ts +2 -0
  37. package/dist/types/src/Header.test.d.ts.map +1 -0
  38. package/dist/types/src/LayerSpec.d.ts +56 -0
  39. package/dist/types/src/LayerSpec.d.ts.map +1 -0
  40. package/dist/types/src/McpServer.d.ts +26 -0
  41. package/dist/types/src/McpServer.d.ts.map +1 -0
  42. package/dist/types/src/Operation.d.ts +385 -0
  43. package/dist/types/src/Operation.d.ts.map +1 -0
  44. package/dist/types/src/OperationHandlerSet.d.ts +69 -0
  45. package/dist/types/src/OperationHandlerSet.d.ts.map +1 -0
  46. package/dist/types/src/OperationHandlerSet.test.d.ts +2 -0
  47. package/dist/types/src/OperationHandlerSet.test.d.ts.map +1 -0
  48. package/dist/types/src/OperationRegistry.d.ts +23 -0
  49. package/dist/types/src/OperationRegistry.d.ts.map +1 -0
  50. package/dist/types/src/Process.d.ts +255 -0
  51. package/dist/types/src/Process.d.ts.map +1 -0
  52. package/dist/types/src/Routine.d.ts +54 -0
  53. package/dist/types/src/Routine.d.ts.map +1 -0
  54. package/dist/types/src/Script.d.ts +21 -0
  55. package/dist/types/src/Script.d.ts.map +1 -0
  56. package/dist/types/src/ServiceResolver.d.ts +92 -0
  57. package/dist/types/src/ServiceResolver.d.ts.map +1 -0
  58. package/dist/types/src/StorageService.d.ts +58 -0
  59. package/dist/types/src/StorageService.d.ts.map +1 -0
  60. package/dist/types/src/Template.d.ts +61 -0
  61. package/dist/types/src/Template.d.ts.map +1 -0
  62. package/dist/types/src/Template.test.d.ts +2 -0
  63. package/dist/types/src/Template.test.d.ts.map +1 -0
  64. package/dist/types/src/Trace.d.ts +266 -0
  65. package/dist/types/src/Trace.d.ts.map +1 -0
  66. package/dist/types/src/Trigger.d.ts +192 -0
  67. package/dist/types/src/Trigger.d.ts.map +1 -0
  68. package/dist/types/src/TriggerEvent.d.ts +83 -0
  69. package/dist/types/src/TriggerEvent.d.ts.map +1 -0
  70. package/dist/types/src/errors.d.ts +308 -0
  71. package/dist/types/src/errors.d.ts.map +1 -0
  72. package/dist/types/src/index.d.ts +18 -5
  73. package/dist/types/src/index.d.ts.map +1 -1
  74. package/dist/types/src/testing/definitions.d.ts +11 -0
  75. package/dist/types/src/testing/definitions.d.ts.map +1 -0
  76. package/dist/types/src/testing/fib.d.ts +8 -0
  77. package/dist/types/src/testing/fib.d.ts.map +1 -0
  78. package/dist/types/src/testing/index.d.ts +6 -3
  79. package/dist/types/src/testing/index.d.ts.map +1 -1
  80. package/dist/types/src/testing/reply.d.ts +4 -0
  81. package/dist/types/src/testing/reply.d.ts.map +1 -0
  82. package/dist/types/src/testing/sleep.d.ts +6 -0
  83. package/dist/types/src/testing/sleep.d.ts.map +1 -0
  84. package/dist/types/src/umbrella.test.d.ts +2 -0
  85. package/dist/types/src/umbrella.test.d.ts.map +1 -0
  86. package/dist/types/tsconfig.tsbuildinfo +1 -1
  87. package/package.json +31 -47
  88. package/src/Blueprint.ts +224 -0
  89. package/src/Credential.ts +53 -0
  90. package/src/Header.test.ts +132 -0
  91. package/src/Header.ts +38 -0
  92. package/src/LayerSpec.ts +91 -0
  93. package/src/McpServer.ts +44 -0
  94. package/src/Operation.ts +646 -0
  95. package/src/OperationHandlerSet.test.ts +97 -0
  96. package/src/OperationHandlerSet.ts +181 -0
  97. package/src/OperationRegistry.ts +50 -0
  98. package/src/Process.ts +541 -0
  99. package/src/Routine.ts +68 -0
  100. package/src/Script.ts +33 -0
  101. package/src/ServiceResolver.ts +200 -0
  102. package/src/StorageService.ts +101 -0
  103. package/src/Template.test.ts +164 -0
  104. package/src/Template.ts +125 -0
  105. package/src/Trace.ts +363 -0
  106. package/src/Trigger.ts +196 -0
  107. package/src/TriggerEvent.ts +64 -0
  108. package/src/errors.ts +71 -0
  109. package/src/index.ts +19 -32
  110. package/src/testing/definitions.ts +51 -0
  111. package/src/testing/fib.ts +22 -0
  112. package/src/testing/index.ts +13 -4
  113. package/src/testing/reply.ts +18 -0
  114. package/src/testing/sleep.ts +16 -0
  115. package/src/umbrella.test.ts +51 -0
  116. package/dist/lib/browser/chunk-X2FWMHD4.mjs +0 -3107
  117. package/dist/lib/browser/chunk-X2FWMHD4.mjs.map +0 -7
  118. package/dist/lib/browser/index.mjs +0 -55
  119. package/dist/lib/browser/meta.json +0 -1
  120. package/dist/lib/browser/testing/index.mjs +0 -176
  121. package/dist/lib/browser/testing/index.mjs.map +0 -7
  122. package/dist/lib/node-esm/chunk-RP7HSSCA.mjs +0 -3109
  123. package/dist/lib/node-esm/chunk-RP7HSSCA.mjs.map +0 -7
  124. package/dist/lib/node-esm/index.mjs +0 -56
  125. package/dist/lib/node-esm/meta.json +0 -1
  126. package/dist/lib/node-esm/testing/index.mjs +0 -177
  127. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  128. package/dist/types/src/compute-graph-registry.d.ts +0 -34
  129. package/dist/types/src/compute-graph-registry.d.ts.map +0 -1
  130. package/dist/types/src/compute-graph.d.ts +0 -64
  131. package/dist/types/src/compute-graph.d.ts.map +0 -1
  132. package/dist/types/src/compute-graph.test.d.ts +0 -2
  133. package/dist/types/src/compute-graph.test.d.ts.map +0 -1
  134. package/dist/types/src/compute-node.d.ts +0 -25
  135. package/dist/types/src/compute-node.d.ts.map +0 -1
  136. package/dist/types/src/functions/async-function.d.ts +0 -66
  137. package/dist/types/src/functions/async-function.d.ts.map +0 -1
  138. package/dist/types/src/functions/edge-function.d.ts +0 -20
  139. package/dist/types/src/functions/edge-function.d.ts.map +0 -1
  140. package/dist/types/src/functions/function-defs.d.ts +0 -11
  141. package/dist/types/src/functions/function-defs.d.ts.map +0 -1
  142. package/dist/types/src/functions/index.d.ts +0 -4
  143. package/dist/types/src/functions/index.d.ts.map +0 -1
  144. package/dist/types/src/hyperformula.test.d.ts +0 -2
  145. package/dist/types/src/hyperformula.test.d.ts.map +0 -1
  146. package/dist/types/src/testing/test-builder.d.ts +0 -15
  147. package/dist/types/src/testing/test-builder.d.ts.map +0 -1
  148. package/dist/types/src/testing/test-plugin.d.ts +0 -36
  149. package/dist/types/src/testing/test-plugin.d.ts.map +0 -1
  150. package/dist/types/src/testing/util.d.ts +0 -2
  151. package/dist/types/src/testing/util.d.ts.map +0 -1
  152. package/dist/types/src/types/hyperformula.d.ts +0 -11
  153. package/dist/types/src/types/hyperformula.d.ts.map +0 -1
  154. package/dist/types/src/types/index.d.ts +0 -3
  155. package/dist/types/src/types/index.d.ts.map +0 -1
  156. package/dist/types/src/types/types.d.ts +0 -20
  157. package/dist/types/src/types/types.d.ts.map +0 -1
  158. package/dist/types/src/types/types.test.d.ts +0 -2
  159. package/dist/types/src/types/types.test.d.ts.map +0 -1
  160. package/dist/types/src/util.d.ts +0 -2
  161. package/dist/types/src/util.d.ts.map +0 -1
  162. package/dist/vendor/hyperformula.mjs +0 -36400
  163. package/src/compute-graph-registry.ts +0 -91
  164. package/src/compute-graph.test.ts +0 -89
  165. package/src/compute-graph.ts +0 -260
  166. package/src/compute-node.ts +0 -62
  167. package/src/functions/async-function.ts +0 -179
  168. package/src/functions/edge-function.ts +0 -112
  169. package/src/functions/function-defs.ts +0 -2427
  170. package/src/functions/index.ts +0 -7
  171. package/src/hyperformula.test.ts +0 -14
  172. package/src/testing/test-builder.ts +0 -54
  173. package/src/testing/test-plugin.ts +0 -101
  174. package/src/testing/util.ts +0 -9
  175. package/src/types/hyperformula.ts +0 -19
  176. package/src/types/index.ts +0 -6
  177. package/src/types/types.test.ts +0 -91
  178. package/src/types/types.ts +0 -80
  179. package/src/util.ts +0 -8
  180. /package/dist/lib/{browser/index.mjs.map → neutral/StorageService.mjs.map} +0 -0
  181. /package/dist/lib/{node-esm/index.mjs.map → neutral/chunk-J5LGTIGS.mjs.map} +0 -0
package/src/Process.ts ADDED
@@ -0,0 +1,541 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ // @import-as-namespace
6
+
7
+ import type { Atom } from '@effect-atom/atom';
8
+ import * as Context from 'effect/Context';
9
+ import * as Effect from 'effect/Effect';
10
+ import type * as Exit from 'effect/Exit';
11
+ import * as Option from 'effect/Option';
12
+ import * as Schema from 'effect/Schema';
13
+ import * as Scope from 'effect/Scope';
14
+ import type * as Types from 'effect/Types';
15
+
16
+ import { assertArgument } from '@dxos/invariant';
17
+ import { DXN, type URI } from '@dxos/keys';
18
+ import { log } from '@dxos/log';
19
+
20
+ import * as Operation from './Operation';
21
+ import * as OperationHandlerSet from './OperationHandlerSet';
22
+ import * as StorageService from './StorageService';
23
+ import * as Trace from './Trace';
24
+
25
+ //
26
+ // Process.
27
+ //
28
+
29
+ /** Opaque process id (arbitrary string). */
30
+ export const ID = Schema.String.pipe(Schema.brand('ProcessId'));
31
+ export type ID = Schema.Schema.Type<typeof ID>;
32
+
33
+ /**
34
+ * A running process callbacks.
35
+ *
36
+ * Process lifecycle: Initial -> Running <-> Suspended -> Terminated.
37
+ *
38
+ * - onSpawn -> called once when the process is spawned.
39
+ * - onInput -> called for every input submitted to the process.
40
+ * - onAlarm -> called for processes scheduling alarms.
41
+ * - onChildEvent -> called when child process produces output or exits.
42
+ */
43
+ export interface Callbacks<I, O, R> {
44
+ /**
45
+ * Called when the process is spawned.
46
+ * Not called for processes that are resumed from a previously suspended state.
47
+ *
48
+ * @returns A signal indicating to the runtime whether the process is finished, or should be resumed later.
49
+ * @throws Throwing in the handler will terminate the process with an error.
50
+ *
51
+ * Note: This function should aim to complete in under 5 seconds to avoid exceeding limits in serverless environments.
52
+ */
53
+ onSpawn(): Effect.Effect<void, never, R | BaseServices>;
54
+
55
+ /**
56
+ * Called when there's input available to process.
57
+ *
58
+ * The function can be called in parallel.
59
+ *
60
+ * @returns A signal indicating to the runtime whether the process is finished, or should be resumed later.
61
+ * @throws Throwing in the handler will terminate the process with an error.
62
+ *
63
+ * Note: This function should aim to complete in under 5 seconds to avoid exceeding limits in serverless environments.
64
+ */
65
+ onInput(input: I): Effect.Effect<void, never, R | BaseServices>;
66
+
67
+ /**
68
+ * Called when the process's alarm is triggered.
69
+ *
70
+ * @throws Throwing in the handler will terminate the process with an error.
71
+ */
72
+ onAlarm(): Effect.Effect<void, never, R | BaseServices>;
73
+
74
+ /**
75
+ * Called when the process's child process produces output or exits.
76
+ *
77
+ * This allows the parent process to hibernate while a long-running child process is running.
78
+ */
79
+ onChildEvent(event: ChildEvent<unknown>): Effect.Effect<void, never, R | BaseServices>;
80
+ }
81
+
82
+ /**
83
+ * Services that are always available to all processes.
84
+ * Provided unconditionally by the runtime, so handlers may use them without declaring them
85
+ * in {@link MakeProcessOpts.services}.
86
+ */
87
+ export type BaseServices = Trace.TraceService | StorageService.StorageService;
88
+
89
+ export type ChildEvent<T> =
90
+ | {
91
+ readonly _tag: 'output';
92
+ readonly pid: ID;
93
+ readonly data: T;
94
+ }
95
+ | {
96
+ readonly _tag: 'exited';
97
+ readonly pid: ID;
98
+ readonly result: Exit.Exit<void>;
99
+ };
100
+
101
+ export interface ProcessContext<I, O> {
102
+ readonly id: ID;
103
+
104
+ /**
105
+ * Parameters assigned during process creation.
106
+ */
107
+ readonly params: Params;
108
+
109
+ /**
110
+ * Complete this process with sucessful result.
111
+ * No additional events will be pushed to the process.
112
+ */
113
+ succeed(): void;
114
+
115
+ /**
116
+ * Complete this process with an error.
117
+ * No additional events will be pushed to the process.
118
+ */
119
+ fail(error: Error): void;
120
+
121
+ /**
122
+ * Submit output of the process.
123
+ */
124
+ submitOutput(output: O): void;
125
+
126
+ /**
127
+ * Set an alarm for the process to be woken up later.
128
+ *
129
+ * @param timeout - Optional timeout in milliseconds. If not provided, the process is woken up as soon as possible.
130
+ */
131
+ setAlarm(timeout?: number): void;
132
+ }
133
+
134
+ /**
135
+ * Generic parameters for a all processes.
136
+ */
137
+ export interface Params {
138
+ /**
139
+ * Process name for debugging purposes.
140
+ */
141
+ readonly name: string | null;
142
+
143
+ /**
144
+ * URI of the target this process is assigned to.
145
+ */
146
+ readonly target: URI.URI | null;
147
+
148
+ /**
149
+ * User-facing notifications requested for this process's lifecycle phases.
150
+ * Surfaced on {@link Info} so a notification tracker can subscribe to the process monitor.
151
+ */
152
+ readonly notify?: Operation.NotifyOptions;
153
+ }
154
+
155
+ //
156
+ // Executable.
157
+ //
158
+
159
+ export const ProcessTypeId = '~@dxos/functions/Process' as const;
160
+ export type ProcessTypeId = typeof ProcessTypeId;
161
+
162
+ /**
163
+ * A process (factory).
164
+ * Can be instantiated mutlitple times to produce new runtime instance with separate state and callbacks.
165
+ * `create` is used to instantiate a new process.
166
+ * Can store runtime state in scope of `create` function.
167
+ */
168
+ export interface Process<I, O, R> extends Process.Variance<I, O, R> {
169
+ /**
170
+ * Unique identifier for the executable in the reverse DNS format.
171
+ */
172
+ readonly key: string;
173
+
174
+ /**
175
+ * Human-readable label from {@link MakeExecutableOpts.name} when provided.
176
+ */
177
+ readonly name?: string;
178
+
179
+ readonly services: readonly Context.Tag<any, any>[];
180
+
181
+ /**
182
+ * Create a new instance of the process.
183
+ */
184
+ create(ctx: ProcessContext<I, O>): Effect.Effect<Callbacks<I, O, R>, never, R | BaseServices | Scope.Scope>;
185
+ }
186
+
187
+ export const isProcess = (executable: unknown): executable is Process.Any =>
188
+ typeof executable === 'object' && executable !== null && ProcessTypeId in executable;
189
+
190
+ export namespace Process {
191
+ export interface Variance<I, O, R> {
192
+ readonly [ProcessTypeId]: {
193
+ readonly _Input: Types.Contravariant<I>;
194
+ readonly _Output: Types.Covariant<O>;
195
+ readonly _Requirements: Types.Covariant<R>;
196
+ };
197
+ }
198
+
199
+ export type Any = Process<any, any, never>;
200
+ }
201
+
202
+ export interface MakeProcessOpts {
203
+ /**
204
+ * Unique identifier for the process in the reverse DNS format.
205
+ */
206
+ readonly key: string;
207
+
208
+ readonly input: Schema.Schema.AnyNoContext;
209
+ readonly output: Schema.Schema.AnyNoContext;
210
+ readonly services: readonly Context.Tag<any, any>[];
211
+ }
212
+
213
+ export const make = <const Opts extends Types.NoExcessProperties<MakeProcessOpts, Opts>>(
214
+ opts: Opts,
215
+ create: (
216
+ ctx: ProcessContext<Schema.Schema.Type<Opts['input']>, Schema.Schema.Type<Opts['output']>>,
217
+ ) => Effect.Effect<
218
+ Partial<
219
+ Callbacks<
220
+ Schema.Schema.Type<Opts['input']>,
221
+ Schema.Schema.Type<Opts['output']>,
222
+ Context.Tag.Identifier<NonNullable<Opts['services']>[number]>
223
+ >
224
+ >,
225
+ never,
226
+ Context.Tag.Identifier<NonNullable<Opts['services']>[number]> | BaseServices | Scope.Scope
227
+ >,
228
+ ): Process<
229
+ Schema.Schema.Type<Opts['input']>,
230
+ Schema.Schema.Type<Opts['output']>,
231
+ Context.Tag.Identifier<NonNullable<Opts['services']>[number]>
232
+ > => {
233
+ assertArgument(/^[a-z0-9]([a-z0-9.\-/]*[a-z0-9])?$/i.test(opts.key), 'key', 'Invalid key');
234
+ return {
235
+ [ProcessTypeId]: {} as any,
236
+ ...opts,
237
+ create: (ctx) =>
238
+ create(ctx).pipe(
239
+ Effect.map((partial) => ({
240
+ onSpawn: () => Effect.void,
241
+ onInput: () => Effect.void,
242
+ onAlarm: () => Effect.void,
243
+ onChildEvent: () => Effect.void,
244
+ ...partial,
245
+ })),
246
+ ),
247
+ };
248
+ };
249
+
250
+ /**
251
+ * Durable marker recording that an operation's input handler has begun executing.
252
+ * Persisted before the handler runs so that, after an interruption (suspend/restart), a
253
+ * re-delivered input can tell that the previous attempt was in-flight. Cleared automatically
254
+ * when the process reaches a terminal state (the runtime clears the process's storage).
255
+ */
256
+ const OperationStartedKey = StorageService.key(Schema.parseJson(Schema.Boolean), 'operation/started').pipe(
257
+ StorageService.withDefault(() => false),
258
+ );
259
+
260
+ export const fromOperation = <const Op extends Operation.Definition.Any>(
261
+ op: Op,
262
+ handler: OperationHandlerSet.OperationHandlerSet,
263
+ ): Process<Operation.Definition.Input<Op>, Operation.Definition.Output<Op>, Operation.Definition.Services<Op>> =>
264
+ make(
265
+ {
266
+ key: DXN.getName(op.meta.key),
267
+ input: op.input,
268
+ output: op.output,
269
+ services: op.services,
270
+ },
271
+ (ctx) =>
272
+ Effect.gen(function* () {
273
+ const semaphore = yield* Effect.makeSemaphore(1);
274
+ // The process runtime assumes handlers are idempotent and always re-delivers an input
275
+ // whose handler was interrupted. Non-idempotent operations opt out of that retry here:
276
+ // a re-delivery that observes the durable "started" marker fails instead of repeating
277
+ // side effects. Idempotent operations skip the marker and are simply re-run.
278
+ const idempotent = Operation.isIdempotent(op);
279
+
280
+ return {
281
+ onInput: (input: Operation.Definition.Input<Op>) =>
282
+ Effect.gen(function* () {
283
+ if (!idempotent) {
284
+ const started = yield* OperationStartedKey.get;
285
+ if (started) {
286
+ return yield* Effect.die(
287
+ new Error(`non-idempotent operation "${op.meta.key}" was interrupted; cannot retry safely`),
288
+ );
289
+ }
290
+ yield* OperationStartedKey.set(true);
291
+ }
292
+
293
+ // Emit operation start event.
294
+ log('operation process invoking', { key: op.meta.key, name: op.meta.name });
295
+ yield* Trace.write(Trace.OperationStart, {
296
+ key: op.meta.key,
297
+ name: op.meta.name,
298
+ icon: op.meta.icon,
299
+ });
300
+ // Emit ephemeral operation input event for live subscribers
301
+ // (history tracker, devtools) without persisting raw input.
302
+ yield* Trace.write(Trace.OperationInput, {
303
+ key: op.meta.key,
304
+ name: op.meta.name,
305
+ input,
306
+ });
307
+
308
+ const opHandler = yield* OperationHandlerSet.getHandler(handler, op).pipe(Effect.orDie);
309
+ const output = yield* opHandler
310
+ .handler(input)
311
+ .pipe(Effect.orDie, Effect.withSpan(op.meta.key)) as Effect.Effect<
312
+ Operation.Definition.Output<Op>,
313
+ never,
314
+ never
315
+ >;
316
+
317
+ ctx.submitOutput(output);
318
+ ctx.succeed();
319
+
320
+ // Emit ephemeral operation output event before the persisted
321
+ // end event so subscribers see output + completion together.
322
+ yield* Trace.write(Trace.OperationOutput, {
323
+ key: op.meta.key,
324
+ name: op.meta.name,
325
+ output,
326
+ });
327
+ // Emit operation end event with success after side-effects complete.
328
+ yield* Trace.write(Trace.OperationEnd, {
329
+ key: op.meta.key,
330
+ name: op.meta.name,
331
+ icon: op.meta.icon,
332
+ outcome: 'success',
333
+ });
334
+ }).pipe(
335
+ Effect.catchAllDefect((defect) =>
336
+ Effect.gen(function* () {
337
+ // Emit operation end event with failure.
338
+ const errorMessage = defect instanceof Error ? defect.message : String(defect);
339
+ yield* Trace.write(Trace.OperationEnd, {
340
+ key: op.meta.key,
341
+ name: op.meta.name,
342
+ icon: op.meta.icon,
343
+ outcome: 'failure',
344
+ error: errorMessage,
345
+ });
346
+ return yield* Effect.die(defect);
347
+ }),
348
+ ),
349
+ semaphore.withPermits(1),
350
+ ),
351
+ };
352
+ }),
353
+ );
354
+
355
+ /**
356
+ * Runtime state of a process.
357
+ */
358
+ export enum State {
359
+ // Process is actively running.
360
+ RUNNING = 'RUNNING',
361
+
362
+ // Process is waiting for a child process to complete or an alarm to trigger.
363
+ HYBERNATING = 'HYBERNATING',
364
+
365
+ // Process is waiting for input. It will only resume when input is submitted.
366
+ IDLE = 'IDLE',
367
+
368
+ // Process is terminating and will transition to TERMINATED state.
369
+ // TODO(dmaretskyi): Consider removing.
370
+ TERMINATING = 'TERMINATING',
371
+
372
+ // Process has been externally terminated.
373
+ TERMINATED = 'TERMINATED',
374
+
375
+ // Process has completed successfully.
376
+ SUCCEEDED = 'SUCCEEDED',
377
+
378
+ // Process has failed.
379
+ FAILED = 'FAILED',
380
+ }
381
+
382
+ /**
383
+ * Read-only view of a process tree
384
+ */
385
+ export interface Monitor {
386
+ /**
387
+ * Get the current state of the process tree.
388
+ */
389
+ processTree: Effect.Effect<readonly Info[]>;
390
+
391
+ /**
392
+ * Atom for the process tree.
393
+ */
394
+ processTreeAtom: Atom.Atom<readonly Info[]>;
395
+ }
396
+
397
+ export class ProcessMonitorService extends Context.Tag('@dxos/functions/ProcessMonitorService')<
398
+ ProcessMonitorService,
399
+ Monitor
400
+ >() {}
401
+
402
+ export interface Info {
403
+ readonly pid: ID;
404
+ readonly parentPid: ID | null;
405
+
406
+ /**
407
+ * Key of the process.
408
+ *
409
+ * NOTE: There might be multiple running processes with the same key.
410
+ */
411
+ readonly key: string;
412
+
413
+ /**
414
+ * Parameters of the process.
415
+ */
416
+ readonly params: Params;
417
+
418
+ /**
419
+ * State of the process.
420
+ */
421
+ readonly state: State;
422
+
423
+ /**
424
+ * Error of the process.
425
+ * Only for process in FAILED state.
426
+ */
427
+ readonly error: string | null;
428
+
429
+ /**
430
+ * UNIX timestamp in milliseconds.
431
+ */
432
+ readonly startedAt: number;
433
+
434
+ /**
435
+ * UNIX timestamp in milliseconds.
436
+ */
437
+ readonly completedAt: Option.Option<number>;
438
+
439
+ readonly metrics: {
440
+ /**
441
+ * Total wall time of all handler invocations of the process in milliseconds.
442
+ */
443
+ readonly wallTime: number;
444
+
445
+ /**
446
+ * Total number of inputs submitted to the process.
447
+ */
448
+ readonly inputCount: number;
449
+
450
+ /**
451
+ * Total number of outputs submitted to the process.
452
+ */
453
+ readonly outputCount: number;
454
+ };
455
+ }
456
+
457
+ /**
458
+ * New process is spawned.
459
+ */
460
+ export const SpawnedEvent = Trace.EventType('process.spawned', {
461
+ schema: Schema.Void,
462
+ isEphemeral: false,
463
+ });
464
+
465
+ /**
466
+ * Process has reached a terminal state.
467
+ */
468
+ export const ExitedEvent = Trace.EventType('process.exited', {
469
+ schema: Schema.Struct({
470
+ outcome: Schema.Literal('succeeded', 'failed', 'terminated'),
471
+ }),
472
+ isEphemeral: false,
473
+ });
474
+
475
+ /**
476
+ * Renders spawned processes as a forest: top-level rows use "- ", nested rows use ├── / └── / │.
477
+ */
478
+ export const prettyProcessTree = (tree: readonly Info[]): string => {
479
+ if (tree.length === 0) {
480
+ return '';
481
+ }
482
+
483
+ const pidSet = new Set(tree.map((node) => node.pid));
484
+ const childrenByParent = new Map<string, Info[]>();
485
+ const roots: Info[] = [];
486
+
487
+ for (const node of tree) {
488
+ const parent = node.parentPid;
489
+ if (parent === null || !pidSet.has(parent)) {
490
+ roots.push(node);
491
+ continue;
492
+ }
493
+ const key = String(parent);
494
+ const siblings = childrenByParent.get(key) ?? [];
495
+ siblings.push(node);
496
+ childrenByParent.set(key, siblings);
497
+ }
498
+
499
+ const byPid = (a: Info, b: Info) => String(a.pid).localeCompare(String(b.pid));
500
+ roots.sort(byPid);
501
+ for (const siblings of childrenByParent.values()) {
502
+ siblings.sort(byPid);
503
+ }
504
+
505
+ const formatLabel = (node: Info): string => {
506
+ const idShort = String(node.pid).slice(0, 6);
507
+ const parts = [idShort, node.state];
508
+ if (node.params.name != null && node.params.name !== '') {
509
+ parts.push(node.params.name);
510
+ }
511
+ if (node.error != null) {
512
+ parts.push(`(${node.error})`);
513
+ }
514
+ const { inputCount, outputCount, wallTime } = node.metrics;
515
+ parts.push(`[in:${inputCount} out:${outputCount} wall:${Math.round(wallTime)}ms]`);
516
+ return parts.join(' ');
517
+ };
518
+
519
+ const lines: string[] = [];
520
+
521
+ const walk = (node: Info, prefix: string, isLast: boolean, isRoot: boolean): void => {
522
+ if (isRoot) {
523
+ lines.push(`- ${formatLabel(node)}`);
524
+ } else {
525
+ const branch = isLast ? '└── ' : '├── ';
526
+ lines.push(`${prefix}${branch}${formatLabel(node)}`);
527
+ }
528
+
529
+ const children = childrenByParent.get(String(node.pid)) ?? [];
530
+ const nextPrefix = isRoot ? ' ' : `${prefix}${isLast ? ' ' : '│ '}`;
531
+ children.forEach((child, index) => {
532
+ walk(child, nextPrefix, index === children.length - 1, false);
533
+ });
534
+ };
535
+
536
+ for (const root of roots) {
537
+ walk(root, '', true, true);
538
+ }
539
+
540
+ return lines.join('\n');
541
+ };
package/src/Routine.ts ADDED
@@ -0,0 +1,68 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ // @import-as-namespace
6
+
7
+ import * as Schema from 'effect/Schema';
8
+
9
+ import { DXN, Annotation, JsonSchema, Obj, Ref, Type, Format } from '@dxos/echo';
10
+ import { Text } from '@dxos/schema';
11
+
12
+ import * as Blueprint from './Blueprint';
13
+
14
+ /**
15
+ * Prompt-based operation.
16
+ * May reference blueprints and additional context.
17
+ */
18
+ export const Routine = Schema.Struct({
19
+ name: Schema.optional(Schema.String),
20
+ description: Schema.optional(Schema.String),
21
+ input: JsonSchema.JsonSchema.pipe(Annotation.FormInputAnnotation.set(false)).annotations({
22
+ description: 'Input schema',
23
+ }),
24
+ output: JsonSchema.JsonSchema.pipe(Annotation.FormInputAnnotation.set(false)).annotations({
25
+ description: 'Output schema',
26
+ }),
27
+ instructions: Ref.Ref(Text.Text).pipe(
28
+ Format.FormatAnnotation.set(Format.TypeFormat.Markdown),
29
+ Schema.annotations({ title: 'Instructions', description: 'Agent instructions' }),
30
+ ),
31
+ blueprints: Schema.Array(Ref.Ref(Blueprint.Blueprint)),
32
+ context: Schema.Array(Schema.Any).pipe(Annotation.FormInputAnnotation.set(false)),
33
+ }).pipe(
34
+ Annotation.LabelAnnotation.set(['name']),
35
+ Annotation.IconAnnotation.set({ icon: 'ph--scroll--regular', hue: 'sky' }),
36
+ Type.makeObject(DXN.make('org.dxos.type.routine', '0.1.0')),
37
+ );
38
+
39
+ export type Routine = Type.InstanceType<typeof Routine>;
40
+
41
+ export type MakeProps = {
42
+ name?: string;
43
+ description?: string;
44
+ input?: Schema.Schema.AnyNoContext;
45
+ output?: Schema.Schema.AnyNoContext;
46
+ instructions?: string;
47
+ blueprints?: Ref.Ref<Blueprint.Blueprint>[];
48
+ context?: any[];
49
+ };
50
+
51
+ export const make = ({
52
+ name,
53
+ description,
54
+ input,
55
+ output,
56
+ instructions,
57
+ blueprints = [],
58
+ context = [],
59
+ }: MakeProps): Routine =>
60
+ Obj.make(Routine, {
61
+ name,
62
+ description,
63
+ input: JsonSchema.toJsonSchema(input ?? Schema.Void),
64
+ output: JsonSchema.toJsonSchema(output ?? Schema.Void),
65
+ instructions: Ref.make(Text.make({ content: instructions ?? '' })),
66
+ blueprints,
67
+ context,
68
+ });
package/src/Script.ts ADDED
@@ -0,0 +1,33 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ // @import-as-namespace
6
+
7
+ import * as Schema from 'effect/Schema';
8
+
9
+ import { DXN, Annotation, Obj, Ref, Type } from '@dxos/echo';
10
+ import { FormInputAnnotation } from '@dxos/echo/Annotation';
11
+ import { Text } from '@dxos/schema';
12
+
13
+ /**
14
+ * Source script.
15
+ */
16
+ export const Script = Schema.Struct({
17
+ name: Schema.String.pipe(Schema.optional),
18
+ description: Schema.String.pipe(Schema.optional),
19
+ // TODO(burdon): Change to hash of deployed content.
20
+ // Whether source has changed since last deploy.
21
+ changed: Schema.Boolean.pipe(FormInputAnnotation.set(false), Schema.optional),
22
+ source: Ref.Ref(Text.Text).pipe(FormInputAnnotation.set(false)),
23
+ }).pipe(
24
+ Annotation.LabelAnnotation.set(['name']),
25
+ Annotation.IconAnnotation.set({ icon: 'ph--code--regular', hue: 'sky' }),
26
+ Type.makeObject(DXN.make('org.dxos.type.script', '0.1.0')),
27
+ );
28
+
29
+ export type Script = Type.InstanceType<typeof Script>;
30
+ type Props = Omit<Obj.MakeProps<typeof Script>, 'source'> & { source?: string };
31
+
32
+ export const make = ({ source = '', ...props }: Props = {}): Script =>
33
+ Obj.make(Script, { ...props, source: Ref.make(Text.make({ content: source })) });