@dxos/compute 0.9.1-staging.ee54ba693a → 0.11.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 (131) hide show
  1. package/dist/lib/AgentService.mjs +16 -0
  2. package/dist/lib/AgentService.mjs.map +1 -0
  3. package/dist/lib/StorageService.mjs +59 -0
  4. package/dist/lib/StorageService.mjs.map +1 -0
  5. package/dist/lib/chunk-OperationHandlerSet.mjs +660 -0
  6. package/dist/lib/chunk-OperationHandlerSet.mjs.map +1 -0
  7. package/dist/lib/chunk-definitions.mjs +41 -0
  8. package/dist/lib/chunk-definitions.mjs.map +1 -0
  9. package/dist/lib/chunk-rolldown-runtime.mjs +13 -0
  10. package/dist/lib/index.mjs +1576 -0
  11. package/dist/lib/index.mjs.map +1 -0
  12. package/dist/lib/testing.mjs +43 -0
  13. package/dist/lib/testing.mjs.map +1 -0
  14. package/dist/types/src/AgentIdentity.d.ts +27 -0
  15. package/dist/types/src/AgentIdentity.d.ts.map +1 -0
  16. package/dist/types/src/AgentService.d.ts +10 -2
  17. package/dist/types/src/AgentService.d.ts.map +1 -1
  18. package/dist/types/src/Cancellation.d.ts +19 -0
  19. package/dist/types/src/Cancellation.d.ts.map +1 -0
  20. package/dist/types/src/Credential.d.ts +41 -0
  21. package/dist/types/src/Credential.d.ts.map +1 -1
  22. package/dist/types/src/Operation.d.ts +79 -1
  23. package/dist/types/src/Operation.d.ts.map +1 -1
  24. package/dist/types/src/Process.d.ts +15 -3
  25. package/dist/types/src/Process.d.ts.map +1 -1
  26. package/dist/types/src/Trace.d.ts +55 -3
  27. package/dist/types/src/Trace.d.ts.map +1 -1
  28. package/dist/types/src/Trace.test.d.ts +2 -0
  29. package/dist/types/src/Trace.test.d.ts.map +1 -0
  30. package/dist/types/src/errors.d.ts +35 -1
  31. package/dist/types/src/errors.d.ts.map +1 -1
  32. package/dist/types/src/index.d.ts +3 -6
  33. package/dist/types/src/index.d.ts.map +1 -1
  34. package/dist/types/src/testing/index.d.ts +1 -0
  35. package/dist/types/src/testing/index.d.ts.map +1 -1
  36. package/dist/types/src/testing/operation.d.ts +5 -0
  37. package/dist/types/src/testing/operation.d.ts.map +1 -0
  38. package/dist/types/src/{Instructions.d.ts → types/Instructions.d.ts} +14 -1
  39. package/dist/types/src/types/Instructions.d.ts.map +1 -0
  40. package/dist/types/src/types/Instructions.test.d.ts +2 -0
  41. package/dist/types/src/types/Instructions.test.d.ts.map +1 -0
  42. package/dist/types/src/types/Project.d.ts +53 -0
  43. package/dist/types/src/types/Project.d.ts.map +1 -0
  44. package/dist/types/src/types/Project.test.d.ts +2 -0
  45. package/dist/types/src/types/Project.test.d.ts.map +1 -0
  46. package/dist/types/src/types/Routine.d.ts +42 -0
  47. package/dist/types/src/types/Routine.d.ts.map +1 -0
  48. package/dist/types/src/types/Routine.test.d.ts +2 -0
  49. package/dist/types/src/types/Routine.test.d.ts.map +1 -0
  50. package/dist/types/src/types/Script.d.ts.map +1 -0
  51. package/dist/types/src/{Skill.d.ts → types/Skill.d.ts} +1 -1
  52. package/dist/types/src/types/Skill.d.ts.map +1 -0
  53. package/dist/types/src/{Template.d.ts → types/Template.d.ts} +2 -2
  54. package/dist/types/src/types/Template.d.ts.map +1 -0
  55. package/dist/types/src/types/Template.test.d.ts.map +1 -0
  56. package/dist/types/src/types/Trigger.d.ts +249 -0
  57. package/dist/types/src/types/Trigger.d.ts.map +1 -0
  58. package/dist/types/src/{TriggerEvent.d.ts → types/TriggerEvent.d.ts} +19 -6
  59. package/dist/types/src/types/TriggerEvent.d.ts.map +1 -0
  60. package/dist/types/src/types/index.d.ts +9 -0
  61. package/dist/types/src/types/index.d.ts.map +1 -0
  62. package/dist/types/tsconfig.tsbuildinfo +1 -1
  63. package/package.json +18 -17
  64. package/src/AgentIdentity.ts +35 -0
  65. package/src/AgentService.ts +10 -2
  66. package/src/Cancellation.ts +33 -0
  67. package/src/Credential.ts +52 -0
  68. package/src/Operation.ts +92 -2
  69. package/src/Process.ts +21 -4
  70. package/src/Trace.test.ts +155 -0
  71. package/src/Trace.ts +207 -6
  72. package/src/errors.ts +9 -5
  73. package/src/index.ts +3 -6
  74. package/src/testing/index.ts +1 -0
  75. package/src/testing/operation.ts +21 -0
  76. package/src/types/Instructions.test.ts +18 -0
  77. package/src/{Instructions.ts → types/Instructions.ts} +22 -1
  78. package/src/types/Project.test.ts +52 -0
  79. package/src/types/Project.ts +99 -0
  80. package/src/types/Routine.test.ts +18 -0
  81. package/src/types/Routine.ts +85 -0
  82. package/src/{Skill.ts → types/Skill.ts} +2 -2
  83. package/src/{Template.test.ts → types/Template.test.ts} +2 -2
  84. package/src/{Template.ts → types/Template.ts} +2 -2
  85. package/src/{Trigger.ts → types/Trigger.ts} +126 -8
  86. package/src/{TriggerEvent.ts → types/TriggerEvent.ts} +23 -5
  87. package/src/types/index.ts +12 -0
  88. package/dist/lib/neutral/AgentService.mjs +0 -12
  89. package/dist/lib/neutral/AgentService.mjs.map +0 -7
  90. package/dist/lib/neutral/StorageService.mjs +0 -22
  91. package/dist/lib/neutral/StorageService.mjs.map +0 -7
  92. package/dist/lib/neutral/chunk-2ZN64ZS7.mjs +0 -25
  93. package/dist/lib/neutral/chunk-2ZN64ZS7.mjs.map +0 -7
  94. package/dist/lib/neutral/chunk-5VAXF6AH.mjs +0 -53
  95. package/dist/lib/neutral/chunk-5VAXF6AH.mjs.map +0 -7
  96. package/dist/lib/neutral/chunk-EVROJER7.mjs +0 -285
  97. package/dist/lib/neutral/chunk-EVROJER7.mjs.map +0 -7
  98. package/dist/lib/neutral/chunk-J5LGTIGS.mjs +0 -10
  99. package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +0 -7
  100. package/dist/lib/neutral/chunk-PF7XP7XP.mjs +0 -62
  101. package/dist/lib/neutral/chunk-PF7XP7XP.mjs.map +0 -7
  102. package/dist/lib/neutral/chunk-SJ37RWHU.mjs +0 -21
  103. package/dist/lib/neutral/chunk-SJ37RWHU.mjs.map +0 -7
  104. package/dist/lib/neutral/chunk-VSAZPWSJ.mjs +0 -25
  105. package/dist/lib/neutral/chunk-VSAZPWSJ.mjs.map +0 -7
  106. package/dist/lib/neutral/chunk-WEO4FCQY.mjs +0 -17
  107. package/dist/lib/neutral/chunk-WEO4FCQY.mjs.map +0 -7
  108. package/dist/lib/neutral/chunk-Z3Y7FQGN.mjs +0 -174
  109. package/dist/lib/neutral/chunk-Z3Y7FQGN.mjs.map +0 -7
  110. package/dist/lib/neutral/fib-5XIYECL5.mjs +0 -10
  111. package/dist/lib/neutral/fib-5XIYECL5.mjs.map +0 -7
  112. package/dist/lib/neutral/index.mjs +0 -1232
  113. package/dist/lib/neutral/index.mjs.map +0 -7
  114. package/dist/lib/neutral/meta.json +0 -1
  115. package/dist/lib/neutral/reply-GCBRCUVM.mjs +0 -10
  116. package/dist/lib/neutral/reply-GCBRCUVM.mjs.map +0 -7
  117. package/dist/lib/neutral/sleep-XR6CL6W3.mjs +0 -10
  118. package/dist/lib/neutral/sleep-XR6CL6W3.mjs.map +0 -7
  119. package/dist/lib/neutral/testing/index.mjs +0 -32
  120. package/dist/lib/neutral/testing/index.mjs.map +0 -7
  121. package/dist/types/src/Instructions.d.ts.map +0 -1
  122. package/dist/types/src/Script.d.ts.map +0 -1
  123. package/dist/types/src/Skill.d.ts.map +0 -1
  124. package/dist/types/src/Template.d.ts.map +0 -1
  125. package/dist/types/src/Template.test.d.ts.map +0 -1
  126. package/dist/types/src/Trigger.d.ts +0 -157
  127. package/dist/types/src/Trigger.d.ts.map +0 -1
  128. package/dist/types/src/TriggerEvent.d.ts.map +0 -1
  129. /package/dist/types/src/{Script.d.ts → types/Script.d.ts} +0 -0
  130. /package/dist/types/src/{Template.test.d.ts → types/Template.test.d.ts} +0 -0
  131. /package/src/{Script.ts → types/Script.ts} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/compute",
