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