@danypops/vehicle-core 0.16.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.
Files changed (123) hide show
  1. package/README.md +12 -0
  2. package/dist/{vehicle-approvals.d.ts → approvals/approval.d.ts} +4 -11
  3. package/dist/{vehicle-approvals.js → approvals/approval.js} +53 -12
  4. package/dist/approvals/index.d.ts +1 -0
  5. package/dist/approvals/index.js +1 -0
  6. package/dist/client/client.d.ts +29 -0
  7. package/dist/client/client.js +1 -0
  8. package/dist/client/index.d.ts +1 -0
  9. package/dist/client/index.js +1 -0
  10. package/dist/content/content.d.ts +32 -0
  11. package/dist/content/content.js +25 -0
  12. package/dist/content/index.d.ts +1 -0
  13. package/dist/content/index.js +1 -0
  14. package/dist/{vehicle-errors.d.ts → errors/error.d.ts} +22 -10
  15. package/dist/{vehicle-errors.js → errors/error.js} +20 -0
  16. package/dist/errors/index.d.ts +1 -0
  17. package/dist/errors/index.js +1 -0
  18. package/dist/events/event.d.ts +42 -0
  19. package/dist/events/event.js +35 -0
  20. package/dist/events/index.d.ts +1 -0
  21. package/dist/events/index.js +1 -0
  22. package/dist/idempotency/index.d.ts +1 -0
  23. package/dist/idempotency/index.js +1 -0
  24. package/dist/idempotency/receipt.d.ts +66 -0
  25. package/dist/idempotency/receipt.js +42 -0
  26. package/dist/index.d.ts +26 -7
  27. package/dist/index.js +26 -7
  28. package/dist/jobs/identity.d.ts +11 -0
  29. package/dist/jobs/identity.js +13 -0
  30. package/dist/jobs/index.d.ts +7 -0
  31. package/dist/jobs/index.js +7 -0
  32. package/dist/jobs/replay.d.ts +8 -0
  33. package/dist/jobs/replay.js +9 -0
  34. package/dist/jobs/retention.d.ts +26 -0
  35. package/dist/jobs/retention.js +39 -0
  36. package/dist/jobs/steer.d.ts +23 -0
  37. package/dist/jobs/steer.js +41 -0
  38. package/dist/jobs/termination.d.ts +5 -0
  39. package/dist/jobs/termination.js +11 -0
  40. package/dist/jobs/wake-log.d.ts +39 -0
  41. package/dist/jobs/wake-log.js +65 -0
  42. package/dist/jobs/wire.d.ts +47 -0
  43. package/dist/jobs/wire.js +1 -0
  44. package/dist/manifest/index.d.ts +1 -0
  45. package/dist/manifest/index.js +1 -0
  46. package/dist/manifest/manifest.d.ts +49 -0
  47. package/dist/manifest/manifest.js +1 -0
  48. package/dist/operations/context.d.ts +52 -0
  49. package/dist/operations/context.js +1 -0
  50. package/dist/operations/effect.d.ts +3 -0
  51. package/dist/operations/effect.js +2 -0
  52. package/dist/operations/index.d.ts +3 -0
  53. package/dist/operations/index.js +3 -0
  54. package/dist/operations/operation.d.ts +98 -0
  55. package/dist/operations/operation.js +79 -0
  56. package/dist/{atomic-json.d.ts → persistence/atomic-json.d.ts} +1 -2
  57. package/dist/{atomic-json.js → persistence/atomic-json.js} +1 -2
  58. package/dist/persistence/index.d.ts +1 -0
  59. package/dist/persistence/index.js +1 -0
  60. package/dist/schedules/index.d.ts +1 -0
  61. package/dist/schedules/index.js +1 -0
  62. package/dist/{vehicle-scheduler.d.ts → schedules/schedule.d.ts} +23 -9
  63. package/dist/schedules/schedule.js +61 -0
  64. package/dist/schemas/codec.d.ts +25 -0
  65. package/dist/schemas/codec.js +7 -0
  66. package/dist/schemas/index.d.ts +4 -0
  67. package/dist/schemas/index.js +4 -0
  68. package/dist/schemas/json.d.ts +7 -0
  69. package/dist/schemas/json.js +14 -0
  70. package/dist/schemas/loose-object.d.ts +20 -0
  71. package/dist/schemas/loose-object.js +73 -0
  72. package/dist/schemas/presentation.d.ts +11 -0
  73. package/dist/schemas/presentation.js +22 -0
  74. package/dist/watches/index.d.ts +1 -0
  75. package/dist/watches/index.js +1 -0
  76. package/dist/{vehicle-watchers.d.ts → watches/registry.d.ts} +2 -9
  77. package/dist/{vehicle-watchers.js → watches/registry.js} +2 -9
  78. package/package.json +5 -1
  79. package/src/{vehicle-approvals.ts → approvals/approval.ts} +47 -13
  80. package/src/approvals/index.ts +1 -0
  81. package/src/client/client.ts +31 -0
  82. package/src/client/index.ts +1 -0
  83. package/src/content/content.ts +46 -0
  84. package/src/content/index.ts +1 -0
  85. package/src/{vehicle-errors.ts → errors/error.ts} +32 -9
  86. package/src/errors/index.ts +1 -0
  87. package/src/events/event.ts +72 -0
  88. package/src/events/index.ts +1 -0
  89. package/src/idempotency/index.ts +1 -0
  90. package/src/idempotency/receipt.ts +97 -0
  91. package/src/index.ts +26 -7
  92. package/src/jobs/identity.ts +13 -0
  93. package/src/jobs/index.ts +7 -0
  94. package/src/jobs/replay.ts +17 -0
  95. package/src/jobs/retention.ts +59 -0
  96. package/src/jobs/steer.ts +49 -0
  97. package/src/jobs/termination.ts +13 -0
  98. package/src/jobs/wake-log.ts +98 -0
  99. package/src/jobs/wire.ts +51 -0
  100. package/src/manifest/index.ts +1 -0
  101. package/src/manifest/manifest.ts +52 -0
  102. package/src/operations/context.ts +56 -0
  103. package/src/operations/effect.ts +3 -0
  104. package/src/operations/index.ts +3 -0
  105. package/src/operations/operation.ts +184 -0
  106. package/src/{atomic-json.ts → persistence/atomic-json.ts} +1 -2
  107. package/src/persistence/index.ts +1 -0
  108. package/src/schedules/index.ts +1 -0
  109. package/src/{vehicle-scheduler.ts → schedules/schedule.ts} +32 -9
  110. package/src/schemas/codec.ts +30 -0
  111. package/src/schemas/index.ts +4 -0
  112. package/src/schemas/json.ts +17 -0
  113. package/src/schemas/loose-object.ts +82 -0
  114. package/src/schemas/presentation.ts +28 -0
  115. package/src/watches/index.ts +1 -0
  116. package/src/{vehicle-watchers.ts → watches/registry.ts} +2 -9
  117. package/dist/vehicle-contract.d.ts +0 -347
  118. package/dist/vehicle-contract.js +0 -219
  119. package/dist/vehicle-jobs.d.ts +0 -159
  120. package/dist/vehicle-jobs.js +0 -182
  121. package/dist/vehicle-scheduler.js +0 -33
  122. package/src/vehicle-contract.ts +0 -563
  123. package/src/vehicle-jobs.ts +0 -305
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
- * 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 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("./vehicle-contract.js").VehicleEvent<VehicleApprovalRequest>;
63
- export declare const vehicleApprovalResolvedEvent: import("./vehicle-contract.js").VehicleEvent<VehicleApprovalOutcome>;
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,29 @@
1
- import { defineVehicleEvent, defineVehicleSchema } from "./vehicle-contract.js";
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";
12
+ /** A sha256 hex digest: exactly 64 lowercase hex characters -- matches hashApprovalInput's own (vehicle-server) output shape. */
13
+ const SHA256_HEX_PATTERN = /^[0-9a-f]{64}$/;
14
+ function isVehicleEffect(value) {
15
+ return typeof value === "string" && VEHICLE_EFFECTS.includes(value);
16
+ }
17
+ function isVehiclePrincipal(value) {
18
+ if (typeof value !== "object" || value === null)
19
+ return false;
20
+ const candidate = value;
21
+ if (typeof candidate["id"] !== "string")
22
+ return false;
23
+ if (candidate["claims"] === undefined)
24
+ return true;
25
+ return typeof candidate["claims"] === "object" && candidate["claims"] !== null && !Array.isArray(candidate["claims"]);
26
+ }
2
27
  /** Set once at registry-configuration time (VehicleRegistry.configureApprovals()); never on a per-invoke basis. */
