@dxos/agent-runtime 0.12.0-next-73daef452f1464b62efdbca75fb0067c1066f264 → 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 (66) hide show
  1. package/dist/lib/chunk-AgentService.mjs +716 -362
  2. package/dist/lib/chunk-AgentService.mjs.map +1 -1
  3. package/dist/lib/index.mjs +2 -2
  4. package/dist/lib/testing.mjs +41 -29
  5. package/dist/lib/testing.mjs.map +1 -1
  6. package/dist/types/src/agent-service/AgentService.d.ts +27 -12
  7. package/dist/types/src/agent-service/AgentService.d.ts.map +1 -1
  8. package/dist/types/src/agent-service/agent-process.d.ts +580 -206
  9. package/dist/types/src/agent-service/agent-process.d.ts.map +1 -1
  10. package/dist/types/src/agent-service/delegation-strategy.d.ts +17 -8
  11. package/dist/types/src/agent-service/delegation-strategy.d.ts.map +1 -1
  12. package/dist/types/src/agent-service/index.d.ts +4 -4
  13. package/dist/types/src/agent-service/index.d.ts.map +1 -1
  14. package/dist/types/src/agent-service/mcp-servers.d.ts +10 -0
  15. package/dist/types/src/agent-service/mcp-servers.d.ts.map +1 -0
  16. package/dist/types/src/agent-service/mcp-servers.test.d.ts +2 -0
  17. package/dist/types/src/agent-service/mcp-servers.test.d.ts.map +1 -0
  18. package/dist/types/src/agent-service/process-selection.test.d.ts +2 -0
  19. package/dist/types/src/agent-service/process-selection.test.d.ts.map +1 -0
  20. package/dist/types/src/agent-service/queue-scripted.test.d.ts +2 -0
  21. package/dist/types/src/agent-service/queue-scripted.test.d.ts.map +1 -0
  22. package/dist/types/src/agent-service/skill-hooks.test.d.ts +2 -0
  23. package/dist/types/src/agent-service/skill-hooks.test.d.ts.map +1 -0
  24. package/dist/types/src/agent-service/tool-backgrounding.test.d.ts +2 -0
  25. package/dist/types/src/agent-service/tool-backgrounding.test.d.ts.map +1 -0
  26. package/dist/types/src/agent-service/turn-producer.d.ts +5 -1
  27. package/dist/types/src/agent-service/turn-producer.d.ts.map +1 -1
  28. package/dist/types/src/index.d.ts +1 -1
  29. package/dist/types/src/index.d.ts.map +1 -1
  30. package/dist/types/src/testing/assistant-test-layer.d.ts +12 -10
  31. package/dist/types/src/testing/assistant-test-layer.d.ts.map +1 -1
  32. package/dist/types/src/testing/index.d.ts +4 -4
  33. package/dist/types/src/testing/index.d.ts.map +1 -1
  34. package/dist/types/src/testing/test-agent.d.ts +4 -5
  35. package/dist/types/src/testing/test-agent.d.ts.map +1 -1
  36. package/dist/types/src/testing/trace-pretty-print.d.ts +1 -1
  37. package/dist/types/src/testing/trace-pretty-print.d.ts.map +1 -1
  38. package/dist/types/tsconfig.tsbuildinfo +1 -1
  39. package/package.json +20 -18
  40. package/src/agent-service/AgentService.test.ts +351 -64
  41. package/src/agent-service/AgentService.ts +304 -149
  42. package/src/agent-service/README.md +16 -14
  43. package/src/agent-service/agent-process.test.ts +63 -131
  44. package/src/agent-service/agent-process.ts +714 -356
  45. package/src/agent-service/delegation-scripted.test.ts +9 -9
  46. package/src/agent-service/delegation-strategy.ts +25 -8
  47. package/src/agent-service/index.ts +13 -4
  48. package/src/agent-service/mcp-servers.test.ts +99 -0
  49. package/src/agent-service/mcp-servers.ts +62 -0
  50. package/src/agent-service/process-selection.test.ts +135 -0
  51. package/src/agent-service/queue-scripted.test.ts +461 -0
  52. package/src/agent-service/redelivery.test.ts +52 -28
  53. package/src/agent-service/skill-hooks.test.ts +182 -0
  54. package/src/agent-service/tool-backgrounding.test.ts +249 -0
  55. package/src/agent-service/turn-producer.ts +6 -2
  56. package/src/assistant-session-tests/format.test.ts +1 -1
  57. package/src/assistant-session-tests/request.test.ts +3 -3
  58. package/src/assistant-session-tests/scripted-loop.test.ts +99 -3
  59. package/src/assistant-session-tests/session-ollama.test.ts +1 -1
  60. package/src/assistant-session-tests/xml-response.test.ts +1 -1
  61. package/src/functions.test.ts +1 -1
  62. package/src/index.ts +1 -1
  63. package/src/testing/assistant-test-layer.ts +84 -35
  64. package/src/testing/index.ts +4 -4
  65. package/src/testing/test-agent.ts +8 -9
  66. package/src/testing/trace-pretty-print.ts +3 -4
@@ -1,33 +1,38 @@
1
1
  import * as Cause from "effect/Cause";
2
2
  import * as Effect from "effect/Effect";
3
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 { ProcessManager } from "@dxos/compute-runtime";
7
- import { AgentService, getSession } from "@dxos/compute/AgentService";
4
+ import * as Atom from "effect/reactivity/Atom";
5
+ import * as Semaphore from "effect/Semaphore";
6
+ import { AiContext, AiSession, Alarm, HarnessControl, SessionStore, SkillHooks, getOperationFromTool, makeToolExecutionService, makeToolResolverFromOperations } from "@dxos/assistant";
7
+ import * as Chat from "@dxos/assistant/Chat";
8
+ import * as AgentService from "@dxos/compute/AgentService";
8
9
  import * as Process from "@dxos/compute/Process";
9
10
  import * as Skill from "@dxos/compute/Skill";
10
- import { Annotation, Database, Feed, Obj, Ref, Registry } from "@dxos/echo";
11
- import { EffectEx } from "@dxos/effect";
11
+ import { Annotation, Database, Feed, Filter, Obj, Ref, Registry } from "@dxos/echo";
12
+ import * as EffectEx from "@dxos/effect/EffectEx";
12
13
  import { DXN, EID } from "@dxos/keys";
13
14
  import { log } from "@dxos/log";
15
+ import * as Tool from "effect/ai/Tool";
16
+ import * as Toolkit from "effect/ai/Toolkit";
14
17
  import * as Clock from "effect/Clock";
15
18
  import * as DateTime from "effect/DateTime";
16
19
  import * as Duration from "effect/Duration";
17
20
  import * as Exit from "effect/Exit";
18
- import * as Match from "effect/Match";
21
+ import * as Option from "effect/Option";
19
22
  import * as Schema from "effect/Schema";
20
23
  import * as Struct from "effect/Struct";
21
- import * as Tool from "effect/unstable/ai/Tool";
22
- import * as Toolkit from "effect/unstable/ai/Toolkit";
23
- import { AiService, OpaqueToolkit } from "@dxos/ai";
24
+ import { AiService, Model, OpaqueToolkit } from "@dxos/ai";
25
+ import { OperationProcess } from "@dxos/compute-runtime";
24
26
  import * as Credential from "@dxos/compute/Credential";
25
- import * as Instructions from "@dxos/compute/Instructions";
27
+ import * as McpServer from "@dxos/compute/McpServer";
26
28
  import * as Operation from "@dxos/compute/Operation";
29
+ import * as OperationHandlerSet from "@dxos/compute/OperationHandlerSet";
27
30
  import * as StorageService from "@dxos/compute/StorageService";
28
31
  import * as Trace from "@dxos/compute/Trace";
29
- import { ContentBlock } from "@dxos/types";
30
- import { trim } from "@dxos/util";
32
+ import { AccessToken } from "@dxos/link";
33
+ import { Actor, ContentBlock, Message } from "@dxos/types";
34
+ import { markWork, trim } from "@dxos/util";
35
+ import { McpOAuth } from "@dxos/mcp-client";
31
36
  //#region \0rolldown/runtime.js
32
37
  var __defProp = Object.defineProperty;
