@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.
Files changed (32) hide show
  1. package/dist/lib/chunk-AgentService.mjs +189 -93
  2. package/dist/lib/chunk-AgentService.mjs.map +1 -1
  3. package/dist/lib/testing.mjs +3 -3
  4. package/dist/lib/testing.mjs.map +1 -1
  5. package/dist/types/src/agent-service/AgentService.d.ts +9 -0
  6. package/dist/types/src/agent-service/AgentService.d.ts.map +1 -1
  7. package/dist/types/src/agent-service/agent-process.d.ts +23 -5
  8. package/dist/types/src/agent-service/agent-process.d.ts.map +1 -1
  9. package/dist/types/src/agent-service/delegation-strategy.d.ts +5 -4
  10. package/dist/types/src/agent-service/delegation-strategy.d.ts.map +1 -1
  11. package/dist/types/src/agent-service/index.d.ts +1 -1
  12. package/dist/types/src/agent-service/index.d.ts.map +1 -1
  13. package/dist/types/src/agent-service/process-selection.test.d.ts +2 -0
  14. package/dist/types/src/agent-service/process-selection.test.d.ts.map +1 -0
  15. package/dist/types/src/agent-service/skill-hooks.test.d.ts +2 -0
  16. package/dist/types/src/agent-service/skill-hooks.test.d.ts.map +1 -0
  17. package/dist/types/src/testing/assistant-test-layer.d.ts +2 -2
  18. package/dist/types/src/testing/assistant-test-layer.d.ts.map +1 -1
  19. package/dist/types/tsconfig.tsbuildinfo +1 -1
  20. package/package.json +17 -17
  21. package/src/agent-service/AgentService.test.ts +4 -4
  22. package/src/agent-service/AgentService.ts +63 -24
  23. package/src/agent-service/README.md +10 -10
  24. package/src/agent-service/agent-process.ts +201 -100
  25. package/src/agent-service/delegation-scripted.test.ts +4 -4
  26. package/src/agent-service/delegation-strategy.ts +9 -4
  27. package/src/agent-service/index.ts +1 -0
  28. package/src/agent-service/process-selection.test.ts +135 -0
  29. package/src/agent-service/queue-scripted.test.ts +39 -0
  30. package/src/agent-service/redelivery.test.ts +23 -1
  31. package/src/agent-service/skill-hooks.test.ts +182 -0
  32. package/src/testing/assistant-test-layer.ts +3 -2
@@ -22,10 +22,11 @@ import * as Option from "effect/Option";
22
22
  import * as Schema from "effect/Schema";
23
23
  import * as Struct from "effect/Struct";
24
24
  import { AiService, Model, OpaqueToolkit } from "@dxos/ai";
25
- import { ProcessManager } from "@dxos/compute-runtime";
25
+ import { OperationProcess } from "@dxos/compute-runtime";
26
26
  import * as Credential from "@dxos/compute/Credential";
27
27
  import * as McpServer from "@dxos/compute/McpServer";
28
28
  import * as Operation from "@dxos/compute/Operation";
29
+ import * as OperationHandlerSet from "@dxos/compute/OperationHandlerSet";
29
30
  import * as StorageService from "@dxos/compute/StorageService";
30
31
  import * as Trace from "@dxos/compute/Trace";
31
32
  import { AccessToken } from "@dxos/link";