3
28
  export const DEFAULT_APPROVAL_EFFECTS = ["destructive", "open-world"];
4
29
  /**
@@ -21,7 +46,8 @@ const requestedPayloadSchema = defineVehicleSchema({
21
46
  requestId: { type: "string" },
22
47
  operationName: { type: "string" },
23
48
  operationVersion: { type: "number" },
24
- effect: { type: "string" },
49
+ effect: { type: "string", enum: [...VEHICLE_EFFECTS] },
50
+ principal: { type: "object" },
25
51
  requestedAt: { type: "number" },
26
52
  expiresAt: { type: "number" },
27
53
  inputHash: { type: "string" },
@@ -33,15 +59,23 @@ const requestedPayloadSchema = defineVehicleSchema({
33
59
  if (typeof value !== "object" || value === null)
34
60
  return { success: false, issues: [{ path: [], message: "input must be an object" }] };
35
61
  const row = value;
36
- if (typeof row.requestId !== "string" ||
37
- typeof row.operationName !== "string" ||
38
- typeof row.operationVersion !== "number" ||
39
- typeof row.effect !== "string" ||
40
- typeof row.requestedAt !== "number" ||
41
- typeof row.expiresAt !== "number" ||
42
- typeof row.inputHash !== "string") {
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"])) {
43
74
  return { success: false, issues: [{ path: [], message: "invalid approval request payload" }] };
44
75
  }
76
+ if (row["principal"] !== undefined && !isVehiclePrincipal(row["principal"])) {
77
+ return { success: false, issues: [{ path: ["principal"], message: "invalid approval request principal" }] };
78
+ }
45
79
  return { success: true, value: row };
46
80
  },
47
81
  });
@@ -62,11 +96,18 @@ const resolvedPayloadSchema = defineVehicleSchema({
62
96
  if (typeof value !== "object" || value === null)
63
97
  return { success: false, issues: [{ path: [], message: "input must be an object" }] };
64
98
  const row = value;
65
- if (typeof row.requestId !== "string" ||
66
- (row.decision !== "granted" && row.decision !== "denied") ||
67
- typeof row.decidedAt !== "number") {
99
+ if (typeof row["requestId"] !== "string" ||
100
+ (row["decision"] !== "granted" && row["decision"] !== "denied") ||
101
+ typeof row["decidedAt"] !== "number" ||
102
+ !Number.isFinite(row["decidedAt"])) {
68
103
  return { success: false, issues: [{ path: [], message: "invalid approval outcome payload" }] };
69
104
  }
105
+ if (row["decidedBy"] !== undefined && typeof row["decidedBy"] !== "string") {
106
+ return { success: false, issues: [{ path: ["decidedBy"], message: "decidedBy must be a string" }] };
107
+ }
108
+ if (row["comment"] !== undefined && typeof row["comment"] !== "string") {
109
+ return { success: false, issues: [{ path: ["comment"], message: "comment must be a string" }] };
110
+ }
70
111
  return { success: true, value: row };
71
112
  },
72
113
  });
@@ -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,6 +1,7 @@
1
- import type { JsonValue, VehicleSchemaIssue } from "./vehicle-contract.js";
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
- 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" | "client-closed" | "operation-unavailable" | "background-not-supported" | "job-not-found" | "job-not-steerable" | "job-steer-queue-full";
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 {
5
6
  readonly operation?: string;
6
7
  readonly message: string;
@@ -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,16 +52,27 @@ 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;
61
62
  }
62
63
  /** Recognizes VehicleError instances across duplicated package installations in one process. */