3
- "version": "0.9.1-staging.ee54ba693a",
3
+ "version": "0.11.0",
4
4
  "description": "Umbrella package for AI-aware DXOS compute primitives.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -16,22 +16,22 @@
16
16
  ".": {
17
17
  "source": "./src/index.ts",
18
18
  "types": "./dist/types/src/index.d.ts",
19
- "default": "./dist/lib/neutral/index.mjs"
19
+ "import": "./dist/lib/index.mjs"
20
20
  },
21
21
  "./AgentService": {
22
22
  "source": "./src/AgentService.ts",
23
23
  "types": "./dist/types/src/AgentService.d.ts",
24
- "default": "./dist/lib/neutral/AgentService.mjs"
24
+ "import": "./dist/lib/AgentService.mjs"
25
25
  },
26
26
  "./StorageService": {
27
27
  "source": "./src/StorageService.ts",
28
28
  "types": "./dist/types/src/StorageService.d.ts",
29
- "default": "./dist/lib/neutral/StorageService.mjs"
29
+ "import": "./dist/lib/StorageService.mjs"
30
30
  },
31
31
  "./testing": {
32
32
  "source": "./src/testing/index.ts",
33
33
  "types": "./dist/types/src/testing/index.d.ts",
34
- "default": "./dist/lib/neutral/testing/index.mjs"
34
+ "import": "./dist/lib/testing.mjs"
35
35
  }