33
38
  var __exportAll = (all, no_symbols) => {
@@ -40,6 +45,51 @@ var __exportAll = (all, no_symbols) => {
40
45
  return target;
41
46
  };
42
47
  //#endregion
48
+ //#region src/agent-service/mcp-servers.ts
49
+ var __dxlog_file$2 = "/__w/dxos/dxos/packages/core/compute/agent-runtime/src/agent-service/mcp-servers.ts";
50
+ /**
51
+ * The space's enabled MCP servers as connection options, read at the start of every turn so a server
52
+ * added, toggled or signed in to from the chat's options applies from the next turn on.
53
+ */
54
+ var loadSpaceMcpServers = () => Effect.gen(function* () {
55
+ const servers = yield* Database.query(Filter.type(McpServer.McpServer)).run;
56
+ return yield* Effect.forEach(servers.filter((server) => server.enabled !== false), toOptions);
57
+ });
58
+ var toOptions = (server) => Effect.gen(function* () {
59
+ const options = {
60
+ url: server.url,
61
+ protocol: server.protocol,
62
+ apiKey: server.apiKey
63
+ };
64
+ if (server.oauth?.tokens) return {
65
+ ...options,
66
+ authProvider: McpOAuth.makeProvider({ store: McpServer.oauthStore(server) })
67
+ };
68
+ if (server.accessToken) {
69
+ const apiKey = yield* resolveAccessToken(server.accessToken);
70
+ return Option.isSome(apiKey) ? {
71
+ ...options,
72
+ apiKey: apiKey.value
73
+ } : options;
74
+ }
75
+ return options;
76
+ });
77
+ /**
78
+ * Resolves through {@link Credential.CredentialsService} rather than reading `token` directly, so a
79
+ * token EDGE custodies (a managed one) resolves the same way a pasted one does.
80
+ */
81
+ var resolveAccessToken = (ref) => Effect.gen(function* () {
82
+ const accessToken = yield* Database.load(ref);
83
+ const credentials = yield* Credential.CredentialsService;
84
+ const credential = yield* Effect.tryPromise(() => credentials.getCredential({ accessTokenId: accessToken.id }));
85
+ return Option.fromNullishOr(credential.apiKey);
86
+ }).pipe(Effect.tapError((error) => Effect.sync(() => log.warn("failed to resolve MCP server credential", { error }, {
87
+ "~LogMeta": "~LogMeta",
88
+ F: __dxlog_file$2,
89
+ L: 60,
90
+ S: void 0
91
+ }))), Effect.orElseSucceed(() => Option.none()));
92
+ //#endregion
43
93
  //#region src/agent-service/turn-producer.ts
44
94
  /**
45
95
  * The default producer: `AiSession.Session`, which resolves the model, binds skills and context
@@ -57,224 +107,477 @@ var makeAiSessionTurnProducer = ({ feed, runtime, instructions }) => EffectEx.ac
57
107
  //#region src/agent-service/agent-process.ts
58
108
  var __dxlog_file$1 = "/__w/dxos/dxos/packages/core/compute/agent-runtime/src/agent-service/agent-process.ts";
59
109
  var AGENT_PROCESS_KEY = "org.dxos.testing.process.agent";
110
+ var AgentPrompt = Schema.Union([Schema.String, Schema.Array(ContentBlock.Any)]);
111
+ /**
112
+ * Who sent a prompt: the plain-data subset of `Actor`, since the input crosses a JSON boundary
113
+ * (EDGE decodes it with the schema's type side) where a `Ref` cannot be supplied.
114
+ */
115
+ var AgentInputSender = Actor.Actor.mapFields(Struct.pick([
116
+ "role",
117
+ "name",
118
+ "identityDid",
119
+ "email"
120
+ ]));
121
+ /**
122
+ * Input accepted by {@link AgentProcess}: a bare prompt, or a prompt attributed to a sender (e.g. a
123
+ * relayed Discord author) whose identity the appended `Message` records.
124
+ */
125
+ var AgentInput = Schema.Union([AgentPrompt, Schema.Struct({
126
+ prompt: AgentPrompt,
127
+ sender: Schema.optional(AgentInputSender),
128
+ /** Foreign identity and other source metadata copied onto the message (e.g. `{ discord: { userId } }`). */
129
+ properties: Schema.optional(Schema.Record(Schema.String, Schema.Any))
130
+ })]);
131
+ /** Builds the feed message for an input; the sender defaults to the plain user role. */
132
+ var makeInputMessage = (input) => {
133
+ const { prompt, sender, properties } = typeof input === "object" && "prompt" in input ? input : {
134
+ prompt: input,
135
+ sender: void 0,
136
+ properties: void 0
137
+ };
138
+ const blocks = typeof prompt === "string" ? [ContentBlock.Text.make({ text: prompt })] : [...prompt];
139
+ return Message.make({
140
+ sender: {
141
+ role: "user",
142
+ ...sender
143
+ },
144
+ blocks,
145
+ properties
146
+ });
147
+ };
148
+ /**
149
+ * How long to wait before re-reading a queue that contradicts a write this process just made, and
150
+ * how many times. A hosted runtime serves the read from an eventually-consistent index, and the lag
151
+ * measured on EDGE is single-digit milliseconds — the cap is what keeps a write that never lands
152
+ * from waking the agent forever.
153
+ */
154
+ var UNSEEN_WRITE_RETRY_MS = 250;
155
+ /** How much of a sub-agent's result the trace keeps: enough to read, never the whole payload. */
156
+ var RESULT_PREVIEW_LENGTH = 200;
157
+ /**
158
+ * A short, safe rendering of a sub-agent's result for the trace.
159
+ *
160
+ * Guarded because this sits between removing the child from `delegations` and running the strategy's
161
+ * `onComplete`: a `BigInt` or a cycle would throw here, become a defect, and take the whole return
162
+ * path with it — the work item would never be updated and work waiting on it never reconciled.
163
+ * Losing the preview is survivable; losing the return is not.
164
+ */
165
+ var resultPreview = (value) => {
166
+ let text;
167
+ try {
168
+ text = typeof value === "string" ? value : JSON.stringify(value);
169
+ } catch {
170
+ return;
171
+ }
172
+ if (text === void 0) return;
173
+ return text.length > RESULT_PREVIEW_LENGTH ? `${text.slice(0, RESULT_PREVIEW_LENGTH)}…` : text;
174
+ };
175
+ var MAX_UNSEEN_WRITE_WAKES = 20;
60
176
  /**
61
177
  * Hosts a persistent, suspendible AiAgent that can process a number of prompts.
62
178
  * The process target is a queue DXN string.
63
179
  */
64
- var AgentProcess = (options) => Process.make({
180
+ var AgentProcess = (options) => Operation.makeDurable({
65
181
  key: AGENT_PROCESS_KEY,
66
- input: Schema.Union([Schema.String, Schema.Array(ContentBlock.Any)]),
182
+ input: AgentInput,
67
183
  output: Schema.Void,
184
+ types: [
185
+ Chat.Chat,
186
+ Feed.Feed,
187
+ Message.Message,
188
+ Alarm.Alarm,
189
+ AiContext.Binding,
190
+ Skill.Skill,
191
+ McpServer.McpServer,
192
+ AccessToken.AccessToken
193
+ ],
68
194
  services: [
69
195
  Database.Service,
70
196
  OpaqueToolkit.OpaqueToolkitProvider,
71
197
  Operation.Service,
72
198
  Registry.Service,
73
199
  StorageService.StorageService,
74
- ProcessManager.ProcessOperationInvoker.Service,
200
+ Process.ManagerService,
201
+ OperationHandlerSet.OperationHandlerProvider,
75
202
  AiService.AiService,
76
203
  Credential.CredentialsService
77
204
  ],
78
205
  rpcs: HarnessControl
79
- }, (ctx) => Effect.gen(function* () {
80
- const feedDxn = Annotation.getDictionary(ctx.params.annotations, Process.TargetAnnotation).pipe(Option.getOrUndefined);
81
- if (feedDxn == null) return yield* Effect.die(/* @__PURE__ */ new Error("Agent executable requires spawn options.target set to a queue DXN."));
82
- const feed = yield* Database.resolve(feedDxn, Feed.Feed).pipe(Effect.orDie);
83
- const instructionsDxn = Annotation.getDictionary(ctx.params.annotations, Process.InstructionsAnnotation).pipe(Option.getOrUndefined);
84
- const instructions = instructionsDxn ? yield* Database.resolve(instructionsDxn, Instructions.Instructions).pipe(Effect.orElseSucceed(() => void 0)) : void 0;
206
+ }).pipe(Operation.withDurableHandler((ctx) => Effect.gen(function* () {
207
+ const chatDxn = Annotation.getDictionary(ctx.params.annotations, Process.TargetAnnotation).pipe(Option.getOrUndefined);
208
+ if (chatDxn == null) return yield* Effect.die(/* @__PURE__ */ new Error("Agent executable requires spawn options.target set to a Chat DXN."));
209
+ const chat = yield* Database.resolve(chatDxn, Chat.Chat).pipe(Effect.orDie);
210
+ const feed = yield* Database.load(chat.feed).pipe(Effect.orDie);
211
+ const instructions = chat.instructions ? yield* Database.load(chat.instructions).pipe(Effect.orElseSucceed(() => void 0)) : void 0;
85
212
  const runtime = yield* Effect.context();
86
213
  const session = yield* (options.makeTurnProducer ?? makeAiSessionTurnProducer)({
214
+ chat,
87
215
  feed,
88
216
  runtime,
89
217
  instructions: instructions ? [instructions] : []
90
218
  });
91
- let inputQueue = [...yield* AgentEventsCell.get];
92
- const storageService = yield* StorageService.StorageService;
93
- const toolCallManager = new ToolCallManager(storageService);
219
+ const sessionStore = new SessionStore();
220
+ let toolResults = [...yield* ToolResultsCell.get];
221
+ let ackedEntries = [...yield* AckedEntriesCell.get];
222
+ let selfWakes = yield* SelfWakesCell.get;
223
+ const toolCallManager = new ToolCallManager(yield* StorageService.StorageService);
94
224
  yield* toolCallManager.load();
95
225
  const clock = yield* Clock.Clock;
96
- const alarmManager = new AlarmManager({
97
- storageService,
98
- setAlarm: (timeout) => ctx.setAlarm(timeout),
99
- now: () => clock.currentTimeMillisUnsafe()
100
- });
101
- yield* alarmManager.load();
102
- yield* toolCallManager.reconcileWithInputQueue(inputQueue);
226
+ const now = () => clock.currentTimeMillisUnsafe();
227
+ yield* toolCallManager.reconcileWithInputQueue(toolResults);
228
+ const unseenAlarms = /* @__PURE__ */ new Map();
229
+ const reconcileAlarmWith = (state) => {
230
+ for (const alarm of state.pendingAlarms) unseenAlarms.delete(alarm.id);
231
+ const observed = state.pendingAlarms[0]?.wakeAt;
232
+ const wakeAts = [...observed != null ? [observed] : [], ...[...unseenAlarms.values()].map(({ wakeAt }) => wakeAt)];
233
+ const delay = computeAlarmDelay({
234
+ hasPendingWork: toolResults.length > 0 || state.pendingMessages.length > 0,
235
+ wakeAt: wakeAts.length > 0 ? Math.min(...wakeAts) : null,
236
+ now: now()
237
+ });
238
+ return delay != null ? ctx.setAlarm(delay) : Effect.void;
239
+ };
240
+ const reconcileAlarm = Effect.flatMap(sessionStore.loadPending(feed), reconcileAlarmWith);
241
+ const startupPending = yield* sessionStore.loadPending(feed);
242
+ yield* reconcileAlarmWith(startupPending);
103
243
  const strategy = Option.fromNullishOr(options.delegationStrategy);
104
244
  let delegations = [...yield* DelegationsCell.get];
105
- const requestModelLayer = AiService.model(options.model ? DXN.getName(options.model) : "com.anthropic.model.claude-opus-4-8.default", { provider: options.provider });
106
- const operationInvoker = yield* ProcessManager.ProcessOperationInvoker.Service;
245
+ let asyncHooks = [...yield* AsyncHooksCell.get];
246
+ const untrackedExits = /* @__PURE__ */ new Set();
247
+ const model = chat.session?.model ?? options.defaultModel;
248
+ const requestModelLayer = AiService.languageModel(DXN.getName(model ?? Model.claudeSonnet5.id), { provider: options.provider });
249
+ const childServices = yield* Effect.context();
107
250
  const runEndRequestHooks = Effect.gen(function* () {
108
251
  yield* SkillHooks.runHooks({
109
252
  skills: session.getSkills(),
110
253
  phase: "end-request",
111
- invoke: (operation, input) => Effect.gen(function* () {
112
- 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, {
113
256
  environment: { conversation: Obj.getURI(feed) },
114
257
  traceMeta: { conversation: Ref.make(feed) }
115
- })).await;
116
- if (Exit.isFailure(exit)) return yield* Effect.failCause(exit.cause);
117
- }).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)
118
267
  });
119
268
  });
120
- const maybeComplete = Effect.gen(function* () {
121
- if (isAgentWorkPending({
122
- inputQueue,
123
- alarmManager,
124
- delegations,
125
- toolCallManager
126
- })) return;
127
- yield* runEndRequestHooks;
128
- if (isAgentWorkPending({
129
- inputQueue,
130
- alarmManager,
131
- delegations,
132
- toolCallManager
133
- })) {
134
- log("agent work enqueued by end-request hook, continuing", void 0, {
269
+ let turnRan = false;
270
+ let endHooksFired = asyncHooks.length > 0;
271
+ const unseenWriteIds = /* @__PURE__ */ new Set();
272
+ let unseenWriteWakes = 0;
273
+ const pendingWork = (state) => isAgentWorkPending({
274
+ toolResults,
275
+ pendingMessages: state.pendingMessages,
276
+ pendingAlarms: state.pendingAlarms,
277
+ delegations,
278
+ toolCallManager
279
+ });
280
+ const reportSleeping = (state) => state.pendingAlarms.length > 0 ? Trace.emitRequestPhase("sleeping") : Effect.void;
281
+ const maybeCompleteWith = (state) => Effect.gen(function* () {
282
+ for (const pid of dropReportedToolResults(toolResults, (pid) => toolCallManager.isReported(pid))) log("drop tool result reported within its turn", { pid }, {
283
+ "~LogMeta": "~LogMeta",
284
+ F: __dxlog_file$1,
285
+ L: 400,
286
+ S: this
287
+ });
288
+ if (pendingWork(state)) {
289
+ yield* reportSleeping(state);
290
+ return;
291
+ }
292
+ if (!turnRan && !endHooksFired) {
293
+ log("agent idle before its first turn, staying resident", void 0, {
294
+ "~LogMeta": "~LogMeta",
295
+ F: __dxlog_file$1,
296
+ L: 411,
297
+ S: this
298
+ });
299
+ return;
300
+ }
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 }, {
135
319
  "~LogMeta": "~LogMeta",
136
320
  F: __dxlog_file$1,
137
- L: 205,
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,
138
331
  S: this
139
332
  });
140
- alarmManager.reconcile(true);
141
333
  return;
142
334
  }
143
335
  log("agent work complete, succeeding", void 0, {
144
336
  "~LogMeta": "~LogMeta",
145
337
  F: __dxlog_file$1,
146
- L: 210,
338
+ L: 441,
147
339
  S: this
148
340
  });
149
341
  ctx.succeed();
150
342
  });
343
+ const maybeComplete = Effect.flatMap(sessionStore.loadPending(feed), maybeCompleteWith);
151
344
  return {
345
+ onSpawn: Effect.fnUntraced(function* () {
346
+ if (startupPending.pendingMessages.length > 0) {
347
+ log.info("discarding queue entries left by a previous process", { count: startupPending.pendingMessages.length }, {
348
+ "~LogMeta": "~LogMeta",
349
+ F: __dxlog_file$1,
350
+ L: 454,
351
+ S: this
352
+ });
353
+ yield* Effect.forEach(startupPending.pendingMessages, (message) => sessionStore.ack(feed, message), { discard: true });
354
+ }
355
+ }),
152
356
  rpcHandlers: yield* HarnessControl.toHandlers({
153
357
  setAlarm: Effect.fn(function* ({ at, message }) {
154
- yield* alarmManager.setWakeAt(DateTime.toEpochMillis(at), message);
155
- alarmManager.reconcile(inputQueue.length > 0);
358
+ const alarm = yield* sessionStore.setAlarm(feed, {
359
+ wakeAt: DateTime.toEpochMillis(at),
360
+ message: message ?? void 0
361
+ });
362
+ unseenAlarms.set(alarm.id, {
363
+ wakeAt: alarm.wakeAt,
364
+ wakes: 0
365
+ });
366
+ yield* reconcileAlarm;
156
367
  }),
157
368
  enqueueMessage: Effect.fn(function* ({ content }) {
158
- inputQueue.push({
159
- _tag: "prompt",
160
- content
369
+ const message = Message.make({
370
+ sender: { role: "user" },
371
+ blocks: [...content]
161
372
  });
162
- yield* AgentEventsCell.set(inputQueue);
163
- alarmManager.reconcile(true);
373
+ yield* sessionStore.enqueueMessage(feed, message);
374
+ unseenWriteIds.add(message.id);
375
+ yield* ctx.setAlarm(0);
164
376
  })
165
377
  }),
166
- onInput: Effect.fnUntraced(function* (prompt) {
167
- log("agent onInput received", { backlog: inputQueue.length }, {
168
- "~LogMeta": "~LogMeta",
169
- F: __dxlog_file$1,
170
- L: 229,
171
- S: this
172
- });
173
- const content = typeof prompt === "string" ? [ContentBlock.Text.make({ text: prompt })] : [...prompt];
174
- inputQueue.push({
175
- _tag: "prompt",
176
- content
177
- });
178
- log("agent onInput persisting queue", { depth: inputQueue.length }, {
378
+ onInput: Effect.fnUntraced(function* (input) {
379
+ log("agent onInput received", { backlog: toolResults.length }, {
179
380
  "~LogMeta": "~LogMeta",
180
381
  F: __dxlog_file$1,
181
- L: 232,
382
+ L: 483,
182
383
  S: this
183
384
  });
184
- yield* AgentEventsCell.set(inputQueue);
185
- log("agent onInput persisted", { depth: inputQueue.length }, {
385
+ const message = makeInputMessage(input);
386
+ yield* sessionStore.enqueueMessage(feed, message);
387
+ markWork("agent.prompt-queued");
388
+ unseenWriteIds.add(message.id);
389
+ yield* ctx.setAlarm(0);
390
+ log("agent onInput enqueued to feed", void 0, {
186
391
  "~LogMeta": "~LogMeta",
187
392
  F: __dxlog_file$1,
188
- L: 234,
393
+ L: 489,
189
394
  S: this
190
395
  });
191
- alarmManager.reconcile(true);
192
- log("agent onInput alarm scheduled", void 0, {
396
+ }),
397
+ onAlarm: Effect.fnUntraced(function* () {
398
+ log("agent onAlarm fired", { backlog: toolResults.length }, {
193
399
  "~LogMeta": "~LogMeta",
194
400
  F: __dxlog_file$1,
195
- L: 236,
401
+ L: 493,
196
402
  S: this
197
403
  });
198
- }),
199
- onAlarm: Effect.fnUntraced(function* () {
200
- log("agent onAlarm fired", { pending: inputQueue.length }, {
404
+ markWork("agent.wake");
405
+ const announced = toolResults.length > 0 || unseenWriteIds.size > 0;
406
+ if (announced) yield* Trace.emitRequestPhase("preparing");
407
+ for (const pid of dropReportedToolResults(toolResults, (pid) => toolCallManager.isReported(pid))) log.info("skip tool result that was reported synchronously", { pid }, {
201
408
  "~LogMeta": "~LogMeta",
202
409
  F: __dxlog_file$1,
203
- L: 240,
410
+ L: 506,
204
411
  S: this
205
412
  });
206
- const fired = yield* alarmManager.takeFiredAlarm();
207
- if (fired != null) {
208
- log("agent onAlarm self-wake", { firedAt: fired.firedAt }, {
413
+ const toolResult = toolResults.shift();
414
+ let prompt;
415
+ let dequeued;
416
+ if (toolResult !== void 0) {
417
+ log("agent onAlarm handling", { tag: toolResult._tag }, {
209
418
  "~LogMeta": "~LogMeta",
210
419
  F: __dxlog_file$1,
211
- L: 245,
420
+ L: 514,
212
421
  S: this
213
422
  });
214
- inputQueue.push({
215
- _tag: "alarm",
216
- firedAt: fired.firedAt,
217
- message: fired.message
218
- });
219
- yield* AgentEventsCell.set(inputQueue);
423
+ prompt = toolResultPrompt(toolResult);
424
+ } else {
425
+ const state = yield* sessionStore.loadPending(feed);
426
+ markWork("agent.pending-loaded");
427
+ const stillPending = /* @__PURE__ */ new Set([...state.pendingMessages.map((message) => message.id), ...state.pendingAlarms.map((alarm) => alarm.id)]);
428
+ if (ackedEntries.some((id) => !stillPending.has(id))) {
429
+ ackedEntries = ackedEntries.filter((id) => stillPending.has(id));
430
+ yield* AckedEntriesCell.set(ackedEntries);
431
+ }
432
+ const acked = new Set(ackedEntries);
433
+ const message = state.pendingMessages.find((candidate) => !acked.has(candidate.id));
434
+ const dueAlarm = state.pendingAlarms.find((alarm) => alarm.wakeAt <= now() && !acked.has(alarm.id));
435
+ for (const [id, entry] of unseenAlarms) {
436
+ if (state.pendingAlarms.some((alarm) => alarm.id === id)) continue;
437
+ entry.wakes++;
438
+ if (entry.wakes >= MAX_UNSEEN_WRITE_WAKES) unseenAlarms.delete(id);
439
+ }
440
+ if (message !== void 0) {
441
+ log("agent onAlarm handling", {
442
+ tag: "message",
443
+ id: message.id
444
+ }, {
445
+ "~LogMeta": "~LogMeta",
446
+ F: __dxlog_file$1,
447
+ L: 546,
448
+ S: this
449
+ });
450
+ unseenWriteIds.delete(message.id);
451
+ unseenWriteWakes = 0;
452
+ if (isUserPrompt(message) && selfWakes > 0) {
453
+ selfWakes = 0;
454
+ yield* SelfWakesCell.set(selfWakes);
455
+ }
456
+ dequeued = message;
457
+ prompt = [...message.blocks];
458
+ } else if (dueAlarm !== void 0) {
459
+ unseenAlarms.delete(dueAlarm.id);
460
+ if (selfWakes >= Alarm.MAX_SELF_WAKES) {
461
+ log.warn("agent self-wake budget spent, dropping alarm", { wakes: selfWakes }, {
462
+ "~LogMeta": "~LogMeta",
463
+ F: __dxlog_file$1,
464
+ L: 559,
465
+ S: this
466
+ });
467
+ yield* sessionStore.ack(feed, dueAlarm);
468
+ ackedEntries = [...ackedEntries, dueAlarm.id];
469
+ yield* AckedEntriesCell.set(ackedEntries);
470
+ const after = yield* sessionStore.loadPending(feed);
471
+ yield* reconcileAlarmWith(after);
472
+ yield* maybeCompleteWith(after);
473
+ return;
474
+ }
475
+ selfWakes++;
476
+ yield* SelfWakesCell.set(selfWakes);
477
+ log("agent onAlarm self-wake", {
478
+ firedAt: dueAlarm.wakeAt,
479
+ wakes: selfWakes
480
+ }, {
481
+ "~LogMeta": "~LogMeta",
482
+ F: __dxlog_file$1,
483
+ L: 570,
484
+ S: this
485
+ });
486
+ dequeued = dueAlarm;
487
+ prompt = [ContentBlock.Text.make({
488
+ text: wakeUpPrompt(dueAlarm.wakeAt, dueAlarm.message ?? null, {
489
+ wake: selfWakes,
490
+ max: Alarm.MAX_SELF_WAKES
491
+ }),
492
+ disposition: "synthetic"
493
+ })];
494
+ } else if (unseenWriteIds.size > 0 && unseenWriteWakes < MAX_UNSEEN_WRITE_WAKES) {
495
+ unseenWriteWakes++;
496
+ markWork("agent.unseen-write-retry");
497
+ log("agent onAlarm empty queue with an unread write, waking again", {
498
+ unseenWrites: unseenWriteIds.size,
499
+ attempt: unseenWriteWakes
500
+ }, {
501
+ "~LogMeta": "~LogMeta",
502
+ F: __dxlog_file$1,
503
+ L: 588,
504
+ S: this
505
+ });
506
+ yield* ctx.setAlarm(UNSEEN_WRITE_RETRY_MS);
507
+ return;
508
+ } else if (unseenWriteIds.size > 0) {
509
+ log.warn("agent giving up on an unread write, staying resident", {
510
+ unseenWrites: unseenWriteIds.size,
511
+ wakes: unseenWriteWakes
512
+ }, {
513
+ "~LogMeta": "~LogMeta",
514
+ F: __dxlog_file$1,
515
+ L: 599,
516
+ S: this
517
+ });
518
+ yield* reconcileAlarmWith(state);
519
+ return;
520
+ } else {
521
+ log("agent onAlarm empty queue", {}, {
522
+ "~LogMeta": "~LogMeta",
523
+ F: __dxlog_file$1,
524
+ L: 606,
525
+ S: this
526
+ });
527
+ yield* reconcileAlarmWith(state);
528
+ yield* maybeCompleteWith(state);
529
+ return;
530
+ }
220
531
  }
221
- for (const pid of dropReportedToolResults(inputQueue, (pid) => toolCallManager.isReported(pid))) log.info("skip tool result that was reported synchronously", { pid }, {
222
- "~LogMeta": "~LogMeta",
223
- F: __dxlog_file$1,
224
- L: 251,
225
- S: this
226
- });
227
- const item = inputQueue.shift();
228
- if (!item) {
229
- log("agent onAlarm empty queue", {}, {
532
+ if (!announced) yield* Trace.emitRequestPhase("preparing");
533
+ const [mcpServers] = yield* Effect.all([loadSpaceMcpServers(), Effect.gen(function* () {
534
+ if (dequeued !== void 0) yield* sessionStore.markInFlight(feed, dequeued);
535
+ log("begin request", { prompt }, {
230
536
  "~LogMeta": "~LogMeta",
231
537
  F: __dxlog_file$1,
232
- L: 256,
538
+ L: 629,
233
539
  S: this
234
540
  });
235
- alarmManager.reconcile(false);
236
- yield* AgentEventsCell.set(inputQueue);
237
- yield* maybeComplete;
238
- return;
239
- }
240
- log("agent onAlarm handling", { tag: item._tag }, {
241
- "~LogMeta": "~LogMeta",
242
- F: __dxlog_file$1,
243
- L: 263,
244
- S: this
245
- });
246
- const prompt = agentEventToPrompt(item);
247
- log("begin request", { prompt }, {
248
- "~LogMeta": "~LogMeta",
249
- F: __dxlog_file$1,
250
- L: 267,
251
- S: this
252
- });
253
- log("trace agent request begin", void 0, {
254
- "~LogMeta": "~LogMeta",
255
- F: __dxlog_file$1,
256
- L: 268,
257
- S: this
258
- });
259
- yield* Trace.write(AgentRequestBegin, {});
541
+ log("trace agent request begin", void 0, {
542
+ "~LogMeta": "~LogMeta",
543
+ F: __dxlog_file$1,
544
+ L: 630,
545
+ S: this
546
+ });
547
+ yield* Trace.write(Trace.AgentRequestBegin, {});
548
+ })], { concurrency: "unbounded" });
549
+ markWork("agent.turn-begin");
260
550
  yield* session.runTurn({
261
551
  prompt,
552
+ sender: dequeued !== void 0 && Obj.instanceOf(Message.Message, dequeued) ? dequeued.sender : void 0,
262
553
  system: options.systemPrompt,
263
- mcpServers: options.getMcpServers?.()
264
- }).pipe(Effect.onExit((exit) => Trace.write(AgentRequestEnd, {
265
- status: Exit.isSuccess(exit) ? "success" : Exit.hasInterrupts(exit) ? "interrupted" : "error",
266
- error: Exit.isFailure(exit) ? Cause.pretty(exit.cause) : void 0
554
+ mcpServers
555
+ }).pipe(Effect.onExit((exit) => Effect.gen(function* () {
556
+ yield* Trace.write(Trace.AgentRequestEnd, {
557
+ status: Exit.isSuccess(exit) ? "success" : Exit.hasInterrupts(exit) ? "interrupted" : "error",
558
+ error: Exit.isFailure(exit) ? Cause.pretty(exit.cause) : void 0
559
+ });
560
+ const onTurnFailed = Option.isSome(strategy) ? strategy.value.onTurnFailed : void 0;
561
+ if (onTurnFailed && Exit.isFailure(exit) && !Cause.hasInterrupts(exit.cause)) yield* onTurnFailed(chat, exit.cause);
267
562
  })));
268
563
  log("end request", void 0, {
269
564
  "~LogMeta": "~LogMeta",
270
565
  F: __dxlog_file$1,
271
- L: 288,
566
+ L: 664,
272
567
  S: this
273
568
  });
274
- yield* AgentEventsCell.set(inputQueue);
569
+ turnRan = true;
570
+ endHooksFired = false;
571
+ yield* ToolResultsCell.set(toolResults);
572
+ if (dequeued !== void 0) {
573
+ yield* sessionStore.ack(feed, dequeued);
574
+ ackedEntries = [...ackedEntries, dequeued.id];
575
+ yield* AckedEntriesCell.set(ackedEntries);
576
+ }
577
+ const after = yield* sessionStore.loadPending(feed);
275
578
  if (Option.isSome(strategy)) {
276
579
  const activeIds = new Set(delegations.map((delegation) => delegation.id));
277
- const pending = yield* strategy.value.reconcile(feed, activeIds);
580
+ const pending = yield* strategy.value.reconcile(chat, activeIds);
278
581
  for (const delegation of pending) {
279
582
  const pid = yield* delegation.spawn;
280
583
  delegations.push({
@@ -287,14 +590,18 @@ var AgentProcess = (options) => Process.make({
287
590
  }, {
288
591
  "~LogMeta": "~LogMeta",
289
592
  F: __dxlog_file$1,
290
- L: 299,
593
+ L: 686,
291
594
  S: this
292
595
  });
596
+ yield* Trace.write(Trace.DelegationSpawned, {
597
+ taskId: delegation.id,
598
+ pid: String(pid)
599
+ });
293
600
  }
294
601
  if (pending.length > 0) yield* DelegationsCell.set(delegations);
295
602
  }
296
- alarmManager.reconcile(inputQueue.length > 0);
297
- yield* maybeComplete;
603
+ yield* reconcileAlarmWith(after);
604
+ yield* maybeCompleteWith(after);
298
605
  }, Effect.orDie, Effect.provide(Layer.mergeAll(makeToolResolverFromOperations(), ToolExecutionService({
299
606
  toolCallManager,
300
607
  feed,
@@ -304,19 +611,40 @@ var AgentProcess = (options) => Process.make({
304
611
  log("childEvent", { event }, {
305
612
  "~LogMeta": "~LogMeta",
306
613
  F: __dxlog_file$1,
307
- L: 325,
614
+ L: 713,
308
615
  S: this
309
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
+ }
310
630
  if (event._tag === "exited") {
311
631
  const delegation = delegations.find((delegation) => delegation.pid === event.pid);
312
632
  if (delegation) {
313
633
  delegations = delegations.filter((other) => other.pid !== event.pid);
314
634
  yield* DelegationsCell.set(delegations);
315
- 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);
637
+ const result = Exit.isSuccess(exit) ? resultPreview(exit.value) : void 0;
638
+ yield* Trace.write(Trace.DelegationCompleted, {
639
+ taskId: delegation.id,
640
+ pid: String(event.pid),
641
+ status: Exit.isSuccess(exit) ? "success" : "failure",
642
+ ...result === void 0 ? {} : { result }
643
+ });
316
644
  if (Option.isSome(strategy)) {
317
- yield* strategy.value.onComplete(feed, delegation.id, exit);
645
+ yield* strategy.value.onComplete(chat, delegation.id, exit);
318
646
  const activeIds = new Set(delegations.map((delegation) => delegation.id));
319
- const pending = yield* strategy.value.reconcile(feed, activeIds);
647
+ const pending = yield* strategy.value.reconcile(chat, activeIds);
320
648
  for (const next of pending) {
321
649
  const pid = yield* next.spawn;
322
650
  delegations.push({
@@ -329,9 +657,13 @@ var AgentProcess = (options) => Process.make({
329
657
  }, {
330
658
  "~LogMeta": "~LogMeta",
331
659
  F: __dxlog_file$1,
332
- L: 347,
660
+ L: 757,
333
661
  S: this
334
662
  });
663
+ yield* Trace.write(Trace.DelegationSpawned, {
664
+ taskId: next.id,
665
+ pid: String(pid)
666
+ });
335
667
  }
336
668
  if (pending.length > 0) yield* DelegationsCell.set(delegations);
337
669
  }
@@ -342,25 +674,22 @@ var AgentProcess = (options) => Process.make({
342
674
  }, {
343
675
  "~LogMeta": "~LogMeta",
344
676
  F: __dxlog_file$1,
345
- L: 353,
677
+ L: 764,
346
678
  S: this
347
679
  });
348
680
  yield* maybeComplete;
349
681
  } else if (toolCallManager.isToolCall(event.pid)) {
350
- const attachExit = yield* (yield* ProcessManager.ProcessOperationInvoker.Service).attachFiber(event.pid).pipe(Effect.exit);
351
- if (Exit.isFailure(attachExit)) {
352
- if (toolCallManager.isToolCall(event.pid) || inputQueue.some((item) => item._tag === "tool_result" && item.pid === event.pid) || toolCallManager.isReported(event.pid)) {
353
- log.verbose("childEvent skipped (process gone, result already handled)", { pid: event.pid }, {
354
- "~LogMeta": "~LogMeta",
355
- F: __dxlog_file$1,
356
- L: 366,
357
- S: this
358
- });
359
- return;
360
- }
361
- 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;
362
691
  }
363
- 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({
364
693
  onSuccess: (value) => ({
365
694
  _tag: "tool_result",
366
695
  pid: event.pid,
@@ -374,47 +703,43 @@ var AgentProcess = (options) => Process.make({
374
703
  isError: true
375
704
  })
376
705
  })));
377
- inputQueue.push(result);
706
+ toolResults.push(result);
378
707
  log("agent onChildEvent persisted tool result", {
379
- depth: inputQueue.length,
708
+ depth: toolResults.length,
380
709
  childPid: event.pid
381
710
  }, {
382
711
  "~LogMeta": "~LogMeta",
383
712
  F: __dxlog_file$1,
384
- L: 391,
713
+ L: 803,
385
714
  S: this
386
715
  });
387
- yield* AgentEventsCell.set(inputQueue);
388
- alarmManager.reconcile(true);
389
- log("agent onChildEvent alarm scheduled", { depth: inputQueue.length }, {
716
+ yield* ToolResultsCell.set(toolResults);
717
+ yield* ctx.setAlarm(0);
718
+ log("agent onChildEvent alarm scheduled", { depth: toolResults.length }, {
390
719
  "~LogMeta": "~LogMeta",
391
720
  F: __dxlog_file$1,
392
- L: 394,
721
+ L: 806,
393
722
  S: this
394
723
  });
395
- } else log.verbose("childEvent ignored non-tool call and not a delegation", { pid: event.pid }, {
396
- "~LogMeta": "~LogMeta",
397
- F: __dxlog_file$1,
398
- L: 396,
399
- S: this
400
- });
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
+ }
401
733
  }
402
734
  })
403
735
  };
404
- }));
405
- var AgentEvent = Schema.Union([
406
- Schema.TaggedStruct("prompt", { content: Schema.Array(ContentBlock.Any) }),
407
- Schema.TaggedStruct("tool_result", {
408
- pid: Process.ID,
409
- result: Schema.Unknown,
410
- isError: Schema.Boolean
411
- }),
412
- Schema.TaggedStruct("alarm", {
413
- firedAt: Schema.Number,
414
- message: Schema.NullOr(Schema.String)
415
- })
416
- ]);
417
- var AgentEventsCell = StorageService.cell(Schema.fromJsonString(Schema.Array(AgentEvent).pipe(Schema.mutable)), "inputQueue").pipe(StorageService.withDefault(() => []));
736
+ })));
737
+ var ToolResultEvent = Schema.TaggedStruct("tool_result", {
738
+ pid: Process.ID,
739
+ result: Schema.Unknown,
740
+ isError: Schema.Boolean
741
+ });
742
+ var ToolResultsCell = StorageService.cell(Schema.fromJsonString(Schema.Array(ToolResultEvent).pipe(Schema.mutable)), "toolResults").pipe(StorageService.withDefault(() => []));
418
743
  /**
419
744
  * Tracks delegated sub-agent child processes (pid -> correlation id) so that, after a hibernation,
420
745
  * a delegated child's exit can be matched back to the work it was fulfilling.
@@ -424,6 +749,27 @@ var Delegation = Schema.Struct({
424
749
  id: Schema.String
425
750
  }).mapFields(Struct.map(Schema.mutableKey));
426
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(() => []));
754
+ /**
755
+ * Ids of queue entries this process has ACKED, held until the ack is visible in the feed read.
756
+ *
757
+ * The ack is a feed append and a hosted process reads the feed back through the eventually
758
+ * consistent space index, so `loadPending` keeps returning an entry whose ack has not landed yet —
759
+ * and `loadPending` deliberately includes in-flight entries, so that an interrupted turn is
760
+ * redelivered. Without this the two combine into a redelivery loop: the agent re-runs the same turn
761
+ * on every wake, forever. Durable rather than in-memory because the isolate does not survive
762
+ * between turns, and pruned as soon as the entry leaves the pending set, so it cannot grow
763
+ * unbounded.
764
+ *
765
+ * This does NOT weaken at-least-once delivery: an entry is recorded only after its turn ran, which
766
+ * is the same point the ack is written, so a crash before that still redelivers.
767
+ */
768
+ var AckedEntriesCell = StorageService.cell(Schema.fromJsonString(Schema.Array(Schema.String).pipe(Schema.mutable)), "ackedEntries").pipe(StorageService.withDefault(() => []));
769
+ /** Alarms that have woken the agent since the last user prompt; bounded by {@link Alarm.MAX_SELF_WAKES}. */
770
+ var SelfWakesCell = StorageService.cell(Schema.fromJsonString(Schema.Number), "selfWakes").pipe(StorageService.withDefault(() => 0));
771
+ /** A prompt someone typed, as opposed to one the system wrote (a report, a tool result). */
772
+ var isUserPrompt = (message) => message.sender.role === "user" && message.blocks.some((block) => block._tag === "text" && block.disposition !== "synthetic");
427
773
  var ToolCallState = Schema.Struct({ activeCalls: Schema.Array(Schema.Struct({
428
774
  pid: Process.ID,
429
775
  reported: Schema.Boolean
@@ -475,7 +821,6 @@ var ToolCallManager = class {
475
821
  return Effect.gen({ self: this }, function* () {
476
822
  let changed = false;
477
823
  for (const item of queue) {
478
- if (item._tag !== "tool_result") continue;
479
824
  const call = this.#state.activeCalls.find((entry) => entry.pid === item.pid);
480
825
  if (call?.reported) {
481
826
  call.reported = false;
@@ -483,7 +828,7 @@ var ToolCallManager = class {
483
828
  log("reconcile queued tool result", { pid: item.pid }, {
484
829
  "~LogMeta": "~LogMeta",
485
830
  F: __dxlog_file$1,
486
- L: 535,
831
+ L: 971,
487
832
  S: this
488
833
  });
489
834
  }
@@ -492,8 +837,13 @@ var ToolCallManager = class {
492
837
  }).pipe(Effect.provideService(StorageService.StorageService, this.#storageService));
493
838
  }
494
839
  };
495
- /** True while the agent still has queued work, a self-wake, subprocesses, or undelivered tool results. */
496
- var isAgentWorkPending = ({ inputQueue, alarmManager, delegations, toolCallManager }) => inputQueue.length > 0 || alarmManager.wakeAt != null || delegations.length > 0 || toolCallManager.hasPendingToolResults();
840
+ /** True while the agent still has queued work, a pending alarm, subprocesses, or undelivered tool results. */
841
+ var isAgentWorkPending = ({ toolResults, pendingMessages, pendingAlarms, delegations, toolCallManager }) => toolResults.length > 0 || pendingMessages.length > 0 || pendingAlarms.length > 0 || delegations.length > 0 || toolCallManager.hasPendingToolResults();
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);
497
847
  /**
498
848
  * Discards tool results at the head of the queue whose values already reached the agent.
499
849
  *
@@ -507,37 +857,24 @@ var dropReportedToolResults = (queue, isReported) => {
507
857
  const dropped = [];
508
858
  while (queue.length > 0) {
509
859
  const head = queue[0];
510
- if (head._tag !== "tool_result" || !isReported(head.pid)) break;
860
+ if (!isReported(head.pid)) break;
511
861
  queue.shift();
512
862
  dropped.push(head.pid);
513
863
  }
514
864
  return dropped;
515
865
  };
516
866
  /**
517
- * Renders a queued event as the next turn's prompt.
518
- *
519
- * A tool result recovered across a reload is redelivered as a synthetic `<result pid=N>` TEXT block
867
+ * Renders a recovered tool result as the next turn's prompt: a synthetic `<result pid=N>` TEXT block
520
868
  * rather than a tool-result part, because the request it belonged to is gone and its tool-call id
521
869
  * cannot be answered. `disposition: 'synthetic'` keeps it out of the user-visible transcript.
522
870
  */
523
- var agentEventToPrompt = (event) => Match.value(event).pipe(Match.tag("prompt", (event) => [...event.content]), Match.tag("tool_result", (event) => [ContentBlock.Text.make({
524
- text: event.isError ? toolErrorResponse(event.pid, event.result) : toolResultResponse(event.pid, event.result),
525
- disposition: "synthetic"
526
- })]), Match.tag("alarm", (event) => [ContentBlock.Text.make({
527
- text: wakeUpPrompt(event.firedAt, event.message),
871
+ var toolResultPrompt = (event) => [ContentBlock.Text.make({
872
+ text: event.isError ? toolErrorResponse(event.pid, String(event.result)) : toolResultResponse(event.pid, event.result),
528
873
  disposition: "synthetic"
529
- })]), Match.exhaustive);
874
+ })];
530
875
  /**
531
- * Persisted next agent-scheduled self-wake: the UNIX timestamp (ms) and an optional reminder
532
- * message delivered when it fires, or `null` when no self-wake is set.
533
- */
534
- var AgentAlarmCell = StorageService.cell(Schema.fromJsonString(Schema.NullOr(Schema.Struct({
535
- wakeAt: Schema.Number,
536
- message: Schema.NullOr(Schema.String)
537
- }))), "agentAlarm").pipe(StorageService.withDefault(() => null));
538
- /**
539
- * Computes the timeout to pass to `ctx.setAlarm`, reconciling pending queue work with the agent's
540
- * self-wake alarm. Returns `null` when no alarm should be scheduled (process can go idle).
876
+ * Computes the timeout to pass to `ctx.setAlarm`, reconciling pending queue work with the earliest
877
+ * pending feed alarm. Returns `null` when no alarm should be scheduled (process can go idle).
541
878
  */
542
879
  var computeAlarmDelay = ({ hasPendingWork, wakeAt, now }) => {
543
880
  if (hasPendingWork) return 0;
@@ -545,96 +882,19 @@ var computeAlarmDelay = ({ hasPendingWork, wakeAt, now }) => {
545
882
  return null;
546
883
  };
547
884
  /**
548
- * Tracks the next agent-scheduled self-wake and keeps it in sync with the process alarm
549
- * (`ctx.setAlarm`). The agent sets alarms via the alarm blueprint, which dispatches to this manager
550
- * through the `HarnessControl` RPC surface; the process reconciles the underlying single-shot alarm
551
- * timer to fire at the earliest of pending work or the self-wake.
552
- */
553
- var AlarmManager = class {
554
- #storageService;
555
- #setAlarm;
556
- #now;
557
- #wakeAt = null;
558
- #message = null;
559
- constructor({ storageService, setAlarm, now = () => Date.now() }) {
560
- this.#storageService = storageService;
561
- this.#setAlarm = setAlarm;
562
- this.#now = now;
563
- }
564
- /** Currently scheduled self-wake timestamp (ms), or `null` when none is set. */
565
- get wakeAt() {
566
- return this.#wakeAt;
567
- }
568
- /** Reminder message delivered when the current self-wake fires, or `null` when none is set. */
569
- get message() {
570
- return this.#message;
571
- }
572
- now() {
573
- return this.#now();
574
- }
575
- /** Restores the persisted alarm state. */
576
- load() {
577
- return Effect.gen({ self: this }, function* () {
578
- const persisted = yield* AgentAlarmCell.get;
579
- this.#wakeAt = persisted?.wakeAt ?? null;
580
- this.#message = persisted?.message ?? null;
581
- }).pipe(Effect.provideService(StorageService.StorageService, this.#storageService));
582
- }
583
- /** Records a new self-wake target (with an optional reminder message) and persists it. */
584
- setWakeAt(wakeAt, message = null) {
585
- return Effect.gen({ self: this }, function* () {
586
- this.#wakeAt = wakeAt;
587
- this.#message = message;
588
- yield* AgentAlarmCell.set({
589
- wakeAt,
590
- message
591
- });
592
- }).pipe(Effect.provideService(StorageService.StorageService, this.#storageService));
593
- }
594
- /**
595
- * Clears the self-wake alarm if it is due, returning the timestamp it was scheduled for and the
596
- * reminder message it carried (or `null` if no alarm was due).
597
- */
598
- takeFiredAlarm() {
599
- return Effect.gen({ self: this }, function* () {
600
- if (this.#wakeAt == null || this.#now() < this.#wakeAt) return null;
601
- const firedAt = this.#wakeAt;
602
- const message = this.#message;
603
- this.#wakeAt = null;
604
- this.#message = null;
605
- yield* AgentAlarmCell.set(null);
606
- return {
607
- firedAt,
608
- message
609
- };
610
- }).pipe(Effect.provideService(StorageService.StorageService, this.#storageService));
611
- }
612
- /**
613
- * Reconciles the process alarm timer with pending work and the tracked self-wake, scheduling the
614
- * earliest of the two. Does nothing when there is neither pending work nor a self-wake.
615
- */
616
- reconcile(hasPendingWork) {
617
- const delay = computeAlarmDelay({
618
- hasPendingWork,
619
- wakeAt: this.#wakeAt,
620
- now: this.#now()
621
- });
622
- if (delay != null) this.#setAlarm(delay);
623
- }
624
- };
625
- /**
626
885
  * Prompt delivered to the agent when a self-scheduled alarm fires. When the alarm carried a
627
886
  * reminder message it is surfaced verbatim, otherwise a generic continuation prompt is used.
887
+ * Exported so the prompt shape stays pinned by tests without spawning an agent.
628
888
  */
629
- var wakeUpPrompt = (firedAt, message) => message != null ? trim`
630
- Your scheduled alarm fired (it was set for ${new Date(firedAt).toISOString()}).
631
- ${message}
632
- ` : trim`
633
- Your scheduled alarm fired (it was set for ${new Date(firedAt).toISOString()}).
634
- Continue with whatever you intended to do when you scheduled this wake-up.
635
- `;
889
+ var wakeUpPrompt = (firedAt, message, budget) => {
890
+ return [
891
+ `Scheduled alarm fired (it was set for ${new Date(firedAt).toISOString()}).`,
892
+ message ?? "Continue with whatever you intended to do when you scheduled this wake-up.",
893
+ budget == null ? void 0 : budget.wake >= budget.max ? `This is self-wake ${budget.wake} of ${budget.max}: further alarms will not wake you until the user writes again, so finish or report where you are now.` : `This is self-wake ${budget.wake} of ${budget.max} before the user must write again.`
894
+ ].filter((line) => line != null).join("\n");
895
+ };
636
896
  var ToolExecutionService = ({ enableBackgrounding, backgroundThreshold = Duration.seconds(1), toolCallManager, feed }) => Layer.unwrap(Effect.gen(function* () {
637
- const operationInvoker = yield* ProcessManager.ProcessOperationInvoker.Service;
897
+ const childServices = yield* Effect.context();
638
898
  return makeToolExecutionService({ invoke: (tool, input) => Effect.gen(function* () {
639
899
  const operationDef = getOperationFromTool(tool).pipe(Option.getOrThrow);
640
900
  log("invoking operation", {
@@ -643,34 +903,37 @@ var ToolExecutionService = ({ enableBackgrounding, backgroundThreshold = Duratio
643
903
  }, {
644
904
  "~LogMeta": "~LogMeta",
645
905
  F: __dxlog_file$1,
646
- L: 770,
906
+ L: 1117,
647
907
  S: this
648
908
  });
649
- const fiber = yield* operationInvoker.invokeFiber(operationDef, input, {
909
+ const handle = yield* Process.spawn(OperationProcess.make(operationDef), input, {
650
910
  environment: { conversation: Obj.getURI(feed) },
651
911
  traceMeta: { conversation: Ref.make(feed) }
652
912
  });
653
- yield* toolCallManager.beginCall(fiber.pid);
913
+ markWork("tool.spawned");
914
+ yield* toolCallManager.beginCall(handle.pid);
915
+ markWork("tool.call-recorded");
654
916
  log("invoked operation", {
655
917
  operationDef,
656
918
  input,
657
- fiber
919
+ pid: handle.pid
658
920
  }, {
659
921
  "~LogMeta": "~LogMeta",
660
922
  F: __dxlog_file$1,
661
- L: 780,
923
+ L: 1129,
662
924
  S: this
663
925
  });
664
- const awaitWithReport = fiber.await.pipe(Effect.tap(() => toolCallManager.markAsReported(fiber.pid)));
665
- 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;
928
+ markWork("tool.settled");
666
929
  log("result", { result }, {
667
930
  "~LogMeta": "~LogMeta",
668
931
  F: __dxlog_file$1,
669
- L: 791,
932
+ L: 1144,
670
933
  S: this
671
934
  });
672
935
  return yield* result;
673
- }) });
936
+ }).pipe(Effect.provide(childServices)) });
674
937
  }));
675
938
  var AsynchronousExectionToolkit = class extends Toolkit.make(Tool.make("poll-tools", {
676
939
  description: trim`
@@ -693,12 +956,16 @@ var AsynchronousExectionToolkit = class extends Toolkit.make(Tool.make("poll-too
693
956
  })
694
957
  })) {};
695
958
  var AsynchronousExectionToolkitLayer = AsynchronousExectionToolkit.toLayer(Effect.gen(function* () {
696
- const invoker = yield* ProcessManager.ProcessOperationInvoker.Service;
959
+ const manager = yield* Process.ManagerService;
697
960
  return { "poll-tools": ({ ids, wait, timeout = 1e4 }) => Effect.gen(function* () {
698
- 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({
699
- onSuccess: (value) => Effect.succeed(toolResultResponse(pid, value)),
700
- onFailure: (cause) => Effect.succeed(toolErrorResponse(pid, Cause.pretty(cause)))
701
- })), 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
+ }));
702
969
  }) };
703
970
  }));
704
971
  /**
@@ -714,9 +981,11 @@ var AgentService_exports = /* @__PURE__ */ __exportAll({
714
981
  layer: () => layer
715
982
  });
716
983
  var __dxlog_file = "/__w/dxos/dxos/packages/core/compute/agent-runtime/src/agent-service/AgentService.ts";
717
- var isTerminalProcess = (state) => state === Process.State.SUCCEEDED || state === Process.State.FAILED || state === Process.State.TERMINATED;
984
+ /**
985
+ * Creates a session on a fresh feed and chat, with `opts.skills` and `opts.context` bound to it.
986
+ */
718
987
  var createSession = Effect.fn("createSession")(function* (opts) {
719
- const skills = yield* Effect.forEach(opts?.skills ?? [], (skill) => Skill.upsert(Skill.getKey(skill)).pipe(Effect.map(Ref.make)));
988
+ const skills = (opts?.skills ?? []).map(Skill.makeRef);
720
989
  const feed = yield* Database.add(Feed.make());
721
990
  const runtime = yield* Effect.context();
722
991
  const binder = yield* EffectEx.acquireReleaseResource(() => new AiContext.Binder({
@@ -727,101 +996,182 @@ var createSession = Effect.fn("createSession")(function* (opts) {
727
996
  skills,
728
997
  objects: opts?.context ?? []
729
998
  }));
730
- return yield* getSession(feed, {
731
- model: opts?.model,
732
- provider: opts?.provider
733
- });
999
+ const chat = yield* Database.add(Chat.make({
1000
+ feed: Ref.make(feed),
1001
+ ...opts?.model ? { session: { model: opts.model } } : {}
1002
+ }));
1003
+ return yield* AgentService.getSession(chat, { provider: opts?.provider });
734
1004
  }, Effect.scoped);
735
- var layer = (opts) => Layer.effect(AgentService, Effect.gen(function* () {
736
- const processManager = yield* ProcessManager.Service;
1005
+ /**
1006
+ * The `AgentService` layer.
1007
+ *
1008
+ * Requires {@link Process.ManagerService}, which runs a session locally or, for `location: 'edge'`,
1009
+ * on the remote runtime. A host without EDGE satisfies its remote half with
1010
+ * `RemoteProcessManager.layerNoop`, so an edge session fails at spawn rather than silently running
1011
+ * locally.
1012
+ */
1013
+ var layer = (opts) => Layer.effect(AgentService.AgentService, Effect.gen(function* () {
1014
+ const processManager = yield* Process.ManagerService;
1015
+ const remoteSpaces = /* @__PURE__ */ new Set();
1016
+ /**
1017
+ * Where a session's agent runs. `edge` needs the space, since one remote runtime spans them,
1018
+ * and a chat with no space cannot name where its agent would run.
1019
+ */
1020
+ const locationFor = (location, spaceId) => {
1021
+ if (location !== "edge") return { kind: "local" };
1022
+ if (!spaceId) throw new Error("Agent requested on edge, but its conversation has no space.");
1023
+ remoteSpaces.add(spaceId);
1024
+ return {
1025
+ kind: "edge",
1026
+ space: spaceId
1027
+ };
1028
+ };
737
1029
  const sessionCache = /* @__PURE__ */ new Map();
738
- const makeExecutable = (model, provider) => AgentProcess({
1030
+ const sessionLocks = /* @__PURE__ */ new Map();
1031
+ const lockFor = (chatId) => {
1032
+ const existing = sessionLocks.get(chatId);
1033
+ if (existing) return existing;
1034
+ const lock = Effect.runSync(Semaphore.make(1));
1035
+ sessionLocks.set(chatId, lock);
1036
+ return lock;
1037
+ };
1038
+ const makeExecutable = (provider) => AgentProcess({
739
1039
  systemPrompt: opts?.systemPrompt,
740
1040
  makeTurnProducer: opts?.makeTurnProducer,
741
- model: model ?? opts?.model,
1041
+ defaultModel: opts?.defaultModel,
742
1042
  provider: provider ?? opts?.provider,
743
- getMcpServers: opts?.getMcpServers,
744
1043
  enableToolBackgrounding: opts?.enableToolBackgrounding,
745
- delegationStrategy: opts?.delegationStrategy
1044
+ delegationStrategy: opts?.delegationStrategy,
1045
+ resident: opts?.resident
746
1046
  });
747
- return {
748
- getSession: (feed, options) => Effect.gen(function* () {
749
- const model = options?.model ?? opts?.model;
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
+ };
1066
+ const service = {
1067
+ getSession: (chat, options) => Effect.suspend(() => lockFor(chat.id).withPermits(1)(Effect.gen(function* () {
750
1068
  const provider = options?.provider ?? opts?.provider;
751
- const instructions = options?.instructions?.uri;
752
- const cached = sessionCache.get(feed.id);
1069
+ const model = chat.session?.model;
1070
+ const instructions = chat.instructions?.uri;
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.`));
1074
+ const cached = sessionCache.get(chat.id);
753
1075
  if (cached) {
754
- if (cached.model === model && cached.provider === provider && cached.instructions === instructions && !isTerminalProcess(cached.handle.status.state)) return cached.session;
755
- if (!isTerminalProcess(cached.handle.status.state)) yield* cached.handle.terminate();
756
- sessionCache.delete(feed.id);
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;
1077
+ if (!Process.isTerminal(cached.handle.status.state)) yield* cached.handle.terminate();
1078
+ sessionCache.delete(chat.id);
757
1079
  }
758
- const target = Obj.getURI(feed);
1080
+ const feed = yield* Database.load(chat.feed).pipe(Effect.orDie);
1081
+ const target = Obj.getURI(chat);
759
1082
  const parsedEchoUri = EID.tryParse(target);
760
1083
  const spaceId = parsedEchoUri ? EID.getSpaceId(parsedEchoUri) : void 0;
761
- const executable = makeExecutable(model, provider);
762
- let activeProcess = (yield* processManager.list({
1084
+ const processLocation = locationFor(options?.location, spaceId);
1085
+ let activeProcess = (yield* processManager.handles({
763
1086
  target,
764
- key: executable.key
765
- })).find((process) => !isTerminalProcess(process.status.state));
766
- if (activeProcess) {
767
- if (Option.getOrUndefined(Annotation.getDictionary(activeProcess.params.annotations, Process.InstructionsAnnotation)) !== instructions) {
768
- yield* activeProcess.terminate();
769
- activeProcess = void 0;
770
- }
771
- }
1087
+ key: executable.key,
1088
+ location: processLocation
1089
+ })).find((process) => !Process.isTerminal(process.status.state));
772
1090
  let handle;
773
- if (activeProcess) {
774
- yield* activeProcess.hydrate(executable);
775
- handle = activeProcess;
776
- } else handle = yield* processManager.spawn(executable, {
1091
+ if (activeProcess) handle = yield* activeProcess.hydrate(executable);
1092
+ else handle = yield* processManager.spawn(executable, {
1093
+ location: processLocation,
777
1094
  name: "Agent",
778
1095
  target,
779
1096
  annotations: Annotation.buildDictionary((dictionary) => {
780
1097
  Annotation.setDictionary(dictionary, Process.HarnessHostAnnotation, true);
781
- if (options?.instructions) Annotation.setDictionary(dictionary, Process.InstructionsAnnotation, options.instructions.uri);
782
1098
  }),
783
1099
  environment: {
784
1100
  ...spaceId !== void 0 ? { space: spaceId } : {},
785
- conversation: target
1101
+ conversation: Obj.getURI(feed)
786
1102
  },
787
1103
  traceMeta: { conversation: Ref.make(feed) }
788
1104
  });
789
1105
  const releaseSession = () => {
790
- sessionCache.delete(feed.id);
1106
+ sessionCache.delete(chat.id);
791
1107
  };
792
- const session = makeSession(handle, feed, releaseSession);
793
- sessionCache.set(feed.id, {
1108
+ const databaseContext = yield* Effect.context();
1109
+ const isFinished = Effect.suspend(() => Process.isTerminal(handle.status.state) || location !== "edge" ? Effect.succeed(Process.isTerminal(handle.status.state)) : processManager.handles({
1110
+ target,
1111
+ key: executable.key,
1112
+ location: processLocation
1113
+ }).pipe(Effect.map((live) => {
1114
+ const current = live.find((process) => process.pid === handle.pid);
1115
+ return current === void 0 || Process.isTerminal(current.status.state);
1116
+ }), Effect.orElseSucceed(() => false)));
1117
+ const resubmit = (prompt, submitOptions) => Effect.sync(releaseSession).pipe(Effect.andThen(service.getSession(chat, options)), Effect.flatMap((next) => next.submitPrompt(prompt, submitOptions)), Effect.provide(databaseContext));
1118
+ const session = makeSession(handle, chat, feed, releaseSession, isFinished, resubmit);
1119
+ sessionCache.set(chat.id, {
794
1120
  model,
795
1121
  provider,
796
1122
  instructions,
1123
+ location,
1124
+ process: executable.key,
797
1125
  handle,
798
1126
  session
799
1127
  });
800
1128
  return session;
801
- }),
1129
+ }))),
802
1130
  hydrate: Effect.fnUntraced(function* () {
803
1131
  sessionCache.clear();
1132
+ const contributed = (opts?.processes?.() ?? []).filter(({ key }) => key !== AGENT_PROCESS_KEY);
804
1133
  const executable = makeExecutable();
805
- const agents = yield* processManager.list({ key: AGENT_PROCESS_KEY });
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
+ ];
806
1154
  log("agent hydrate", { count: agents.length }, {
807
1155
  "~LogMeta": "~LogMeta",
808
1156
  F: __dxlog_file,
809
- L: 150,
1157
+ L: 231,
810
1158
  S: this
811
1159
  });
812
- 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", {
813
1161
  pid: agent.pid,
814
1162
  cause: Cause.pretty(cause)
815
1163
  }, {
816
1164
  "~LogMeta": "~LogMeta",
817
1165
  F: __dxlog_file,
818
- L: 156,
1166
+ L: 237,
819
1167
  S: this
820
1168
  }))));
821
1169
  })
822
1170
  };
1171
+ return service;
823
1172
  }));
824
- var makeSession = (process, feed, releaseSession) => ({
1173
+ var makeSession = (process, chat, feed, releaseSession, isFinished, resubmit) => ({
1174
+ chat,
825
1175
  feed,
826
1176
  getContext: () => Effect.gen(function* () {
827
1177
  const runtime = yield* Effect.context();
@@ -841,12 +1191,16 @@ var makeSession = (process, feed, releaseSession) => ({
841
1191
  objects: context
842
1192
  }));
843
1193
  }).pipe(Effect.scoped),
844
- submitPrompt: (prompt) => process.submitInput(prompt),
1194
+ submitPrompt: (prompt, options) => Effect.flatMap(isFinished, (finished) => finished ? resubmit(prompt, options) : process.submitInput(options?.sender ? {
1195
+ prompt,
1196
+ sender: options.sender
1197
+ } : prompt)),
1198
+ running: Atom.make((get) => get(process.statusAtom).state === Process.State.RUNNING || get(process.statusAtom).state === Process.State.HYBERNATING),
845
1199
  waitForCompletion: () => process.runUntilSettled(),
846
1200
  terminate: () => process.terminate().pipe(Effect.tap(() => Effect.sync(releaseSession))),
847
- subscribeEphemeral: () => process.subscribeEphemeral()
1201
+ subscribeEphemeral: (options) => process.subscribeEphemeral(options)
848
1202
  });
849
1203
  //#endregion
850
- export { __exportAll as a, makeAiSessionTurnProducer as i, layer as n, AGENT_PROCESS_KEY as r, AgentService_exports as t };
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 };
851
1205
 
852
1206
  //# sourceMappingURL=chunk-AgentService.mjs.map