@dxos/assistant 0.9.1-main.c7dcc2e112 → 0.10.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 (70) hide show
  1. package/dist/lib/neutral/extraction/index.mjs +1 -1
  2. package/dist/lib/neutral/extraction/index.mjs.map +1 -1
  3. package/dist/lib/neutral/index.mjs +387 -230
  4. package/dist/lib/neutral/index.mjs.map +4 -4
  5. package/dist/lib/neutral/meta.json +1 -1
  6. package/dist/types/src/extraction/extraction-llm-function/proper-noun-extraction.d.ts +1 -202
  7. package/dist/types/src/extraction/extraction-llm-function/proper-noun-extraction.d.ts.map +1 -1
  8. package/dist/types/src/index.d.ts +1 -1
  9. package/dist/types/src/index.d.ts.map +1 -1
  10. package/dist/types/src/request/AiRequest.d.ts +13 -208
  11. package/dist/types/src/request/AiRequest.d.ts.map +1 -1
  12. package/dist/types/src/request/format.d.ts +1 -1
  13. package/dist/types/src/request/format.d.ts.map +1 -1
  14. package/dist/types/src/session/AiContext.d.ts +27 -105
  15. package/dist/types/src/session/AiContext.d.ts.map +1 -1
  16. package/dist/types/src/session/AiSession.d.ts +5 -29
  17. package/dist/types/src/session/AiSession.d.ts.map +1 -1
  18. package/dist/types/src/session/Harness.d.ts +122 -0
  19. package/dist/types/src/session/Harness.d.ts.map +1 -0
  20. package/dist/types/src/session/SessionLink.d.ts +8 -11
  21. package/dist/types/src/session/SessionLink.d.ts.map +1 -1
  22. package/dist/types/src/session/SessionLoader.d.ts +2 -2
  23. package/dist/types/src/session/SessionLoader.d.ts.map +1 -1
  24. package/dist/types/src/session/SkillHooks.d.ts +28 -0
  25. package/dist/types/src/session/SkillHooks.d.ts.map +1 -0
  26. package/dist/types/src/session/harness-control.d.ts +177 -0
  27. package/dist/types/src/session/harness-control.d.ts.map +1 -0
  28. package/dist/types/src/session/index.d.ts +3 -0
  29. package/dist/types/src/session/index.d.ts.map +1 -1
  30. package/dist/types/src/session/toolkit.d.ts +4 -4
  31. package/dist/types/src/session/toolkit.d.ts.map +1 -1
  32. package/dist/types/src/templates/system.d.ts +4 -6
  33. package/dist/types/src/templates/system.d.ts.map +1 -1
  34. package/dist/types/src/tool-runtime/index.d.ts.map +1 -0
  35. package/dist/types/src/tool-runtime/services.d.ts.map +1 -0
  36. package/dist/types/src/util/artifact.d.ts.map +1 -1
  37. package/dist/types/src/util/diff.d.ts +9 -3
  38. package/dist/types/src/util/diff.d.ts.map +1 -1
  39. package/dist/types/src/util/tracing.d.ts +40 -0
  40. package/dist/types/src/util/tracing.d.ts.map +1 -1
  41. package/dist/types/tsconfig.tsbuildinfo +1 -1
  42. package/package.json +28 -25
  43. package/src/extraction/extraction-llm-function/proper-noun-extraction.ts +1 -1
  44. package/src/index.ts +1 -1
  45. package/src/request/AiRequest.ts +35 -14
  46. package/src/request/format.ts +9 -11
  47. package/src/session/AiContext.test.ts +52 -32
  48. package/src/session/AiContext.ts +73 -95
  49. package/src/session/AiSession.test.ts +4 -4
  50. package/src/session/AiSession.ts +43 -83
  51. package/src/session/Harness.ts +241 -0
  52. package/src/session/SessionLink.ts +8 -8
  53. package/src/session/SessionLoader.ts +4 -4
  54. package/src/session/SkillHooks.ts +71 -0
  55. package/src/session/harness-control.ts +30 -0
  56. package/src/session/index.ts +3 -0
  57. package/src/session/toolkit.ts +8 -8
  58. package/src/templates/instructions/format.tpl +3 -3
  59. package/src/templates/instructions/system.tpl +10 -12
  60. package/src/templates/system.ts +6 -2
  61. package/src/util/artifact.ts +9 -2
  62. package/src/util/diff.test.ts +4 -4
  63. package/src/util/diff.ts +15 -8
  64. package/src/util/tracing.ts +1 -1
  65. package/dist/types/src/functions/index.d.ts.map +0 -1
  66. package/dist/types/src/functions/services.d.ts.map +0 -1
  67. /package/dist/types/src/{functions → tool-runtime}/index.d.ts +0 -0
  68. /package/dist/types/src/{functions → tool-runtime}/services.d.ts +0 -0
  69. /package/src/{functions → tool-runtime}/index.ts +0 -0
  70. /package/src/{functions → tool-runtime}/services.ts +0 -0