36
36
  },
37
37
  "types": "dist/types/src/index.d.ts",
@@ -43,21 +43,22 @@
43
43
  "@effect-atom/atom": "^0.5.3",
44
44
  "@effect/platform": "0.96.2",
45
45
  "@effect/rpc": "0.75.1",
46
- "@dxos/ai": "0.9.1-staging.ee54ba693a",
47
- "@dxos/echo": "0.9.1-staging.ee54ba693a",
48
- "@dxos/effect": "0.9.1-staging.ee54ba693a",
49
- "@dxos/invariant": "0.9.1-staging.ee54ba693a",
50
- "@dxos/keys": "0.9.1-staging.ee54ba693a",
51
- "@dxos/errors": "0.9.1-staging.ee54ba693a",
52
- "@dxos/log": "0.9.1-staging.ee54ba693a",
53
- "@dxos/vendor-kbn-handlebars": "0.9.1-staging.ee54ba693a",
54
- "@dxos/schema": "0.9.1-staging.ee54ba693a",
55
- "@dxos/protocols": "0.9.1-staging.ee54ba693a"
46
+ "@dxos/ai": "0.11.0",
47
+ "@dxos/echo": "0.11.0",
48
+ "@dxos/effect": "0.11.0",
49
+ "@dxos/errors": "0.11.0",
50
+ "@dxos/invariant": "0.11.0",
51
+ "@dxos/keys": "0.11.0",
52
+ "@dxos/log": "0.11.0",
53
+ "@dxos/protocols": "0.11.0",
54
+ "@dxos/schema": "0.11.0",
55
+ "@dxos/types": "0.11.0",
56
+ "@dxos/vendor-kbn-handlebars": "0.11.0"
56
57
  },
57
58
  "devDependencies": {
58
59
  "effect": "3.21.4",
59
- "vitest": "4.1.8",
60
- "@dxos/echo-client": "0.9.1-staging.ee54ba693a"
60
+ "vitest": "4.1.10",
61
+ "@dxos/echo-client": "0.11.0"
61
62
  },
62
63
  "peerDependencies": {
63
64
  "effect": "3.21.4"
@@ -0,0 +1,35 @@
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 * as Option from 'effect/Option';
10
+
11
+ /**
12
+ * Identity of the actor authoring content within an operation. Today this is an agent (see
13
+ * `assistant-toolkit` `Agent.did`); it attributes content the operation authors — e.g. the
14
+ * `creator` of a suggestion branch — so it reads as its own author, distinct from the human user
15
+ * and from other agents. Provided by the agent runtime for the duration of a tool/operation call.
16
+ */
17
+ export interface Identity {
18
+ /** Identity DID (e.g. an agent's `did:halo:` DID). */
19
+ readonly did: string;
20
+ /** Optional display name. */
21
+ readonly name?: string;
22
+ /** Optional palette hue. */
23
+ readonly hue?: string;
24
+ }
25
+
26
+ export class AgentIdentity extends Context.Tag('@dxos/compute/AgentIdentity')<AgentIdentity, Identity>() {}
27
+
28
+ /**
29
+ * The current author DID, or `undefined` when no {@link AgentIdentity} is in scope (e.g. a human-
30
+ * initiated invocation). Reads optionally, so an operation that defaults its author from the agent
31
+ * identity does not add a hard service requirement.
32
+ */
33
+ export const currentDid: Effect.Effect<string | undefined> = Effect.serviceOption(AgentIdentity).pipe(
34
+ Effect.map(Option.match({ onNone: () => undefined, onSome: (identity) => identity.did })),
35
+ );
@@ -10,8 +10,10 @@ import type * as Stream from 'effect/Stream';
10
10
 
11
11
  import type { Database, Feed, Obj, Ref } from '@dxos/echo';
12
12
  import { DXN } from '@dxos/keys';
13
+ import type { ContentBlock } from '@dxos/types';
13
14
 
14
15
  import type * as Trace from './Trace';
16
+ import { Instructions } from './types';
15
17
 
16
18
  /**
17
19
  * Service interface for the agent session manager.
@@ -52,9 +54,9 @@ export interface Session {
52
54
  addContext: (context: Ref.Ref<Obj.Unknown>[]) => Effect.Effect<void, never, Database.Service>;
53
55
 
54
56
  /**
55
- * Submits a prompt to the agent.
57
+ * Submit a turn: a plain user prompt, or pre-built content blocks (e.g. synthetic context + prompt).
56
58
  */
57
- submitPrompt: (prompt: string) => Effect.Effect<void>;
59
+ submitPrompt: (prompt: string | ContentBlock.Any[]) => Effect.Effect<void>;
58
60
 
59
61
  /**
60
62
  * Wait until agent has completed its work.
@@ -87,4 +89,10 @@ export interface GetSessionOptions {
87
89
  // the model into the agent process — the id alone does not identify a resolver.
88
90
  readonly provider?: DXN.DXN;
89
91
  readonly systemPrompt?: string;
92
+ /**
93
+ * Instructions steering the conversation (typically the Chat's `instructions` ref), persisted as a
94
+ * spawn annotation so a re-hydrated process recovers it. Read at spawn only: repointing requires a
95
+ * process restart (same staleness model as `model`/`provider`).
96
+ */
97
+ readonly instructions?: Ref.Ref<Instructions.Instructions>;
90
98
  }