63
64
  export declare function isVehicleError(value: unknown): value is VehicleError;
65
+ /**
66
+ * Reconstructs a throwable VehicleError from a previously-serialized VehicleFailure -- the inverse
67
+ * of VehicleError.prototype.toFailure(), needed anywhere a wire-safe failure gets replayed as a
68
+ * real rejection later (e.g. VehicleIdempotencyPolicy replaying a settled failed receipt to a
69
+ * second caller reusing the same idempotency key). Lossy on purpose: a VehicleFailure never
70
+ * carries the original `cause` (toFailure() already reduced it to an optional bounded
71
+ * causeMessage per the throw site's own exposeCause choice), so the reconstructed error has no
72
+ * cause at all rather than fabricating one -- a replayed failure only needs to match the original
73
+ * code/category/message/details a caller would react to, not its internal cause chain.
74
+ */
75
+ export declare function vehicleErrorFromFailure(failure: VehicleFailure): VehicleError;
64
76
  /** Extracts a bounded, wire-safe message from an unknown cause -- never the full stack trace, never an unbounded payload. */
65
77
  export declare function boundedCauseMessage(cause: unknown): string | undefined;
66
78
  export declare function boundedValidationDetails(issues: readonly VehicleSchemaIssue[] | undefined): JsonValue | undefined;