@@ -0,0 +1,241 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ // @import-as-namespace
6
+
7
+ import type * as RpcClient from '@effect/rpc/RpcClient';
8
+ import * as Context from 'effect/Context';
9
+ import * as DateTime from 'effect/DateTime';
10
+ import * as Effect from 'effect/Effect';
11
+ import * as Layer from 'effect/Layer';
12
+ import * as Option from 'effect/Option';
13
+ import type * as Runtime from 'effect/Runtime';
14
+ import type * as Scope from 'effect/Scope';
15
+
16
+ import { LayerSpec, Process, ServiceNotAvailableError } from '@dxos/compute';
17
+ import { ProcessManager } from '@dxos/compute-runtime';
18
+ import { Annotation, Database, EID, Feed, Filter, Obj, type URI } from '@dxos/echo';
19
+ import { EffectEx } from '@dxos/effect';
20
+ import { BaseError } from '@dxos/errors';
21
+ import { type ContentBlock, Message } from '@dxos/types';
22
+
23
+ import * as AiContext from './AiContext';
24
+ import { type HarnessControlRpcs } from './harness-control';
25
+ import { SessionLoader } from './SessionLoader';
26
+
27
+ export interface Service {
28
+ /** The conversation {@link AiContext.Binder} (Tier A). */
29
+ binder: Effect.Effect<AiContext.Binder, NotSupportedError>;
30
+ /** The conversation message history (Tier A). */
31
+ history: Effect.Effect<Message.Message[], NotSupportedError>;
32
+ /** Objects bound to the conversation that match the filter (Tier A). */
33
+ queryContext<T extends Obj.Unknown>(filter: Filter.Filter<T>): Effect.Effect<T[], NotSupportedError>;
34
+ /** Enqueue a message into the owning host's input queue (Tier B). */
35
+ enqueueMessage(options: EnqueueMessageOptions): Effect.Effect<void, NotSupportedError>;
36
+ /** Schedule a self-wake on the owning host (Tier B). */
37
+ setAlarm(options: SetAlarmOptions): Effect.Effect<void, NotSupportedError>;
38
+ }
39
+
40
+ /**
41
+ * Provides access to the controlling harness for tools and operations that are executed by the agent.
42
+ *
43
+ * Replaces AiContextService and AiSessionService.
44
+ */
45
+ export class HarnessService extends Context.Tag('@dxos/assistant/HarnessService')<HarnessService, Service>() {}
46
+
47
+ /**
48
+ * Acess current context binder.
49
+ */
50
+ export const binder: Effect.Effect<AiContext.Binder, NotSupportedError, HarnessService> = Effect.flatMap(
51
+ HarnessService,
52
+ (service) => service.binder,
53
+ );
54
+
55
+ /**
56
+ * Query the context using a filter.
57
+ */
58
+ export const queryContext = <T extends Obj.Unknown>(
59
+ filter: Filter.Filter<T>,
60
+ ): Effect.Effect<T[], NotSupportedError, HarnessService> =>
61
+ Effect.flatMap(HarnessService, (service) => service.queryContext(filter));
62
+
63
+ interface EnqueueMessageOptions {
64
+ content: ContentBlock.Any[];
65
+
66
+ // TODO(dmaretskyi): Order: 'first' | 'last'
67
+ }
68
+
69
+ /**
70
+ * Query the session history.
71
+ */
72
+ export const history: Effect.Effect<Message.Message[], NotSupportedError, HarnessService> = Effect.flatMap(
73
+ HarnessService,
74
+ (service) => service.history,
75
+ );
76
+
77
+ /**
78
+ * Enqueue a message to the harness.
79
+ */
80
+ export const enqueueMessage = (
81
+ options: EnqueueMessageOptions,
82
+ ): Effect.Effect<void, NotSupportedError, HarnessService> =>
83
+ Effect.flatMap(HarnessService, (service) => service.enqueueMessage(options));
84
+
85
+ interface SetAlarmOptions {
86
+ at: DateTime.DateTime;
87
+ /**
88
+ * Message to send when the alarm fires.
89
+ */
90
+ message: string | null;
91
+ }
92
+
93
+ export const setAlarm = (options: SetAlarmOptions): Effect.Effect<void, NotSupportedError, HarnessService> =>
94
+ Effect.flatMap(HarnessService, (service) => service.setAlarm(options));
95
+
96
+ /**
97
+ * Binds skills and/or objects to the conversation context.
98
+ */
99
+ export const bindContext = ({
100
+ skills,
101
+ objects,
102
+ }: AiContext.BindingProps): Effect.Effect<void, NotSupportedError, HarnessService> =>
103
+ Effect.gen(function* () {
104
+ const sessionBinder = yield* binder;
105
+ yield* Effect.promise(() => sessionBinder.bind({ skills, objects }));
106
+ });
107
+
108
+ export class NotSupportedError extends BaseError.extend('NotSupportedError', 'Operation not supported') {}
109
+
110
+ /**
111
+ * Process-affinity {@link LayerSpec} that materialises {@link HarnessService} from the resolution
112
+ * context's `conversation` DXN. Merges the former `AiContext.Service` (Tier A) with live host
113
+ * dispatch (Tier B).
114
+ */
115
+ export const layerSpec: LayerSpec.LayerSpec = LayerSpec.make(
116
+ {
117
+ affinity: 'process',
118
+ requires: [Database.Service, ProcessManager.Service],
119
+ provides: [HarnessService],
120
+ },
121
+ (context) =>
122
+ Layer.scoped(
123
+ HarnessService,
124
+ Effect.gen(function* () {
125
+ if (!context.conversation) {
126
+ // LayerSpec.make requires a never-failing layer (error channel = never), so typed failure
127
+ // is not possible here. Die with ServiceNotAvailableError to signal a programming error
128
+ // (missing 'conversation' in spawn environment) that callers cannot recover from.
129
+ return yield* Effect.die(
130
+ new ServiceNotAvailableError(HarnessService.key, {
131
+ message: `Service not available: ${HarnessService.key} — process spawn is missing 'conversation' in environment (set via Operation.withInvocationOptions or ProcessManager.spawn environment)`,
132
+ }),
133
+ );
134
+ }
135
+ const conversation = context.conversation;
136
+ const processManager = yield* ProcessManager.Service;
137
+ const runtime = yield* Effect.runtime<Database.Service>();
138
+ return yield* make({ conversation, processManager, runtime });
139
+ }),
140
+ ),
141
+ );
142
+
143
+ interface MakeOptions {
144
+ conversation: URI.URI;
145
+ processManager: Context.Tag.Service<ProcessManager.Service>;
146
+ runtime: Runtime.Runtime<Database.Service>;
147
+ }
148
+
149
+ /**
150
+ * Builds the {@link Service} for a conversation-scoped resolution context. Tier A resolves the
151
+ * conversation feed / {@link AiContext.Binder} once; Tier B looks up the owning host process lazily per call
152
+ * and dispatches over its `Handle.rpc` control surface (raising {@link NotSupportedError} when no
153
+ * live host owns the conversation).
154
+ */
155
+ export const make = ({
156
+ conversation,
157
+ processManager,
158
+ runtime,
159
+ }: MakeOptions): Effect.Effect<Service, never, Scope.Scope> =>
160
+ Effect.gen(function* () {
161
+ const feed = yield* Database.resolve(EID.parse(conversation), Feed.Feed).pipe(
162
+ Effect.provide(runtime),
163
+ Effect.orDie,
164
+ );
165
+ const boundBinder = yield* EffectEx.acquireReleaseResource(() => new AiContext.Binder({ feed, runtime }));
166
+ return makeService({
167
+ feed,
168
+ runtime,
169
+ binder: boundBinder,
170
+ owningHost: lookupOwningHost(processManager, conversation),
171
+ });
172
+ });
173
+
174
+ interface FromBinderOptions {
175
+ feed: Feed.Feed;
176
+ runtime: Runtime.Runtime<Database.Service>;
177
+ binder: AiContext.Binder;
178
+ }
179
+
180
+ /**
181
+ * Builds the {@link Service} from a {@link AiContext.Binder} already opened for the conversation (e.g. the
182
+ * agent's own turn fiber, which has no `ProcessManager.Service` in scope). Tier A is fully served;
183
+ * Tier B raises {@link NotSupportedError} since the live-host control surface is not reachable here.
184
+ */
185
+ export const fromBinder = ({ feed, runtime, binder }: FromBinderOptions): Service =>
186
+ makeService({ feed, runtime, binder, owningHost: Effect.fail(new NotSupportedError()) });
187
+
188
+ interface MakeServiceOptions {
189
+ feed: Feed.Feed;
190
+ runtime: Runtime.Runtime<Database.Service>;
191
+ binder: AiContext.Binder;
192
+ owningHost: Effect.Effect<RpcClient.RpcClient<HarnessControlRpcs>, NotSupportedError>;
193
+ }
194
+
195
+ const makeService = ({ feed, runtime, binder, owningHost }: MakeServiceOptions): Service => ({
196
+ binder: Effect.succeed(binder),
197
+ history: Effect.gen(function* () {
198
+ const messages = yield* Feed.query(feed, Filter.type(Message.Message)).run;
199
+ return yield* new SessionLoader().reifyHistory(feed, messages);
200
+ }).pipe(Effect.provide(runtime)),
201
+ queryContext: <T extends Obj.Unknown>(filter: Filter.Filter<T>) =>
202
+ Effect.sync(() => {
203
+ const match = Filter.toPredicate(filter);
204
+ return binder.getObjects().filter(match);
205
+ }),
206
+ enqueueMessage: ({ content }) => owningHost.pipe(Effect.flatMap((rpc) => rpc.enqueueMessage({ content }))),
207
+ setAlarm: ({ at, message }) =>
208
+ owningHost.pipe(Effect.flatMap((rpc) => rpc.setAlarm({ at: DateTime.toUtc(at), message }))),
209
+ });
210
+
211
+ /**
212
+ * Resolves the live host owning `conversation` per call (so a process replacement — e.g. a model
213
+ * switch — is never captured stale) and exposes its `HarnessControl` RPC client.
214
+ */
215
+ const lookupOwningHost = (
216
+ processManager: Context.Tag.Service<ProcessManager.Service>,
217
+ conversation: URI.URI,
218
+ ): Effect.Effect<RpcClient.RpcClient<HarnessControlRpcs>, NotSupportedError> =>
219
+ Effect.gen(function* () {
220
+ const processes = yield* processManager.list({ target: conversation });
221
+ const host = processes.find(
222
+ (process) =>
223
+ !isTerminalProcess(process.status.state) &&
224
+ Option.getOrElse(
225
+ Annotation.getDictionary(process.params.annotations, Process.HarnessHostAnnotation),
226
+ () => false,
227
+ ),
228
+ );
229
+ if (!host) {
230
+ return yield* Effect.fail(new NotSupportedError());
231
+ }
232
+ // The host is discovered by annotation, but the HarnessControl RPC contract is fixed at the
233
+ // spawn site (agent-process declares it as its `rpcs`); narrow the variance-erased
234
+ // `RpcClient<any>` to that contract so the dispatch below is typed. No typed path exists here:
235
+ // the host handle carries `RpcClient<any>` (design spec §4.4) and the concrete definition lives
236
+ // in `@dxos/functions-runtime`, which depends on this package — importing it would cycle.
237
+ return host.rpc as unknown as RpcClient.RpcClient<HarnessControlRpcs>;
238
+ });
239
+
240
+ const isTerminalProcess = (state: Process.State): boolean =>
241
+ state === Process.State.SUCCEEDED || state === Process.State.FAILED || state === Process.State.TERMINATED;
@@ -13,11 +13,11 @@ import { DXN, Feed, Obj, Ref, Type } from '@dxos/echo';
13
13
  * When present in a feed, the messages from the referenced feed up to and including
