@dxos/agent-runtime 0.0.0 → 0.11.0

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