@dxos/compute 0.13.0 → 0.14.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 (48) hide show
  1. package/dist/lib/AgentService.mjs.map +1 -1
  2. package/dist/lib/Operation.mjs +41 -14
  3. package/dist/lib/Operation.mjs.map +1 -1
  4. package/dist/lib/OperationHandlerSet.mjs +1 -1
  5. package/dist/lib/Process.mjs +34 -2
  6. package/dist/lib/Process.mjs.map +1 -1
  7. package/dist/lib/ShellService.mjs +24 -0
  8. package/dist/lib/ShellService.mjs.map +1 -0
  9. package/dist/lib/chunk-SqlService.mjs +2 -2
  10. package/dist/lib/errors.mjs +7 -1
  11. package/dist/lib/errors.mjs.map +1 -1
  12. package/dist/lib/index.mjs +2 -1
  13. package/dist/lib/types/Project.mjs +8 -0
  14. package/dist/lib/types/Project.mjs.map +1 -1
  15. package/dist/lib/types/Skill.mjs +7 -1
  16. package/dist/lib/types/Skill.mjs.map +1 -1
  17. package/dist/lib/types/Trigger.mjs +6 -5
  18. package/dist/lib/types/Trigger.mjs.map +1 -1
  19. package/dist/types/src/AgentService.d.ts +2 -1
  20. package/dist/types/src/AgentService.d.ts.map +1 -1
  21. package/dist/types/src/Operation.d.ts +64 -11
  22. package/dist/types/src/Operation.d.ts.map +1 -1
  23. package/dist/types/src/Process.d.ts +53 -31
  24. package/dist/types/src/Process.d.ts.map +1 -1
  25. package/dist/types/src/ShellService.d.ts +85 -0
  26. package/dist/types/src/ShellService.d.ts.map +1 -0
  27. package/dist/types/src/errors.d.ts +31 -0
  28. package/dist/types/src/errors.d.ts.map +1 -1
  29. package/dist/types/src/index.d.ts +1 -0
  30. package/dist/types/src/index.d.ts.map +1 -1
  31. package/dist/types/src/types/Project.d.ts +2 -0
  32. package/dist/types/src/types/Project.d.ts.map +1 -1
  33. package/dist/types/src/types/Skill.d.ts +8 -0
  34. package/dist/types/src/types/Skill.d.ts.map +1 -1
  35. package/dist/types/src/types/Trigger.d.ts +11 -9
  36. package/dist/types/src/types/Trigger.d.ts.map +1 -1
  37. package/dist/types/tsconfig.tsbuildinfo +1 -1
  38. package/package.json +20 -15
  39. package/src/AgentService.ts +2 -1
  40. package/src/DurableOperation.tst.ts +155 -0
  41. package/src/Operation.ts +121 -46
  42. package/src/Process.ts +99 -41
  43. package/src/ShellService.ts +71 -0
  44. package/src/errors.ts +7 -0
  45. package/src/index.ts +1 -0
  46. package/src/types/Project.ts +11 -0
  47. package/src/types/Skill.ts +9 -0
  48. package/src/types/Trigger.ts +9 -7
package/src/Process.ts CHANGED
@@ -4,15 +4,16 @@
4
4
 
5
5
  // @import-as-namespace
6
6
 
7
+ import * as Cause from 'effect/Cause';
7
8
  import * as Context from 'effect/Context';
8
9
  import * as Effect from 'effect/Effect';
9
- import type * as Exit from 'effect/Exit';
10
+ import * as Exit from 'effect/Exit';
10
11
  import * as Option from 'effect/Option';
11
12
  import type * as Atom from 'effect/reactivity/Atom';
12
13
  import type * as Rpc from 'effect/rpc/Rpc';
13
14
  import type * as RpcClient from 'effect/rpc/RpcClient';
14
15
  import * as Schema from 'effect/Schema';
15
- import type * as Stream from 'effect/Stream';
16
+ import * as Stream from 'effect/Stream';
16
17
 
17
18
  import { Annotation, type Database } from '@dxos/echo';
18
19
  import { type SpaceId, URI } from '@dxos/keys';