@@ -0,0 +1,33 @@
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 * as Option from 'effect/Option';
10
+
11
+ /**
12
+ * Cooperative cancellation of the current operation run. The runtime that owns execution provides
13
+ * the service and fires the signal when the run is cancelled — the EDGE worker on a client cancel
14
+ * request, the local process runtime on terminate. Operations observe it via {@link signal}.
15
+ */
16
+ // Tags unify by key string: a version-skewed bundle carrying two `@dxos/compute` instances (e.g. an
17
+ // EDGE worker whose provider and handlers resolve different copies) must still share this service,
18
+ // so the key must never change.
19
+ export class Service extends Context.Tag('@dxos/compute/Cancellation')<Service, { readonly signal: AbortSignal }>() {}
20
+
21
+ /**
22
+ * The current run's cancellation signal — e.g. for `Pipeline.abortWith` or `fetch`. Never fires when
23
+ * the runtime provided no cancellation channel.
24
+ */
25
+ // A fresh signal per access: a shared never-firing singleton would accumulate listeners across runs.
26
+ export const signal: Effect.Effect<AbortSignal> = Effect.serviceOption(Service).pipe(
27
+ Effect.map(
28
+ Option.match({
29
+ onNone: () => new AbortController().signal,
30
+ onSome: (service) => service.signal,
31
+ }),
32
+ ),
33
+ );
package/src/Credential.ts CHANGED
@@ -6,10 +6,19 @@
6
6
 
7
7
  import * as Context from 'effect/Context';
8
8
  import * as Effect from 'effect/Effect';
9
+ import * as Layer from 'effect/Layer';
9
10
  import * as Redacted from 'effect/Redacted';
10
11
 
12
+ import { type SpaceId } from '@dxos/keys';
13
+
11
14
  export type CredentialQuery = {
12
15
  service?: string;
16
+ /**
17
+ * Look up one specific `AccessToken` object instead of any credential for a service. Required
18
+ * wherever a space can hold several connections to the same service, since a by-service lookup
19
+ * picks among them arbitrarily.
20
+ */
21
+ accessTokenId?: string;
13
22
  };
14
23
 
15
24
  // TODO(dmaretskyi): Unify with other apis.
@@ -24,6 +33,42 @@ export type ServiceCredential = {
24
33
  account?: string;
25
34
  };
26
35
 
36
+ /**
37
+ * Source for credentials EDGE custodies rather than replicating into the space (those stored as
38
+ * `MANAGED_ACCESS_TOKEN`).
39
+ *
40
+ * An implementation detail of {@link CredentialsService}, not a consumer-facing service: it exists
41
+ * as a tag only because the two surfaces reach EDGE differently — clients over authenticated HTTP,
42
+ * functions over a space-bound service binding. Implementations cache; pass `refresh` to bypass that
43
+ * cache after an authorization failure.
44
+ */
45
+ export class AccessTokenResolver extends Context.Tag('@dxos/functions/AccessTokenResolver')<
46
+ AccessTokenResolver,
47
+ {
48
+ resolve: (request: { spaceId: SpaceId; accessTokenId: string; refresh?: boolean }) => Promise<string>;
49
+ }
50
+ >() {
51
+ static resolve = (request: {
52
+ spaceId: SpaceId;
53
+ accessTokenId: string;
54
+ refresh?: boolean;
55
+ }): Effect.Effect<string, never, AccessTokenResolver> =>
56
+ Effect.gen(function* () {
57
+ const resolver = yield* AccessTokenResolver;
58
+ return yield* Effect.promise(() => resolver.resolve(request));
59
+ });
60
+
61
+ /**
62
+ * Fails on every managed token. The default where EDGE is unreachable (tests, offline CLI runs):
63
+ * a managed credential genuinely cannot be resolved there, and failing says so.
64
+ */
65
+ static notAvailable = Layer.succeed(AccessTokenResolver, {
66
+ resolve: async ({ accessTokenId }) => {
67
+ throw new Error(`No access token resolver configured; cannot resolve managed token: ${accessTokenId}`);
68
+ },
69
+ });
70
+ }
71
+
27
72
  export class CredentialsService extends Context.Tag('@dxos/functions/CredentialsService')<
