@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.
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/compute",
|
|
3
|
-
"version": "0.
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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.
|
|
47
|
-
"@dxos/echo": "0.
|
|
48
|
-
"@dxos/effect": "0.
|
|
49
|
-
"@dxos/
|
|
50
|
-
"@dxos/
|
|
51
|
-
"@dxos/
|
|
52
|
-
"@dxos/log": "0.
|
|
53
|
-
"@dxos/
|
|
54
|
-
"@dxos/schema": "0.
|
|
55
|
-
"@dxos/
|
|
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.
|
|
60
|
-
"@dxos/echo-client": "0.
|
|
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
|
+
);
|
package/src/AgentService.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
495
|
-
*
|
|
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:
|
|
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
|
+
});
|