@danypops/vehicle-core 0.17.0 → 0.17.1
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/README.md +12 -0
- package/dist/{vehicle-approvals.d.ts → approvals/approval.d.ts} +4 -11
- package/dist/{vehicle-approvals.js → approvals/approval.js} +33 -23
- package/dist/approvals/index.d.ts +1 -0
- package/dist/approvals/index.js +1 -0
- package/dist/client/client.d.ts +29 -0
- package/dist/client/client.js +1 -0
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/content/content.d.ts +32 -0
- package/dist/content/content.js +25 -0
- package/dist/content/index.d.ts +1 -0
- package/dist/content/index.js +1 -0
- package/dist/{vehicle-errors.d.ts → errors/error.d.ts} +10 -9
- package/dist/errors/index.d.ts +1 -0
- package/dist/errors/index.js +1 -0
- package/dist/events/event.d.ts +42 -0
- package/dist/events/event.js +35 -0
- package/dist/events/index.d.ts +1 -0
- package/dist/events/index.js +1 -0
- package/dist/idempotency/index.d.ts +1 -0
- package/dist/idempotency/index.js +1 -0
- package/dist/{vehicle-idempotency.d.ts → idempotency/receipt.d.ts} +1 -1
- package/dist/index.d.ts +26 -8
- package/dist/index.js +26 -8
- package/dist/jobs/identity.d.ts +11 -0
- package/dist/jobs/identity.js +13 -0
- package/dist/jobs/index.d.ts +7 -0
- package/dist/jobs/index.js +7 -0
- package/dist/jobs/replay.d.ts +8 -0
- package/dist/jobs/replay.js +9 -0
- package/dist/jobs/retention.d.ts +26 -0
- package/dist/jobs/retention.js +39 -0
- package/dist/jobs/steer.d.ts +23 -0
- package/dist/jobs/steer.js +41 -0
- package/dist/jobs/termination.d.ts +5 -0
- package/dist/jobs/termination.js +11 -0
- package/dist/jobs/wake-log.d.ts +39 -0
- package/dist/jobs/wake-log.js +65 -0
- package/dist/jobs/wire.d.ts +47 -0
- package/dist/jobs/wire.js +1 -0
- package/dist/manifest/index.d.ts +1 -0
- package/dist/manifest/index.js +1 -0
- package/dist/manifest/manifest.d.ts +49 -0
- package/dist/manifest/manifest.js +1 -0
- package/dist/operations/context.d.ts +52 -0
- package/dist/operations/context.js +1 -0
- package/dist/operations/effect.d.ts +3 -0
- package/dist/operations/effect.js +2 -0
- package/dist/operations/index.d.ts +3 -0
- package/dist/operations/index.js +3 -0
- package/dist/operations/operation.d.ts +98 -0
- package/dist/operations/operation.js +79 -0
- package/dist/{atomic-json.d.ts → persistence/atomic-json.d.ts} +1 -2
- package/dist/{atomic-json.js → persistence/atomic-json.js} +1 -2
- package/dist/persistence/index.d.ts +1 -0
- package/dist/persistence/index.js +1 -0
- package/dist/schedules/index.d.ts +1 -0
- package/dist/schedules/index.js +1 -0
- package/dist/{vehicle-scheduler.d.ts → schedules/schedule.d.ts} +6 -8
- package/dist/schemas/codec.d.ts +25 -0
- package/dist/schemas/codec.js +7 -0
- package/dist/schemas/index.d.ts +4 -0
- package/dist/schemas/index.js +4 -0
- package/dist/schemas/json.d.ts +7 -0
- package/dist/schemas/json.js +14 -0
- package/dist/schemas/loose-object.d.ts +20 -0
- package/dist/schemas/loose-object.js +73 -0
- package/dist/schemas/presentation.d.ts +11 -0
- package/dist/schemas/presentation.js +22 -0
- package/dist/watches/index.d.ts +1 -0
- package/dist/watches/index.js +1 -0
- package/dist/{vehicle-watchers.d.ts → watches/registry.d.ts} +2 -9
- package/dist/{vehicle-watchers.js → watches/registry.js} +2 -9
- package/package.json +5 -1
- package/src/{vehicle-approvals.ts → approvals/approval.ts} +27 -24
- package/src/approvals/index.ts +1 -0
- package/src/client/client.ts +31 -0
- package/src/client/index.ts +1 -0
- package/src/content/content.ts +46 -0
- package/src/content/index.ts +1 -0
- package/src/{vehicle-errors.ts → errors/error.ts} +10 -9
- package/src/errors/index.ts +1 -0
- package/src/events/event.ts +72 -0
- package/src/events/index.ts +1 -0
- package/src/idempotency/index.ts +1 -0
- package/src/{vehicle-idempotency.ts → idempotency/receipt.ts} +1 -1
- package/src/index.ts +26 -8
- package/src/jobs/identity.ts +13 -0
- package/src/jobs/index.ts +7 -0
- package/src/jobs/replay.ts +17 -0
- package/src/jobs/retention.ts +59 -0
- package/src/jobs/steer.ts +49 -0
- package/src/jobs/termination.ts +13 -0
- package/src/jobs/wake-log.ts +98 -0
- package/src/jobs/wire.ts +51 -0
- package/src/manifest/index.ts +1 -0
- package/src/manifest/manifest.ts +52 -0
- package/src/operations/context.ts +56 -0
- package/src/operations/effect.ts +3 -0
- package/src/operations/index.ts +3 -0
- package/src/operations/operation.ts +184 -0
- package/src/{atomic-json.ts → persistence/atomic-json.ts} +1 -2
- package/src/persistence/index.ts +1 -0
- package/src/schedules/index.ts +1 -0
- package/src/{vehicle-scheduler.ts → schedules/schedule.ts} +6 -8
- package/src/schemas/codec.ts +30 -0
- package/src/schemas/index.ts +4 -0
- package/src/schemas/json.ts +17 -0
- package/src/schemas/loose-object.ts +82 -0
- package/src/schemas/presentation.ts +28 -0
- package/src/watches/index.ts +1 -0
- package/src/{vehicle-watchers.ts → watches/registry.ts} +2 -9
- package/dist/vehicle-contract.d.ts +0 -353
- package/dist/vehicle-contract.js +0 -253
- package/dist/vehicle-jobs.d.ts +0 -159
- package/dist/vehicle-jobs.js +0 -182
- package/src/vehicle-contract.ts +0 -597
- package/src/vehicle-jobs.ts +0 -305
- /package/dist/{vehicle-errors.js → errors/error.js} +0 -0
- /package/dist/{vehicle-idempotency.js → idempotency/receipt.js} +0 -0
- /package/dist/{vehicle-scheduler.js → schedules/schedule.js} +0 -0
package/README.md
CHANGED
|
@@ -22,3 +22,15 @@ tool-result type use, so no translation layer is needed at either boundary.
|
|
|
22
22
|
`extractVehicleContent(output)` reads those blocks back out for a generic
|
|
23
23
|
Vehicle client to prefer over raw JSON, returning undefined for absent or
|
|
24
24
|
malformed content so the caller can fall back safely.
|
|
25
|
+
|
|
26
|
+
## Layout
|
|
27
|
+
|
|
28
|
+
`src/` is organized by capability, not by feature-flat filename prefix:
|
|
29
|
+
`operations/`, `events/`, `manifest/`, `client/`, `schemas/`, `content/`,
|
|
30
|
+
`approvals/`, `jobs/`, `schedules/`, `watches/`, and `errors/`/`idempotency/`
|
|
31
|
+
each hold one cohesive concern; `persistence/` (atomic JSON writes) is kept
|
|
32
|
+
separate as a technical utility, not a Vehicle protocol capability. Every
|
|
33
|
+
public symbol is re-exported unchanged from the package root
|
|
34
|
+
(`import { X } from "@danypops/vehicle-core"`); `./persistence` is also
|
|
35
|
+
available as its own subpath export for a consumer that only needs the
|
|
36
|
+
atomic-write primitive without the rest of the wire contract.
|
|
@@ -1,12 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* that stands between a gated-effect invoke() and its handler actually
|
|
4
|
-
* running. Kept in vehicle-core (runtime-neutral, zero dependencies) --
|
|
5
|
-
* the actual signing/verification authority (HmacApprovalAuthority) needs
|
|
6
|
-
* node:crypto and lives in vehicle-server instead, the same split
|
|
7
|
-
* atomic-json.ts already uses for fs access.
|
|
8
|
-
*/
|
|
9
|
-
import type { VehicleEffect, VehiclePrincipal } from "./vehicle-contract.js";
|
|
1
|
+
import type { VehiclePrincipal } from "../operations/context.js";
|
|
2
|
+
import type { VehicleEffect } from "../operations/effect.js";
|
|
10
3
|
/** Set once at registry-configuration time (VehicleRegistry.configureApprovals()); never on a per-invoke basis. */
|
|
11
4
|
export declare const DEFAULT_APPROVAL_EFFECTS: readonly VehicleEffect[];
|
|
12
5
|
/**
|
|
@@ -59,5 +52,5 @@ export interface VehicleApprovalAuthority {
|
|
|
59
52
|
verify(capability: string, operationName: string, operationVersion: number, inputHash: string): boolean;
|
|
60
53
|
}
|
|
61
54
|
/** Built into every VehicleRegistry once configureApprovals() is called -- never registered unconditionally, so a Vehicle that never opts in has zero manifest/shape change. */
|
|
62
|
-
export declare const vehicleApprovalRequestedEvent: import("
|
|
63
|
-
export declare const vehicleApprovalResolvedEvent: import("
|
|
55
|
+
export declare const vehicleApprovalRequestedEvent: import("../events/event.js").VehicleEvent<VehicleApprovalRequest>;
|
|
56
|
+
export declare const vehicleApprovalResolvedEvent: import("../events/event.js").VehicleEvent<VehicleApprovalOutcome>;
|
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Wire-level shapes for the Approval Gate: a pending human/authority decision
|
|
3
|
+
* that stands between a gated-effect invoke() and its handler actually
|
|
4
|
+
* running. Kept in vehicle-core (runtime-neutral, zero dependencies) --
|
|
5
|
+
* the actual signing/verification authority (HmacApprovalAuthority) needs
|
|
6
|
+
* node:crypto and lives in vehicle-server instead, the same split
|
|
7
|
+
* atomic-json.ts already uses for fs access.
|
|
8
|
+
*/
|
|
9
|
+
import { defineVehicleEvent } from "../events/event.js";
|
|
10
|
+
import { VEHICLE_EFFECTS } from "../operations/effect.js";
|
|
11
|
+
import { defineVehicleSchema } from "../schemas/codec.js";
|
|
2
12
|
/** A sha256 hex digest: exactly 64 lowercase hex characters -- matches hashApprovalInput's own (vehicle-server) output shape. */
|
|
3
13
|
const SHA256_HEX_PATTERN = /^[0-9a-f]{64}$/;
|
|
4
14
|
function isVehicleEffect(value) {
|
|
@@ -8,11 +18,11 @@ function isVehiclePrincipal(value) {
|
|
|
8
18
|
if (typeof value !== "object" || value === null)
|
|
9
19
|
return false;
|
|
10
20
|
const candidate = value;
|
|
11
|
-
if (typeof candidate
|
|
21
|
+
if (typeof candidate["id"] !== "string")
|
|
12
22
|
return false;
|
|
13
|
-
if (candidate
|
|
23
|
+
if (candidate["claims"] === undefined)
|
|
14
24
|
return true;
|
|
15
|
-
return typeof candidate
|
|
25
|
+
return typeof candidate["claims"] === "object" && candidate["claims"] !== null && !Array.isArray(candidate["claims"]);
|
|
16
26
|
}
|
|
17
27
|
/** Set once at registry-configuration time (VehicleRegistry.configureApprovals()); never on a per-invoke basis. */
|
|
18
28
|
export const DEFAULT_APPROVAL_EFFECTS = ["destructive", "open-world"];
|
|
@@ -49,21 +59,21 @@ const requestedPayloadSchema = defineVehicleSchema({
|
|
|
49
59
|
if (typeof value !== "object" || value === null)
|
|
50
60
|
return { success: false, issues: [{ path: [], message: "input must be an object" }] };
|
|
51
61
|
const row = value;
|
|
52
|
-
if (typeof row
|
|
53
|
-
typeof row
|
|
54
|
-
typeof row
|
|
55
|
-
!Number.isInteger(row
|
|
56
|
-
row
|
|
57
|
-
!isVehicleEffect(row
|
|
58
|
-
typeof row
|
|
59
|
-
!Number.isFinite(row
|
|
60
|
-
typeof row
|
|
61
|
-
!Number.isFinite(row
|
|
62
|
-
typeof row
|
|
63
|
-
!SHA256_HEX_PATTERN.test(row
|
|
62
|
+
if (typeof row["requestId"] !== "string" ||
|
|
63
|
+
typeof row["operationName"] !== "string" ||
|
|
64
|
+
typeof row["operationVersion"] !== "number" ||
|
|
65
|
+
!Number.isInteger(row["operationVersion"]) ||
|
|
66
|
+
row["operationVersion"] < 1 ||
|
|
67
|
+
!isVehicleEffect(row["effect"]) ||
|
|
68
|
+
typeof row["requestedAt"] !== "number" ||
|
|
69
|
+
!Number.isFinite(row["requestedAt"]) ||
|
|
70
|
+
typeof row["expiresAt"] !== "number" ||
|
|
71
|
+
!Number.isFinite(row["expiresAt"]) ||
|
|
72
|
+
typeof row["inputHash"] !== "string" ||
|
|
73
|
+
!SHA256_HEX_PATTERN.test(row["inputHash"])) {
|
|
64
74
|
return { success: false, issues: [{ path: [], message: "invalid approval request payload" }] };
|
|
65
75
|
}
|
|
66
|
-
if (row
|
|
76
|
+
if (row["principal"] !== undefined && !isVehiclePrincipal(row["principal"])) {
|
|
67
77
|
return { success: false, issues: [{ path: ["principal"], message: "invalid approval request principal" }] };
|
|
68
78
|
}
|
|
69
79
|
return { success: true, value: row };
|
|
@@ -86,16 +96,16 @@ const resolvedPayloadSchema = defineVehicleSchema({
|
|
|
86
96
|
if (typeof value !== "object" || value === null)
|
|
87
97
|
return { success: false, issues: [{ path: [], message: "input must be an object" }] };
|
|
88
98
|
const row = value;
|
|
89
|
-
if (typeof row
|
|
90
|
-
(row
|
|
91
|
-
typeof row
|
|
92
|
-
!Number.isFinite(row
|
|
99
|
+
if (typeof row["requestId"] !== "string" ||
|
|
100
|
+
(row["decision"] !== "granted" && row["decision"] !== "denied") ||
|
|
101
|
+
typeof row["decidedAt"] !== "number" ||
|
|
102
|
+
!Number.isFinite(row["decidedAt"])) {
|
|
93
103
|
return { success: false, issues: [{ path: [], message: "invalid approval outcome payload" }] };
|
|
94
104
|
}
|
|
95
|
-
if (row
|
|
105
|
+
if (row["decidedBy"] !== undefined && typeof row["decidedBy"] !== "string") {
|
|
96
106
|
return { success: false, issues: [{ path: ["decidedBy"], message: "decidedBy must be a string" }] };
|
|
97
107
|
}
|
|
98
|
-
if (row
|
|
108
|
+
if (row["comment"] !== undefined && typeof row["comment"] !== "string") {
|
|
99
109
|
return { success: false, issues: [{ path: ["comment"], message: "comment must be a string" }] };
|
|
100
110
|
}
|
|
101
111
|
return { success: true, value: row };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./approval.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./approval.js";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { VehicleJobSnapshot, VehicleJobSubmitOptions, VehicleJobSubmitResult, VehicleJobTailResult } from "../jobs/wire.js";
|
|
2
|
+
import type { VehicleManifest } from "../manifest/manifest.js";
|
|
3
|
+
import type { VehicleInvocationOptions } from "../operations/context.js";
|
|
4
|
+
export interface VehicleSubscription {
|
|
5
|
+
close(): void;
|
|
6
|
+
}
|
|
7
|
+
export interface VehicleClient {
|
|
8
|
+
manifest(): Promise<VehicleManifest>;
|
|
9
|
+
invoke<Output = unknown>(name: string, version: number, input: unknown, options?: VehicleInvocationOptions): Promise<Output>;
|
|
10
|
+
close(): Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Vehicle Jobs -- submit a background-capable operation (one whose descriptor declares
|
|
13
|
+
* `background`, see {@link VehicleBackgroundCapability}) and get its jobId back immediately,
|
|
14
|
+
* without waiting for the operation itself to make any progress. Optional: a client that
|
|
15
|
+
* never talks to a job-capable Vehicle (or a hand-rolled test double) simply omits these five
|
|
16
|
+
* methods, exactly like this interface's own long-standing `subscribe()`-shaped extras --
|
|
17
|
+
* present on both LocalVehicleClient and RemoteVehicleClient, absent elsewhere. Feature-detect
|
|
18
|
+
* via the operation's own manifest `background` capability, not by probing for these methods.
|
|
19
|
+
*/
|
|
20
|
+
submitJob?(name: string, version: number, input: unknown, options?: VehicleJobSubmitOptions): Promise<VehicleJobSubmitResult>;
|
|
21
|
+
/** Never blocks -- current status, plus output/error once terminal. */
|
|
22
|
+
pollJob?(jobId: string): Promise<VehicleJobSnapshot>;
|
|
23
|
+
/** Progress entries strictly after `cursor` (0 for everything so far), plus the next cursor. Never blocks. */
|
|
24
|
+
tailJob?(jobId: string, cursor?: number): Promise<VehicleJobTailResult>;
|
|
25
|
+
/** Pushes new input to an already-running job's handler, if it opted in via context.steerInputs. */
|
|
26
|
+
steerJob?(jobId: string, input: unknown): Promise<void>;
|
|
27
|
+
/** Best-effort cancellation of a still-running job -- a no-op against an already-terminal one. */
|
|
28
|
+
cancelJob?(jobId: string): Promise<void>;
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./client.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./client.js";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A block of narrative text meant to be read by the model, not parsed as
|
|
3
|
+
* data -- same field name and shape MCP's own CallToolResult.content and
|
|
4
|
+
* Pi's own ToolDefinition.execute() return already use, so a Vehicle
|
|
5
|
+
* operation adopting this needs no translation layer at either boundary.
|
|
6
|
+
* Only the "text" variant exists here; there's no Vehicle use case yet for
|
|
7
|
+
* MCP's image/audio/resource-link block kinds.
|
|
8
|
+
*/
|
|
9
|
+
export interface VehicleContentBlock {
|
|
10
|
+
readonly type: "text";
|
|
11
|
+
readonly text: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* An operation's Output type can intersect this to carry its own
|
|
15
|
+
* model-facing narrative alongside its structured data, e.g.
|
|
16
|
+
* `type RunOutput = { runId: string; created: Task[] } & WithVehicleContent`.
|
|
17
|
+
* The operation itself builds `content` since it's the only code that
|
|
18
|
+
* actually knows how to describe what it computed -- never a per-consumer
|
|
19
|
+
* override bolted on wherever the operation happens to get registered.
|
|
20
|
+
*/
|
|
21
|
+
export interface WithVehicleContent {
|
|
22
|
+
readonly content?: readonly VehicleContentBlock[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Reads an operation's own `content` blocks off its output when present and
|
|
26
|
+
* well-formed, so a generic Vehicle client can prefer them over dumping raw
|
|
27
|
+
* JSON at the model -- without knowing anything about the operation's own
|
|
28
|
+
* domain shape. Returns undefined for a malformed or absent `content` field;
|
|
29
|
+
* the caller falls back to its own default (formatted JSON) rather than
|
|
30
|
+
* risk forwarding partial/garbled blocks.
|
|
31
|
+
*/
|
|
32
|
+
export declare function extractVehicleContent(output: unknown): readonly VehicleContentBlock[] | undefined;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads an operation's own `content` blocks off its output when present and
|
|
3
|
+
* well-formed, so a generic Vehicle client can prefer them over dumping raw
|
|
4
|
+
* JSON at the model -- without knowing anything about the operation's own
|
|
5
|
+
* domain shape. Returns undefined for a malformed or absent `content` field;
|
|
6
|
+
* the caller falls back to its own default (formatted JSON) rather than
|
|
7
|
+
* risk forwarding partial/garbled blocks.
|
|
8
|
+
*/
|
|
9
|
+
export function extractVehicleContent(output) {
|
|
10
|
+
if (typeof output !== "object" || output === null || Array.isArray(output))
|
|
11
|
+
return undefined;
|
|
12
|
+
const content = output.content;
|
|
13
|
+
if (!Array.isArray(content) || content.length === 0)
|
|
14
|
+
return undefined;
|
|
15
|
+
const blocks = [];
|
|
16
|
+
for (const block of content) {
|
|
17
|
+
if (typeof block !== "object" || block === null)
|
|
18
|
+
return undefined;
|
|
19
|
+
const { type, text } = block;
|
|
20
|
+
if (type !== "text" || typeof text !== "string")
|
|
21
|
+
return undefined;
|
|
22
|
+
blocks.push({ type: "text", text });
|
|
23
|
+
}
|
|
24
|
+
return blocks;
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./content.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./content.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { VehicleSchemaIssue } from "../schemas/codec.js";
|
|
2
|
+
import type { JsonValue } from "../schemas/json.js";
|
|
2
3
|
export type VehicleFailureCategory = "validation" | "not_found" | "conflict" | "authorization" | "capacity" | "timeout" | "cancelled" | "unavailable" | "internal";
|
|
3
4
|
export type VehicleCoreErrorCode = "duplicate-owner" | "not-found" | "invalid-input" | "invalid-output" | "permission-denied" | "request-too-large" | "response-too-large" | "cancelled" | "deadline-exceeded" | "handler-failed" | "policy-failed" | "idempotency-key-required" | "idempotency-conflict" | "client-closed" | "operation-unavailable" | "background-not-supported" | "job-not-found" | "job-not-steerable" | "job-steer-queue-full";
|
|
4
5
|
export interface VehicleRecovery {
|
|
@@ -20,10 +21,10 @@ export interface VehicleFailure {
|
|
|
20
21
|
export interface VehicleErrorOptions {
|
|
21
22
|
readonly category: VehicleFailureCategory;
|
|
22
23
|
readonly retryable?: boolean;
|
|
23
|
-
readonly retryAfterMs?: number;
|
|
24
|
-
readonly recovery?: VehicleRecovery;
|
|
25
|
-
readonly details?: JsonValue;
|
|
26
|
-
readonly operationId?: string;
|
|
24
|
+
readonly retryAfterMs?: number | undefined;
|
|
25
|
+
readonly recovery?: VehicleRecovery | undefined;
|
|
26
|
+
readonly details?: JsonValue | undefined;
|
|
27
|
+
readonly operationId?: string | undefined;
|
|
27
28
|
readonly cause?: unknown;
|
|
28
29
|
/** Includes cause's message in toFailure().causeMessage. Default false -- an arbitrary cause could carry a credential or internal detail. */
|
|
29
30
|
readonly exposeCause?: boolean;
|
|
@@ -51,10 +52,10 @@ export declare class VehicleError extends Error {
|
|
|
51
52
|
readonly code: string;
|
|
52
53
|
readonly category: VehicleFailureCategory;
|
|
53
54
|
readonly retryable: boolean;
|
|
54
|
-
readonly retryAfterMs?: number;
|
|
55
|
-
readonly recovery?: VehicleRecovery;
|
|
56
|
-
readonly details?: JsonValue;
|
|
57
|
-
readonly operationId?: string;
|
|
55
|
+
readonly retryAfterMs?: number | undefined;
|
|
56
|
+
readonly recovery?: VehicleRecovery | undefined;
|
|
57
|
+
readonly details?: JsonValue | undefined;
|
|
58
|
+
readonly operationId?: string | undefined;
|
|
58
59
|
private readonly exposeCause;
|
|
59
60
|
constructor(code: string, message: string, options: VehicleErrorOptions);
|
|
60
61
|
toFailure(): VehicleFailure;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./error.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./error.js";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { VehicleSchemaCodec } from "../schemas/codec.js";
|
|
2
|
+
import { type JsonSchema } from "../schemas/json.js";
|
|
3
|
+
/**
|
|
4
|
+
* A named, schema'd event type a provider declares as part of its
|
|
5
|
+
* manifest -- the typed alternative to a raw PushChannel.publish(topic,
|
|
6
|
+
* payload) call with a hand-invented topic string. No `available` flag the
|
|
7
|
+
* way an operation has one: an event type, once declared, is always
|
|
8
|
+
* emittable -- there's no credential-gated "this event is currently
|
|
9
|
+
* unavailable" concept the way a live external-service-backed operation
|
|
10
|
+
* can have.
|
|
11
|
+
*/
|
|
12
|
+
export interface VehicleEventDescriptor {
|
|
13
|
+
readonly name: string;
|
|
14
|
+
readonly version: number;
|
|
15
|
+
readonly description: string;
|
|
16
|
+
readonly payloadSchema: JsonSchema;
|
|
17
|
+
/** Same bounded-resource discipline as an operation's own maxRequestBytes/maxResponseBytes -- required, never silently defaulted. */
|
|
18
|
+
readonly maxPayloadBytes: number;
|
|
19
|
+
}
|
|
20
|
+
export interface VehicleEvent<Payload> {
|
|
21
|
+
readonly descriptor: VehicleEventDescriptor;
|
|
22
|
+
readonly payload: VehicleSchemaCodec<Payload>;
|
|
23
|
+
}
|
|
24
|
+
export interface DefineVehicleEventOptions<Payload> {
|
|
25
|
+
readonly name: string;
|
|
26
|
+
readonly version: number;
|
|
27
|
+
readonly description: string;
|
|
28
|
+
readonly payload: VehicleSchemaCodec<Payload>;
|
|
29
|
+
readonly maxPayloadBytes: number;
|
|
30
|
+
}
|
|
31
|
+
export declare function defineVehicleEvent<Payload>(options: DefineVehicleEventOptions<Payload>): VehicleEvent<Payload>;
|
|
32
|
+
export type VehicleManifestEvent = VehicleEventDescriptor;
|
|
33
|
+
export type VehicleEventHandler<Payload> = (payload: Payload) => void;
|
|
34
|
+
/**
|
|
35
|
+
* The wire topic name a bridge (bridgeVehicleEventsToPushChannel, in
|
|
36
|
+
* vehicle-server) publishes an event under, and a subscriber
|
|
37
|
+
* (RemoteVehicleClient.subscribe()) subscribes to -- one shared naming
|
|
38
|
+
* function in vehicle-core so both sides can never drift apart on the
|
|
39
|
+
* convention, the same failure mode this primitive exists to prevent
|
|
40
|
+
* providers from reinventing per-project.
|
|
41
|
+
*/
|
|
42
|
+
export declare function vehicleEventTopic(name: string, version: number): string;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { cloneJson } from "../schemas/json.js";
|
|
2
|
+
function validateEventMetadata(options) {
|
|
3
|
+
if (!options.name.trim())
|
|
4
|
+
throw new Error("Vehicle event name must not be empty");
|
|
5
|
+
if (!Number.isInteger(options.version) || options.version < 1) {
|
|
6
|
+
throw new Error("Vehicle event version must be a positive integer");
|
|
7
|
+
}
|
|
8
|
+
if (!options.description.trim())
|
|
9
|
+
throw new Error("Vehicle event description must not be empty");
|
|
10
|
+
if (!Number.isSafeInteger(options.maxPayloadBytes) || options.maxPayloadBytes < 1) {
|
|
11
|
+
throw new Error("Vehicle event maxPayloadBytes must be a positive integer");
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function defineVehicleEvent(options) {
|
|
15
|
+
validateEventMetadata(options);
|
|
16
|
+
const descriptor = Object.freeze({
|
|
17
|
+
name: options.name,
|
|
18
|
+
version: options.version,
|
|
19
|
+
description: options.description,
|
|
20
|
+
payloadSchema: cloneJson(options.payload.jsonSchema),
|
|
21
|
+
maxPayloadBytes: options.maxPayloadBytes,
|
|
22
|
+
});
|
|
23
|
+
return Object.freeze({ descriptor, payload: options.payload });
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The wire topic name a bridge (bridgeVehicleEventsToPushChannel, in
|
|
27
|
+
* vehicle-server) publishes an event under, and a subscriber
|
|
28
|
+
* (RemoteVehicleClient.subscribe()) subscribes to -- one shared naming
|
|
29
|
+
* function in vehicle-core so both sides can never drift apart on the
|
|
30
|
+
* convention, the same failure mode this primitive exists to prevent
|
|
31
|
+
* providers from reinventing per-project.
|
|
32
|
+
*/
|
|
33
|
+
export function vehicleEventTopic(name, version) {
|
|
34
|
+
return `vehicle-event:${name}@${version}`;
|
|
35
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./event.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./event.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./receipt.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./receipt.js";
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* VehicleJobStore split (a pure, independently-testable bounded-retention rule here; the stateful
|
|
6
6
|
* store that calls it lives in vehicle-server).
|
|
7
7
|
*/
|
|
8
|
-
import type { VehicleFailure } from "
|
|
8
|
+
import type { VehicleFailure } from "../errors/error.js";
|
|
9
9
|
/** A settled keyed-idempotency outcome -- exactly what gets replayed to a caller reusing the same key. Never carries the original request's raw input (only its hash is ever retained, see VehicleIdempotencyReceipt) and never a credential: `output`/`failure` are already what the operation would hand back to any caller, the same wire-safe boundary VehicleJobPersistedRecord's own `output`/`error` fields already cross. */
|
|
10
10
|
export type VehicleIdempotencyResult = {
|
|
11
11
|
readonly ok: true;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Vehicle's runtime-neutral wire contract, organized by capability (see each
|
|
3
|
+
* subfolder's own index.ts) rather than one flat forest of vehicle-*.ts files:
|
|
4
|
+
* schemas (codecs, loose-object validation, credential presentation), content
|
|
5
|
+
* (model-facing narrative blocks), operations (descriptors, effect
|
|
6
|
+
* classification, invocation context), events, manifest, client (the port a
|
|
7
|
+
* caller programs against), approvals (the Approval Gate's wire shapes), jobs
|
|
8
|
+
* (Vehicle Jobs' pure pieces), schedules, watches, and persistence (atomic
|
|
9
|
+
* JSON -- a technical utility, not a Vehicle protocol capability, kept
|
|
10
|
+
* distinct for that reason). Every symbol below is re-exported unchanged
|
|
11
|
+
* from its historical flat-file home, so root-level `import { X } from
|
|
12
|
+
* "@danypops/vehicle-core"` usage is completely unaffected by this layout.
|
|
13
|
+
*/
|
|
14
|
+
export * from "./approvals/index.js";
|
|
15
|
+
export * from "./client/index.js";
|
|
16
|
+
export * from "./content/index.js";
|
|
17
|
+
export * from "./errors/index.js";
|
|
18
|
+
export * from "./events/index.js";
|
|
19
|
+
export * from "./idempotency/index.js";
|
|
20
|
+
export * from "./jobs/index.js";
|
|
21
|
+
export * from "./manifest/index.js";
|
|
22
|
+
export * from "./operations/index.js";
|
|
23
|
+
export * from "./persistence/index.js";
|
|
24
|
+
export * from "./schedules/index.js";
|
|
25
|
+
export * from "./schemas/index.js";
|
|
26
|
+
export * from "./watches/index.js";
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Vehicle's runtime-neutral wire contract, organized by capability (see each
|
|
3
|
+
* subfolder's own index.ts) rather than one flat forest of vehicle-*.ts files:
|
|
4
|
+
* schemas (codecs, loose-object validation, credential presentation), content
|
|
5
|
+
* (model-facing narrative blocks), operations (descriptors, effect
|
|
6
|
+
* classification, invocation context), events, manifest, client (the port a
|
|
7
|
+
* caller programs against), approvals (the Approval Gate's wire shapes), jobs
|
|
8
|
+
* (Vehicle Jobs' pure pieces), schedules, watches, and persistence (atomic
|
|
9
|
+
* JSON -- a technical utility, not a Vehicle protocol capability, kept
|
|
10
|
+
* distinct for that reason). Every symbol below is re-exported unchanged
|
|
11
|
+
* from its historical flat-file home, so root-level `import { X } from
|
|
12
|
+
* "@danypops/vehicle-core"` usage is completely unaffected by this layout.
|
|
13
|
+
*/
|
|
14
|
+
export * from "./approvals/index.js";
|
|
15
|
+
export * from "./client/index.js";
|
|
16
|
+
export * from "./content/index.js";
|
|
17
|
+
export * from "./errors/index.js";
|
|
18
|
+
export * from "./events/index.js";
|
|
19
|
+
export * from "./idempotency/index.js";
|
|
20
|
+
export * from "./jobs/index.js";
|
|
21
|
+
export * from "./manifest/index.js";
|
|
22
|
+
export * from "./operations/index.js";
|
|
23
|
+
export * from "./persistence/index.js";
|
|
24
|
+
export * from "./schedules/index.js";
|
|
25
|
+
export * from "./schemas/index.js";
|
|
26
|
+
export * from "./watches/index.js";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vehicle Jobs run as in-process promises, not child processes -- there is
|
|
3
|
+
* no PID to reuse, but a persisted job record written by one process
|
|
4
|
+
* instance must still never be mistaken for one this (possibly restarted)
|
|
5
|
+
* instance can still resolve. Each VehicleJobStore construction gets a
|
|
6
|
+
* fresh random instanceToken; a persisted record's own stamped token only
|
|
7
|
+
* ever matches the instance that wrote it. A mismatch means "the original
|
|
8
|
+
* run is gone" -- this instance should treat the record as orphaned rather
|
|
9
|
+
* than resolve it.
|
|
10
|
+
*/
|
|
11
|
+
export declare function vehicleJobIdentityMatches(recordInstanceToken: string, currentInstanceToken: string): boolean;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vehicle Jobs run as in-process promises, not child processes -- there is
|
|
3
|
+
* no PID to reuse, but a persisted job record written by one process
|
|
4
|
+
* instance must still never be mistaken for one this (possibly restarted)
|
|
5
|
+
* instance can still resolve. Each VehicleJobStore construction gets a
|
|
6
|
+
* fresh random instanceToken; a persisted record's own stamped token only
|
|
7
|
+
* ever matches the instance that wrote it. A mismatch means "the original
|
|
8
|
+
* run is gone" -- this instance should treat the record as orphaned rather
|
|
9
|
+
* than resolve it.
|
|
10
|
+
*/
|
|
11
|
+
export function vehicleJobIdentityMatches(recordInstanceToken, currentInstanceToken) {
|
|
12
|
+
return recordInstanceToken === currentInstanceToken;
|
|
13
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { VehicleJobWakeEntry } from "./wake-log.js";
|
|
2
|
+
/** Read-only replay side of a wake log -- both a live VehicleJobWakeLog and a restored (no-longer-appendable) job satisfy this with the same tail() semantics. */
|
|
3
|
+
export interface VehicleJobWakeLogReader {
|
|
4
|
+
since(cursor: number): readonly VehicleJobWakeEntry[];
|
|
5
|
+
readonly cursor: number;
|
|
6
|
+
}
|
|
7
|
+
/** Wraps a fixed, already-finalized list of entries (e.g. restored from disk) in the same reader shape a live VehicleJobWakeLog exposes, so VehicleJobStore.tail() doesn't need to special-case a restored job. */
|
|
8
|
+
export declare function createStaticVehicleJobWakeLog(entries: readonly VehicleJobWakeEntry[]): VehicleJobWakeLogReader;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Wraps a fixed, already-finalized list of entries (e.g. restored from disk) in the same reader shape a live VehicleJobWakeLog exposes, so VehicleJobStore.tail() doesn't need to special-case a restored job. */
|
|
2
|
+
export function createStaticVehicleJobWakeLog(entries) {
|
|
3
|
+
const sorted = [...entries].sort((a, b) => a.seq - b.seq);
|
|
4
|
+
const cursor = sorted.length > 0 ? sorted[sorted.length - 1].seq : 0;
|
|
5
|
+
return {
|
|
6
|
+
since: (cursorArg) => sorted.filter((entry) => entry.seq > cursorArg),
|
|
7
|
+
cursor,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { VehicleJobStatus } from "./termination.js";
|
|
2
|
+
/** Minimal shape selectVehicleJobsForEviction needs from a job record -- kept separate from VehicleJobSnapshot so vehicle-server doesn't have to construct a full snapshot just to ask "should this be swept". */
|
|
3
|
+
export interface VehicleJobEvictionCandidate {
|
|
4
|
+
readonly jobId: string;
|
|
5
|
+
readonly status: VehicleJobStatus;
|
|
6
|
+
readonly delivered: boolean;
|
|
7
|
+
readonly updatedAt: number;
|
|
8
|
+
}
|
|
9
|
+
export interface VehicleJobRetentionOptions {
|
|
10
|
+
/** Hard cap on total retained job records (of any status). A running job is never evicted regardless of this cap. */
|
|
11
|
+
readonly maxRetainedJobs: number;
|
|
12
|
+
/** A delivered terminal job becomes eligible for eviction once this many ms have passed since it was delivered (== updatedAt at delivery time). */
|
|
13
|
+
readonly deliveredRetentionMs: number;
|
|
14
|
+
readonly now: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Pure eviction-selection policy, kept separate from VehicleJobStore's own
|
|
18
|
+
* bookkeeping so the bounded-retention rule is independently testable.
|
|
19
|
+
* Preference order: (1) delivered and past deliveredRetentionMs, oldest
|
|
20
|
+
* first; (2) once still over maxRetainedJobs, any delivered terminal job,
|
|
21
|
+
* oldest first; (3) only as a last resort, an undelivered terminal job,
|
|
22
|
+
* oldest first -- a real loss (a caller may still want that result), but
|
|
23
|
+
* an unbounded store is a worse failure mode. A running job is never a
|
|
24
|
+
* candidate.
|
|
25
|
+
*/
|
|
26
|
+
export declare function selectVehicleJobsForEviction(candidates: readonly VehicleJobEvictionCandidate[], options: VehicleJobRetentionOptions): readonly string[];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure eviction-selection policy, kept separate from VehicleJobStore's own
|
|
3
|
+
* bookkeeping so the bounded-retention rule is independently testable.
|
|
4
|
+
* Preference order: (1) delivered and past deliveredRetentionMs, oldest
|
|
5
|
+
* first; (2) once still over maxRetainedJobs, any delivered terminal job,
|
|
6
|
+
* oldest first; (3) only as a last resort, an undelivered terminal job,
|
|
7
|
+
* oldest first -- a real loss (a caller may still want that result), but
|
|
8
|
+
* an unbounded store is a worse failure mode. A running job is never a
|
|
9
|
+
* candidate.
|
|
10
|
+
*/
|
|
11
|
+
export function selectVehicleJobsForEviction(candidates, options) {
|
|
12
|
+
const terminal = candidates.filter((candidate) => candidate.status !== "running");
|
|
13
|
+
const byAgeAscending = (a, b) => a.updatedAt - b.updatedAt;
|
|
14
|
+
const evicted = new Set();
|
|
15
|
+
for (const candidate of terminal) {
|
|
16
|
+
if (candidate.delivered && options.now - candidate.updatedAt >= options.deliveredRetentionMs)
|
|
17
|
+
evicted.add(candidate.jobId);
|
|
18
|
+
}
|
|
19
|
+
const remainingCount = () => candidates.length - evicted.size;
|
|
20
|
+
if (remainingCount() > options.maxRetainedJobs) {
|
|
21
|
+
const deliveredOldestFirst = terminal.filter((candidate) => candidate.delivered && !evicted.has(candidate.jobId)).sort(byAgeAscending);
|
|
22
|
+
for (const candidate of deliveredOldestFirst) {
|
|
23
|
+
if (remainingCount() <= options.maxRetainedJobs)
|
|
24
|
+
break;
|
|
25
|
+
evicted.add(candidate.jobId);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (remainingCount() > options.maxRetainedJobs) {
|
|
29
|
+
const undeliveredOldestFirst = terminal
|
|
30
|
+
.filter((candidate) => !candidate.delivered && !evicted.has(candidate.jobId))
|
|
31
|
+
.sort(byAgeAscending);
|
|
32
|
+
for (const candidate of undeliveredOldestFirst) {
|
|
33
|
+
if (remainingCount() <= options.maxRetainedJobs)
|
|
34
|
+
break;
|
|
35
|
+
evicted.add(candidate.jobId);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return [...evicted];
|
|
39
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A job's mid-flight input channel -- the "steer" primitive. Bounded FIFO:
|
|
3
|
+
* push() while a handler isn't yet reading buffers up to maxQueueSize, then
|
|
4
|
+
* refuses further input rather than growing unboundedly or silently
|
|
5
|
+
* overwriting an unread entry. A handler consumes it via `for await (const
|
|
6
|
+
* input of context.steerInputs)`, which ends cleanly once close() is
|
|
7
|
+
* called (VehicleJobStore does this at job finalization).
|
|
8
|
+
*/
|
|
9
|
+
export interface VehicleJobSteerPushResult {
|
|
10
|
+
readonly accepted: boolean;
|
|
11
|
+
readonly dropReason?: "queue-full" | "channel-closed";
|
|
12
|
+
}
|
|
13
|
+
export declare class VehicleJobSteerChannel implements AsyncIterable<unknown> {
|
|
14
|
+
private readonly maxQueueSize;
|
|
15
|
+
private readonly buffer;
|
|
16
|
+
private readonly waiters;
|
|
17
|
+
private closed;
|
|
18
|
+
constructor(maxQueueSize?: number);
|
|
19
|
+
push(value: unknown): VehicleJobSteerPushResult;
|
|
20
|
+
/** Ends every pending and future iteration with done:true; further push() calls report "channel-closed". Idempotent. */
|
|
21
|
+
close(): void;
|
|
22
|
+
[Symbol.asyncIterator](): AsyncIterator<unknown>;
|
|
23
|
+
}
|