28
73
  CredentialsService,
29
74
  {
@@ -54,3 +99,10 @@ export class CredentialsService extends Context.Tag('@dxos/functions/Credentials
54
99
  return Redacted.make(credential.apiKey);
55
100
  });
56
101
  }
102
+
103
+ /**
104
+ * The credential's API key as a plain string — what a caller passing it to an HTTP client needs.
105
+ * Resolves a server-custodied value transparently, so callers never branch on where it came from.
106
+ */
107
+ export const getApiKeyValue = (query: CredentialQuery): Effect.Effect<string, never, CredentialsService> =>
108
+ Effect.map(CredentialsService.getApiKey(query), Redacted.value);
package/src/Operation.ts CHANGED
@@ -4,6 +4,7 @@
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
10
  import * as Layer from 'effect/Layer';
@@ -16,7 +17,7 @@ import type * as Types from 'effect/Types';
16
17
  import { Annotation, DXN, JsonSchema, type Key, Migration, Obj, Ref, Type } from '@dxos/echo';
17
18
  import type { URI } from '@dxos/keys';
18
19
 
19
- import type { NoHandlerError } from './errors';
20
+ import { type NoHandlerError, RunAgainError } from './errors';
20
21
  import type { Operation } from './index';
21
22
 
22
23
  /**
@@ -324,7 +325,7 @@ export class PersistentOperation extends Type.makeObject<PersistentOperation>(
324
325
 
325
326
  /**
326
327
  * List of required services.
327
- * Match the Context.Tag keys of the FunctionServices variants.
328
+ * Match the Context.Tag keys of the services the operation handler declares.
328
329
  */
329
330
  services: Schema$.optional(Schema$.Array(Schema$.String)),
330
331
 
@@ -473,6 +474,30 @@ export const NotifyOptionsAnnotation = Annotation.make({
473
474
  schema: NotifyOptions,
474
475
  });
475
476
 
477
+ /**
478
+ * A serializable reference to an operation invocation — the target operation's key plus its input.
479
+ * Describes a deferred/late-bound invocation that must survive a serialization boundary (e.g. riding
480
+ * on a process-failure error, or persisted) where a live {@link Definition} cannot. Resolve the key
481
+ * with `OperationHandlerSet.getHandlerByKey` and hand the result to the invoker. Carries no functions,
482
+ * so `input` must itself be serializable.
483
+ */
484
+ export interface SerializedInvocation {
485
+ /** Target operation key as a URI (a DXN, e.g. `dxn:org.dxos.plugin.deck.operation.open`). */
486
+ readonly operation: URI.URI;
487
+ /** Input passed to the operation; must be serializable. */
488
+ readonly input?: unknown;
489
+ }
490
+
491
+ /**
492
+ * Builds a {@link SerializedInvocation} from a live {@link Definition} and its input — the serializable
493
+ * counterpart to a direct `invoke`, for describing an invocation that must cross a serialization
494
+ * boundary (e.g. a deferred toast action). Resolve it later with `OperationHandlerSet.getHandlerByKey`.
495
+ */
496
+ export const prepare = <I, O>(operation: Definition<I, O>, input: I): SerializedInvocation => ({
497
+ operation: operation.meta.key,
498
+ input,
499
+ });
500
+
476
501
  /**
477
502
  * Options for operation invocation.
478
503
  */
@@ -496,6 +521,12 @@ export interface InvokeOptions {
496
521
  * Optional process-runtime tracing metadata (consumed by `@dxos/functions-runtime` when wired).
497
522
  */
498
523
  tracing?: unknown;
524
+
525
+ /**
526
+ * Specifies the runtime environment for the operation.
527
+ * By default, the operation is executed on the local runtime.
528
+ */
529
+ on?: 'edge' | 'local';
499
530
  }
500
531
 
