@dxos/agent-runtime 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/chunk-AgentService.mjs +189 -93
- package/dist/lib/chunk-AgentService.mjs.map +1 -1
- package/dist/lib/testing.mjs +3 -3
- package/dist/lib/testing.mjs.map +1 -1
- package/dist/types/src/agent-service/AgentService.d.ts +9 -0
- package/dist/types/src/agent-service/AgentService.d.ts.map +1 -1
- package/dist/types/src/agent-service/agent-process.d.ts +23 -5
- package/dist/types/src/agent-service/agent-process.d.ts.map +1 -1
- package/dist/types/src/agent-service/delegation-strategy.d.ts +5 -4
- package/dist/types/src/agent-service/delegation-strategy.d.ts.map +1 -1
- package/dist/types/src/agent-service/index.d.ts +1 -1
- package/dist/types/src/agent-service/index.d.ts.map +1 -1
- package/dist/types/src/agent-service/process-selection.test.d.ts +2 -0
- package/dist/types/src/agent-service/process-selection.test.d.ts.map +1 -0
- package/dist/types/src/agent-service/skill-hooks.test.d.ts +2 -0
- package/dist/types/src/agent-service/skill-hooks.test.d.ts.map +1 -0
- package/dist/types/src/testing/assistant-test-layer.d.ts +2 -2
- package/dist/types/src/testing/assistant-test-layer.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +17 -17
- package/src/agent-service/AgentService.test.ts +4 -4
- package/src/agent-service/AgentService.ts +63 -24
- package/src/agent-service/README.md +10 -10
- package/src/agent-service/agent-process.ts +201 -100
- package/src/agent-service/delegation-scripted.test.ts +4 -4
- package/src/agent-service/delegation-strategy.ts +9 -4
- package/src/agent-service/index.ts +1 -0
- package/src/agent-service/process-selection.test.ts +135 -0
- package/src/agent-service/queue-scripted.test.ts +39 -0
- package/src/agent-service/redelivery.test.ts +23 -1
- package/src/agent-service/skill-hooks.test.ts +182 -0
- package/src/testing/assistant-test-layer.ts +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/agent-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Local agent process runtime (agent-service) and assistant-coupled test helpers.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -39,29 +39,29 @@
|
|
|
39
39
|
],
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"effect": "4.0.0",
|
|
42
|
-
"@dxos/ai": "0.
|
|
43
|
-
"@dxos/compute": "0.
|
|
44
|
-
"@dxos/compute-runtime": "0.
|
|
45
|
-
"@dxos/echo": "0.
|
|
46
|
-
"@dxos/echo-client": "0.
|
|
47
|
-
"@dxos/effect": "0.
|
|
48
|
-
"@dxos/keys": "0.
|
|
49
|
-
"@dxos/link": "0.
|
|
50
|
-
"@dxos/log": "0.
|
|
51
|
-
"@dxos/mcp-client": "0.
|
|
52
|
-
"@dxos/schema": "0.
|
|
53
|
-
"@dxos/types": "0.
|
|
54
|
-
"@dxos/util": "0.
|
|
42
|
+
"@dxos/ai": "0.14.0",
|
|
43
|
+
"@dxos/compute": "0.14.0",
|
|
44
|
+
"@dxos/compute-runtime": "0.14.0",
|
|
45
|
+
"@dxos/echo": "0.14.0",
|
|
46
|
+
"@dxos/echo-client": "0.14.0",
|
|
47
|
+
"@dxos/effect": "0.14.0",
|
|
48
|
+
"@dxos/keys": "0.14.0",
|
|
49
|
+
"@dxos/link": "0.14.0",
|
|
50
|
+
"@dxos/log": "0.14.0",
|
|
51
|
+
"@dxos/mcp-client": "0.14.0",
|
|
52
|
+
"@dxos/schema": "0.14.0",
|
|
53
|
+
"@dxos/types": "0.14.0",
|
|
54
|
+
"@dxos/util": "0.14.0"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@effect/vitest": "4.0.0",
|
|
58
58
|
"vitest": "4.1.10",
|
|
59
|
-
"@dxos/assistant": "0.
|
|
60
|
-
"@dxos/errors": "0.
|
|
59
|
+
"@dxos/assistant": "0.14.0",
|
|
60
|
+
"@dxos/errors": "0.14.0"
|
|
61
61
|
},
|
|
62
62
|
"peerDependencies": {
|
|
63
63
|
"effect": "4.0.0",
|
|
64
|
-
"@dxos/assistant": "^0.
|
|
64
|
+
"@dxos/assistant": "^0.14.0"
|
|
65
65
|
},
|
|
66
66
|
"publishConfig": {
|
|
67
67
|
"access": "public"
|
|
@@ -22,6 +22,7 @@ import { LanguageModelFixture } from '@dxos/ai/testing';
|
|
|
22
22
|
import { type HarnessControlRpcs, SessionLink } from '@dxos/assistant';
|
|
23
23
|
import * as Chat from '@dxos/assistant/Chat';
|
|
24
24
|
import { ProcessManager } from '@dxos/compute-runtime';
|
|
25
|
+
import { OperationProcess } from '@dxos/compute-runtime';
|
|
25
26
|
import * as ComputeAgentService from '@dxos/compute/AgentService';
|
|
26
27
|
import * as Instructions from '@dxos/compute/Instructions';
|
|
27
28
|
import * as Operation from '@dxos/compute/Operation';
|
|
@@ -213,9 +214,8 @@ const StubDelegationStrategy: DelegationStrategy = {
|
|
|
213
214
|
.map((work) => ({
|
|
214
215
|
id: work.id,
|
|
215
216
|
spawn: Effect.gen(function* () {
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
return fiber.pid;
|
|
217
|
+
const handle = yield* Process.spawn(OperationProcess.make(DelegatedWork), work.input);
|
|
218
|
+
return handle.pid;
|
|
219
219
|
}),
|
|
220
220
|
})),
|
|
221
221
|
),
|
|
@@ -597,7 +597,7 @@ describe('Agent Service', { tags: ['model-fixture'] }, () => {
|
|
|
597
597
|
const target = Obj.getURI(session.chat);
|
|
598
598
|
// `list` erases the RPC group to `any`, which Effect 4 resolves to an `unknown` requirement
|
|
599
599
|
// on every call; naming the group restores it.
|
|
600
|
-
const handles: readonly Process.
|
|
600
|
+
const handles: readonly Process.Process<string | readonly ContentBlock.Any[], void, HarnessControlRpcs>[] =
|
|
601
601
|
yield* processManager.list({
|
|
602
602
|
target,
|
|
603
603
|
key: AGENT_PROCESS_KEY,
|
|
@@ -10,10 +10,9 @@ import * as Layer from 'effect/Layer';
|
|
|
10
10
|
import * as Atom from 'effect/reactivity/Atom';
|
|
11
11
|
import * as Semaphore from 'effect/Semaphore';
|
|
12
12
|
|
|
13
|
-
import { AiContext } from '@dxos/assistant';
|
|
13
|
+
import { AiContext, type HarnessControlRpcs } from '@dxos/assistant';
|
|
14
14
|
import * as Chat from '@dxos/assistant/Chat';
|
|
15
15
|
import * as AgentService from '@dxos/compute/AgentService';
|
|
16
|
-
import * as Operation from '@dxos/compute/Operation';
|
|
17
16
|
import * as Process from '@dxos/compute/Process';
|
|
18
17
|
import * as Skill from '@dxos/compute/Skill';
|
|
19
18
|
import { Annotation, Database, Feed, Obj, Ref, Registry } from '@dxos/echo';
|
|
@@ -22,24 +21,12 @@ import { DXN, EID, type SpaceId } from '@dxos/keys';
|
|
|
22
21
|
import { log } from '@dxos/log';
|
|
23
22
|
import type { ContentBlock } from '@dxos/types';
|
|
24
23
|
|
|
25
|
-
import { AGENT_PROCESS_KEY, AgentProcess } from './agent-process.ts';
|
|
24
|
+
import { AGENT_PROCESS_KEY, type AgentInput, AgentProcess, type AgentProcessDefinition } from './agent-process.ts';
|
|
26
25
|
import { type DelegationStrategy } from './delegation-strategy.ts';
|
|
27
26
|
import { type MakeTurnProducer } from './turn-producer.ts';
|
|
28
27
|
|
|
29
|
-
/**
|
|
30
|
-
type
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Live handle to a spawned {@link AgentProcess}, carrying its `HarnessControl` RPC surface.
|
|
34
|
-
*
|
|
35
|
-
* Derived from the definition rather than restated: `Process` exposes its input/output codecs, which
|
|
36
|
-
* puts `_Input` in an invariant position, so a hand-written `ContentBlock.Any[]` no longer relates to
|
|
37
|
-
* the `readonly` array the process's own schema yields.
|
|
38
|
-
*/
|
|
39
|
-
type AgentHandle =
|
|
40
|
-
ReturnType<typeof AgentProcess> extends Operation.Durable<infer Input, infer Output, any, infer Rpcs>
|
|
41
|
-
? Process.Handle<Input, Output, Rpcs>
|
|
42
|
-
: never;
|
|
28
|
+
/** Live handle to a spawned agent process, carrying its `HarnessControl` RPC surface. */
|
|
29
|
+
type AgentHandle = Process.Process<AgentInput, void, HarnessControlRpcs>;
|
|
43
30
|
|
|
44
31
|
// TODO(burdon): Agent identity?
|
|
45
32
|
export interface CreateSessionOptions {
|
|
@@ -111,6 +98,15 @@ export interface Options {
|
|
|
111
98
|
* child processes and folds their results back into the conversation. Absent — a plain agent.
|
|
112
99
|
*/
|
|
113
100
|
delegationStrategy?: DelegationStrategy;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Process definitions a chat can name in `chat.session.process` to run on instead of
|
|
104
|
+
* {@link AgentProcess}. Read per session, so a definition contributed after the layer was built
|
|
105
|
+
* is still found.
|
|
106
|
+
*/
|
|
107
|
+
processes?: () => readonly AgentProcessDefinition[];
|
|
108
|
+
/** Keep each session's process resident between turns (see `AgentProcessOptions.resident`). */
|
|
109
|
+
resident?: boolean;
|
|
114
110
|
}
|
|
115
111
|
|
|
116
112
|
/**
|
|
@@ -159,6 +155,7 @@ export const layer = (opts?: Options): Layer.Layer<AgentService.AgentService, ne
|
|
|
159
155
|
provider: DXN.DXN | undefined;
|
|
160
156
|
instructions: string | undefined;
|
|
161
157
|
location: AgentService.AgentLocation;
|
|
158
|
+
process: string;
|
|
162
159
|
handle: AgentHandle;
|
|
163
160
|
session: AgentService.Session;
|
|
164
161
|
}
|
|
@@ -187,25 +184,52 @@ export const layer = (opts?: Options): Layer.Layer<AgentService.AgentService, ne
|
|
|
187
184
|
provider: provider ?? opts?.provider,
|
|
188
185
|
enableToolBackgrounding: opts?.enableToolBackgrounding,
|
|
189
186
|
delegationStrategy: opts?.delegationStrategy,
|
|
187
|
+
resident: opts?.resident,
|
|
190
188
|
});
|
|
191
189
|
|
|
190
|
+
/**
|
|
191
|
+
* The process a chat runs on: the one its session names, or ours. A name nothing contributed
|
|
192
|
+
* (its plugin was removed) falls back to ours rather than leaving the chat unable to run.
|
|
193
|
+
*/
|
|
194
|
+
const executableFor = (chat: AgentService.Conversation, provider?: DXN.DXN): AgentProcessDefinition => {
|
|
195
|
+
const key = chat.session?.process;
|
|
196
|
+
if (key === undefined || key === AGENT_PROCESS_KEY) {
|
|
197
|
+
return makeExecutable(provider);
|
|
198
|
+
}
|
|
199
|
+
const definition = opts?.processes?.().find((candidate) => candidate.key === key);
|
|
200
|
+
if (!definition) {
|
|
201
|
+
log.warn('agent process not contributed; running the default', { key });
|
|
202
|
+
return makeExecutable(provider);
|
|
203
|
+
}
|
|
204
|
+
return definition;
|
|
205
|
+
};
|
|
206
|
+
|
|
192
207
|
const hydrateAgents = Effect.fnUntraced(function* () {
|
|
193
208
|
// Handles cached before shutdown are suspended and no longer registered with the manager.
|
|
194
209
|
sessionCache.clear();
|
|
195
210
|
|
|
211
|
+
// Contributed processes run only locally (see `getSession`), so only ours is looked for on edge.
|
|
212
|
+
const contributed = (opts?.processes?.() ?? []).filter(({ key }) => key !== AGENT_PROCESS_KEY);
|
|
196
213
|
const executable = makeExecutable();
|
|
197
214
|
// Local, plus every space a session has already been opened on this run. A fresh client knows
|
|
198
215
|
// no edge spaces yet and cannot enumerate them (one manager spans them all), but an edge agent
|
|
199
216
|
// does not need the pre-warm: `getSession` reattaches to a process still running for its
|
|
200
217
|
// chat, which is the path opening one takes.
|
|
201
218
|
const agents = [
|
|
202
|
-
...(yield* processManager.handles({ key: AGENT_PROCESS_KEY })),
|
|
219
|
+
...(yield* processManager.handles({ key: AGENT_PROCESS_KEY })).map((agent) => ({ agent, executable })),
|
|
220
|
+
...(yield* Effect.forEach(contributed, (definition) =>
|
|
221
|
+
processManager
|
|
222
|
+
.handles({ key: definition.key })
|
|
223
|
+
.pipe(Effect.map((agents) => agents.map((agent) => ({ agent, executable: definition })))),
|
|
224
|
+
)).flat(),
|
|
203
225
|
...(yield* Effect.forEach([...remoteSpaces], (space) =>
|
|
204
226
|
processManager.handles({ key: AGENT_PROCESS_KEY, location: { kind: 'edge', space } }),
|
|
205
|
-
))
|
|
227
|
+
))
|
|
228
|
+
.flat()
|
|
229
|
+
.map((agent) => ({ agent, executable })),
|
|
206
230
|
];
|
|
207
231
|
log('agent hydrate', { count: agents.length });
|
|
208
|
-
for (const agent of agents) {
|
|
232
|
+
for (const { agent, executable } of agents) {
|
|
209
233
|
yield* agent
|
|
210
234
|
.hydrate(executable)
|
|
211
235
|
.pipe(
|
|
@@ -227,6 +251,13 @@ export const layer = (opts?: Options): Layer.Layer<AgentService.AgentService, ne
|
|
|
227
251
|
const model = chat.session?.model;
|
|
228
252
|
const instructions = chat.instructions?.uri;
|
|
229
253
|
const location: AgentService.AgentLocation = options?.location ?? 'local';
|
|
254
|
+
const executable = executableFor(chat, provider);
|
|
255
|
+
if (location === 'edge' && executable.key !== AGENT_PROCESS_KEY) {
|
|
256
|
+
// EDGE spawns by key from its own registry, which holds no process a plugin contributed.
|
|
257
|
+
return yield* Effect.die(
|
|
258
|
+
new Error(`Agent process ${executable.key} runs only locally, but the chat asked for edge.`),
|
|
259
|
+
);
|
|
260
|
+
}
|
|
230
261
|
const cached = sessionCache.get(chat.id);
|
|
231
262
|
if (cached) {
|
|
232
263
|
if (
|
|
@@ -234,13 +265,14 @@ export const layer = (opts?: Options): Layer.Layer<AgentService.AgentService, ne
|
|
|
234
265
|
cached.provider === provider &&
|
|
235
266
|
cached.instructions === instructions &&
|
|
236
267
|
cached.location === location &&
|
|
268
|
+
cached.process === executable.key &&
|
|
237
269
|
!Process.isTerminal(cached.handle.status.state)
|
|
238
270
|
) {
|
|
239
271
|
return cached.session;
|
|
240
272
|
}
|
|
241
273
|
|
|
242
274
|
if (!Process.isTerminal(cached.handle.status.state)) {
|
|
243
|
-
// Model, provider, steering instructions or
|
|
275
|
+
// Model, provider, steering instructions, location or process changed (e.g. the user
|
|
244
276
|
// toggled online/offline, or moved the chat to the cloud): terminate the
|
|
245
277
|
// existing process so the conversation continues on a fresh process bound to the new
|
|
246
278
|
// configuration. Conversation history is preserved via the feed, which the new
|
|
@@ -255,7 +287,6 @@ export const layer = (opts?: Options): Layer.Layer<AgentService.AgentService, ne
|
|
|
255
287
|
const parsedEchoUri = EID.tryParse(target);
|
|
256
288
|
const spaceId = parsedEchoUri ? EID.getSpaceId(parsedEchoUri) : undefined;
|
|
257
289
|
const processLocation = locationFor(options?.location, spaceId);
|
|
258
|
-
const executable = makeExecutable(provider);
|
|
259
290
|
|
|
260
291
|
// Reuse a still-running process for this feed only when there was no cached session
|
|
261
292
|
// (e.g. after the UI remounted). A process adopted this way re-reads the chat when it
|
|
@@ -330,7 +361,15 @@ export const layer = (opts?: Options): Layer.Layer<AgentService.AgentService, ne
|
|
|
330
361
|
Effect.provide(databaseContext),
|
|
331
362
|
);
|
|
332
363
|
const session = makeSession(handle, chat, feed, releaseSession, isFinished, resubmit);
|
|
333
|
-
sessionCache.set(chat.id, {
|
|
364
|
+
sessionCache.set(chat.id, {
|
|
365
|
+
model,
|
|
366
|
+
provider,
|
|
367
|
+
instructions,
|
|
368
|
+
location,
|
|
369
|
+
process: executable.key,
|
|
370
|
+
handle,
|
|
371
|
+
session,
|
|
372
|
+
});
|
|
334
373
|
return session;
|
|
335
374
|
}),
|
|
336
375
|
),
|
|
@@ -390,5 +429,5 @@ const makeSession = (
|
|
|
390
429
|
// turn open, so anything it did afterwards would land after the reader's next prompt. The next
|
|
391
430
|
// process to spawn on this feed discards what it inherits instead (see `onSpawn` in agent-process).
|
|
392
431
|
terminate: () => process.terminate().pipe(Effect.tap(() => Effect.sync(releaseSession))),
|
|
393
|
-
subscribeEphemeral: () => process.subscribeEphemeral(),
|
|
432
|
+
subscribeEphemeral: (options) => process.subscribeEphemeral(options),
|
|
394
433
|
});
|
|
@@ -14,8 +14,8 @@ the agent/plan ECHO types (it cannot depend on `@dxos/assistant-toolkit`).
|
|
|
14
14
|
|
|
15
15
|
```mermaid
|
|
16
16
|
flowchart TB
|
|
17
|
-
subgraph rt["@dxos/compute
|
|
18
|
-
POI["
|
|
17
|
+
subgraph rt["@dxos/compute"]
|
|
18
|
+
POI["Process.ManagerService<br/>Process.spawn() / attach()<br/><i>(linked child)</i>"]
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
subgraph fr["@dxos/functions-runtime · agent-service"]
|
|
@@ -40,9 +40,9 @@ flowchart TB
|
|
|
40
40
|
IMPL --> CAP
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
- **`
|
|
44
|
-
|
|
45
|
-
finished child's
|
|
43
|
+
- **`Process.ManagerService`** (runtime primitive, no AI): the process's own manager links every child
|
|
44
|
+
it spawns to the process. `Process.spawn` spawns an operation as such a child and returns its
|
|
45
|
+
`Process.Process`; `attach(pid)` + `Process.awaitOutput` reads the finished child's result.
|
|
46
46
|
- **`DelegationStrategy`** (this dir, type-only): the pluggable policy `AgentProcess` calls —
|
|
47
47
|
`reconcile` (what to delegate) and `onComplete` (how to fold a result back). Absent → plain chat.
|
|
48
48
|
- **`makeDelegationStrategy()`** (assistant-toolkit): the concrete, agent/plan-aware implementation,
|
|
@@ -51,7 +51,7 @@ flowchart TB
|
|
|
51
51
|
## Delegation lifecycle
|
|
52
52
|
|
|
53
53
|
The child's exit is only a **wake signal** — its output value is read separately via
|
|
54
|
-
`
|
|
54
|
+
`attach(pid)` + `Process.awaitOutput` (the `ChildEvent` does not carry the payload). `DelegationsKey` persists the
|
|
55
55
|
`pid → id` map so a child that exits after the supervisor hibernates can still be correlated.
|
|
56
56
|
|
|
57
57
|
```mermaid
|
|
@@ -59,7 +59,7 @@ sequenceDiagram
|
|
|
59
59
|
participant U as User
|
|
60
60
|
participant AP as AgentProcess (supervisor)
|
|
61
61
|
participant ST as DelegationStrategy
|
|
62
|
-
participant POI as
|
|
62
|
+
participant POI as Process.ManagerService
|
|
63
63
|
participant CH as Sub-agent (child process)
|
|
64
64
|
|
|
65
65
|
U->>AP: submitInput(prompt)
|
|
@@ -67,7 +67,7 @@ sequenceDiagram
|
|
|
67
67
|
AP->>ST: reconcile(chat, activeIds)
|
|
68
68
|
ST-->>AP: Delegation[] { id, spawn }
|
|
69
69
|
loop per delegation
|
|
70
|
-
AP->>POI:
|
|
70
|
+
AP->>POI: Process.spawn(op, input)
|
|
71
71
|
POI->>CH: linked child (non-blocking)
|
|
72
72
|
POI-->>AP: pid
|
|
73
73
|
AP->>AP: DelegationsKey.set(pid → id)
|
|
@@ -76,7 +76,7 @@ sequenceDiagram
|
|
|
76
76
|
|
|
77
77
|
CH-->>AP: exit (wake only — no payload)
|
|
78
78
|
AP->>AP: onChildEvent: match pid → id, drop from DelegationsKey
|
|
79
|
-
AP->>POI:
|
|
79
|
+
AP->>POI: attach(pid) + Process.awaitOutput
|
|
80
80
|
POI-->>AP: Exit of output value
|
|
81
81
|
AP->>ST: onComplete(chat, id, exit)
|
|
82
82
|
ST->>U: update Task status + post message (reference blocks → dx-anchor)
|
|
@@ -89,5 +89,5 @@ sequenceDiagram
|
|
|
89
89
|
| `AgentService` / `layer` | `AgentService.ts` | Per-chat session cache (model-aware); wires `delegationStrategy` into `AgentProcess`. |
|
|
90
90
|
| `AgentProcess` | `agent-process.ts` | Turn loop (`onAlarm`) + child-exit wake (`onChildEvent`); owns `DelegationsKey`. |
|
|
91
91
|
| `DelegationStrategy`, `Delegation` | `delegation-strategy.ts` | Type-only seam: `reconcile` / `onComplete`; `Delegation = { id, spawn }`. |
|
|
92
|
-
| `
|
|
92
|
+
| `Process.spawn` / `Process.Manager.attach` | `@dxos/compute` | Linked-child spawn + result read. |
|
|
93
93
|
| `makeDelegationStrategy()` | `@dxos/assistant-toolkit` | Concrete agent/plan-aware strategy. |
|