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

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 (133) hide show
  1. package/README.md +290 -61
  2. package/dist/agent-host-session-store.js +10 -0
  3. package/dist/agent-host-session-store.js.map +1 -0
  4. package/dist/agent-loop.js +57 -28
  5. package/dist/agent-loop.js.map +1 -1
  6. package/dist/agent-namespace.js +6 -3
  7. package/dist/agent-namespace.js.map +1 -1
  8. package/dist/agent-options.d.ts +29 -0
  9. package/dist/agent-options.js +16 -0
  10. package/dist/agent-options.js.map +1 -0
  11. package/dist/agent-resume.js +63 -0
  12. package/dist/agent-resume.js.map +1 -0
  13. package/dist/agent-session-entry.d.ts +13 -0
  14. package/dist/agent.d.ts +8 -44
  15. package/dist/agent.js +61 -83
  16. package/dist/agent.js.map +1 -1
  17. package/dist/cloudflare/cloudflare-agent-context.d.ts +40 -0
  18. package/dist/cloudflare/cloudflare-agent-context.js +37 -0
  19. package/dist/cloudflare/cloudflare-agent-context.js.map +1 -0
  20. package/dist/cloudflare/cloudflare-alarm-budget.d.ts +18 -0
  21. package/dist/cloudflare/cloudflare-alarm-budget.js +77 -0
  22. package/dist/cloudflare/cloudflare-alarm-budget.js.map +1 -0
  23. package/dist/cloudflare/cloudflare-alarm-drainer.d.ts +45 -0
  24. package/dist/cloudflare/cloudflare-alarm-drainer.js +103 -0
  25. package/dist/cloudflare/cloudflare-alarm-drainer.js.map +1 -0
  26. package/dist/cloudflare/cloudflare-alarm-run-drain.d.ts +13 -0
  27. package/dist/cloudflare/cloudflare-alarm-run-drain.js +81 -0
  28. package/dist/cloudflare/cloudflare-alarm-run-drain.js.map +1 -0
  29. package/dist/cloudflare/cloudflare-alarm-work.js +110 -0
  30. package/dist/cloudflare/cloudflare-alarm-work.js.map +1 -0
  31. package/dist/cloudflare/cloudflare-checkpoint-store.js +39 -0
  32. package/dist/cloudflare/cloudflare-checkpoint-store.js.map +1 -0
  33. package/dist/cloudflare/cloudflare-durable-object-fetch.d.ts +21 -0
  34. package/dist/cloudflare/cloudflare-durable-object-fetch.js +11 -0
  35. package/dist/cloudflare/cloudflare-durable-object-fetch.js.map +1 -0
  36. package/dist/cloudflare/cloudflare-event-store.js +33 -0
  37. package/dist/cloudflare/cloudflare-event-store.js.map +1 -0
  38. package/dist/cloudflare/cloudflare-execution-session-store.js +40 -0
  39. package/dist/cloudflare/cloudflare-execution-session-store.js.map +1 -0
  40. package/dist/cloudflare/cloudflare-execution-store.js +35 -0
  41. package/dist/cloudflare/cloudflare-execution-store.js.map +1 -0
  42. package/dist/cloudflare/cloudflare-host.d.ts +61 -0
  43. package/dist/cloudflare/cloudflare-host.js +113 -0
  44. package/dist/cloudflare/cloudflare-host.js.map +1 -0
  45. package/dist/cloudflare/cloudflare-notification-store.js +59 -0
  46. package/dist/cloudflare/cloudflare-notification-store.js.map +1 -0
  47. package/dist/cloudflare/cloudflare-run-store.js +81 -0
  48. package/dist/cloudflare/cloudflare-run-store.js.map +1 -0
  49. package/dist/cloudflare/cloudflare-store-utils.js +43 -0
  50. package/dist/cloudflare/cloudflare-store-utils.js.map +1 -0
  51. package/dist/cloudflare/durable-object-storage.d.ts +20 -0
  52. package/dist/cloudflare/durable-object-storage.js +76 -0
  53. package/dist/cloudflare/durable-object-storage.js.map +1 -0
  54. package/dist/cloudflare/index.d.ts +7 -0
  55. package/dist/cloudflare/index.js +6 -0
  56. package/dist/execution/capabilities.d.ts +40 -0
  57. package/dist/execution/host.d.ts +9 -0
  58. package/dist/execution/host.js +62 -0
  59. package/dist/execution/host.js.map +1 -0
  60. package/dist/execution/index.d.ts +6 -0
  61. package/dist/execution/index.js +4 -0
  62. package/dist/execution/memory-notifications.js +54 -0
  63. package/dist/execution/memory-notifications.js.map +1 -0
  64. package/dist/execution/memory-state.js +34 -0
  65. package/dist/execution/memory-state.js.map +1 -0
  66. package/dist/execution/memory-store.js +203 -0
  67. package/dist/execution/memory-store.js.map +1 -0
  68. package/dist/execution/memory.d.ts +7 -0
  69. package/dist/execution/memory.js +28 -0
  70. package/dist/execution/memory.js.map +1 -0
  71. package/dist/execution/types.d.ts +150 -0
  72. package/dist/index.d.ts +13 -6
  73. package/dist/index.js +6 -1
  74. package/dist/llm-tool-execution.d.ts +35 -0
  75. package/dist/llm-tool-execution.js +126 -0
  76. package/dist/llm-tool-execution.js.map +1 -0
  77. package/dist/llm.d.ts +11 -15
  78. package/dist/llm.js +5 -3
  79. package/dist/llm.js.map +1 -1
  80. package/dist/plugins.d.ts +42 -0
  81. package/dist/plugins.js +43 -0
  82. package/dist/plugins.js.map +1 -0
  83. package/dist/session/delegate-input.d.ts +9 -0
  84. package/dist/session/delegate-input.js +16 -0
  85. package/dist/session/delegate-input.js.map +1 -0
  86. package/dist/session/events.d.ts +43 -22
  87. package/dist/session/events.js +41 -0
  88. package/dist/session/events.js.map +1 -0
  89. package/dist/session/input-meta-types.d.ts +10 -0
  90. package/dist/session/input-meta.d.ts +13 -0
  91. package/dist/session/input-meta.js +45 -0
  92. package/dist/session/input-meta.js.map +1 -0
  93. package/dist/session/input.d.ts +4 -0
  94. package/dist/session/mapping.js +4 -2
  95. package/dist/session/mapping.js.map +1 -1
  96. package/dist/session/runtime-input-emit.js +41 -0
  97. package/dist/session/runtime-input-emit.js.map +1 -0
  98. package/dist/session/runtime-input.js +10 -24
  99. package/dist/session/runtime-input.js.map +1 -1
  100. package/dist/session/session-errors.js +1 -6
  101. package/dist/session/session-errors.js.map +1 -1
  102. package/dist/session/session-events.js +73 -0
  103. package/dist/session/session-events.js.map +1 -0
  104. package/dist/session/session-execution.js +88 -0
  105. package/dist/session/session-execution.js.map +1 -0
  106. package/dist/session/session-notification.js +59 -0
  107. package/dist/session/session-notification.js.map +1 -0
  108. package/dist/session/session-runtime-drain.js +3 -9
  109. package/dist/session/session-runtime-drain.js.map +1 -1
  110. package/dist/session/session-turn-processor.js +125 -0
  111. package/dist/session/session-turn-processor.js.map +1 -0
  112. package/dist/session/session.js +81 -102
  113. package/dist/session/session.js.map +1 -1
  114. package/dist/session/snapshot.js.map +1 -1
  115. package/package.json +16 -1
  116. package/dist/agent-validation.js +0 -35
  117. package/dist/agent-validation.js.map +0 -1
  118. package/dist/child-session-cleanups.js +0 -61
  119. package/dist/child-session-cleanups.js.map +0 -1
  120. package/dist/hooks.d.ts +0 -32
  121. package/dist/subagent-job-cancel.js +0 -28
  122. package/dist/subagent-job-cancel.js.map +0 -1
  123. package/dist/subagent-job-output.js +0 -63
  124. package/dist/subagent-job-output.js.map +0 -1
  125. package/dist/subagent-jobs.js +0 -151
  126. package/dist/subagent-jobs.js.map +0 -1
  127. package/dist/subagent-prompt-schema.js +0 -114
  128. package/dist/subagent-prompt-schema.js.map +0 -1
  129. package/dist/subagent-run.js +0 -111
  130. package/dist/subagent-run.js.map +0 -1
  131. package/dist/subagents.js +0 -92
  132. package/dist/subagents.js.map +0 -1
  133. /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` and `tools`, or
54
+ 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
@@ -80,74 +116,180 @@ The public transcript protocol is `AgentEvent`: live runs emit runtime-defined
80
116
  events through `run.events()`. Provider/model message history is internal
81
117
  continuation state, not a public history API.
82
118
 
83
- ## Subagents
119
+ ## Delegation
84
120
 
85
- Compose specialist agents by constructing them first and passing them as an
86
- array. Top-level agents may omit metadata, but agents used as subagents need a
87
- stable `name` and `description` so the runtime can expose clear model-facing
88
- delegate tools.
121
+ Delegation is app-owned. Build ordinary tools that call another `Agent`,
122
+ `session.send(...)`, `session.notify(...)`, or host-owned background work, then
123
+ return the compact result shape your product wants the model to see.
89
124
 
90
125
  ```ts
