@dxos/agent-runtime 0.0.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 (66) hide show
  1. package/LICENSE +105 -0
  2. package/dist/lib/neutral/chunk-EZKG2ZGG.mjs +754 -0
  3. package/dist/lib/neutral/chunk-EZKG2ZGG.mjs.map +7 -0
  4. package/dist/lib/neutral/index.mjs +9 -0
  5. package/dist/lib/neutral/index.mjs.map +7 -0
  6. package/dist/lib/neutral/meta.json +1 -0
  7. package/dist/lib/neutral/testing/index.mjs +285 -0
  8. package/dist/lib/neutral/testing/index.mjs.map +7 -0
  9. package/dist/types/src/agent-service/AgentService.d.ts +45 -0
  10. package/dist/types/src/agent-service/AgentService.d.ts.map +1 -0
  11. package/dist/types/src/agent-service/AgentService.test.d.ts +2 -0
  12. package/dist/types/src/agent-service/AgentService.test.d.ts.map +1 -0
  13. package/dist/types/src/agent-service/agent-process.d.ts +457 -0
  14. package/dist/types/src/agent-service/agent-process.d.ts.map +1 -0
  15. package/dist/types/src/agent-service/agent-process.test.d.ts +2 -0
  16. package/dist/types/src/agent-service/agent-process.test.d.ts.map +1 -0
  17. package/dist/types/src/agent-service/delegation-strategy.d.ts +47 -0
  18. package/dist/types/src/agent-service/delegation-strategy.d.ts.map +1 -0
  19. package/dist/types/src/agent-service/index.d.ts +4 -0
  20. package/dist/types/src/agent-service/index.d.ts.map +1 -0
  21. package/dist/types/src/assistant-session-tests/format.test.d.ts +2 -0
  22. package/dist/types/src/assistant-session-tests/format.test.d.ts.map +1 -0
  23. package/dist/types/src/assistant-session-tests/request.test.d.ts +2 -0
  24. package/dist/types/src/assistant-session-tests/request.test.d.ts.map +1 -0
  25. package/dist/types/src/assistant-session-tests/session-ollama.test.d.ts +2 -0
  26. package/dist/types/src/assistant-session-tests/session-ollama.test.d.ts.map +1 -0
  27. package/dist/types/src/assistant-session-tests/session.test.d.ts +2 -0
  28. package/dist/types/src/assistant-session-tests/session.test.d.ts.map +1 -0
  29. package/dist/types/src/assistant-session-tests/xml-response.test.d.ts +2 -0
  30. package/dist/types/src/assistant-session-tests/xml-response.test.d.ts.map +1 -0
  31. package/dist/types/src/functions.test.d.ts +2 -0
  32. package/dist/types/src/functions.test.d.ts.map +1 -0
  33. package/dist/types/src/index.d.ts +2 -0
  34. package/dist/types/src/index.d.ts.map +1 -0
  35. package/dist/types/src/testing/ResearchService.d.ts +28 -0
  36. package/dist/types/src/testing/ResearchService.d.ts.map +1 -0
  37. package/dist/types/src/testing/assistant-test-layer.d.ts +70 -0
  38. package/dist/types/src/testing/assistant-test-layer.d.ts.map +1 -0
  39. package/dist/types/src/testing/index.d.ts +4 -0
  40. package/dist/types/src/testing/index.d.ts.map +1 -0
  41. package/dist/types/src/testing/trace-pretty-print.d.ts +16 -0
  42. package/dist/types/src/testing/trace-pretty-print.d.ts.map +1 -0
  43. package/dist/types/tsconfig.tsbuildinfo +1 -0
  44. package/package.json +70 -0
  45. package/src/agent-service/AgentService.conversations.json +1 -0
  46. package/src/agent-service/AgentService.test.ts +523 -0
  47. package/src/agent-service/AgentService.ts +239 -0
  48. package/src/agent-service/README.md +91 -0
  49. package/src/agent-service/agent-process.test.ts +206 -0
  50. package/src/agent-service/agent-process.ts +793 -0
  51. package/src/agent-service/delegation-strategy.ts +63 -0
  52. package/src/agent-service/index.ts +7 -0
  53. package/src/assistant-session-tests/format.test.ts +131 -0
  54. package/src/assistant-session-tests/request.conversations.json +1 -0
  55. package/src/assistant-session-tests/request.test.ts +190 -0
  56. package/src/assistant-session-tests/session-ollama.test.ts +80 -0
  57. package/src/assistant-session-tests/session.test.ts +56 -0
  58. package/src/assistant-session-tests/xml-response.conversations.json +1 -0
  59. package/src/assistant-session-tests/xml-response.test.ts +65 -0
  60. package/src/functions.conversations.json +1 -0
  61. package/src/functions.test.ts +87 -0
  62. package/src/index.ts +7 -0
  63. package/src/testing/ResearchService.ts +144 -0
  64. package/src/testing/assistant-test-layer.ts +299 -0
  65. package/src/testing/index.ts +7 -0
  66. package/src/testing/trace-pretty-print.ts +55 -0
