@nebutra/agent-runtime 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/.turbo/turbo-build.log +115 -0
  2. package/.turbo/turbo-test.log +44 -0
  3. package/.turbo/turbo-typecheck.log +4 -0
  4. package/CHANGELOG.md +253 -0
  5. package/LICENSE +676 -0
  6. package/README.md +50 -0
  7. package/dist/adapters/dispatcher-sse.d.ts +68 -0
  8. package/dist/adapters/dispatcher-sse.js +11 -0
  9. package/dist/adapters/dispatcher-sse.js.map +1 -0
  10. package/dist/adapters/index.d.ts +12 -0
  11. package/dist/adapters/index.js +21 -0
  12. package/dist/adapters/index.js.map +1 -0
  13. package/dist/adapters/mcp-catalog.d.ts +58 -0
  14. package/dist/adapters/mcp-catalog.js +9 -0
  15. package/dist/adapters/mcp-catalog.js.map +1 -0
  16. package/dist/adapters/prisma-rollout.d.ts +60 -0
  17. package/dist/adapters/prisma-rollout.js +7 -0
  18. package/dist/adapters/prisma-rollout.js.map +1 -0
  19. package/dist/chunk-24ZXP7FI.js +93 -0
  20. package/dist/chunk-24ZXP7FI.js.map +1 -0
  21. package/dist/chunk-2DA6Q6TN.js +126 -0
  22. package/dist/chunk-2DA6Q6TN.js.map +1 -0
  23. package/dist/chunk-37BBB2P2.js +73 -0
  24. package/dist/chunk-37BBB2P2.js.map +1 -0
  25. package/dist/chunk-57W3AR43.js +52 -0
  26. package/dist/chunk-57W3AR43.js.map +1 -0
  27. package/dist/chunk-5N4644PB.js +67 -0
  28. package/dist/chunk-5N4644PB.js.map +1 -0
  29. package/dist/chunk-5YS7WAPS.js +177 -0
  30. package/dist/chunk-5YS7WAPS.js.map +1 -0
  31. package/dist/chunk-6EGG2OZC.js +13 -0
  32. package/dist/chunk-6EGG2OZC.js.map +1 -0
  33. package/dist/chunk-7BUOF367.js +126 -0
  34. package/dist/chunk-7BUOF367.js.map +1 -0
  35. package/dist/chunk-BJBBR3QA.js +121 -0
  36. package/dist/chunk-BJBBR3QA.js.map +1 -0
  37. package/dist/chunk-CGRCUKGT.js +73 -0
  38. package/dist/chunk-CGRCUKGT.js.map +1 -0
  39. package/dist/chunk-FUG5DT2C.js +75 -0
  40. package/dist/chunk-FUG5DT2C.js.map +1 -0
  41. package/dist/chunk-LO24VOA3.js +199 -0
  42. package/dist/chunk-LO24VOA3.js.map +1 -0
  43. package/dist/chunk-MUF7ZZTO.js +57 -0
  44. package/dist/chunk-MUF7ZZTO.js.map +1 -0
  45. package/dist/chunk-NN7DATXA.js +46 -0
  46. package/dist/chunk-NN7DATXA.js.map +1 -0
  47. package/dist/chunk-PGGWSUTM.js +33 -0
  48. package/dist/chunk-PGGWSUTM.js.map +1 -0
  49. package/dist/chunk-RDKYDMXT.js +135 -0
  50. package/dist/chunk-RDKYDMXT.js.map +1 -0
  51. package/dist/chunk-YYFPDBJG.js +63 -0
  52. package/dist/chunk-YYFPDBJG.js.map +1 -0
  53. package/dist/chunk-ZMYX5VBU.js +135 -0
  54. package/dist/chunk-ZMYX5VBU.js.map +1 -0
  55. package/dist/chunk-ZTSKS42I.js +131 -0
  56. package/dist/chunk-ZTSKS42I.js.map +1 -0
  57. package/dist/commands.d.ts +74 -0
  58. package/dist/commands.js +10 -0
  59. package/dist/commands.js.map +1 -0
  60. package/dist/definitions.d.ts +94 -0
  61. package/dist/definitions.js +15 -0
  62. package/dist/definitions.js.map +1 -0
  63. package/dist/dispatcher.d.ts +50 -0
  64. package/dist/dispatcher.js +8 -0
  65. package/dist/dispatcher.js.map +1 -0
  66. package/dist/durable-turn.d.ts +58 -0
  67. package/dist/durable-turn.js +9 -0
  68. package/dist/durable-turn.js.map +1 -0
  69. package/dist/hook-pipeline.d.ts +114 -0
  70. package/dist/hook-pipeline.js +13 -0
  71. package/dist/hook-pipeline.js.map +1 -0
  72. package/dist/index.d.ts +1874 -0
  73. package/dist/index.js +3117 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/loop.d.ts +78 -0
  76. package/dist/loop.js +9 -0
  77. package/dist/loop.js.map +1 -0
  78. package/dist/mcp-bridge.d.ts +48 -0
  79. package/dist/mcp-bridge.js +8 -0
  80. package/dist/mcp-bridge.js.map +1 -0
  81. package/dist/model.d.ts +154 -0
  82. package/dist/model.js +9 -0
  83. package/dist/model.js.map +1 -0
  84. package/dist/policy.d.ts +130 -0
  85. package/dist/policy.js +23 -0
  86. package/dist/policy.js.map +1 -0
  87. package/dist/protocol.d.ts +170 -0
  88. package/dist/protocol.js +15 -0
  89. package/dist/protocol.js.map +1 -0
  90. package/dist/rollout-store-persistent.d.ts +48 -0
  91. package/dist/rollout-store-persistent.js +9 -0
  92. package/dist/rollout-store-persistent.js.map +1 -0
  93. package/dist/rollout.d.ts +82 -0
  94. package/dist/rollout.js +15 -0
  95. package/dist/rollout.js.map +1 -0
  96. package/dist/sandbox.d.ts +65 -0
  97. package/dist/sandbox.js +15 -0
  98. package/dist/sandbox.js.map +1 -0
  99. package/dist/skills.d.ts +93 -0
  100. package/dist/skills.js +10 -0
  101. package/dist/skills.js.map +1 -0
  102. package/dist/subagents.d.ts +129 -0
  103. package/dist/subagents.js +21 -0
  104. package/dist/subagents.js.map +1 -0
  105. package/dist/tools.d.ts +77 -0
  106. package/dist/tools.js +9 -0
  107. package/dist/tools.js.map +1 -0
  108. package/package.json +74 -0
  109. package/src/adapters/dispatcher-sse.test.ts +218 -0
  110. package/src/adapters/dispatcher-sse.ts +222 -0
  111. package/src/adapters/index.ts +18 -0
  112. package/src/adapters/mcp-catalog.test.ts +213 -0
  113. package/src/adapters/mcp-catalog.ts +188 -0
  114. package/src/adapters/prisma-rollout.test.ts +153 -0
  115. package/src/adapters/prisma-rollout.ts +104 -0
  116. package/src/agent-runtime.test.ts +176 -0
  117. package/src/artifact-stream.test.ts +330 -0
  118. package/src/artifact-stream.ts +453 -0
  119. package/src/channel-gateway.test.ts +432 -0
  120. package/src/channel-gateway.ts +357 -0
  121. package/src/code-review.test.ts +501 -0
  122. package/src/code-review.ts +495 -0
  123. package/src/command-suggestions.test.ts +251 -0
  124. package/src/command-suggestions.ts +338 -0
  125. package/src/commands.test.ts +184 -0
  126. package/src/commands.ts +140 -0
  127. package/src/commit-message.test.ts +249 -0
  128. package/src/commit-message.ts +180 -0
  129. package/src/context-compaction.test.ts +522 -0
  130. package/src/context-compaction.ts +434 -0
  131. package/src/definitions.test.ts +78 -0
  132. package/src/definitions.ts +190 -0
  133. package/src/deployment-status.test.ts +215 -0
  134. package/src/deployment-status.ts +227 -0
  135. package/src/design-context.test.ts +195 -0
  136. package/src/design-context.ts +198 -0
  137. package/src/dispatcher.test.ts +234 -0
  138. package/src/dispatcher.ts +189 -0
  139. package/src/durable-turn.test.ts +209 -0
  140. package/src/durable-turn.ts +135 -0
  141. package/src/edit-planner.test.ts +204 -0
  142. package/src/edit-planner.ts +325 -0
  143. package/src/fuzzy-match.test.ts +311 -0
  144. package/src/fuzzy-match.ts +444 -0
  145. package/src/hook-pipeline.test.ts +279 -0
  146. package/src/hook-pipeline.ts +373 -0
  147. package/src/inbound-admission.test.ts +394 -0
  148. package/src/inbound-admission.ts +246 -0
  149. package/src/index.ts +47 -0
  150. package/src/loop.test.ts +161 -0
  151. package/src/loop.ts +211 -0
  152. package/src/mcp-bridge.test.ts +165 -0
  153. package/src/mcp-bridge.ts +76 -0
  154. package/src/memory-provider.test.ts +232 -0
  155. package/src/memory-provider.ts +257 -0
  156. package/src/model.ts +168 -0
  157. package/src/permission-ruleset.test.ts +301 -0
  158. package/src/permission-ruleset.ts +200 -0
  159. package/src/policy.ts +151 -0
  160. package/src/project-repo.test.ts +232 -0
  161. package/src/project-repo.ts +311 -0
  162. package/src/protocol.ts +159 -0
  163. package/src/rollout-store-persistent.test.ts +217 -0
  164. package/src/rollout-store-persistent.ts +166 -0
  165. package/src/rollout.ts +150 -0
  166. package/src/sandbox.ts +113 -0
  167. package/src/session-share.test.ts +360 -0
  168. package/src/session-share.ts +310 -0
  169. package/src/skill-distillation.test.ts +177 -0
  170. package/src/skill-distillation.ts +369 -0
  171. package/src/skills.test.ts +277 -0
  172. package/src/skills.ts +255 -0
  173. package/src/subagents.test.ts +290 -0
  174. package/src/subagents.ts +332 -0
  175. package/src/tools.ts +126 -0
  176. package/src/workbench.test.ts +0 -0
  177. package/src/workbench.ts +0 -0
  178. package/tsconfig.json +12 -0
  179. package/tsup.config.ts +33 -0