501
532
  /**
@@ -560,6 +591,57 @@ export const visible = annotate(VisibleAnnotation, true);
560
591
  export const isVisible = (op: PersistentOperation): boolean =>
561
592
  Option.getOrElse(Annotation.get(op, VisibleAnnotation), () => false);
562
593
 
594
+ /**
595
+ * Projection marker for an operation exposed as an MCP tool to external agents.
596
+ * See plugin-projects `MILESTONE-5.md` §7.4 for the full contract.
597
+ */
598
+ export const McpTool = Schema$.Struct({
599
+ /** Tool name as exposed to MCP clients; camelCase, domain-prefixed (e.g. `taskCreate`). */
600
+ name: Schema$.String,
601
+ /** Model-facing description; falls back to the operation's own description when absent. */
602
+ description: Schema$.optional(Schema$.String),
603
+ /**
604
+ * Safety class the server maps to MCP tool hints: `read` is side-effect free (readOnlyHint),
605
+ * `write` mutates space data, `destructive` deletes or is otherwise irreversible.
606
+ */
607
+ safety: Schema$.Literal('read', 'write', 'destructive'),
608
+ /** Aspect/toolset, for server-side filtering (e.g. `/mcp?toolsets=tasks`). */
609
+ aspect: Schema$.optional(Schema$.String),
610
+ });
611
+ export type McpTool = Schema$.Schema.Type<typeof McpTool>;
612
+
613
+ /**
614
+ * Annotation that opts an operation into MCP projection. The annotation rides through
615
+ * {@link serialize} into the persisted record, so a remote projector (edge mcp-space-service)
616
+ * discovers tools from the operation registry rather than a hand-maintained table.
617
+ *
618
+ * Projected operations must be remotely invocable: refs (not live objects) in, JSON snapshots
619
+ * out, schemas that survive serialization, and worker-safe handlers — MILESTONE-5.md §7.4.
620
+ */
621
+ export const McpToolAnnotation = Annotation.make({
622
+ id: 'org.dxos.operation.mcp-tool',
623
+ schema: McpTool,
624
+ });
625
+
626
+ /**
627
+ * Pipeable combinator that opts an operation into MCP projection. Apply at the definition site:
628
+ * `Operation.make({ ... }).pipe(Operation.mcpTool({ name: 'taskComplete', safety: 'write' }))`.
629
+ */
630
+ export const mcpTool = (props: McpTool) => annotate(McpToolAnnotation, props);
631
+
632
+ /**
633
+ * Returns the MCP projection descriptor when the operation is annotated for it, else undefined.
634
+ * Reads from the persisted operation — the form the projector holds.
635
+ */
636
+ export const getMcpTool = (op: PersistentOperation): McpTool | undefined =>
637
+ Option.getOrUndefined(Annotation.get(op, McpToolAnnotation));
638
+
639
+ /**
640
+ * Pipeable combinator that marks an operation idempotent — see {@link IdempotentAnnotation}. Apply at
641
+ * the definition site: `Operation.make({ ... }).pipe(Operation.idempotent)`.
642
+ */
643
+ export const idempotent = annotate(IdempotentAnnotation, true);
644
+
563
645
  /**
564
646
  * Operation service interface - provides unified access to operation invocation and scheduling.
565
647
  * This service is automatically provided to operation handlers.
@@ -651,6 +733,14 @@ export const schedule = <I, O>(
651
733
  Effect.withSpan('Operation.schedule'),
652
734
  );
653
735
 
736
+ /**
737
+ * Call inside an operation to stop the current invocation and have the system re-run it (e.g. a capped
738
+ * sync run with more work left). Surfaces as a {@link RunAgainError} defect; the trigger dispatcher
739
+ * re-queues the same event FIFO to continue. A caller invoking via `Operation.invoke` directly gets the
740
+ * defect and does not auto-continue.
741
+ */
742
+ export const runAgain = (): Effect.Effect<never, void> => Effect.failCauseSync(() => Cause.die(new RunAgainError()));
743
+
654
744
  /**
655
745
  * Provides additional invocation options to all invocations.
656
746
  */
package/src/Process.ts CHANGED
@@ -13,12 +13,14 @@ import type * as Exit from 'effect/Exit';
13
13
  import * as Option from 'effect/Option';
14
14
  import * as Schema from 'effect/Schema';
15
15
  import * as Scope from 'effect/Scope';
16
+ import * as Stream from 'effect/Stream';
16
17
  import type * as Types from 'effect/Types';
17
18
 
18
19
  import { Annotation } from '@dxos/echo';
19
20
  import { assertArgument } from '@dxos/invariant';
20
21
  import { DXN, URI } from '@dxos/keys';
21
22
  import { log } from '@dxos/log';
23
+ import type { SerializedError } from '@dxos/protocols';
22
24
 
23
25
  import * as Operation from './Operation';
24
26
  import * as OperationHandlerSet from './OperationHandlerSet';
@@ -171,6 +173,15 @@ export const NotifyAnnotation = Annotation.make({
171
173
  schema: Operation.NotifyOptions,
172
174
  });
173
175
 
176
+ /**
177
+ * URI of the Instructions object steering the process's conversation. Persisted at spawn (like
178
+ * {@link TargetAnnotation}) so a re-hydrated process recovers its steering without reaching the Chat.
179
+ */
180
+ export const InstructionsAnnotation = Annotation.make({
181
+ id: 'org.dxos.process.instructions',
182
+ schema: URI.Schema,
183
+ });
184
+
174
185
  /**
175
186
  * Marks a process as the harness host for its conversation (discovery substrate).
176
187
  */
@@ -462,6 +473,12 @@ export interface Monitor {
462
473
  * Atom for the process tree.
463
474
  */
464
475
  processTreeAtom: Atom.Atom<readonly Info[]>;
476
+
477
+ /**
478
+ * Stream ephemeral trace messages matching `filter` (DX-1125), sourced from local in-process
479
+ * runtimes and remote runtimes broadcasting over the space swarm. Used to drive live progress UI.
480
+ */
481
+ subscribeToTraceMessages(filter: Trace.Filter): Stream.Stream<Trace.Message>;
465
482
  }