14
14
  * `messageId` are prepended to the conversation history before each turn.
15
15
  */
16
- export const SessionLink = Schema.Struct({
17
- /** Reference to the feed of the source session. */
18
- feedRef: Ref.Ref(Feed.Feed),
19
- /** Messages from the source feed up to and including this message ID are injected. */
20
- messageId: Obj.ID,
21
- }).pipe(Type.makeObject(DXN.make('org.dxos.type.sessionLink', '0.1.0')));
22
-
23
- export type SessionLink = Type.InstanceType<typeof SessionLink>;
16
+ export class SessionLink extends Type.makeObject<SessionLink>(DXN.make('org.dxos.type.sessionLink', '0.1.0'))(
17
+ Schema.Struct({
18
+ /** Reference to the feed of the source session. */
19
+ feedRef: Ref.Ref(Feed.Feed),
20
+ /** Messages from the source feed up to and including this message ID are injected. */
21
+ messageId: Obj.ID,
22
+ }),
23
+ ) {}
@@ -4,7 +4,7 @@
4
4
 
5
5
  import * as Effect from 'effect/Effect';
6
6
 
7
- import { Feed, Filter, Obj } from '@dxos/echo';
7
+ import { Database, Feed, Filter, Obj } from '@dxos/echo';
8
8
  import { Message } from '@dxos/types';
