@nylorun/runtime 0.25.0-beta → 0.27.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 +169 -0
- package/README.md +26 -16
- package/dist/adapters/execution/restate.d.ts +15 -18
- package/dist/adapters/execution/restate.js +31 -27
- package/dist/adapters/replication/pgoutput.d.ts +1 -1
- package/dist/adapters/replication/pgoutput.js +3 -2
- package/dist/adapters/sandbox/local.js +8 -5
- package/dist/adapters/sandbox/virtual.js +2 -2
- package/dist/adapters/streams/s2.d.ts +1 -1
- package/dist/adapters/streams/s2.js +18 -14
- package/dist/api/a2a/card.js +1 -1
- package/dist/api/a2a/endpoint.js +3 -4
- package/dist/api/a2a/protocol.js +4 -0
- package/dist/api/a2a/routes.d.ts +2 -6
- package/dist/api/a2a/routes.js +12 -18
- package/dist/api/ag-ui/endpoint.js +2 -3
- package/dist/api/ag-ui/routes.js +4 -4
- package/dist/api/components.d.ts +326 -45
- package/dist/api/components.js +5 -1
- package/dist/api/gate/keys.js +7 -4
- package/dist/api/gate/routes.d.ts +6 -2
- package/dist/api/gate/routes.js +15 -11
- package/dist/api/http/app.d.ts +2 -2
- package/dist/api/http/app.js +2 -2
- package/dist/api/http/body.d.ts +15 -3
- package/dist/api/http/body.js +15 -7
- package/dist/api/http/define.d.ts +1 -1
- package/dist/api/http/respond.js +7 -3
- package/dist/api/http/routes/access.js +5 -4
- package/dist/api/http/routes/artifacts.js +19 -7
- package/dist/api/http/routes/coverage.js +1 -1
- package/dist/api/http/routes/environment-manifests.js +1 -1
- package/dist/api/http/routes/environments.js +6 -6
- package/dist/api/http/routes/files.d.ts +2 -1
- package/dist/api/http/routes/files.js +31 -5
- package/dist/api/http/routes/mcp.js +1 -1
- package/dist/api/http/routes/reads.d.ts +35 -0
- package/dist/api/http/routes/reads.js +50 -22
- package/dist/api/http/routes/sessions.js +12 -11
- package/dist/api/http/routes/tenant.d.ts +2 -2
- package/dist/api/http/routes/tenant.js +69 -16
- package/dist/api/inbound-files.d.ts +3 -9
- package/dist/api/inbound-files.js +18 -24
- package/dist/api/openapi.js +6 -2
- package/dist/artifacts/disk.d.ts +0 -4
- package/dist/artifacts/disk.js +77 -73
- package/dist/artifacts/files.d.ts +13 -4
- package/dist/artifacts/files.js +27 -9
- package/dist/artifacts/folder-upload.d.ts +75 -0
- package/dist/artifacts/folder-upload.js +263 -0
- package/dist/artifacts/folders.d.ts +14 -8
- package/dist/artifacts/folders.js +19 -11
- package/dist/artifacts/links.js +7 -6
- package/dist/artifacts/parts.d.ts +9 -14
- package/dist/artifacts/parts.js +11 -9
- package/dist/artifacts/pipe.d.ts +15 -0
- package/dist/artifacts/pipe.js +71 -0
- package/dist/artifacts/quota.d.ts +36 -0
- package/dist/artifacts/quota.js +44 -0
- package/dist/artifacts/service.d.ts +25 -1
- package/dist/artifacts/service.js +70 -25
- package/dist/blob/fs.js +2 -1
- package/dist/blob/s3.d.ts +6 -2
- package/dist/blob/s3.js +9 -7
- package/dist/blob/types.d.ts +1 -1
- package/dist/blob/types.js +1 -1
- package/dist/core/limits.d.ts +11 -35
- package/dist/core/limits.js +5 -73
- package/dist/core/sandbox-routes.js +0 -1
- package/dist/core/turn-manifest.d.ts +1 -2
- package/dist/core/turn-manifest.js +4 -28
- package/dist/execution/memory.d.ts +8 -5
- package/dist/execution/memory.js +8 -6
- package/dist/execution/types.d.ts +1 -6
- package/dist/execution/types.js +0 -5
- package/dist/gates/contract.d.ts +3 -2
- package/dist/gates/contract.js +1 -1
- package/dist/gates/egress-credentials.js +4 -3
- package/dist/gates/egress.d.ts +9 -9
- package/dist/gates/egress.js +19 -19
- package/dist/gates/fixture-model.js +13 -2
- package/dist/gates/handler.d.ts +1 -3
- package/dist/gates/handler.js +4 -3
- package/dist/gates/http-client.d.ts +4 -3
- package/dist/gates/http-client.js +5 -7
- package/dist/gates/http-tool.d.ts +24 -4
- package/dist/gates/http-tool.js +53 -17
- package/dist/gates/in-process.d.ts +2 -1
- package/dist/gates/in-process.js +17 -9
- package/dist/gates/inflight.d.ts +2 -2
- package/dist/gates/inflight.js +3 -2
- package/dist/gates/journal.js +5 -7
- package/dist/gates/mcp-handler.d.ts +4 -3
- package/dist/gates/mcp-handler.js +15 -5
- package/dist/gates/model-gate.d.ts +11 -2
- package/dist/gates/policy.d.ts +28 -5
- package/dist/gates/policy.js +60 -17
- package/dist/gates/provider-gate.d.ts +22 -0
- package/dist/gates/provider-gate.js +30 -0
- package/dist/gates/tool-calls.d.ts +1 -3
- package/dist/gates/tool-calls.js +2 -3
- package/dist/gates/tool-client.d.ts +1 -1
- package/dist/gates/tool-client.js +5 -6
- package/dist/gates/tool-contract.d.ts +8 -5
- package/dist/gates/tool-contract.js +1 -1
- package/dist/gates/tool-gate.d.ts +18 -7
- package/dist/gates/tool-gate.js +49 -7
- package/dist/harness/calls.d.ts +27 -25
- package/dist/harness/calls.js +40 -46
- package/dist/harness/executors.d.ts +10 -3
- package/dist/harness/executors.js +53 -23
- package/dist/harness/main.js +2 -2
- package/dist/harness/pod.d.ts +0 -2
- package/dist/harness/pod.js +7 -6
- package/dist/harness/run-tokens.d.ts +4 -1
- package/dist/harness/run-tokens.js +6 -2
- package/dist/harness/service.d.ts +17 -14
- package/dist/harness/service.js +21 -35
- package/dist/harness/setup.d.ts +0 -2
- package/dist/harness/setup.js +4 -9
- package/dist/harness/ws-client.d.ts +2 -2
- package/dist/harness/ws-client.js +7 -5
- package/dist/harness/ws-port.d.ts +3 -4
- package/dist/harness/ws-port.js +3 -4
- package/dist/harness-api/artifacts.d.ts +20 -0
- package/dist/harness-api/artifacts.js +478 -0
- package/dist/harness-api/in-process.d.ts +2 -2
- package/dist/harness-api/in-process.js +18 -16
- package/dist/harness-api/record.d.ts +10 -9
- package/dist/harness-api/record.js +102 -157
- package/dist/harness-api/renew.d.ts +1 -1
- package/dist/harness-api/renew.js +2 -2
- package/dist/harness-api/server.d.ts +1 -16
- package/dist/harness-api/server.js +82 -73
- package/dist/harness-api/start.d.ts +20 -2
- package/dist/harness-api/start.js +57 -18
- package/dist/harness-api/tool-journal.d.ts +15 -10
- package/dist/harness-api/tool-journal.js +26 -24
- package/dist/harness-api/workspace.d.ts +0 -4
- package/dist/harness-api/workspace.js +2 -45
- package/dist/harness-api/ws-server.js +10 -10
- package/dist/host/create-host.js +2 -3
- package/dist/host/execution.js +2 -3
- package/dist/host/gates.d.ts +2 -9
- package/dist/host/gates.js +15 -12
- package/dist/host/http.d.ts +0 -2
- package/dist/host/http.js +0 -27
- package/dist/host/main.js +10 -3
- package/dist/host/operate.js +12 -3
- package/dist/host/stack-config.d.ts +11 -0
- package/dist/host/stack-config.js +21 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/infra/index.js +2 -1
- package/dist/infra/readiness.d.ts +0 -2
- package/dist/infra/readiness.js +4 -3
- package/dist/keys/client.d.ts +1 -0
- package/dist/keys/client.js +2 -3
- package/dist/keys/contract.d.ts +1 -3
- package/dist/keys/contract.js +1 -3
- package/dist/keys/egress-ca.d.ts +0 -2
- package/dist/keys/egress-ca.js +2 -3
- package/dist/keys/keys.js +6 -1
- package/dist/keys/x509.d.ts +8 -4
- package/dist/keys/x509.js +14 -5
- package/dist/management-openapi.json +1961 -994
- package/dist/mcp/connect.d.ts +35 -26
- package/dist/mcp/connect.js +112 -141
- package/dist/mcp/pool.d.ts +27 -22
- package/dist/mcp/pool.js +34 -48
- package/dist/mcp/preview.d.ts +13 -6
- package/dist/mcp/preview.js +37 -20
- package/dist/mcp/snapshot.d.ts +35 -86
- package/dist/mcp/snapshot.js +93 -135
- package/dist/model/auth-store.js +5 -3
- package/dist/model/capabilities.d.ts +1 -1
- package/dist/model/catalog.d.ts +9 -36
- package/dist/model/catalog.js +6 -33
- package/dist/model/descriptor.d.ts +51 -0
- package/dist/model/descriptor.js +83 -0
- package/dist/model/models.d.ts +0 -3
- package/dist/model/models.js +3 -5
- package/dist/model/native-blocks.js +1 -1
- package/dist/model/pi-model.d.ts +16 -5
- package/dist/model/pi-model.js +86 -105
- package/dist/openapi.json +866 -1218
- package/dist/reads/cursor.js +3 -1
- package/dist/record/append.d.ts +34 -2
- package/dist/record/append.js +50 -1
- package/dist/record/environment.d.ts +2 -0
- package/dist/record/environment.js +2 -0
- package/dist/record/index.d.ts +1 -1
- package/dist/record/index.js +1 -1
- package/dist/redact.d.ts +1 -1
- package/dist/redact.js +3 -2
- package/dist/sandbox/egress-token.d.ts +3 -3
- package/dist/sandbox/egress-token.js +3 -3
- package/dist/sandbox/join.js +3 -2
- package/dist/sandbox/manager.d.ts +41 -41
- package/dist/sandbox/manager.js +112 -44
- package/dist/sandbox/placement.d.ts +1 -1
- package/dist/sandbox/placement.js +5 -8
- package/dist/sandbox/pods/client.d.ts +1 -10
- package/dist/sandbox/pods/client.js +8 -4
- package/dist/sandbox/pods/lifecycle.d.ts +19 -11
- package/dist/sandbox/pods/lifecycle.js +45 -23
- package/dist/sandbox/pods/network-gate.d.ts +4 -5
- package/dist/sandbox/pods/network-gate.js +7 -7
- package/dist/sandbox/pods/reconcile.js +4 -1
- package/dist/sandbox/pods/spec.js +8 -5
- package/dist/sandbox/policy.d.ts +7 -1
- package/dist/sandbox/policy.js +10 -3
- package/dist/sandbox/resolve.d.ts +8 -3
- package/dist/sandbox/resolve.js +12 -2
- package/dist/sandbox/setup.d.ts +3 -1
- package/dist/sandbox/setup.js +34 -11
- package/dist/sandbox/share.d.ts +5 -14
- package/dist/sandbox/share.js +5 -34
- package/dist/sandbox/skills.d.ts +7 -13
- package/dist/sandbox/skills.js +16 -31
- package/dist/sandbox/tenant-config.d.ts +2 -2
- package/dist/sandbox/tenant-config.js +4 -3
- package/dist/sandbox/tools.d.ts +7 -1
- package/dist/sandbox/tools.js +26 -26
- package/dist/sandbox/types.d.ts +10 -3
- package/dist/sandbox/types.js +9 -0
- package/dist/store/postgres/connect.d.ts +6 -5
- package/dist/store/postgres/connect.js +14 -11
- package/dist/store/postgres/control.js +13 -13
- package/dist/store/postgres/drizzle/0028_artifact_write_keys.sql +2 -0
- package/dist/store/postgres/drizzle/0029_protocol_15.sql +11 -0
- package/dist/store/postgres/drizzle/0030_protocol_15_runtime_block.sql +11 -0
- package/dist/store/postgres/drizzle/0031_protocol_15_mcp_snapshot.sql +8 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
- package/dist/store/postgres/migrate.js +1 -1
- package/dist/store/postgres/reads.js +10 -2
- package/dist/store/postgres/schema.d.ts +19 -0
- package/dist/store/postgres/schema.js +7 -0
- package/dist/store/postgres/store.js +55 -26
- package/dist/store/types.d.ts +17 -7
- package/dist/streams/relay/core.d.ts +3 -3
- package/dist/streams/relay/core.js +9 -10
- package/dist/tenant/advance.js +21 -31
- package/dist/tenant/commands.js +9 -16
- package/dist/tenant/context.d.ts +14 -17
- package/dist/tenant/definition-files.d.ts +25 -4
- package/dist/tenant/definition-files.js +85 -47
- package/dist/tenant/effects.js +9 -36
- package/dist/tenant/environment-manifests.js +9 -1
- package/dist/tenant/environments.d.ts +0 -2
- package/dist/tenant/environments.js +54 -30
- package/dist/tenant/ephemeral.d.ts +1 -1
- package/dist/tenant/ephemeral.js +2 -1
- package/dist/tenant/history.d.ts +1 -1
- package/dist/tenant/history.js +1 -1
- package/dist/tenant/host-token.d.ts +3 -3
- package/dist/tenant/host-token.js +12 -11
- package/dist/tenant/http.d.ts +5 -8
- package/dist/tenant/http.js +9 -0
- package/dist/tenant/identity-file.d.ts +24 -1
- package/dist/tenant/identity-file.js +35 -7
- package/dist/tenant/issuers.d.ts +5 -5
- package/dist/tenant/issuers.js +31 -23
- package/dist/tenant/jwt.d.ts +8 -2
- package/dist/tenant/jwt.js +10 -2
- package/dist/tenant/limits.d.ts +90 -0
- package/dist/tenant/limits.js +175 -0
- package/dist/tenant/module.d.ts +4 -1
- package/dist/tenant/module.js +9 -8
- package/dist/tenant/outbound.d.ts +17 -19
- package/dist/tenant/outbound.js +18 -21
- package/dist/tenant/run-grants.js +3 -2
- package/dist/tenant/run-token.d.ts +0 -4
- package/dist/tenant/run-token.js +6 -11
- package/dist/tenant/runtime-limits.d.ts +24 -0
- package/dist/tenant/runtime-limits.js +27 -0
- package/dist/tenant/runtime.js +30 -43
- package/dist/tenant/session-streams.js +7 -6
- package/dist/tenant/session.d.ts +1 -1
- package/dist/tenant/session.js +1 -1
- package/dist/tenant/sessions.js +32 -29
- package/dist/tenant/signing-keys.d.ts +1 -1
- package/dist/tenant/signing-keys.js +5 -6
- package/dist/tenant/status.js +4 -3
- package/dist/tenant/streams.js +18 -20
- package/dist/tenant/sweep.d.ts +1 -21
- package/dist/tenant/sweep.js +39 -48
- package/dist/tenant/transcript.d.ts +70 -23
- package/dist/tenant/transcript.js +28 -22
- package/dist/tenant/types.d.ts +11 -16
- package/dist/tenant/worker.d.ts +2 -10
- package/dist/tenant/worker.js +5 -14
- package/dist/traces/ids.js +5 -1
- package/dist/traces/map.d.ts +9 -6
- package/dist/traces/map.js +9 -11
- package/dist/traces/otlp.d.ts +3 -3
- package/dist/traces/otlp.js +5 -3
- package/dist/traces/projector.d.ts +9 -4
- package/dist/traces/projector.js +13 -7
- package/dist/traces/read.js +6 -1
- package/dist/tuning.d.ts +383 -0
- package/dist/tuning.js +658 -0
- package/dist/vault/headers.js +4 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/dist/artifacts/export.d.ts +0 -8
- package/dist/artifacts/export.js +0 -204
- package/dist/artifacts/media-types.d.ts +0 -30
- package/dist/artifacts/media-types.js +0 -137
- package/dist/artifacts/workspace.d.ts +0 -60
- package/dist/artifacts/workspace.js +0 -45
- package/dist/media.d.ts +0 -10
- package/dist/media.js +0 -58
- package/dist/sandbox/runtime-tools.d.ts +0 -9
- package/dist/sandbox/runtime-tools.js +0 -16
- package/dist/sandbox/session-sandbox.d.ts +0 -16
- package/dist/sandbox/session-sandbox.js +0 -77
- package/dist/tenant/artifact-tool.d.ts +0 -13
- package/dist/tenant/artifact-tool.js +0 -243
- package/dist/tenant/skill-tool.d.ts +0 -20
- package/dist/tenant/skill-tool.js +0 -67
- package/dist/tenant/tool-results.d.ts +0 -57
- package/dist/tenant/tool-results.js +0 -298
- package/dist/tenant/tool-search.d.ts +0 -39
- package/dist/tenant/tool-search.js +0 -103
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { limitsInForce, readTenantLimits } from "../tenant/limits.js";
|
|
2
|
+
/** The quota for an artifact of `environmentId`, or of the Tenant alone when undefined. */
|
|
3
|
+
export async function artifactQuota(t, environmentId) {
|
|
4
|
+
const tenant = await readTenantLimits(t);
|
|
5
|
+
const set = environmentId === undefined ? undefined : (await t.environment(environmentId))?.applied.limits;
|
|
6
|
+
const inForce = limitsInForce(tenant, set);
|
|
7
|
+
return {
|
|
8
|
+
fileBytes: inForce.artifacts.fileBytes,
|
|
9
|
+
totals: [
|
|
10
|
+
{ limitBytes: tenant.artifacts.totalBytes },
|
|
11
|
+
...(environmentId !== undefined && set?.artifacts?.totalBytes !== undefined
|
|
12
|
+
? [{ environmentId, limitBytes: inForce.artifacts.totalBytes }]
|
|
13
|
+
: []),
|
|
14
|
+
],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** The bytes each of the quota's totals holds now. */
|
|
18
|
+
export async function artifactUsage(t, quota) {
|
|
19
|
+
const usage = [];
|
|
20
|
+
for (const total of quota.totals)
|
|
21
|
+
usage.push({
|
|
22
|
+
total,
|
|
23
|
+
usedBytes: await t.artifactBytes(total.environmentId === undefined ? undefined : { environmentId: total.environmentId }),
|
|
24
|
+
});
|
|
25
|
+
return usage;
|
|
26
|
+
}
|
|
27
|
+
/** The total with the least room left, and that room (none left: zero or less). */
|
|
28
|
+
export function tightest(usage) {
|
|
29
|
+
let best = usage[0];
|
|
30
|
+
for (const entry of usage)
|
|
31
|
+
if (entry.total.limitBytes - entry.usedBytes < best.total.limitBytes - best.usedBytes)
|
|
32
|
+
best = entry;
|
|
33
|
+
return { ...best, roomBytes: best.total.limitBytes - best.usedBytes };
|
|
34
|
+
}
|
|
35
|
+
/** Whose artifacts a total counts: `The Tenant's artifacts`, or `Environment <id>'s artifacts`. */
|
|
36
|
+
export function totalSubject(total) {
|
|
37
|
+
return total.environmentId === undefined ? "The Tenant's artifacts" : `Environment ${total.environmentId}'s artifacts`;
|
|
38
|
+
}
|
|
39
|
+
/** The Environment a session's artifacts belong to, if it names one. */
|
|
40
|
+
export async function environmentOfSession(t, sessionId) {
|
|
41
|
+
if (sessionId === null)
|
|
42
|
+
return undefined;
|
|
43
|
+
return (await t.get("sessions", sessionId))?.environmentId;
|
|
44
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { ArtifactSource, ArtifactVersionView, ArtifactView, FolderManifest
|
|
|
2
2
|
import { type BlobBody } from "../blob/index.js";
|
|
3
3
|
import type { ArtifactRow, ArtifactVersionRow, Tx } from "../store/types.js";
|
|
4
4
|
import { type SessionAccess, type TenantContext } from "../tenant/context.js";
|
|
5
|
+
import { type ArtifactQuota, type ArtifactUsage } from "./quota.js";
|
|
5
6
|
/** What the caller may reach: undefined for the whole Tenant (an application principal). */
|
|
6
7
|
export type ArtifactAccess = SessionAccess | undefined;
|
|
7
8
|
export interface UploadInput {
|
|
@@ -17,15 +18,38 @@ export interface UploadInput {
|
|
|
17
18
|
/** The body's declared length (`Content-Length`): one past the limits is refused unread. */
|
|
18
19
|
readonly declaredBytes?: number;
|
|
19
20
|
readonly source: ArtifactSource;
|
|
20
|
-
/** The turn that saved it (`
|
|
21
|
+
/** The turn that saved it (a harness's `artifact.write`), for the event. */
|
|
21
22
|
readonly turnId?: string | null;
|
|
22
23
|
/** The tool call that saved it, for the event. */
|
|
23
24
|
readonly callId?: string;
|
|
25
|
+
/**
|
|
26
|
+
* The idempotency key of a harness write (`writeKeyOf`): a version already stored with it is
|
|
27
|
+
* answered instead, and this upload's bytes are dropped. Needs `sessionId` or `artifactId`.
|
|
28
|
+
*/
|
|
29
|
+
readonly writeKey?: string;
|
|
24
30
|
}
|
|
25
31
|
export interface Uploaded {
|
|
26
32
|
readonly artifact: ArtifactView;
|
|
27
33
|
readonly version: ArtifactVersionView;
|
|
34
|
+
/** False when `writeKey` named a version an earlier write stored. */
|
|
35
|
+
readonly created: boolean;
|
|
28
36
|
}
|
|
37
|
+
/** The key a harness write of session `sessionId` is stored by: its key, scoped to the session. */
|
|
38
|
+
export declare function writeKeyOf(sessionId: string, key: string): string;
|
|
39
|
+
/** Why a write does not fit: the message, and the limit it passes. */
|
|
40
|
+
export interface ArtifactRefusal {
|
|
41
|
+
readonly message: string;
|
|
42
|
+
readonly limitBytes: number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* What one file may hold now within `quota`: the per-file cap, or what is left of the fullest
|
|
46
|
+
* total (`maxBytes`), and why a file of `size` bytes is refused (`refusal`; with no size, whether
|
|
47
|
+
* any room is left at all).
|
|
48
|
+
*/
|
|
49
|
+
export declare function artifactRoom(quota: ArtifactQuota, usage: readonly ArtifactUsage[]): {
|
|
50
|
+
readonly maxBytes: number;
|
|
51
|
+
refusal(size?: number): ArtifactRefusal | undefined;
|
|
52
|
+
};
|
|
29
53
|
export declare function versionView(row: ArtifactVersionRow): ArtifactVersionView;
|
|
30
54
|
export declare function artifactView(row: ArtifactRow, versions?: ArtifactVersionRow[]): ArtifactView;
|
|
31
55
|
/** An artifact name: 1–255 characters, no path separators or control characters. */
|
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
* (F8.2) are written by the turn-end export (`./export.ts`, `./folders.ts`) and read here.
|
|
5
5
|
*
|
|
6
6
|
* - **Uploads stream.** The body goes to the store as it arrives, at a fresh random key, counted
|
|
7
|
-
* against the per-file cap and what is left of
|
|
8
|
-
*
|
|
7
|
+
* against the per-file cap and what is left of each total it counts towards (`maxBytes`,
|
|
8
|
+
* `./quota.ts`: the Tenant's, and a session's Environment's when its manifest sets one):
|
|
9
|
+
* nothing is buffered whole, and a body past a limit stores nothing (`413 limit_exceeded`).
|
|
9
10
|
* - **Postgres is the commit point.** One transaction writes the version row and, for a session's
|
|
10
11
|
* artifact, its `artifact.created` or `artifact.version.created` event through the record
|
|
11
|
-
* module, after re-checking
|
|
12
|
+
* module, after re-checking each total under the quota lock. Until it commits the object
|
|
12
13
|
* is garbage, and a failed commit deletes it.
|
|
13
14
|
* - **Access mirrors sessions.** An application principal reaches every artifact. A request acting
|
|
14
15
|
* for a person (a trusted issuer's token or subject headers) reaches only artifacts of that person's
|
|
@@ -16,24 +17,53 @@
|
|
|
16
17
|
*
|
|
17
18
|
* Lock order, the same in every write: the session, then the quota lock, then the artifact row.
|
|
18
19
|
*/
|
|
19
|
-
import { randomUUID } from "node:crypto";
|
|
20
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
20
21
|
import { ARTIFACT_NAME_MAX } from "@nylorun/core/contracts";
|
|
21
22
|
import { isArtifactId, newArtifactId } from "@nylorun/core/compatibility";
|
|
22
23
|
import { BlobTooLargeError } from "../blob/index.js";
|
|
23
24
|
import { lockedSession, sessionOf, } from "../tenant/context.js";
|
|
24
25
|
import { fail } from "../tenant/http.js";
|
|
25
|
-
import {
|
|
26
|
+
import { artifactQuota, artifactUsage, environmentOfSession, tightest, totalSubject, } from "./quota.js";
|
|
26
27
|
import { collectContent, readManifest } from "./folders.js";
|
|
27
|
-
import { contentTypeFor } from "
|
|
28
|
+
import { contentTypeFor } from "@nylorun/core/contracts";
|
|
29
|
+
/** The key a harness write of session `sessionId` is stored by: its key, scoped to the session. */
|
|
30
|
+
export function writeKeyOf(sessionId, key) {
|
|
31
|
+
return createHash("sha256").update(`${sessionId}\n${key}`).digest("hex");
|
|
32
|
+
}
|
|
28
33
|
const notFound = () => fail(404, "Artifact not found");
|
|
29
34
|
function tooLarge(message, limit) {
|
|
30
35
|
return fail(413, message, { code: "limit_exceeded", details: { limitBytes: limit } });
|
|
31
36
|
}
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
/**
|
|
38
|
+
* What one file may hold now within `quota`: the per-file cap, or what is left of the fullest
|
|
39
|
+
* total (`maxBytes`), and why a file of `size` bytes is refused (`refusal`; with no size, whether
|
|
40
|
+
* any room is left at all).
|
|
41
|
+
*/
|
|
42
|
+
export function artifactRoom(quota, usage) {
|
|
43
|
+
const fullest = tightest(usage);
|
|
44
|
+
const maxBytes = Math.max(0, Math.min(quota.fileBytes, fullest.roomBytes));
|
|
45
|
+
return {
|
|
46
|
+
maxBytes,
|
|
47
|
+
refusal(size) {
|
|
48
|
+
if (fullest.roomBytes <= 0)
|
|
49
|
+
return {
|
|
50
|
+
message: `${totalSubject(fullest.total)} hold ${fullest.usedBytes} of ${fullest.total.limitBytes} bytes; delete some first`,
|
|
51
|
+
limitBytes: fullest.total.limitBytes,
|
|
52
|
+
};
|
|
53
|
+
if (size === undefined || size <= maxBytes)
|
|
54
|
+
return undefined;
|
|
55
|
+
if (maxBytes === quota.fileBytes)
|
|
56
|
+
return { message: `A file may hold at most ${quota.fileBytes} bytes`, limitBytes: quota.fileBytes };
|
|
57
|
+
return {
|
|
58
|
+
message: `${totalSubject(fullest.total)} may hold at most ${fullest.total.limitBytes} bytes; this file does not fit`,
|
|
59
|
+
limitBytes: fullest.total.limitBytes,
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function refused(refusal) {
|
|
65
|
+
if (refusal)
|
|
66
|
+
tooLarge(refusal.message, refusal.limitBytes);
|
|
37
67
|
}
|
|
38
68
|
export function versionView(row) {
|
|
39
69
|
return {
|
|
@@ -108,7 +138,7 @@ export async function versionOf(t, row, version) {
|
|
|
108
138
|
export async function uploadArtifact(ctx, input, body, access, signal) {
|
|
109
139
|
const name = input.artifactId === undefined ? checkArtifactName(input.name) : undefined;
|
|
110
140
|
// Where it goes and how much room is left, before a byte is stored.
|
|
111
|
-
const {
|
|
141
|
+
const { quota, usage, sessionId, fileName } = await ctx.store.tx(async (t) => {
|
|
112
142
|
let sessionId;
|
|
113
143
|
let fileName = name;
|
|
114
144
|
if (input.artifactId !== undefined) {
|
|
@@ -125,15 +155,15 @@ export async function uploadArtifact(ctx, input, body, access, signal) {
|
|
|
125
155
|
await sessionOf(t, input.sessionId, access);
|
|
126
156
|
sessionId = input.sessionId ?? null;
|
|
127
157
|
}
|
|
128
|
-
|
|
158
|
+
const quota = await artifactQuota(t, await environmentOfSession(t, sessionId));
|
|
159
|
+
return { quota, usage: await artifactUsage(t, quota), sessionId, fileName };
|
|
129
160
|
});
|
|
130
161
|
const contentType = input.contentType ?? contentTypeFor(fileName);
|
|
131
|
-
const room =
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
tooLargeFor(maxBytes, limits);
|
|
162
|
+
const room = artifactRoom(quota, usage);
|
|
163
|
+
refused(room.refusal());
|
|
164
|
+
const { maxBytes } = room;
|
|
165
|
+
if (input.declaredBytes !== undefined)
|
|
166
|
+
refused(room.refusal(input.declaredBytes));
|
|
137
167
|
const artifactId = input.artifactId ?? newArtifactId();
|
|
138
168
|
const key = `artifacts/${artifactId}/${randomUUID()}`;
|
|
139
169
|
let stored;
|
|
@@ -146,20 +176,29 @@ export async function uploadArtifact(ctx, input, body, access, signal) {
|
|
|
146
176
|
}
|
|
147
177
|
catch (error) {
|
|
148
178
|
if (error instanceof BlobTooLargeError)
|
|
149
|
-
|
|
179
|
+
refused(room.refusal(maxBytes + 1));
|
|
150
180
|
throw error;
|
|
151
181
|
}
|
|
152
182
|
const createdAt = new Date().toISOString();
|
|
183
|
+
let uploaded;
|
|
153
184
|
try {
|
|
154
|
-
|
|
185
|
+
uploaded = await ctx.store.tx(async (t) => {
|
|
155
186
|
// The session first, then the quota, then the artifact (the module's lock order).
|
|
156
187
|
let session;
|
|
157
188
|
if (sessionId !== null)
|
|
158
189
|
session = await lockedSession(t, sessionId, access);
|
|
190
|
+
// A harness's write repeated: the session's lock orders it after the first, whose version
|
|
191
|
+
// it answers.
|
|
192
|
+
const known = input.writeKey === undefined ? undefined : await t.artifactVersionByWriteKey(input.writeKey);
|
|
193
|
+
if (known) {
|
|
194
|
+
const row = await t.artifact(known.artifactId);
|
|
195
|
+
if (row)
|
|
196
|
+
return { artifact: artifactView(row), version: versionView(known), created: false };
|
|
197
|
+
}
|
|
159
198
|
await t.lockArtifactQuota();
|
|
160
|
-
const total
|
|
161
|
-
|
|
162
|
-
|
|
199
|
+
for (const { total, usedBytes } of await artifactUsage(t, quota))
|
|
200
|
+
if (usedBytes + stored.size > total.limitBytes)
|
|
201
|
+
tooLarge(`${totalSubject(total)} may hold at most ${total.limitBytes} bytes; this file does not fit`, total.limitBytes);
|
|
163
202
|
let row;
|
|
164
203
|
let version;
|
|
165
204
|
if (input.artifactId !== undefined) {
|
|
@@ -173,6 +212,7 @@ export async function uploadArtifact(ctx, input, body, access, signal) {
|
|
|
173
212
|
contentType,
|
|
174
213
|
source: input.source,
|
|
175
214
|
createdAt,
|
|
215
|
+
writeKey: input.writeKey ?? null,
|
|
176
216
|
};
|
|
177
217
|
await t.insertArtifactVersion(version);
|
|
178
218
|
row = {
|
|
@@ -206,6 +246,7 @@ export async function uploadArtifact(ctx, input, body, access, signal) {
|
|
|
206
246
|
contentType,
|
|
207
247
|
source: input.source,
|
|
208
248
|
createdAt,
|
|
249
|
+
writeKey: input.writeKey ?? null,
|
|
209
250
|
};
|
|
210
251
|
await t.insertArtifact(row, version);
|
|
211
252
|
}
|
|
@@ -221,7 +262,7 @@ export async function uploadArtifact(ctx, input, body, access, signal) {
|
|
|
221
262
|
source: version.source,
|
|
222
263
|
...(input.callId === undefined ? {} : { callId: input.callId }),
|
|
223
264
|
});
|
|
224
|
-
return { artifact: artifactView(row), version: versionView(version) };
|
|
265
|
+
return { artifact: artifactView(row), version: versionView(version), created: true };
|
|
225
266
|
});
|
|
226
267
|
}
|
|
227
268
|
catch (error) {
|
|
@@ -229,6 +270,10 @@ export async function uploadArtifact(ctx, input, body, access, signal) {
|
|
|
229
270
|
await ctx.blobs.delete(key).catch(() => undefined);
|
|
230
271
|
throw error;
|
|
231
272
|
}
|
|
273
|
+
// A repeated write stored nothing of its own: its object is garbage too.
|
|
274
|
+
if (!uploaded.created)
|
|
275
|
+
await ctx.blobs.delete(key).catch(() => undefined);
|
|
276
|
+
return uploaded;
|
|
232
277
|
}
|
|
233
278
|
/** The caller's artifacts: of one session, or of every session it reaches. */
|
|
234
279
|
export async function listArtifacts(ctx, filter, access) {
|
package/dist/blob/fs.js
CHANGED
|
@@ -2,7 +2,8 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { mkdir, open, readFile, readdir, rename, rm, stat, unlink } from "node:fs/promises";
|
|
3
3
|
import { dirname, join, relative, sep } from "node:path";
|
|
4
4
|
import { Readable } from "node:stream";
|
|
5
|
-
import {
|
|
5
|
+
import { DEFAULT_CONTENT_TYPE } from "@nylorun/core/contracts";
|
|
6
|
+
import { BlobRangeError, BlobTooLargeError, assertBlobKey, assertBlobPrefix, assertMaxBytes, assertRange, chunksOf, } from "./types.js";
|
|
6
7
|
function isMissing(error) {
|
|
7
8
|
const code = error?.code;
|
|
8
9
|
return code === "ENOENT" || code === "ENOTDIR";
|
package/dist/blob/s3.d.ts
CHANGED
|
@@ -14,10 +14,14 @@ export interface S3BlobStoreOptions {
|
|
|
14
14
|
forcePathStyle?: boolean;
|
|
15
15
|
/**
|
|
16
16
|
* A body longer than this is uploaded in parts of about this size (multipart upload); a
|
|
17
|
-
* shorter one in one request. At least 5 MiB, the S3 minimum. Default
|
|
17
|
+
* shorter one in one request. At least 5 MiB, the S3 minimum (`S3_MIN_PART_BYTES`). Default
|
|
18
|
+
* `objectStore.partSizeBytes` (Host tuning, 8 MiB).
|
|
18
19
|
*/
|
|
19
20
|
partSize?: number;
|
|
20
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Attempts for a request that fails with a network error, 429 or 5xx. Default
|
|
23
|
+
* `objectStore.attempts` (3); the wait after a failed one is `objectStore.retryBaseMs`, doubled.
|
|
24
|
+
*/
|
|
21
25
|
attempts?: number;
|
|
22
26
|
/** Replaces the global `fetch` (tests). */
|
|
23
27
|
fetch?: typeof fetch;
|
package/dist/blob/s3.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { AwsClient } from "aws4fetch";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import { hostTuning, S3_MIN_PART_BYTES } from "../tuning.js";
|
|
4
|
+
import { DEFAULT_CONTENT_TYPE } from "@nylorun/core/contracts";
|
|
5
|
+
import { BlobRangeError, BlobStoreError, BlobTooLargeError, assertBlobKey, assertBlobPrefix, assertMaxBytes, assertRange, chunksOf, } from "./types.js";
|
|
6
6
|
function sha256Hex(bytes) {
|
|
7
7
|
return createHash("sha256").update(bytes).digest("hex");
|
|
8
8
|
}
|
|
@@ -57,8 +57,10 @@ export function createS3BlobStore(options) {
|
|
|
57
57
|
const bucketUrl = pathStyle
|
|
58
58
|
? `${base}/${options.bucket}`
|
|
59
59
|
: `${endpoint.protocol}//${options.bucket}.${endpoint.host}${endpoint.pathname.replace(/\/+$/, "")}`;
|
|
60
|
-
const
|
|
61
|
-
const
|
|
60
|
+
const tuning = hostTuning().objectStore;
|
|
61
|
+
const partSize = Math.max(options.partSize ?? tuning.partSizeBytes, S3_MIN_PART_BYTES);
|
|
62
|
+
const attempts = Math.max(1, options.attempts ?? tuning.attempts);
|
|
63
|
+
const pause = (attempt) => new Promise((resolve) => setTimeout(resolve, tuning.retryBaseMs * 2 ** (attempt - 1)));
|
|
62
64
|
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
63
65
|
const client = new AwsClient({
|
|
64
66
|
accessKeyId: options.accessKeyId,
|
|
@@ -81,7 +83,7 @@ export function createS3BlobStore(options) {
|
|
|
81
83
|
const response = await fetchImpl(await client.sign(url, init));
|
|
82
84
|
if ((response.status >= 500 || response.status === 429) && attempt < attempts && !once) {
|
|
83
85
|
await response.body?.cancel();
|
|
84
|
-
await
|
|
86
|
+
await pause(attempt);
|
|
85
87
|
continue;
|
|
86
88
|
}
|
|
87
89
|
return response;
|
|
@@ -91,7 +93,7 @@ export function createS3BlobStore(options) {
|
|
|
91
93
|
throw error;
|
|
92
94
|
lastError = error;
|
|
93
95
|
if (attempt < (once ? 1 : attempts))
|
|
94
|
-
await
|
|
96
|
+
await pause(attempt);
|
|
95
97
|
}
|
|
96
98
|
}
|
|
97
99
|
throw new BlobStoreError(`The Object store is unreachable: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
|
package/dist/blob/types.d.ts
CHANGED
|
@@ -93,7 +93,7 @@ export declare class BlobRangeError extends Error {
|
|
|
93
93
|
readonly size: number;
|
|
94
94
|
constructor(range: BlobRange, size: number);
|
|
95
95
|
}
|
|
96
|
-
|
|
96
|
+
/** The longest object key S3 takes: 1024 bytes of UTF-8. A constant of the S3 API, not a setting. */
|
|
97
97
|
export declare const MAX_KEY_BYTES = 1024;
|
|
98
98
|
/**
|
|
99
99
|
* Keys are `/`-separated segments of `A-Z a-z 0-9 . _ -`, neither `.` nor `..`, at most 1024
|
package/dist/blob/types.js
CHANGED
|
@@ -41,7 +41,7 @@ export class BlobRangeError extends Error {
|
|
|
41
41
|
this.name = "BlobRangeError";
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
-
|
|
44
|
+
/** The longest object key S3 takes: 1024 bytes of UTF-8. A constant of the S3 API, not a setting. */
|
|
45
45
|
export const MAX_KEY_BYTES = 1024;
|
|
46
46
|
const SEGMENT = /^[A-Za-z0-9._-]+$/;
|
|
47
47
|
/**
|
package/dist/core/limits.d.ts
CHANGED
|
@@ -1,40 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Flow limits (`workflows.md` §13): the Tenant's Runtime limits `flowConcurrency`, `flowMapItems`
|
|
3
|
+
* and `flowLoopIterations` (`PUT /v1/tenant/limits`), read where they are checked. Core holds a
|
|
4
|
+
* flow turn to `flowConcurrency` when it dispatches effects; the flow engine checks the other two,
|
|
5
|
+
* which a flow's `turn.start` carries (`flowLimitsOf`).
|
|
4
6
|
*/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
+
import type { RuntimeLimits } from "@nylorun/core/contracts";
|
|
8
|
+
/** What a flow's `turn.start.options.flowLimits` carries to the flow engine. */
|
|
9
|
+
export interface FlowEngineLimits {
|
|
7
10
|
readonly maxMapItems: number;
|
|
8
11
|
readonly maxLoopIterations: number;
|
|
9
|
-
};
|
|
10
|
-
export declare const FLOW_LIMIT_DEFAULTS: FlowLimits;
|
|
11
|
-
export declare const FLOW_LIMIT_ENV: {
|
|
12
|
-
readonly maxConcurrency: "NYLORUN_FLOW_MAX_CONCURRENCY";
|
|
13
|
-
readonly maxMapItems: "NYLORUN_FLOW_MAX_MAP_ITEMS";
|
|
14
|
-
readonly maxLoopIterations: "NYLORUN_FLOW_MAX_LOOP_ITERATIONS";
|
|
15
|
-
};
|
|
16
|
-
export type FlowLimitErrorCode = "map.too-many-items" | "loop.too-many-iterations";
|
|
17
|
-
export declare class FlowLimitError extends Error {
|
|
18
|
-
readonly code: FlowLimitErrorCode;
|
|
19
|
-
readonly path?: string;
|
|
20
|
-
constructor(code: FlowLimitErrorCode, message: string, path?: string);
|
|
21
12
|
}
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
export declare function resolveFlowLimits(input?: {
|
|
27
|
-
readonly flow?: Partial<FlowLimits> | null;
|
|
28
|
-
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
29
|
-
}): FlowLimits;
|
|
30
|
-
/** Further ready work waits when active >= maxConcurrency (not an error). */
|
|
31
|
-
export declare function mayDispatchMore(activeCount: number, limits: Pick<FlowLimits, "maxConcurrency">): boolean;
|
|
32
|
-
/**
|
|
33
|
-
* Map item ceiling. Exceeding raises `map.too-many-items` and nothing should start.
|
|
34
|
-
*/
|
|
35
|
-
export declare function assertMapItemCount(count: number, limits: Pick<FlowLimits, "maxMapItems">, path?: string): void;
|
|
36
|
-
/**
|
|
37
|
-
* Loop operator ceiling. Exceeding raises `loop.too-many-iterations`.
|
|
38
|
-
* `n` is the 1-based iteration about to run.
|
|
39
|
-
*/
|
|
40
|
-
export declare function assertLoopIteration(n: number, limits: Pick<FlowLimits, "maxLoopIterations">, path?: string): void;
|
|
13
|
+
/** The flow engine's limits from the Tenant's Runtime limits. */
|
|
14
|
+
export declare function flowLimitsOf(limits: Pick<RuntimeLimits, "flowMapItems" | "flowLoopIterations">): FlowEngineLimits;
|
|
15
|
+
/** Further ready work waits when `active` reaches `flowConcurrency` (not an error). */
|
|
16
|
+
export declare function mayDispatchMore(activeCount: number, limits: Pick<RuntimeLimits, "flowConcurrency">): boolean;
|
package/dist/core/limits.js
CHANGED
|
@@ -1,76 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*/
|
|
5
|
-
export const FLOW_LIMIT_DEFAULTS = {
|
|
6
|
-
maxConcurrency: 8,
|
|
7
|
-
maxMapItems: 1000,
|
|
8
|
-
maxLoopIterations: 100,
|
|
9
|
-
};
|
|
10
|
-
export const FLOW_LIMIT_ENV = {
|
|
11
|
-
maxConcurrency: "NYLORUN_FLOW_MAX_CONCURRENCY",
|
|
12
|
-
maxMapItems: "NYLORUN_FLOW_MAX_MAP_ITEMS",
|
|
13
|
-
maxLoopIterations: "NYLORUN_FLOW_MAX_LOOP_ITERATIONS",
|
|
14
|
-
};
|
|
15
|
-
export class FlowLimitError extends Error {
|
|
16
|
-
code;
|
|
17
|
-
path;
|
|
18
|
-
constructor(code, message, path) {
|
|
19
|
-
super(message);
|
|
20
|
-
this.name = "FlowLimitError";
|
|
21
|
-
this.code = code;
|
|
22
|
-
if (path !== undefined)
|
|
23
|
-
this.path = path;
|
|
24
|
-
}
|
|
1
|
+
/** The flow engine's limits from the Tenant's Runtime limits. */
|
|
2
|
+
export function flowLimitsOf(limits) {
|
|
3
|
+
return { maxMapItems: limits.flowMapItems, maxLoopIterations: limits.flowLoopIterations };
|
|
25
4
|
}
|
|
26
|
-
|
|
27
|
-
if (raw === undefined || raw === "")
|
|
28
|
-
return undefined;
|
|
29
|
-
const n = Number(raw);
|
|
30
|
-
if (!Number.isFinite(n) || !Number.isInteger(n) || n <= 0)
|
|
31
|
-
throw new Error(`${field} must be a positive integer`);
|
|
32
|
-
return n;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Resolve limits from `RuntimeOptions.flow` overrides and optional env snapshot.
|
|
36
|
-
* Does not read ambient process environment — callers pass an explicit env map.
|
|
37
|
-
*/
|
|
38
|
-
export function resolveFlowLimits(input) {
|
|
39
|
-
const env = input?.env ?? {};
|
|
40
|
-
const flow = input?.flow ?? {};
|
|
41
|
-
const fromEnv = {
|
|
42
|
-
maxConcurrency: parsePositiveInt(env[FLOW_LIMIT_ENV.maxConcurrency], FLOW_LIMIT_ENV.maxConcurrency),
|
|
43
|
-
maxMapItems: parsePositiveInt(env[FLOW_LIMIT_ENV.maxMapItems], FLOW_LIMIT_ENV.maxMapItems),
|
|
44
|
-
maxLoopIterations: parsePositiveInt(env[FLOW_LIMIT_ENV.maxLoopIterations], FLOW_LIMIT_ENV.maxLoopIterations),
|
|
45
|
-
};
|
|
46
|
-
const pick = (field, override, envValue) => {
|
|
47
|
-
const value = override ?? envValue ?? FLOW_LIMIT_DEFAULTS[field];
|
|
48
|
-
if (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0)
|
|
49
|
-
throw new Error(`flow.${field} must be a positive integer`);
|
|
50
|
-
return value;
|
|
51
|
-
};
|
|
52
|
-
return {
|
|
53
|
-
maxConcurrency: pick("maxConcurrency", flow.maxConcurrency, fromEnv.maxConcurrency),
|
|
54
|
-
maxMapItems: pick("maxMapItems", flow.maxMapItems, fromEnv.maxMapItems),
|
|
55
|
-
maxLoopIterations: pick("maxLoopIterations", flow.maxLoopIterations, fromEnv.maxLoopIterations),
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
/** Further ready work waits when active >= maxConcurrency (not an error). */
|
|
5
|
+
/** Further ready work waits when `active` reaches `flowConcurrency` (not an error). */
|
|
59
6
|
export function mayDispatchMore(activeCount, limits) {
|
|
60
|
-
return activeCount < limits.
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Map item ceiling. Exceeding raises `map.too-many-items` and nothing should start.
|
|
64
|
-
*/
|
|
65
|
-
export function assertMapItemCount(count, limits, path) {
|
|
66
|
-
if (count > limits.maxMapItems)
|
|
67
|
-
throw new FlowLimitError("map.too-many-items", `Map produced ${count} items; maxMapItems is ${limits.maxMapItems}`, path);
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Loop operator ceiling. Exceeding raises `loop.too-many-iterations`.
|
|
71
|
-
* `n` is the 1-based iteration about to run.
|
|
72
|
-
*/
|
|
73
|
-
export function assertLoopIteration(n, limits, path) {
|
|
74
|
-
if (n > limits.maxLoopIterations)
|
|
75
|
-
throw new FlowLimitError("loop.too-many-iterations", `Loop iteration ${n} exceeds maxLoopIterations ${limits.maxLoopIterations}`, path);
|
|
7
|
+
return activeCount < limits.flowConcurrency;
|
|
76
8
|
}
|
|
@@ -38,7 +38,6 @@ async function runTool(deps, session, tool, input, signal) {
|
|
|
38
38
|
const ref = {
|
|
39
39
|
id: workspace.ownerId,
|
|
40
40
|
activeTurnId: session.activeTurnId,
|
|
41
|
-
manifest: session.manifest,
|
|
42
41
|
...(workspace.environmentId === undefined ? {} : { environmentId: workspace.environmentId }),
|
|
43
42
|
};
|
|
44
43
|
return deps.sandbox.run(ref, spec, tool, input, signal);
|
|
@@ -17,8 +17,7 @@ export type TurnManifestErr = {
|
|
|
17
17
|
};
|
|
18
18
|
/**
|
|
19
19
|
* Parse `message.manifest`, require it to be a variant of the session pin, and store the
|
|
20
|
-
* manifest the turn runs with
|
|
21
|
-
* one), by its hash. Applies to that turn only (caller must not latch).
|
|
20
|
+
* manifest the turn runs with, by its hash. Applies to that turn only (caller must not latch).
|
|
22
21
|
*/
|
|
23
22
|
export declare function pinTurnManifest(args: {
|
|
24
23
|
readonly candidate: unknown;
|
|
@@ -1,30 +1,9 @@
|
|
|
1
1
|
import { AgentManifestSchema, } from "@nylorun/core/contracts";
|
|
2
2
|
import { hashManifest } from "@nylorun/core/compatibility";
|
|
3
3
|
import { isVariantOf } from "@nylorun/core/define";
|
|
4
|
-
/**
|
|
5
|
-
* The candidate's agents used as tools with the `runtime` block the pin gave each, when the
|
|
6
|
-
* candidate left it out: a turn variant is written from the definition, and only the Runtime
|
|
7
|
-
* writes `runtime`. A candidate that sets one keeps it, and must then match the pin's.
|
|
8
|
-
*/
|
|
9
|
-
function withPinnedDelegates(candidate, pinned) {
|
|
10
|
-
if (candidate.tools === undefined)
|
|
11
|
-
return candidate;
|
|
12
|
-
const pinnedAgents = new Map((pinned.tools ?? []).flatMap((tool) => tool.agent !== undefined && !("kind" in tool.agent) ? [[tool.name, tool.agent]] : []));
|
|
13
|
-
let changed = false;
|
|
14
|
-
const tools = candidate.tools.map((tool) => {
|
|
15
|
-
const agent = tool.agent;
|
|
16
|
-
const prior = pinnedAgents.get(tool.name);
|
|
17
|
-
if (agent === undefined || "kind" in agent || agent.runtime !== undefined || prior?.runtime === undefined)
|
|
18
|
-
return tool;
|
|
19
|
-
changed = true;
|
|
20
|
-
return { ...tool, agent: { ...agent, runtime: prior.runtime } };
|
|
21
|
-
});
|
|
22
|
-
return changed ? { ...candidate, tools } : candidate;
|
|
23
|
-
}
|
|
24
4
|
/**
|
|
25
5
|
* Parse `message.manifest`, require it to be a variant of the session pin, and store the
|
|
26
|
-
* manifest the turn runs with
|
|
27
|
-
* one), by its hash. Applies to that turn only (caller must not latch).
|
|
6
|
+
* manifest the turn runs with, by its hash. Applies to that turn only (caller must not latch).
|
|
28
7
|
*/
|
|
29
8
|
export function pinTurnManifest(args) {
|
|
30
9
|
const parsed = AgentManifestSchema.safeParse(args.candidate);
|
|
@@ -35,17 +14,14 @@ export function pinTurnManifest(args) {
|
|
|
35
14
|
message: "Turn manifest failed AgentManifestSchema validation",
|
|
36
15
|
};
|
|
37
16
|
}
|
|
38
|
-
const
|
|
39
|
-
if (!isVariantOf(
|
|
17
|
+
const manifest = parsed.data;
|
|
18
|
+
if (!isVariantOf(manifest, args.pinned)) {
|
|
40
19
|
return {
|
|
41
20
|
ok: false,
|
|
42
21
|
code: "loop.invalid-agent",
|
|
43
|
-
message:
|
|
44
|
-
? "Turn manifest is not a variant of the session's pinned manifest"
|
|
45
|
-
: "Turn manifest sets runtime, which only the Runtime writes: leave it out, and the turn runs with the session's",
|
|
22
|
+
message: "Turn manifest is not a variant of the session's pinned manifest",
|
|
46
23
|
};
|
|
47
24
|
}
|
|
48
|
-
const manifest = args.pinned.runtime === undefined ? candidate : { ...candidate, runtime: args.pinned.runtime };
|
|
49
25
|
const hash = hashManifest(manifest);
|
|
50
26
|
args.store.put(hash, manifest);
|
|
51
27
|
return { ok: true, manifest, hash };
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { type DurableExecution, type SandboxSignal, type Wake, type WorkerHandlers } from "./types.js";
|
|
2
2
|
export interface MemoryExecutionOptions {
|
|
3
|
-
/** Delay between sweep passes of one Tenant. Default
|
|
3
|
+
/** Delay between sweep passes of one Tenant. Default `sweep.intervalMs` (Host tuning, 5 s). */
|
|
4
4
|
sweepIntervalMs?: number;
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* First retry delay after `advance` throws; doubles per attempt. Default
|
|
7
|
+
* `execution.memoryRetryMs` (50 ms).
|
|
8
|
+
*/
|
|
6
9
|
retryDelayMs?: number;
|
|
7
|
-
/** Attempts before giving up on a throwing handler and reporting it. Default 5. */
|
|
10
|
+
/** Attempts before giving up on a throwing handler and reporting it. Default `execution.memoryAttempts` (5). */
|
|
8
11
|
maxAttempts?: number;
|
|
9
|
-
/** How long a dedupe key merges repeated wakes. Default 24 hours. */
|
|
12
|
+
/** How long a dedupe key merges repeated wakes. Default `execution.dedupeRetentionMs` (24 hours). */
|
|
10
13
|
dedupeRetentionMs?: number;
|
|
11
14
|
/**
|
|
12
15
|
* How long `stop` waits for running handlers after aborting them; the ones still running
|
|
13
|
-
* then are abandoned. Default `
|
|
16
|
+
* then are abandoned. Default `execution.stopGraceMs` (30 s).
|
|
14
17
|
*/
|
|
15
18
|
stopGraceMs?: number;
|
|
16
19
|
/** Receives handler failures that are not retried. */
|
package/dist/execution/memory.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { WAKE_REASONS, sessionKey, settleWithin, } from "./types.js";
|
|
2
|
+
import { hostTuning } from "../tuning.js";
|
|
2
3
|
/**
|
|
3
4
|
* In-process `DurableExecution`. Not durable: wakes, timers and sweeps live in
|
|
4
5
|
* memory. It keeps the seam's ordering guarantees within one process, which is
|
|
@@ -21,11 +22,12 @@ export class MemoryExecution {
|
|
|
21
22
|
stopGraceMs;
|
|
22
23
|
onError;
|
|
23
24
|
constructor(options = {}) {
|
|
24
|
-
|
|
25
|
-
this.
|
|
26
|
-
this.
|
|
27
|
-
this.
|
|
28
|
-
this.
|
|
25
|
+
const tuning = hostTuning();
|
|
26
|
+
this.sweepIntervalMs = options.sweepIntervalMs ?? tuning.sweep.intervalMs;
|
|
27
|
+
this.retryDelayMs = options.retryDelayMs ?? tuning.execution.memoryRetryMs;
|
|
28
|
+
this.maxAttempts = options.maxAttempts ?? tuning.execution.memoryAttempts;
|
|
29
|
+
this.dedupeRetentionMs = options.dedupeRetentionMs ?? tuning.execution.dedupeRetentionMs;
|
|
30
|
+
this.stopGraceMs = options.stopGraceMs ?? tuning.execution.stopGraceMs;
|
|
29
31
|
this.onError =
|
|
30
32
|
options.onError ??
|
|
31
33
|
((error) => queueMicrotask(() => {
|