@@ -0,0 +1,754 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ // src/agent-service/agent-process.ts
8
+ import * as Tool from "@effect/ai/Tool";
9
+ import * as Toolkit from "@effect/ai/Toolkit";
10
+ import * as Cause from "effect/Cause";
11
+ import * as DateTime from "effect/DateTime";
12
+ import * as Duration from "effect/Duration";
13
+ import * as Effect from "effect/Effect";
14
+ import * as Exit from "effect/Exit";
15
+ import * as Layer from "effect/Layer";
16
+ import * as Match from "effect/Match";
17
+ import * as Option from "effect/Option";
18
+ import * as Schema from "effect/Schema";
19
+ import { AiService, OpaqueToolkit } from "@dxos/ai";
20
+ import { AgentRequestBegin, AgentRequestEnd, AiSession, HarnessControl, SkillHooks, getOperationFromTool, makeToolExecutionService, makeToolResolverFromOperations } from "@dxos/assistant";
21
+ import { Credential, Operation, Trace } from "@dxos/compute";
22
+ import { Process } from "@dxos/compute";
23
+ import { ProcessManager } from "@dxos/compute-runtime";
24
+ import * as StorageService from "@dxos/compute/StorageService";
25
+ import { Annotation, Database, Feed, Obj, Ref, Registry } from "@dxos/echo";
26
+ import { EffectEx } from "@dxos/effect";
27
+ import { DXN } from "@dxos/keys";
28
+ import { log } from "@dxos/log";
29
+ import { ContentBlock } from "@dxos/types";
30
+ import { trim } from "@dxos/util";
31
+ var __dxlog_file = "/Users/dmaretskyi/Code/dxos/dxos/packages/core/compute/agent-runtime/src/agent-service/agent-process.ts";
32
+ var AGENT_PROCESS_KEY = "org.dxos.testing.process.agent";
33
+ var AgentProcess = (options) => Process.make({
34
+ key: AGENT_PROCESS_KEY,
35
+ input: Schema.String,
36
+ output: Schema.Void,
37
+ services: [
38
+ Database.Service,
39
+ OpaqueToolkit.OpaqueToolkitProvider,
40
+ Operation.Service,
41
+ Registry.Service,
42
+ StorageService.StorageService,
43
+ ProcessManager.ProcessOperationInvoker.Service,
44
+ AiService.AiService,
45
+ // Needed in the fiber's context — `Header.byokLayer`'s per-request callback reads it.
46
+ Credential.CredentialsService
47
+ ],
48
+ rpcs: HarnessControl
49
+ }, (ctx) => Effect.gen(function* () {
50
+ const feedDxn = Annotation.getDictionary(ctx.params.annotations, Process.TargetAnnotation).pipe(Option.getOrUndefined);
51
+ if (feedDxn == null) {
52
+ return yield* Effect.die(new Error("Agent executable requires spawn options.target set to a queue DXN."));
53
+ }
54
+ const feed = yield* Database.resolve(feedDxn, Feed.Feed).pipe(Effect.orDie);
55
+ const runtime3 = yield* Effect.runtime();
56
+ const session = yield* EffectEx.acquireReleaseResource(() => new AiSession.Session({
57
+ feed,
58
+ runtime: runtime3
59
+ }));
60
+ let inputQueue = [
61
+ ...yield* AgentEventsCell.get
62
+ ];
63
+ const storageService = yield* StorageService.StorageService;
64
+ const toolCallManager = new ToolCallManager(storageService);
65
+ yield* toolCallManager.load();
66
+ const clock2 = yield* Effect.clock;
67
+ const alarmManager = new AlarmManager({
68
+ storageService,
69
+ setAlarm: (timeout2) => ctx.setAlarm(timeout2),
70
+ now: () => clock2.unsafeCurrentTimeMillis()
71
+ });
72
+ yield* alarmManager.load();
73
+ yield* toolCallManager.reconcileWithInputQueue(inputQueue);
74
+ const strategy = Option.fromNullable(options.delegationStrategy);
75
+ let delegations = [
76
+ ...yield* DelegationsCell.get
77
+ ];
78
+ const requestModelLayer = AiService.model(options.model ? DXN.getName(options.model) : "com.anthropic.model.claude-opus-4-8.default", {
79
+ provider: options.provider
80
+ });
81
+ const operationInvoker = yield* ProcessManager.ProcessOperationInvoker.Service;
82
+ const runEndRequestHooks = Effect.gen(function* () {
83
+ yield* SkillHooks.runHooks({
84
+ skills: session.context.getSkills(),
85
+ phase: "end-request",
86
+ invoke: (operation, input) => Effect.gen(function* () {
87
+ const fiber = yield* operationInvoker.invokeFiber(operation, input, {
88
+ environment: {
89
+ conversation: Obj.getURI(feed)
90
+ },
91
+ traceMeta: {
92
+ conversation: Ref.make(feed)
93
+ }
94
+ });
95
+ const exit2 = yield* fiber.await;
96
+ if (Exit.isFailure(exit2)) {
97
+ yield* Effect.failCause(exit2.cause);
98
+ }
99
+ }).pipe(Effect.asVoid, Effect.orDie)
100
+ });
101
+ });
102
+ const maybeComplete = Effect.gen(function* () {
103
+ if (isAgentWorkPending({
104
+ inputQueue,
105
+ alarmManager,
106
+ delegations,
107
+ toolCallManager
108
+ })) {
109
+ return;
110
+ }
111
+ yield* runEndRequestHooks;
112
+ if (isAgentWorkPending({
113
+ inputQueue,
114
+ alarmManager,
115
+ delegations,
116
+ toolCallManager
117
+ })) {
118
+ log("agent work enqueued by end-request hook, continuing", void 0, { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 133, S: this });
119
+ alarmManager.reconcile(true);
120
+ return;
121
+ }
122
+ log("agent work complete, succeeding", void 0, { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 137, S: this });
123
+ ctx.succeed();
124
+ });
125
+ return {
126
+ // Control plane (§4.3): handlers run on the host process's server fiber, closing over the
127
+ // live `alarmManager`/`inputQueue` and persisting their durable effect inline.
128
+ rpcHandlers: yield* HarnessControl.toHandlersContext({
129
+ setAlarm: Effect.fn(function* ({ at, message }) {
130
+ yield* alarmManager.setWakeAt(DateTime.toEpochMillis(at), message);
131
+ alarmManager.reconcile(inputQueue.length > 0);
132
+ }),
133
+ enqueueMessage: Effect.fn(function* ({ content }) {
134
+ inputQueue.push({
135
+ _tag: "prompt",
136
+ content
137
+ });
138
+ yield* AgentEventsCell.set(inputQueue);
139
+ alarmManager.reconcile(true);
140
+ })
141
+ }),
142
+ onInput: Effect.fnUntraced(function* (prompt) {
143
+ log("agent onInput received", {
144
+ promptLength: prompt.length,
145
+ backlog: inputQueue.length
146
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 158, S: this });
147
+ inputQueue.push({
148
+ _tag: "prompt",
149
+ content: [
150
+ ContentBlock.Text.make({
151
+ text: prompt
152
+ })
153
+ ]
154
+ });
155
+ log("agent onInput persisting queue", {
156
+ depth: inputQueue.length
157
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 170, S: this });
158
+ yield* AgentEventsCell.set(inputQueue);
159
+ log("agent onInput persisted", {
160
+ depth: inputQueue.length
161
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 174, S: this });
162
+ alarmManager.reconcile(true);
163
+ log("agent onInput alarm scheduled", void 0, { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 178, S: this });
164
+ }),
165
+ onAlarm: Effect.fnUntraced(function* () {
166
+ log("agent onAlarm fired", {
167
+ pending: inputQueue.length
168
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 181, S: this });
169
+ const fired = yield* alarmManager.takeFiredAlarm();
170
+ if (fired != null) {
171
+ log("agent onAlarm self-wake", {
172
+ firedAt: fired.firedAt
173
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 187, S: this });
174
+ inputQueue.push({
175
+ _tag: "alarm",
176
+ firedAt: fired.firedAt,
177
+ message: fired.message
178
+ });
179
+ yield* AgentEventsCell.set(inputQueue);
180
+ }
181
+ while (inputQueue.length > 0) {
182
+ const head = inputQueue[0];
183
+ if (head._tag === "tool_result" && toolCallManager.isReported(head.pid)) {
184
+ inputQueue.shift();
185
+ log.info("skip tool result that was reported synchronously", {
186
+ pid: head.pid
187
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 202, S: this });
188
+ continue;
189
+ }
190
+ break;
191
+ }
192
+ const item = inputQueue.shift();
193
+ if (!item) {
194
+ log("agent onAlarm empty queue", {}, { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 211, S: this });
195
+ alarmManager.reconcile(false);
196
+ yield* AgentEventsCell.set(inputQueue);
197
+ yield* maybeComplete;
198
+ return;
199
+ }
200
+ log("agent onAlarm handling", {
201
+ tag: item._tag
202
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 217, S: this });
203
+ const prompt = Match.value(item).pipe(Match.tag("prompt", (item2) => [
204
+ ...item2.content
205
+ ]), Match.tag("tool_result", (item2) => item2.isError ? [
206
+ ContentBlock.Text.make({
207
+ text: toolErrorResponse(item2.pid, item2.result),
208
+ disposition: "synthetic"
209
+ })
210
+ ] : [
211
+ ContentBlock.Text.make({
212
+ text: toolResultResponse(item2.pid, item2.result),
213
+ disposition: "synthetic"
214
+ })
215
+ ]), Match.tag("alarm", (item2) => [
216
+ ContentBlock.Text.make({
217
+ text: wakeUpPrompt(item2.firedAt, item2.message),
218
+ disposition: "synthetic"
219
+ })
220
+ ]), Match.exhaustive);
221
+ log("begin request", {
222
+ prompt
223
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 238, S: this });
224
+ log("trace agent request begin", void 0, { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 241, S: this });
225
+ yield* Trace.write(AgentRequestBegin, {});
226
+ yield* session.createRequest({
227
+ prompt,
228
+ // TODO(dmaretskyi): Polling currently broken, agent relies on completion notifications being delivered.
229
+ // toolkit: AsynchronousExectionToolkit,
230
+ // The alarm tools (set-alarm/get-current-date) now arrive as a bound blueprint whose
231
+ // operations reach this host's AlarmManager over HarnessService Tier B.
232
+ system: options.systemPrompt,
233
+ mcpServers: options.getMcpServers?.()
234
+ }).pipe(Effect.onExit((exit2) => Trace.write(AgentRequestEnd, {
235
+ status: Exit.isSuccess(exit2) ? "success" : Exit.isInterrupted(exit2) ? "interrupted" : "error",
236
+ error: Exit.isFailure(exit2) ? Cause.pretty(exit2.cause) : void 0
237
+ })));
238
+ log("end request", void 0, { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 255, S: this });
239
+ yield* AgentEventsCell.set(inputQueue);
240
+ if (Option.isSome(strategy)) {
241
+ const activeIds = new Set(delegations.map((delegation) => delegation.id));
242
+ const pending = yield* strategy.value.reconcile(feed, activeIds);
243
+ for (const delegation of pending) {
244
+ const pid = yield* delegation.spawn;
245
+ delegations.push({
246
+ pid,
247
+ id: delegation.id
248
+ });
249
+ log("delegated work", {
250
+ pid,
251
+ id: delegation.id
252
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 268, S: this });
253
+ }
254
+ if (pending.length > 0) {
255
+ yield* DelegationsCell.set(delegations);
256
+ }
257
+ }
258
+ alarmManager.reconcile(inputQueue.length > 0);
259
+ yield* maybeComplete;
260
+ }, Effect.orDie, Effect.provide(Layer.mergeAll(makeToolResolverFromOperations(), ToolExecutionService({
261
+ toolCallManager,
262
+ feed,
263
+ enableBackgrounding: options.enableToolBackgrounding ?? false
264
+ }), AsynchronousExectionToolkitLayer, requestModelLayer).pipe(Layer.orDie))),
265
+ onChildEvent: Effect.fnUntraced(function* (event) {
266
+ log("childEvent", {
267
+ event
268
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 286, S: this });
269
+ if (event._tag === "exited") {
270
+ const delegation = delegations.find((delegation2) => delegation2.pid === event.pid);
271
+ if (delegation) {
272
+ delegations = delegations.filter((other) => other.pid !== event.pid);
273
+ yield* DelegationsCell.set(delegations);
274
+ const operationInvoker2 = yield* ProcessManager.ProcessOperationInvoker.Service;
275
+ const fiber = yield* operationInvoker2.attachFiber(event.pid).pipe(Effect.orDie);
276
+ const exit2 = yield* fiber.await;
277
+ if (Option.isSome(strategy)) {
278
+ yield* strategy.value.onComplete(feed, delegation.id, exit2);
279
+ }
280
+ log("delegated work completed", {
281
+ pid: event.pid,
282
+ id: delegation.id,
283
+ success: Exit.isSuccess(exit2)
284
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 303, S: this });
285
+ yield* maybeComplete;
286
+ } else if (toolCallManager.isToolCall(event.pid)) {
287
+ const operationInvoker2 = yield* ProcessManager.ProcessOperationInvoker.Service;
288
+ const attachExit = yield* operationInvoker2.attachFiber(event.pid).pipe(Effect.exit);
289
+ if (Exit.isFailure(attachExit)) {
290
+ if (toolCallManager.isToolCall(event.pid) || inputQueue.some((item) => item._tag === "tool_result" && item.pid === event.pid) || toolCallManager.isReported(event.pid)) {
291
+ log.verbose("childEvent skipped (process gone, result already handled)", {
292
+ pid: event.pid
293
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 316, S: this });
294
+ return;
295
+ }
296
+ return yield* Effect.failCause(attachExit.cause).pipe(Effect.orDie);
297
+ }
298
+ const fiber = attachExit.value;
299
+ const result = yield* fiber.await.pipe(Effect.orDie).pipe(Effect.map(Exit.match({
300
+ onSuccess: (value2) => ({
301
+ _tag: "tool_result",
302
+ pid: event.pid,
303
+ result: value2,
304
+ isError: false
305
+ }),
306
+ onFailure: (cause) => ({
307
+ _tag: "tool_result",
308
+ pid: event.pid,
309
+ result: Cause.pretty(cause),
310
+ isError: true
311
+ })
312
+ })));
313
+ inputQueue.push(result);
314
+ log("agent onChildEvent persisted tool result", {
315
+ depth: inputQueue.length,
316
+ childPid: event.pid
317
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 339, S: this });
318
+ yield* AgentEventsCell.set(inputQueue);
319
+ alarmManager.reconcile(true);
320
+ log("agent onChildEvent alarm scheduled", {
321
+ depth: inputQueue.length
322
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 345, S: this });
323
+ } else {
324
+ log.verbose("childEvent ignored non-tool call and not a delegation", {
325
+ pid: event.pid
326
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 349, S: this });
327
+ }
328
+ }
329
+ })
330
+ };
331
+ }));
332
+ var AgentEvent = Schema.Union(Schema.TaggedStruct("prompt", {
333
+ content: Schema.Array(ContentBlock.Any)
334
+ }), Schema.TaggedStruct("tool_result", {
335
+ pid: Process.ID,
336
+ result: Schema.Unknown,
337
+ isError: Schema.Boolean
338
+ }), Schema.TaggedStruct("alarm", {
339
+ firedAt: Schema.Number,
340
+ // Optional reminder carried from the self-wake; surfaced to the agent when the alarm fires.
341
+ message: Schema.NullOr(Schema.String)
342
+ }));
343
+ var AgentEventsCell = StorageService.cell(Schema.parseJson(Schema.Array(AgentEvent).pipe(Schema.mutable)), "inputQueue").pipe(StorageService.withDefault(() => []));
344
+ var Delegation = Schema.Struct({
345
+ pid: Process.ID,
346
+ id: Schema.String
347
+ }).pipe(Schema.mutable);
348
+ var DelegationsCell = StorageService.cell(Schema.parseJson(Schema.Array(Delegation).pipe(Schema.mutable)), "delegations").pipe(StorageService.withDefault(() => []));
349
+ var ToolCallState = Schema.Struct({
350
+ activeCalls: Schema.Array(Schema.Struct({
351
+ pid: Process.ID,
352
+ // Whether the result was reported to the agent.
353
+ reported: Schema.Boolean
354
+ }).pipe(Schema.mutable)).pipe(Schema.mutable)
355
+ });
356
+ var ToolCallStateCell = StorageService.cell(Schema.parseJson(ToolCallState.pipe(Schema.mutable)), "toolCallState").pipe(StorageService.withDefault(() => ({
357
+ activeCalls: []
358
+ })));
359
+ var ToolCallManager = class {
360
+ #storageService;
361
+ #state = {
362
+ activeCalls: []
363
+ };
364
+ constructor(storageService) {
365
+ this.#storageService = storageService;
366
+ }
367
+ load() {
368
+ return Effect.gen(this, function* () {
369
+ this.#state = yield* ToolCallStateCell.get;
370
+ }).pipe(Effect.provideService(StorageService.StorageService, this.#storageService));
371
+ }
372
+ beginCall(pid) {
373
+ return Effect.gen(this, function* () {
374
+ this.#state.activeCalls.push({
375
+ pid,
376
+ reported: false
377
+ });
378
+ yield* ToolCallStateCell.set(this.#state);
379
+ }).pipe(Effect.provideService(StorageService.StorageService, this.#storageService));
380
+ }
381
+ markAsReported(pid) {
382
+ return Effect.gen(this, function* () {
383
+ const call = this.#state.activeCalls.find((call2) => call2.pid === pid);
384
+ if (!call) {
385
+ return;
386
+ }
387
+ call.reported = true;
388
+ yield* ToolCallStateCell.set(this.#state);
389
+ }).pipe(Effect.provideService(StorageService.StorageService, this.#storageService));
390
+ }
391
+ isToolCall(pid) {
392
+ return this.#state.activeCalls.some((call) => call.pid === pid);
393
+ }
394
+ isReported(pid) {
395
+ return this.#state.activeCalls.some((call) => call.pid === pid && call.reported);
396
+ }
397
+ /** True while a tool-call result has not yet been delivered back to the agent turn. */
398
+ hasPendingToolResults() {
399
+ return this.#state.activeCalls.some((call) => !call.reported);
400
+ }
401
+ /**
402
+ * Clears reported flags for tool calls that still have a pending queue entry.
403
+ * After reload the in-flight createRequest is gone, so those results must be redelivered via onAlarm.
404
+ */
405
+ reconcileWithInputQueue(queue) {
406
+ return Effect.gen(this, function* () {
407
+ let changed = false;
408
+ for (const item of queue) {
409
+ if (item._tag !== "tool_result") {
410
+ continue;
411
+ }
412
+ const call = this.#state.activeCalls.find((entry) => entry.pid === item.pid);
413
+ if (call?.reported) {
414
+ call.reported = false;
415
+ changed = true;
416
+ log("reconcile queued tool result", {
417
+ pid: item.pid
418
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 443, S: this });
419
+ }
420
+ }
421
+ if (changed) {
422
+ yield* ToolCallStateCell.set(this.#state);
423
+ }
424
+ }).pipe(Effect.provideService(StorageService.StorageService, this.#storageService));
425
+ }
426
+ };
427
+ var isAgentWorkPending = ({ inputQueue, alarmManager, delegations, toolCallManager }) => inputQueue.length > 0 || alarmManager.wakeAt != null || delegations.length > 0 || toolCallManager.hasPendingToolResults();
428
+ var AgentAlarmCell = StorageService.cell(Schema.parseJson(Schema.NullOr(Schema.Struct({
429
+ wakeAt: Schema.Number,
430
+ message: Schema.NullOr(Schema.String)
431
+ }))), "agentAlarm").pipe(StorageService.withDefault(() => null));
432
+ var computeAlarmDelay = ({ hasPendingWork, wakeAt, now }) => {
433
+ if (hasPendingWork) {
434
+ return 0;
435
+ }
436
+ if (wakeAt != null) {
437
+ return Math.max(0, wakeAt - now);
438
+ }
439
+ return null;
440
+ };
441
+ var AlarmManager = class {
442
+ #storageService;
443
+ #setAlarm;
444
+ #now;
445
+ #wakeAt = null;
446
+ #message = null;
447
+ constructor({ storageService, setAlarm, now = () => Date.now() }) {
448
+ this.#storageService = storageService;
449
+ this.#setAlarm = setAlarm;
450
+ this.#now = now;
451
+ }
452
+ /** Currently scheduled self-wake timestamp (ms), or `null` when none is set. */
453
+ get wakeAt() {
454
+ return this.#wakeAt;
455
+ }
456
+ /** Reminder message delivered when the current self-wake fires, or `null` when none is set. */
457
+ get message() {
458
+ return this.#message;
459
+ }
460
+ now() {
461
+ return this.#now();
462
+ }
463
+ /** Restores the persisted alarm state. */
464
+ load() {
465
+ return Effect.gen(this, function* () {
466
+ const persisted = yield* AgentAlarmCell.get;
467
+ this.#wakeAt = persisted?.wakeAt ?? null;
468
+ this.#message = persisted?.message ?? null;
469
+ }).pipe(Effect.provideService(StorageService.StorageService, this.#storageService));
470
+ }
471
+ /** Records a new self-wake target (with an optional reminder message) and persists it. */
472
+ setWakeAt(wakeAt, message = null) {
473
+ return Effect.gen(this, function* () {
474
+ this.#wakeAt = wakeAt;
475
+ this.#message = message;
476
+ yield* AgentAlarmCell.set({
477
+ wakeAt,
478
+ message
479
+ });
480
+ }).pipe(Effect.provideService(StorageService.StorageService, this.#storageService));
481
+ }
482
+ /**
483
+ * Clears the self-wake alarm if it is due, returning the timestamp it was scheduled for and the
484
+ * reminder message it carried (or `null` if no alarm was due).
485
+ */
486
+ takeFiredAlarm() {
487
+ return Effect.gen(this, function* () {
488
+ if (this.#wakeAt == null || this.#now() < this.#wakeAt) {
489
+ return null;
490
+ }
491
+ const firedAt = this.#wakeAt;
492
+ const message = this.#message;
493
+ this.#wakeAt = null;
494
+ this.#message = null;
495
+ yield* AgentAlarmCell.set(null);
496
+ return {
497
+ firedAt,
498
+ message
499
+ };
500
+ }).pipe(Effect.provideService(StorageService.StorageService, this.#storageService));
501
+ }
502
+ /**
503
+ * Reconciles the process alarm timer with pending work and the tracked self-wake, scheduling the
504
+ * earliest of the two. Does nothing when there is neither pending work nor a self-wake.
505
+ */
506
+ reconcile(hasPendingWork) {
507
+ const delay = computeAlarmDelay({
508
+ hasPendingWork,
509
+ wakeAt: this.#wakeAt,
510
+ now: this.#now()
511
+ });
512
+ if (delay != null) {
513
+ this.#setAlarm(delay);
514
+ }
515
+ }
516
+ };
517
+ var wakeUpPrompt = (firedAt, message) => message != null ? trim`
518
+ Your scheduled alarm fired (it was set for ${new Date(firedAt).toISOString()}).
519
+ ${message}
520
+ ` : trim`
521
+ Your scheduled alarm fired (it was set for ${new Date(firedAt).toISOString()}).
522
+ Continue with whatever you intended to do when you scheduled this wake-up.
523
+ `;
524
+ var ToolExecutionService = ({ enableBackgrounding, backgroundThreshold = Duration.seconds(1), toolCallManager, feed }) => Layer.unwrapEffect(Effect.gen(function* () {
525
+ const operationInvoker = yield* ProcessManager.ProcessOperationInvoker.Service;
526
+ return makeToolExecutionService({
527
+ invoke: (tool, input) => Effect.gen(function* () {
528
+ const operationDef = getOperationFromTool(tool).pipe(Option.getOrThrow);
529
+ log("invoking operation", {
530
+ operationDef,
531
+ input
532
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 567, S: this });
533
+ const fiber = yield* operationInvoker.invokeFiber(operationDef, input, {
534
+ environment: {
535
+ conversation: Obj.getURI(feed)
536
+ },
537
+ traceMeta: {
538
+ conversation: Ref.make(feed)
539
+ }
540
+ });
541
+ yield* toolCallManager.beginCall(fiber.pid);
542
+ log("invoked operation", {
543
+ operationDef,
544
+ input,
545
+ fiber
546
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 580, S: this });
547
+ const awaitWithReport = fiber.await.pipe(Effect.tap(() => toolCallManager.markAsReported(fiber.pid)));
548
+ const result = enableBackgrounding ? yield* awaitWithReport.pipe(Effect.timeout(backgroundThreshold), Effect.catchTag("TimeoutException", () => Effect.succeed(Exit.succeed(toolIsRunningInBackgroundResponse(fiber.pid))))) : yield* awaitWithReport;
549
+ log("result", {
550
+ result
551
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 587, S: this });
552
+ return yield* result;
553
+ })
554
+ });
555
+ }));
556
+ var AsynchronousExectionToolkit = class extends Toolkit.make(Tool.make("poll-tools", {
557
+ description: trim`
558
+ Poll tool calls running in the background.
559
+ Set wait to true to wait for the tool call to complete before returning.
560
+ Only set wait to true if you dont have other tasks to perform in parallel.
561
+ Set an appropriate timeout to avoid waiting forever.
562
+ You will also be notified about the job completion separatelly, so you do not always need to inspect the job if you dont need the result right now.
563
+ `,
564
+ parameters: {
565
+ ids: Schema.Array(Schema.String).annotations({
566
+ description: "The IDs of the jobs to inspect."
567
+ }),
568
+ wait: Schema.optional(Schema.Boolean).annotations({
569
+ description: "Whether to wait for the tool call to complete before returning.",
570
+ default: false
571
+ }),
572
+ timeout: Schema.optional(Schema.Number).annotations({
573
+ description: "Maximum time to wait for the job to complete. If the job does not complete within the timeout, the current state is returned.",
574
+ default: 1e4
575
+ })
576
+ }
577
+ })) {
578
+ };
579
+ var AsynchronousExectionToolkitLayer = AsynchronousExectionToolkit.toLayer(Effect.gen(function* () {
580
+ const invoker = yield* ProcessManager.ProcessOperationInvoker.Service;
581
+ return {
582
+ "poll-tools": ({ ids, wait, timeout: timeout2 = 1e4 }) => Effect.gen(function* () {
583
+ return yield* Effect.forEach(ids, (pid) => invoker.attachFiber(Process.ID.make(pid)).pipe(Effect.flatMap((_) => _.await), Effect.timeout(Duration.millis(timeout2)), Effect.flatMap(Exit.match({
584
+ onSuccess: (value2) => Effect.succeed(toolResultResponse(pid, value2)),
585
+ onFailure: (cause) => Effect.succeed(toolErrorResponse(pid, Cause.pretty(cause)))
586
+ })), Effect.catchTag("ProcessNotFoundError", () => Effect.succeed(`Process not found: ${pid}`)), Effect.catchTag("TimeoutException", () => Effect.succeed(`Process still running: ${pid}`))));
587
+ })
588
+ };
589
+ }));
590
+ var toolIsRunningInBackgroundResponse = (pid) => `Tool is running in the background (id=${pid}); wait for the completion notification to get the result.`;
591
+ var toolResultResponse = (pid, value2) => `<result pid=${pid}>${JSON.stringify(value2)}</result>`;
592
+ var toolErrorResponse = (pid, cause) => `<error pid=${pid}>${cause}</error>`;
593
+
594
+ // src/agent-service/AgentService.ts
595
+ var AgentService_exports = {};
596
+ __export(AgentService_exports, {
597
+ createSession: () => createSession,
598
+ layer: () => layer
599
+ });
600
+ import * as Cause2 from "effect/Cause";
601
+ import * as Effect2 from "effect/Effect";
602
+ import * as Layer2 from "effect/Layer";
603
+ import { AiContext } from "@dxos/assistant";
604
+ import { Process as Process2, Skill } from "@dxos/compute";
605
+ import { ProcessManager as ProcessManager2 } from "@dxos/compute-runtime";
606
+ import { AgentService, getSession } from "@dxos/compute/AgentService";
607
+ import { Annotation as Annotation2, Database as Database2, Feed as Feed2, Obj as Obj2, Ref as Ref2 } from "@dxos/echo";
608
+ import { EffectEx as EffectEx2 } from "@dxos/effect";
609
+ import { EID } from "@dxos/keys";
610
+ import { log as log2 } from "@dxos/log";
611
+ var __dxlog_file2 = "/Users/dmaretskyi/Code/dxos/dxos/packages/core/compute/agent-runtime/src/agent-service/AgentService.ts";
612
+ var isTerminalProcess = (state) => state === Process2.State.SUCCEEDED || state === Process2.State.FAILED || state === Process2.State.TERMINATED;
613
+ var createSession = Effect2.fn("createSession")(function* (opts) {
614
+ const skills = yield* Effect2.forEach(opts?.skills ?? [], (skill) => Skill.upsert(Skill.getKey(skill)).pipe(Effect2.map(Ref2.make)));
615
+ const feed = yield* Database2.add(Feed2.make());
616
+ const runtime3 = yield* Effect2.runtime();
617
+ const binder = yield* EffectEx2.acquireReleaseResource(() => new AiContext.Binder({
618
+ feed,
619
+ runtime: runtime3
620
+ }));
621
+ yield* Effect2.promise(() => binder.bind({
622
+ skills,
623
+ objects: opts?.context ?? []
624
+ }));
625
+ return yield* getSession(feed, {
626
+ model: opts?.model,
627
+ provider: opts?.provider
628
+ });
629
+ }, Effect2.scoped);
630
+ var layer = (opts) => Layer2.effect(AgentService, Effect2.gen(function* () {
631
+ const processManager = yield* ProcessManager2.Service;
632
+ const sessionCache = /* @__PURE__ */ new Map();
633
+ const makeExecutable = (model, provider) => AgentProcess({
634
+ systemPrompt: opts?.systemPrompt,
635
+ model: model ?? opts?.model,
636
+ provider: provider ?? opts?.provider,
637
+ getMcpServers: opts?.getMcpServers,
638
+ enableToolBackgrounding: opts?.enableToolBackgrounding,
639
+ delegationStrategy: opts?.delegationStrategy
640
+ });
641
+ const hydrateAgents = Effect2.fnUntraced(function* () {
642
+ sessionCache.clear();
643
+ const executable = makeExecutable();
644
+ const agents = yield* processManager.list({
645
+ key: AGENT_PROCESS_KEY
646
+ });
647
+ log2("agent hydrate", {
648
+ count: agents.length
649
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file2, L: 56, S: this });
650
+ for (const agent of agents) {
651
+ yield* agent.hydrate(executable).pipe(Effect2.catchAllCause((cause) => Effect2.sync(() => log2.warn("agent hydrate skipped", {
652
+ pid: agent.pid,
653
+ cause: Cause2.pretty(cause)
654
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file2, L: 60, S: this }))));
655
+ }
656
+ });
657
+ const service = {
658
+ getSession: (feed, options) => Effect2.gen(function* () {
659
+ const model = options?.model ?? opts?.model;
660
+ const provider = options?.provider ?? opts?.provider;
661
+ const cached = sessionCache.get(feed.id);
662
+ if (cached) {
663
+ if (cached.model === model && cached.provider === provider && !isTerminalProcess(cached.handle.status.state)) {
664
+ return cached.session;
665
+ }
666
+ if (!isTerminalProcess(cached.handle.status.state)) {
667
+ yield* cached.handle.terminate();
668
+ }
669
+ sessionCache.delete(feed.id);
670
+ }
671
+ const target = Obj2.getURI(feed);
672
+ const parsedEchoUri = EID.tryParse(target);
673
+ const spaceId = parsedEchoUri ? EID.getSpaceId(parsedEchoUri) : void 0;
674
+ const executable = makeExecutable(model, provider);
675
+ const processes = yield* processManager.list({
676
+ target,
677
+ key: executable.key
678
+ });
679
+ const activeProcess = processes.find((process) => !isTerminalProcess(process.status.state));
680
+ let handle;
681
+ if (activeProcess) {
682
+ yield* activeProcess.hydrate(executable);
683
+ handle = activeProcess;
684
+ } else {
685
+ handle = yield* processManager.spawn(executable, {
686
+ name: "Agent",
687
+ target,
688
+ // Stamp the host marker so the harness control surface is discoverable by annotation
689
+ // lookup (set once at spawn, immutable — the identity plane).
690
+ annotations: Annotation2.buildDictionary((dictionary) => {
691
+ Annotation2.setDictionary(dictionary, Process2.HarnessHostAnnotation, true);
692
+ }),
693
+ environment: {
694
+ ...spaceId !== void 0 ? {
695
+ space: spaceId
696
+ } : {},
697
+ conversation: target
698
+ },
699
+ traceMeta: {
700
+ conversation: Ref2.make(feed)
701
+ }
702
+ });
703
+ }
704
+ const releaseSession = () => {
705
+ sessionCache.delete(feed.id);
706
+ };
707
+ const session = makeSession(handle, feed, releaseSession);
708
+ sessionCache.set(feed.id, {
709
+ model,
710
+ provider,
711
+ handle,
712
+ session
713
+ });
714
+ return session;
715
+ }),
716
+ hydrate: hydrateAgents
717
+ };
718
+ return service;
719
+ }));
720
+ var makeSession = (process, feed, releaseSession) => ({
721
+ feed,
722
+ getContext: () => Effect2.gen(function* () {
723
+ const runtime3 = yield* Effect2.runtime();
724
+ const binder = yield* EffectEx2.acquireReleaseResource(() => new AiContext.Binder({
725
+ feed,
726
+ runtime: runtime3
727
+ }));
728
+ return binder.getObjects().map((object) => Ref2.make(object));
729
+ }).pipe(Effect2.scoped),
730
+ addContext: (context) => Effect2.gen(function* () {
731
+ const runtime3 = yield* Effect2.runtime();
732
+ const binder = yield* EffectEx2.acquireReleaseResource(() => new AiContext.Binder({
733
+ feed,
734
+ runtime: runtime3
735
+ }));
736
+ yield* Effect2.promise(() => binder.bind({
737
+ skills: [],
738
+ objects: context
739
+ }));
740
+ }).pipe(Effect2.scoped),
741
+ submitPrompt: (prompt) => process.submitInput(prompt),
742
+ // Settle when the turn's reply is complete; do NOT block on background sub-agents
743
+ // (a supervisor delegates work that runs after the turn and reports back out of band).
744
+ waitForCompletion: () => process.runUntilSettled(),
745
+ terminate: () => process.terminate().pipe(Effect2.tap(() => Effect2.sync(releaseSession))),
746
+ subscribeEphemeral: () => process.subscribeEphemeral()
747
+ });
748
+
749
+ export {
750
+ __export,
751
+ AGENT_PROCESS_KEY,
752
+ AgentService_exports
753
+ };
754
+ //# sourceMappingURL=chunk-EZKG2ZGG.mjs.map