@danypops/vehicle-core 0.17.0 → 0.18.0

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 (125) 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} +33 -23
  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} +10 -9
  15. package/dist/errors/index.d.ts +1 -0
  16. package/dist/errors/index.js +1 -0
  17. package/dist/events/event.d.ts +42 -0
  18. package/dist/events/event.js +35 -0
  19. package/dist/events/index.d.ts +1 -0
  20. package/dist/events/index.js +1 -0
  21. package/dist/idempotency/index.d.ts +1 -0
  22. package/dist/idempotency/index.js +1 -0
  23. package/dist/{vehicle-idempotency.d.ts → idempotency/receipt.d.ts} +1 -1
  24. package/dist/index.d.ts +26 -8
  25. package/dist/index.js +26 -8
  26. package/dist/jobs/grant.d.ts +36 -0
  27. package/dist/jobs/grant.js +31 -0
  28. package/dist/jobs/identity.d.ts +11 -0
  29. package/dist/jobs/identity.js +13 -0
  30. package/dist/jobs/index.d.ts +8 -0
  31. package/dist/jobs/index.js +8 -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} +6 -8
  63. package/dist/schemas/codec.d.ts +25 -0
  64. package/dist/schemas/codec.js +7 -0
  65. package/dist/schemas/index.d.ts +4 -0
  66. package/dist/schemas/index.js +4 -0
  67. package/dist/schemas/json.d.ts +7 -0
  68. package/dist/schemas/json.js +14 -0
  69. package/dist/schemas/loose-object.d.ts +20 -0
  70. package/dist/schemas/loose-object.js +73 -0
  71. package/dist/schemas/presentation.d.ts +11 -0
  72. package/dist/schemas/presentation.js +22 -0
  73. package/dist/watches/index.d.ts +1 -0
  74. package/dist/watches/index.js +1 -0
  75. package/dist/{vehicle-watchers.d.ts → watches/registry.d.ts} +2 -9
  76. package/dist/{vehicle-watchers.js → watches/registry.js} +2 -9
  77. package/package.json +5 -1
  78. package/src/{vehicle-approvals.ts → approvals/approval.ts} +27 -24
  79. package/src/approvals/index.ts +1 -0
  80. package/src/client/client.ts +31 -0
  81. package/src/client/index.ts +1 -0
  82. package/src/content/content.ts +46 -0
  83. package/src/content/index.ts +1 -0
  84. package/src/{vehicle-errors.ts → errors/error.ts} +10 -9
  85. package/src/errors/index.ts +1 -0
  86. package/src/events/event.ts +72 -0
  87. package/src/events/index.ts +1 -0
  88. package/src/idempotency/index.ts +1 -0
  89. package/src/{vehicle-idempotency.ts → idempotency/receipt.ts} +1 -1
  90. package/src/index.ts +26 -8
  91. package/src/jobs/grant.ts +53 -0
  92. package/src/jobs/identity.ts +13 -0
  93. package/src/jobs/index.ts +8 -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} +6 -8
  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 -353
  118. package/dist/vehicle-contract.js +0 -253
  119. package/dist/vehicle-jobs.d.ts +0 -159
  120. package/dist/vehicle-jobs.js +0 -182
  121. package/src/vehicle-contract.ts +0 -597
  122. package/src/vehicle-jobs.ts +0 -305
  123. /package/dist/{vehicle-errors.js → errors/error.js} +0 -0
  124. /package/dist/{vehicle-idempotency.js → idempotency/receipt.js} +0 -0
  125. /package/dist/{vehicle-scheduler.js → schedules/schedule.js} +0 -0
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The Grant primitive's own wire-neutral shape: a resource budget bounding how much an
3
+ * already-authorized, already-running long-running operation may still do before it must ask
4
+ * for more. Mirrors VehicleJobWakeBudget's own convention (a plain, optional-per-dimension
5
+ * object, not a class) -- see wake-log.ts's own doc comment for that precedent. Every dimension
6
+ * is independently optional: a caller states only the ceilings that matter for its own operation
7
+ * (an operation with no real notion of "tokens" simply never sets maxTokens).
8
+ *
9
+ * Deliberately agent-flavored (turns/tool-calls/tokens) rather than a generic {maxCount, maxBytes}
10
+ * pair the way VehicleJobWakeBudget is -- Vehicle was built specifically for agent consumers
11
+ * (vehicle-client-pi exists precisely to project operations as Pi tools), so this vocabulary
12
+ * belongs here rather than being reinvented per-consumer.
13
+ */
14
+ export interface VehicleGrantBudget {
15
+ readonly maxTurns?: number;
16
+ readonly maxToolCalls?: number;
17
+ readonly maxTokens?: number;
18
+ readonly maxWallClockMs?: number;
19
+ }
20
+
21
+ /**
22
+ * True the moment any one *set* dimension reaches zero or below -- the tightest dimension
23
+ * governs, not an average or a sum. A dimension the caller never set imposes no ceiling of its
24
+ * own. An entirely empty budget ({}) is never exhausted: that's an unbounded grant (every
25
+ * dimension omitted), not a zero one -- a caller that wants "no more of anything" states at
26
+ * least one dimension as 0, it doesn't rely on {} meaning that.
27
+ */
28
+ export function grantBudgetExhausted(remaining: VehicleGrantBudget): boolean {
29
+ return (
30
+ (remaining.maxTurns !== undefined && remaining.maxTurns <= 0) ||
31
+ (remaining.maxToolCalls !== undefined && remaining.maxToolCalls <= 0) ||
32
+ (remaining.maxTokens !== undefined && remaining.maxTokens <= 0) ||
33
+ (remaining.maxWallClockMs !== undefined && remaining.maxWallClockMs <= 0)
34
+ );
35
+ }
36
+
37
+ /**
38
+ * Adds a top-up onto the current remaining budget, dimension by dimension. A dimension absent
39
+ * from the top-up is left exactly as it was; a dimension the current budget never had but the
40
+ * top-up introduces is taken as-is (not added to an implicit 0, since the current budget's own
41
+ * "never set" already means unbounded for that dimension -- introducing a ceiling for the first
42
+ * time via a top-up is a real, deliberate narrowing a caller must do explicitly, not an artifact
43
+ * of the merge itself).
44
+ */
45
+ export function mergeGrantBudget(current: VehicleGrantBudget, additional: VehicleGrantBudget): VehicleGrantBudget {
46
+ const merged: { -readonly [K in keyof VehicleGrantBudget]?: number } = { ...current };
47
+ for (const key of ["maxTurns", "maxToolCalls", "maxTokens", "maxWallClockMs"] as const) {
48
+ const addition = additional[key];
49
+ if (addition === undefined) continue;
50
+ merged[key] = (current[key] ?? 0) + addition;
51
+ }
52
+ return merged;
53
+ }
@@ -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: string, currentInstanceToken: string): boolean {
12
+ return recordInstanceToken === currentInstanceToken;
13
+ }
@@ -0,0 +1,8 @@
1
+ export * from "./grant.js";
2
+ export * from "./identity.js";
3
+ export * from "./replay.js";
4
+ export * from "./retention.js";
5
+ export * from "./steer.js";
6
+ export * from "./termination.js";
7
+ export * from "./wake-log.js";
8
+ export * from "./wire.js";
@@ -0,0 +1,17 @@
1
+ import type { VehicleJobWakeEntry } from "./wake-log.js";
2
+
3
+ /** 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. */
4
+ export interface VehicleJobWakeLogReader {
5
+ since(cursor: number): readonly VehicleJobWakeEntry[];
6
+ readonly cursor: number;
7
+ }
8
+
9
+ /** 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. */
10
+ export function createStaticVehicleJobWakeLog(entries: readonly VehicleJobWakeEntry[]): VehicleJobWakeLogReader {
11
+ const sorted = [...entries].sort((a, b) => a.seq - b.seq);
12
+ const cursor = sorted.length > 0 ? sorted[sorted.length - 1]!.seq : 0;
13
+ return {
14
+ since: (cursorArg) => sorted.filter((entry) => entry.seq > cursorArg),
15
+ cursor,
16
+ };
17
+ }
@@ -0,0 +1,59 @@
1
+ import type { VehicleJobStatus } from "./termination.js";
2
+
3
+ /** 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". */
4
+ export interface VehicleJobEvictionCandidate {
5
+ readonly jobId: string;
6
+ readonly status: VehicleJobStatus;
7
+ readonly delivered: boolean;
8
+ readonly updatedAt: number;
9
+ }
10
+
11
+ export interface VehicleJobRetentionOptions {
12
+ /** Hard cap on total retained job records (of any status). A running job is never evicted regardless of this cap. */
13
+ readonly maxRetainedJobs: number;
14
+ /** A delivered terminal job becomes eligible for eviction once this many ms have passed since it was delivered (== updatedAt at delivery time). */
15
+ readonly deliveredRetentionMs: number;
16
+ readonly now: number;
17
+ }
18
+
19
+ /**
20
+ * Pure eviction-selection policy, kept separate from VehicleJobStore's own
21
+ * bookkeeping so the bounded-retention rule is independently testable.
22
+ * Preference order: (1) delivered and past deliveredRetentionMs, oldest
23
+ * first; (2) once still over maxRetainedJobs, any delivered terminal job,
24
+ * oldest first; (3) only as a last resort, an undelivered terminal job,
25
+ * oldest first -- a real loss (a caller may still want that result), but
26
+ * an unbounded store is a worse failure mode. A running job is never a
27
+ * candidate.
28
+ */
29
+ export function selectVehicleJobsForEviction(
30
+ candidates: readonly VehicleJobEvictionCandidate[],
31
+ options: VehicleJobRetentionOptions,
32
+ ): readonly string[] {
33
+ const terminal = candidates.filter((candidate) => candidate.status !== "running");
34
+ const byAgeAscending = (a: VehicleJobEvictionCandidate, b: VehicleJobEvictionCandidate) => a.updatedAt - b.updatedAt;
35
+
36
+ const evicted = new Set<string>();
37
+ for (const candidate of terminal) {
38
+ if (candidate.delivered && options.now - candidate.updatedAt >= options.deliveredRetentionMs) evicted.add(candidate.jobId);
39
+ }
40
+
41
+ const remainingCount = () => candidates.length - evicted.size;
42
+ if (remainingCount() > options.maxRetainedJobs) {
43
+ const deliveredOldestFirst = terminal.filter((candidate) => candidate.delivered && !evicted.has(candidate.jobId)).sort(byAgeAscending);
44
+ for (const candidate of deliveredOldestFirst) {
45
+ if (remainingCount() <= options.maxRetainedJobs) break;
46
+ evicted.add(candidate.jobId);
47
+ }
48
+ }
49
+ if (remainingCount() > options.maxRetainedJobs) {
50
+ const undeliveredOldestFirst = terminal
51
+ .filter((candidate) => !candidate.delivered && !evicted.has(candidate.jobId))
52
+ .sort(byAgeAscending);
53
+ for (const candidate of undeliveredOldestFirst) {
54
+ if (remainingCount() <= options.maxRetainedJobs) break;
55
+ evicted.add(candidate.jobId);
56
+ }
57
+ }
58
+ return [...evicted];
59
+ }
@@ -0,0 +1,49 @@
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
+
14
+ export class VehicleJobSteerChannel implements AsyncIterable<unknown> {
15
+ private readonly buffer: unknown[] = [];
16
+ private readonly waiters: ((result: IteratorResult<unknown>) => void)[] = [];
17
+ private closed = false;
18
+
19
+ constructor(private readonly maxQueueSize: number = 64) {}
20
+
21
+ push(value: unknown): VehicleJobSteerPushResult {
22
+ if (this.closed) return { accepted: false, dropReason: "channel-closed" };
23
+ const waiter = this.waiters.shift();
24
+ if (waiter) {
25
+ waiter({ value, done: false });
26
+ return { accepted: true };
27
+ }
28
+ if (this.buffer.length >= this.maxQueueSize) return { accepted: false, dropReason: "queue-full" };
29
+ this.buffer.push(value);
30
+ return { accepted: true };
31
+ }
32
+
33
+ /** Ends every pending and future iteration with done:true; further push() calls report "channel-closed". Idempotent. */
34
+ close(): void {
35
+ if (this.closed) return;
36
+ this.closed = true;
37
+ for (const waiter of this.waiters.splice(0)) waiter({ value: undefined, done: true });
38
+ }
39
+
40
+ [Symbol.asyncIterator](): AsyncIterator<unknown> {
41
+ return {
42
+ next: (): Promise<IteratorResult<unknown>> => {
43
+ if (this.buffer.length > 0) return Promise.resolve({ value: this.buffer.shift(), done: false });
44
+ if (this.closed) return Promise.resolve({ value: undefined, done: true });
45
+ return new Promise((resolve) => this.waiters.push(resolve));
46
+ },
47
+ };
48
+ }
49
+ }
@@ -0,0 +1,13 @@
1
+ export type VehicleJobStatus = "running" | "succeeded" | "failed" | "canceled";
2
+
3
+ /** Highest precedence first -- an explicit cancel always wins even if the handler also settled around the same time. "orphaned" is a restart-reconciliation outcome: a job that was still "running" when its process died, so nothing ever really failed or succeeded -- the record's own status just goes stale. */
4
+ export const VEHICLE_JOB_TERMINATION_PRECEDENCE = ["canceled", "timeout", "orphaned", "failed", "succeeded"] as const;
5
+ export type VehicleJobTerminationReason = (typeof VEHICLE_JOB_TERMINATION_PRECEDENCE)[number];
6
+
7
+ export function resolveVehicleJobTerminationReason(candidates: readonly VehicleJobTerminationReason[]): VehicleJobTerminationReason {
8
+ if (candidates.length === 0) throw new Error("resolveVehicleJobTerminationReason requires at least one candidate");
9
+ for (const reason of VEHICLE_JOB_TERMINATION_PRECEDENCE) {
10
+ if (candidates.includes(reason)) return reason;
11
+ }
12
+ throw new Error(`Unrecognized Vehicle job termination candidate(s): ${candidates.join(", ")}`);
13
+ }
@@ -0,0 +1,98 @@
1
+ /** "always" keeps every notification; "transition" drops one identical to the last (hash dedup); "first-only" keeps just the first. */
2
+ export type VehicleJobNotifyMode = "always" | "transition" | "first-only";
3
+
4
+ export interface VehicleJobWakeBudget {
5
+ readonly maxCount: number;
6
+ readonly maxBytes: number;
7
+ }
8
+
9
+ export type VehicleJobWakeDropReason =
10
+ | "count-budget-exhausted"
11
+ | "byte-budget-exhausted"
12
+ | "deduplicated-transition"
13
+ | "superseded-by-first-only";
14
+
15
+ export interface VehicleJobWakeEntry {
16
+ readonly seq: number;
17
+ readonly at: number;
18
+ readonly progress: unknown;
19
+ }
20
+
21
+ export interface VehicleJobWakeAppendResult {
22
+ readonly accepted: boolean;
23
+ readonly entry?: VehicleJobWakeEntry;
24
+ readonly dropReason?: VehicleJobWakeDropReason;
25
+ }
26
+
27
+ export interface VehicleJobWakeLogOptions {
28
+ readonly notifyMode: VehicleJobNotifyMode;
29
+ readonly budget: VehicleJobWakeBudget;
30
+ /** Defaults to Date.now. */
31
+ readonly now?: () => number;
32
+ }
33
+
34
+ function safeJsonStringify(value: unknown): string {
35
+ let serialized: string | undefined;
36
+ try {
37
+ serialized = JSON.stringify(value);
38
+ } catch (error) {
39
+ throw new Error("Vehicle job progress value is not JSON-serializable", { cause: error });
40
+ }
41
+ if (serialized === undefined) throw new Error("Vehicle job progress value is not JSON-serializable");
42
+ return serialized;
43
+ }
44
+
45
+ /** Non-cryptographic (FNV-1a) -- dedup only. */
46
+ function fnv1aHash(value: string): string {
47
+ let hash = 0x811c9dc5;
48
+ for (let i = 0; i < value.length; i++) {
49
+ hash ^= value.charCodeAt(i);
50
+ hash = Math.imul(hash, 0x01000193);
51
+ }
52
+ return (hash >>> 0).toString(16);
53
+ }
54
+
55
+ /** Bounds a job's accumulated progress notifications by count+bytes, same discipline as enforcePayloadSize but across a job's whole lifetime. */
56
+ export class VehicleJobWakeLog {
57
+ private readonly entries: VehicleJobWakeEntry[] = [];
58
+ private usedBytes = 0;
59
+ private nextSeq = 1;
60
+ private lastHash: string | undefined;
61
+ private acceptedFirst = false;
62
+ private readonly now: () => number;
63
+
64
+ constructor(private readonly options: VehicleJobWakeLogOptions) {
65
+ this.now = options.now ?? Date.now;
66
+ }
67
+
68
+ append(progress: unknown): VehicleJobWakeAppendResult {
69
+ if (this.options.notifyMode === "first-only" && this.acceptedFirst) {
70
+ return { accepted: false, dropReason: "superseded-by-first-only" };
71
+ }
72
+ const serialized = safeJsonStringify(progress);
73
+ if (this.options.notifyMode === "transition") {
74
+ const hash = fnv1aHash(serialized);
75
+ if (hash === this.lastHash) return { accepted: false, dropReason: "deduplicated-transition" };
76
+ this.lastHash = hash;
77
+ }
78
+ const bytes = new TextEncoder().encode(serialized).byteLength;
79
+ if (this.entries.length >= this.options.budget.maxCount) return { accepted: false, dropReason: "count-budget-exhausted" };
80
+ if (this.usedBytes + bytes > this.options.budget.maxBytes) return { accepted: false, dropReason: "byte-budget-exhausted" };
81
+
82
+ const entry: VehicleJobWakeEntry = { seq: this.nextSeq++, at: this.now(), progress };
83
+ this.entries.push(entry);
84
+ this.usedBytes += bytes;
85
+ this.acceptedFirst = true;
86
+ return { accepted: true, entry };
87
+ }
88
+
89
+ /** Entries with seq strictly greater than `cursor`. */
90
+ since(cursor: number): readonly VehicleJobWakeEntry[] {
91
+ return this.entries.filter((entry) => entry.seq > cursor);
92
+ }
93
+
94
+ /** Highest seq issued so far (0 if none accepted yet). */
95
+ get cursor(): number {
96
+ return this.nextSeq - 1;
97
+ }
98
+ }
@@ -0,0 +1,51 @@
1
+ import type { VehicleFailure } from "../errors/error.js";
2
+ import type { VehiclePrincipal } from "../operations/context.js";
3
+ import type { VehicleJobStatus, VehicleJobTerminationReason } from "./termination.js";
4
+ import type { VehicleJobNotifyMode, VehicleJobWakeBudget, VehicleJobWakeEntry } from "./wake-log.js";
5
+
6
+ /**
7
+ * The client-facing wire shapes for Vehicle Jobs -- submit/poll/tail options and results, shared by
8
+ * vehicle-server's VehicleJobStore (the orchestration side) and vehicle-client's job-capable clients
9
+ * (the calling side), so both halves of the wire agree on one definition instead of two structurally
10
+ * -identical copies drifting apart. Every field type referenced here already lives in vehicle-core
11
+ * (VehiclePrincipal, VehicleFailure, VehicleJobStatus, ...), which is what makes it safe for these
12
+ * shapes to live here too, alongside the rest of Vehicle Jobs' pure pieces.
13
+ */
14
+ export interface VehicleJobSubmitOptions {
15
+ readonly permissions?: readonly string[];
16
+ readonly principal?: VehiclePrincipal;
17
+ readonly idempotencyKey?: string;
18
+ readonly expectedRevision?: string | number;
19
+ readonly approvalCapability?: string;
20
+ readonly correlationId?: string;
21
+ readonly callerSessionId?: string;
22
+ readonly callerProjectRoot?: string;
23
+ /** Defaults to "transition". */
24
+ readonly notifyMode?: VehicleJobNotifyMode;
25
+ /** Defaults to background.defaultWakeBudget; clamped to background.maxWakeBudget either way. */
26
+ readonly wakeBudget?: VehicleJobWakeBudget;
27
+ /** No default -- unset means the job runs until it settles or is canceled. */
28
+ readonly maxLifetimeMs?: number;
29
+ }
30
+
31
+ export interface VehicleJobSubmitResult {
32
+ readonly jobId: string;
33
+ }
34
+
35
+ export interface VehicleJobSnapshot {
36
+ readonly jobId: string;
37
+ readonly operationName: string;
38
+ readonly operationVersion: number;
39
+ readonly status: VehicleJobStatus;
40
+ readonly createdAt: number;
41
+ readonly updatedAt: number;
42
+ readonly delivered: boolean;
43
+ readonly terminationReason?: VehicleJobTerminationReason;
44
+ readonly output?: unknown;
45
+ readonly error?: VehicleFailure;
46
+ }
47
+
48
+ export interface VehicleJobTailResult {
49
+ readonly entries: readonly VehicleJobWakeEntry[];
50
+ readonly cursor: number;
51
+ }
@@ -0,0 +1 @@
1
+ export * from "./manifest.js";
@@ -0,0 +1,52 @@
1
+ import type { VehicleManifestEvent } from "../events/event.js";
2
+ import type { VehicleOperationDescriptor } from "../operations/operation.js";
3
+
4
+ export interface VehicleManifestIdentity {
5
+ readonly name: string;
6
+ readonly version: string;
7
+ readonly description: string;
8
+ readonly guidance?: readonly string[];
9
+ }
10
+
11
+ /**
12
+ * A manifest's own view of an operation: the static descriptor plus
13
+ * whether it's currently usable on this particular server instance right
14
+ * now. Availability is a runtime property of a live registry (a
15
+ * credential got configured or removed), never baked into the static
16
+ * descriptor defineVehicleOperation() produces -- two manifest() calls
17
+ * against the same registry can report different availability for the
18
+ * exact same descriptor.
19
+ */
20
+ export interface VehicleManifestOperation extends VehicleOperationDescriptor {
21
+ readonly available: boolean;
22
+ readonly unavailableReason?: string;
23
+ /**
24
+ * The registry's own, live, fully-resolved answer to "does invoking this operation right
25
+ * now require approval" -- accounts for the registry's current approval policy being
26
+ * enabled/disabled, this operation's own `requiresApproval` override when set, and the
27
+ * effect-derived default otherwise. A real VehicleRegistry.manifest() always sets this
28
+ * (false when the registry never called configureApprovals() at all) -- unlike
29
+ * `requiresApproval` (the static, author-declared override on the descriptor itself),
30
+ * this always reflects the current instant, so a client re-fetching the manifest after a
31
+ * live policy change (VehicleRegistry.updateApprovalPolicy) sees the new answer with no
32
+ * separate sync mechanism needed.
33
+ *
34
+ * Optional purely for backward compatibility with every hand-authored VehicleManifest
35
+ * test fixture across the ecosystem that predates this field (the same reason
36
+ * VehicleManifest.events is optional) -- a consumer reading it should treat undefined the
37
+ * same as a caller of classifyVehicleOperationSafety does: fall back to the effect-level
38
+ * default, never assume false.
39
+ */
40
+ readonly approvalRequired?: boolean;
41
+ }
42
+
43
+ /**
44
+ * `events` is optional purely for backward compatibility with every
45
+ * hand-authored VehicleManifest test fixture across the ecosystem that
46
+ * predates this field -- a real VehicleRegistry.manifest() always
47
+ * populates it (as [] when no events are declared), never omits it.
48
+ */
49
+ export interface VehicleManifest extends VehicleManifestIdentity {
50
+ readonly operations: readonly VehicleManifestOperation[];
51
+ readonly events?: readonly VehicleManifestEvent[];
52
+ }
@@ -0,0 +1,56 @@
1
+ import type { JsonValue } from "../schemas/json.js";
2
+
3
+ export interface VehiclePrincipal {
4
+ readonly id: string;
5
+ readonly claims?: Readonly<Record<string, JsonValue>>;
6
+ }
7
+
8
+ /**
9
+ * callerSessionId/callerProjectRoot identify the real host session (e.g. one Pi TUI process) that
10
+ * originated this call, and its working directory at call time -- a generic ownership/attribution
11
+ * hook any operation handler can read (e.g. scoping a background subscription to the session or
12
+ * project that created it), distinct from both:
13
+ * - correlationId: a caller-CHOSEN id deliberately meant to span several separate invoke() calls
14
+ * (a batch/business-transaction id), not an automatically-derived caller identity.
15
+ * - principal: broader identity/claims used for permission and approval decisions, usually a
16
+ * fixed per-extension value (e.g. {id: "pi-pipes"}), not a distinguishing per-session id.
17
+ * A Pi projection layer (see vehicle-client-pi's invokeVehicleOperation) auto-derives both from
18
+ * context.sessionManager.getSessionId()/context.cwd on every call, the same way it already
19
+ * auto-derives correlationId -- a handler that never reads them pays nothing extra.
20
+ */
21
+ export interface VehicleInvocationOptions {
22
+ readonly operationId?: string;
23
+ readonly correlationId?: string;
24
+ readonly callerSessionId?: string;
25
+ readonly callerProjectRoot?: string;
26
+ readonly signal?: AbortSignal;
27
+ readonly deadline?: number;
28
+ readonly permissions?: readonly string[];
29
+ readonly principal?: VehiclePrincipal;
30
+ readonly idempotencyKey?: string;
31
+ readonly expectedRevision?: string | number;
32
+ readonly approvalCapability?: string;
33
+ readonly onProgress?: (progress: unknown) => void;
34
+ }
35
+
36
+ export interface VehicleOperationContext<Input> {
37
+ readonly input: Input;
38
+ readonly operationId: string;
39
+ readonly correlationId?: string;
40
+ /** See VehicleInvocationOptions's own doc comment. */
41
+ readonly callerSessionId?: string;
42
+ /** See VehicleInvocationOptions's own doc comment. */
43
+ readonly callerProjectRoot?: string;
44
+ readonly signal: AbortSignal;
45
+ readonly deadline: number;
46
+ readonly permissions: readonly string[];
47
+ readonly principal?: VehiclePrincipal;
48
+ readonly idempotencyKey?: string;
49
+ readonly expectedRevision?: string | number;
50
+ readonly approvalCapability?: string;
51
+ /** Set only for a job execution (VehicleJobStore.submit()); undefined for a plain invoke(). A handler that wants mid-flight input opts in with `for await (const input of context.steerInputs ?? [])`. */
52
+ readonly steerInputs?: AsyncIterable<unknown>;
53
+ reportProgress(progress: unknown): void;
54
+ }
55
+
56
+ export type VehicleOperationHandler<Input, Output> = (context: VehicleOperationContext<Input>) => Promise<Output>;
@@ -0,0 +1,3 @@
1
+ /** Canonical list VehicleEffect is itself derived from, so a runtime discriminator check (e.g. a persisted/wire VehicleApprovalRequest's own `effect` field) has one real source to check against instead of a second, driftable hardcoded list. */
2
+ export const VEHICLE_EFFECTS = ["read", "local-write", "external-write", "destructive", "open-world"] as const;
3
+ export type VehicleEffect = (typeof VEHICLE_EFFECTS)[number];
@@ -0,0 +1,3 @@
1
+ export * from "./context.js";
2
+ export * from "./effect.js";
3
+ export * from "./operation.js";