package/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # @nebutra/agent-runtime
2
+
3
+ > Status: WIP — not yet integrated into a production app. Track-B kernel
4
+ > transport and durable-turn queue binding are interface-only.
5
+
6
+ Multi-tenant **agent-runtime grammar**. A faithful re-expression of a terminal
7
+ coding-agent's runtime *design* into Sailor's grammar — TypeScript,
8
+ multi-tenant, zero infra changes, **no in-process untrusted-code execution**.
9
+
10
+ ## Why this exists
11
+
12
+ Sailor already covers the surrounding capabilities (`@nebutra/agents` for model
13
+ execution + provider routing + fallback + telemetry, `@nebutra/tenant`,
14
+ `@nebutra/db`, `@nebutra/permissions`, `@nebutra/queue`, `@nebutra/mcp`,
15
+ `backends/gateway`). What it lacked was a *coherent runtime grammar*: a
16
+ thread/turn/item model, a two-axis approval/capability policy, an event-sourced
17
+ session trace, and a uniform tool/MCP abstraction. This package supplies exactly
18
+ that and nothing else — it **wraps** existing primitives, it does not rebuild
19
+ them.
20
+
21
+ ## Dual-track architecture
22
+
23
+ - **Track A (this package)** — policy, protocol contract, model, rollout. All
24
+ tenant-scoped. Runs inside Sailor's TS web runtime.
25
+ - **Track B (decoupled, not this repo)** — an optional self-hosted isolator /
26
+ kernel sidecar that implements `ExternalSandbox` over the `./protocol`
27
+ contract. The protocol is the only coupling seam.
28
+
29
+ ## Modules
30
+
31
+ | Export | Capability | What it gives you |
32
+ |---|---|---|
33
+ | `./model` | thread/turn/item + event lifecycle | discriminated-union item taxonomy, `started/updated/completed/failed` SM, per-turn config snapshot+override |
34
+ | `./policy` | approval × capability axes | `ApprovalPolicy`, `CapabilityPolicy` (default `external_sandbox`), rule `Decision`, rich `ReviewDecision` |
35
+ | `./protocol` | JSON-RPC contract | method registry, **tenant+thread serialization scope**, server-initiated approval requests, notifications |
36
+ | `./tools` | uniform tool/MCP | `ToolRegistry` (registry→router→hooks), `adaptMcpTool` (MCP-as-adapter) |
37
+ | `./rollout` | event-sourced trace | append-only typed log, replay-to-state, compaction marker, per-item persistence policy |
38
+ | `./sandbox` | external-sandbox seam | `ExternalSandbox` delegation interface; fail-closed `REFUSING_SANDBOX` default |
39
+ | `./adapters/*` | reusable concrete bindings | MCP catalog, Web-standard SSE transport, and Prisma rollout persistence without a separate package |
40
+
41
+ ## Non-negotiables enforced here
42
+
43
+ - **Multi-tenant**: every scope, store key, and dispatch carries `tenantId`;
44
+ cross-tenant requests can never share a serialization scope.
45
+ - **No infra change**: no sandbox runtime, no new datastore — only interfaces.
46
+ - **No in-process untrusted exec**: the default executor refuses; real
47
+ execution is delegated to a decoupled isolator.
48
+
49
+ See `docs/capabilities/agent-runtime/` for the capability map and replication
50
+ guide.
@@ -0,0 +1,68 @@
1
+ import { ProtocolDispatcher } from '../dispatcher.js';
2
+ import { ThreadEvent } from '../model.js';
3
+ import { NotificationName } from '../protocol.js';
4
+ import 'zod';
5
+
6
+ /**
7
+ * Runtime-agnostic SSE / HTTP transport binding for `ProtocolDispatcher`.
8
+ *
9
+ * Built on Web-standard primitives ONLY — `Request`, `Response`,
10
+ * `ReadableStream`, `TextEncoder`, `AbortSignal`. There is no Hono / Express /
11
+ * `node:http` dependency, so the same code runs unchanged on edge runtimes,
12
+ * Cloudflare Workers, Deno, Bun, and Node (>=18, where these globals exist).
13
+ *
14
+ * Design goals:
15
+ * - Pure functions returning Web objects; nothing is mutated in place.
16
+ * - The dispatcher already fails closed on a missing `tenantId` (its
17
+ * `requestEnvelopeSchema` makes `tenantId` mandatory). This binding does
18
+ * not re-implement that policy — it faithfully relays the JSON-RPC error.
19
+ * - Never throw across the transport boundary and never leak a stack trace:
20
+ * a malformed body becomes a well-formed JSON-RPC parse error.
21
+ *
22
+ * Wire framing (SSE): each event is emitted as
23
+ * `event: <ThreadEvent.type>\n` + `data: <JSON(event)>\n\n`
24
+ */
25
+
26
+ /**
27
+ * Build a Web-standard request handler bound to a dispatcher.
28
+ *
29
+ * Contract:
30
+ * - Unparseable body → `400` with a JSON-RPC parse-error envelope.
31
+ * - Any parseable body (incl. missing tenant / unknown method / handler
32
+ * failure) → `200` with the dispatcher's own JSON-RPC envelope. The HTTP
33
+ * layer stays `200` for business-level JSON-RPC errors per JSON-RPC-over-
34
+ * HTTP convention; the error lives in the envelope, not the status line.
35
+ * - The returned function never throws and never emits a 500-with-stack.
36
+ */
37
+ declare function createRpcHandler(dispatcher: ProtocolDispatcher): (request: Request) => Promise<Response>;
38
+ interface SseOptions {
39
+ /** Cancels the stream cleanly; closing the consumer also cancels it. */
40
+ readonly signal?: AbortSignal;
41
+ }
42
+ /**
43
+ * Adapt an async `ThreadEvent` stream into a Web-standard
44
+ * `text/event-stream` `Response`.
45
+ *
46
+ * Guarantees:
47
+ * - Incremental flush: each event is enqueued as soon as it is produced.
48
+ * - Clean termination: the stream closes when the iterable is exhausted.
49
+ * - Error surfacing: an iterable error is emitted as a final
50
+ * `event: error` frame, then the stream closes (it never hangs).
51
+ * - Cancellation: an aborted `signal` (or a cancelled reader) stops
52
+ * pulling from the source and releases its iterator.
53
+ */
54
+ declare function sseResponse(stream: AsyncIterable<ThreadEvent>, opts?: SseOptions): Response;
55
+ interface NotificationFrame {
56
+ readonly name: NotificationName;
57
+ readonly payload: unknown;
58
+ }
59
+ /**
60
+ * Bridge `dispatcher.onNotification` (a push listener API) into a pull-based
61
+ * `AsyncIterable`. A bounded backlog buffers events that arrive between
62
+ * `next()` calls; pending readers are resolved immediately when an event
63
+ * arrives. Calling `return()` (or breaking a `for await`) unsubscribes every
64
+ * underlying listener — no leaks, fail-closed teardown.
65
+ */
66
+ declare function subscribeNotifications(dispatcher: ProtocolDispatcher, names: readonly NotificationName[]): AsyncIterable<NotificationFrame>;
67
+
68
+ export { type NotificationFrame, type SseOptions, createRpcHandler, sseResponse, subscribeNotifications };
@@ -0,0 +1,11 @@
1
+ import {
2
+ createRpcHandler,
3
+ sseResponse,
4
+ subscribeNotifications
5
+ } from "../chunk-ZTSKS42I.js";
6
+ export {
7
+ createRpcHandler,
8
+ sseResponse,
9
+ subscribeNotifications
10
+ };
11
+ //# sourceMappingURL=dispatcher-sse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,12 @@
1
+ export { NotificationFrame, SseOptions, createRpcHandler, sseResponse, subscribeNotifications } from './dispatcher-sse.js';
2
+ export { McpExecutorLike, McpRegistryLike, createMcpClientPort, createMcpServerCatalog } from './mcp-catalog.js';
3
+ export { PrismaRolloutDelegate, RolloutDelegateResolver, createPrismaRolloutPersistence } from './prisma-rollout.js';
4
+ import '../dispatcher.js';
5
+ import '../protocol.js';
6
+ import 'zod';
7
+ import '../model.js';
8
+ import '@nebutra/mcp';
9
+ import '../mcp-bridge.js';
10
+ import '../tools.js';
11
+ import '../rollout-store-persistent.js';
12
+ import '../rollout.js';
@@ -0,0 +1,21 @@
1
+ import {
2
+ createRpcHandler,
3
+ sseResponse,
4
+ subscribeNotifications
5
+ } from "../chunk-ZTSKS42I.js";
6
+ import {
7
+ createMcpClientPort,
8
+ createMcpServerCatalog
9
+ } from "../chunk-24ZXP7FI.js";
10
+ import {
11
+ createPrismaRolloutPersistence
12
+ } from "../chunk-57W3AR43.js";
13
+ export {
14
+ createMcpClientPort,
15
+ createMcpServerCatalog,
16
+ createPrismaRolloutPersistence,
17
+ createRpcHandler,
18
+ sseResponse,
19
+ subscribeNotifications
20
+ };
21
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,58 @@
1
+ import { MCPContext, ToolExecutionResult, MCPServerRegistry } from '@nebutra/mcp';
2
+ import { McpServerCatalogPort } from '../mcp-bridge.js';
3
+ import { McpClientLike } from '../tools.js';
4
+ import 'zod';
5
+
6
+ /**
7
+ * MCP catalog adapter — concrete wiring of `@nebutra/mcp` into
8
+ * `@nebutra/agent-runtime`'s injectable ports.
9
+ *
10
+ * `@nebutra/agent-runtime` is intentionally dependency-free: it declares the
11
+ * `McpServerCatalogPort` / `McpClientLike` ports and an `activateMcpTools`
12
+ * composer, but never imports `@nebutra/mcp`. This module supplies the only
13
+ * concrete bridge so apps/backends reuse it instead of re-implementing glue.
14
+ *
15
+ * Invariants:
16
+ * - tenantId is mandatory and validated *before* any registry/client call
17
+ * (fail-closed): an empty/blank tenant can never enumerate or execute.
18
+ * - Plan/tenant gating is delegated to `MCPServerRegistry` — a tenant only
19
+ * ever sees the tools its plan/tenant policy permits.
20
+ * - MCP `parameters` are translated into a Zod `inputSchema`; a tool with no
21
+ * usable schema still yields a permissive, never-crashing definition.
22
+ * - The MCP `ToolExecutionResult` envelope is unwrapped: a non-success
23
+ * result throws (fail-closed) rather than leaking a falsy value upstream.
24
+ *
25
+ * Both factories accept an injected dependency (registry / client) and default
26
+ * to the `@nebutra/mcp` singletons, keeping the module free of hidden globals
27
+ * and fully unit-testable.
28
+ */
29
+
30
+ /** Narrow structural shapes — we depend on behaviour, not concrete classes. */
31
+ /** The slice of `MCPServerRegistry` the catalog port needs. */
32
+ type McpRegistryLike = Pick<MCPServerRegistry, "getAccessibleTools">;
33
+ /** The slice of `mcpClient` the client port needs. */
34
+ interface McpExecutorLike {
35
+ executeTool(name: string, args: Record<string, unknown>, ctx: MCPContext): Promise<ToolExecutionResult>;
36
+ }
37
+ /**
38
+ * Adapt `MCPServerRegistry`'s plan/tenant-gated tool listing into the
39
+ * agent-runtime `McpServerCatalogPort`.
40
+ *
41
+ * @param registry Defaults to `@nebutra/mcp`'s `serverRegistry` singleton;
42
+ * inject a fresh `MCPServerRegistry` for tests/multi-tenant
43
+ * isolation.
44
+ */
45
+ declare function createMcpServerCatalog(registry?: McpRegistryLike): McpServerCatalogPort;
46
+ /**
47
+ * Adapt `@nebutra/mcp`'s `mcpClient` into agent-runtime's `McpClientLike`.
48
+ *
49
+ * Maps `{ requestId, tenantId }` → the richer `MCPContext`, and unwraps the
50
+ * `ToolExecutionResult` envelope: a non-success result throws so a failed MCP
51
+ * call never silently surfaces as a falsy tool output.
52
+ *
53
+ * @param client Defaults to `@nebutra/mcp`'s `mcpClient` singleton;
54
+ * inject a fake for tests.
55
+ */
56
+ declare function createMcpClientPort(client?: McpExecutorLike): McpClientLike;
57
+
58
+ export { type McpExecutorLike, type McpRegistryLike, createMcpClientPort, createMcpServerCatalog };
@@ -0,0 +1,9 @@
1
+ import {
2
+ createMcpClientPort,
3
+ createMcpServerCatalog
4
+ } from "../chunk-24ZXP7FI.js";
5
+ export {
6
+ createMcpClientPort,
7
+ createMcpServerCatalog
8
+ };
9
+ //# sourceMappingURL=mcp-catalog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,60 @@
1
+ import { RolloutPersistencePort } from '../rollout-store-persistent.js';
2
+ import '../rollout.js';
3
+ import '../model.js';
4
+
5
+ /**
6
+ * Durable rollout-store backend: `RolloutPersistencePort` over Prisma.
7
+ *
8
+ * This is the fail-loud system-of-record adapter (table `agent_rollout_lines`,
9
+ * migration `20260519000000_add_agent_rollout_store`). Unlike a best-effort
10
+ * audit log it never swallows a failed write — a rejected `put` propagates so
11
+ * the caller's "never silently drop" contract holds.
12
+ *
13
+ * Reusability: it depends only on a minimal injected delegate (the
14
+ * `PrismaAuditDelegate` pattern), NOT on the generated Prisma client or
15
+ * `@nebutra/db`. Any tenant-scoped Prisma client (`getTenantDb(orgId)`)
16
+ * structurally satisfies it, so this package stays dependency-light.
17
+ */
18
+
19
+ /**
20
+ * Minimal structural slice of `prisma.agentRolloutLine`. Decoupled from the
21
+ * generated client on purpose — only these two operations are needed.
22
+ */
23
+ interface PrismaRolloutDelegate {
24
+ create(args: {
25
+ data: {
26
+ tenantId: string;
27
+ threadId: string;
28
+ seq: number;
29
+ at: Date;
30
+ payload: string;
31
+ };
32
+ }): Promise<unknown>;
33
+ findMany(args: {
34
+ where: {
35
+ tenantId: string;
36
+ threadId: string;
37
+ };
38
+ orderBy: {
39
+ seq: "asc";
40
+ };
41
+ select: {
42
+ seq: true;
43
+ payload: true;
44
+ };
45
+ }): Promise<{
46
+ seq: number;
47
+ payload: string;
48
+ }[]>;
49
+ }
50
+ /** Resolves the delegate per call so a request-scoped (RLS) client can be used. */
51
+ type RolloutDelegateResolver = PrismaRolloutDelegate | ((tenantId: string) => PrismaRolloutDelegate | Promise<PrismaRolloutDelegate>);
52
+ /**
53
+ * Build a {@link RolloutPersistencePort} backed by Prisma.
54
+ *
55
+ * @param delegate a `PrismaRolloutDelegate`, or a resolver mapping a tenantId
56
+ * to one (use the latter to obtain a per-tenant RLS client per call).
57
+ */
58
+ declare function createPrismaRolloutPersistence(delegate: RolloutDelegateResolver): RolloutPersistencePort;
59
+
60
+ export { type PrismaRolloutDelegate, type RolloutDelegateResolver, createPrismaRolloutPersistence };
@@ -0,0 +1,7 @@
1
+ import {
2
+ createPrismaRolloutPersistence
3
+ } from "../chunk-57W3AR43.js";
4
+ export {
5
+ createPrismaRolloutPersistence
6
+ };
7
+ //# sourceMappingURL=prisma-rollout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,93 @@
1
+ // src/adapters/mcp-catalog.ts
2
+ import { mcpClient, serverRegistry } from "@nebutra/mcp";
3
+ import { z } from "zod";
4
+ var tenantSchema = z.string({ message: "tenantId is required (fail-closed)" }).trim().min(1, "tenantId is required (fail-closed)");
5
+ var catalogCtxSchema = z.object({
6
+ tenantId: tenantSchema,
7
+ plan: z.string().min(1).optional()
8
+ });
9
+ var clientCtxSchema = z.object({
10
+ requestId: z.string().min(1, "requestId is required"),
11
+ tenantId: tenantSchema
12
+ });
13
+ function paramToZod(param) {
14
+ switch (param.type) {
15
+ case "string":
16
+ return z.string();
17
+ case "number":
18
+ return z.number();
19
+ case "boolean":
20
+ return z.boolean();
21
+ case "array":
22
+ return z.array(z.unknown());
23
+ case "object":
24
+ return z.record(z.string(), z.unknown());
25
+ default:
26
+ return z.unknown();
27
+ }
28
+ }
29
+ function toInputSchema(parameters) {
30
+ const entries = parameters ? Object.entries(parameters) : [];
31
+ if (entries.length === 0) {
32
+ return z.record(z.string(), z.unknown());
33
+ }
34
+ const shape = {};
35
+ for (const [key, param] of entries) {
36
+ let leaf = paramToZod(param);
37
+ if (param.required !== true) {
38
+ leaf = leaf.optional();
39
+ }
40
+ shape[key] = leaf;
41
+ }
42
+ return z.object(shape).passthrough();
43
+ }
44
+ function toToolDefinition(tool) {
45
+ return {
46
+ name: tool.name,
47
+ description: tool.description ?? tool.name,
48
+ inputSchema: toInputSchema(tool.parameters)
49
+ };
50
+ }
51
+ function createMcpServerCatalog(registry = serverRegistry) {
52
+ return {
53
+ async listTools(ctx) {
54
+ const scope = catalogCtxSchema.parse(ctx);
55
+ const mcpCtx = {
56
+ requestId: "catalog-list",
57
+ tenantId: scope.tenantId,
58
+ ...scope.plan === void 0 ? {} : { plan: scope.plan }
59
+ };
60
+ const tools = registry.getAccessibleTools(mcpCtx);
61
+ return tools.map((tool) => ({
62
+ server: tool.serverId,
63
+ definition: toToolDefinition(tool)
64
+ }));
65
+ }
66
+ };
67
+ }
68
+ function createMcpClientPort(client = mcpClient) {
69
+ return {
70
+ async executeTool(name, args, ctx) {
71
+ const scope = clientCtxSchema.parse(ctx);
72
+ const mcpCtx = {
73
+ requestId: scope.requestId,
74
+ tenantId: scope.tenantId
75
+ };
76
+ const result = await client.executeTool(
77
+ name,
78
+ args ?? {},
79
+ mcpCtx
80
+ );
81
+ if (!result.success) {
82
+ throw new Error(`MCP tool "${name}" failed: ${result.error ?? "unknown error"}`);
83
+ }
84
+ return result.result;
85
+ }
86
+ };
87
+ }
88
+
89
+ export {
90
+ createMcpServerCatalog,
91
+ createMcpClientPort
92
+ };
93
+ //# sourceMappingURL=chunk-24ZXP7FI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/adapters/mcp-catalog.ts"],"sourcesContent":["/**\n * MCP catalog adapter — concrete wiring of `@nebutra/mcp` into\n * `@nebutra/agent-runtime`'s injectable ports.\n *\n * `@nebutra/agent-runtime` is intentionally dependency-free: it declares the\n * `McpServerCatalogPort` / `McpClientLike` ports and an `activateMcpTools`\n * composer, but never imports `@nebutra/mcp`. This module supplies the only\n * concrete bridge so apps/backends reuse it instead of re-implementing glue.\n *\n * Invariants:\n * - tenantId is mandatory and validated *before* any registry/client call\n * (fail-closed): an empty/blank tenant can never enumerate or execute.\n * - Plan/tenant gating is delegated to `MCPServerRegistry` — a tenant only\n * ever sees the tools its plan/tenant policy permits.\n * - MCP `parameters` are translated into a Zod `inputSchema`; a tool with no\n * usable schema still yields a permissive, never-crashing definition.\n * - The MCP `ToolExecutionResult` envelope is unwrapped: a non-success\n * result throws (fail-closed) rather than leaking a falsy value upstream.\n *\n * Both factories accept an injected dependency (registry / client) and default\n * to the `@nebutra/mcp` singletons, keeping the module free of hidden globals\n * and fully unit-testable.\n */\n\nimport type { MCPContext, MCPServerRegistry, ToolExecutionResult } from \"@nebutra/mcp\";\nimport { mcpClient, serverRegistry } from \"@nebutra/mcp\";\nimport { z } from \"zod\";\nimport type { McpServerCatalogPort } from \"../mcp-bridge\";\nimport type { McpClientLike, ToolDefinition } from \"../tools\";\n\n/** Narrow structural shapes — we depend on behaviour, not concrete classes. */\n\n/** The slice of `MCPServerRegistry` the catalog port needs. */\nexport type McpRegistryLike = Pick<MCPServerRegistry, \"getAccessibleTools\">;\n\n/** The slice of `mcpClient` the client port needs. */\nexport interface McpExecutorLike {\n executeTool(\n name: string,\n args: Record<string, unknown>,\n ctx: MCPContext,\n ): Promise<ToolExecutionResult>;\n}\n\ntype McpParameter = {\n type?: \"string\" | \"number\" | \"boolean\" | \"object\" | \"array\";\n description?: string;\n required?: boolean;\n default?: unknown;\n};\n\ntype McpToolShape = {\n name: string;\n description?: string;\n parameters?: Record<string, McpParameter> | undefined;\n serverId: string;\n};\n\n/** Fail-closed tenant guard — the single chokepoint for both ports. */\nconst tenantSchema = z\n .string({ message: \"tenantId is required (fail-closed)\" })\n .trim()\n .min(1, \"tenantId is required (fail-closed)\");\n\nconst catalogCtxSchema = z.object({\n tenantId: tenantSchema,\n plan: z.string().min(1).optional(),\n});\n\nconst clientCtxSchema = z.object({\n requestId: z.string().min(1, \"requestId is required\"),\n tenantId: tenantSchema,\n});\n\n/** Map one MCP parameter to its Zod leaf type. Unknown → permissive. */\nfunction paramToZod(param: McpParameter): z.ZodTypeAny {\n switch (param.type) {\n case \"string\":\n return z.string();\n case \"number\":\n return z.number();\n case \"boolean\":\n return z.boolean();\n case \"array\":\n return z.array(z.unknown());\n case \"object\":\n return z.record(z.string(), z.unknown());\n default:\n return z.unknown();\n }\n}\n\n/**\n * Build a Zod `inputSchema` from an MCP tool's `parameters`. A tool with no\n * declared parameters becomes a permissive passthrough object (never throws on\n * unknown keys) so schema-less MCP tools remain dispatchable.\n */\nfunction toInputSchema(parameters: Record<string, McpParameter> | undefined): z.ZodType {\n const entries = parameters ? Object.entries(parameters) : [];\n if (entries.length === 0) {\n return z.record(z.string(), z.unknown());\n }\n\n const shape: Record<string, z.ZodTypeAny> = {};\n for (const [key, param] of entries) {\n let leaf = paramToZod(param);\n if (param.required !== true) {\n leaf = leaf.optional();\n }\n shape[key] = leaf;\n }\n // `passthrough` keeps unknown keys: MCP schemas are advisory, not strict.\n return z.object(shape).passthrough();\n}\n\n/** Adapt one registry tool entry into an agent-runtime `ToolDefinition`. */\nfunction toToolDefinition(tool: McpToolShape): ToolDefinition {\n return {\n name: tool.name,\n description: tool.description ?? tool.name,\n inputSchema: toInputSchema(tool.parameters),\n };\n}\n\n/**\n * Adapt `MCPServerRegistry`'s plan/tenant-gated tool listing into the\n * agent-runtime `McpServerCatalogPort`.\n *\n * @param registry Defaults to `@nebutra/mcp`'s `serverRegistry` singleton;\n * inject a fresh `MCPServerRegistry` for tests/multi-tenant\n * isolation.\n */\nexport function createMcpServerCatalog(\n registry: McpRegistryLike = serverRegistry,\n): McpServerCatalogPort {\n return {\n async listTools(ctx) {\n const scope = catalogCtxSchema.parse(ctx); // fail-closed first\n\n const mcpCtx: MCPContext = {\n requestId: \"catalog-list\",\n tenantId: scope.tenantId,\n ...(scope.plan === undefined ? {} : { plan: scope.plan }),\n };\n\n const tools = registry.getAccessibleTools(mcpCtx) as McpToolShape[];\n\n return tools.map((tool) => ({\n server: tool.serverId,\n definition: toToolDefinition(tool),\n }));\n },\n };\n}\n\n/**\n * Adapt `@nebutra/mcp`'s `mcpClient` into agent-runtime's `McpClientLike`.\n *\n * Maps `{ requestId, tenantId }` → the richer `MCPContext`, and unwraps the\n * `ToolExecutionResult` envelope: a non-success result throws so a failed MCP\n * call never silently surfaces as a falsy tool output.\n *\n * @param client Defaults to `@nebutra/mcp`'s `mcpClient` singleton;\n * inject a fake for tests.\n */\nexport function createMcpClientPort(client: McpExecutorLike = mcpClient): McpClientLike {\n return {\n async executeTool(name, args, ctx) {\n const scope = clientCtxSchema.parse(ctx); // fail-closed first\n\n const mcpCtx: MCPContext = {\n requestId: scope.requestId,\n tenantId: scope.tenantId,\n };\n\n const result = await client.executeTool(\n name,\n (args ?? {}) as Record<string, unknown>,\n mcpCtx,\n );\n\n if (!result.success) {\n throw new Error(`MCP tool \"${name}\" failed: ${result.error ?? \"unknown error\"}`);\n }\n return result.result;\n },\n };\n}\n"],"mappings":";AAyBA,SAAS,WAAW,sBAAsB;AAC1C,SAAS,SAAS;AAiClB,IAAM,eAAe,EAClB,OAAO,EAAE,SAAS,qCAAqC,CAAC,EACxD,KAAK,EACL,IAAI,GAAG,oCAAoC;AAE9C,IAAM,mBAAmB,EAAE,OAAO;AAAA,EAChC,UAAU;AAAA,EACV,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS;AACnC,CAAC;AAED,IAAM,kBAAkB,EAAE,OAAO;AAAA,EAC/B,WAAW,EAAE,OAAO,EAAE,IAAI,GAAG,uBAAuB;AAAA,EACpD,UAAU;AACZ,CAAC;AAGD,SAAS,WAAW,OAAmC;AACrD,UAAQ,MAAM,MAAM;AAAA,IAClB,KAAK;AACH,aAAO,EAAE,OAAO;AAAA,IAClB,KAAK;AACH,aAAO,EAAE,OAAO;AAAA,IAClB,KAAK;AACH,aAAO,EAAE,QAAQ;AAAA,IACnB,KAAK;AACH,aAAO,EAAE,MAAM,EAAE,QAAQ,CAAC;AAAA,IAC5B,KAAK;AACH,aAAO,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC;AAAA,IACzC;AACE,aAAO,EAAE,QAAQ;AAAA,EACrB;AACF;AAOA,SAAS,cAAc,YAAiE;AACtF,QAAM,UAAU,aAAa,OAAO,QAAQ,UAAU,IAAI,CAAC;AAC3D,MAAI,QAAQ,WAAW,GAAG;AACxB,WAAO,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC;AAAA,EACzC;AAEA,QAAM,QAAsC,CAAC;AAC7C,aAAW,CAAC,KAAK,KAAK,KAAK,SAAS;AAClC,QAAI,OAAO,WAAW,KAAK;AAC3B,QAAI,MAAM,aAAa,MAAM;AAC3B,aAAO,KAAK,SAAS;AAAA,IACvB;AACA,UAAM,GAAG,IAAI;AAAA,EACf;AAEA,SAAO,EAAE,OAAO,KAAK,EAAE,YAAY;AACrC;AAGA,SAAS,iBAAiB,MAAoC;AAC5D,SAAO;AAAA,IACL,MAAM,KAAK;AAAA,IACX,aAAa,KAAK,eAAe,KAAK;AAAA,IACtC,aAAa,cAAc,KAAK,UAAU;AAAA,EAC5C;AACF;AAUO,SAAS,uBACd,WAA4B,gBACN;AACtB,SAAO;AAAA,IACL,MAAM,UAAU,KAAK;AACnB,YAAM,QAAQ,iBAAiB,MAAM,GAAG;AAExC,YAAM,SAAqB;AAAA,QACzB,WAAW;AAAA,QACX,UAAU,MAAM;AAAA,QAChB,GAAI,MAAM,SAAS,SAAY,CAAC,IAAI,EAAE,MAAM,MAAM,KAAK;AAAA,MACzD;AAEA,YAAM,QAAQ,SAAS,mBAAmB,MAAM;AAEhD,aAAO,MAAM,IAAI,CAAC,UAAU;AAAA,QAC1B,QAAQ,KAAK;AAAA,QACb,YAAY,iBAAiB,IAAI;AAAA,MACnC,EAAE;AAAA,IACJ;AAAA,EACF;AACF;AAYO,SAAS,oBAAoB,SAA0B,WAA0B;AACtF,SAAO;AAAA,IACL,MAAM,YAAY,MAAM,MAAM,KAAK;AACjC,YAAM,QAAQ,gBAAgB,MAAM,GAAG;AAEvC,YAAM,SAAqB;AAAA,QACzB,WAAW,MAAM;AAAA,QACjB,UAAU,MAAM;AAAA,MAClB;AAEA,YAAM,SAAS,MAAM,OAAO;AAAA,QAC1B;AAAA,QACC,QAAQ,CAAC;AAAA,QACV;AAAA,MACF;AAEA,UAAI,CAAC,OAAO,SAAS;AACnB,cAAM,IAAI,MAAM,aAAa,IAAI,aAAa,OAAO,SAAS,eAAe,EAAE;AAAA,MACjF;AACA,aAAO,OAAO;AAAA,IAChB;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,126 @@
1
+ // src/rollout-store-persistent.ts
2
+ import { z } from "zod";
3
+ var RoundTripError = class extends Error {
4
+ constructor(message, options) {
5
+ super(message, options);
6
+ this.name = "RoundTripError";
7
+ }
8
+ };
9
+ var tenantId = z.string().min(1, "tenantId is required");
10
+ var threadId = z.string().min(1, "threadId is required");
11
+ var at = z.string().min(1);
12
+ var turnConfigSchema = z.object({
13
+ model: z.string(),
14
+ provider: z.string(),
15
+ approvalPolicy: z.string(),
16
+ capabilityPolicy: z.string(),
17
+ reasoningEffort: z.enum(["low", "medium", "high"]).optional()
18
+ });
19
+ var threadEventSchema = z.custom(
20
+ (v) => typeof v === "object" && v !== null && typeof v.type === "string",
21
+ { message: "invalid ThreadEvent" }
22
+ );
23
+ var rolloutLineSchema = z.discriminatedUnion("type", [
24
+ z.object({
25
+ tenantId,
26
+ threadId,
27
+ type: z.literal("session_meta"),
28
+ config: turnConfigSchema,
29
+ at
30
+ }),
31
+ z.object({
32
+ tenantId,
33
+ threadId,
34
+ type: z.literal("event"),
35
+ event: threadEventSchema,
36
+ at
37
+ }),
38
+ z.object({
39
+ tenantId,
40
+ threadId,
41
+ type: z.literal("turn_context"),
42
+ config: turnConfigSchema,
43
+ at
44
+ }),
45
+ z.object({
46
+ tenantId,
47
+ threadId,
48
+ type: z.literal("compacted"),
49
+ summary: z.string(),
50
+ droppedThrough: z.string(),
51
+ at
52
+ })
53
+ ]);
54
+ var tenantThreadSchema = z.object({ tenantId, threadId });
55
+ var PersistentRolloutStore = class {
56
+ #port;
57
+ /**
58
+ * Per-(tenant,thread) seq allocation chained through a promise so concurrent
59
+ * `append` calls to the same thread serialize and never collide on seq —
60
+ * without blocking appends to other threads.
61
+ */
62
+ #tails = /* @__PURE__ */ new Map();
63
+ constructor(port) {
64
+ this.#port = port;
65
+ }
66
+ #key(t, th) {
67
+ return `${t}::${th}`;
68
+ }
69
+ async append(line) {
70
+ const validated = rolloutLineSchema.parse(line);
71
+ const key = this.#key(validated.tenantId, validated.threadId);
72
+ const prev = this.#tails.get(key) ?? Promise.resolve(-1);
73
+ const next = prev.then(async (last) => {
74
+ const seq = last + 1;
75
+ await this.#port.put({
76
+ tenantId: validated.tenantId,
77
+ threadId: validated.threadId,
78
+ seq,
79
+ at: validated.at,
80
+ payload: JSON.stringify(validated)
81
+ });
82
+ return seq;
83
+ });
84
+ this.#tails.set(
85
+ key,
86
+ next.catch(() => this.#tails.get(key) === next ? -1 : next)
87
+ );
88
+ await next;
89
+ }
90
+ async read(tenantIdArg, threadIdArg) {
91
+ const { tenantId: t, threadId: th } = tenantThreadSchema.parse({
92
+ tenantId: tenantIdArg,
93
+ threadId: threadIdArg
94
+ });
95
+ const records = await this.#port.list(t, th);
96
+ const ordered = [...records].sort((a, b) => a.seq - b.seq);
97
+ return ordered.map((r) => {
98
+ let parsed;
99
+ try {
100
+ parsed = JSON.parse(r.payload);
101
+ } catch (cause) {
102
+ throw new RoundTripError(`malformed stored payload at seq ${r.seq} for ${t}::${th}`, {
103
+ cause
104
+ });
105
+ }
106
+ const result = rolloutLineSchema.safeParse(parsed);
107
+ if (!result.success) {
108
+ throw new RoundTripError(
109
+ `structurally-invalid stored payload at seq ${r.seq} for ${t}::${th}`,
110
+ { cause: result.error }
111
+ );
112
+ }
113
+ const reconstructed = result.data;
114
+ if (reconstructed.tenantId !== t || reconstructed.threadId !== th) {
115
+ throw new RoundTripError(`tenant/thread mismatch at seq ${r.seq} for ${t}::${th}`);
116
+ }
117
+ return reconstructed;
118
+ });
119
+ }
120
+ };
121
+
122
+ export {
123
+ RoundTripError,
124
+ PersistentRolloutStore
125
+ };
126
+ //# sourceMappingURL=chunk-2DA6Q6TN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/rollout-store-persistent.ts"],"sourcesContent":["/**\n * Production-grade, tenant-scoped {@link RolloutStore} backed by an injectable\n * persistence port. This package depends on NO datastore: a Postgres /\n * ClickHouse / `@nebutra/db` / `@nebutra/audit` adapter satisfies\n * {@link RolloutPersistencePort} from the outside, so the \"no infra change\"\n * promise stays honest — swap the adapter, keep the interface.\n *\n * Tenancy is structural: `tenantId` is part of the storage key AND Zod-validated\n * on every read and write. Cross-tenant access is impossible by construction.\n * The store fails closed — empty identifiers and malformed stored payloads\n * raise typed errors; records are never silently dropped.\n */\n\nimport { z } from \"zod\";\nimport type { RolloutLine, RolloutStore } from \"./rollout\";\n\n/**\n * Minimal append-only persistence seam. Implementations MUST persist records\n * durably and return them ascending by `seq` for a given (tenant, thread).\n */\nexport interface RolloutPersistencePort {\n put(record: {\n tenantId: string;\n threadId: string;\n seq: number;\n at: string;\n payload: string;\n }): Promise<void>;\n /** Records for one (tenant, thread), ascending by seq. */\n list(tenantId: string, threadId: string): Promise<{ seq: number; payload: string }[]>;\n}\n\n/** Raised when a stored payload cannot be faithfully reconstructed. */\nexport class RoundTripError extends Error {\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = \"RoundTripError\";\n }\n}\n\nconst tenantId = z.string().min(1, \"tenantId is required\");\nconst threadId = z.string().min(1, \"threadId is required\");\nconst at = z.string().min(1);\n\nconst turnConfigSchema = z.object({\n model: z.string(),\n provider: z.string(),\n approvalPolicy: z.string(),\n capabilityPolicy: z.string(),\n reasoningEffort: z.enum([\"low\", \"medium\", \"high\"]).optional(),\n});\n\nconst threadEventSchema = z.custom<RolloutLine extends { event: infer E } ? E : never>(\n (v) => typeof v === \"object\" && v !== null && typeof (v as { type?: unknown }).type === \"string\",\n { message: \"invalid ThreadEvent\" },\n);\n\nconst rolloutLineSchema: z.ZodType<RolloutLine> = z.discriminatedUnion(\"type\", [\n z.object({\n tenantId,\n threadId,\n type: z.literal(\"session_meta\"),\n config: turnConfigSchema,\n at,\n }),\n z.object({\n tenantId,\n threadId,\n type: z.literal(\"event\"),\n event: threadEventSchema,\n at,\n }),\n z.object({\n tenantId,\n threadId,\n type: z.literal(\"turn_context\"),\n config: turnConfigSchema,\n at,\n }),\n z.object({\n tenantId,\n threadId,\n type: z.literal(\"compacted\"),\n summary: z.string(),\n droppedThrough: z.string(),\n at,\n }),\n]) as z.ZodType<RolloutLine>;\n\nconst tenantThreadSchema = z.object({ tenantId, threadId });\n\nexport class PersistentRolloutStore implements RolloutStore {\n readonly #port: RolloutPersistencePort;\n /**\n * Per-(tenant,thread) seq allocation chained through a promise so concurrent\n * `append` calls to the same thread serialize and never collide on seq —\n * without blocking appends to other threads.\n */\n readonly #tails = new Map<string, Promise<number>>();\n\n constructor(port: RolloutPersistencePort) {\n this.#port = port;\n }\n\n #key(t: string, th: string): string {\n return `${t}::${th}`;\n }\n\n async append(line: RolloutLine): Promise<void> {\n const validated = rolloutLineSchema.parse(line);\n const key = this.#key(validated.tenantId, validated.threadId);\n\n const prev = this.#tails.get(key) ?? Promise.resolve(-1);\n const next = prev.then(async (last) => {\n const seq = last + 1;\n await this.#port.put({\n tenantId: validated.tenantId,\n threadId: validated.threadId,\n seq,\n at: validated.at,\n payload: JSON.stringify(validated),\n });\n return seq;\n });\n // Keep the chain advancing even if a put fails (fail-closed for the caller,\n // but the next append must not deadlock on a rejected tail).\n this.#tails.set(\n key,\n next.catch(() => (this.#tails.get(key) === next ? -1 : next)),\n );\n await next;\n }\n\n async read(tenantIdArg: string, threadIdArg: string): Promise<readonly RolloutLine[]> {\n const { tenantId: t, threadId: th } = tenantThreadSchema.parse({\n tenantId: tenantIdArg,\n threadId: threadIdArg,\n });\n\n const records = await this.#port.list(t, th);\n const ordered = [...records].sort((a, b) => a.seq - b.seq);\n\n return ordered.map((r) => {\n let parsed: unknown;\n try {\n parsed = JSON.parse(r.payload);\n } catch (cause) {\n throw new RoundTripError(`malformed stored payload at seq ${r.seq} for ${t}::${th}`, {\n cause,\n });\n }\n const result = rolloutLineSchema.safeParse(parsed);\n if (!result.success) {\n throw new RoundTripError(\n `structurally-invalid stored payload at seq ${r.seq} for ${t}::${th}`,\n { cause: result.error },\n );\n }\n const reconstructed = result.data;\n if (reconstructed.tenantId !== t || reconstructed.threadId !== th) {\n throw new RoundTripError(`tenant/thread mismatch at seq ${r.seq} for ${t}::${th}`);\n }\n return reconstructed;\n });\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,73 @@
1
+ // src/rollout.ts
2
+ var PERSISTED_OUTPUT_MAX_BYTES = 1e4;
3
+ function isPersisted(item, mode) {
4
+ if (item.type === "reasoning") return mode === "extended";
5
+ return true;
6
+ }
7
+ function sanitizeForPersist(item) {
8
+ if (item.type !== "command_execution") return item;
9
+ const bytes = Buffer.byteLength(item.aggregatedOutput, "utf8");
10
+ if (bytes <= PERSISTED_OUTPUT_MAX_BYTES) return item;
11
+ const truncated = Buffer.from(item.aggregatedOutput, "utf8").subarray(0, PERSISTED_OUTPUT_MAX_BYTES).toString("utf8");
12
+ return { ...item, aggregatedOutput: `${truncated}
13
+ \u2026[truncated]` };
14
+ }
15
+ var InMemoryRolloutStore = class {
16
+ #lines = /* @__PURE__ */ new Map();
17
+ #key(tenantId, threadId) {
18
+ return `${tenantId}::${threadId}`;
19
+ }
20
+ async append(line) {
21
+ const key = this.#key(line.tenantId, line.threadId);
22
+ const list = this.#lines.get(key) ?? [];
23
+ list.push(line);
24
+ this.#lines.set(key, list);
25
+ }
26
+ async read(tenantId, threadId) {
27
+ return [...this.#lines.get(this.#key(tenantId, threadId)) ?? []];
28
+ }
29
+ };
30
+ function replay(lines) {
31
+ const first = lines[0];
32
+ if (!first) return null;
33
+ const projection = {
34
+ tenantId: first.tenantId,
35
+ threadId: first.threadId,
36
+ items: []
37
+ };
38
+ for (const line of lines) {
39
+ switch (line.type) {
40
+ case "session_meta":
41
+ case "turn_context":
42
+ projection.config = line.config;
43
+ break;
44
+ case "compacted":
45
+ projection.compactionSummary = line.summary;
46
+ projection.items.length = 0;
47
+ break;
48
+ case "event": {
49
+ const e = line.event;
50
+ if (e.type === "item.completed" || e.type === "item.started") {
51
+ const idx = projection.items.findIndex((i) => i.id === e.item.id);
52
+ if (idx >= 0) projection.items[idx] = e.item;
53
+ else projection.items.push(e.item);
54
+ } else if (e.type === "item.updated") {
55
+ const idx = projection.items.findIndex((i) => i.id === e.item.id);
56
+ if (idx >= 0) projection.items[idx] = e.item;
57
+ else projection.items.push(e.item);
58
+ }
59
+ break;
60
+ }
61
+ }
62
+ }
63
+ return projection;
64
+ }
65
+
66
+ export {
67
+ PERSISTED_OUTPUT_MAX_BYTES,
68
+ isPersisted,
69
+ sanitizeForPersist,
70
+ InMemoryRolloutStore,
71
+ replay
72
+ };
73
+ //# sourceMappingURL=chunk-37BBB2P2.js.map