@@ -60,6 +60,26 @@ export class VehicleError extends Error {
60
60
  export function isVehicleError(value) {
61
61
  return value instanceof Error && Reflect.get(value, VEHICLE_ERROR_BRAND) === true;
62
62
  }
63
+ /**
64
+ * Reconstructs a throwable VehicleError from a previously-serialized VehicleFailure -- the inverse
65
+ * of VehicleError.prototype.toFailure(), needed anywhere a wire-safe failure gets replayed as a
66
+ * real rejection later (e.g. VehicleIdempotencyPolicy replaying a settled failed receipt to a
67
+ * second caller reusing the same idempotency key). Lossy on purpose: a VehicleFailure never
68
+ * carries the original `cause` (toFailure() already reduced it to an optional bounded
69
+ * causeMessage per the throw site's own exposeCause choice), so the reconstructed error has no
70
+ * cause at all rather than fabricating one -- a replayed failure only needs to match the original
71
+ * code/category/message/details a caller would react to, not its internal cause chain.
72
+ */
73
+ export function vehicleErrorFromFailure(failure) {
74
+ return new VehicleError(failure.code, failure.message, {
75
+ category: failure.category,
76
+ retryable: failure.retryable,
77
+ retryAfterMs: failure.retryAfterMs,
78
+ recovery: failure.recovery,
79
+ details: failure.details,
80
+ operationId: failure.operationId,
81
+ });
82
+ }
63
83
  const MAX_CAUSE_MESSAGE_LENGTH = 500;
64
84
  /** Extracts a bounded, wire-safe message from an unknown cause -- never the full stack trace, never an unbounded payload. */
65
85
  export function boundedCauseMessage(cause) {
@@ -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";
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Pure pieces of Vehicle's bounded keyed-idempotency replay policy: the settled-receipt shape and
3
+ * its eviction-selection rule. Orchestration (in-flight dedup, persistence, fail-closed conflict
4
+ * detection) lives in vehicle-server's VehicleIdempotencyPolicy -- mirrors the vehicle-jobs.js /
5
+ * VehicleJobStore split (a pure, independently-testable bounded-retention rule here; the stateful
6
+ * store that calls it lives in vehicle-server).
7
+ */
8
+ import type { VehicleFailure } from "../errors/error.js";
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
+ export type VehicleIdempotencyResult = {
11
+ readonly ok: true;
12
+ readonly output: unknown;
13
+ } | {
14
+ readonly ok: false;
15
+ readonly failure: VehicleFailure;
16
+ };
17
+ /**
18
+ * One settled keyed-idempotency receipt. Deliberately excludes the original request's raw input --
19
+ * `inputHash` is the only trace of it retained, so a persisted receipt can never leak whatever the
20
+ * caller originally sent (which may carry sensitive arguments the operation's own output does not).
21
+ * A still-in-flight (pending) request has no receipt yet -- that bookkeeping is transient,
22
+ * in-memory-only state in VehicleIdempotencyPolicy, deliberately never a candidate for persistence
23
+ * or eviction (mirrors "a running job is never a candidate" in vehicle-jobs.js's own job eviction).
24
+ */
25
+ export interface VehicleIdempotencyReceipt {
26
+ readonly key: string;
27
+ readonly operationName: string;
28
+ readonly operationVersion: number;
29
+ readonly inputHash: string;
30
+ readonly settledAt: number;
31
+ /** settledAt + the descriptor's own keyed retentionMs at the time this receipt settled. A receipt past this is no longer a valid replay -- see selectVehicleIdempotencyReceiptsForEviction. */
32
+ readonly expiresAt: number;
33
+ readonly result: VehicleIdempotencyResult;
34
+ /** Approximate serialized size of `result`, used only to enforce maxTotalBytes -- never exact byte-for-byte, matching every other Vehicle capacity bound's own "good enough to stay bounded" precedent (e.g. enforcePayloadSize). */
35
+ readonly sizeBytes: number;
36
+ }
37
+ /** Minimal shape selectVehicleIdempotencyReceiptsForEviction needs -- kept separate from VehicleIdempotencyReceipt's own `result` so a sweep never has to touch (or risk logging) the actual settled output/failure it's merely deciding whether to keep. */
38
+ export interface VehicleIdempotencyEvictionCandidate {
39
+ readonly key: string;
40
+ readonly settledAt: number;
41
+ readonly expiresAt: number;
42
+ readonly sizeBytes: number;
43
+ }
44
+ export interface VehicleIdempotencyRetentionOptions {
45
+ /** Hard cap on total retained settled receipts. */
46
+ readonly maxEntries: number;
47
+ /** Hard cap on the sum of every retained receipt's own sizeBytes. */
48
+ readonly maxTotalBytes: number;
49
+ readonly now: number;
50
+ }
51
+ /**
52
+ * Pure eviction-selection policy for settled keyed-idempotency receipts, independently testable
53
+ * from VehicleIdempotencyPolicy's own bookkeeping -- mirrors selectVehicleJobsForEviction's own
54
+ * three-phase shape:
55
+ *
56
+ * 1. Any receipt already past its own `expiresAt` (a real per-operation retentionMs elapsed --
57
+ * replaying it would no longer be correct, keeping it around would only be wasted memory).
58
+ * 2. If still over maxEntries once (1) is applied, the oldest remaining receipts by settledAt,
59
+ * until back within budget.
60
+ * 3. If still over maxTotalBytes once (1)+(2) are applied, the oldest remaining receipts by
61
+ * settledAt, until back within budget.
62
+ *
63
+ * A pending (still in-flight) request is never a candidate -- it has no receipt yet, so it can
64
+ * never appear in `candidates` at all; this function only ever sees settled ones.
65
+ */
66
+ export declare function selectVehicleIdempotencyReceiptsForEviction(candidates: readonly VehicleIdempotencyEvictionCandidate[], options: VehicleIdempotencyRetentionOptions): readonly string[];
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Pure eviction-selection policy for settled keyed-idempotency receipts, independently testable
3
+ * from VehicleIdempotencyPolicy's own bookkeeping -- mirrors selectVehicleJobsForEviction's own
4
+ * three-phase shape:
5
+ *
6
+ * 1. Any receipt already past its own `expiresAt` (a real per-operation retentionMs elapsed --
7
+ * replaying it would no longer be correct, keeping it around would only be wasted memory).
8
+ * 2. If still over maxEntries once (1) is applied, the oldest remaining receipts by settledAt,
9
+ * until back within budget.
10
+ * 3. If still over maxTotalBytes once (1)+(2) are applied, the oldest remaining receipts by
11
+ * settledAt, until back within budget.
12
+ *
13
+ * A pending (still in-flight) request is never a candidate -- it has no receipt yet, so it can
14
+ * never appear in `candidates` at all; this function only ever sees settled ones.
15
+ */
16
+ export function selectVehicleIdempotencyReceiptsForEviction(candidates, options) {
17
+ const byAgeAscending = (a, b) => a.settledAt - b.settledAt;
18
+ const evicted = new Set();
19
+ for (const candidate of candidates) {
20
+ if (options.now >= candidate.expiresAt)
21
+ evicted.add(candidate.key);
22
+ }
23
+ const remaining = () => candidates.filter((candidate) => !evicted.has(candidate.key));
24
+ if (remaining().length > options.maxEntries) {
25
+ const oldestFirst = remaining().sort(byAgeAscending);
26
+ for (const candidate of oldestFirst) {
27
+ if (remaining().length <= options.maxEntries)
28
+ break;
29
+ evicted.add(candidate.key);
30
+ }
31
+ }
32
+ const totalBytes = () => remaining().reduce((sum, candidate) => sum + candidate.sizeBytes, 0);
33
+ if (totalBytes() > options.maxTotalBytes) {
34
+ const oldestFirst = remaining().sort(byAgeAscending);
35
+ for (const candidate of oldestFirst) {
36
+ if (totalBytes() <= options.maxTotalBytes)
37
+ break;
38
+ evicted.add(candidate.key);
39
+ }
40
+ }
41
+ return [...evicted];
42
+ }
package/dist/index.d.ts CHANGED
@@ -1,7 +1,26 @@
1
- export * from "./atomic-json.js";
2
- export * from "./vehicle-approvals.js";
3
- export * from "./vehicle-contract.js";
4
- export * from "./vehicle-errors.js";
5
- export * from "./vehicle-jobs.js";
6
- export * from "./vehicle-scheduler.js";
7
- export * from "./vehicle-watchers.js";
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,7 +1,26 @@
1
- export * from "./atomic-json.js";
2
- export * from "./vehicle-approvals.js";
3
- export * from "./vehicle-contract.js";
4
- export * from "./vehicle-errors.js";
5
- export * from "./vehicle-jobs.js";
6
- export * from "./vehicle-scheduler.js";
7
- export * from "./vehicle-watchers.js";
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;