@nylorun/runtime 0.13.0-beta → 0.15.0-beta
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/CHANGELOG.md +434 -0
- package/README.md +137 -41
- package/dist/adapters/execution/restate.d.ts +7 -2
- package/dist/adapters/execution/restate.js +40 -10
- package/dist/adapters/replication/pgoutput.d.ts +21 -0
- package/dist/adapters/replication/pgoutput.js +170 -0
- package/dist/adapters/streams/s2.js +37 -20
- package/dist/admin-openapi.json +570 -0
- package/dist/api/a2a/card.d.ts +15 -0
- package/dist/api/a2a/card.js +24 -0
- package/dist/api/a2a/endpoint.d.ts +8 -0
- package/dist/api/a2a/endpoint.js +81 -0
- package/dist/api/a2a/protocol.d.ts +144 -0
- package/dist/api/a2a/protocol.js +188 -0
- package/dist/api/a2a/routes.d.ts +19 -0
- package/dist/api/a2a/routes.js +289 -0
- package/dist/api/a2a/tasks.d.ts +50 -0
- package/dist/api/a2a/tasks.js +186 -0
- package/dist/api/ag-ui/endpoint.d.ts +8 -0
- package/dist/api/ag-ui/endpoint.js +126 -0
- package/dist/api/ag-ui/history.d.ts +7 -0
- package/dist/api/ag-ui/history.js +60 -0
- package/dist/api/ag-ui/routes.d.ts +32 -0
- package/dist/api/ag-ui/routes.js +305 -0
- package/dist/api/ag-ui/session-id.d.ts +1 -0
- package/dist/api/ag-ui/session-id.js +12 -0
- package/dist/api/ag-ui/sse.d.ts +9 -0
- package/dist/api/ag-ui/sse.js +7 -0
- package/dist/api/ag-ui/translate.d.ts +38 -0
- package/dist/api/ag-ui/translate.js +222 -0
- package/dist/api/components.d.ts +8065 -0
- package/dist/api/components.js +82 -0
- package/dist/api/gate/keys.d.ts +14 -0
- package/dist/api/gate/keys.js +59 -0
- package/dist/api/gate/routes.d.ts +34 -0
- package/dist/api/gate/routes.js +235 -0
- package/dist/api/http/app.d.ts +18 -0
- package/dist/api/http/app.js +58 -0
- package/dist/api/http/body.d.ts +4 -0
- package/dist/api/http/body.js +37 -0
- package/dist/api/http/define.d.ts +39 -0
- package/dist/api/http/define.js +122 -0
- package/dist/api/http/respond.d.ts +9 -0
- package/dist/api/http/respond.js +44 -0
- package/dist/api/http/routes/access.d.ts +24 -0
- package/dist/api/http/routes/access.js +266 -0
- package/dist/api/http/routes/actions.d.ts +8 -0
- package/dist/api/http/routes/actions.js +76 -0
- package/dist/api/http/routes/endpoints.d.ts +7 -0
- package/dist/api/http/routes/endpoints.js +100 -0
- package/dist/api/http/routes/sessions.d.ts +3 -0
- package/dist/api/http/routes/sessions.js +205 -0
- package/dist/api/http/routes/tenant.d.ts +11 -0
- package/dist/api/http/routes/tenant.js +244 -0
- package/dist/api/http/routes/vaults.d.ts +8 -0
- package/dist/api/http/routes/vaults.js +167 -0
- package/dist/api/openapi.d.ts +19 -0
- package/dist/api/openapi.js +137 -0
- package/dist/api/route.d.ts +12 -0
- package/dist/api/route.js +4 -0
- package/dist/core/bearer.d.ts +8 -0
- package/dist/core/bearer.js +11 -0
- package/dist/core/flow-host.d.ts +7 -15
- package/dist/core/flow-host.js +16 -41
- package/dist/core/sandbox-routes.d.ts +12 -7
- package/dist/core/sandbox-routes.js +24 -25
- package/dist/execution/memory.d.ts +8 -5
- package/dist/execution/memory.js +33 -13
- package/dist/execution/types.d.ts +38 -6
- package/dist/execution/types.js +12 -3
- package/dist/gates/contract.d.ts +51 -0
- package/dist/gates/contract.js +37 -0
- package/dist/gates/handler.d.ts +21 -0
- package/dist/gates/handler.js +55 -0
- package/dist/gates/http-client.d.ts +12 -0
- package/dist/gates/http-client.js +160 -0
- package/dist/gates/in-process.d.ts +32 -0
- package/dist/gates/in-process.js +42 -0
- package/dist/gates/inflight.d.ts +39 -0
- package/dist/gates/inflight.js +70 -0
- package/dist/gates/mcp-handler.d.ts +35 -0
- package/dist/gates/mcp-handler.js +142 -0
- package/dist/gates/meter.d.ts +24 -0
- package/dist/gates/meter.js +165 -0
- package/dist/gates/model-gate.d.ts +45 -0
- package/dist/gates/model-gate.js +1 -0
- package/dist/gates/tenant-vaults.d.ts +64 -0
- package/dist/gates/tenant-vaults.js +94 -0
- package/dist/gates/tool-calls.d.ts +51 -0
- package/dist/gates/tool-calls.js +64 -0
- package/dist/gates/tool-client.d.ts +12 -0
- package/dist/gates/tool-client.js +201 -0
- package/dist/gates/tool-contract.d.ts +116 -0
- package/dist/gates/tool-contract.js +64 -0
- package/dist/gates/tool-gate.d.ts +45 -0
- package/dist/gates/tool-gate.js +10 -0
- package/dist/host/admin-api.d.ts +23 -0
- package/dist/host/admin-api.js +74 -0
- package/dist/host/app.d.ts +34 -0
- package/dist/host/app.js +181 -0
- package/dist/host/config-for.d.ts +3 -1
- package/dist/host/config-for.js +3 -2
- package/dist/host/config.d.ts +11 -0
- package/dist/host/cors.d.ts +27 -0
- package/dist/host/cors.js +64 -0
- package/dist/host/create-host.d.ts +36 -8
- package/dist/host/create-host.js +161 -330
- package/dist/host/execution.d.ts +20 -18
- package/dist/host/execution.js +2 -2
- package/dist/host/gates.d.ts +58 -0
- package/dist/host/gates.js +127 -0
- package/dist/host/http.d.ts +20 -4
- package/dist/host/http.js +81 -23
- package/dist/host/index.d.ts +1 -1
- package/dist/host/main.js +189 -58
- package/dist/host/stack-config.d.ts +91 -19
- package/dist/host/stack-config.js +246 -17
- package/dist/infra/database.d.ts +2 -2
- package/dist/infra/execution.d.ts +5 -5
- package/dist/infra/execution.js +8 -6
- package/dist/infra/index.d.ts +1 -1
- package/dist/infra/workers.d.ts +17 -17
- package/dist/infra/workers.js +9 -9
- package/dist/keys/client.d.ts +9 -0
- package/dist/keys/client.js +70 -0
- package/dist/keys/contract.d.ts +24 -0
- package/dist/keys/contract.js +12 -0
- package/dist/keys/keys.d.ts +41 -0
- package/dist/keys/keys.js +50 -0
- package/dist/mcp/connect.d.ts +43 -3
- package/dist/mcp/connect.js +12 -5
- package/dist/mcp/pool.d.ts +39 -3
- package/dist/mcp/pool.js +74 -11
- package/dist/model/catalog.d.ts +2 -10
- package/dist/model/classify.d.ts +13 -0
- package/dist/model/classify.js +61 -0
- package/dist/model/models.d.ts +10 -1
- package/dist/model/models.js +14 -6
- package/dist/model/pi-model.d.ts +17 -2
- package/dist/model/pi-model.js +366 -222
- package/dist/openapi.json +25889 -0
- package/dist/record/append.d.ts +51 -0
- package/dist/record/append.js +20 -0
- package/dist/record/envelope.d.ts +19 -0
- package/dist/record/envelope.js +49 -0
- package/dist/record/index.d.ts +11 -0
- package/dist/record/index.js +11 -0
- package/dist/sandbox/manager.d.ts +13 -1
- package/dist/sandbox/manager.js +8 -11
- package/dist/sandbox/resolve.d.ts +33 -0
- package/dist/sandbox/resolve.js +81 -0
- package/dist/sandbox/session-sandbox.d.ts +11 -0
- package/dist/sandbox/session-sandbox.js +51 -0
- package/dist/sandbox/share.d.ts +14 -2
- package/dist/sandbox/share.js +33 -3
- package/dist/sandbox/tenant-config.d.ts +37 -0
- package/dist/sandbox/tenant-config.js +51 -0
- package/dist/sandbox/tools.d.ts +7 -9
- package/dist/sandbox/tools.js +11 -3
- package/dist/sandbox/types.d.ts +8 -2
- package/dist/store/postgres/connect.d.ts +25 -9
- package/dist/store/postgres/connect.js +27 -10
- package/dist/store/postgres/db.d.ts +16 -0
- package/dist/store/postgres/db.js +28 -0
- package/dist/store/postgres/drizzle/0000_foundation.sql +22 -0
- package/dist/store/postgres/drizzle/0001_baseline.sql +255 -0
- package/dist/store/postgres/drizzle/0002_stream_relay.sql +4 -0
- package/dist/store/postgres/drizzle/0003_tool_crossings.sql +9 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +34 -0
- package/dist/store/postgres/migrate.d.ts +68 -0
- package/dist/store/postgres/migrate.js +183 -0
- package/dist/store/postgres/record-writer.d.ts +4 -0
- package/dist/store/postgres/record-writer.js +34 -0
- package/dist/store/postgres/record.d.ts +5 -0
- package/dist/store/postgres/record.js +60 -0
- package/dist/store/postgres/schema.d.ts +3096 -0
- package/dist/store/postgres/schema.js +363 -0
- package/dist/store/postgres/store.d.ts +2 -4
- package/dist/store/postgres/store.js +590 -434
- package/dist/store/postgres/tenant.d.ts +56 -0
- package/dist/store/postgres/tenant.js +154 -0
- package/dist/store/types.d.ts +183 -132
- package/dist/store/types.js +0 -1
- package/dist/streams/basin.d.ts +18 -1
- package/dist/streams/basin.js +34 -2
- package/dist/streams/control.d.ts +18 -0
- package/dist/streams/control.js +33 -0
- package/dist/streams/memory.d.ts +3 -1
- package/dist/streams/memory.js +14 -8
- package/dist/streams/relay/core.d.ts +35 -0
- package/dist/streams/relay/core.js +366 -0
- package/dist/streams/relay/types.d.ts +78 -0
- package/dist/streams/relay/types.js +8 -0
- package/dist/streams/types.d.ts +71 -36
- package/dist/streams/types.js +18 -53
- package/dist/tenant/access-policy.d.ts +30 -0
- package/dist/tenant/access-policy.js +52 -0
- package/dist/tenant/actions.d.ts +10 -149
- package/dist/tenant/actions.js +5 -167
- package/dist/tenant/advance.d.ts +7 -2
- package/dist/tenant/advance.js +125 -14
- package/dist/tenant/auth.d.ts +19 -17
- package/dist/tenant/auth.js +90 -93
- package/dist/tenant/browser.d.ts +15 -0
- package/dist/tenant/browser.js +39 -0
- package/dist/tenant/cause.d.ts +25 -0
- package/dist/tenant/cause.js +45 -0
- package/dist/tenant/commands.d.ts +18 -2
- package/dist/tenant/commands.js +111 -115
- package/dist/tenant/context.d.ts +101 -21
- package/dist/tenant/context.js +14 -10
- package/dist/tenant/delivery-token.d.ts +41 -0
- package/dist/tenant/delivery-token.js +104 -0
- package/dist/tenant/delivery.d.ts +62 -0
- package/dist/tenant/delivery.js +481 -0
- package/dist/tenant/effects.d.ts +20 -1
- package/dist/tenant/effects.js +159 -42
- package/dist/tenant/endpoints.d.ts +67 -0
- package/dist/tenant/endpoints.js +90 -0
- package/dist/tenant/ephemeral.d.ts +22 -7
- package/dist/tenant/ephemeral.js +83 -125
- package/dist/tenant/history.d.ts +64 -0
- package/dist/tenant/history.js +120 -0
- package/dist/tenant/http.d.ts +8 -9
- package/dist/tenant/http.js +13 -27
- package/dist/tenant/jwt.d.ts +16 -0
- package/dist/tenant/jwt.js +50 -0
- package/dist/tenant/model-setting.d.ts +2 -2
- package/dist/tenant/module.d.ts +17 -28
- package/dist/tenant/module.js +133 -279
- package/dist/tenant/outbound.d.ts +44 -0
- package/dist/tenant/outbound.js +127 -0
- package/dist/tenant/paths.d.ts +11 -16
- package/dist/tenant/paths.js +24 -68
- package/dist/tenant/pool.d.ts +1 -3
- package/dist/tenant/pool.js +1 -19
- package/dist/tenant/principals.d.ts +23 -11
- package/dist/tenant/principals.js +61 -49
- package/dist/tenant/reset.d.ts +0 -4
- package/dist/tenant/reset.js +0 -5
- package/dist/tenant/runtime.d.ts +28 -20
- package/dist/tenant/runtime.js +95 -64
- package/dist/tenant/scheduler.d.ts +10 -3
- package/dist/tenant/scheduler.js +9 -5
- package/dist/tenant/session-streams.d.ts +127 -0
- package/dist/tenant/session-streams.js +404 -0
- package/dist/tenant/sessions.d.ts +28 -4
- package/dist/tenant/sessions.js +118 -24
- package/dist/tenant/signing-keys.d.ts +55 -0
- package/dist/tenant/signing-keys.js +172 -0
- package/dist/tenant/slim.d.ts +8 -0
- package/dist/tenant/slim.js +21 -0
- package/dist/tenant/status.d.ts +3 -3
- package/dist/tenant/status.js +20 -13
- package/dist/tenant/store-pg.d.ts +9 -6
- package/dist/tenant/store-pg.js +60 -125
- package/dist/tenant/streams.d.ts +62 -85
- package/dist/tenant/streams.js +143 -226
- package/dist/tenant/subject-limits.d.ts +16 -0
- package/dist/tenant/subject-limits.js +45 -0
- package/dist/tenant/sweep.d.ts +1 -10
- package/dist/tenant/sweep.js +29 -69
- package/dist/tenant/tokens.d.ts +14 -0
- package/dist/tenant/tokens.js +180 -0
- package/dist/tenant/transcript.d.ts +48 -5
- package/dist/tenant/transcript.js +58 -1
- package/dist/tenant/types.d.ts +69 -98
- package/dist/tenant/types.js +4 -15
- package/dist/tenant/worker.d.ts +3 -1
- package/dist/tenant/worker.js +13 -7
- package/dist/vault/host-model.d.ts +31 -0
- package/dist/vault/host-model.js +57 -0
- package/dist/vault/service.d.ts +44 -17
- package/dist/vault/service.js +126 -121
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +25 -9
- package/dist/core/executors.d.ts +0 -44
- package/dist/core/executors.js +0 -95
- package/dist/store/memory.d.ts +0 -57
- package/dist/store/memory.js +0 -645
- package/dist/store/postgres/migrations/001_initial.d.ts +0 -27
- package/dist/store/postgres/migrations/001_initial.js +0 -196
- package/dist/store/postgres/migrations/002_session_owner.d.ts +0 -6
- package/dist/store/postgres/migrations/002_session_owner.js +0 -13
- package/dist/store/postgres/migrations/index.d.ts +0 -49
- package/dist/store/postgres/migrations/index.js +0 -76
- package/dist/store/postgres/names.d.ts +0 -11
- package/dist/store/postgres/names.js +0 -42
- package/dist/store/postgres/tenants.d.ts +0 -90
- package/dist/store/postgres/tenants.js +0 -214
- package/dist/streams/relay.d.ts +0 -37
- package/dist/streams/relay.js +0 -254
- package/dist/tenant/envelope.d.ts +0 -7
- package/dist/tenant/envelope.js +0 -22
- package/dist/tenant/live.d.ts +0 -92
- package/dist/tenant/live.js +0 -290
- package/dist/tenant/quarantine-error.d.ts +0 -6
- package/dist/tenant/quarantine-error.js +0 -9
- package/dist/tenant/quarantine.d.ts +0 -27
- package/dist/tenant/quarantine.js +0 -72
- package/dist/tenant/routes-tenant.d.ts +0 -12
- package/dist/tenant/routes-tenant.js +0 -137
- package/dist/tenant/routes.d.ts +0 -9
- package/dist/tenant/routes.js +0 -151
- package/dist/tenant/store-memory.d.ts +0 -16
- package/dist/tenant/store-memory.js +0 -111
- /package/dist/{store → record}/cursor.d.ts +0 -0
- /package/dist/{store → record}/cursor.js +0 -0
|
@@ -27,21 +27,31 @@
|
|
|
27
27
|
* becomes `uncertain` is settled in the Session Store and `advance` returns
|
|
28
28
|
* `done`.
|
|
29
29
|
* - **Sweeps.** `armSweep` arms one self-re-arming sweep per Tenant; arming an
|
|
30
|
-
* armed Tenant is a no-op. The sweep
|
|
31
|
-
* `runnable` sessions
|
|
30
|
+
* armed Tenant is a no-op. The sweep settles lapsed deliveries and re-wakes
|
|
31
|
+
* orphaned `runnable` sessions.
|
|
32
|
+
* - **One delivery per Action at a time.** `deliver` runs
|
|
33
|
+
* `WorkerHandlers.deliver` for the key `<tenantId>:<actionId>` at least once
|
|
34
|
+
* after it resolves. Two deliveries for one key never overlap; repeated
|
|
35
|
+
* `deliver` calls may each run it, so the handler reads the Action's state
|
|
36
|
+
* and does nothing when there is nothing to deliver.
|
|
37
|
+
* - **Retry after `retry`.** A delivery that returns `retry` is run again for
|
|
38
|
+
* the same key after `retryAfterMs`. This is how an Action endpoint that is
|
|
39
|
+
* down or busy is retried; the handler throws only on infrastructure errors.
|
|
32
40
|
*/
|
|
33
41
|
/** Why a session is woken (§12.3, "Where wakes come from"). */
|
|
34
42
|
export type WakeReason =
|
|
35
43
|
/** Session commands. */
|
|
36
44
|
"message" | "approve" | "respond"
|
|
37
|
-
/** An
|
|
45
|
+
/** An Action's outcome was recorded (a delivery's answer or a background result). */
|
|
38
46
|
| "action_result"
|
|
39
47
|
/** A linked agent turn completed, failed or was cancelled. */
|
|
40
48
|
| "linked"
|
|
41
49
|
/** Queued flow effects, pending agent effects reconciled. */
|
|
42
50
|
| "flow"
|
|
43
51
|
/** The Tenant sweep and `busy` re-wakes. */
|
|
44
|
-
| "recover"
|
|
52
|
+
| "recover"
|
|
53
|
+
/** A long turn ended a segment at a step boundary and continues in the next (Model Calls §10). */
|
|
54
|
+
| "rollover";
|
|
45
55
|
export declare const WAKE_REASONS: readonly WakeReason[];
|
|
46
56
|
export interface Wake {
|
|
47
57
|
reason: WakeReason;
|
|
@@ -59,6 +69,14 @@ export type AdvanceResult = {
|
|
|
59
69
|
status: "busy";
|
|
60
70
|
retryAfterMs: number;
|
|
61
71
|
};
|
|
72
|
+
export type DeliverResult = {
|
|
73
|
+
status: "done";
|
|
74
|
+
}
|
|
75
|
+
/** The Action is still to be delivered; run again for this key after `retryAfterMs`. */
|
|
76
|
+
| {
|
|
77
|
+
status: "retry";
|
|
78
|
+
retryAfterMs: number;
|
|
79
|
+
};
|
|
62
80
|
/** What a Worker runs when Durable Session Execution calls it. */
|
|
63
81
|
export interface WorkerHandlers {
|
|
64
82
|
/**
|
|
@@ -71,6 +89,11 @@ export interface WorkerHandlers {
|
|
|
71
89
|
sweep(tenantId: string): Promise<void>;
|
|
72
90
|
/** A durable timer set with `timer` fired. Required when `timer` is used. */
|
|
73
91
|
fire?(tenantId: string, key: string): Promise<void>;
|
|
92
|
+
/**
|
|
93
|
+
* Delivers one Action to its Action endpoint. `signal` aborts when the Worker
|
|
94
|
+
* stops. Throw only on infrastructure errors. Required when `deliver` is used.
|
|
95
|
+
*/
|
|
96
|
+
deliver?(tenantId: string, actionId: string, signal: AbortSignal): Promise<DeliverResult>;
|
|
74
97
|
}
|
|
75
98
|
export interface DurableExecution {
|
|
76
99
|
/** At-least-once. `dedupeKey` merges repeated wakes for the same cause. */
|
|
@@ -81,6 +104,12 @@ export interface DurableExecution {
|
|
|
81
104
|
* implementation can; `fire` must be idempotent either way.
|
|
82
105
|
*/
|
|
83
106
|
timer(tenantId: string, key: string, at: Date): Promise<void>;
|
|
107
|
+
/**
|
|
108
|
+
* At-least-once: `WorkerHandlers.deliver(tenantId, actionId)` runs after this
|
|
109
|
+
* resolves, never overlapping another delivery of the same Action. Send it
|
|
110
|
+
* after the transaction that made the Action pending commits.
|
|
111
|
+
*/
|
|
112
|
+
deliver(tenantId: string, actionId: string): Promise<void>;
|
|
84
113
|
/** Arms the Tenant's self-re-arming sweep. Idempotent. */
|
|
85
114
|
armSweep(tenantId: string): Promise<void>;
|
|
86
115
|
/** Stops re-arming the Tenant's sweep (Tenant deleted). A pass already running finishes. */
|
|
@@ -109,14 +138,17 @@ export interface StuckInvocation {
|
|
|
109
138
|
/** Service name without any prefix, e.g. `NylorunSession`. */
|
|
110
139
|
service: string;
|
|
111
140
|
handler: string;
|
|
112
|
-
/**
|
|
141
|
+
/**
|
|
142
|
+
* Object key: `<tenantId>:<sessionId>`, `<tenantId>`, `<tenantId>:<timer key>` or
|
|
143
|
+
* `<tenantId>:<actionId>`.
|
|
144
|
+
*/
|
|
113
145
|
key: string;
|
|
114
146
|
tenantId?: string;
|
|
115
147
|
retryCount: number;
|
|
116
148
|
lastFailure?: string;
|
|
117
149
|
modifiedAt?: string;
|
|
118
150
|
}
|
|
119
|
-
/** The key one advance at a time is serialized on. */
|
|
151
|
+
/** The key one advance (or one delivery, with an Action id) at a time is serialized on. */
|
|
120
152
|
export declare function sessionKey(tenantId: string, sessionId: string): string;
|
|
121
153
|
/** Splits a key made by `sessionKey`. Tenant ids never contain `:`. */
|
|
122
154
|
export declare function parseSessionKey(key: string): {
|
package/dist/execution/types.js
CHANGED
|
@@ -27,8 +27,16 @@
|
|
|
27
27
|
* becomes `uncertain` is settled in the Session Store and `advance` returns
|
|
28
28
|
* `done`.
|
|
29
29
|
* - **Sweeps.** `armSweep` arms one self-re-arming sweep per Tenant; arming an
|
|
30
|
-
* armed Tenant is a no-op. The sweep
|
|
31
|
-
* `runnable` sessions
|
|
30
|
+
* armed Tenant is a no-op. The sweep settles lapsed deliveries and re-wakes
|
|
31
|
+
* orphaned `runnable` sessions.
|
|
32
|
+
* - **One delivery per Action at a time.** `deliver` runs
|
|
33
|
+
* `WorkerHandlers.deliver` for the key `<tenantId>:<actionId>` at least once
|
|
34
|
+
* after it resolves. Two deliveries for one key never overlap; repeated
|
|
35
|
+
* `deliver` calls may each run it, so the handler reads the Action's state
|
|
36
|
+
* and does nothing when there is nothing to deliver.
|
|
37
|
+
* - **Retry after `retry`.** A delivery that returns `retry` is run again for
|
|
38
|
+
* the same key after `retryAfterMs`. This is how an Action endpoint that is
|
|
39
|
+
* down or busy is retried; the handler throws only on infrastructure errors.
|
|
32
40
|
*/
|
|
33
41
|
export const WAKE_REASONS = [
|
|
34
42
|
"message",
|
|
@@ -38,8 +46,9 @@ export const WAKE_REASONS = [
|
|
|
38
46
|
"linked",
|
|
39
47
|
"flow",
|
|
40
48
|
"recover",
|
|
49
|
+
"rollover",
|
|
41
50
|
];
|
|
42
|
-
/** The key one advance at a time is serialized on. */
|
|
51
|
+
/** The key one advance (or one delivery, with an Action id) at a time is serialized on. */
|
|
43
52
|
export function sessionKey(tenantId, sessionId) {
|
|
44
53
|
return `${tenantId}:${sessionId}`;
|
|
45
54
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gates service's wire format (blueprint §15, P1.1). Internal: only the loop calls it, so
|
|
3
|
+
* it is not in the published OpenAPI documents.
|
|
4
|
+
*
|
|
5
|
+
* `POST /nylorun/v1/model-calls`
|
|
6
|
+
* - Headers: `Authorization: Bearer <NYLORUN_GATES_TOKEN>`, `Idempotency-Key: <effect id>`,
|
|
7
|
+
* and optionally `Nylorun-Tenant: <tenant id>`, which must then be the database's one
|
|
8
|
+
* Tenant (a call naming another is refused).
|
|
9
|
+
* - Body: `ModelCallBody`, at most `MAX_MODEL_CALL_BYTES`.
|
|
10
|
+
* - `200 {outcome}` once the call has finished, whether the outcome is a candidate or a
|
|
11
|
+
* failure; nothing is sent while it runs. `401 gate_unauthorized` for a missing or wrong
|
|
12
|
+
* token, `400 invalid_request` for a malformed Tenant header or a bad body, `409
|
|
13
|
+
* gate_conflict` when a different request already runs under the same key.
|
|
14
|
+
* - With an `Idempotency-Key` the call outlives its client (P1.2): a re-send with the same key
|
|
15
|
+
* and body joins it, or gets its outcome for 30 minutes. Without one, a client that goes
|
|
16
|
+
* away aborts the call.
|
|
17
|
+
*
|
|
18
|
+
* `POST /nylorun/v1/model-calls/{key}/cancel` (same headers): aborts the keyed call; `204`
|
|
19
|
+
* whether or not it exists.
|
|
20
|
+
*/
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
import type { ModelGateOutcome } from "./model-gate.js";
|
|
23
|
+
export declare const MODEL_CALLS_PATH = "/nylorun/v1/model-calls";
|
|
24
|
+
/** Header naming the Tenant a call is for: optional, and checked against the gate's Tenant. */
|
|
25
|
+
export declare const TENANT_HEADER = "nylorun-tenant";
|
|
26
|
+
/** Largest request body the gate reads: one `ModelCall` with its whole projected history. */
|
|
27
|
+
export declare const MAX_MODEL_CALL_BYTES: number;
|
|
28
|
+
export declare const ModelCallBodySchema: z.ZodObject<{
|
|
29
|
+
sessionId: z.ZodString;
|
|
30
|
+
turnId: z.ZodString;
|
|
31
|
+
agentId: z.ZodString;
|
|
32
|
+
effectId: z.ZodString;
|
|
33
|
+
invocationId: z.ZodString;
|
|
34
|
+
call: z.ZodObject<{
|
|
35
|
+
prompt: z.ZodArray<z.ZodUnknown>;
|
|
36
|
+
tools: z.ZodArray<z.ZodObject<{
|
|
37
|
+
name: z.ZodString;
|
|
38
|
+
}, z.core.$loose>>;
|
|
39
|
+
}, z.core.$loose>;
|
|
40
|
+
}, z.core.$strip>;
|
|
41
|
+
export type ModelCallBody = z.infer<typeof ModelCallBodySchema>;
|
|
42
|
+
export interface ModelCallResponse {
|
|
43
|
+
readonly outcome: ModelGateOutcome;
|
|
44
|
+
}
|
|
45
|
+
/** An error answer: `{error: {code, message}}`. */
|
|
46
|
+
export interface GateErrorBody {
|
|
47
|
+
readonly error: {
|
|
48
|
+
readonly code: "gate_unauthorized" | "invalid_request" | "gate_conflict";
|
|
49
|
+
readonly message: string;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gates service's wire format (blueprint §15, P1.1). Internal: only the loop calls it, so
|
|
3
|
+
* it is not in the published OpenAPI documents.
|
|
4
|
+
*
|
|
5
|
+
* `POST /nylorun/v1/model-calls`
|
|
6
|
+
* - Headers: `Authorization: Bearer <NYLORUN_GATES_TOKEN>`, `Idempotency-Key: <effect id>`,
|
|
7
|
+
* and optionally `Nylorun-Tenant: <tenant id>`, which must then be the database's one
|
|
8
|
+
* Tenant (a call naming another is refused).
|
|
9
|
+
* - Body: `ModelCallBody`, at most `MAX_MODEL_CALL_BYTES`.
|
|
10
|
+
* - `200 {outcome}` once the call has finished, whether the outcome is a candidate or a
|
|
11
|
+
* failure; nothing is sent while it runs. `401 gate_unauthorized` for a missing or wrong
|
|
12
|
+
* token, `400 invalid_request` for a malformed Tenant header or a bad body, `409
|
|
13
|
+
* gate_conflict` when a different request already runs under the same key.
|
|
14
|
+
* - With an `Idempotency-Key` the call outlives its client (P1.2): a re-send with the same key
|
|
15
|
+
* and body joins it, or gets its outcome for 30 minutes. Without one, a client that goes
|
|
16
|
+
* away aborts the call.
|
|
17
|
+
*
|
|
18
|
+
* `POST /nylorun/v1/model-calls/{key}/cancel` (same headers): aborts the keyed call; `204`
|
|
19
|
+
* whether or not it exists.
|
|
20
|
+
*/
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
export const MODEL_CALLS_PATH = "/nylorun/v1/model-calls";
|
|
23
|
+
/** Header naming the Tenant a call is for: optional, and checked against the gate's Tenant. */
|
|
24
|
+
export const TENANT_HEADER = "nylorun-tenant";
|
|
25
|
+
/** Largest request body the gate reads: one `ModelCall` with its whole projected history. */
|
|
26
|
+
export const MAX_MODEL_CALL_BYTES = 32 * 1024 * 1024;
|
|
27
|
+
export const ModelCallBodySchema = z.object({
|
|
28
|
+
sessionId: z.string().min(1),
|
|
29
|
+
turnId: z.string().min(1),
|
|
30
|
+
agentId: z.string().min(1),
|
|
31
|
+
effectId: z.string().min(1),
|
|
32
|
+
invocationId: z.string().min(1),
|
|
33
|
+
call: z.looseObject({
|
|
34
|
+
prompt: z.array(z.unknown()),
|
|
35
|
+
tools: z.array(z.looseObject({ name: z.string() })),
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Logger } from "../tenant/types.js";
|
|
2
|
+
import type { ModelCallSettings, ModelGate, ModelGateRequest } from "./model-gate.js";
|
|
3
|
+
import { type TenantVaults } from "./tenant-vaults.js";
|
|
4
|
+
export interface ModelCallHandlerOptions {
|
|
5
|
+
readonly vaults: TenantVaults;
|
|
6
|
+
readonly logger: Logger;
|
|
7
|
+
/** Retries and timeouts; `piModel`'s defaults when absent. */
|
|
8
|
+
readonly settings?: ModelCallSettings;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A model call as the gate receives it: the Tenant is optional (the loop of a protocol 4
|
|
12
|
+
* Runtime names it; the gate serves the database's one Tenant either way).
|
|
13
|
+
*/
|
|
14
|
+
export type ModelCallRequest = Omit<ModelGateRequest, "tenantId"> & {
|
|
15
|
+
readonly tenantId?: string;
|
|
16
|
+
};
|
|
17
|
+
/** What the gate's route calls: a `ModelGate` whose request may leave the Tenant out. */
|
|
18
|
+
export interface ModelCallHandler {
|
|
19
|
+
call(request: ModelCallRequest, signal: AbortSignal): ReturnType<ModelGate["call"]>;
|
|
20
|
+
}
|
|
21
|
+
export declare function createModelCallHandler(options: ModelCallHandlerOptions): ModelCallHandler;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serves one model call in the gates service: opens the Tenant's vault (the database's one
|
|
3
|
+
* Tenant; a call naming another is refused) and calls the model
|
|
4
|
+
* through `inProcessModelGate`, the same code the loop ran before the gate, so outcomes,
|
|
5
|
+
* retries, classification and redaction are unchanged. The meter records the call's usage in
|
|
6
|
+
* the Tenant's ledger. Logs one line per call, never the prompt, the output or a credential.
|
|
7
|
+
*/
|
|
8
|
+
import { classifyThrown } from "../model/classify.js";
|
|
9
|
+
import { inProcessModelGate } from "./in-process.js";
|
|
10
|
+
import { createMeter } from "./meter.js";
|
|
11
|
+
import { GateRefusal } from "./tenant-vaults.js";
|
|
12
|
+
export function createModelCallHandler(options) {
|
|
13
|
+
const { vaults, logger, settings } = options;
|
|
14
|
+
const meter = createMeter({ logger });
|
|
15
|
+
return {
|
|
16
|
+
async call(named, signal) {
|
|
17
|
+
const started = Date.now();
|
|
18
|
+
let request = named;
|
|
19
|
+
let outcome;
|
|
20
|
+
try {
|
|
21
|
+
const vault = await vaults.open(named.tenantId);
|
|
22
|
+
const call = { ...named, tenantId: vault.tenantId };
|
|
23
|
+
request = call;
|
|
24
|
+
const gate = inProcessModelGate({
|
|
25
|
+
root: vault.root,
|
|
26
|
+
readHostModel: () => vault.readHostModel(),
|
|
27
|
+
writeHostCredential: (credential) => vault.writeHostCredential(credential),
|
|
28
|
+
...(settings ? { settings } : {}),
|
|
29
|
+
});
|
|
30
|
+
outcome = await meter.call(vault.store, call, () => gate.call(call, signal));
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (signal.aborted) {
|
|
34
|
+
logger.info("model_call", { ...fields(request), ms: Date.now() - started, outcome: "aborted" });
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
outcome = error instanceof GateRefusal ? error.outcome : classifyThrown(error);
|
|
38
|
+
}
|
|
39
|
+
logger.info("model_call", {
|
|
40
|
+
...fields(request),
|
|
41
|
+
ms: Date.now() - started,
|
|
42
|
+
outcome: typeof outcome === "object" && outcome !== null && "kind" in outcome
|
|
43
|
+
? outcome.code
|
|
44
|
+
: "ok",
|
|
45
|
+
});
|
|
46
|
+
return outcome;
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const fields = (request) => ({
|
|
51
|
+
tenant: request.tenantId,
|
|
52
|
+
session: request.sessionId,
|
|
53
|
+
effect: request.effectId,
|
|
54
|
+
...(request.call.model?.id ? { model: request.call.model.id } : {}),
|
|
55
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ModelGate } from "./model-gate.js";
|
|
2
|
+
/** Above the gate's 600 s provider request timeout. */
|
|
3
|
+
export declare const GATE_CLIENT_TIMEOUT_MS = 630000;
|
|
4
|
+
export interface HttpModelGateOptions {
|
|
5
|
+
/** The gates service, e.g. `http://gateway:4100` (`NYLORUN_GATES_URL`). */
|
|
6
|
+
readonly url: string;
|
|
7
|
+
/** `NYLORUN_GATES_TOKEN`. */
|
|
8
|
+
readonly token: string;
|
|
9
|
+
/** How long the connection may stay silent. Default `GATE_CLIENT_TIMEOUT_MS`. */
|
|
10
|
+
readonly timeoutMs?: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function httpModelGate(options: HttpModelGateOptions): ModelGate;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The loop's client of the gates service (blueprint §15, P1.1): each vault-backed model call
|
|
3
|
+
* is one POST to the gate and one JSON answer, `gates/contract.ts`. The loop never sees the
|
|
4
|
+
* model credential.
|
|
5
|
+
*
|
|
6
|
+
* Over `node:http`, not `fetch`: the gate answers only when the call has finished, and
|
|
7
|
+
* `fetch`'s default 300 s headers timeout would end every longer call. The only timeout here
|
|
8
|
+
* is the socket's idle time, above the gate's own 600 s provider timeout.
|
|
9
|
+
*
|
|
10
|
+
* A failure of the hop is a failure outcome, never a throw: `resolveEffect` marks anything
|
|
11
|
+
* thrown `uncertain`. Only an abort of the caller's signal throws. The client never retries;
|
|
12
|
+
* the gate retries the provider.
|
|
13
|
+
*/
|
|
14
|
+
import { request as httpRequest } from "node:http";
|
|
15
|
+
import { request as httpsRequest } from "node:https";
|
|
16
|
+
import { failure } from "../model/classify.js";
|
|
17
|
+
import { MODEL_CALLS_PATH, TENANT_HEADER, } from "./contract.js";
|
|
18
|
+
/** How long a cancel may take; the call it stops ends with the provider timeout anyway. */
|
|
19
|
+
const CANCEL_TIMEOUT_MS = 2_000;
|
|
20
|
+
/** Above the gate's 600 s provider request timeout. */
|
|
21
|
+
export const GATE_CLIENT_TIMEOUT_MS = 630_000;
|
|
22
|
+
export function httpModelGate(options) {
|
|
23
|
+
const endpoint = new URL(MODEL_CALLS_PATH, options.url);
|
|
24
|
+
const send = endpoint.protocol === "https:" ? httpsRequest : httpRequest;
|
|
25
|
+
const timeoutMs = options.timeoutMs ?? GATE_CLIENT_TIMEOUT_MS;
|
|
26
|
+
const where = new URL(options.url).origin;
|
|
27
|
+
return {
|
|
28
|
+
recovers: true,
|
|
29
|
+
async cancel(request) {
|
|
30
|
+
const url = new URL(`${MODEL_CALLS_PATH}/${encodeURIComponent(request.effectId)}/cancel`, options.url);
|
|
31
|
+
await new Promise((resolve) => {
|
|
32
|
+
const outgoing = send(url, {
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: {
|
|
35
|
+
authorization: `Bearer ${options.token}`,
|
|
36
|
+
[TENANT_HEADER]: request.tenantId,
|
|
37
|
+
"content-length": 0,
|
|
38
|
+
},
|
|
39
|
+
timeout: CANCEL_TIMEOUT_MS,
|
|
40
|
+
}, (response) => {
|
|
41
|
+
response.resume();
|
|
42
|
+
response.on("end", resolve);
|
|
43
|
+
response.on("error", () => resolve());
|
|
44
|
+
});
|
|
45
|
+
outgoing.on("timeout", () => outgoing.destroy());
|
|
46
|
+
outgoing.on("error", () => resolve());
|
|
47
|
+
outgoing.end();
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
async call(request, signal) {
|
|
51
|
+
signal.throwIfAborted();
|
|
52
|
+
const body = {
|
|
53
|
+
sessionId: request.sessionId,
|
|
54
|
+
turnId: request.turnId,
|
|
55
|
+
agentId: request.agentId,
|
|
56
|
+
effectId: request.effectId,
|
|
57
|
+
invocationId: request.invocationId,
|
|
58
|
+
call: request.call,
|
|
59
|
+
};
|
|
60
|
+
const payload = Buffer.from(JSON.stringify(body), "utf8");
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
let settled = false;
|
|
63
|
+
/** True once the whole request was handed to the socket. */
|
|
64
|
+
let sent = false;
|
|
65
|
+
/** True once the gate's answer started; its body settles the call. */
|
|
66
|
+
let responded = false;
|
|
67
|
+
let timedOut = false;
|
|
68
|
+
const settle = (outcome) => {
|
|
69
|
+
if (settled)
|
|
70
|
+
return;
|
|
71
|
+
settled = true;
|
|
72
|
+
signal.removeEventListener("abort", onAbort);
|
|
73
|
+
resolve(outcome);
|
|
74
|
+
};
|
|
75
|
+
const onAbort = () => {
|
|
76
|
+
if (settled)
|
|
77
|
+
return;
|
|
78
|
+
settled = true;
|
|
79
|
+
outgoing.destroy();
|
|
80
|
+
reject(signal.reason ?? new Error("aborted"));
|
|
81
|
+
};
|
|
82
|
+
const lost = (error) => settle(timedOut
|
|
83
|
+
? failure("transient", `Model gate at ${where} sent nothing for ${Math.round(timeoutMs / 1000)} s`, true)
|
|
84
|
+
: sent
|
|
85
|
+
? failure("transient", `Model gate connection lost mid-call${error ? ` (${error.message})` : ""}; the provider may have billed the call`, true)
|
|
86
|
+
: failure("transient", `Model gate unreachable at ${where}${error ? ` (${error.message})` : ""}`, true));
|
|
87
|
+
const outgoing = send(endpoint, {
|
|
88
|
+
method: "POST",
|
|
89
|
+
headers: {
|
|
90
|
+
authorization: `Bearer ${options.token}`,
|
|
91
|
+
"content-type": "application/json",
|
|
92
|
+
"content-length": payload.byteLength,
|
|
93
|
+
[TENANT_HEADER]: request.tenantId,
|
|
94
|
+
"idempotency-key": request.effectId,
|
|
95
|
+
},
|
|
96
|
+
timeout: timeoutMs,
|
|
97
|
+
}, (response) => {
|
|
98
|
+
responded = true;
|
|
99
|
+
readBody(response).then((text) => settle(answer(response.statusCode ?? 0, text)), (error) => lost(error));
|
|
100
|
+
});
|
|
101
|
+
outgoing.on("socket", (socket) => socket.setKeepAlive(true, 30_000));
|
|
102
|
+
outgoing.on("finish", () => {
|
|
103
|
+
sent = true;
|
|
104
|
+
});
|
|
105
|
+
outgoing.on("timeout", () => {
|
|
106
|
+
timedOut = true;
|
|
107
|
+
outgoing.destroy();
|
|
108
|
+
});
|
|
109
|
+
outgoing.on("error", (error) => {
|
|
110
|
+
if (signal.aborted)
|
|
111
|
+
return;
|
|
112
|
+
lost(error);
|
|
113
|
+
});
|
|
114
|
+
outgoing.on("close", () => {
|
|
115
|
+
if (!signal.aborted && !responded)
|
|
116
|
+
lost();
|
|
117
|
+
});
|
|
118
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
119
|
+
outgoing.end(payload);
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function readBody(response) {
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
const chunks = [];
|
|
127
|
+
response.on("data", (chunk) => chunks.push(chunk));
|
|
128
|
+
response.on("end", () => {
|
|
129
|
+
if (response.complete)
|
|
130
|
+
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
131
|
+
else
|
|
132
|
+
reject(new Error("response ended early"));
|
|
133
|
+
});
|
|
134
|
+
response.on("error", reject);
|
|
135
|
+
response.on("aborted", () => reject(new Error("response aborted")));
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/** The outcome a gate answer stands for. */
|
|
139
|
+
function answer(status, text) {
|
|
140
|
+
let parsed;
|
|
141
|
+
try {
|
|
142
|
+
parsed = JSON.parse(text);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
parsed = undefined;
|
|
146
|
+
}
|
|
147
|
+
if (status === 200) {
|
|
148
|
+
if (typeof parsed === "object" && parsed !== null && "outcome" in parsed)
|
|
149
|
+
return parsed.outcome;
|
|
150
|
+
return failure("transient", "Model gate answered without an outcome", false);
|
|
151
|
+
}
|
|
152
|
+
const message = parsed?.error?.message;
|
|
153
|
+
if (status === 401)
|
|
154
|
+
return failure("auth", "The model gate refused this Runtime's token: the runtime and gateway containers must share NYLORUN_GATES_TOKEN", false);
|
|
155
|
+
if (status === 409)
|
|
156
|
+
return failure("invalid_request", `The call's request changed under the same effect id${message ? `: ${message}` : ""}`, false);
|
|
157
|
+
if (status === 400)
|
|
158
|
+
return failure("invalid_request", message ?? "The model gate refused the call", false);
|
|
159
|
+
return failure("transient", `Model gate answered ${status}${message ? `: ${message}` : ""}`, true);
|
|
160
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Model Gate in the caller's own process: `piModel` with the Tenant's host model. Used
|
|
3
|
+
* when nothing separates the loop from the credential anyway (the ephemeral Runtime,
|
|
4
|
+
* embedding, tests), and by the gates service itself to serve each call.
|
|
5
|
+
*/
|
|
6
|
+
import type { Credential } from "@earendil-works/pi-ai";
|
|
7
|
+
import { type ModelCallSettings } from "../model/pi-model.js";
|
|
8
|
+
import type { SessionStore } from "../store/types.js";
|
|
9
|
+
import type { Logger } from "../tenant/types.js";
|
|
10
|
+
import type { HostModelSecret } from "../vault/service.js";
|
|
11
|
+
import type { ModelGate } from "./model-gate.js";
|
|
12
|
+
export interface InProcessModelGateOptions {
|
|
13
|
+
/** The Tenant's home: `piModel` redacts secrets it finds there from failure messages. */
|
|
14
|
+
readonly root?: string;
|
|
15
|
+
readonly readHostModel: () => Promise<HostModelSecret | undefined>;
|
|
16
|
+
/** Writes back a credential pi-ai refreshed (OAuth). */
|
|
17
|
+
readonly writeHostCredential: (credential: Credential) => Promise<void>;
|
|
18
|
+
readonly settings?: ModelCallSettings;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The in-process gate of an open Tenant: reads its host model from its own vault and records
|
|
22
|
+
* each call in its usage ledger. Only a Runtime without the gates service (embedding, the
|
|
23
|
+
* ephemeral Runtime, tests) builds one.
|
|
24
|
+
*/
|
|
25
|
+
export declare function tenantModelGate(options: {
|
|
26
|
+
readonly store: SessionStore;
|
|
27
|
+
readonly kek: () => Buffer;
|
|
28
|
+
readonly root: string;
|
|
29
|
+
readonly logger: Logger;
|
|
30
|
+
readonly settings?: ModelCallSettings;
|
|
31
|
+
}): ModelGate;
|
|
32
|
+
export declare function inProcessModelGate(options: InProcessModelGateOptions): ModelGate;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { piModel } from "../model/pi-model.js";
|
|
2
|
+
import { HostModelVault } from "../vault/host-model.js";
|
|
3
|
+
import { createMeter } from "./meter.js";
|
|
4
|
+
/**
|
|
5
|
+
* The in-process gate of an open Tenant: reads its host model from its own vault and records
|
|
6
|
+
* each call in its usage ledger. Only a Runtime without the gates service (embedding, the
|
|
7
|
+
* ephemeral Runtime, tests) builds one.
|
|
8
|
+
*/
|
|
9
|
+
export function tenantModelGate(options) {
|
|
10
|
+
const vault = new HostModelVault({ store: options.store, kek: options.kek });
|
|
11
|
+
const gate = inProcessModelGate({
|
|
12
|
+
root: options.root,
|
|
13
|
+
readHostModel: () => vault.readHostModel(),
|
|
14
|
+
writeHostCredential: (credential) => vault.updateHostCredential(credential),
|
|
15
|
+
...(options.settings ? { settings: options.settings } : {}),
|
|
16
|
+
});
|
|
17
|
+
const meter = createMeter({ logger: options.logger });
|
|
18
|
+
return {
|
|
19
|
+
call: (request, signal) => meter.call(options.store, request, () => gate.call(request, signal)),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function inProcessModelGate(options) {
|
|
23
|
+
return {
|
|
24
|
+
async call(request, signal) {
|
|
25
|
+
// One adapter per call, as before the gate: it reads the host model when invoked.
|
|
26
|
+
const adapter = piModel({
|
|
27
|
+
...(options.root !== undefined ? { root: options.root } : {}),
|
|
28
|
+
readHostModel: options.readHostModel,
|
|
29
|
+
writeHostCredential: options.writeHostCredential,
|
|
30
|
+
...(options.settings ? { settings: options.settings } : {}),
|
|
31
|
+
});
|
|
32
|
+
return adapter(request.call, {
|
|
33
|
+
// The Runtime never journals the provider request (harness `durable.ts`), and
|
|
34
|
+
// `piModel` doesn't read it.
|
|
35
|
+
request: undefined,
|
|
36
|
+
invocationId: request.invocationId,
|
|
37
|
+
signal,
|
|
38
|
+
reportPreparedCall() { },
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model calls (and, from F4.1, the remote MCP tool calls) the gates service is running or
|
|
3
|
+
* has just finished, by caller key (blueprint §15, P1.2). A call with an `Idempotency-Key` (the loop's effect id) runs under its own
|
|
4
|
+
* controller, so it outlives its client: when the runtime that sent it dies or shuts down, the
|
|
5
|
+
* runtime that takes the session over re-sends the call and joins it, or gets the outcome kept
|
|
6
|
+
* here, instead of calling the provider again.
|
|
7
|
+
*
|
|
8
|
+
* In memory, one gateway only (M2): a gateway restart loses these, and a re-sent call runs
|
|
9
|
+
* again (the ledger flags the duplicate). Outcomes are kept `ttlMs` after they settle; a call
|
|
10
|
+
* that throws (an abort) is not kept.
|
|
11
|
+
*/
|
|
12
|
+
import type { ModelGateOutcome } from "./model-gate.js";
|
|
13
|
+
/** A re-send whose request differs from the call already running under its key. */
|
|
14
|
+
export declare class InflightConflict extends Error {
|
|
15
|
+
readonly name = "InflightConflict";
|
|
16
|
+
}
|
|
17
|
+
export interface InflightCalls<T = ModelGateOutcome> {
|
|
18
|
+
/**
|
|
19
|
+
* Joins the call under `key`, or starts it with `start` under the entry's own signal.
|
|
20
|
+
* Rejects with `InflightConflict` when `hash` differs from the running call's.
|
|
21
|
+
*/
|
|
22
|
+
run(key: string, hash: string, start: (signal: AbortSignal) => Promise<T>): Promise<T>;
|
|
23
|
+
/** True while a call runs under `key`, or its outcome is still kept. */
|
|
24
|
+
has(key: string): boolean;
|
|
25
|
+
/** Aborts the call under `key` and forgets it; nothing when there is none. */
|
|
26
|
+
cancel(key: string): void;
|
|
27
|
+
/** Aborts every running call (gateway shutdown). */
|
|
28
|
+
close(): void;
|
|
29
|
+
/** Entries held: running, or settled within the TTL. */
|
|
30
|
+
readonly size: number;
|
|
31
|
+
}
|
|
32
|
+
export interface InflightCallsOptions {
|
|
33
|
+
/** How long a settled outcome is kept. Default 30 minutes. */
|
|
34
|
+
readonly ttlMs?: number;
|
|
35
|
+
/** Most entries; the oldest settled ones go first. Default 10,000. */
|
|
36
|
+
readonly max?: number;
|
|
37
|
+
readonly now?: () => number;
|
|
38
|
+
}
|
|
39
|
+
export declare function createInflightCalls<T = ModelGateOutcome>(options?: InflightCallsOptions): InflightCalls<T>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** A re-send whose request differs from the call already running under its key. */
|
|
2
|
+
export class InflightConflict extends Error {
|
|
3
|
+
name = "InflightConflict";
|
|
4
|
+
}
|
|
5
|
+
export function createInflightCalls(options = {}) {
|
|
6
|
+
const ttlMs = options.ttlMs ?? 30 * 60_000;
|
|
7
|
+
const max = options.max ?? 10_000;
|
|
8
|
+
const now = options.now ?? Date.now;
|
|
9
|
+
const entries = new Map();
|
|
10
|
+
/** Drops expired outcomes, then the oldest settled ones while over `max`. */
|
|
11
|
+
function prune() {
|
|
12
|
+
const expired = now() - ttlMs;
|
|
13
|
+
for (const [key, entry] of entries)
|
|
14
|
+
if (entry.settledAt !== undefined && entry.settledAt <= expired)
|
|
15
|
+
entries.delete(key);
|
|
16
|
+
if (entries.size <= max)
|
|
17
|
+
return;
|
|
18
|
+
for (const [key, entry] of entries) {
|
|
19
|
+
if (entries.size <= max)
|
|
20
|
+
break;
|
|
21
|
+
if (entry.settledAt !== undefined)
|
|
22
|
+
entries.delete(key);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
run(key, hash, start) {
|
|
27
|
+
prune();
|
|
28
|
+
const existing = entries.get(key);
|
|
29
|
+
if (existing) {
|
|
30
|
+
if (existing.hash !== hash)
|
|
31
|
+
return Promise.reject(new InflightConflict(`A different request is already running under ${key}`));
|
|
32
|
+
return existing.promise;
|
|
33
|
+
}
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
const promise = start(controller.signal).then((outcome) => {
|
|
36
|
+
entry.settledAt = now();
|
|
37
|
+
return outcome;
|
|
38
|
+
}, (error) => {
|
|
39
|
+
if (entries.get(key) === entry)
|
|
40
|
+
entries.delete(key);
|
|
41
|
+
throw error;
|
|
42
|
+
});
|
|
43
|
+
// Nobody may be waiting (the client went away); a rejection is not an unhandled one.
|
|
44
|
+
promise.catch(() => { });
|
|
45
|
+
const entry = { hash, controller, promise };
|
|
46
|
+
entries.set(key, entry);
|
|
47
|
+
return promise;
|
|
48
|
+
},
|
|
49
|
+
has(key) {
|
|
50
|
+
prune();
|
|
51
|
+
return entries.has(key);
|
|
52
|
+
},
|
|
53
|
+
cancel(key) {
|
|
54
|
+
const entry = entries.get(key);
|
|
55
|
+
if (!entry)
|
|
56
|
+
return;
|
|
57
|
+
entries.delete(key);
|
|
58
|
+
entry.controller.abort(new Error("The caller cancelled the call"));
|
|
59
|
+
},
|
|
60
|
+
close() {
|
|
61
|
+
for (const entry of entries.values())
|
|
62
|
+
if (entry.settledAt === undefined)
|
|
63
|
+
entry.controller.abort(new Error("The gateway is shutting down"));
|
|
64
|
+
entries.clear();
|
|
65
|
+
},
|
|
66
|
+
get size() {
|
|
67
|
+
return entries.size;
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|