9
9
 
10
10
  import * as SessionLink from './SessionLink';
@@ -21,9 +21,9 @@ export class SessionLoader {
21
21
  reifyHistory(
22
22
  feed: Feed.Feed,
23
23
  messages: Message.Message[],
24
- ): Effect.Effect<Message.Message[], never, Feed.FeedService> {
24
+ ): Effect.Effect<Message.Message[], never, Database.Service> {
25
25
  return Effect.gen(function* () {
26
- const links = yield* Feed.runQuery(feed, Filter.type(SessionLink.SessionLink));
26
+ const links = yield* Feed.query(feed, Filter.type(SessionLink.SessionLink)).run;
27
27
  const sessionLinks = links.filter(Obj.instanceOf(SessionLink.SessionLink));
28
28
 
29
29
  const link = sessionLinks[0];
@@ -36,7 +36,7 @@ export class SessionLoader {
36
36
  return messages;
37
37
  }
38
38
 
39
- const sourceMessages = yield* Feed.runQuery(sourceFeed, Filter.type(Message.Message));
39
+ const sourceMessages = yield* Feed.query(sourceFeed, Filter.type(Message.Message)).run;
40
40
  const filtered = sourceMessages.filter(Obj.instanceOf(Message.Message));
41
41
 
42
42
  // Sort by creation timestamp so history is in chronological order.
@@ -0,0 +1,71 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ // @import-as-namespace
6
+
7
+ import * as Effect from 'effect/Effect';
8
+
9
+ import { Operation, Skill } from '@dxos/compute';
10
+ import { Database, Obj } from '@dxos/echo';
11
+ import { invariant } from '@dxos/invariant';
12
+ import { log } from '@dxos/log';
13
+
14
+ /**
15
+ * Tag of the lifecycle point a hook fires at, matching {@link Skill.HookSpec}.
16
+ */
17
+ export type HookPhase = 'begin-request' | 'end-request';
18
+
19
+ /**
20
+ * Invokes a resolved operation with the hook's templated input. Begin and end hooks fire in
21
+ * different contexts (the agent's turn fiber vs. a child process with Tier B reachable), so the
22
+ * invocation strategy is supplied by the caller rather than fixed here. `R` carries the invoker's
23
+ * own requirements (e.g. `Operation.Service`) up into {@link runHooks}.
24
+ */
25
+ export type HookInvoker<R> = (
26
+ operation: Operation.Definition.Any,
27
+ input: Record<string, unknown>,
28
+ ) => Effect.Effect<void, never, R>;
29
+
30
+ /**
31
+ * Runs every {@link Skill.Hook} on `skills` whose spec matches `phase`. Each hook resolves its
32
+ * `function` ref to a {@link Operation.PersistentOperation}, deserializes it to a definition, and
33
+ * invokes it with the templated `input` via `invoke`. Hooks without a function ref are skipped.
34
+ *
35
+ * Hook failures are isolated: a throwing hook logs and is swallowed so one skill's hook cannot
36
+ * abort the request lifecycle (or the sibling hooks).
37
+ */
38
+ export const runHooks = <R>({
39
+ skills,
40
+ phase,
41
+ invoke,
42
+ }: {
43
+ skills: readonly Skill.Skill[];
44
+ phase: HookPhase;
45
+ invoke: HookInvoker<R>;
46
+ }): Effect.Effect<void, never, Database.Service | R> =>
47
+ Effect.gen(function* () {
48
+ for (const skill of skills) {
49
+ for (const hook of skill.hooks ?? []) {
50
+ if (hook.spec._tag !== phase) {
51
+ continue;
52
+ }
53
+ if (!hook.function) {
54
+ continue;
55
+ }
56
+
57
+ yield* Effect.gen(function* () {
58
+ const record = yield* Database.load(hook.function!);
59
+ invariant(Obj.instanceOf(Operation.PersistentOperation, record), 'Hook function must be an operation.');
60
+ const operation = Operation.deserialize(record);
61
+ // Hooks carry no event payload (the spec is a bare tag), so the input template is passed
62
+ // through verbatim — the hook's operation reads any conversation state it needs itself.
63
+ yield* invoke(operation, { ...hook.input });
64
+ }).pipe(
65
+ Effect.catchAllCause((cause) =>
66
+ Effect.sync(() => log.warn('skill hook failed', { phase, skill: Skill.getKey(skill), cause })),
67
+ ),
68
+ );
69
+ }
70
+ }
71
+ });
@@ -0,0 +1,30 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import * as Rpc from '@effect/rpc/Rpc';
6
+ import * as RpcGroup from '@effect/rpc/RpcGroup';
7
+ import * as Schema from 'effect/Schema';
8
+
9
+ import { ContentBlock } from '@dxos/types';
10
+
11
+ /**
12
+ * Typed control surface on the live host process that owns a conversation's harness.
13
+ *
14
+ * The same group is declared as the agent process's `rpcs` (so its handlers run on the host's
15
+ * server fiber with the live `AlarmManager`/`inputQueue` in scope) and used by {@link HarnessService}
16
+ * Tier B to dispatch over the discovered host's `Handle.rpc` — keeping both ends on one contract.
17
+ */
18
+ export const HarnessControl = RpcGroup.make(
19
+ Rpc.make('setAlarm', {
20
+ payload: Schema.Struct({ at: Schema.DateTimeUtc, message: Schema.NullOr(Schema.String) }),
21
+ success: Schema.Void,
22
+ }),
23
+ Rpc.make('enqueueMessage', {
24
+ payload: Schema.Struct({ content: Schema.Array(ContentBlock.Any) }),
25
+ success: Schema.Void,
26
+ }),
27
+ );
28
+
29
+ /** Union of the RPCs declared in {@link HarnessControl}. */
30
+ export type HarnessControlRpcs = RpcGroup.Rpcs<typeof HarnessControl>;
@@ -6,5 +6,8 @@ export * from './toolkit';
6
6
 
7
7
  export * as AiContext from './AiContext';
8
8
  export * as AiSession from './AiSession';
9
+ export * as Harness from './Harness';
10
+ export * as SkillHooks from './SkillHooks';
11
+ export { HarnessControl, type HarnessControlRpcs } from './harness-control';
9
12
  export { SessionLoader } from './SessionLoader';
10
13
  export * as SessionLink from './SessionLink';
@@ -7,12 +7,12 @@ import * as Effect from 'effect/Effect';
7
7
  import * as Layer from 'effect/Layer';
8
8
 
9
9
  import { type AiToolNotFoundError, OpaqueToolkit, ToolExecutionService, ToolResolverService } from '@dxos/ai';
10
- import { type Blueprint } from '@dxos/compute';
10
+ import { type Skill } from '@dxos/compute';
11
11
  import { isTruthy } from '@dxos/util';
12
12
 
13
13
  export type CreateToolkitProps = {
14
14
  toolkit?: OpaqueToolkit.Any;
15
- blueprints?: readonly Blueprint.Blueprint[];
15
+ skills?: readonly Skill.Skill[];
16
16
  /**
17
17
  * Self-contained with handlers toolkits.
18
18
  */
@@ -20,11 +20,11 @@ export type CreateToolkitProps = {
20
20
  };
21
21
 
22
22
  /**
23
- * Build a combined toolkit from the blueprint tools and the provided toolkit.
23
+ * Build a combined toolkit from the skill tools and the provided toolkit.
24
24
  */
25
25
  export const createToolkit = ({
26
26
  toolkit: toolkitProp,
27
- blueprints = [],
27
+ skills = [],
28
28
  opaqueToolkits = [],
29
29
  }: CreateToolkitProps): Effect.Effect<
30
30
  OpaqueToolkit.OpaqueToolkit,
@@ -32,14 +32,14 @@ export const createToolkit = ({
32
32
  ToolResolverService | ToolExecutionService
33
33
  > =>
34
34
  Effect.gen(function* () {
35
- const blueprintToolkit = yield* ToolResolverService.resolveToolkit(blueprints.flatMap(({ tools }) => tools));
36
- const blueprintToolHandler = yield* blueprintToolkit.toContext(ToolExecutionService.handlersFor(blueprintToolkit));
35
+ const skillToolkit = yield* ToolResolverService.resolveToolkit(skills.flatMap(({ tools }) => tools));
36
+ const skillToolHandler = yield* skillToolkit.toContext(ToolExecutionService.handlersFor(skillToolkit));
37
37
  const opaqueToolkit = OpaqueToolkit.merge(...opaqueToolkits);
38
38
 
39
- const toolkitDefs = [toolkitProp?.toolkit, blueprintToolkit, opaqueToolkit.toolkit].filter(isTruthy);
39
+ const toolkitDefs = [toolkitProp?.toolkit, skillToolkit, opaqueToolkit.toolkit].filter(isTruthy);
40
40
  const mergedToolkit = Toolkit.merge(...toolkitDefs);
41
41
  const combinedHandlerLayer = Layer.mergeAll(
42
- Layer.succeedContext(blueprintToolHandler),
42
+ Layer.succeedContext(skillToolHandler),
43
43
  toolkitProp?.layer ?? OpaqueToolkit.empty.layer,
44
44
  opaqueToolkit.layer,
45
45
  );
@@ -2,10 +2,10 @@
2
2
 
3
3
  ## Input Formats
4
4
 
5
- ### Blueprints
5
+ ### Skills
6
6
 
7
- You may be provided with Blueprint definitions that include specific instructions and tools for a given set of tasks.
8
- Blue print instructions will be enclosed in a <blueprint> tag.
7
+ You may be provided with Skill definitions that include specific instructions and tools for a given set of tasks.
8
+ Blue print instructions will be enclosed in a <skill> tag.
9
9
 
10
10
  ### Context objects
11
11
 
@@ -12,20 +12,20 @@
12
12
  ## Planning
13
13
 
14
14
  - Analyze the structure and type of the content in the user's message.
15
- - Determine if you complete the task using the available blueprint definitions?
16
- - If you can't complete the task using the available blueprint definitions, query the list of available blueprint definitions using the appropriate tool.
17
- - Identify which blueprint definitions are relevant to the user's request.
15
+ - Determine if you complete the task using the available skill definitions?
16
+ - If you can't complete the task using the available skill definitions, query the list of available skill definitions using the appropriate tool.
17
+ - Identify which skill definitions are relevant to the user's request.
18
18
  - Evaluate the potential benefits of creating an artifact vs. normal processing for each identified element.
19
19
  - Make a final decision on whether to create an artifact and explain your reasoning.
20
- - Are the required blueprint definitions already available?
21
- - If not, select which blueprint definition(s) will be the most relevant and require them using the require_artifact_definitions tool.
20
+ - Are the required skill definitions already available?
21
+ - If not, select which skill definition(s) will be the most relevant and require them using the require_artifact_definitions tool.
22
22
  - The require'd artifact tools will be available for use after require.
23
23
  - If creating an artifact, outline how you will structure it within the response.
24
24
  - Decide if the artifact needs to be shown to the user.
25
25
  - Call the show tool to show the artifact to the user.
26
26
  - Your reasoning must include: whether to use artifacts or not, to create one or query, whether to show the artifact to the user, and how to structure the response.
27
27
 
28
- ## Blueprints and Artifacts
28
+ ## Skills and Artifacts
29
29
 
30
30
  - Determine if the interaction involves an artifact. Prefer artifacts for tables, lists, spreadsheets, kanbans, games, images, and other structured data.
31
31
  - Determine if the user is explicitly talking about creating a new artifact, or wants to use an existing artifact.
@@ -45,12 +45,10 @@
45
45
 
46
46
  ## Navigation
47
47
 
48
- - When the user asks to open, go to, navigate to, or show a specific document, page, or object, use the navigation tools instead of loading content inline.
49
- - First, call the Resolve navigation targets tool to find the navigation path for the requested item.
50
- - Without arguments, it returns available pages that can be navigated to.
51
- - With a DXN in the query, it resolves that specific object to a navigation path.
52
- - If you know the object's DXN (e.g. from a database query or context objects), pass it in the query to get the exact navigation path.
53
- - Then, call the Open tool with the resolved navigation path(s) in the subject array to navigate the user to that item.
48
+ - When the user asks to open, go to, navigate to, or show a specific document, page, or object, use the navigation tools instead of loading content inline. Navigation is always two steps: first resolve a navigation path with Resolve navigation targets, then open that path with Open.
49
+ - The Open tool accepts navigation **paths** only. Never pass an object DXN, URI, or id to Open — pass only a `path` returned by Resolve navigation targets. Passing anything else will fail to navigate.
50
+ - To open a specific object: call Resolve navigation targets with the object's URI in the `query.uri` field — use the value from the object's `<dxn>` tag in context (which may be an `echo:` EID or a `dxn:` DXN), then pass the returned target's `path` to Open.
51
+ - To navigate to a page (not a specific object): call Resolve navigation targets without arguments to list the available pages, then pass the chosen target's `path` to Open.
54
52
  - Do not respond with the document's content when the user asks to open it — open it in the main content area instead.
55
53
  - If multiple targets match, present the options to the user and let them choose which to open.
56
54
 
@@ -4,8 +4,12 @@
4
4
 
5
5
  import defaultsDeep from 'lodash.defaultsdeep';
6
6
 
7
- import { type ModelCapabilities } from '@dxos/ai';
7
+ import { Model } from '@dxos/ai';
8
8
  import { Template } from '@dxos/compute';
9
+ // Text is referenced in the inferred type of 'templates' (via Template.make → Template.Template.source → Ref.Ref(Text.Text));
10
+ // the import lets TypeScript name it in the emitted .d.ts.
11
+ // eslint-disable-next-line unused-imports/no-unused-imports
12
+ import { type Text } from '@dxos/schema';
9
13
 
10
14
  import FORMAT from './instructions/format.tpl?raw';
11
15
  import SYSTEM from './instructions/system.tpl?raw';
@@ -13,7 +17,7 @@ import SYSTEM from './instructions/system.tpl?raw';
13
17
  /**
14
18
  * System template variables.
15
19
  */
16
- export type SystemPromptOptions = ModelCapabilities & {
20
+ export type SystemPromptOptions = Model.Capabilities & {
17
21
  DATETIME: string;
18
22
  };
19
23
 
@@ -88,8 +88,15 @@ type ArtifactURI = Schema.Schema.Type<typeof ArtifactURI>;
88
88
  * Schema that decodes ECHO reference object from an LLM-friendly input.
89
89
  */
90
90
  export const RefFromLLM = Schema.transform(ArtifactURI, Ref.Ref(Obj.Unknown), {
91
- decode: (fromA, fromI) => EncodedReference.fromURI(ArtifactURI.toEchoURI(fromA)),
92
- encode: (toI, toA) => EncodedReference.toURI(toI),
91
+ decode: (fromA) => {
92
+ const eid = ArtifactURI.toEchoURI(fromA);
93
+ // Normalize to local form: strip any space authority so the ref resolves within the current
94
+ // space context. The AI commonly sends echo://SPACE/ENTITY format (mirroring what it sees in
95
+ // the database context), but cross-space resolution is not yet supported and the space ID
96
+ // encoded in the URI is always the current space anyway.
97
+ return EncodedReference.fromURI(EID.toLocal(eid));
98
+ },
99
+ encode: (toI) => EncodedReference.toURI(toI),
93
100
  strict: false,
94
101
  }).annotations({
95
102
  description: ArtifactURI.ast.annotations.description as string,
@@ -4,8 +4,8 @@
4
4
 
5
5
  import { afterEach, beforeEach, describe, expect, it } from 'vitest';
6
6
 
7
- import { createDocAccessor } from '@dxos/echo-client';
8
7
  import { EchoTestBuilder } from '@dxos/echo-client/testing';
8
+ import { Doc } from '@dxos/echo-doc';
9
9
  import { Text } from '@dxos/schema';
10
10
  import { trim } from '@dxos/util';
11
11
 
@@ -32,7 +32,7 @@ describe('diff', () => {
32
32
  `;
33
33
 
34
34
  const text = db.add(Text.make({ content: document }));
35
- const accessor = createDocAccessor(text, ['content']);
35
+ const accessor = Doc.createAccessor(text, ['content']);
36
36
  const result = applyDiffs(accessor, [
37
37
  `- There'z a typo in this sentence.`,
38
38
  `+ There is a typo in this sentence.`,
@@ -54,7 +54,7 @@ describe('diff', () => {
54
54
  const document = '# Shopping list';
55
55
 
56
56
  const text = db.add(Text.make({ content: document }));
57
- const accessor = createDocAccessor(text, ['content']);
57
+ const accessor = Doc.createAccessor(text, ['content']);
58
58
  const result = applyDiffs(accessor, ['- # Shopping list', '+ # Shopping list\n- Milk']);
59
59
 
60
60
  expect(result).toBe('# Shopping list\n- Milk');
@@ -66,7 +66,7 @@ describe('diff', () => {
66
66
  graph.registry.add([Text.Text]);
67
67
 
68
68
  const text = db.add(Text.make({ content: '' }));
69
- const accessor = createDocAccessor(text, ['content']);
69
+ const accessor = Doc.createAccessor(text, ['content']);
70
70
  const result = applyDiffs(accessor, ['+ # Shopping list']);
71
71
 
72
72
  expect(result).toBe('# Shopping list');
package/src/util/diff.ts CHANGED
@@ -2,9 +2,10 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { next as A, type Doc } from '@automerge/automerge';
5
+ import { next as A, type Doc as AutomergeDoc } from '@automerge/automerge';
6
6
 
7
- import { DocAccessor, toCursorRange } from '@dxos/echo-client';
7
+ import { toCursorRange } from '@dxos/echo-client';
8
+ import { Doc } from '@dxos/echo-doc';
8
9
  import { log } from '@dxos/log';
9
10
  import { isNonNullable } from '@dxos/util';
10
11
 
@@ -54,10 +55,13 @@ export const reduceDiffs = (diffs: readonly string[]): Diff[] => {
54
55
  }, []);
55
56
  };
56
57
 
57
- export const computeDiffsWithCursors = <T>(accessor: DocAccessor<T>, diffs: readonly string[]) => {
58
+ /**
59
+ * Resolves text diffs against the accessor's current value into cursor-anchored replacements.
60
+ */
61
+ export const computeDiffsWithCursors = <T>(accessor: Doc.Accessor<T>, diffs: readonly string[]) => {
58
62
  return reduceDiffs(diffs)
59
63
  .map((diff) => {
60
- const text = DocAccessor.getValue<string>(accessor);
64
+ const text = Doc.getValue<string>(accessor);
61
65
  const idx = text.indexOf(diff.match);
62
66
  if (idx !== -1) {
63
67
  return { cursor: toCursorRange(accessor, idx, idx + diff.match.length), text: diff.replace };
@@ -66,14 +70,17 @@ export const computeDiffsWithCursors = <T>(accessor: DocAccessor<T>, diffs: read
66
70
  .filter(isNonNullable);
67
71
  };
68
72
 
73
+ /**
74
+ * Applies text diffs to the accessor-backed document and returns the resulting string.
75
+ */
69
76
  export const applyDiffs = <T>(
70
- accessor: DocAccessor<T>,
77
+ accessor: Doc.Accessor<T>,
71
78
  diffs: readonly string[],
72
79
  { errorOnNotFound = false }: { errorOnNotFound?: boolean } = {},
73
80
  ): string => {
74
81
  for (const diff of reduceDiffs(diffs)) {
75
- accessor.handle.change((doc: Doc<T>) => {
76
- const text = DocAccessor.getValue<string>(accessor);
82
+ accessor.handle.change((doc: AutomergeDoc<T>) => {
83
+ const text = Doc.getValue<string>(accessor);
77
84
  const idx = text.indexOf(diff.match);
78
85
  if (idx !== -1) {
79
86
  // TODO(burdon): Replace smallest substring.
@@ -89,5 +96,5 @@ export const applyDiffs = <T>(
89
96
  });
90
97
  }
91
98
 
92
- return DocAccessor.getValue<string>(accessor);
99
+ return Doc.getValue<string>(accessor);
93
100
  };
@@ -6,7 +6,7 @@ import * as Schema from 'effect/Schema';
6
6
 
7
7
  import { Trace } from '@dxos/compute';
8
8
  import { Obj } from '@dxos/echo';
9
- import { ContentBlock, Actor } from '@dxos/types';
9
+ import { Actor, ContentBlock } from '@dxos/types';
10
10
 
11
11
  /**
12
12
  * Partial content block emitted.
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/functions/index.ts"],"names":[],"mappings":"AAIA,cAAc,YAAY,CAAC"}