@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.
- package/.turbo/turbo-build.log +115 -0
- package/.turbo/turbo-test.log +44 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/CHANGELOG.md +253 -0
- package/LICENSE +676 -0
- package/README.md +50 -0
- package/dist/adapters/dispatcher-sse.d.ts +68 -0
- package/dist/adapters/dispatcher-sse.js +11 -0
- package/dist/adapters/dispatcher-sse.js.map +1 -0
- package/dist/adapters/index.d.ts +12 -0
- package/dist/adapters/index.js +21 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/mcp-catalog.d.ts +58 -0
- package/dist/adapters/mcp-catalog.js +9 -0
- package/dist/adapters/mcp-catalog.js.map +1 -0
- package/dist/adapters/prisma-rollout.d.ts +60 -0
- package/dist/adapters/prisma-rollout.js +7 -0
- package/dist/adapters/prisma-rollout.js.map +1 -0
- package/dist/chunk-24ZXP7FI.js +93 -0
- package/dist/chunk-24ZXP7FI.js.map +1 -0
- package/dist/chunk-2DA6Q6TN.js +126 -0
- package/dist/chunk-2DA6Q6TN.js.map +1 -0
- package/dist/chunk-37BBB2P2.js +73 -0
- package/dist/chunk-37BBB2P2.js.map +1 -0
- package/dist/chunk-57W3AR43.js +52 -0
- package/dist/chunk-57W3AR43.js.map +1 -0
- package/dist/chunk-5N4644PB.js +67 -0
- package/dist/chunk-5N4644PB.js.map +1 -0
- package/dist/chunk-5YS7WAPS.js +177 -0
- package/dist/chunk-5YS7WAPS.js.map +1 -0
- package/dist/chunk-6EGG2OZC.js +13 -0
- package/dist/chunk-6EGG2OZC.js.map +1 -0
- package/dist/chunk-7BUOF367.js +126 -0
- package/dist/chunk-7BUOF367.js.map +1 -0
- package/dist/chunk-BJBBR3QA.js +121 -0
- package/dist/chunk-BJBBR3QA.js.map +1 -0
- package/dist/chunk-CGRCUKGT.js +73 -0
- package/dist/chunk-CGRCUKGT.js.map +1 -0
- package/dist/chunk-FUG5DT2C.js +75 -0
- package/dist/chunk-FUG5DT2C.js.map +1 -0
- package/dist/chunk-LO24VOA3.js +199 -0
- package/dist/chunk-LO24VOA3.js.map +1 -0
- package/dist/chunk-MUF7ZZTO.js +57 -0
- package/dist/chunk-MUF7ZZTO.js.map +1 -0
- package/dist/chunk-NN7DATXA.js +46 -0
- package/dist/chunk-NN7DATXA.js.map +1 -0
- package/dist/chunk-PGGWSUTM.js +33 -0
- package/dist/chunk-PGGWSUTM.js.map +1 -0
- package/dist/chunk-RDKYDMXT.js +135 -0
- package/dist/chunk-RDKYDMXT.js.map +1 -0
- package/dist/chunk-YYFPDBJG.js +63 -0
- package/dist/chunk-YYFPDBJG.js.map +1 -0
- package/dist/chunk-ZMYX5VBU.js +135 -0
- package/dist/chunk-ZMYX5VBU.js.map +1 -0
- package/dist/chunk-ZTSKS42I.js +131 -0
- package/dist/chunk-ZTSKS42I.js.map +1 -0
- package/dist/commands.d.ts +74 -0
- package/dist/commands.js +10 -0
- package/dist/commands.js.map +1 -0
- package/dist/definitions.d.ts +94 -0
- package/dist/definitions.js +15 -0
- package/dist/definitions.js.map +1 -0
- package/dist/dispatcher.d.ts +50 -0
- package/dist/dispatcher.js +8 -0
- package/dist/dispatcher.js.map +1 -0
- package/dist/durable-turn.d.ts +58 -0
- package/dist/durable-turn.js +9 -0
- package/dist/durable-turn.js.map +1 -0
- package/dist/hook-pipeline.d.ts +114 -0
- package/dist/hook-pipeline.js +13 -0
- package/dist/hook-pipeline.js.map +1 -0
- package/dist/index.d.ts +1874 -0
- package/dist/index.js +3117 -0
- package/dist/index.js.map +1 -0
- package/dist/loop.d.ts +78 -0
- package/dist/loop.js +9 -0
- package/dist/loop.js.map +1 -0
- package/dist/mcp-bridge.d.ts +48 -0
- package/dist/mcp-bridge.js +8 -0
- package/dist/mcp-bridge.js.map +1 -0
- package/dist/model.d.ts +154 -0
- package/dist/model.js +9 -0
- package/dist/model.js.map +1 -0
- package/dist/policy.d.ts +130 -0
- package/dist/policy.js +23 -0
- package/dist/policy.js.map +1 -0
- package/dist/protocol.d.ts +170 -0
- package/dist/protocol.js +15 -0
- package/dist/protocol.js.map +1 -0
- package/dist/rollout-store-persistent.d.ts +48 -0
- package/dist/rollout-store-persistent.js +9 -0
- package/dist/rollout-store-persistent.js.map +1 -0
- package/dist/rollout.d.ts +82 -0
- package/dist/rollout.js +15 -0
- package/dist/rollout.js.map +1 -0
- package/dist/sandbox.d.ts +65 -0
- package/dist/sandbox.js +15 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/skills.d.ts +93 -0
- package/dist/skills.js +10 -0
- package/dist/skills.js.map +1 -0
- package/dist/subagents.d.ts +129 -0
- package/dist/subagents.js +21 -0
- package/dist/subagents.js.map +1 -0
- package/dist/tools.d.ts +77 -0
- package/dist/tools.js +9 -0
- package/dist/tools.js.map +1 -0
- package/package.json +74 -0
- package/src/adapters/dispatcher-sse.test.ts +218 -0
- package/src/adapters/dispatcher-sse.ts +222 -0
- package/src/adapters/index.ts +18 -0
- package/src/adapters/mcp-catalog.test.ts +213 -0
- package/src/adapters/mcp-catalog.ts +188 -0
- package/src/adapters/prisma-rollout.test.ts +153 -0
- package/src/adapters/prisma-rollout.ts +104 -0
- package/src/agent-runtime.test.ts +176 -0
- package/src/artifact-stream.test.ts +330 -0
- package/src/artifact-stream.ts +453 -0
- package/src/channel-gateway.test.ts +432 -0
- package/src/channel-gateway.ts +357 -0
- package/src/code-review.test.ts +501 -0
- package/src/code-review.ts +495 -0
- package/src/command-suggestions.test.ts +251 -0
- package/src/command-suggestions.ts +338 -0
- package/src/commands.test.ts +184 -0
- package/src/commands.ts +140 -0
- package/src/commit-message.test.ts +249 -0
- package/src/commit-message.ts +180 -0
- package/src/context-compaction.test.ts +522 -0
- package/src/context-compaction.ts +434 -0
- package/src/definitions.test.ts +78 -0
- package/src/definitions.ts +190 -0
- package/src/deployment-status.test.ts +215 -0
- package/src/deployment-status.ts +227 -0
- package/src/design-context.test.ts +195 -0
- package/src/design-context.ts +198 -0
- package/src/dispatcher.test.ts +234 -0
- package/src/dispatcher.ts +189 -0
- package/src/durable-turn.test.ts +209 -0
- package/src/durable-turn.ts +135 -0
- package/src/edit-planner.test.ts +204 -0
- package/src/edit-planner.ts +325 -0
- package/src/fuzzy-match.test.ts +311 -0
- package/src/fuzzy-match.ts +444 -0
- package/src/hook-pipeline.test.ts +279 -0
- package/src/hook-pipeline.ts +373 -0
- package/src/inbound-admission.test.ts +394 -0
- package/src/inbound-admission.ts +246 -0
- package/src/index.ts +47 -0
- package/src/loop.test.ts +161 -0
- package/src/loop.ts +211 -0
- package/src/mcp-bridge.test.ts +165 -0
- package/src/mcp-bridge.ts +76 -0
- package/src/memory-provider.test.ts +232 -0
- package/src/memory-provider.ts +257 -0
- package/src/model.ts +168 -0
- package/src/permission-ruleset.test.ts +301 -0
- package/src/permission-ruleset.ts +200 -0
- package/src/policy.ts +151 -0
- package/src/project-repo.test.ts +232 -0
- package/src/project-repo.ts +311 -0
- package/src/protocol.ts +159 -0
- package/src/rollout-store-persistent.test.ts +217 -0
- package/src/rollout-store-persistent.ts +166 -0
- package/src/rollout.ts +150 -0
- package/src/sandbox.ts +113 -0
- package/src/session-share.test.ts +360 -0
- package/src/session-share.ts +310 -0
- package/src/skill-distillation.test.ts +177 -0
- package/src/skill-distillation.ts +369 -0
- package/src/skills.test.ts +277 -0
- package/src/skills.ts +255 -0
- package/src/subagents.test.ts +290 -0
- package/src/subagents.ts +332 -0
- package/src/tools.ts +126 -0
- package/src/workbench.test.ts +0 -0
- package/src/workbench.ts +0 -0
- package/tsconfig.json +12 -0
- 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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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
|