@minpeter/pss-runtime 0.1.0-next.1 → 0.1.0-next.2

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 (101) hide show
  1. package/README.md +169 -28
  2. package/dist/agent-child-runs.js +16 -0
  3. package/dist/agent-child-runs.js.map +1 -0
  4. package/dist/agent-host-capabilities.js +9 -0
  5. package/dist/agent-host-capabilities.js.map +1 -0
  6. package/dist/agent-host-session-store.js +12 -0
  7. package/dist/agent-host-session-store.js.map +1 -0
  8. package/dist/agent-loop.js +58 -28
  9. package/dist/agent-loop.js.map +1 -1
  10. package/dist/agent-namespace.js +8 -1
  11. package/dist/agent-namespace.js.map +1 -1
  12. package/dist/agent-options.d.ts +35 -0
  13. package/dist/agent-options.js +16 -0
  14. package/dist/agent-options.js.map +1 -0
  15. package/dist/agent-resume.js +143 -0
  16. package/dist/agent-resume.js.map +1 -0
  17. package/dist/agent-session-entry.d.ts +13 -0
  18. package/dist/agent-validation.js +2 -2
  19. package/dist/agent-validation.js.map +1 -1
  20. package/dist/agent.d.ts +5 -41
  21. package/dist/agent.js +81 -49
  22. package/dist/agent.js.map +1 -1
  23. package/dist/execution/host.js +14 -0
  24. package/dist/execution/host.js.map +1 -0
  25. package/dist/execution/index.d.ts +4 -0
  26. package/dist/execution/index.js +3 -0
  27. package/dist/execution/memory-notifications.js +54 -0
  28. package/dist/execution/memory-notifications.js.map +1 -0
  29. package/dist/execution/memory-state.js +34 -0
  30. package/dist/execution/memory-state.js.map +1 -0
  31. package/dist/execution/memory-store.js +203 -0
  32. package/dist/execution/memory-store.js.map +1 -0
  33. package/dist/execution/memory.d.ts +7 -0
  34. package/dist/execution/memory.js +28 -0
  35. package/dist/execution/memory.js.map +1 -0
  36. package/dist/execution/run.js +55 -0
  37. package/dist/execution/run.js.map +1 -0
  38. package/dist/execution/types.d.ts +155 -0
  39. package/dist/index.d.ts +8 -5
  40. package/dist/llm-tool-execution.d.ts +35 -0
  41. package/dist/llm-tool-execution.js +126 -0
  42. package/dist/llm-tool-execution.js.map +1 -0
  43. package/dist/llm.d.ts +11 -15
  44. package/dist/llm.js +5 -3
  45. package/dist/llm.js.map +1 -1
  46. package/dist/plugins.d.ts +20 -0
  47. package/dist/plugins.js +14 -0
  48. package/dist/plugins.js.map +1 -0
  49. package/dist/session/events.d.ts +3 -0
  50. package/dist/session/runtime-input.js +5 -23
  51. package/dist/session/runtime-input.js.map +1 -1
  52. package/dist/session/session-errors.js +1 -6
  53. package/dist/session/session-errors.js.map +1 -1
  54. package/dist/session/session-events.js +59 -0
  55. package/dist/session/session-events.js.map +1 -0
  56. package/dist/session/session-execution.js +88 -0
  57. package/dist/session/session-execution.js.map +1 -0
  58. package/dist/session/session-notification.js +58 -0
  59. package/dist/session/session-notification.js.map +1 -0
  60. package/dist/session/session-runtime-drain.js +2 -2
  61. package/dist/session/session-runtime-drain.js.map +1 -1
  62. package/dist/session/session-turn-processor.js +135 -0
  63. package/dist/session/session-turn-processor.js.map +1 -0
  64. package/dist/session/session.js +73 -101
  65. package/dist/session/session.js.map +1 -1
  66. package/dist/session/snapshot.js.map +1 -1
  67. package/dist/subagent-background-child-run-state.js +51 -0
  68. package/dist/subagent-background-child-run-state.js.map +1 -0
  69. package/dist/subagent-background-child-run.js +103 -0
  70. package/dist/subagent-background-child-run.js.map +1 -0
  71. package/dist/subagent-background-in-process.js +98 -0
  72. package/dist/subagent-background-in-process.js.map +1 -0
  73. package/dist/subagent-background-notification-inbox.js +106 -0
  74. package/dist/subagent-background-notification-inbox.js.map +1 -0
  75. package/dist/subagent-background-notify.js +136 -0
  76. package/dist/subagent-background-notify.js.map +1 -0
  77. package/dist/subagent-background-resume-group.js +99 -0
  78. package/dist/subagent-background-resume-group.js.map +1 -0
  79. package/dist/subagent-background-runner.js +115 -0
  80. package/dist/subagent-background-runner.js.map +1 -0
  81. package/dist/subagent-background-schedule.js +43 -0
  82. package/dist/subagent-background-schedule.js.map +1 -0
  83. package/dist/subagent-child-run.js +68 -0
  84. package/dist/subagent-child-run.js.map +1 -0
  85. package/dist/subagent-job-cancel.js +60 -4
  86. package/dist/subagent-job-cancel.js.map +1 -1
  87. package/dist/subagent-job-observer.js +19 -0
  88. package/dist/subagent-job-observer.js.map +1 -0
  89. package/dist/subagent-job-output.js +28 -4
  90. package/dist/subagent-job-output.js.map +1 -1
  91. package/dist/subagent-job-state.js +66 -0
  92. package/dist/subagent-job-state.js.map +1 -0
  93. package/dist/subagent-jobs.js +78 -133
  94. package/dist/subagent-jobs.js.map +1 -1
  95. package/dist/subagent-run.js +4 -4
  96. package/dist/subagent-run.js.map +1 -1
  97. package/dist/subagents.js +68 -35
  98. package/dist/subagents.js.map +1 -1
  99. package/package.json +11 -1
  100. package/dist/hooks.d.ts +0 -32
  101. /package/dist/session/{runtime-input.d.ts → session-execution.d.ts} +0 -0
package/README.md CHANGED
@@ -10,12 +10,31 @@ Minimal, platform-agnostic agent runtime with keyed sessions, synchronized
10
10
  ## Core DX
11
11
 
12
12
  ```ts
13
+ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
13
14
  import { Agent } from "@minpeter/pss-runtime";
14
- import { createYourLanguageModel } from "...";
15
+ import { createEnv } from "@t3-oss/env-core";
16
+ import { config as loadEnv } from "dotenv";
17
+ import { z } from "zod";
18
+
19
+ loadEnv({ path: ".env", quiet: true, override: true });
20
+ const env = createEnv({
21
+ runtimeEnv: process.env,
22
+ server: {
23
+ AI_API_KEY: z.string().trim().min(1),
24
+ AI_BASE_URL: z.url().trim().default("https://apis.opengateway.ai/v1"),
25
+ AI_MODEL: z.string().trim().min(1).default("minimax/MiniMax-M2.7"),
26
+ },
27
+ });
28
+
29
+ const provider = createOpenAICompatible({
30
+ name: "custom",
31
+ apiKey: env.AI_API_KEY,
32
+ baseURL: env.AI_BASE_URL,
33
+ });
15
34
 
16
35
  const agent = new Agent({
17
36
  instructions: "Answer briefly.",
18
- model: createYourLanguageModel(),
37
+ model: provider(env.AI_MODEL),
19
38
  });
20
39
 
21
40
  const run = await agent.send("Hello");
@@ -30,6 +49,23 @@ consume the events for the run to progress. This is what lets code react to
30
49
  `turn-start`, `step-start`, and `step-end` before the next model snapshot is
31
50
  created.
32
51
 
52
+ `model` is the single public constructor key for model execution. Pass an AI SDK
53
+ `LanguageModel` for the managed runtime path with `instructions`, `tools`, and
54
+ `subagents`, or pass a custom `RuntimeLlm` function when you want to own the model
55
+ adapter yourself:
56
+
57
+ ```ts
58
+ import { Agent, type RuntimeLlm } from "@minpeter/pss-runtime";
59
+
60
+ const runtimeModel: RuntimeLlm = async ({ history }) => [
61
+ { role: "assistant", content: `Seen ${history.length} messages.` },
62
+ ];
63
+
64
+ const agent = new Agent({
65
+ model: runtimeModel,
66
+ });
67
+ ```
68
+
33
69
  Per-key conversations use `session(key)`:
34
70
 
35
71
  ```ts