91
- const researcher = new Agent({
92
- name: "researcher",
93
- description: "Researches facts and returns concise evidence.",
126
+ const reader = new Agent({
127
+ instructions: "Read knowledge-base files and cite paths.",
94
128
  model,
95
- instructions: "Research facts and return concise evidence.",
129
+ namespace: "reader",
96
130
  });
97
131
 
98
132
  const coordinator = new Agent({
133
+ instructions: "Coordinate work and delegate knowledge-base reads.",
99
134
  model,
100
- instructions: "Coordinate work and delegate when useful.",
101
- subagents: [researcher],
135
+ namespace: "coordinator",
136
+ tools: {
137
+ delegate_to_reader: tool({
138
+ description: "Ask the reader agent to inspect the knowledge base.",
139
+ execute: async ({ prompt }) => {
140
+ const run = await reader.session("kb").send(prompt);
141
+ const text: string[] = [];
142
+ for await (const event of run.events()) {
143
+ if (event.type === "assistant-text") {
144
+ text.push(event.text);
145
+ }
146
+ }
147
+ return { result: text.join("\n") };
148
+ },
149
+ inputSchema,
150
+ }),
151
+ },
102
152
  });
103
153
  ```
104
154
 
105
- For each subagent, the parent model receives a generated
106
- `delegate_to_<name>` tool. The tool accepts `prompt`, optional `description`,
107
- optional `sessionKey` suffix, and `run_in_background`. A provided `sessionKey`
108
- is always scoped under the parent session and subagent name; the model cannot
109
- select an arbitrary child session key. Omitting `run_in_background` defaults to
110
- blocking behavior and returns compact child text, not the full child event
111
- stream.
155
+ For background delegation, let your host own task ids, scheduling, output
156
+ storage, and notification resume. The runtime provides generic execution stores,
157
+ notifications, `Agent.resume(...)`, and `run.events()`; it does not generate
158
+ delegation tools or own child-agent lifecycle semantics. See
159
+ the sync and background example packages for app-owned blocking and background
160
+ delegation patterns.
161
+
162
+ ## Plugins
163
+
164
+ Pass `plugins: [...]` on `Agent` to observe or intercept runtime events. Each
165
+ plugin exposes one handler:
112
166
 
113
167
  ```ts
114
- delegate_to_researcher({
115
- prompt: "Find the current release notes and summarize the evidence.",
168
+ import type { AgentPlugin } from "@minpeter/pss-runtime";
169
+ import { Agent } from "@minpeter/pss-runtime";
170
+
171
+ const tracePlugin: AgentPlugin = {
172
+ name: "trace",
173
+ on: ({ event }) => {
174
+ if (event.type === "turn-end") {
175
+ console.log("turn finished");
176
+ }
177
+ },
178
+ };
179
+
180
+ const agent = new Agent({
181
+ model,
182
+ plugins: [tracePlugin],
116
183
  });
117
184
  ```
118
185
 
119
- 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`.
186
+ ### Observe vs intercept
187
+
188
+ For most events, `on` is observe-only: return nothing (or `{ action: "continue" }`)
189
+ and the runtime emits the event unchanged.
190
+
191
+ Three input event types support intercept returns:
192
+
193
+ - `user-text`
194
+ - `user-message`
195
+ - `runtime-input`
196
+
197
+ Return one of:
198
+
199
+ - `{ action: "continue" }` — emit the current event (default when omitted)
200
+ - `{ action: "transform", event }` — emit a replacement input event
201
+ - `{ action: "handled" }` — skip emit; for `session.send`, close the run without
202
+ starting a turn
203
+
204
+ Plugins run in registration order. Each `transform` updates the event seen by
205
+ later plugins, so transforms chain sequentially.
206
+
207
+ ### Input `meta.source`
208
+
209
+ The runtime attaches `meta` on input events at API boundaries. Plugins can route
210
+ on `event.meta?.source`:
211
+
212
+ | `source` | Boundary |
213
+ |----------|----------|
214
+ | `send` | `session.send()` / `agent.send()` |
215
+ | `steer` | `session.steer()` and drained steering queue |
216
+ | `notify` | `session.notify()` runtime input |
217
+ | `delegate` | parent `delegate_to_*` child `session.send()` |
218
+
219
+ `meta` appears on `run.events()` for input events but is stripped before session
220
+ history persistence and model mapping. It never reaches the LLM prompt.
221
+
222
+ ### Delegate prompt wrapping
223
+
224
+ Child agents receive delegated prompts with `meta.source === "delegate"`. Wrap or
225
+ rewrite them with a plugin instead of agent-level prompt shims:
123
226
 
124
227
  ```ts
125
- delegate_to_researcher({
126
- prompt: "Compare the API designs.",
127
- run_in_background: true,
128
- });
228
+ import type { AgentPlugin, UserText } from "@minpeter/pss-runtime";
229
+ import { Agent } from "@minpeter/pss-runtime";
230
+
231
+ const pokeTagsPlugin: AgentPlugin = {
232
+ name: "poke-tags",
233
+ on: ({ event }) => {
234
+ if (event.type !== "user-text" || event.meta?.source !== "delegate") {
235
+ return;
236
+ }
237
+
238
+ const text =
239
+ typeof event.text === "string" ? event.text : event.text.join("\n");
240
+
241
+ return {
242
+ action: "transform",
243
+ event: {
244
+ ...event,
245
+ text: `<poke>\n${text}\n</poke>`,
246
+ } satisfies UserText,
247
+ };
248
+ },
249
+ };
129
250
 
130
- background_output({ task_id: "bg_...", block: true });
131
- background_cancel({ task_id: "bg_..." });
251
+ const executionAgent = new Agent({
252
+ namespace: "execution",
253
+ plugins: [pokeTagsPlugin],
254
+ model,
255
+ });
132
256
  ```
133
257
 
134
- The parent model context stays compact by default: completion reminders include
135
- the task id, subagent name, description, and retrieval instruction. Full child
136
- 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.
258
+ The parent coordinator stays unchanged; only the nested child agent carries the
259
+ plugin.
260
+
261
+ ### Migration
139
262
 
140
- ## Send and Steer
263
+ - **`plugins[].events.on`** deprecated. Use top-level `plugins[].on`. The legacy
264
+ handler still receives every event but intercept returns are ignored (observe-only).
265
+ - **`wrapDelegatePrompt`** — removed. Use a child `plugins[].on` handler that checks
266
+ `meta.source === "delegate"` and returns `transform`, as above.
267
+
268
+ ## Send, Host Resume, and Steer
141
269
 
142
270
  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.
271
+ turn is queued until the active run finishes. Use `session.steer(input)` when
272
+ the input should steer the active run; if no run is active, it starts a normal
273
+ run.
274
+
275
+ Durable hosts resume completed background work by writing a notification record
276
+ and calling `agent.resume(notificationRunId)`. The resume call claims the
277
+ notification idempotently through its durable run id and returns one `AgentRun`,
278
+ or `null` when a duplicate queue/alarm delivery already claimed it.
279
+
280
+ Runtime-originated input is delivered through the host notification inbox and
281
+ internal plugin paths. App code should use `session.send()`, `session.steer()`,
282
+ or `agent.resume(runId)` for host-scheduled durable work.
283
+
284
+ Each accepted call returns one `AgentRun`. Drain that run's `events()` stream to
285
+ observe the turn; each `AgentRun.events()` stream is single-consumer.
145
286
 
146
- Both APIs accept the same input shapes: strings, arrays of strings,
287
+ Input APIs accept the same input shapes: strings, arrays of strings,
147
288
  `{ 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.
289
+ values. Active steering and host resume input emit `runtime-input` events. A
290
+ `runtime-input` is runtime/API-originated input mapped internally to the model's
291
+ user role. It is distinct from human-origin `user-text` and `user-message`
292
+ events.
151
293
 
152
294
  Runtime input windows are tied to synchronized events:
153
295
 
@@ -188,6 +330,10 @@ Stored session state is an opaque, versioned runtime snapshot for continuation.
188
330
  Do not inspect it as a replay log; exact replay should be modeled separately as
189
331
  an `AgentEvent` log if that capability is added later.
190
332
 
333
+ `SessionStore` is snapshot-only. It does not own background task ids, run
334
+ leases, checkpoints, notification inbox state, or scheduling. Those live on the
335
+ optional `host` execution contract.
336
+
191
337
  Custom stores own version generation. `load(key)` returns the opaque `state` with
192
338
  the store-minted `version`; `commit(key, { state }, { expectedVersion })` receives
193
339
  state only and should reject stale versions by returning `{ ok: false, reason:
@@ -196,37 +342,89 @@ new version to the runtime. `delete(key)` removes the persisted session for that
196
342
  key.
197
343
 
198
344
  ```ts
199
- import type { SessionStore } from "@minpeter/pss-runtime";
200
345
  import { MemorySessionStore } from "@minpeter/pss-runtime/session-store/memory";
201
346
 
202
347
  const agent = new Agent({
203
- model,
204
- sessions: {
205
- namespace: "support-agent",
206
- store: new MemorySessionStore(), // default when omitted
348
+ host: {
349
+ sessionStore: new MemorySessionStore(), // default when omitted
207
350
  },
351
+ model,
352
+ namespace: "support-agent",
208
353
  });
209
354
  ```
210
355
 
211
- For durable sessions, use the exported file POC. Set a stable `namespace` when
212
- subagents also use durable stores, so reconstructed agents map the same parent
213
- session and child `sessionKey` suffixes back to the same child transcripts:
356
+ For durable sessions, use the exported file POC. Set a stable `namespace` so
357
+ reconstructed agents map the same app-owned session keys back to the same
358
+ transcripts:
214
359
 
215
360
  ```ts
216
361
  import { FileSessionStore } from "@minpeter/pss-runtime/session-store/file";
217
362
 
218
363
  const agent = new Agent({
219
- model,
220
- sessions: {
221
- namespace: "support-agent",
222
- store: new FileSessionStore(".pss/sessions"),
364
+ host: {
365
+ sessionStore: new FileSessionStore(".pss/sessions"),
223
366
  },
367
+ model,
368
+ namespace: "support-agent",
224
369
  });
225
370
  ```
226
371
 
227
- ## Future adapter boundary: Cloudflare multi-user DX
372
+ Hosts that need durable runs pass `host:` into `Agent`. The execution subpath
373
+ keeps the durable surface split by responsibility, so hosts can implement only
374
+ the capabilities they need: `SessionHost`, `RunHost`, `CheckpointHost`,
375
+ `EventHost`, `NotificationHost`, `BackgroundSchedulerHost`, and
376
+ `ExecutionTransactionHost`. `ExecutionHost` remains the aggregate contract for
377
+ in-process or full-store hosts, while `DurableBackgroundHost` and
378
+ `DurableNotificationResumeHost` describe the smaller durable surfaces required
379
+ for background scheduling and notification resume.
380
+
381
+ ```ts
382
+ import { Agent } from "@minpeter/pss-runtime";
383
+ import {
384
+ createInMemoryExecutionHost,
385
+ type DurableBackgroundHost,
386
+ type ExecutionHost,
387
+ } from "@minpeter/pss-runtime/execution";
388
+
389
+ const host = createInMemoryExecutionHost();
390
+
391
+ const agent = new Agent({
392
+ host,
393
+ model,
394
+ namespace: "support-agent",
395
+ });
396
+
397
+ const durableHost: DurableBackgroundHost = {
398
+ capabilities: {},
399
+ backgroundScheduler,
400
+ checkpointStore,
401
+ eventStore,
402
+ notificationInbox,
403
+ runStore,
404
+ sessionStore,
405
+ transaction,
406
+ };
407
+ ```
408
+
409
+ ## Supported Deployment Shapes
410
+
411
+ The runtime supports both long-running Node.js processes and edge hosts that
412
+ reconstruct runtime objects between turns. The same public DX stays centered on
413
+ `new Agent({ model, tools, host })`; host-specific durability and scheduling live
414
+ behind the `host` boundary.
415
+
416
+ Long-running Node.js can keep an `Agent` and `SessionHandle` alive across turns.
417
+ `FileSessionStore` persists session snapshots only; app-owned background work
418
+ needs its own durable task/output storage if it must survive process restarts.
419
+
420
+ Cloudflare Durable Objects and similar edge hosts should reconstruct `Agent`
421
+ objects per turn and persist opaque session state through a durable
422
+ `sessionStore`.
423
+ Use `@minpeter/pss-runtime/cloudflare` for the packaged Cloudflare Durable
424
+ Object adapter. See the sync example package for blocking app-owned delegation
425
+ and the background example package for durable background delegation in a local
426
+ interactive CLI.
228
427
 
229
- Cloudflare Durable Objects are a future adapter target, not a runtime dependency.
230
428
  The same core API supports room/user/session routing through stable session keys.
231
429
 
232
430
  Recommended key patterns:
@@ -235,6 +433,37 @@ Recommended key patterns:
235
433
  - Per-user memory inside room: `room:<roomId>:user:<userId>`
236
434
  - Ticketed workspace flows: `tenant:<tenantId>:ticket:<ticketId>`
237
435
 
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.
436
+ In a Durable Object, map the execution store contract to `ctx.storage` so DO
437
+ storage is durable across hibernation/restores, while in-memory state remains
438
+ request-local. Do not store canonical agent session or run state in memory
439
+ attachments.
440
+
441
+ Durable background workflows require host-owned task ids, attempts, leases,
442
+ checkpoints, cancellation, scheduling, session snapshots, and completion
443
+ notifications. The Cloudflare adapter persists scheduled runs and session
444
+ prompts, sets alarms, and resumes work through `Agent.resume(...)`.
445
+
446
+ ## Checkpoints and Cancellation
447
+
448
+ Resume is safe only at committed boundaries. Durable hosts can checkpoint before
449
+ and after model steps, around notifications, before child run creation, when a
450
+ child link is committed, and when a run suspends. If a process is killed inside a
451
+ provider call or unsafe tool execution, resume rolls back to the last committed
452
+ checkpoint and may re-enter the operation.
453
+
454
+ When `Agent` receives an `ExecutionHost`, high-level model turns create a
455
+ `user-turn` run record and thread tool execution context into managed model
456
+ calls. Tools are checkpointed before and after execution and receive stable
457
+ `attempt`, `idempotencyKey`, `retryPolicy`, `signal`, and public `toolCallId`
458
+ values. The `@minpeter/pss-runtime/execution` entrypoint also exposes the same
459
+ low-level tool execution checkpoint types for custom resume runners built
460
+ directly on `createLlm`.
461
+
462
+ These checkpoints are rollback boundaries, not a complete host adapter by
463
+ themselves. Edge hosts still need durable scheduling, leases, resume workers,
464
+ and notification resume handling; externally visible side-effect tools still need
465
+ idempotent execution or a manual recovery flow.
466
+
467
+ Cancellation is persisted before aborting active work. `delete()` and `dispose()`
468
+ stop the current session's in-process work; durable hosts remain responsible for
469
+ any app-owned background run cancellation, cleanup, and notification policy.
@@ -0,0 +1,10 @@
1
+ import { sessionHost } 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
+ return sessionHost(host).sessionStore ?? new MemorySessionStore();
6
+ }
7
+ //#endregion
8
+ export { sessionStoreForHost };
9
+
10
+ //# 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 { sessionHost } 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 return sessionHost(host).sessionStore ?? new MemorySessionStore();\n}\n"],"mappings":";;;AAKA,SAAgB,oBAAoB,MAA+B;CACjE,OAAO,YAAY,IAAI,EAAE,gBAAgB,IAAI,mBAAmB;AAClE"}
@@ -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,74 @@ 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
+ return true;
127
+ }
99
128
  //#endregion
100
129
  export { runAgentLoop };
101
130
 
@@ -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 return true;\n}\n"],"mappings":"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"}
@@ -8,10 +8,13 @@ function agentNamespace(namespace) {
8
8
  function namespacePart(value) {
9
9
  return encodeURIComponent(value);
10
10
  }
11
- function parentSessionNamespace({ generation, sessionKey, sessionNamespace }) {
12
- return `${sessionNamespace}:session:${namespacePart(sessionKey)}:generation:${generation}`;
11
+ function ownsAgentNamespace(ownerNamespace, sessionNamespace) {
12
+ return ownerNamespace === sessionNamespace || ownerNamespace?.startsWith(`${sessionNamespace}:session:`) === true;
13
+ }
14
+ function stableAgentNamespace({ namespace }) {
15
+ return namespace ? agentNamespace(namespace) : randomAgentNamespace();
13
16
  }
14
17
  //#endregion
15
- export { agentNamespace, parentSessionNamespace, randomAgentNamespace };
18
+ export { ownsAgentNamespace, stableAgentNamespace };
16
19
 
17
20
  //# sourceMappingURL=agent-namespace.js.map