@@ -196,12 +197,13 @@ var AgentProcess = (options) => Operation.makeDurable({
196
197
  Operation.Service,
197
198
  Registry.Service,
198
199
  StorageService.StorageService,
199
- ProcessManager.ProcessOperationInvoker.Service,
200
+ Process.ManagerService,
201
+ OperationHandlerSet.OperationHandlerProvider,
200
202
  AiService.AiService,
201
203
  Credential.CredentialsService
202
204
  ],
203
205
  rpcs: HarnessControl
204
- }, (ctx) => Effect.gen(function* () {
206
+ }).pipe(Operation.withDurableHandler((ctx) => Effect.gen(function* () {
205
207
  const chatDxn = Annotation.getDictionary(ctx.params.annotations, Process.TargetAnnotation).pipe(Option.getOrUndefined);
206
208
  if (chatDxn == null) return yield* Effect.die(/* @__PURE__ */ new Error("Agent executable requires spawn options.target set to a Chat DXN."));
207
209
  const chat = yield* Database.resolve(chatDxn, Chat.Chat).pipe(Effect.orDie);
@@ -240,23 +242,32 @@ var AgentProcess = (options) => Operation.makeDurable({
240
242
  yield* reconcileAlarmWith(startupPending);
241
243
  const strategy = Option.fromNullishOr(options.delegationStrategy);
242
244
  let delegations = [...yield* DelegationsCell.get];
245
+ let asyncHooks = [...yield* AsyncHooksCell.get];
246
+ const untrackedExits = /* @__PURE__ */ new Set();
243
247
  const model = chat.session?.model ?? options.defaultModel;
244
248
  const requestModelLayer = AiService.languageModel(DXN.getName(model ?? Model.claudeSonnet5.id), { provider: options.provider });
245
- const operationInvoker = yield* ProcessManager.ProcessOperationInvoker.Service;
249
+ const childServices = yield* Effect.context();
246
250
  const runEndRequestHooks = Effect.gen(function* () {
247
251
  yield* SkillHooks.runHooks({
248
252
  skills: session.getSkills(),
249
253
  phase: "end-request",
250
- invoke: (operation, input) => Effect.gen(function* () {
251
- const exit = yield* (yield* operationInvoker.invokeFiber(operation, input, {
254
+ invoke: (operation, input, hook) => Effect.gen(function* () {
255
+ const handle = yield* Process.spawn(OperationProcess.make(operation), input, {
252
256
  environment: { conversation: Obj.getURI(feed) },
253
257
  traceMeta: { conversation: Ref.make(feed) }
254
- })).await;
255
- if (Exit.isFailure(exit)) return yield* Effect.failCause(exit.cause);
256
- }).pipe(Effect.asVoid, Effect.orDie)
258
+ });
259
+ if (hook.async) {
260
+ if (untrackedExits.delete(handle.pid)) return;
261
+ asyncHooks.push(handle.pid);
262
+ yield* AsyncHooksCell.set(asyncHooks);
263
+ return;
264
+ }
265
+ yield* Process.awaitOutput(handle);
266
+ }).pipe(Effect.provide(childServices), Effect.asVoid, Effect.orDie)
257
267
  });
258
268
  });
259
269
  let turnRan = false;
270
+ let endHooksFired = asyncHooks.length > 0;
260
271
  const unseenWriteIds = /* @__PURE__ */ new Set();
261
272
  let unseenWriteWakes = 0;
262
273
  const pendingWork = (state) => isAgentWorkPending({
@@ -271,39 +282,60 @@ var AgentProcess = (options) => Operation.makeDurable({
271
282
  for (const pid of dropReportedToolResults(toolResults, (pid) => toolCallManager.isReported(pid))) log("drop tool result reported within its turn", { pid }, {
272
283
  "~LogMeta": "~LogMeta",
273
284
  F: __dxlog_file$1,
274
- L: 364,
285
+ L: 400,
275
286
  S: this
276
287
  });
277
288
  if (pendingWork(state)) {
278
289
  yield* reportSleeping(state);
279
290
  return;
280
291
  }
281
- if (!turnRan) {
292
+ if (!turnRan && !endHooksFired) {
282
293
  log("agent idle before its first turn, staying resident", void 0, {
283
294
  "~LogMeta": "~LogMeta",
284
295
  F: __dxlog_file$1,
285
- L: 375,
296
+ L: 411,
286
297
  S: this
287
298
  });
288
299
  return;
289
300
  }
290
- yield* runEndRequestHooks;
291
- const after = yield* sessionStore.loadPending(feed);
292
- if (pendingWork(after)) {
293
- log("agent work enqueued by end-request hook, continuing", void 0, {
301
+ if (!endHooksFired) {
302
+ endHooksFired = true;
303
+ yield* runEndRequestHooks;
304
+ const after = yield* sessionStore.loadPending(feed);
305
+ if (pendingWork(after)) {
306
+ log("agent work enqueued by end-request hook, continuing", void 0, {
307
+ "~LogMeta": "~LogMeta",
308
+ F: __dxlog_file$1,
309
+ L: 422,
310
+ S: this
311
+ });
312
+ yield* reconcileAlarmWith(after);
313
+ yield* reportSleeping(after);
314
+ return;
315
+ }
316
+ }
317
+ if (asyncHooks.length > 0) {
318
+ log("awaiting background end-request hooks", { count: asyncHooks.length }, {
294
319
  "~LogMeta": "~LogMeta",
295
320
  F: __dxlog_file$1,
296
- L: 384,
321
+ L: 432,
322
+ S: this
323
+ });
324
+ return;
325
+ }
326
+ if (options.resident) {
327
+ log("agent work complete, staying resident", void 0, {
328
+ "~LogMeta": "~LogMeta",
329
+ F: __dxlog_file$1,
330
+ L: 437,
297
331
  S: this
298
332
  });
299
- yield* reconcileAlarmWith(after);
300
- yield* reportSleeping(after);
301
333
  return;
302
334
  }
303
335
  log("agent work complete, succeeding", void 0, {
304
336
  "~LogMeta": "~LogMeta",
305
337
  F: __dxlog_file$1,
306
- L: 390,
338
+ L: 441,
307
339
  S: this
308
340
  });
309
341
  ctx.succeed();
@@ -315,7 +347,7 @@ var AgentProcess = (options) => Operation.makeDurable({
315
347
  log.info("discarding queue entries left by a previous process", { count: startupPending.pendingMessages.length }, {
316
348
  "~LogMeta": "~LogMeta",
317
349
  F: __dxlog_file$1,
318
- L: 403,
350
+ L: 454,
319
351
  S: this
320
352
  });
321
353
  yield* Effect.forEach(startupPending.pendingMessages, (message) => sessionStore.ack(feed, message), { discard: true });
@@ -347,7 +379,7 @@ var AgentProcess = (options) => Operation.makeDurable({
347
379
  log("agent onInput received", { backlog: toolResults.length }, {
348
380
  "~LogMeta": "~LogMeta",
349
381
  F: __dxlog_file$1,
350
- L: 432,
382
+ L: 483,
351
383
  S: this
352
384
  });
353
385
  const message = makeInputMessage(input);
@@ -358,7 +390,7 @@ var AgentProcess = (options) => Operation.makeDurable({
358
390
  log("agent onInput enqueued to feed", void 0, {
359
391
  "~LogMeta": "~LogMeta",
360
392
  F: __dxlog_file$1,
361
- L: 438,
393
+ L: 489,
362
394
  S: this
363
395
  });
364
396
  }),
@@ -366,15 +398,16 @@ var AgentProcess = (options) => Operation.makeDurable({
366
398
  log("agent onAlarm fired", { backlog: toolResults.length }, {
367
399
  "~LogMeta": "~LogMeta",
368
400
  F: __dxlog_file$1,
369
- L: 442,
401
+ L: 493,
370
402
  S: this
371
403
  });
372
404
  markWork("agent.wake");
373
- yield* Trace.emitRequestPhase("preparing");
405
+ const announced = toolResults.length > 0 || unseenWriteIds.size > 0;
406
+ if (announced) yield* Trace.emitRequestPhase("preparing");
374
407
  for (const pid of dropReportedToolResults(toolResults, (pid) => toolCallManager.isReported(pid))) log.info("skip tool result that was reported synchronously", { pid }, {
375
408
  "~LogMeta": "~LogMeta",
376
409
  F: __dxlog_file$1,
377
- L: 451,
410
+ L: 506,
378
411
  S: this
379
412
  });
380
413
  const toolResult = toolResults.shift();
@@ -384,7 +417,7 @@ var AgentProcess = (options) => Operation.makeDurable({
384
417
  log("agent onAlarm handling", { tag: toolResult._tag }, {
385
418
  "~LogMeta": "~LogMeta",
386
419
  F: __dxlog_file$1,
387
- L: 459,
420
+ L: 514,
388
421
  S: this
389
422
  });
390
423
  prompt = toolResultPrompt(toolResult);
@@ -411,7 +444,7 @@ var AgentProcess = (options) => Operation.makeDurable({
411
444
  }, {
412
445
  "~LogMeta": "~LogMeta",
413
446
  F: __dxlog_file$1,
414
- L: 491,
447
+ L: 546,
415
448
  S: this
416
449
  });
417
450
  unseenWriteIds.delete(message.id);
@@ -428,7 +461,7 @@ var AgentProcess = (options) => Operation.makeDurable({
428
461
  log.warn("agent self-wake budget spent, dropping alarm", { wakes: selfWakes }, {
429
462
  "~LogMeta": "~LogMeta",
430
463
  F: __dxlog_file$1,
431
- L: 504,
464
+ L: 559,
432
465
  S: this
433
466
  });
434
467
  yield* sessionStore.ack(feed, dueAlarm);
@@ -447,7 +480,7 @@ var AgentProcess = (options) => Operation.makeDurable({
447
480
  }, {
448
481
  "~LogMeta": "~LogMeta",
449
482
  F: __dxlog_file$1,
450
- L: 515,
483
+ L: 570,
451
484
  S: this
452
485
  });
453
486
  dequeued = dueAlarm;
@@ -467,7 +500,7 @@ var AgentProcess = (options) => Operation.makeDurable({
467
500
  }, {
468
501
  "~LogMeta": "~LogMeta",
469
502
  F: __dxlog_file$1,
470
- L: 533,
503
+ L: 588,
471
504
  S: this
472
505
  });
473
506
  yield* ctx.setAlarm(UNSEEN_WRITE_RETRY_MS);
@@ -479,7 +512,7 @@ var AgentProcess = (options) => Operation.makeDurable({
479
512
  }, {
480
513
  "~LogMeta": "~LogMeta",
481
514
  F: __dxlog_file$1,
482
- L: 544,
515
+ L: 599,
483
516
  S: this
484
517
  });
485
518
  yield* reconcileAlarmWith(state);
@@ -488,7 +521,7 @@ var AgentProcess = (options) => Operation.makeDurable({
488
521
  log("agent onAlarm empty queue", {}, {
489
522
  "~LogMeta": "~LogMeta",
490
523
  F: __dxlog_file$1,
491
- L: 551,
524
+ L: 606,
492
525
  S: this
493
526
  });
494
527
  yield* reconcileAlarmWith(state);
@@ -496,18 +529,19 @@ var AgentProcess = (options) => Operation.makeDurable({
496
529
  return;
497
530
  }
498
531
  }
532
+ if (!announced) yield* Trace.emitRequestPhase("preparing");
499
533
  const [mcpServers] = yield* Effect.all([loadSpaceMcpServers(), Effect.gen(function* () {
500
534
  if (dequeued !== void 0) yield* sessionStore.markInFlight(feed, dequeued);
501
535
  log("begin request", { prompt }, {
502
536
  "~LogMeta": "~LogMeta",
503
537
  F: __dxlog_file$1,
504
- L: 570,
538
+ L: 629,
505
539
  S: this
506
540
  });
507
541
  log("trace agent request begin", void 0, {
508
542
  "~LogMeta": "~LogMeta",
509
543
  F: __dxlog_file$1,
510
- L: 571,
544
+ L: 630,
511
545
  S: this
512
546
  });
513
547
  yield* Trace.write(Trace.AgentRequestBegin, {});
@@ -529,10 +563,11 @@ var AgentProcess = (options) => Operation.makeDurable({
529
563
  log("end request", void 0, {
530
564
  "~LogMeta": "~LogMeta",
531
565
  F: __dxlog_file$1,
532
- L: 605,
566
+ L: 664,
533
567
  S: this
534
568
  });
535
569
  turnRan = true;
570
+ endHooksFired = false;
536
571
  yield* ToolResultsCell.set(toolResults);
537
572
  if (dequeued !== void 0) {
538
573
  yield* sessionStore.ack(feed, dequeued);
@@ -555,7 +590,7 @@ var AgentProcess = (options) => Operation.makeDurable({
555
590
  }, {
556
591
  "~LogMeta": "~LogMeta",
557
592
  F: __dxlog_file$1,
558
- L: 626,
593
+ L: 686,
559
594
  S: this
560
595
  });
561
596
  yield* Trace.write(Trace.DelegationSpawned, {
@@ -576,15 +611,29 @@ var AgentProcess = (options) => Operation.makeDurable({
576
611
  log("childEvent", { event }, {
577
612
  "~LogMeta": "~LogMeta",
578
613
  F: __dxlog_file$1,
579
- L: 653,
614
+ L: 713,
580
615
  S: this
581
616
  });
617
+ if (event._tag === "exited" && asyncHooks.includes(event.pid)) {
618
+ asyncHooks = asyncHooks.filter((pid) => pid !== event.pid);
619
+ yield* AsyncHooksCell.set(asyncHooks);
620
+ const exit = yield* (yield* Process.ManagerService).attach(event.pid).pipe(Effect.flatMap(Process.awaitOutput), Effect.exit);
621
+ if (Exit.isFailure(exit)) log.warn("background end-request hook failed", { pid: event.pid }, {
622
+ "~LogMeta": "~LogMeta",
623
+ F: __dxlog_file$1,
624
+ L: 721,
625
+ S: this
626
+ });
627
+ yield* maybeComplete;
628
+ return;
629
+ }
582
630
  if (event._tag === "exited") {
583
631
  const delegation = delegations.find((delegation) => delegation.pid === event.pid);
584
632
  if (delegation) {
585
633
  delegations = delegations.filter((other) => other.pid !== event.pid);
586
634
  yield* DelegationsCell.set(delegations);
587
- const exit = yield* (yield* (yield* ProcessManager.ProcessOperationInvoker.Service).attachFiber(event.pid).pipe(Effect.orDie)).await;
635
+ const child = yield* (yield* Process.ManagerService).attach(event.pid);
636
+ const exit = yield* Process.awaitOutput(child).pipe(Effect.exit);
588
637
  const result = Exit.isSuccess(exit) ? resultPreview(exit.value) : void 0;
589
638
  yield* Trace.write(Trace.DelegationCompleted, {
590
639
  taskId: delegation.id,
@@ -608,7 +657,7 @@ var AgentProcess = (options) => Operation.makeDurable({
608
657
  }, {
609
658
  "~LogMeta": "~LogMeta",
610
659
  F: __dxlog_file$1,
611
- L: 685,
660
+ L: 757,
612
661
  S: this
613
662
  });
614
663
  yield* Trace.write(Trace.DelegationSpawned, {
@@ -625,25 +674,22 @@ var AgentProcess = (options) => Operation.makeDurable({
625
674
  }, {
626
675
  "~LogMeta": "~LogMeta",
627
676
  F: __dxlog_file$1,
628
- L: 692,
677
+ L: 764,
629
678
  S: this
630
679
  });
631
680
  yield* maybeComplete;
632
681
  } else if (toolCallManager.isToolCall(event.pid)) {
633
- const attachExit = yield* (yield* ProcessManager.ProcessOperationInvoker.Service).attachFiber(event.pid).pipe(Effect.exit);
634
- if (Exit.isFailure(attachExit)) {
635
- if (toolCallManager.isToolCall(event.pid) || toolResults.some((item) => item.pid === event.pid) || toolCallManager.isReported(event.pid)) {
636
- log.verbose("childEvent skipped (process gone, result already handled)", { pid: event.pid }, {
637
- "~LogMeta": "~LogMeta",
638
- F: __dxlog_file$1,
639
- L: 705,
640
- S: this
641
- });
642
- return;
643
- }
644
- return yield* Effect.failCause(attachExit.cause).pipe(Effect.orDie);
682
+ const attachExit = yield* (yield* Process.ManagerService).attach(event.pid).pipe(Effect.exit);
683
+ if (Exit.isFailure(attachExit) && isToolResultHandled(toolResults, event.pid, (pid) => toolCallManager.isReported(pid))) {
684
+ log.verbose("childEvent skipped (process gone, result already handled)", { pid: event.pid }, {
685
+ "~LogMeta": "~LogMeta",
686
+ F: __dxlog_file$1,
687
+ L: 775,
688
+ S: this
689
+ });
690
+ return;
645
691
  }
646
- const result = yield* attachExit.value.await.pipe(Effect.orDie).pipe(Effect.map(Exit.match({
692
+ const result = yield* (Exit.isFailure(attachExit) ? Effect.failCause(attachExit.cause) : Process.awaitOutput(attachExit.value)).pipe(Effect.exit, Effect.map(Exit.match({
647
693
  onSuccess: (value) => ({
648
694
  _tag: "tool_result",
649
695
  pid: event.pid,
@@ -664,7 +710,7 @@ var AgentProcess = (options) => Operation.makeDurable({
664
710
  }, {
665
711
  "~LogMeta": "~LogMeta",
666
712
  F: __dxlog_file$1,
667
- L: 730,
713
+ L: 803,
668
714
  S: this
669
715
  });
670
716
  yield* ToolResultsCell.set(toolResults);
@@ -672,19 +718,22 @@ var AgentProcess = (options) => Operation.makeDurable({
672
718
  log("agent onChildEvent alarm scheduled", { depth: toolResults.length }, {
673
719
  "~LogMeta": "~LogMeta",
674
720
  F: __dxlog_file$1,
675
- L: 733,
721
+ L: 806,
676
722
  S: this
677
723
  });
678
- } else log.verbose("childEvent ignored non-tool call and not a delegation", { pid: event.pid }, {
679
- "~LogMeta": "~LogMeta",
680
- F: __dxlog_file$1,
681
- L: 735,
682
- S: this
683
- });
724
+ } else {
725
+ log.verbose("childEvent ignored non-tool call and not a delegation", { pid: event.pid }, {
726
+ "~LogMeta": "~LogMeta",
727
+ F: __dxlog_file$1,
728
+ L: 808,
729
+ S: this
730
+ });
731
+ untrackedExits.add(event.pid);
732
+ }
684
733
  }
685
734
  })
686
735
  };
687
- }));
736
+ })));
688
737
  var ToolResultEvent = Schema.TaggedStruct("tool_result", {
689
738
  pid: Process.ID,
690
739
  result: Schema.Unknown,
@@ -700,6 +749,8 @@ var Delegation = Schema.Struct({
700
749
  id: Schema.String
701
750
  }).mapFields(Struct.map(Schema.mutableKey));
702
751
  var DelegationsCell = StorageService.cell(Schema.fromJsonString(Schema.Array(Delegation).pipe(Schema.mutable)), "delegations").pipe(StorageService.withDefault(() => []));
752
+ /** Pids of background ({@link Skill.Hook.async}) end-request hooks the process is waiting on. */
753
+ var AsyncHooksCell = StorageService.cell(Schema.fromJsonString(Schema.Array(Process.ID).pipe(Schema.mutable)), "asyncHooks").pipe(StorageService.withDefault(() => []));
703
754
  /**
704
755
  * Ids of queue entries this process has ACKED, held until the ack is visible in the feed read.
705
756
  *
@@ -777,7 +828,7 @@ var ToolCallManager = class {
777
828
  log("reconcile queued tool result", { pid: item.pid }, {
778
829
  "~LogMeta": "~LogMeta",
779
830
  F: __dxlog_file$1,
780
- L: 890,
831
+ L: 971,
781
832
  S: this
782
833
  });
783
834
  }
@@ -789,6 +840,11 @@ var ToolCallManager = class {
789
840
  /** True while the agent still has queued work, a pending alarm, subprocesses, or undelivered tool results. */
790
841
  var isAgentWorkPending = ({ toolResults, pendingMessages, pendingAlarms, delegations, toolCallManager }) => toolResults.length > 0 || pendingMessages.length > 0 || pendingAlarms.length > 0 || delegations.length > 0 || toolCallManager.hasPendingToolResults();
791
842
  /**
843
+ * Whether a tool call's result is already accounted for — queued for the next turn or delivered to the
844
+ * agent — so a finished child that can no longer be reattached needs no answer of its own.
845
+ */
846
+ var isToolResultHandled = (queue, pid, isReported) => queue.some((item) => item.pid === pid) || isReported(pid);
847
+ /**
792
848
  * Discards tool results at the head of the queue whose values already reached the agent.
793
849
  *
794
850
  * A tool that returned inside its turn is reported synchronously AND left queued; after a reload the
@@ -838,7 +894,7 @@ var wakeUpPrompt = (firedAt, message, budget) => {
838
894
  ].filter((line) => line != null).join("\n");
839
895
  };
840
896
  var ToolExecutionService = ({ enableBackgrounding, backgroundThreshold = Duration.seconds(1), toolCallManager, feed }) => Layer.unwrap(Effect.gen(function* () {
841
- const operationInvoker = yield* ProcessManager.ProcessOperationInvoker.Service;
897
+ const childServices = yield* Effect.context();
842
898
  return makeToolExecutionService({ invoke: (tool, input) => Effect.gen(function* () {
843
899
  const operationDef = getOperationFromTool(tool).pipe(Option.getOrThrow);
844
900
  log("invoking operation", {
@@ -847,37 +903,37 @@ var ToolExecutionService = ({ enableBackgrounding, backgroundThreshold = Duratio
847
903
  }, {
848
904
  "~LogMeta": "~LogMeta",
849
905
  F: __dxlog_file$1,
850
- L: 1024,
906
+ L: 1117,
851
907
  S: this
852
908
  });
853
- const fiber = yield* operationInvoker.invokeFiber(operationDef, input, {
909
+ const handle = yield* Process.spawn(OperationProcess.make(operationDef), input, {
854
910
  environment: { conversation: Obj.getURI(feed) },
855
911
  traceMeta: { conversation: Ref.make(feed) }
856
912
  });
857
913
  markWork("tool.spawned");
858
- yield* toolCallManager.beginCall(fiber.pid);
914
+ yield* toolCallManager.beginCall(handle.pid);
859
915
  markWork("tool.call-recorded");
860
916
  log("invoked operation", {
861
917
  operationDef,
862
918
  input,
863
- fiber
919
+ pid: handle.pid
864
920
  }, {
865
921
  "~LogMeta": "~LogMeta",
866
922
  F: __dxlog_file$1,
867
- L: 1036,
923
+ L: 1129,
868
924
  S: this
869
925
  });
870
- const awaitWithReport = fiber.await.pipe(Effect.tap(() => toolCallManager.markAsReported(fiber.pid)));
871
- const result = enableBackgrounding ? yield* awaitWithReport.pipe(Effect.timeout(backgroundThreshold), Effect.catchTag("TimeoutError", () => Effect.succeed(Exit.succeed(toolIsRunningInBackgroundResponse(fiber.pid))))) : yield* awaitWithReport;
926
+ const awaitWithReport = Process.awaitOutput(handle).pipe(Effect.exit, Effect.tap(() => toolCallManager.markAsReported(handle.pid)));
927
+ const result = enableBackgrounding ? yield* awaitWithReport.pipe(Effect.timeout(backgroundThreshold), Effect.catchTag("TimeoutError", () => Effect.succeed(Exit.succeed(toolIsRunningInBackgroundResponse(handle.pid))))) : yield* awaitWithReport;
872
928
  markWork("tool.settled");
873
929
  log("result", { result }, {
874
930
  "~LogMeta": "~LogMeta",
875
931
  F: __dxlog_file$1,
876
- L: 1048,
932
+ L: 1144,
877
933
  S: this
878
934
  });
879
935
  return yield* result;
880
- }) });
936
+ }).pipe(Effect.provide(childServices)) });
881
937
  }));
882
938
  var AsynchronousExectionToolkit = class extends Toolkit.make(Tool.make("poll-tools", {
883
939
  description: trim`
@@ -900,12 +956,16 @@ var AsynchronousExectionToolkit = class extends Toolkit.make(Tool.make("poll-too
900
956
  })
901
957
  })) {};
902
958
  var AsynchronousExectionToolkitLayer = AsynchronousExectionToolkit.toLayer(Effect.gen(function* () {
903
- const invoker = yield* ProcessManager.ProcessOperationInvoker.Service;
959
+ const manager = yield* Process.ManagerService;
904
960
  return { "poll-tools": ({ ids, wait, timeout = 1e4 }) => Effect.gen(function* () {
905
- 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({
906
- onSuccess: (value) => Effect.succeed(toolResultResponse(pid, value)),
907
- onFailure: (cause) => Effect.succeed(toolErrorResponse(pid, Cause.pretty(cause)))
908
- })), Effect.catchTag("ProcessNotFoundError", () => Effect.succeed(`Process not found: ${pid}`)), Effect.catchTag("TimeoutError", () => Effect.succeed(`Process still running: ${pid}`))));
961
+ return yield* Effect.forEach(ids, (pid) => Effect.gen(function* () {
962
+ const attached = yield* manager.attach(Process.ID.make(pid)).pipe(Effect.exit);
963
+ if (Exit.isFailure(attached)) return `Process not found: ${pid}`;
964
+ return yield* Process.awaitOutput(attached.value).pipe(Effect.exit, Effect.timeout(Duration.millis(timeout)), Effect.map(Exit.match({
965
+ onSuccess: (value) => toolResultResponse(pid, value),
966
+ onFailure: (cause) => toolErrorResponse(pid, Cause.pretty(cause))
967
+ })), Effect.catchTag("TimeoutError", () => Effect.succeed(`Process still running: ${pid}`)));
968
+ }));
909
969
  }) };
910
970
  }));
911
971
  /**
@@ -981,17 +1041,39 @@ var layer = (opts) => Layer.effect(AgentService.AgentService, Effect.gen(functio
981
1041
  defaultModel: opts?.defaultModel,
982
1042
  provider: provider ?? opts?.provider,
983
1043
  enableToolBackgrounding: opts?.enableToolBackgrounding,
984
- delegationStrategy: opts?.delegationStrategy
1044
+ delegationStrategy: opts?.delegationStrategy,
1045
+ resident: opts?.resident
985
1046
  });
1047
+ /**
1048
+ * The process a chat runs on: the one its session names, or ours. A name nothing contributed
1049
+ * (its plugin was removed) falls back to ours rather than leaving the chat unable to run.
1050
+ */
1051
+ const executableFor = (chat, provider) => {
1052
+ const key = chat.session?.process;
1053
+ if (key === void 0 || key === "org.dxos.testing.process.agent") return makeExecutable(provider);
1054
+ const definition = opts?.processes?.().find((candidate) => candidate.key === key);
1055
+ if (!definition) {
1056
+ log.warn("agent process not contributed; running the default", { key }, {
1057
+ "~LogMeta": "~LogMeta",
1058
+ F: __dxlog_file,
1059
+ L: 201,
1060
+ S: this
1061
+ });
1062
+ return makeExecutable(provider);
1063
+ }
1064
+ return definition;
1065
+ };
986
1066
  const service = {
987
1067
  getSession: (chat, options) => Effect.suspend(() => lockFor(chat.id).withPermits(1)(Effect.gen(function* () {
988
1068
  const provider = options?.provider ?? opts?.provider;
989
1069
  const model = chat.session?.model;
990
1070
  const instructions = chat.instructions?.uri;
991
1071
  const location = options?.location ?? "local";
1072
+ const executable = executableFor(chat, provider);
1073
+ if (location === "edge" && executable.key !== "org.dxos.testing.process.agent") return yield* Effect.die(/* @__PURE__ */ new Error(`Agent process ${executable.key} runs only locally, but the chat asked for edge.`));
992
1074
  const cached = sessionCache.get(chat.id);
993
1075
  if (cached) {
994
- if (cached.model === model && cached.provider === provider && cached.instructions === instructions && cached.location === location && !Process.isTerminal(cached.handle.status.state)) return cached.session;
1076
+ if (cached.model === model && cached.provider === provider && cached.instructions === instructions && cached.location === location && cached.process === executable.key && !Process.isTerminal(cached.handle.status.state)) return cached.session;
995
1077
  if (!Process.isTerminal(cached.handle.status.state)) yield* cached.handle.terminate();
996
1078
  sessionCache.delete(chat.id);
997
1079
  }
@@ -1000,7 +1082,6 @@ var layer = (opts) => Layer.effect(AgentService.AgentService, Effect.gen(functio
1000
1082
  const parsedEchoUri = EID.tryParse(target);
1001
1083
  const spaceId = parsedEchoUri ? EID.getSpaceId(parsedEchoUri) : void 0;
1002
1084
  const processLocation = locationFor(options?.location, spaceId);
1003
- const executable = makeExecutable(provider);
1004
1085
  let activeProcess = (yield* processManager.handles({
1005
1086
  target,
1006
1087
  key: executable.key,
@@ -1040,6 +1121,7 @@ var layer = (opts) => Layer.effect(AgentService.AgentService, Effect.gen(functio
1040
1121
  provider,
1041
1122
  instructions,
1042
1123
  location,
1124
+ process: executable.key,
1043
1125
  handle,
1044
1126
  session
1045
1127
  });
@@ -1047,27 +1129,41 @@ var layer = (opts) => Layer.effect(AgentService.AgentService, Effect.gen(functio
1047
1129
  }))),
1048
1130
  hydrate: Effect.fnUntraced(function* () {
1049
1131
  sessionCache.clear();
1132
+ const contributed = (opts?.processes?.() ?? []).filter(({ key }) => key !== AGENT_PROCESS_KEY);
1050
1133
  const executable = makeExecutable();
1051
- const agents = [...yield* processManager.handles({ key: AGENT_PROCESS_KEY }), ...(yield* Effect.forEach([...remoteSpaces], (space) => processManager.handles({
1052
- key: AGENT_PROCESS_KEY,
1053
- location: {
1054
- kind: "edge",
1055
- space
1056
- }
1057
- }))).flat()];
1134
+ const agents = [
1135
+ ...(yield* processManager.handles({ key: AGENT_PROCESS_KEY })).map((agent) => ({
1136
+ agent,
1137
+ executable
1138
+ })),
1139
+ ...(yield* Effect.forEach(contributed, (definition) => processManager.handles({ key: definition.key }).pipe(Effect.map((agents) => agents.map((agent) => ({
1140
+ agent,
1141
+ executable: definition
1142
+ })))))).flat(),
1143
+ ...(yield* Effect.forEach([...remoteSpaces], (space) => processManager.handles({
1144
+ key: AGENT_PROCESS_KEY,
1145
+ location: {
1146
+ kind: "edge",
1147
+ space
1148
+ }
1149
+ }))).flat().map((agent) => ({
1150
+ agent,
1151
+ executable
1152
+ }))
1153
+ ];
1058
1154
  log("agent hydrate", { count: agents.length }, {
1059
1155
  "~LogMeta": "~LogMeta",
1060
1156
  F: __dxlog_file,
1061
- L: 207,
1157
+ L: 231,
1062
1158
  S: this
1063
1159
  });
1064
- for (const agent of agents) yield* agent.hydrate(executable).pipe(Effect.catchCause((cause) => Effect.sync(() => log.warn("agent hydrate skipped", {
1160
+ for (const { agent, executable } of agents) yield* agent.hydrate(executable).pipe(Effect.catchCause((cause) => Effect.sync(() => log.warn("agent hydrate skipped", {
1065
1161
  pid: agent.pid,
1066
1162
  cause: Cause.pretty(cause)
1067
1163
  }, {
1068
1164
  "~LogMeta": "~LogMeta",
1069
1165
  F: __dxlog_file,
1070
- L: 213,
1166
+ L: 237,
1071
1167
  S: this
1072
1168
  }))));
1073
1169
  })
@@ -1102,7 +1198,7 @@ var makeSession = (process, chat, feed, releaseSession, isFinished, resubmit) =>
1102
1198
  running: Atom.make((get) => get(process.statusAtom).state === Process.State.RUNNING || get(process.statusAtom).state === Process.State.HYBERNATING),
1103
1199
  waitForCompletion: () => process.runUntilSettled(),
1104
1200
  terminate: () => process.terminate().pipe(Effect.tap(() => Effect.sync(releaseSession))),
1105
- subscribeEphemeral: () => process.subscribeEphemeral()
1201
+ subscribeEphemeral: (options) => process.subscribeEphemeral(options)
1106
1202
  });
1107
1203
  //#endregion
1108
1204
  export { AgentProcess as a, __exportAll as c, AgentInput as i, layer as n, makeInputMessage as o, AGENT_PROCESS_KEY as r, makeAiSessionTurnProducer as s, AgentService_exports as t };