@minpeter/pss-runtime 0.1.0-next.0 → 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 (161) hide show
  1. package/README.md +223 -191
  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 +59 -35
  9. package/dist/agent-loop.js.map +1 -1
  10. package/dist/agent-namespace.js +24 -0
  11. package/dist/agent-namespace.js.map +1 -0
  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 +35 -0
  19. package/dist/agent-validation.js.map +1 -0
  20. package/dist/agent.d.ts +8 -33
  21. package/dist/agent.js +131 -55
  22. package/dist/agent.js.map +1 -1
  23. package/dist/child-session-cleanups.js +61 -0
  24. package/dist/child-session-cleanups.js.map +1 -0
  25. package/dist/execution/host.js +14 -0
  26. package/dist/execution/host.js.map +1 -0
  27. package/dist/execution/index.d.ts +4 -0
  28. package/dist/execution/index.js +3 -0
  29. package/dist/execution/memory-notifications.js +54 -0
  30. package/dist/execution/memory-notifications.js.map +1 -0
  31. package/dist/execution/memory-state.js +34 -0
  32. package/dist/execution/memory-state.js.map +1 -0
  33. package/dist/execution/memory-store.js +203 -0
  34. package/dist/execution/memory-store.js.map +1 -0
  35. package/dist/execution/memory.d.ts +7 -0
  36. package/dist/execution/memory.js +28 -0
  37. package/dist/execution/memory.js.map +1 -0
  38. package/dist/execution/run.js +55 -0
  39. package/dist/execution/run.js.map +1 -0
  40. package/dist/execution/types.d.ts +155 -0
  41. package/dist/index.d.ts +9 -10
  42. package/dist/index.js +1 -6
  43. package/dist/llm-tool-execution.d.ts +35 -0
  44. package/dist/llm-tool-execution.js +126 -0
  45. package/dist/llm-tool-execution.js.map +1 -0
  46. package/dist/llm.d.ts +11 -15
  47. package/dist/llm.js +5 -9
  48. package/dist/llm.js.map +1 -1
  49. package/dist/plugins.d.ts +20 -0
  50. package/dist/plugins.js +14 -0
  51. package/dist/plugins.js.map +1 -0
  52. package/dist/session/events.d.ts +26 -20
  53. package/dist/session/input-normalization.js +66 -0
  54. package/dist/session/input-normalization.js.map +1 -0
  55. package/dist/session/input.d.ts +0 -4
  56. package/dist/session/mapping.js +1 -2
  57. package/dist/session/mapping.js.map +1 -1
  58. package/dist/session/run.js +1 -0
  59. package/dist/session/run.js.map +1 -1
  60. package/dist/session/runtime-input.js +20 -58
  61. package/dist/session/runtime-input.js.map +1 -1
  62. package/dist/session/session-errors.js +18 -0
  63. package/dist/session/session-errors.js.map +1 -0
  64. package/dist/session/session-events.js +59 -0
  65. package/dist/session/session-events.js.map +1 -0
  66. package/dist/session/session-execution.js +88 -0
  67. package/dist/session/session-execution.js.map +1 -0
  68. package/dist/session/session-kill.js +23 -0
  69. package/dist/session/session-kill.js.map +1 -0
  70. package/dist/session/session-notification.js +58 -0
  71. package/dist/session/session-notification.js.map +1 -0
  72. package/dist/session/session-runtime-drain.js +22 -0
  73. package/dist/session/session-runtime-drain.js.map +1 -0
  74. package/dist/session/session-state.js +102 -0
  75. package/dist/session/session-state.js.map +1 -0
  76. package/dist/session/session-turn-error.js +35 -0
  77. package/dist/session/session-turn-error.js.map +1 -0
  78. package/dist/session/session-turn-processor.js +135 -0
  79. package/dist/session/session-turn-processor.js.map +1 -0
  80. package/dist/session/session.js +125 -335
  81. package/dist/session/session.js.map +1 -1
  82. package/dist/session/snapshot.js +5 -31
  83. package/dist/session/snapshot.js.map +1 -1
  84. package/dist/session/store/file.d.ts +1 -0
  85. package/dist/session/store/file.js +14 -0
  86. package/dist/session/store/file.js.map +1 -1
  87. package/dist/session/store/memory.d.ts +1 -0
  88. package/dist/session/store/memory.js +5 -0
  89. package/dist/session/store/memory.js.map +1 -1
  90. package/dist/session/store/types.d.ts +1 -0
  91. package/dist/subagent-background-child-run-state.js +51 -0
  92. package/dist/subagent-background-child-run-state.js.map +1 -0
  93. package/dist/subagent-background-child-run.js +103 -0
  94. package/dist/subagent-background-child-run.js.map +1 -0
  95. package/dist/subagent-background-in-process.js +98 -0
  96. package/dist/subagent-background-in-process.js.map +1 -0
  97. package/dist/subagent-background-notification-inbox.js +106 -0
  98. package/dist/subagent-background-notification-inbox.js.map +1 -0
  99. package/dist/subagent-background-notify.js +136 -0
  100. package/dist/subagent-background-notify.js.map +1 -0
  101. package/dist/subagent-background-resume-group.js +99 -0
  102. package/dist/subagent-background-resume-group.js.map +1 -0
  103. package/dist/subagent-background-runner.js +115 -0
  104. package/dist/subagent-background-runner.js.map +1 -0
  105. package/dist/subagent-background-schedule.js +43 -0
  106. package/dist/subagent-background-schedule.js.map +1 -0
  107. package/dist/subagent-child-run.js +68 -0
  108. package/dist/subagent-child-run.js.map +1 -0
  109. package/dist/subagent-job-cancel.js +84 -0
  110. package/dist/subagent-job-cancel.js.map +1 -0
  111. package/dist/subagent-job-observer.js +19 -0
  112. package/dist/subagent-job-observer.js.map +1 -0
  113. package/dist/subagent-job-output.js +87 -0
  114. package/dist/subagent-job-output.js.map +1 -0
  115. package/dist/subagent-job-state.js +66 -0
  116. package/dist/subagent-job-state.js.map +1 -0
  117. package/dist/subagent-jobs.js +96 -0
  118. package/dist/subagent-jobs.js.map +1 -0
  119. package/dist/subagent-prompt-schema.js +114 -0
  120. package/dist/subagent-prompt-schema.js.map +1 -0
  121. package/dist/subagent-run.js +111 -0
  122. package/dist/subagent-run.js.map +1 -0
  123. package/dist/subagents.js +125 -0
  124. package/dist/subagents.js.map +1 -0
  125. package/package.json +11 -6
  126. package/dist/plugins/compaction.d.ts +0 -15
  127. package/dist/plugins/compaction.js +0 -98
  128. package/dist/plugins/compaction.js.map +0 -1
  129. package/dist/plugins/index.d.ts +0 -5
  130. package/dist/plugins/index.js +0 -5
  131. package/dist/plugins/memory.d.ts +0 -11
  132. package/dist/plugins/memory.js +0 -146
  133. package/dist/plugins/memory.js.map +0 -1
  134. package/dist/plugins/runner.d.ts +0 -1
  135. package/dist/plugins/runner.js +0 -83
  136. package/dist/plugins/runner.js.map +0 -1
  137. package/dist/plugins/scope.js +0 -13
  138. package/dist/plugins/scope.js.map +0 -1
  139. package/dist/plugins/sessions.d.ts +0 -12
  140. package/dist/plugins/sessions.js +0 -34
  141. package/dist/plugins/sessions.js.map +0 -1
  142. package/dist/plugins/tool-hook-handlers.js +0 -77
  143. package/dist/plugins/tool-hook-handlers.js.map +0 -1
  144. package/dist/plugins/tool-hook-results.js +0 -64
  145. package/dist/plugins/tool-hook-results.js.map +0 -1
  146. package/dist/plugins/tool-hooks.js +0 -111
  147. package/dist/plugins/tool-hooks.js.map +0 -1
  148. package/dist/plugins/types.d.ts +0 -105
  149. package/dist/plugins/types.js +0 -20
  150. package/dist/plugins/types.js.map +0 -1
  151. package/dist/session/lifecycle.d.ts +0 -12
  152. package/dist/session/lifecycle.js +0 -126
  153. package/dist/session/lifecycle.js.map +0 -1
  154. package/dist/session/overlay-anchor.js +0 -151
  155. package/dist/session/overlay-anchor.js.map +0 -1
  156. package/dist/session/overlay.js +0 -141
  157. package/dist/session/overlay.js.map +0 -1
  158. package/dist/session/snapshot.d.ts +0 -1
  159. /package/dist/{agent-loop.d.ts → session/history.d.ts} +0 -0
  160. /package/dist/session/{runtime-input.d.ts → session-execution.d.ts} +0 -0
  161. /package/dist/{plugins/scope.d.ts → session/session-state.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
+ });
15
28
 