@@ -117,9 +153,11 @@ delegate_to_researcher({
117
153
  ```
118
154
 
119
155
  When the model sets `run_in_background: true`, the parent run can finish while
120
- the child keeps working. The launch result includes a `bg_...` `task_id`. A
121
- compact runtime reminder is queued for the parent when the child finishes, and
122
- the model can retrieve the result with `background_output`.
156
+ the child keeps working. The launch result includes a `bg_...` `task_id` and
157
+ tells the model to wait for a `<system-reminder>` before retrieving output.
158
+ When background work finishes, the session is notified with compact runtime
159
+ input. Hosts that support durable background work should resume the parent session
160
+ through the notification inbox and drain the returned `AgentRun`.
123
161
 
124
162
  ```ts
125
163
  delegate_to_researcher({
@@ -127,6 +165,7 @@ delegate_to_researcher({
127
165
  run_in_background: true,
128
166
  });
129
167
 
168
+ // After the completion reminder arrives:
130
169
  background_output({ task_id: "bg_...", block: true });
131
170
  background_cancel({ task_id: "bg_..." });
132
171
  ```
@@ -134,20 +173,38 @@ background_cancel({ task_id: "bg_..." });
134
173
  The parent model context stays compact by default: completion reminders include
135
174
  the task id, subagent name, description, and retrieval instruction. Full child
136
175
  traces are not injected into the parent transcript by default. Background jobs
137
- run in task-scoped child sessions, and retrieved completed jobs are forgotten
138
- after `background_output` returns.
176
+ run in task-scoped child sessions. In-memory job handles are cleaned up after
177
+ completed output retrieval, while durable hosts keep compact completed output
178
+ available through the execution store for reconstructed agents. Background jobs
179
+ launched during the same parent turn share an internal completion barrier:
180
+ successful jobs notify once when the group settles, while failed jobs notify
181
+ immediately.
139
182
 
140
- ## Send and Steer
183
+ ## Send, Host Resume, and Steer
141
184
 
142
185
  Use `session.send(input)` for a new user turn. If a run is already active, the
143
- turn is queued until the active run finishes. Use `session.steer(input)` when the
144
- input should steer the active run; if no run is active, it starts a normal run.
186
+ turn is queued until the active run finishes. Use `session.steer(input)` when
187
+ the input should steer the active run; if no run is active, it starts a normal
188
+ run.
189
+
190
+ Durable hosts resume completed background work by writing a notification record
191
+ and calling `agent.resume(notificationRunId)`. The resume call claims the
192
+ notification idempotently through its durable run id and returns one `AgentRun`,
193
+ or `null` when a duplicate queue/alarm delivery already claimed it.
194
+
195
+ Runtime-originated input is delivered through the host notification inbox and
196
+ internal plugin paths. App code should use `session.send()`, `session.steer()`,
197
+ or `agent.resume(runId)` for host-scheduled durable work.
145
198
 
146
- Both APIs accept the same input shapes: strings, arrays of strings,
199
+ Each accepted call returns one `AgentRun`. Drain that run's `events()` stream to
200
+ observe the turn; each `AgentRun.events()` stream is single-consumer.
201
+
202
+ Input APIs accept the same input shapes: strings, arrays of strings,
147
203
  `{ type: "user-text", text }`, and multipart `{ type: "user-message", content }`
148
- values. Active steering emits `runtime-input` events. A `runtime-input` is
149
- runtime/API-originated input mapped internally to the model's user role. It is
150
- distinct from human-origin `user-text` and `user-message` events.
204
+ values. Active steering and host resume input emit `runtime-input` events. A
205
+ `runtime-input` is runtime/API-originated input mapped internally to the model's
206
+ user role. It is distinct from human-origin `user-text` and `user-message`
207
+ events.
151
208
 
152
209
  Runtime input windows are tied to synchronized events:
153
210
 
@@ -188,6 +245,10 @@ Stored session state is an opaque, versioned runtime snapshot for continuation.
188
245
  Do not inspect it as a replay log; exact replay should be modeled separately as
189
246
  an `AgentEvent` log if that capability is added later.
190
247
 
248
+ `SessionStore` is snapshot-only. It does not own background task ids, run
249
+ leases, checkpoints, notification inbox state, or scheduling. Those live on the
250
+ optional `host` execution contract.
251
+
191
252
  Custom stores own version generation. `load(key)` returns the opaque `state` with
192
253
  the store-minted `version`; `commit(key, { state }, { expectedVersion })` receives
193
254
  state only and should reject stale versions by returning `{ ok: false, reason:
@@ -196,15 +257,14 @@ new version to the runtime. `delete(key)` removes the persisted session for that
196
257
  key.
197
258
 
198
259
  ```ts
199
- import type { SessionStore } from "@minpeter/pss-runtime";
200
260
  import { MemorySessionStore } from "@minpeter/pss-runtime/session-store/memory";
201
261
 
202
262
  const agent = new Agent({
203
- model,
204
- sessions: {
205
- namespace: "support-agent",
206
- store: new MemorySessionStore(), // default when omitted
263
+ host: {
264
+ sessionStore: new MemorySessionStore(), // default when omitted
207
265
  },
266
+ model,
267
+ namespace: "support-agent",
208
268
  });
209
269
  ```
210
270
 
@@ -216,17 +276,65 @@ session and child `sessionKey` suffixes back to the same child transcripts:
216
276
  import { FileSessionStore } from "@minpeter/pss-runtime/session-store/file";
217
277
 
218
278
  const agent = new Agent({
219
- model,
220
- sessions: {
221
- namespace: "support-agent",
222
- store: new FileSessionStore(".pss/sessions"),
279
+ host: {
280
+ sessionStore: new FileSessionStore(".pss/sessions"),
223
281
  },
282
+ model,
283
+ namespace: "support-agent",
224
284
  });
225
285
  ```
226
286
 
227
- ## Future adapter boundary: Cloudflare multi-user DX
287
+ Hosts that need durable runs pass `host:` into `Agent`. A durable host owns
288
+ execution state, scheduling, and its session snapshot store through
289
+ `store.sessions`; `ExecutionHost` does not accept a separate `sessionStore`
290
+ override.
291
+
292
+ ```ts
293
+ import { Agent } from "@minpeter/pss-runtime";
294
+ import {
295
+ createInMemoryExecutionHost,
296
+ type ExecutionHost,
297
+ } from "@minpeter/pss-runtime/execution";
298
+
299
+ const host = createInMemoryExecutionHost();
300
+
301
+ const agent = new Agent({
302
+ host,
303
+ model,
304
+ namespace: "support-agent",
305
+ });
306
+
307
+ const durableHost: ExecutionHost = {
308
+ capabilities: { backgroundSubagents: "durable" },
309
+ scheduler,
310
+ store,
311
+ };
312
+ ```
313
+
314
+ ## Supported Deployment Shapes
315
+
316
+ The runtime supports both long-running Node.js processes and edge hosts that
317
+ reconstruct runtime objects between turns. The same public DX stays centered on
318
+ `new Agent({ subagents: [...] })`; host-specific durability and scheduling live
319
+ behind the `host` boundary.
320
+
321
+ Long-running Node.js can keep an `Agent` and `SessionHandle` alive across turns.
322
+ The default in-memory host advertises in-process background subagent capability,
323
+ so `run_in_background`, `background_output`, and `background_cancel` remain
324
+ available while that process owns the work. `FileSessionStore` persists session
325
+ snapshots only; it does not make background work durable by itself.
326
+
327
+ Cloudflare Durable Objects and similar edge hosts should reconstruct `Agent`
328
+ objects per turn and persist opaque session state through `store.sessions`.
329
+ When a host does not advertise background subagent capability, the runtime hides
330
+ background tools from the parent model and exposes blocking delegation only.
331
+ This avoids pretending that in-memory background work survived a hibernation,
332
+ isolate restart, or request deadline.
333
+
334
+ See `examples/cloudflare-edge-subagent` for an edge-hosted turn loop with a
335
+ Worker/Durable Object-shaped host, and `examples/subagent` for a long-running
336
+ local background subagent flow.
228
337
 
229
- Cloudflare Durable Objects are a future adapter target, not a runtime dependency.
230
338
  The same core API supports room/user/session routing through stable session keys.
231
339
 
232
340
  Recommended key patterns:
@@ -235,6 +343,39 @@ Recommended key patterns:
235
343
  - Per-user memory inside room: `room:<roomId>:user:<userId>`
236
344
  - Ticketed workspace flows: `tenant:<tenantId>:ticket:<ticketId>`
237
345
 
238
- In a Durable Object, map the `SessionStore` contract to `ctx.storage` so DO storage is
239
- durable across hibernation/restores, while in-memory state remains request-local.
240
- Do not store canonical agent session state in memory attachments.
346
+ In a Durable Object, map the execution store contract to `ctx.storage` so DO
347
+ storage is durable across hibernation/restores, while in-memory state remains
348
+ request-local. Do not store canonical agent session or run state in memory
349
+ attachments.
350
+
351
+ Durable background subagents require a host capability that owns task ids,
352
+ attempts, leases, checkpoints, cancellation, scheduling, and completion
353
+ notifications. The Cloudflare edge example includes a Worker/Durable
354
+ Object-shaped host that persists scheduled runs and session prompts, sets alarms, and
355
+ resumes work through `Agent.resume(...)`.
356
+
357
+ ## Checkpoints and Cancellation
358
+
359
+ Resume is safe only at committed boundaries. Durable hosts can checkpoint before
360
+ and after model steps, around notifications, before child run creation, when a
361
+ child link is committed, and when a run suspends. If a process is killed inside a
362
+ provider call or unsafe tool execution, resume rolls back to the last committed
363
+ checkpoint and may re-enter the operation.
364
+
365
+ When `Agent` receives an `ExecutionHost`, high-level model turns create a
366
+ `user-turn` run record and thread tool execution context into managed model
367
+ calls. Tools are checkpointed before and after execution and receive stable
368
+ `attempt`, `idempotencyKey`, `retryPolicy`, `signal`, and public `toolCallId`
369
+ values. The `@minpeter/pss-runtime/execution` entrypoint also exposes the same
370
+ low-level tool execution checkpoint types for custom resume runners built
371
+ directly on `createLlm`.
372
+
373
+ These checkpoints are rollback boundaries, not a complete host adapter by
374
+ themselves. Edge hosts still need durable scheduling, leases, resume workers,
375
+ and notification resume handling; externally visible side-effect tools still need
376
+ idempotent execution or a manual recovery flow.
377
+
378
+ Cancellation is persisted before aborting active work. Parent `delete()` and
379
+ `kill()` mark linked child runs as `cancelled` through the execution store before
380
+ falling back to process-local cleanup, so stale child completions cannot enqueue
381
+ new parent notifications.
@@ -0,0 +1,16 @@
1
+ import { executionHost } from "./execution/host.js";
2
+ import { cancelBackgroundChildRun } from "./subagent-background-child-run.js";
3
+ //#region src/agent-child-runs.ts
4
+ async function cancelDurableChildRuns(host, parentRunId) {
5
+ const durableHost = executionHost(host);
6
+ if (!durableHost) return;
7
+ const childRuns = await durableHost.store.runs.listByParentRunId(parentRunId);
8
+ await Promise.all(childRuns.map((run) => cancelBackgroundChildRun({
9
+ executionHost: durableHost,
10
+ runId: run.runId
11
+ })));
12
+ }
13
+ //#endregion
14
+ export { cancelDurableChildRuns };
15
+
16
+ //# sourceMappingURL=agent-child-runs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-child-runs.js","names":[],"sources":["../src/agent-child-runs.ts"],"sourcesContent":["import { executionHost } from \"./execution/host\";\nimport type { AgentHost } from \"./execution/types\";\nimport { cancelBackgroundChildRun } from \"./subagent-background-child-run\";\n\nexport async function cancelDurableChildRuns(\n host: AgentHost,\n parentRunId: string\n): Promise<void> {\n const durableHost = executionHost(host);\n if (!durableHost) {\n return;\n }\n\n const childRuns = await durableHost.store.runs.listByParentRunId(parentRunId);\n await Promise.all(\n childRuns.map((run) =>\n cancelBackgroundChildRun({\n executionHost: durableHost,\n runId: run.runId,\n })\n )\n );\n}\n"],"mappings":";;;AAIA,eAAsB,uBACpB,MACA,aACe;CACf,MAAM,cAAc,cAAc,IAAI;CACtC,IAAI,CAAC,aACH;CAGF,MAAM,YAAY,MAAM,YAAY,MAAM,KAAK,kBAAkB,WAAW;CAC5E,MAAM,QAAQ,IACZ,UAAU,KAAK,QACb,yBAAyB;EACvB,eAAe;EACf,OAAO,IAAI;CACb,CAAC,CACH,CACF;AACF"}
@@ -0,0 +1,9 @@
1
+ //#region src/agent-host-capabilities.ts
2
+ function supportsBackgroundSubagents(host, executionHost) {
3
+ const capability = host.capabilities?.backgroundSubagents;
4
+ return capability === "in-process" || capability === "durable" && executionHost !== void 0;
5
+ }
6
+ //#endregion
7
+ export { supportsBackgroundSubagents };
8
+
9
+ //# sourceMappingURL=agent-host-capabilities.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-host-capabilities.js","names":[],"sources":["../src/agent-host-capabilities.ts"],"sourcesContent":["import type { AgentHost, ExecutionHost } from \"./execution/types\";\n\nexport function supportsBackgroundSubagents(\n host: AgentHost,\n executionHost: ExecutionHost | undefined\n): boolean {\n const capability = host.capabilities?.backgroundSubagents;\n return (\n capability === \"in-process\" ||\n (capability === \"durable\" && executionHost !== undefined)\n );\n}\n"],"mappings":";AAEA,SAAgB,4BACd,MACA,eACS;CACT,MAAM,aAAa,KAAK,cAAc;CACtC,OACE,eAAe,gBACd,eAAe,aAAa,kBAAkB,KAAA;AAEnD"}
@@ -0,0 +1,12 @@
1
+ import { executionHost } from "./execution/host.js";
2
+ import { MemorySessionStore } from "./session/store/memory.js";
3
+ //#region src/agent-host-session-store.ts
4
+ function sessionStoreForHost(host) {
5
+ if ("sessionStore" in host && host.sessionStore) return host.sessionStore;
6
+ const hostExecution = executionHost(host);
7
+ return hostExecution ? hostExecution.store.sessions : new MemorySessionStore();
8
+ }
9
+ //#endregion
10
+ export { sessionStoreForHost };
11
+
12
+ //# sourceMappingURL=agent-host-session-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-host-session-store.js","names":[],"sources":["../src/agent-host-session-store.ts"],"sourcesContent":["import { executionHost } from \"./execution/host\";\nimport type { AgentHost } from \"./execution/types\";\nimport { MemorySessionStore } from \"./session/store/memory\";\nimport type { SessionStore } from \"./session/store/types\";\n\nexport function sessionStoreForHost(host: AgentHost): SessionStore {\n if (\"sessionStore\" in host && host.sessionStore) {\n return host.sessionStore;\n }\n\n const hostExecution = executionHost(host);\n return hostExecution\n ? hostExecution.store.sessions\n : new MemorySessionStore();\n}\n"],"mappings":";;;AAKA,SAAgB,oBAAoB,MAA+B;CACjE,IAAI,kBAAkB,QAAQ,KAAK,cACjC,OAAO,KAAK;CAGd,MAAM,gBAAgB,cAAc,IAAI;CACxC,OAAO,gBACH,cAAc,MAAM,WACpB,IAAI,mBAAmB;AAC7B"}
@@ -1,39 +1,29 @@
1
1
  import { modelMessageToAgentEvents } from "./session/mapping.js";
2
2
  //#region src/agent-loop.ts
3
- async function runAgentLoop({ emit, history, hooks, llm, signal = new AbortController().signal }) {
4
- let stepIndex = 0;
3
+ async function runAgentLoop({ captureObserverEvents = captureNoObserverEvents, emit, history, llm, signal = new AbortController().signal, toolExecution }) {
5
4
  while (true) {
6
- if (signal.aborted) return "aborted";
7
- await hooks?.beforeStep?.({
8
- history: history.modelSnapshot(),
9
- signal,
10
- stepIndex
11
- });
12
5
  if (signal.aborted) return "aborted";
13
6
  if (await emitBoundary({
14
7
  emit,
15
8
  event: { type: "step-start" },
16
9
  signal
17
10
  }) === "aborted") return "aborted";
18
- const output = await readLlmOutput({
11
+ const capturedOutput = await captureObserverEvents(() => readLlmOutput({
19
12
  history,
20
13
  llm,
21
- signal
22
- });
14
+ signal,
15
+ toolExecution
16
+ }));
17
+ const output = capturedOutput.value;
23
18
  if (output === "aborted") return "aborted";
24
- const result = appendStepOutput({
19
+ const result = await appendCapturedStepOutput({
20
+ capturedOutput,
25
21
  emit,
26
22
  history,
27
23
  output,
28
24
  signal
29
25
  });
30
26
  if (result === "aborted") return "aborted";
31
- await runAfterStepHook(hooks, {
32
- history: history.modelSnapshot(),
33
- result,
34
- signal,
35
- stepIndex
36
- });
37
27
  const stepEndDecision = await emitBoundary({
38
28
  emit,
39
29
  event: { type: "step-end" },
@@ -41,7 +31,6 @@ async function runAgentLoop({ emit, history, hooks, llm, signal = new AbortContr
41
31
  });
42
32
  if (stepEndDecision === "aborted") return "aborted";
43
33
  if (result === "completed" && !stepEndDecision?.runtimeInputAdded) return "completed";
44
- stepIndex += 1;
45
34
  }
46
35
  }
47
36
  async function emitBoundary({ emit, event, signal }) {
@@ -68,34 +57,75 @@ function createAbortBoundary(signal) {
68
57
  promise
69
58
  };
70
59
  }
71
- async function runAfterStepHook(hooks, context) {
72
- const hook = hooks?.afterStep;
73
- if (!hook) return;
74
- await Promise.allSettled([Promise.resolve().then(() => hook(context))]);
60
+ async function captureNoObserverEvents(callback) {
61
+ return {
62
+ events: [],
63
+ release: releaseNoObserverEvents,
64
+ value: await callback()
65
+ };
75
66
  }
76
- async function readLlmOutput({ history, llm, signal }) {
67
+ function releaseNoObserverEvents() {}
68
+ async function readLlmOutput({ history, llm, signal, toolExecution }) {
77
69
  try {
78
70
  return await llm({
79
71
  history: history.modelSnapshot(),
80
- signal
72
+ signal,
73
+ toolExecution
81
74
  });
82
75
  } catch (error) {
83
76
  if (signal.aborted) return "aborted";
84
77
  throw error;
85
78
  }
86
79
  }
87
- function appendStepOutput({ emit, history, output, signal }) {
80
+ async function appendCapturedStepOutput({ capturedOutput, emit, history, output, signal }) {
81
+ try {
82
+ return await appendStepOutput({
83
+ emit,
84
+ history,
85
+ observerEvents: capturedOutput.events,
86
+ output,
87
+ signal
88
+ });
89
+ } finally {
90
+ capturedOutput.release();
91
+ }
92
+ }
93
+ async function appendStepOutput({ emit, history, observerEvents, output, signal }) {
88
94
  if (signal.aborted) return "aborted";
89
95
  let shouldContinue = false;
96
+ const pendingObserverEvents = observerEvents;
97
+ const flushObserverEvents = async (shouldFlush = () => true) => {
98
+ for (let index = 0; index < pendingObserverEvents.length;) {
99
+ const event = pendingObserverEvents[index];
100
+ if (!(event && shouldFlush(event))) {
101
+ index += 1;
102
+ continue;
103
+ }
104
+ pendingObserverEvents.splice(index, 1);
105
+ await emit(event);
106
+ }
107
+ };
90
108
  for (const message of output) {
91
109
  if (signal.aborted) return "aborted";
92
110
  history.appendModelMessage(message);
93
111
  const events = modelMessageToAgentEvents(message);
94
- for (const event of events) emit(event);
95
- if (events.some((event) => event.type === "tool-call")) shouldContinue = true;
112
+ const hasToolResult = events.some((event) => event.type === "tool-result");
113
+ for (const event of events) {
114
+ await emit(event);
115
+ if (event.type === "tool-call") {
116
+ shouldContinue = true;
117
+ await flushObserverEvents(isLaunchOrBlockingObserverEvent);
118
+ }
119
+ }
120
+ if (hasToolResult) await flushObserverEvents();
96
121
  }
122
+ await flushObserverEvents();
97
123
  return shouldContinue ? "continue" : "completed";
98
124
  }
125
+ function isLaunchOrBlockingObserverEvent(event) {
126
+ if (event.type === "subagent-job-update") return false;
127
+ return !(event.type === "subagent-job-end" && event.task_id);
128
+ }
99
129
  //#endregion
100
130
  export { runAgentLoop };
101
131
 
@@ -1 +1 @@
1
- {"version":3,"file":"agent-loop.js","names":[],"sources":["../src/agent-loop.ts"],"sourcesContent":["import type { ModelMessage } from \"ai\";\nimport type { AgentHooks, AgentStepResult } from \"./hooks\";\nimport type { Llm, LlmOutput } from \"./llm\";\nimport type { AgentEvent, AgentEventListener } from \"./session/events\";\nimport { modelMessageToAgentEvents } from \"./session/mapping\";\n\ninterface ModelHistory {\n appendModelMessage(message: ModelMessage): void;\n modelSnapshot(): ModelMessage[];\n}\n\ninterface RunAgentLoopOptions {\n emit: AgentLoopEventListener;\n history: ModelHistory;\n hooks?: AgentHooks;\n llm: Llm;\n signal?: AbortSignal;\n}\n\nexport type AgentLoopResult = \"completed\" | \"aborted\";\ntype AgentLoopBoundaryEvent = Extract<\n AgentEvent,\n { type: \"step-end\" } | { type: \"step-start\" }\n>;\ninterface AgentLoopBoundaryDecision {\n readonly runtimeInputAdded?: boolean;\n}\ntype AgentLoopEventListener = (\n event: AgentEvent\n) =>\n | AgentLoopBoundaryDecision\n | Promise<AgentLoopBoundaryDecision | undefined>\n | undefined;\ntype StepOutputResult = AgentStepResult | \"aborted\";\n\nexport async function runAgentLoop({\n emit,\n history,\n hooks,\n llm,\n signal = new AbortController().signal,\n}: RunAgentLoopOptions): Promise<AgentLoopResult> {\n let stepIndex = 0;\n\n while (true) {\n if (signal.aborted) {\n return \"aborted\";\n }\n\n await hooks?.beforeStep?.({\n history: history.modelSnapshot(),\n signal,\n stepIndex,\n });\n\n if (signal.aborted) {\n return \"aborted\";\n }\n\n const stepStartDecision = await emitBoundary({\n emit,\n event: { type: \"step-start\" },\n signal,\n });\n\n if (stepStartDecision === \"aborted\") {\n return \"aborted\";\n }\n\n const output = await readLlmOutput({ history, llm, signal });\n\n if (output === \"aborted\") {\n return \"aborted\";\n }\n\n const result = appendStepOutput({ emit, history, output, signal });\n\n if (result === \"aborted\") {\n return \"aborted\";\n }\n\n await runAfterStepHook(hooks, {\n history: history.modelSnapshot(),\n result,\n signal,\n stepIndex,\n });\n\n const stepEndDecision = await emitBoundary({\n emit,\n event: { type: \"step-end\" },\n signal,\n });\n\n if (stepEndDecision === \"aborted\") {\n return \"aborted\";\n }\n\n // Runtime input after step-end intentionally forces another inference step,\n // even after final-looking assistant text. Unconditional insertion on every\n // step-end can create an unbounded loop.\n if (result === \"completed\" && !stepEndDecision?.runtimeInputAdded) {\n return \"completed\";\n }\n\n stepIndex += 1;\n }\n}\n\nasync function emitBoundary({\n emit,\n event,\n signal,\n}: Pick<RunAgentLoopOptions, \"emit\"> & {\n event: AgentLoopBoundaryEvent;\n signal: AbortSignal;\n}): Promise<AgentLoopBoundaryDecision | \"aborted\" | undefined> {\n if (signal.aborted) {\n return \"aborted\";\n }\n\n const abort = createAbortBoundary(signal);\n try {\n return await Promise.race([Promise.resolve(emit(event)), abort.promise]);\n } catch (error) {\n if (signal.aborted) {\n return \"aborted\";\n }\n\n throw error;\n } finally {\n abort.dispose();\n }\n}\n\nfunction createAbortBoundary(signal: AbortSignal): {\n dispose: () => void;\n promise: Promise<\"aborted\">;\n} {\n let dispose: () => void = () => undefined;\n\n const promise = new Promise<\"aborted\">((resolve) => {\n const onAbort = () => resolve(\"aborted\");\n dispose = () => signal.removeEventListener(\"abort\", onAbort);\n signal.addEventListener(\"abort\", onAbort, { once: true });\n });\n\n return { dispose, promise };\n}\n\nasync function runAfterStepHook(\n hooks: AgentHooks | undefined,\n context: Parameters<NonNullable<AgentHooks[\"afterStep\"]>>[0]\n): Promise<void> {\n const hook = hooks?.afterStep;\n if (!hook) {\n return;\n }\n\n await Promise.allSettled([Promise.resolve().then(() => hook(context))]);\n}\n\nasync function readLlmOutput({\n history,\n llm,\n signal,\n}: Pick<RunAgentLoopOptions, \"history\" | \"llm\"> & {\n signal: AbortSignal;\n}): Promise<LlmOutput | \"aborted\"> {\n try {\n return await llm({ history: history.modelSnapshot(), signal });\n } catch (error) {\n if (signal.aborted) {\n return \"aborted\";\n }\n\n throw error;\n }\n}\n\nfunction appendStepOutput({\n emit,\n history,\n output,\n signal,\n}: { emit: AgentEventListener; history: ModelHistory } & {\n output: LlmOutput;\n signal: AbortSignal;\n}): StepOutputResult {\n if (signal.aborted) {\n return \"aborted\";\n }\n\n let shouldContinue = false;\n\n for (const message of output) {\n if (signal.aborted) {\n return \"aborted\";\n }\n\n history.appendModelMessage(message);\n const events = modelMessageToAgentEvents(message);\n\n for (const event of events) {\n emit(event);\n }\n\n if (events.some((event) => event.type === \"tool-call\")) {\n shouldContinue = true;\n }\n }\n\n return shouldContinue ? \"continue\" : \"completed\";\n}\n"],"mappings":";;AAmCA,eAAsB,aAAa,EACjC,MACA,SACA,OACA,KACA,SAAS,IAAI,gBAAgB,EAAE,UACiB;CAChD,IAAI,YAAY;CAEhB,OAAO,MAAM;EACX,IAAI,OAAO,SACT,OAAO;EAGT,MAAM,OAAO,aAAa;GACxB,SAAS,QAAQ,cAAc;GAC/B;GACA;EACF,CAAC;EAED,IAAI,OAAO,SACT,OAAO;EAST,IAAI,MAN4B,aAAa;GAC3C;GACA,OAAO,EAAE,MAAM,aAAa;GAC5B;EACF,CAAC,MAEyB,WACxB,OAAO;EAGT,MAAM,SAAS,MAAM,cAAc;GAAE;GAAS;GAAK;EAAO,CAAC;EAE3D,IAAI,WAAW,WACb,OAAO;EAGT,MAAM,SAAS,iBAAiB;GAAE;GAAM;GAAS;GAAQ;EAAO,CAAC;EAEjE,IAAI,WAAW,WACb,OAAO;EAGT,MAAM,iBAAiB,OAAO;GAC5B,SAAS,QAAQ,cAAc;GAC/B;GACA;GACA;EACF,CAAC;EAED,MAAM,kBAAkB,MAAM,aAAa;GACzC;GACA,OAAO,EAAE,MAAM,WAAW;GAC1B;EACF,CAAC;EAED,IAAI,oBAAoB,WACtB,OAAO;EAMT,IAAI,WAAW,eAAe,CAAC,iBAAiB,mBAC9C,OAAO;EAGT,aAAa;CACf;AACF;AAEA,eAAe,aAAa,EAC1B,MACA,OACA,UAI6D;CAC7D,IAAI,OAAO,SACT,OAAO;CAGT,MAAM,QAAQ,oBAAoB,MAAM;CACxC,IAAI;EACF,OAAO,MAAM,QAAQ,KAAK,CAAC,QAAQ,QAAQ,KAAK,KAAK,CAAC,GAAG,MAAM,OAAO,CAAC;CACzE,SAAS,OAAO;EACd,IAAI,OAAO,SACT,OAAO;EAGT,MAAM;CACR,UAAU;EACR,MAAM,QAAQ;CAChB;AACF;AAEA,SAAS,oBAAoB,QAG3B;CACA,IAAI,gBAA4B,KAAA;CAEhC,MAAM,UAAU,IAAI,SAAoB,YAAY;EAClD,MAAM,gBAAgB,QAAQ,SAAS;EACvC,gBAAgB,OAAO,oBAAoB,SAAS,OAAO;EAC3D,OAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;CAC1D,CAAC;CAED,OAAO;EAAE;EAAS;CAAQ;AAC5B;AAEA,eAAe,iBACb,OACA,SACe;CACf,MAAM,OAAO,OAAO;CACpB,IAAI,CAAC,MACH;CAGF,MAAM,QAAQ,WAAW,CAAC,QAAQ,QAAQ,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC;AACxE;AAEA,eAAe,cAAc,EAC3B,SACA,KACA,UAGiC;CACjC,IAAI;EACF,OAAO,MAAM,IAAI;GAAE,SAAS,QAAQ,cAAc;GAAG;EAAO,CAAC;CAC/D,SAAS,OAAO;EACd,IAAI,OAAO,SACT,OAAO;EAGT,MAAM;CACR;AACF;AAEA,SAAS,iBAAiB,EACxB,MACA,SACA,QACA,UAImB;CACnB,IAAI,OAAO,SACT,OAAO;CAGT,IAAI,iBAAiB;CAErB,KAAK,MAAM,WAAW,QAAQ;EAC5B,IAAI,OAAO,SACT,OAAO;EAGT,QAAQ,mBAAmB,OAAO;EAClC,MAAM,SAAS,0BAA0B,OAAO;EAEhD,KAAK,MAAM,SAAS,QAClB,KAAK,KAAK;EAGZ,IAAI,OAAO,MAAM,UAAU,MAAM,SAAS,WAAW,GACnD,iBAAiB;CAErB;CAEA,OAAO,iBAAiB,aAAa;AACvC"}
1
+ {"version":3,"file":"agent-loop.js","names":[],"sources":["../src/agent-loop.ts"],"sourcesContent":["import type { ModelMessage } from \"ai\";\nimport type { RuntimeLlm, RuntimeLlmOutput } from \"./llm\";\nimport type { RuntimeToolExecutionContext } from \"./llm-tool-execution\";\nimport type { AgentEvent } from \"./session/events\";\nimport { modelMessageToAgentEvents } from \"./session/mapping\";\n\ninterface ModelHistory {\n appendModelMessage(message: ModelMessage): void;\n modelSnapshot(): ModelMessage[];\n}\n\ninterface RunAgentLoopOptions {\n captureObserverEvents?: ObserverEventCapture;\n emit: AgentLoopEventListener;\n history: ModelHistory;\n llm: RuntimeLlm;\n signal?: AbortSignal;\n toolExecution?: RuntimeToolExecutionContext;\n}\n\ntype AgentLoopResult = \"completed\" | \"aborted\";\ntype AgentLoopBoundaryEvent = Extract<\n AgentEvent,\n { type: \"step-end\" } | { type: \"step-start\" }\n>;\ninterface AgentLoopBoundaryDecision {\n readonly runtimeInputAdded?: boolean;\n}\ntype AgentLoopEventListener = (\n event: AgentEvent\n) =>\n | AgentLoopBoundaryDecision\n | Promise<AgentLoopBoundaryDecision | undefined>\n | undefined;\ntype StepOutputResult = \"aborted\" | \"completed\" | \"continue\";\ninterface ObserverEventCaptureResult<T> {\n readonly events: AgentEvent[];\n readonly release: () => void;\n readonly value: T;\n}\ntype ObserverEventCapture = <T>(\n callback: () => Promise<T>\n) => Promise<ObserverEventCaptureResult<T>>;\n\nexport async function runAgentLoop({\n captureObserverEvents = captureNoObserverEvents,\n emit,\n history,\n llm,\n signal = new AbortController().signal,\n toolExecution,\n}: RunAgentLoopOptions): Promise<AgentLoopResult> {\n while (true) {\n if (signal.aborted) {\n return \"aborted\";\n }\n\n const stepStartDecision = await emitBoundary({\n emit,\n event: { type: \"step-start\" },\n signal,\n });\n\n if (stepStartDecision === \"aborted\") {\n return \"aborted\";\n }\n\n const capturedOutput = await captureObserverEvents(() =>\n readLlmOutput({ history, llm, signal, toolExecution })\n );\n const output = capturedOutput.value;\n\n if (output === \"aborted\") {\n return \"aborted\";\n }\n\n const result = await appendCapturedStepOutput({\n capturedOutput,\n emit,\n history,\n output,\n signal,\n });\n\n if (result === \"aborted\") {\n return \"aborted\";\n }\n\n const stepEndDecision = await emitBoundary({\n emit,\n event: { type: \"step-end\" },\n signal,\n });\n\n if (stepEndDecision === \"aborted\") {\n return \"aborted\";\n }\n\n // Runtime input after step-end intentionally forces another inference step,\n // even after final-looking assistant text. Unconditional insertion on every\n // step-end can create an unbounded loop.\n if (result === \"completed\" && !stepEndDecision?.runtimeInputAdded) {\n return \"completed\";\n }\n }\n}\n\nasync function emitBoundary({\n emit,\n event,\n signal,\n}: Pick<RunAgentLoopOptions, \"emit\"> & {\n event: AgentLoopBoundaryEvent;\n signal: AbortSignal;\n}): Promise<AgentLoopBoundaryDecision | \"aborted\" | undefined> {\n if (signal.aborted) {\n return \"aborted\";\n }\n\n const abort = createAbortBoundary(signal);\n try {\n return await Promise.race([Promise.resolve(emit(event)), abort.promise]);\n } catch (error) {\n if (signal.aborted) {\n return \"aborted\";\n }\n\n throw error;\n } finally {\n abort.dispose();\n }\n}\n\nfunction createAbortBoundary(signal: AbortSignal): {\n dispose: () => void;\n promise: Promise<\"aborted\">;\n} {\n let dispose: () => void = () => undefined;\n\n const promise = new Promise<\"aborted\">((resolve) => {\n const onAbort = () => resolve(\"aborted\");\n dispose = () => signal.removeEventListener(\"abort\", onAbort);\n signal.addEventListener(\"abort\", onAbort, { once: true });\n });\n\n return { dispose, promise };\n}\n\nasync function captureNoObserverEvents<T>(callback: () => Promise<T>): Promise<{\n readonly events: AgentEvent[];\n readonly release: () => void;\n readonly value: T;\n}> {\n return {\n events: [],\n release: releaseNoObserverEvents,\n value: await callback(),\n };\n}\n\nfunction releaseNoObserverEvents(): void {\n return;\n}\n\nasync function readLlmOutput({\n history,\n llm,\n signal,\n toolExecution,\n}: Pick<RunAgentLoopOptions, \"history\" | \"llm\"> & {\n signal: AbortSignal;\n toolExecution?: RuntimeToolExecutionContext;\n}): Promise<RuntimeLlmOutput | \"aborted\"> {\n try {\n return await llm({\n history: history.modelSnapshot(),\n signal,\n toolExecution,\n });\n } catch (error) {\n if (signal.aborted) {\n return \"aborted\";\n }\n\n throw error;\n }\n}\n\nasync function appendCapturedStepOutput({\n capturedOutput,\n emit,\n history,\n output,\n signal,\n}: Pick<RunAgentLoopOptions, \"emit\"> & { history: ModelHistory } & {\n capturedOutput: ObserverEventCaptureResult<RuntimeLlmOutput | \"aborted\">;\n output: RuntimeLlmOutput;\n signal: AbortSignal;\n}): Promise<StepOutputResult> {\n try {\n return await appendStepOutput({\n emit,\n history,\n observerEvents: capturedOutput.events,\n output,\n signal,\n });\n } finally {\n capturedOutput.release();\n }\n}\n\nasync function appendStepOutput({\n emit,\n history,\n observerEvents,\n output,\n signal,\n}: Pick<RunAgentLoopOptions, \"emit\"> & { history: ModelHistory } & {\n observerEvents: AgentEvent[];\n output: RuntimeLlmOutput;\n signal: AbortSignal;\n}): Promise<StepOutputResult> {\n if (signal.aborted) {\n return \"aborted\";\n }\n\n let shouldContinue = false;\n const pendingObserverEvents = observerEvents;\n const flushObserverEvents = async (\n shouldFlush: (event: AgentEvent) => boolean = () => true\n ) => {\n for (let index = 0; index < pendingObserverEvents.length; ) {\n const event = pendingObserverEvents[index];\n if (!(event && shouldFlush(event))) {\n index += 1;\n continue;\n }\n pendingObserverEvents.splice(index, 1);\n await emit(event);\n }\n };\n\n for (const message of output) {\n if (signal.aborted) {\n return \"aborted\";\n }\n\n history.appendModelMessage(message);\n const events = modelMessageToAgentEvents(message);\n const hasToolResult = events.some((event) => event.type === \"tool-result\");\n\n for (const event of events) {\n await emit(event);\n if (event.type === \"tool-call\") {\n shouldContinue = true;\n await flushObserverEvents(isLaunchOrBlockingObserverEvent);\n }\n }\n\n if (hasToolResult) {\n await flushObserverEvents();\n }\n }\n\n await flushObserverEvents();\n\n return shouldContinue ? \"continue\" : \"completed\";\n}\n\nfunction isLaunchOrBlockingObserverEvent(event: AgentEvent): boolean {\n if (event.type === \"subagent-job-update\") {\n return false;\n }\n\n return !(event.type === \"subagent-job-end\" && event.task_id);\n}\n"],"mappings":"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"}
@@ -11,7 +11,14 @@ function namespacePart(value) {
11
11
  function parentSessionNamespace({ generation, sessionKey, sessionNamespace }) {
12
12
  return `${sessionNamespace}:session:${namespacePart(sessionKey)}:generation:${generation}`;
13
13
  }
14
+ function ownsAgentNamespace(ownerNamespace, sessionNamespace) {
15
+ return ownerNamespace === sessionNamespace || ownerNamespace?.startsWith(`${sessionNamespace}:session:`) === true;
16
+ }
17
+ function stableAgentNamespace({ name, namespace }) {
18
+ const stableNamespace = namespace ?? name;
19
+ return stableNamespace ? agentNamespace(stableNamespace) : randomAgentNamespace();
20
+ }
14
21
  //#endregion
15
- export { agentNamespace, parentSessionNamespace, randomAgentNamespace };
22
+ export { ownsAgentNamespace, parentSessionNamespace, stableAgentNamespace };
16
23
 
17
24
  //# sourceMappingURL=agent-namespace.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"agent-namespace.js","names":[],"sources":["../src/agent-namespace.ts"],"sourcesContent":["export function randomAgentNamespace(): string {\n return agentNamespace(crypto.randomUUID());\n}\n\nexport function agentNamespace(namespace: string): string {\n return `agent:${namespacePart(namespace)}`;\n}\n\nexport function namespacePart(value: string): string {\n return encodeURIComponent(value);\n}\n\nexport function parentSessionNamespace({\n generation,\n sessionKey,\n sessionNamespace,\n}: {\n readonly generation: number;\n readonly sessionKey: string;\n readonly sessionNamespace: string;\n}): string {\n return `${sessionNamespace}:session:${namespacePart(\n sessionKey\n )}:generation:${generation}`;\n}\n"],"mappings":";AAAA,SAAgB,uBAA+B;CAC7C,OAAO,eAAe,OAAO,WAAW,CAAC;AAC3C;AAEA,SAAgB,eAAe,WAA2B;CACxD,OAAO,SAAS,cAAc,SAAS;AACzC;AAEA,SAAgB,cAAc,OAAuB;CACnD,OAAO,mBAAmB,KAAK;AACjC;AAEA,SAAgB,uBAAuB,EACrC,YACA,YACA,oBAKS;CACT,OAAO,GAAG,iBAAiB,WAAW,cACpC,UACF,EAAE,cAAc;AAClB"}
1
+ {"version":3,"file":"agent-namespace.js","names":[],"sources":["../src/agent-namespace.ts"],"sourcesContent":["export function randomAgentNamespace(): string {\n return agentNamespace(crypto.randomUUID());\n}\n\nexport function agentNamespace(namespace: string): string {\n return `agent:${namespacePart(namespace)}`;\n}\n\nexport function namespacePart(value: string): string {\n return encodeURIComponent(value);\n}\n\nexport function parentSessionNamespace({\n generation,\n sessionKey,\n sessionNamespace,\n}: {\n readonly generation: number;\n readonly sessionKey: string;\n readonly sessionNamespace: string;\n}): string {\n return `${sessionNamespace}:session:${namespacePart(\n sessionKey\n )}:generation:${generation}`;\n}\n\nexport function ownsAgentNamespace(\n ownerNamespace: string | undefined,\n sessionNamespace: string\n): boolean {\n return (\n ownerNamespace === sessionNamespace ||\n ownerNamespace?.startsWith(`${sessionNamespace}:session:`) === true\n );\n}\n\nexport function stableAgentNamespace({\n name,\n namespace,\n}: {\n readonly name?: string;\n readonly namespace?: string;\n}): string {\n const stableNamespace = namespace ?? name;\n return stableNamespace\n ? agentNamespace(stableNamespace)\n : randomAgentNamespace();\n}\n"],"mappings":";AAAA,SAAgB,uBAA+B;CAC7C,OAAO,eAAe,OAAO,WAAW,CAAC;AAC3C;AAEA,SAAgB,eAAe,WAA2B;CACxD,OAAO,SAAS,cAAc,SAAS;AACzC;AAEA,SAAgB,cAAc,OAAuB;CACnD,OAAO,mBAAmB,KAAK;AACjC;AAEA,SAAgB,uBAAuB,EACrC,YACA,YACA,oBAKS;CACT,OAAO,GAAG,iBAAiB,WAAW,cACpC,UACF,EAAE,cAAc;AAClB;AAEA,SAAgB,mBACd,gBACA,kBACS;CACT,OACE,mBAAmB,oBACnB,gBAAgB,WAAW,GAAG,iBAAiB,UAAU,MAAM;AAEnE;AAEA,SAAgB,qBAAqB,EACnC,MACA,aAIS;CACT,MAAM,kBAAkB,aAAa;CACrC,OAAO,kBACH,eAAe,eAAe,IAC9B,qBAAqB;AAC3B"}
@@ -0,0 +1,35 @@
1
+ import { AgentToolChoice, RuntimeLlm } from "./llm.js";
2
+ import { AgentHost } from "./execution/types.js";
3
+ import { AgentPlugin } from "./plugins.js";
4
+ import { Agent } from "./agent.js";
5
+ import { LanguageModel, ToolSet } from "ai";
6
+
7
+ //#region src/agent-options.d.ts
8
+ interface AgentLanguageModelOptions {
9
+ readonly description?: string;
10
+ readonly host?: AgentHost;
11
+ readonly instructions?: string;
12
+ readonly model: LanguageModel;
13
+ readonly name?: string;
14
+ readonly namespace?: string;
15
+ readonly plugins?: readonly AgentPlugin[];
16
+ readonly subagents?: readonly Agent[];
17
+ readonly toolChoice?: AgentToolChoice;
18
+ readonly tools?: ToolSet;
19
+ }
20
+ interface AgentRuntimeModelOptions {
21
+ readonly description?: string;
22
+ readonly host?: AgentHost;
23
+ readonly instructions?: never;
24
+ readonly model: RuntimeLlm;
25
+ readonly name?: string;
26
+ readonly namespace?: string;
27
+ readonly plugins?: readonly AgentPlugin[];
28
+ readonly subagents?: never;
29
+ readonly toolChoice?: never;
30
+ readonly tools?: never;
31
+ }
32
+ type AgentOptions = AgentLanguageModelOptions | AgentRuntimeModelOptions;
33
+ //#endregion
34
+ export { AgentOptions };
35
+ //# sourceMappingURL=agent-options.d.ts.map
@@ -0,0 +1,16 @@
1
+ //#region src/agent-options.ts
2
+ function assertAgentOptions(options) {
3
+ if (options === null || typeof options !== "object") throw new TypeError("Agent options are required. Provide { model }.");
4
+ if ("sessions" in options) throw new TypeError("Agent: unsupported options.sessions. Use host: { sessionStore } and namespace instead.");
5
+ if ("runtime" in options) throw new TypeError("Agent: unsupported options.runtime. Use host.");
6
+ if ("llm" in options) throw new TypeError("Agent: unsupported options.llm. Use model for both AI SDK models and custom RuntimeLlm functions.");
7
+ if (!("model" in options && options.model != null)) throw new TypeError("Agent: missing options.model.");
8
+ if (typeof options.model !== "function" && (typeof options.model !== "object" || options.model === null)) throw new TypeError("Agent: invalid options.model.");
9
+ }
10
+ function hasRuntimeModel(options) {
11
+ return typeof options.model === "function";
12
+ }
13
+ //#endregion
14
+ export { assertAgentOptions, hasRuntimeModel };
15
+
16
+ //# sourceMappingURL=agent-options.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-options.js","names":[],"sources":["../src/agent-options.ts"],"sourcesContent":["import type { LanguageModel, ToolSet } from \"ai\";\nimport type { Agent } from \"./agent\";\nimport type { AgentHost } from \"./execution/types\";\nimport type { AgentToolChoice, RuntimeLlm } from \"./llm\";\nimport type { AgentPlugin } from \"./plugins\";\n\ninterface AgentLanguageModelOptions {\n readonly description?: string;\n readonly host?: AgentHost;\n readonly instructions?: string;\n readonly model: LanguageModel;\n readonly name?: string;\n readonly namespace?: string;\n readonly plugins?: readonly AgentPlugin[];\n readonly subagents?: readonly Agent[];\n readonly toolChoice?: AgentToolChoice;\n readonly tools?: ToolSet;\n}\n\ninterface AgentRuntimeModelOptions {\n readonly description?: string;\n readonly host?: AgentHost;\n readonly instructions?: never;\n readonly model: RuntimeLlm;\n readonly name?: string;\n readonly namespace?: string;\n readonly plugins?: readonly AgentPlugin[];\n readonly subagents?: never;\n readonly toolChoice?: never;\n readonly tools?: never;\n}\n\nexport type AgentModelOptions = Pick<\n AgentLanguageModelOptions,\n \"instructions\" | \"model\" | \"toolChoice\"\n>;\nexport type AgentOptions = AgentLanguageModelOptions | AgentRuntimeModelOptions;\n\nexport function assertAgentOptions(\n options: unknown\n): asserts options is AgentOptions {\n if (options === null || typeof options !== \"object\") {\n throw new TypeError(\"Agent options are required. Provide { model }.\");\n }\n\n if (\"sessions\" in options) {\n throw new TypeError(\n \"Agent: unsupported options.sessions. Use host: { sessionStore } and namespace instead.\"\n );\n }\n\n if (\"runtime\" in options) {\n throw new TypeError(\"Agent: unsupported options.runtime. Use host.\");\n }\n\n if (\"llm\" in options) {\n throw new TypeError(\n \"Agent: unsupported options.llm. Use model for both AI SDK models and custom RuntimeLlm functions.\"\n );\n }\n\n const hasModel = \"model\" in options && options.model != null;\n\n if (!hasModel) {\n throw new TypeError(\"Agent: missing options.model.\");\n }\n\n if (\n typeof options.model !== \"function\" &&\n (typeof options.model !== \"object\" || options.model === null)\n ) {\n throw new TypeError(\"Agent: invalid options.model.\");\n }\n}\n\nexport function hasRuntimeModel(\n options: AgentOptions\n): options is AgentRuntimeModelOptions {\n return typeof options.model === \"function\";\n}\n"],"mappings":";AAsCA,SAAgB,mBACd,SACiC;CACjC,IAAI,YAAY,QAAQ,OAAO,YAAY,UACzC,MAAM,IAAI,UAAU,gDAAgD;CAGtE,IAAI,cAAc,SAChB,MAAM,IAAI,UACR,wFACF;CAGF,IAAI,aAAa,SACf,MAAM,IAAI,UAAU,+CAA+C;CAGrE,IAAI,SAAS,SACX,MAAM,IAAI,UACR,mGACF;CAKF,IAAI,EAFa,WAAW,WAAW,QAAQ,SAAS,OAGtD,MAAM,IAAI,UAAU,+BAA+B;CAGrD,IACE,OAAO,QAAQ,UAAU,eACxB,OAAO,QAAQ,UAAU,YAAY,QAAQ,UAAU,OAExD,MAAM,IAAI,UAAU,+BAA+B;AAEvD;AAEA,SAAgB,gBACd,SACqC;CACrC,OAAO,OAAO,QAAQ,UAAU;AAClC"}