466
483
 
467
484
  export class ProcessMonitorService extends Context.Tag('@dxos/functions/ProcessMonitorService')<
@@ -491,10 +508,10 @@ export interface Info {
491
508
  readonly state: State;
492
509
 
493
510
  /**
494
- * Error of the process.
495
- * Only for process in FAILED state.
511
+ * How the process failed as a serializable {@link SerializedError} (its `context` carries any
512
+ * structured detail, e.g. a notify override), or `null` unless it is in FAILED state.
496
513
  */
497
- readonly error: string | null;
514
+ readonly error: SerializedError | null;
498
515
 
499
516
  /**
500
517
  * UNIX timestamp in milliseconds.
@@ -579,7 +596,7 @@ export const prettyProcessTree = (tree: readonly Info[]): string => {
579
596
  parts.push(node.params.name);
580
597
  }
581
598
  if (node.error != null) {
582
- parts.push(`(${node.error})`);
599
+ parts.push(`(${node.error.message ?? node.error.name ?? 'error'})`);
583
600
  }
584
601
  const { inputCount, outputCount, wallTime } = node.metrics;
585
602
  parts.push(`[in:${inputCount} out:${outputCount} wall:${Math.round(wallTime)}ms]`);
@@ -0,0 +1,155 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { describe, test } from 'vitest';
6
+
7
+ import { Ref } from '@dxos/echo';
8
+ import { EID } from '@dxos/keys';
9
+
10
+ import * as Trace from './Trace';
11
+
12
+ // DX-1125: the swarm broadcast tag/filter/wire helpers. The tag format is a cross-repo contract —
13
+ // the edge producer (functions-service) derives the same strings independently, so these lock it in.
14
+
15
+ const message = (
16
+ meta: Trace.Meta,
17
+ events: { type: string; data?: unknown }[],
18
+ isEphemeral = true,
19
+ ): Pick<Trace.MessageData, 'meta' | 'events' | 'isEphemeral'> => ({
20
+ meta,
21
+ isEphemeral,
22
+ events: events.map((event) => ({ timestamp: 0, type: event.type, data: event.data ?? {} })),
23
+ });
24
+
25
+ describe('Trace.messageToTags', () => {
26
+ test('emits a type tag per event and a key:value tag per present meta field', ({ expect }) => {
27
+ const tags = Trace.messageToTags(
28
+ message(
29
+ {
30
+ pid: 'p1',
31
+ parentPid: 'p0',
32
+ space: 'SPACE1',
33
+ runtimeName: Trace.CommonRuntimeName.edgeIntrinsic,
34
+ toolCallId: 'tc1',
35
+ },
36
+ [{ type: 'status.update' }, { type: 'operation.input' }],
37
+ ),
38
+ );
39
+
40
+ expect(tags).toEqual(
41
+ expect.arrayContaining([
42
+ 'type:status.update',
43
+ 'type:operation.input',
44
+ 'pid:p1',
45
+ 'parentPid:p0',
46
+ 'space:SPACE1',
47
+ `runtime:${Trace.CommonRuntimeName.edgeIntrinsic}`,
48
+ 'toolCall:tc1',
49
+ ]),
50
+ );
51
+ });
52
+
53
+ test('omits absent meta fields', ({ expect }) => {
54
+ const tags = Trace.messageToTags(message({ pid: 'p1' }, [{ type: 'status.update' }]));
55
+ expect(tags).toEqual(['type:status.update', 'pid:p1']);
56
+ });
57
+ });
58
+
59
+ describe('Trace.matchesFilter', () => {
60
+ const msg = message({ pid: 'p1', space: 'SPACE1' }, [{ type: 'status.update' }]);
61
+
62
+ test('empty filter matches anything', ({ expect }) => {
63
+ expect(Trace.matchesFilter(msg, {})).toBe(true);
64
+ });
65
+
66
+ test('type matches when any event has the type', ({ expect }) => {
67
+ expect(Trace.matchesFilter(msg, { type: 'status.update' })).toBe(true);
68
+ expect(Trace.matchesFilter(msg, { type: 'operation.end' })).toBe(false);
69
+ });
70
+
71
+ test('ANDs all present fields', ({ expect }) => {
72
+ expect(Trace.matchesFilter(msg, { type: 'status.update', pid: 'p1', space: 'SPACE1' })).toBe(true);
73
+ // One mismatched dimension fails the whole filter.
74
+ expect(Trace.matchesFilter(msg, { type: 'status.update', pid: 'other' })).toBe(false);
75
+ expect(Trace.matchesFilter(msg, { space: 'SPACE2' })).toBe(false);
76
+ });
77
+ });
78
+
79
+ describe('Trace.subscriptionTagForFilter', () => {
80
+ test('picks the most selective present dimension', ({ expect }) => {
81
+ // Priority: conversation > trigger > parentPid > pid > space > type.
82
+ expect(Trace.subscriptionTagForFilter({ conversation: 'dxn:c', pid: 'p1', space: 's' })).toBe('conversation:dxn:c');
83
+ expect(Trace.subscriptionTagForFilter({ pid: 'p1', space: 's' })).toBe('pid:p1');
84
+ expect(Trace.subscriptionTagForFilter({ space: 's' })).toBe('space:s');
85
+ expect(Trace.subscriptionTagForFilter({ type: 'status.update' })).toBe('type:status.update');
86
+ });
87
+
88
+ test('returns undefined for an empty filter (matches nothing at the swarm)', ({ expect }) => {
89
+ expect(Trace.subscriptionTagForFilter({})).toBeUndefined();
90
+ });
91
+ });
92
+
93
+ describe('Trace.encodeTraceMessage / decodeTraceMessage', () => {
94
+ test('round-trips meta string fields, isEphemeral, and events', ({ expect }) => {
95
+ const original = message({ pid: 'p1', space: 'SPACE1', runtimeName: Trace.CommonRuntimeName.edgeIntrinsic }, [
96
+ { type: 'status.update', data: { message: 'Syncing', progress: { key: 'k1', current: 3, total: 10 } } },
97
+ ]);
98
+
99
+ const decoded = Trace.decodeTraceMessage(Trace.encodeTraceMessage(original));
100
+
101
+ expect(decoded.isEphemeral).toBe(true);
102
+ expect(decoded.meta.pid).toBe('p1');
103
+ expect(decoded.meta.space).toBe('SPACE1');
104
+ expect(decoded.meta.runtimeName).toBe(Trace.CommonRuntimeName.edgeIntrinsic);
105
+ expect(decoded.events).toHaveLength(1);
106
+ expect(decoded.events[0].type).toBe('status.update');
107
+ expect((decoded.events[0].data as any).progress.key).toBe('k1');
108
+ // The decoded value is a real Trace.Message and still matches its filter.
109
+ expect(Trace.matchesFilter(decoded, { type: 'status.update', space: 'SPACE1' })).toBe(true);
110
+ });
111
+
112
+ test('a decoded message re-derives the same tags the producer would emit', ({ expect }) => {
113
+ const original = message({ pid: 'p9', space: 'SPACE9' }, [{ type: 'status.update' }]);
114
+ const decoded = Trace.decodeTraceMessage(Trace.encodeTraceMessage(original));
115
+ expect(Trace.messageToTags(decoded)).toEqual(['type:status.update', 'pid:p9', 'space:SPACE9']);
116
+ });
117
+
118
+ // The wire payload deliberately drops ref meta fields (`trigger`) — they travel only as envelope
119
+ // tags. A consumer that needs the trigger (e.g. cancel addressing) must get it back at decode, or
120
+ // downstream guards silently no-op (the mailbox-sync cancel bug).
121
+ test('restores meta.trigger from the envelope tags the payload encoding drops', ({ expect }) => {
122
+ const trigger = Ref.fromURI(EID.make({ entityId: 'TRIGGER1' }));
123
+ const original = message({ pid: 'p1', space: 'SPACE1', trigger }, [{ type: 'status.update' }]);
124
+ const tags = Trace.messageToTags(original);
125
+
126
+ // The payload alone loses the trigger by design.
127
+ expect(Trace.decodeTraceMessage(Trace.encodeTraceMessage(original)).meta.trigger).toBeUndefined();
128
+
129
+ // The envelope tags restore it.
130
+ const decoded = Trace.decodeTraceMessage(Trace.encodeTraceMessage(original), tags);
131
+ expect(decoded.meta.trigger?.uri.toString()).toBe(trigger.uri.toString());
132
+ expect(Trace.matchesFilter(decoded, { trigger: trigger.uri.toString() })).toBe(true);
133
+ });
134
+
135
+ test('decode without a trigger tag leaves meta.trigger undefined', ({ expect }) => {
136
+ const original = message({ pid: 'p1', space: 'SPACE1' }, [{ type: 'status.update' }]);
137
+ const decoded = Trace.decodeTraceMessage(Trace.encodeTraceMessage(original), Trace.messageToTags(original));
138
+ expect(decoded.meta.trigger).toBeUndefined();
139
+ });
140
+
141
+ // `conversation` is the other ref the payload drops, and it is what `matchesFilter` compares — so a
142
+ // conversation-filtered subscription (the coarse tag `subscriptionTagForFilter` prefers) receives
143
+ // the broadcast and then discards it unless decode restores the ref.
144
+ test('restores meta.conversation from the envelope tags', ({ expect }) => {
145
+ const conversation = Ref.fromURI(EID.make({ entityId: 'CONVERSATION1' }));
146
+ const original = message({ pid: 'p1', space: 'SPACE1', conversation }, [{ type: 'status.update' }]);
147
+ const tags = Trace.messageToTags(original);
148
+
149
+ expect(Trace.decodeTraceMessage(Trace.encodeTraceMessage(original)).meta.conversation).toBeUndefined();
150
+
151
+ const decoded = Trace.decodeTraceMessage(Trace.encodeTraceMessage(original), tags);
152
+ expect(decoded.meta.conversation?.uri.toString()).toBe(conversation.uri.toString());
153
+ expect(Trace.matchesFilter(decoded, { conversation: conversation.uri.toString() })).toBe(true);
154
+ });
155
+ });