@nylorun/runtime 0.13.0-beta → 0.14.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 +330 -0
- package/README.md +69 -15
- package/dist/adapters/execution/restate.d.ts +7 -2
- package/dist/adapters/execution/restate.js +38 -8
- package/dist/adapters/replication/pgoutput.d.ts +16 -0
- package/dist/adapters/replication/pgoutput.js +169 -0
- package/dist/adapters/streams/s2.js +37 -20
- package/dist/admin-openapi.json +1071 -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 +7825 -0
- package/dist/api/components.js +83 -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 +130 -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 +259 -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 +10 -0
- package/dist/api/http/routes/tenant.js +172 -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 +138 -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/host/admin-api.d.ts +22 -0
- package/dist/host/admin-api.js +178 -0
- package/dist/host/app.d.ts +34 -0
- package/dist/host/app.js +185 -0
- package/dist/host/config-for.d.ts +2 -0
- package/dist/host/config-for.js +1 -0
- 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 +31 -3
- package/dist/host/create-host.js +168 -327
- package/dist/host/http.d.ts +14 -3
- package/dist/host/http.js +64 -23
- package/dist/host/main.js +55 -4
- package/dist/host/stack-config.d.ts +20 -0
- package/dist/host/stack-config.js +55 -0
- package/dist/mcp/pool.d.ts +19 -1
- package/dist/mcp/pool.js +60 -10
- 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 +25696 -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/event.d.ts +19 -0
- package/dist/store/event.js +49 -0
- package/dist/store/memory.d.ts +20 -8
- package/dist/store/memory.js +265 -100
- package/dist/store/postgres/connect.d.ts +13 -0
- package/dist/store/postgres/connect.js +5 -0
- package/dist/store/postgres/migrations/003_subject_tokens.d.ts +8 -0
- package/dist/store/postgres/migrations/003_subject_tokens.js +42 -0
- package/dist/store/postgres/migrations/004_publishable_keys.d.ts +6 -0
- package/dist/store/postgres/migrations/004_publishable_keys.js +18 -0
- package/dist/store/postgres/migrations/005_action_endpoints.d.ts +6 -0
- package/dist/store/postgres/migrations/005_action_endpoints.js +32 -0
- package/dist/store/postgres/migrations/006_drop_executors.d.ts +11 -0
- package/dist/store/postgres/migrations/006_drop_executors.js +32 -0
- package/dist/store/postgres/migrations/007_event_time.d.ts +7 -0
- package/dist/store/postgres/migrations/007_event_time.js +14 -0
- package/dist/store/postgres/migrations/008_durable_streams.d.ts +13 -0
- package/dist/store/postgres/migrations/008_durable_streams.js +35 -0
- package/dist/store/postgres/migrations/index.d.ts +5 -1
- package/dist/store/postgres/migrations/index.js +22 -2
- package/dist/store/postgres/migrations/shared/index.d.ts +31 -0
- package/dist/store/postgres/migrations/shared/index.js +92 -0
- package/dist/store/postgres/record.d.ts +11 -0
- package/dist/store/postgres/record.js +64 -0
- package/dist/store/postgres/store.js +337 -95
- package/dist/store/postgres/tenants.js +14 -0
- package/dist/store/types.d.ts +176 -81
- 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 +29 -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/memory.d.ts +69 -0
- package/dist/streams/relay/memory.js +176 -0
- package/dist/streams/relay/types.d.ts +77 -0
- package/dist/streams/relay/types.js +7 -0
- package/dist/streams/types.d.ts +66 -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.js +32 -0
- 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/commands.d.ts +18 -2
- package/dist/tenant/commands.js +107 -113
- package/dist/tenant/context.d.ts +75 -18
- package/dist/tenant/context.js +14 -10
- package/dist/tenant/delivery-token.d.ts +41 -0
- package/dist/tenant/delivery-token.js +105 -0
- package/dist/tenant/delivery.d.ts +62 -0
- package/dist/tenant/delivery.js +476 -0
- package/dist/tenant/effects.d.ts +5 -0
- package/dist/tenant/effects.js +78 -37
- package/dist/tenant/endpoints.d.ts +67 -0
- package/dist/tenant/endpoints.js +90 -0
- package/dist/tenant/ephemeral.d.ts +6 -0
- package/dist/tenant/ephemeral.js +60 -36
- 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/module.d.ts +3 -0
- package/dist/tenant/module.js +6 -12
- package/dist/tenant/outbound.d.ts +44 -0
- package/dist/tenant/outbound.js +127 -0
- package/dist/tenant/pool.d.ts +0 -2
- package/dist/tenant/pool.js +0 -18
- package/dist/tenant/reset.d.ts +0 -4
- package/dist/tenant/reset.js +0 -5
- package/dist/tenant/runtime.d.ts +10 -20
- package/dist/tenant/runtime.js +59 -42
- package/dist/tenant/scheduler.d.ts +7 -2
- package/dist/tenant/scheduler.js +7 -3
- package/dist/tenant/session-streams.d.ts +125 -0
- package/dist/tenant/session-streams.js +399 -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 +0 -3
- package/dist/tenant/status.js +19 -12
- package/dist/tenant/streams.d.ts +60 -67
- package/dist/tenant/streams.js +140 -208
- 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 +178 -0
- package/dist/tenant/transcript.d.ts +48 -5
- package/dist/tenant/transcript.js +58 -1
- package/dist/tenant/types.d.ts +29 -7
- package/dist/tenant/worker.d.ts +3 -1
- package/dist/tenant/worker.js +9 -3
- package/dist/vault/service.d.ts +18 -2
- package/dist/vault/service.js +57 -13
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +20 -8
- package/dist/core/executors.d.ts +0 -44
- package/dist/core/executors.js +0 -95
- package/dist/streams/relay.d.ts +0 -37
- package/dist/streams/relay.js +0 -254
- package/dist/tenant/live.d.ts +0 -92
- package/dist/tenant/live.js +0 -290
- 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/host/http.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { jsonResponse } from "../api/http/respond.js";
|
|
1
3
|
import { HOST_PROTOCOL, } from "@nylorun/core/compatibility";
|
|
2
4
|
export const OPAQUE_NOT_FOUND = {
|
|
3
5
|
status: "rejected",
|
|
@@ -19,23 +21,25 @@ export class HostListenError extends Error {
|
|
|
19
21
|
export function sendJson(response, status, body) {
|
|
20
22
|
const payload = JSON.stringify(body);
|
|
21
23
|
// No Access-Control-* headers (D§11).
|
|
22
|
-
response.writeHead(status,
|
|
23
|
-
"content-type": "application/json",
|
|
24
|
-
"content-length": Buffer.byteLength(payload),
|
|
25
|
-
});
|
|
24
|
+
response.writeHead(status, jsonHeaders(payload));
|
|
26
25
|
response.end(payload);
|
|
27
26
|
}
|
|
28
|
-
|
|
27
|
+
/** `sendJson` as a Response, byte for byte (with any extra headers): the Host's answers. */
|
|
28
|
+
export { jsonResponse };
|
|
29
|
+
function jsonHeaders(payload) {
|
|
30
|
+
return {
|
|
31
|
+
"content-type": "application/json",
|
|
32
|
+
"content-length": String(Buffer.byteLength(payload)),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function rejection(code, message, details) {
|
|
29
36
|
const body = { status: "rejected", code, message };
|
|
30
37
|
if (details !== undefined)
|
|
31
38
|
body.details = details;
|
|
32
|
-
|
|
39
|
+
return body;
|
|
33
40
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
export function sendProtocolRejected(response, protocol = HOST_PROTOCOL) {
|
|
38
|
-
sendJson(response, 426, {
|
|
41
|
+
function protocolRejection(protocol) {
|
|
42
|
+
return {
|
|
39
43
|
status: "rejected",
|
|
40
44
|
code: "protocol_unsupported",
|
|
41
45
|
protocol: {
|
|
@@ -43,7 +47,19 @@ export function sendProtocolRejected(response, protocol = HOST_PROTOCOL) {
|
|
|
43
47
|
max: protocol.max,
|
|
44
48
|
features: [...protocol.features],
|
|
45
49
|
},
|
|
46
|
-
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function sendRejected(response, status, code, message, details) {
|
|
53
|
+
sendJson(response, status, rejection(code, message, details));
|
|
54
|
+
}
|
|
55
|
+
export function rejectedResponse(status, code, message, details) {
|
|
56
|
+
return jsonResponse(status, rejection(code, message, details));
|
|
57
|
+
}
|
|
58
|
+
export function opaqueNotFoundResponse() {
|
|
59
|
+
return jsonResponse(404, OPAQUE_NOT_FOUND);
|
|
60
|
+
}
|
|
61
|
+
export function protocolRejectedResponse(protocol = HOST_PROTOCOL) {
|
|
62
|
+
return jsonResponse(426, protocolRejection(protocol));
|
|
47
63
|
}
|
|
48
64
|
/**
|
|
49
65
|
* D§11 Host allowlist: loopback forms with the listening port, plus the
|
|
@@ -95,6 +111,13 @@ export function requestHasBody(request) {
|
|
|
95
111
|
const n = Number(raw);
|
|
96
112
|
return Number.isFinite(n) && n > 0;
|
|
97
113
|
}
|
|
114
|
+
/** `/health` and `/ready` are polled; the request log leaves them out. */
|
|
115
|
+
export function pathnameIsLogged(rawUrl) {
|
|
116
|
+
if (!rawUrl)
|
|
117
|
+
return true;
|
|
118
|
+
const pathname = new URL(rawUrl, "http://runtime.local").pathname;
|
|
119
|
+
return pathname !== "/health" && pathname !== "/ready";
|
|
120
|
+
}
|
|
98
121
|
/** Redact path parameters after `/v1/sessions/` for Host logs. */
|
|
99
122
|
export function redactRoutePath(pathname) {
|
|
100
123
|
const parts = pathname.split("/");
|
|
@@ -104,34 +127,52 @@ export function redactRoutePath(pathname) {
|
|
|
104
127
|
}
|
|
105
128
|
return pathname;
|
|
106
129
|
}
|
|
130
|
+
/** Constant-time comparison of SHA-256 digests of the presented and stored admin keys. */
|
|
131
|
+
export function adminKeyMatches(presented, adminKey) {
|
|
132
|
+
if (presented === undefined)
|
|
133
|
+
return false;
|
|
134
|
+
const a = createHash("sha256").update(presented, "utf8").digest();
|
|
135
|
+
const b = createHash("sha256").update(adminKey, "utf8").digest();
|
|
136
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
137
|
+
}
|
|
138
|
+
/** A request header's first value. */
|
|
139
|
+
export function headerValue(incoming, name) {
|
|
140
|
+
const raw = incoming.headers[name.toLowerCase()];
|
|
141
|
+
if (Array.isArray(raw))
|
|
142
|
+
return raw[0];
|
|
143
|
+
return raw;
|
|
144
|
+
}
|
|
107
145
|
export function readBearer(authorization) {
|
|
108
146
|
if (!authorization)
|
|
109
147
|
return undefined;
|
|
110
148
|
const match = /^Bearer\s+(\S+)/i.exec(authorization.trim());
|
|
111
149
|
return match?.[1];
|
|
112
150
|
}
|
|
151
|
+
/** A JSON request body, capped at `limit` bytes (413); `undefined` when there is none. */
|
|
113
152
|
export async function readJsonBody(request, limit = 1024 * 1024) {
|
|
153
|
+
if (request.body === null)
|
|
154
|
+
return undefined;
|
|
114
155
|
const chunks = [];
|
|
115
156
|
let size = 0;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
157
|
+
const reader = request.body.getReader();
|
|
158
|
+
for (;;) {
|
|
159
|
+
const { done, value } = await reader.read();
|
|
160
|
+
if (done)
|
|
161
|
+
break;
|
|
162
|
+
size += value.byteLength;
|
|
119
163
|
if (size > limit) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
throw error;
|
|
164
|
+
await reader.cancel();
|
|
165
|
+
throw Object.assign(new Error("Request too large"), { status: 413 });
|
|
123
166
|
}
|
|
124
|
-
chunks.push(
|
|
167
|
+
chunks.push(value);
|
|
125
168
|
}
|
|
126
|
-
if (
|
|
169
|
+
if (size === 0)
|
|
127
170
|
return undefined;
|
|
128
171
|
try {
|
|
129
172
|
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
130
173
|
}
|
|
131
174
|
catch {
|
|
132
|
-
|
|
133
|
-
error.status = 400;
|
|
134
|
-
throw error;
|
|
175
|
+
throw Object.assign(new Error("Invalid JSON"), { status: 400 });
|
|
135
176
|
}
|
|
136
177
|
}
|
|
137
178
|
export function isLoopbackHost(host) {
|
package/dist/host/main.js
CHANGED
|
@@ -8,8 +8,11 @@
|
|
|
8
8
|
* Composition: `createInfra` builds the Postgres pool, Durable Session
|
|
9
9
|
* Execution and Durable Streams from the endpoints; `createHostExecution`
|
|
10
10
|
* shares one execution across the Tenants; the Tenant store is Postgres
|
|
11
|
-
* (`NYLORUN_DATABASE_URL`, required)
|
|
12
|
-
*
|
|
11
|
+
* (`NYLORUN_DATABASE_URL`, required), with the shared record of session events
|
|
12
|
+
* (`nylorun_streams`, migrated here); with S2, an `api` or `all` process runs
|
|
13
|
+
* the stream relay, which feeds every Tenant's streams from the record over
|
|
14
|
+
* logical replication (one process at a time holds the slot); `/ready` reports
|
|
15
|
+
* the infrastructure checks. See the startup order in `main()`. Tests compose a Host without this
|
|
13
16
|
* entry, with `createHost` and an injected Tenant module.
|
|
14
17
|
*/
|
|
15
18
|
import { readFileSync, mkdirSync, existsSync } from "node:fs";
|
|
@@ -21,6 +24,10 @@ import { createTenantModule } from "../tenant/module.js";
|
|
|
21
24
|
import { createPostgresTenantStore } from "../tenant/store-pg.js";
|
|
22
25
|
import { openTenantRuntime } from "../tenant/runtime.js";
|
|
23
26
|
import { createTenantStreams, deleteTenantStreams } from "../tenant/streams.js";
|
|
27
|
+
import { createPgoutputSource } from "../adapters/replication/pgoutput.js";
|
|
28
|
+
import { assertLogicalReplication, migrateStreamsSchema, } from "../store/postgres/migrations/shared/index.js";
|
|
29
|
+
import { createPostgresRecordReader } from "../store/postgres/record.js";
|
|
30
|
+
import { createStreamRelay } from "../streams/relay/core.js";
|
|
24
31
|
import { configForFactory } from "./config-for.js";
|
|
25
32
|
import { createHost } from "./create-host.js";
|
|
26
33
|
import { createHostExecution } from "./execution.js";
|
|
@@ -46,6 +53,11 @@ function resolveHostRoot() {
|
|
|
46
53
|
const root = fromEnv && fromEnv.length > 0 ? fromEnv : resolve(homedir(), ".nylorun");
|
|
47
54
|
return resolve(root);
|
|
48
55
|
}
|
|
56
|
+
/** The slot's lag in WAL bytes, when the source can tell. */
|
|
57
|
+
async function lagOf(source) {
|
|
58
|
+
const lagBytes = await source.lag?.().catch(() => undefined);
|
|
59
|
+
return lagBytes === undefined ? {} : { lagBytes };
|
|
60
|
+
}
|
|
49
61
|
export async function main() {
|
|
50
62
|
const stack = parseStackConfig(process.env, process.argv.slice(2));
|
|
51
63
|
const hostRoot = resolveHostRoot();
|
|
@@ -89,6 +101,7 @@ export async function main() {
|
|
|
89
101
|
hostConfig: config,
|
|
90
102
|
logger,
|
|
91
103
|
baseline,
|
|
104
|
+
...(stack.delivery ? { delivery: stack.delivery } : {}),
|
|
92
105
|
});
|
|
93
106
|
// One Durable Session Execution for every Tenant this process opens: Restate
|
|
94
107
|
// when its endpoints are set, else the in-process memory execution. An
|
|
@@ -103,6 +116,28 @@ export async function main() {
|
|
|
103
116
|
// them (a local Host only) each Tenant keeps in-process streams, whose history
|
|
104
117
|
// does not survive a restart.
|
|
105
118
|
const streams = infra.streams;
|
|
119
|
+
let relayLag;
|
|
120
|
+
// The record of session events is shared by every Tenant; it exists before any opens.
|
|
121
|
+
await migrateStreamsSchema(database);
|
|
122
|
+
// With S2, the stream relay feeds every Tenant's streams from the record. Every api or all
|
|
123
|
+
// process runs one; the replication slot lets exactly one be active. Without S2, each
|
|
124
|
+
// Tenant relays its own commits to its in-process streams.
|
|
125
|
+
let relay;
|
|
126
|
+
if (streams && stack.role !== "worker") {
|
|
127
|
+
await assertLogicalReplication(database);
|
|
128
|
+
const source = createPgoutputSource({
|
|
129
|
+
connectionString: stack.endpoints.databaseUrl,
|
|
130
|
+
log: (message, fields) => logger.info(message, fields),
|
|
131
|
+
});
|
|
132
|
+
relay = createStreamRelay({
|
|
133
|
+
source,
|
|
134
|
+
record: createPostgresRecordReader(database),
|
|
135
|
+
streams,
|
|
136
|
+
log: (message, fields) => logger.info(message, fields),
|
|
137
|
+
});
|
|
138
|
+
const status = relay.status;
|
|
139
|
+
relayLag = async () => ({ ...status(), ...(await lagOf(source)) });
|
|
140
|
+
}
|
|
106
141
|
const store = createPostgresTenantStore({
|
|
107
142
|
hostRoot,
|
|
108
143
|
sql: database,
|
|
@@ -110,13 +145,14 @@ export async function main() {
|
|
|
110
145
|
logger,
|
|
111
146
|
openRuntime: (tenantConfig, opened) => openTenantRuntime(tenantConfig, {
|
|
112
147
|
execution: hostExecution.tenantExecution,
|
|
113
|
-
...(streams ? { streams } : {}),
|
|
148
|
+
...(streams ? { streams, hostRelay: true } : {}),
|
|
114
149
|
...opened,
|
|
115
150
|
}),
|
|
116
151
|
});
|
|
117
152
|
const module = createTenantModule({
|
|
118
153
|
store,
|
|
119
154
|
logger,
|
|
155
|
+
...(relayLag ? { relayStatus: relayLag } : {}),
|
|
120
156
|
// A new Tenant's basin is created with it (opening it repairs a failure);
|
|
121
157
|
// a deleted Tenant's sweep stops re-arming and its basin goes.
|
|
122
158
|
...(streams
|
|
@@ -140,11 +176,24 @@ export async function main() {
|
|
|
140
176
|
coreVersion: coreVersion(),
|
|
141
177
|
...(stack.listen ? { listen: stack.listen } : {}),
|
|
142
178
|
...(stack.publicUrl ? { publicUrl: stack.publicUrl } : {}),
|
|
179
|
+
// Container mode (the local stack) allows browsers unless told not to; a Host started
|
|
180
|
+
// from host.json only when it says so. Without publishable keys nothing is reachable.
|
|
181
|
+
browserAccess: stack.browserAccess ?? (stack.listen ? true : config.browserAccess === true),
|
|
182
|
+
// The Admin API on its own listener: from the container environment, or from host.json
|
|
183
|
+
// (loopback, on the same host) when the Host runs outside a container.
|
|
184
|
+
...(stack.operator
|
|
185
|
+
? { operator: stack.operator }
|
|
186
|
+
: !stack.listen && typeof config.adminPort === "number"
|
|
187
|
+
? { operator: { host: config.host, port: config.adminPort } }
|
|
188
|
+
: {}),
|
|
143
189
|
...(infra.readiness ? { readiness: infra.readiness } : {}),
|
|
144
190
|
// SIGTERM and POST /v1/admin/host/shutdown both close the Host this way:
|
|
145
191
|
// stop the Worker, close the Tenants, then end the infrastructure clients.
|
|
146
192
|
shutdown: {
|
|
147
|
-
beforeTenants: () =>
|
|
193
|
+
beforeTenants: async () => {
|
|
194
|
+
await hostExecution.stop();
|
|
195
|
+
await relay?.stop();
|
|
196
|
+
},
|
|
148
197
|
afterTenants: () => infra.close(),
|
|
149
198
|
},
|
|
150
199
|
};
|
|
@@ -159,10 +208,12 @@ export async function main() {
|
|
|
159
208
|
// sweep is re-armed, which recovers wakes lost with Restate's state (§14.8).
|
|
160
209
|
try {
|
|
161
210
|
await hostExecution.start();
|
|
211
|
+
relay?.start();
|
|
162
212
|
await host.listen();
|
|
163
213
|
}
|
|
164
214
|
catch (error) {
|
|
165
215
|
await hostExecution.stop().catch(() => undefined);
|
|
216
|
+
await relay?.stop().catch(() => undefined);
|
|
166
217
|
await infra.close();
|
|
167
218
|
if (error instanceof HostListenError) {
|
|
168
219
|
logger.error("listen_failed", {
|
|
@@ -54,6 +54,26 @@ export interface StackConfig {
|
|
|
54
54
|
* means nothing outside the container.
|
|
55
55
|
*/
|
|
56
56
|
publicUrl?: string;
|
|
57
|
+
/**
|
|
58
|
+
* `NYLORUN_BROWSER_ACCESS` (`on` or `off`): whether browser requests may reach Tenant
|
|
59
|
+
* routes. Absent means the Host's default (on in container mode).
|
|
60
|
+
*/
|
|
61
|
+
browserAccess?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* The operator listener in container mode (`NYLORUN_ADMIN_LISTEN_PORT`, `…_HOST`,
|
|
64
|
+
* `…_ALLOWED_HOSTS`). Absent: one listener serves the Admin API and the Tenant API.
|
|
65
|
+
*/
|
|
66
|
+
operator?: ContainerListen;
|
|
67
|
+
/**
|
|
68
|
+
* How the Runtime may call Action endpoints: `NYLORUN_ENDPOINT_LOOPBACK=docker-host` (the local
|
|
69
|
+
* stack: `localhost` means the machine that runs Docker), `NYLORUN_ENDPOINT_PRIVATE`
|
|
70
|
+
* (`allow` or `refuse`) and `NYLORUN_ENDPOINT_HTTP` (`allow` or `refuse`).
|
|
71
|
+
*/
|
|
72
|
+
delivery?: {
|
|
73
|
+
loopback?: "docker-host";
|
|
74
|
+
privateAddresses?: "allow" | "refuse";
|
|
75
|
+
allowHttp?: boolean;
|
|
76
|
+
};
|
|
57
77
|
}
|
|
58
78
|
export declare class StackConfigError extends Error {
|
|
59
79
|
constructor(message: string);
|
|
@@ -124,6 +124,30 @@ function parseListen(env) {
|
|
|
124
124
|
const allowedHosts = [...new Set([...explicit, ...loopbackForms(port)])];
|
|
125
125
|
return { host, port, allowedHosts };
|
|
126
126
|
}
|
|
127
|
+
function parseAdminListen(env) {
|
|
128
|
+
const rawPort = read(env, "NYLORUN_ADMIN_LISTEN_PORT");
|
|
129
|
+
const rawHost = read(env, "NYLORUN_ADMIN_LISTEN_HOST");
|
|
130
|
+
const rawAllowed = read(env, "NYLORUN_ADMIN_ALLOWED_HOSTS");
|
|
131
|
+
if (rawPort === undefined) {
|
|
132
|
+
if (rawHost !== undefined || rawAllowed !== undefined)
|
|
133
|
+
throw new StackConfigError("NYLORUN_ADMIN_LISTEN_PORT is required with NYLORUN_ADMIN_LISTEN_HOST or NYLORUN_ADMIN_ALLOWED_HOSTS");
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
const host = rawHost ?? DEFAULT_CONTAINER_LISTEN_HOST;
|
|
137
|
+
if (/\s|\//.test(host))
|
|
138
|
+
throw new StackConfigError(`NYLORUN_ADMIN_LISTEN_HOST is not an address: ${host}`);
|
|
139
|
+
const port = parsePort("NYLORUN_ADMIN_LISTEN_PORT", rawPort);
|
|
140
|
+
const explicit = rawAllowed === undefined
|
|
141
|
+
? []
|
|
142
|
+
: rawAllowed
|
|
143
|
+
.split(",")
|
|
144
|
+
.map((entry) => entry.trim())
|
|
145
|
+
.filter((entry) => entry !== "")
|
|
146
|
+
.map((entry) => normalizeAllowedHost("NYLORUN_ADMIN_ALLOWED_HOSTS", entry));
|
|
147
|
+
if (explicit.length === 0 && !isLoopbackAddress(host))
|
|
148
|
+
throw new StackConfigError(`NYLORUN_ADMIN_ALLOWED_HOSTS is required when NYLORUN_ADMIN_LISTEN_HOST is ${host}: list the Host headers operators send, e.g. runtime:${port},localhost:<published port>`);
|
|
149
|
+
return { host, port, allowedHosts: [...new Set([...explicit, ...loopbackForms(port)])] };
|
|
150
|
+
}
|
|
127
151
|
const IDENTITY_KEY = /^publickeyv1_[1-9A-HJ-NP-Za-km-z]{32,64}$/;
|
|
128
152
|
function parseIdentityKeys(env) {
|
|
129
153
|
const raw = read(env, "NYLORUN_RESTATE_IDENTITY_KEY");
|
|
@@ -142,6 +166,9 @@ function parseIdentityKeys(env) {
|
|
|
142
166
|
export function parseStackConfig(env, argv) {
|
|
143
167
|
const role = parseRole(argv);
|
|
144
168
|
const listen = parseListen(env);
|
|
169
|
+
const operator = parseAdminListen(env);
|
|
170
|
+
if (operator && listen && operator.port === listen.port)
|
|
171
|
+
throw new StackConfigError("NYLORUN_ADMIN_LISTEN_PORT must differ from NYLORUN_LISTEN_PORT");
|
|
145
172
|
const http = ["http:", "https:"];
|
|
146
173
|
const endpoints = {};
|
|
147
174
|
const databaseUrl = parseUrl(env, "NYLORUN_DATABASE_URL", [
|
|
@@ -177,11 +204,39 @@ export function parseStackConfig(env, argv) {
|
|
|
177
204
|
if (restateIdentityKeys)
|
|
178
205
|
endpoints.restateIdentityKeys = restateIdentityKeys;
|
|
179
206
|
const publicUrl = parseUrl(env, "NYLORUN_PUBLIC_URL", http)?.replace(/\/+$/, "");
|
|
207
|
+
const rawBrowser = read(env, "NYLORUN_BROWSER_ACCESS");
|
|
208
|
+
if (rawBrowser !== undefined && rawBrowser !== "on" && rawBrowser !== "off")
|
|
209
|
+
throw new StackConfigError(`NYLORUN_BROWSER_ACCESS must be on or off, not ${rawBrowser}`);
|
|
210
|
+
const delivery = parseDelivery(env);
|
|
180
211
|
return {
|
|
181
212
|
role,
|
|
182
213
|
...(listen ? { listen } : {}),
|
|
183
214
|
endpoints,
|
|
184
215
|
...(publicUrl ? { publicUrl } : {}),
|
|
216
|
+
...(rawBrowser === undefined ? {} : { browserAccess: rawBrowser === "on" }),
|
|
217
|
+
...(operator ? { operator } : {}),
|
|
218
|
+
...(delivery ? { delivery } : {}),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/** `StackConfig.delivery` from `NYLORUN_ENDPOINT_*`, or `undefined` when none is set. */
|
|
222
|
+
function parseDelivery(env) {
|
|
223
|
+
const loopback = read(env, "NYLORUN_ENDPOINT_LOOPBACK");
|
|
224
|
+
if (loopback !== undefined && loopback !== "docker-host")
|
|
225
|
+
throw new StackConfigError(`NYLORUN_ENDPOINT_LOOPBACK must be docker-host, not ${loopback}`);
|
|
226
|
+
const choice = (name) => {
|
|
227
|
+
const value = read(env, name);
|
|
228
|
+
if (value !== undefined && value !== "allow" && value !== "refuse")
|
|
229
|
+
throw new StackConfigError(`${name} must be allow or refuse, not ${value}`);
|
|
230
|
+
return value;
|
|
231
|
+
};
|
|
232
|
+
const privateAddresses = choice("NYLORUN_ENDPOINT_PRIVATE");
|
|
233
|
+
const http = choice("NYLORUN_ENDPOINT_HTTP");
|
|
234
|
+
if (!loopback && !privateAddresses && !http)
|
|
235
|
+
return undefined;
|
|
236
|
+
return {
|
|
237
|
+
...(loopback ? { loopback } : {}),
|
|
238
|
+
...(privateAddresses ? { privateAddresses } : {}),
|
|
239
|
+
...(http ? { allowHttp: http === "allow" } : {}),
|
|
185
240
|
};
|
|
186
241
|
}
|
|
187
242
|
/** Endpoint summary for logs: which endpoints are set, never their values. */
|
package/dist/mcp/pool.d.ts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import type { AgentManifest, McpServerManifest } from "@nylorun/core/define";
|
|
2
2
|
import type { AuthorizeResult } from "../vault/service.js";
|
|
3
|
-
import { callMcpTool } from "./connect.js";
|
|
3
|
+
import { callMcpTool, openMcpServer } from "./connect.js";
|
|
4
4
|
import { type McpDiagnostic, type McpSnapshot, type McpToolRecord } from "./snapshot.js";
|
|
5
|
+
/** A connection unused this long is closed by the sweep; the next call opens it again. */
|
|
6
|
+
export declare const DEFAULT_MCP_IDLE_MS: number;
|
|
5
7
|
export declare class McpPool {
|
|
6
8
|
private readonly options;
|
|
7
9
|
private readonly live;
|
|
10
|
+
/** Connections being opened by `call`, so concurrent calls share one. */
|
|
11
|
+
private readonly opening;
|
|
12
|
+
private readonly idleMs;
|
|
13
|
+
private readonly now;
|
|
14
|
+
private readonly open;
|
|
8
15
|
constructor(options: {
|
|
9
16
|
/** Absolute Tenant `plugin-data` directory. */
|
|
10
17
|
readonly pluginData: string;
|
|
@@ -13,6 +20,11 @@ export declare class McpPool {
|
|
|
13
20
|
url: string;
|
|
14
21
|
serverName: string;
|
|
15
22
|
}) => Promise<AuthorizeResult>;
|
|
23
|
+
/** Default `DEFAULT_MCP_IDLE_MS`. */
|
|
24
|
+
readonly idleMs?: number;
|
|
25
|
+
readonly now?: () => number;
|
|
26
|
+
/** Tests replace how a declared server is opened. */
|
|
27
|
+
readonly open?: typeof openMcpServer;
|
|
16
28
|
});
|
|
17
29
|
discover(input: {
|
|
18
30
|
sessionId: string;
|
|
@@ -41,7 +53,13 @@ export declare class McpPool {
|
|
|
41
53
|
manifest: AgentManifest;
|
|
42
54
|
pluginRoots: Readonly<Record<string, string>>;
|
|
43
55
|
}): Promise<Awaited<ReturnType<typeof callMcpTool>>>;
|
|
56
|
+
/**
|
|
57
|
+
* Closes connections unused for the idle timeout (the Tenant sweep). A session that calls
|
|
58
|
+
* one again reconnects, as after a restart; this bounds the stdio processes a Tenant keeps.
|
|
59
|
+
*/
|
|
60
|
+
sweep(): Promise<void>;
|
|
44
61
|
close(): Promise<void>;
|
|
62
|
+
/** Keeps the connection, or closes it when the server is already connected for the session. */
|
|
45
63
|
private remember;
|
|
46
64
|
private liveKey;
|
|
47
65
|
private connectDeclared;
|
package/dist/mcp/pool.js
CHANGED
|
@@ -2,11 +2,21 @@ import { join } from "node:path";
|
|
|
2
2
|
import { delegatesOf } from "@nylorun/core/define";
|
|
3
3
|
import { callMcpTool, diagnosticFromError, listMcpTools, openMcpServer, } from "./connect.js";
|
|
4
4
|
import { declaredToolNames, } from "./snapshot.js";
|
|
5
|
+
/** A connection unused this long is closed by the sweep; the next call opens it again. */
|
|
6
|
+
export const DEFAULT_MCP_IDLE_MS = 15 * 60_000;
|
|
5
7
|
export class McpPool {
|
|
6
8
|
options;
|
|
7
9
|
live = new Map();
|
|
10
|
+
/** Connections being opened by `call`, so concurrent calls share one. */
|
|
11
|
+
opening = new Map();
|
|
12
|
+
idleMs;
|
|
13
|
+
now;
|
|
14
|
+
open;
|
|
8
15
|
constructor(options) {
|
|
9
16
|
this.options = options;
|
|
17
|
+
this.idleMs = options.idleMs ?? DEFAULT_MCP_IDLE_MS;
|
|
18
|
+
this.now = options.now ?? Date.now;
|
|
19
|
+
this.open = options.open ?? openMcpServer;
|
|
10
20
|
}
|
|
11
21
|
async discover(input) {
|
|
12
22
|
// Each agent names its own tools, so collisions are checked per agent.
|
|
@@ -62,8 +72,12 @@ export class McpPool {
|
|
|
62
72
|
if (seen.has(key))
|
|
63
73
|
continue;
|
|
64
74
|
seen.add(key);
|
|
65
|
-
|
|
75
|
+
const live = this.live.get(key);
|
|
76
|
+
if (live) {
|
|
77
|
+
// The advance is about to use it: not idle.
|
|
78
|
+
live.lastUsedAt = this.now();
|
|
66
79
|
continue;
|
|
80
|
+
}
|
|
67
81
|
input.signal?.throwIfAborted();
|
|
68
82
|
const declared = findServer(input.manifest, tool.agentId, tool.capabilityId, tool.serverName);
|
|
69
83
|
if (!declared) {
|
|
@@ -92,34 +106,70 @@ export class McpPool {
|
|
|
92
106
|
const declared = findServer(input.manifest, input.agentId, input.capabilityId, input.serverName);
|
|
93
107
|
if (!declared)
|
|
94
108
|
throw new Error(`MCP server '${input.serverName}' is not declared`);
|
|
95
|
-
|
|
109
|
+
let opening = this.opening.get(key);
|
|
110
|
+
if (!opening) {
|
|
111
|
+
opening = this.connectDeclared(input, declared).finally(() => this.opening.delete(key));
|
|
112
|
+
this.opening.set(key, opening);
|
|
113
|
+
}
|
|
114
|
+
const opened = await opening;
|
|
96
115
|
if (!opened.ok)
|
|
97
116
|
throw new Error(opened.diagnostic.message);
|
|
98
|
-
this.remember(input.sessionId, declared, opened.connection);
|
|
99
|
-
|
|
117
|
+
live = this.remember(input.sessionId, declared, opened.connection);
|
|
118
|
+
}
|
|
119
|
+
live.active += 1;
|
|
120
|
+
try {
|
|
121
|
+
return await callMcpTool(live.connection.client, input.serverToolName, input.args);
|
|
100
122
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
123
|
+
finally {
|
|
124
|
+
live.active -= 1;
|
|
125
|
+
live.lastUsedAt = this.now();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Closes connections unused for the idle timeout (the Tenant sweep). A session that calls
|
|
130
|
+
* one again reconnects, as after a restart; this bounds the stdio processes a Tenant keeps.
|
|
131
|
+
*/
|
|
132
|
+
async sweep() {
|
|
133
|
+
const now = this.now();
|
|
134
|
+
const idle = [];
|
|
135
|
+
for (const [key, live] of this.live) {
|
|
136
|
+
if (live.active > 0 || now - live.lastUsedAt < this.idleMs)
|
|
137
|
+
continue;
|
|
138
|
+
this.live.delete(key);
|
|
139
|
+
idle.push(live);
|
|
140
|
+
}
|
|
141
|
+
await Promise.all(idle.map((item) => item.connection.close().catch(() => { })));
|
|
104
142
|
}
|
|
105
143
|
async close() {
|
|
106
144
|
const connections = [...this.live.values()];
|
|
107
145
|
this.live.clear();
|
|
108
146
|
await Promise.all(connections.map((item) => item.connection.close().catch(() => { })));
|
|
109
147
|
}
|
|
148
|
+
/** Keeps the connection, or closes it when the server is already connected for the session. */
|
|
110
149
|
remember(sessionId, declared, connection) {
|
|
111
|
-
this.
|
|
150
|
+
const key = this.liveKey(sessionId, declared.agentId, declared.capabilityId, declared.server.name);
|
|
151
|
+
const existing = this.live.get(key);
|
|
152
|
+
if (existing) {
|
|
153
|
+
if (existing.connection !== connection)
|
|
154
|
+
void connection.close().catch(() => { });
|
|
155
|
+
return existing;
|
|
156
|
+
}
|
|
157
|
+
const live = {
|
|
112
158
|
capabilityId: declared.capabilityId,
|
|
113
159
|
serverName: declared.server.name,
|
|
114
160
|
connection,
|
|
115
|
-
|
|
161
|
+
lastUsedAt: this.now(),
|
|
162
|
+
active: 0,
|
|
163
|
+
};
|
|
164
|
+
this.live.set(key, live);
|
|
165
|
+
return live;
|
|
116
166
|
}
|
|
117
167
|
liveKey(sessionId, agentId, capabilityId, serverName) {
|
|
118
168
|
return JSON.stringify([sessionId, agentId ?? null, capabilityId, serverName]);
|
|
119
169
|
}
|
|
120
170
|
async connectDeclared(input, declared) {
|
|
121
171
|
try {
|
|
122
|
-
const connection = await
|
|
172
|
+
const connection = await this.open({
|
|
123
173
|
server: declared.server,
|
|
124
174
|
pluginRoot: input.pluginRoots[pluginKey(declared)],
|
|
125
175
|
pluginData: join(this.options.pluginData, ...pluginKey(declared).split("/")),
|
package/dist/model/catalog.d.ts
CHANGED
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
id: string;
|
|
4
|
-
name: string;
|
|
5
|
-
models: {
|
|
6
|
-
id: string;
|
|
7
|
-
name: string;
|
|
8
|
-
}[];
|
|
9
|
-
}[];
|
|
10
|
-
};
|
|
1
|
+
import type { HostModelCatalog } from "@nylorun/core/contracts";
|
|
2
|
+
export type { HostModelCatalog };
|
|
11
3
|
/** Public provider and model names. No credentials. */
|
|
12
4
|
export declare function hostModelCatalog(): HostModelCatalog;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type AssistantMessage } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ModelFailureCode, ModelFailureOutcome } from "@nylorun/core/define";
|
|
3
|
+
/**
|
|
4
|
+
* Classify a failed pi-ai response (`stopReason: "error"`) into a failure outcome
|
|
5
|
+
* (Model Calls §6.1). Context overflow is checked first, with the binding's window so
|
|
6
|
+
* silent overflow is caught too.
|
|
7
|
+
*/
|
|
8
|
+
export declare function classifyAssistantError(message: AssistantMessage, contextWindow?: number): ModelFailureOutcome;
|
|
9
|
+
/** Classify an error a model provider threw (gateway, fixture, or a setup failure). */
|
|
10
|
+
export declare function classifyThrown(error: unknown, contextWindow?: number): ModelFailureOutcome;
|
|
11
|
+
export declare function failure(code: ModelFailureCode, message: string, retryable: boolean): ModelFailureOutcome;
|
|
12
|
+
/** A pi-ai error message, for classifying failures that did not come from a stream. */
|
|
13
|
+
export declare function errorMessage(text: string): AssistantMessage;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { isContextOverflow, isRetryableAssistantError, } from "@earendil-works/pi-ai";
|
|
2
|
+
const RATE_LIMITED = /\b429\b|rate.?limit|too many requests/i;
|
|
3
|
+
const QUOTA = /insufficient_quota|quota exceeded|out of budget|billing|usage limit/i;
|
|
4
|
+
const OVERLOADED = /overloaded|\b529\b|\b503\b|service.?unavailable|high demand/i;
|
|
5
|
+
const TIMEOUT = /timed? out|timeout/i;
|
|
6
|
+
const CONTENT_POLICY = /content.?(policy|filter|management)|safety|flagged|moderation/i;
|
|
7
|
+
const AUTH = /\b401\b|\b403\b|unauthori[sz]ed|forbidden|invalid.?api.?key|incorrect api key|authentication|not configured/i;
|
|
8
|
+
/**
|
|
9
|
+
* Classify a failed pi-ai response (`stopReason: "error"`) into a failure outcome
|
|
10
|
+
* (Model Calls §6.1). Context overflow is checked first, with the binding's window so
|
|
11
|
+
* silent overflow is caught too.
|
|
12
|
+
*/
|
|
13
|
+
export function classifyAssistantError(message, contextWindow) {
|
|
14
|
+
const text = message.errorMessage ?? `Provider stopped: ${message.stopReason}`;
|
|
15
|
+
if (isContextOverflow(message, contextWindow))
|
|
16
|
+
return failure("context_overflow", text, false);
|
|
17
|
+
if (RATE_LIMITED.test(text))
|
|
18
|
+
return failure("rate_limited", text, !QUOTA.test(text));
|
|
19
|
+
if (QUOTA.test(text))
|
|
20
|
+
return failure("rate_limited", text, false);
|
|
21
|
+
if (OVERLOADED.test(text))
|
|
22
|
+
return failure("overloaded", text, true);
|
|
23
|
+
if (TIMEOUT.test(text))
|
|
24
|
+
return failure("timeout", text, true);
|
|
25
|
+
if (CONTENT_POLICY.test(text))
|
|
26
|
+
return failure("content_policy", text, false);
|
|
27
|
+
if (AUTH.test(text))
|
|
28
|
+
return failure("auth", text, false);
|
|
29
|
+
if (isRetryableAssistantError(message))
|
|
30
|
+
return failure("transient", text, true);
|
|
31
|
+
return failure("invalid_request", text, false);
|
|
32
|
+
}
|
|
33
|
+
/** Classify an error a model provider threw (gateway, fixture, or a setup failure). */
|
|
34
|
+
export function classifyThrown(error, contextWindow) {
|
|
35
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
36
|
+
return classifyAssistantError(errorMessage(text), contextWindow);
|
|
37
|
+
}
|
|
38
|
+
export function failure(code, message, retryable) {
|
|
39
|
+
return { kind: "failed", code, message, retryable };
|
|
40
|
+
}
|
|
41
|
+
/** A pi-ai error message, for classifying failures that did not come from a stream. */
|
|
42
|
+
export function errorMessage(text) {
|
|
43
|
+
return {
|
|
44
|
+
role: "assistant",
|
|
45
|
+
content: [],
|
|
46
|
+
api: "openai-completions",
|
|
47
|
+
provider: "unknown",
|
|
48
|
+
model: "unknown",
|
|
49
|
+
usage: {
|
|
50
|
+
input: 0,
|
|
51
|
+
output: 0,
|
|
52
|
+
cacheRead: 0,
|
|
53
|
+
cacheWrite: 0,
|
|
54
|
+
totalTokens: 0,
|
|
55
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
56
|
+
},
|
|
57
|
+
stopReason: "error",
|
|
58
|
+
errorMessage: text,
|
|
59
|
+
timestamp: 0,
|
|
60
|
+
};
|
|
61
|
+
}
|
package/dist/model/models.d.ts
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
|
-
import { type CredentialStore } from "@earendil-works/pi-ai";
|
|
1
|
+
import { type AuthContext, type CredentialStore } from "@earendil-works/pi-ai";
|
|
2
2
|
export type Selection = Readonly<{
|
|
3
3
|
provider: string;
|
|
4
4
|
model: string;
|
|
5
5
|
custom?: Readonly<{
|
|
6
6
|
baseUrl: string;
|
|
7
|
+
/** Model Settings for the endpoint (Model Calls §7); defaults are deliberately small. */
|
|
8
|
+
contextWindow?: number;
|
|
9
|
+
maxTokens?: number;
|
|
10
|
+
reasoning?: boolean;
|
|
11
|
+
compat?: Readonly<Record<string, unknown>>;
|
|
7
12
|
}>;
|
|
8
13
|
}>;
|
|
14
|
+
/** A custom endpoint whose window is unknown is assumed small: an early compaction beats a failed call. */
|
|
15
|
+
export declare const CUSTOM_CONTEXT_WINDOW = 32768;
|
|
16
|
+
export declare const CUSTOM_MAX_TOKENS = 8192;
|
|
9
17
|
/**
|
|
10
18
|
* Build the pi-ai model registry from explicit credentials only.
|
|
11
19
|
* Ambient process environment / provider env auth is never consulted (A7, A10).
|
|
12
20
|
*/
|
|
13
21
|
export declare function modelsFor(selection: Selection, credentials: CredentialStore, options?: {
|
|
14
22
|
environment?: boolean;
|
|
23
|
+
authContext?: AuthContext;
|
|
15
24
|
}): import("@earendil-works/pi-ai").MutableModels;
|