@@ -162,7 +163,7 @@ export interface Filter {
162
163
  * Whether `info` satisfies `filter`. Exported so every {@link Manager} filters identically rather
163
164
  * than each implementation growing its own notion of a match.
164
165
  */
165
- export const matchesFilter = (info: Process, filter: Filter = {}): boolean => {
166
+ export const matchesFilter = (info: Data, filter: Filter = {}): boolean => {
166
167
  if (filter.key !== undefined && info.key !== filter.key) {
167
168
  return false;
168
169
  }
@@ -190,10 +191,20 @@ export const listFromTree =
190
191
  (filter?: Filter): Effect.Effect<readonly Process[]> =>
191
192
  Effect.map(processTree, (tree) => tree.filter((info) => matchesFilter(info, filter)));
192
193
 
194
+ export interface SubscribeEphemeralOptions {
195
+ /**
196
+ * Replay the buffered events before streaming new ones (the default), so a reader attaching mid-turn
197
+ * sees the turn so far. A reader that subscribes BEFORE causing the work it wants to follow passes
198
+ * `false`: the buffer then holds only earlier turns, which would read as the new one.
199
+ */
200
+ readonly replay?: boolean;
201
+ }
202
+
193
203
  /**
194
- * A process: one running (or finished) instance of a durable operation, wherever its runtime runs.
204
+ * The plain fields of a {@link Process}: what a process tree holds, what crosses the wire, and what
205
+ * {@link make} turns back into a process.
195
206
  */
196
- export interface Process {
207
+ export interface Data {
197
208
  readonly pid: ID;
198
209
  readonly parentPid: ID | null;
199
210
 
@@ -253,10 +264,6 @@ export interface Process {
253
264
  };
254
265
  }
255
266
 
256
- //
257
- // Handles.
258
- //
259
-
260
267
  export interface Status {
261
268
  readonly state: State;
262
269
  readonly exit: Option.Option<Exit.Exit<void>>;
@@ -265,25 +272,14 @@ export interface Status {
265
272
  readonly completedAt: Option.Option<Date>;
266
273
  }
267
274
 
268
- export interface Handle<_Input, _Output, _Rpcs extends Rpc.Any> {
269
- readonly pid: ID;
270
- readonly parentId: ID | null;
271
-
272
- /**
273
- * Process definition key ({@link Operation.Durable.key}) for this process.
274
- */
275
- readonly key: string;
276
-
277
- /**
278
- * Parameters of the process.
279
- */
280
- readonly params: Params;
281
-
282
- /**
283
- * What the process is running on behalf of. See {@link Environment}.
284
- */
285
- readonly environment: Environment;
286
-
275
+ /**
276
+ * A process: one running (or finished) instance of a durable operation, wherever its runtime runs.
277
+ *
278
+ * Its {@link Data} fields are plain values; the rest are live members that talk to the runtime.
279
+ * A process read from a tree or list is a cheap snapshot that reaches its runtime only once a
280
+ * live member is used (see {@link make}).
281
+ */
282
+ export interface Process<_Input = any, _Output = any, _Rpcs extends Rpc.Any = any> extends Data {
287
283
  submitInput(input: _Input): Effect.Effect<void>;
288
284
  subscribeOutputs(): Stream.Stream<_Output>;
289
285
 
@@ -297,9 +293,9 @@ export interface Handle<_Input, _Output, _Rpcs extends Rpc.Any> {
297
293
  * the collector with {@link Effect.forkDetach}, not {@link Effect.forkChild} — the
298
294
  * parent scope closes as soon as `forEach` finishes and interrupts scoped forks
299
295
  * before live `pushEphemeral` events arrive. Interrupt the daemon fiber explicitly
300
- * on dispose (see {@link ProcessOperationInvoker.fiberFromProcess}).
296
+ * on dispose.
301
297
  */
302
- subscribeEphemeral(): Stream.Stream<Trace.Message>;
298
+ subscribeEphemeral(options?: SubscribeEphemeralOptions): Stream.Stream<Trace.Message>;
303
299
 
304
300
  terminate(): Effect.Effect<void>;
305
301
  readonly status: Status;
@@ -347,22 +343,23 @@ export interface Handle<_Input, _Output, _Rpcs extends Rpc.Any> {
347
343
  * Hydrates a dormant persisted process using the supplied definition.
348
344
  * No-op when the handle is already live (returns self).
349
345
  */
350
- hydrate(definition: Operation.Durable<_Input, _Output, any, any>): Effect.Effect<Handle<_Input, _Output, _Rpcs>>;
346
+ hydrate(definition: Operation.Durable<_Input, _Output, any, any>): Effect.Effect<Process<_Input, _Output, _Rpcs>>;
351
347
 
352
348
  readonly rpc: RpcClient.RpcClient<_Rpcs>;
353
349
  }
354
350
 
355
- export namespace Handle {
356
- // Widened to `any` Rpcs so the implemented `rpc: RpcClient<any>` is assignable
357
- // regardless of a handle's concrete RPC group (variance, see design spec §4.4).
358
- export type Any = Handle<any, any, any>;
359
- }
351
+ // `any` Rpcs keeps every process assignable regardless of its concrete RPC group (variance, see design spec §4.4).
352
+ export type Any = Process<any, any, any>;
360
353
 
361
354
  /**
362
355
  * Options for spawning a process.
363
356
  */
364
357
  export interface SpawnOptions {
365
- /** Parent process ID — child inherits the parent's trace context. */
358
+ /**
359
+ * Parent process ID — child inherits the parent's trace context.
360
+ * Inside a process, its {@link ManagerService} defaults this to that process; pass `undefined`
361
+ * explicitly to spawn a detached process.
362
+ */
366
363
  readonly parentProcessId?: ID;
367
364
 
368
365
  /**
@@ -473,21 +470,82 @@ export interface Manager {
473
470
  spawn<I, O, Rpcs extends Rpc.Any = never>(
474
471
  definition: Operation.Durable<I, O, any, Rpcs>,
475
472
  options?: SpawnOptions & LocationOptions,
476
- ): Effect.Effect<Handle<I, O, Rpcs>>;
473
+ ): Effect.Effect<Process<I, O, Rpcs>>;
477
474
 
478
475
  /**
479
- * Handles on the processes at `options.location` matching `options`. Dormant entries require
480
- * {@link Handle.hydrate} before inputs can be submitted.
476
+ * Live processes at `options.location` matching `options`. Dormant entries require
477
+ * {@link Process.hydrate} before inputs can be submitted.
481
478
  *
482
479
  * Dies when the location is a remote runtime that offers no process control.
483
480
  */
484
- handles(options?: ListOptions & LocationOptions): Effect.Effect<readonly Handle.Any[]>;
481
+ handles(options?: ListOptions & LocationOptions): Effect.Effect<readonly Any[]>;
482
+
483
+ /**
484
+ * The live process `pid` at `options.location`. A process that has already exited
485
+ * replays its outputs, so its result stays readable after the exit.
486
+ *
487
+ * Dies when no such process is known, or when the location is a remote runtime that offers no
488
+ * process control.
489
+ */
490
+ attach<I, O, Rpcs extends Rpc.Any = never>(pid: ID, options?: LocationOptions): Effect.Effect<Process<I, O, Rpcs>>;
485
491
  }
486
492
 
493
+ /**
494
+ * The first output of a single-output process (e.g. one running an operation), failing with the
495
+ * process's own cause when it fails without one.
496
+ */
497
+ export const awaitOutput = <O>(handle: Process<any, O, any>): Effect.Effect<O> =>
498
+ handle.subscribeOutputs().pipe(
499
+ Stream.runHead,
500
+ Effect.flatMap(
501
+ Option.match({
502
+ onSome: Effect.succeed,
503
+ onNone: () => {
504
+ switch (handle.status.state) {
505
+ case State.FAILED:
506
+ return Effect.failCause(
507
+ handle.status.exit.pipe(
508
+ Option.flatMap(Exit.getCause),
509
+ Option.getOrElse(() => Cause.die('Operation failed with unknown error')),
510
+ ),
511
+ );
512
+ case State.TERMINATED:
513
+ return Effect.die('Operation was terminated');
514
+ case State.SUCCEEDED:
515
+ return Effect.die('Process produced no output');
516
+ default:
517
+ // Outputs close on a live process only when its manager suspends it (app shutdown): the
518
+ // wait was cut short rather than answered.
519
+ return Effect.interrupt;
520
+ }
521
+ },
522
+ }),
523
+ ),
524
+ );
525
+
487
526
  export class ManagerService extends Context.Service<ManagerService, Manager>()(
488
527
  '@dxos/compute/Process.ManagerService',
489
528
  ) {}
490
529
 
530
+ /**
531
+ * Spawns `durable` through the ambient {@link ManagerService} and submits `input`; read its result with
532
+ * {@link awaitOutput}.
533
+ */
534
+ export const spawn = <I, O, Rpcs extends Rpc.Any = never>(
535
+ durable: Operation.Durable<I, O, any, Rpcs>,
536
+ input: I,
537
+ options?: SpawnOptions & LocationOptions,
538
+ ): Effect.Effect<Process<I, O, Rpcs>, never, ManagerService> =>
539
+ Effect.gen(function* () {
540
+ const manager = yield* ManagerService;
541
+ const handle = yield* manager.spawn(durable, {
542
+ ...(durable.name !== undefined ? { name: durable.name } : {}),
543
+ ...options,
544
+ });
545
+ yield* handle.submitInput(input);
546
+ return handle;
547
+ });
548
+
491
549
  /**
492
550
  * New process is spawned.
493
551
  */
@@ -0,0 +1,71 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ // @import-as-namespace
6
+
7
+ import * as Context from 'effect/Context';
8
+ import * as Effect from 'effect/Effect';
9
+ import type * as Scope from 'effect/Scope';
10
+
11
+ import { BaseError } from '@dxos/errors';
12
+
13
+ /** Raised when an operating-system process cannot be started. */
14
+ export class ShellError extends BaseError.extend('ShellError', 'Shell command failed to start') {}
15
+
16
+ export type SpawnOptions = {
17
+ /** Executable to run, resolved against `PATH` when not a path. */
18
+ readonly command: string;
19
+ readonly args?: readonly string[];
20
+ /** Working directory; the host's own when unset. */
21
+ readonly cwd?: string;
22
+ /** Added to the host's environment, overriding what it already has. */
23
+ readonly env?: Readonly<Record<string, string>>;
24
+ };
25
+
26
+ export type ExecOptions = Omit<SpawnOptions, 'command' | 'args'> & {
27
+ /** A bash script, run as `bash -c <script>`. */
28
+ readonly script: string;
29
+ };
30
+
31
+ export type ExecResult = {
32
+ /** `null` when a signal ended the command. */
33
+ readonly exitCode: number | null;
34
+ readonly stdout: string;
35
+ readonly stderr: string;
36
+ };
37
+
38
+ /** A running operating-system process and its standard streams. */
39
+ export interface Child {
40
+ readonly pid: number | undefined;
41
+ readonly stdin: WritableStream<Uint8Array>;
42
+ readonly stdout: ReadableStream<Uint8Array>;
43
+ readonly stderr: ReadableStream<Uint8Array>;
44
+ /** Completes with the exit code once the process exits; `null` when a signal ended it. */
45
+ readonly exited: Effect.Effect<number | null>;
46
+ /** Ends the process; a process that already exited is left as it is. */
47
+ readonly kill: Effect.Effect<void>;
48
+ }
49
+
50
+ export interface Service {
51
+ /**
52
+ * Starts `options.command` as an operating-system process. It is killed when the scope closes or
53
+ * when the compute process that owns this service ends, whichever comes first.
54
+ */
55
+ spawn(options: SpawnOptions): Effect.Effect<Child, ShellError, Scope.Scope>;
56
+ /** Runs a bash script to completion and collects its output. */
57
+ exec(options: ExecOptions): Effect.Effect<ExecResult, ShellError>;
58
+ }
59
+
60
+ /**
61
+ * Runs operating-system processes and bash commands for a compute process. Each compute process gets
62
+ * its own instance (process affinity), so nothing it started outlives it. Only a host that can start
63
+ * one provides it (the desktop app, the vite dev server, a CLI); a process that requires it runs nowhere else.
64
+ */
65
+ export class ShellService extends Context.Service<ShellService, Service>()('@dxos/compute/ShellService') {}
66
+
67
+ export const spawn = (options: SpawnOptions): Effect.Effect<Child, ShellError, ShellService | Scope.Scope> =>
68
+ ShellService.use((service) => service.spawn(options));
69
+
70
+ export const exec = (options: ExecOptions): Effect.Effect<ExecResult, ShellError, ShellService> =>
71
+ ShellService.use((service) => service.exec(options));
package/src/errors.ts CHANGED
@@ -67,6 +67,13 @@ export class InvalidOperationOutputError extends BaseError.extend(
67
67
 
68
68
  export class TriggerStateNotFoundError extends BaseError.extend('TriggerStateNotFound', 'Trigger state not found') {}
69
69
 
70
+ /** A manual run was asked of a trigger that is switched off; nothing was dispatched. */
71
+ export class TriggerDisabledError extends BaseError.extend('TriggerDisabledError', 'Trigger is disabled') {
72
+ constructor(triggerId: string) {
73
+ super({ context: { triggerId } });
74
+ }
75
+ }
76
+
70
77
  /**
71
78
  * Raised when the upstream AI gateway responds with a structured JSON error envelope
72
79
  * (`{ "type": "error", "error": { "type": ..., "message": ... } }`). Surfaces as a typed
package/src/index.ts CHANGED
@@ -18,6 +18,7 @@ export * as OperationRegistry from './OperationRegistry.ts';
18
18
  export * as Process from './Process.ts';
19
19
  export * as Runnable from './Runnable.ts';
20
20
  export * as ServiceResolver from './ServiceResolver.ts';
21
+ export * as ShellService from './ShellService.ts';
21
22
  export * as SqlService from './SqlService.ts';
22
23
  export * as StorageService from './StorageService.ts';
23
24
  export * as Trace from './Trace.ts';
@@ -65,6 +65,17 @@ export class Project extends Type.makeObject<Project>(DXN.make('org.dxos.type.pr
65
65
  // TODO(burdon): Change to array? Move into taskSet?
66
66
  repo: Schema.optional(Ref.Ref(Repo.Repo).annotate({ title: 'Repository' })),
67
67
 
68
+ /**
69
+ * Repositories the project's coding agents work on. A cloud session checks each one out in its
70
+ * sandbox; a session on this computer works in the folder chosen for the project there.
71
+ */
72
+ repositories: Schema.optional(
73
+ Schema.Array(Ref.Ref(Repo.Repo)).annotate({
74
+ title: 'Repositories',
75
+ description: 'Checked out for coding agents that run in the cloud.',
76
+ }),
77
+ ),
78
+
68
79
  /** The default configuration (model, ...) for the sessions this project starts; a chat's own overrides it. */
69
80
  session: Schema.optional(SessionConfig.SessionConfig.pipe(Annotation.FormInputAnnotation.set(false))),
70
81
  }).pipe(
@@ -121,8 +121,17 @@ export const Hook = Schema.Struct({
121
121
  * Must match the function's input schema.
122
122
  */
123
123
  input: Trigger.InputTemplate.pipe(Annotation.FormInputAnnotation.set(false), Schema.optional),
124
+
125
+ /**
126
+ * Run in the background (end-request only): the request settles without waiting for the hook, and
127
+ * the agent process awaits it before it finishes. For work the user should not wait on, e.g.
128
+ * reviewing the turn that just ended.
129
+ */
130
+ async: Schema.optional(Schema.Boolean.annotate({ title: 'Run in background' })),
124
131
  });
125
132
 
133
+ export type Hook = Schema.Schema.Type<typeof Hook>;
134
+
126
135
  /**
127
136
  * Create a new Skill.
128
137
  * The `key` (and optional `version`) are stored in the object meta.
@@ -14,6 +14,7 @@ import { Annotation, DXN, Feed, Obj, type Query, QueryAST, Ref, Type } from '@dx
14
14
  import { OptionsAnnotationId } from '@dxos/echo/Format';
15
15
  import * as SchemaAST from '@dxos/effect/SchemaAST';
16
16
 
17
+ import type { TriggerDisabledError } from '../errors.ts';
17
18
  import * as Runnable from '../Runnable.ts';
18
19
  import type * as TriggerEvent from './TriggerEvent.ts';
19
20
 
@@ -288,9 +289,9 @@ export interface InvokeOptions {
288
289
  }
289
290
 
290
291
  /**
291
- * Service for monitoring trigger dispatcher state.
292
+ * Observes and invokes the triggers registered with the dispatchers (local and edge).
292
293
  */
293
- export interface Monitor {
294
+ export interface Manager {
294
295
  /**
295
296
  * Triggers actively registered in the dispatcher.
296
297
  * Could contain entries for both local and edge triggers, but only the edge ones are actually running.
@@ -303,15 +304,16 @@ export interface Monitor {
303
304
  * Invoke a trigger.
304
305
  * Available only for direct and timer triggers.
305
306
  * Invocation respects the trigger's concurrency limit.
307
+ * Fails with {@link TriggerDisabledError} for a trigger that is switched off.
306
308
  */
307
- readonly invokeTrigger: (options: InvokeOptions) => Effect.Effect<void>;
309
+ readonly invokeTrigger: (options: InvokeOptions) => Effect.Effect<void, TriggerDisabledError>;
308
310
  }
309
311
 
310
312
  /**
311
- * Service for monitoring trigger executions.
313
+ * Service providing the {@link Manager}.
312
314
  */
313
- export class TriggerMonitorService extends Context.Service<TriggerMonitorService, Monitor>()(
314
- '@dxos/functions/TriggerMonitorService',
315
+ export class ManagerService extends Context.Service<ManagerService, Manager>()(
316
+ '@dxos/compute/Trigger.ManagerService',
315
317
  ) {}
316
318
 
317
- export { TriggerStateNotFoundError } from '../errors.ts';
319
+ export { TriggerDisabledError, TriggerStateNotFoundError } from '../errors.ts';