16
- const agent = await Agent.create({
29
+ const provider = createOpenAICompatible({
30
+ name: "custom",
31
+ apiKey: env.AI_API_KEY,
32
+ baseURL: env.AI_BASE_URL,
33
+ });
34
+
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");
@@ -28,22 +47,23 @@ for await (const event of run.events()) {
28
47
  boundaries until the events consumer asks for the next event, so callers must
29
48
  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
- created. `AgentRun.events()` is single-consumer by design: keep rendering,
32
- logging, tracing, and continuation policy in the same app-owned loop when those
33
- concerns must share synchronized boundary control.
50
+ created.
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:
34
56
 
35
57
  ```ts
36
- const run = await agent.send("Implement the plan.");
37
- const session = agent.session("default");
58
+ import { Agent, type RuntimeLlm } from "@minpeter/pss-runtime";
38
59
 
39
- for await (const event of run.events()) {
40
- renderEvent(event);
41
- traceEvent(event);
60
+ const runtimeModel: RuntimeLlm = async ({ history }) => [
61
+ { role: "assistant", content: `Seen ${history.length} messages.` },
62
+ ];
42
63
 
43
- if (event.type === "step-end" && shouldContinueWork()) {
44
- await session.steer("Continue. The task is not complete yet.");
45
- }
46
- }
64
+ const agent = new Agent({
65
+ model: runtimeModel,
66
+ });
47
67
  ```
48
68
 
49
69
  Per-key conversations use `session(key)`:
@@ -96,17 +116,95 @@ The public transcript protocol is `AgentEvent`: live runs emit runtime-defined
96
116
  events through `run.events()`. Provider/model message history is internal
97
117
  continuation state, not a public history API.
98
118
 
99
- ## Send and Steer
119
+ ## Subagents
120
+
121
+ Compose specialist agents by constructing them first and passing them as an
122
+ array. Top-level agents may omit metadata, but agents used as subagents need a
123
+ stable `name` and `description` so the runtime can expose clear model-facing
124
+ delegate tools.
125
+
126
+ ```ts
127
+ const researcher = new Agent({
128
+ name: "researcher",
129
+ description: "Researches facts and returns concise evidence.",
130
+ model,
131
+ instructions: "Research facts and return concise evidence.",
132
+ });
133
+
134
+ const coordinator = new Agent({
135
+ model,
136
+ instructions: "Coordinate work and delegate when useful.",
137
+ subagents: [researcher],
138
+ });
139
+ ```
140
+
141
+ For each subagent, the parent model receives a generated
142
+ `delegate_to_<name>` tool. The tool accepts `prompt`, optional `description`,
143
+ optional `sessionKey` suffix, and `run_in_background`. A provided `sessionKey`
144
+ is always scoped under the parent session and subagent name; the model cannot
145
+ select an arbitrary child session key. Omitting `run_in_background` defaults to
146
+ blocking behavior and returns compact child text, not the full child event
147
+ stream.
148
+
149
+ ```ts
150
+ delegate_to_researcher({
151
+ prompt: "Find the current release notes and summarize the evidence.",
152
+ });
153
+ ```
154
+
155
+ When the model sets `run_in_background: true`, the parent run can finish while
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`.
161
+
162
+ ```ts
163
+ delegate_to_researcher({
164
+ prompt: "Compare the API designs.",
165
+ run_in_background: true,
166
+ });
167
+
168
+ // After the completion reminder arrives:
169
+ background_output({ task_id: "bg_...", block: true });
170
+ background_cancel({ task_id: "bg_..." });
171
+ ```
172
+
173
+ The parent model context stays compact by default: completion reminders include
174
+ the task id, subagent name, description, and retrieval instruction. Full child
175
+ traces are not injected into the parent transcript by default. Background jobs
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.
182
+
183
+ ## Send, Host Resume, and Steer
100
184
 
101
185
  Use `session.send(input)` for a new user turn. If a run is already active, the
102
- turn is queued until the active run finishes. Use `session.steer(input)` when the
103
- 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.
104
198
 
105
- 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,
106
203
  `{ type: "user-text", text }`, and multipart `{ type: "user-message", content }`
107
- values. Active steering emits `runtime-input` events. A `runtime-input` is
108
- runtime/API-originated input mapped internally to the model's user role. It is
109
- 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.
110
208
 
111
209
  Runtime input windows are tied to synchronized events:
112
210
 
@@ -138,204 +236,105 @@ for await (const event of run.events()) {
138
236
  pending steering path or, when idle, when a new run is scheduled. It does not wait
139
237
  for a later model snapshot.
140
238
 
141
- ## Overlay
142
-
143
- Use `session.overlay(input)` for turn-scoped model context that should affect
144
- the next model snapshot without becoming canonical session history. It accepts
145
- the same input shapes as `send()` and `steer()` and has no options in v0.
146
-
147
- Overlay context accepted before the first model inference in a turn is composed
148
- above the current user prompt. This keeps the current prompt near the bottom of
149
- the model snapshot while still giving the model fresh runtime context first.
150
- Overlay context accepted after a model inference has already happened is
151
- append-only: it is added after the messages that already existed for that turn
152
- and never moves earlier content around.
153
-
154
- ```ts
155
- const session = agent.session("room:123:user:456");
156
-
157
- await session.overlay("Current wall-clock time: 2026-06-05T19:00:00Z");
158
- const run = await session.send("What should I do next?");
159
-
160
- for await (const event of run.events()) {
161
- if (event.type === "step-end" && needsOneMorePass()) {
162
- await session.overlay("Additional constraint: answer in two sentences.");
163
- }
164
- }
165
- ```
166
-
167
- Active `step-end` overlays intentionally continue the current turn for one more
168
- model snapshot, like `step-end` steering, but they are not persisted as
169
- `runtime-input`. Idle overlays do not start a turn; they queue for the next
170
- `send()` on that session.
171
-
172
- Runs emit `overlay-accepted` when overlay input is accepted and
173
- `overlay-expired` when the turn-scoped frame is discarded at turn end, abort,
174
- error, or kill. Overlay text is not written to stored history, not encoded in
175
- session snapshots, and does not survive session reload.
176
-
177
- ## Plugins, Session Storage, Memory, And Compaction
239
+ ## Session storage and portability
178
240
 
179
241
  The runtime owns full session state encoding and history compaction semantics.
180
- Persistence, memory, and compaction are configured through in-process plugins:
181
-
182
- ```ts
183
- import { Agent } from "@minpeter/pss-runtime";
184
- import { compaction, memory, sessions } from "@minpeter/pss-runtime/plugins";
185
-
186
- const agent = await Agent.create({
187
- model,
188
- plugins: [sessions.file(".pss/sessions"), memory(), compaction()],
189
- });
190
- ```
242
+ Adapters own persistence only through `SessionStore`:
191
243
 
192
- If no persistence plugin is provided, sessions are memory-backed by default.
244
+ Stored session state is an opaque, versioned runtime snapshot for continuation.
245
+ Do not inspect it as a replay log; exact replay should be modeled separately as
246
+ an `AgentEvent` log if that capability is added later.
193
247
 
194
- Reusable middleware belongs in plugins. Plugins can observe turn and step
195
- lifecycle events and call the scoped `steer` function to insert runtime input at
196
- the active boundary or the scoped `overlay` function to add turn-scoped
197
- non-persistent context before the turn ends. Plugin `overlay` is available from
198
- `turn.before`, `step.before`, and `step.after`; `turn.after` runs after the
199
- turn-scoped overlay frame is closed and rejects overlay calls. App-level control
200
- should stay with `run.events()` plus `session.steer()` or `session.overlay()`;
201
- plugin lifecycle is for reusable policy.
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.
202
251
 
203
- Plugin event names are dotted middleware names: `turn.before`, `step.before`,
204
- `step.after`, `turn.after`, `tool.call`, and `tool.result`. These are separate
205
- from public `run.events()` transcript names such as `turn-start`, `step-start`,
206
- `assistant-text`, `tool-call`, `tool-result`, `step-end`, and `turn-end`.
252
+ Custom stores own version generation. `load(key)` returns the opaque `state` with
253
+ the store-minted `version`; `commit(key, { state }, { expectedVersion })` receives
254
+ state only and should reject stale versions by returning `{ ok: false, reason:
255
+ "conflict" }`. On success, the store persists `{ state, version }` and returns the
256
+ new version to the runtime. `delete(key)` removes the persisted session for that
257
+ key.
207
258
 
208
259
  ```ts
209
- import { Agent, definePlugin } from "@minpeter/pss-runtime";
210
-
211
- const continuePlugin = definePlugin({
212
- name: "continue-policy",
213
- setup(host) {
214
- host.on("step.after", async ({ result, history, overlay, steer, stepIndex }) => {
215
- if (result === "completed" && stepIndex === 0 && shouldContinueWork(history)) {
216
- await overlay("Continue, but do not persist this policy text.");
217
- await steer("Continue. The task is not complete yet.");
218
- }
219
- });
220
- },
221
- });
260
+ import { MemorySessionStore } from "@minpeter/pss-runtime/session-store/memory";
222
261
 
223
- const agent = await Agent.create({
262
+ const agent = new Agent({
263
+ host: {
264
+ sessionStore: new MemorySessionStore(), // default when omitted
265
+ },
224
266
  model,
225
- plugins: [continuePlugin],
267
+ namespace: "support-agent",
226
268
  });
227
269
  ```
228
270
 
229
- `turn.after` is useful for audit, metrics, or scheduling a separate follow-up
230
- run after the current turn has committed.
271
+ For durable sessions, use the exported file POC. Set a stable `namespace` when
272
+ subagents also use durable stores, so reconstructed agents map the same parent
273
+ session and child `sessionKey` suffixes back to the same child transcripts:
231
274
 
232
275
  ```ts
233
- const auditPlugin = definePlugin({
234
- name: "turn-audit",
235
- setup(host) {
236
- host.on("turn.after", ({ result, sessionKey }) => {
237
- recordTurnResult(sessionKey, result);
238
- });
239
- },
240
- });
241
- ```
242
-
243
- Tool policy hooks apply to runtime-owned tools in the `Agent.create({ model,
244
- tools })` path, including tools registered by plugins. Custom `llm` callers own
245
- their tool execution and do not receive synthetic tool hook events.
276
+ import { FileSessionStore } from "@minpeter/pss-runtime/session-store/file";
246
277
 
247
- `tool.call` runs after AI SDK input parsing and before the original tool
248
- `execute`. Handlers run in plugin registration order. `allow` continues to the
249
- next handler, `modify` replaces the input for later handlers and execution,
250
- `reject-and-continue` skips the original tool and returns a rejection payload to
251
- the model, `synthesize` skips the original tool and returns a synthetic output,
252
- and `error` fails the active run.
253
-
254
- ```ts
255
- import { definePlugin } from "@minpeter/pss-runtime";
256
-
257
- const toolPolicyPlugin = definePlugin({
258
- name: "tool-policy",
259
- setup(host) {
260
- host.on("tool.call", ({ input, tool }) => {
261
- if (tool === "delete_file") {
262
- return {
263
- action: "reject-and-continue",
264
- message: "delete_file is disabled in this workspace.",
265
- };
266
- }
267
-
268
- if (tool === "search" && shouldNarrowSearch(input)) {
269
- return { action: "modify", input: narrowSearchInput(input) };
270
- }
271
-
272
- return { action: "allow" };
273
- });
274
- },
275
- });
276
- ```
277
-
278
- `tool.result` runs after allowed/modified execution, rejected calls, synthesized
279
- calls, and original tool errors. It can observe or replace the model-facing
280
- result with `{ status: "done", output }`, `{ status: "error", error, output }`,
281
- or `{ status: "cancelled", error, output }`. Replacements flow into later
282
- `tool.result` handlers.
283
-
284
- ```ts
285
- const resultPolicyPlugin = definePlugin({
286
- name: "tool-result-policy",
287
- setup(host) {
288
- host.on("tool.result", ({ output, status, tool }) => {
289
- if (tool === "read_secret" && status === "done") {
290
- return {
291
- status: "done",
292
- output: redactSecretOutput(output),
293
- };
294
- }
295
- });
278
+ const agent = new Agent({
279
+ host: {
280
+ sessionStore: new FileSessionStore(".pss/sessions"),
296
281
  },
282
+ model,
283
+ namespace: "support-agent",
297
284
  });
298
285
  ```
299
286
 
300
- Custom stores still own version generation through `SessionStore`. Use
301
- `sessions.custom(store)` when the runtime should persist through a caller-owned
302
- store:
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.
303
291
 
304
292
  ```ts
305
- import type { SessionStore } from "@minpeter/pss-runtime";
306
- import { sessions } from "@minpeter/pss-runtime/plugins";
293
+ import { Agent } from "@minpeter/pss-runtime";
294
+ import {
295
+ createInMemoryExecutionHost,
296
+ type ExecutionHost,
297
+ } from "@minpeter/pss-runtime/execution";
307
298
 
308
- declare const store: SessionStore;
299
+ const host = createInMemoryExecutionHost();
309
300
 
310
- const agent = await Agent.create({
301
+ const agent = new Agent({
302
+ host,
311
303
  model,
312
- plugins: [sessions.custom(store)],
304
+ namespace: "support-agent",
313
305
  });
314
- ```
315
306
 
316
- Stored session state is opaque, versioned runtime continuation state:
307
+ const durableHost: ExecutionHost = {
308
+ capabilities: { backgroundSubagents: "durable" },
309
+ scheduler,
310
+ store,
311
+ };
312
+ ```
317
313
 
318
- Do not inspect it as a replay log; exact replay should be modeled separately as
319
- an `AgentEvent` log if that capability is added later.
314
+ ## Supported Deployment Shapes
320
315
 
321
- `load(key)` returns the opaque `state` with the store-minted `version`;
322
- `commit(key, { state }, { expectedVersion })` receives state only and should
323
- reject stale versions by returning `{ ok: false, reason: "conflict" }`. On
324
- success, the store persists `{ state, version }` and returns the new version to
325
- the runtime.
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.
326
320
 
327
- `memory()` adds session-scoped tools named `set_context`, `load_context`, and
328
- `search_context`. Search is deterministic lexical matching by default; no
329
- embedding provider is required. Memory is injected into model-facing context
330
- without mutating top-level instructions.
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.
331
326
 
332
- `compaction()` stores non-destructive overlays with `startIndex` and `endIndex`.
333
- The full canonical history remains in the session snapshot; summaries are
334
- applied only to model-facing context.
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.
335
333
 
336
- ## Future adapter boundary: Cloudflare multi-user DX
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.
337
337
 
338
- Cloudflare Durable Objects are a future adapter target, not a runtime dependency.
339
338
  The same core API supports room/user/session routing through stable session keys.
340
339
 
341
340
  Recommended key patterns:
@@ -344,6 +343,39 @@ Recommended key patterns:
344
343
  - Per-user memory inside room: `room:<roomId>:user:<userId>`
345
344
  - Ticketed workspace flows: `tenant:<tenantId>:ticket:<ticketId>`
346
345
 
347
- In a Durable Object, map the `SessionStore` contract to `ctx.storage` so DO storage is
348
- durable across hibernation/restores, while in-memory state remains request-local.
349
- 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"}