@danypops/vehicle-core 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -10,6 +10,15 @@ bun add @danypops/vehicle-core
10
10
 
11
11
  `defineVehicleOperation()`/`bindVehicleOperation()` build a serializable
12
12
  descriptor kept separate from its executable handler. See the
13
- [workspace README](https://github.com/DanyPops/daemon-kit#readme) for how it
13
+ [workspace README](https://github.com/DanyPops/vehicle#readme) for how it
14
14
  fits with `@danypops/vehicle-server`, `@danypops/vehicle-client`, and
15
15
  `@danypops/vehicle-client-pi`.
16
+
17
+ An operation whose result should be read as a narrative rather than parsed
18
+ as data can intersect its Output type with `WithVehicleContent` and include
19
+ a `content: [{ type: "text", text }]` field alongside its own domain data --
20
+ the same field name and shape MCP's `CallToolResult.content` and Pi's own
21
+ tool-result type use, so no translation layer is needed at either boundary.
22
+ `extractVehicleContent(output)` reads those blocks back out for a generic
23
+ Vehicle client to prefer over raw JSON, returning undefined for absent or
24
+ malformed content so the caller can fall back safely.
@@ -19,6 +19,53 @@ export interface VehicleSchemaCodec<T> {
19
19
  safeParse(value: unknown): VehicleSchemaResult<T>;
20
20
  }
21
21
  export declare function defineVehicleSchema<T>(codec: VehicleSchemaCodec<T>): VehicleSchemaCodec<T>;
22
+ export interface LooseObjectProperty {
23
+ readonly type: string;
24
+ readonly enum?: readonly string[];
25
+ }
26
+ /**
27
+ * A VehicleRegistry only ever calls a schema's own safeParse -- jsonSchema is
28
+ * descriptive metadata surfaced to a client/Pi projection, never itself
29
+ * enforced at runtime -- so a declared `enum` has to be checked here for
30
+ * real, or it's a documentation gesture, not an honest contract. Every
31
+ * consumer projecting a plain-object input onto a VehicleOperation needs the
32
+ * same required/enum checks; this is that check written once.
33
+ */
34
+ export declare function defineLooseObjectSchema(properties: Record<string, LooseObjectProperty>, required?: readonly string[]): VehicleSchemaCodec<Record<string, unknown>>;
35
+ /** Accepts any value unvalidated -- for an operation whose output shape isn't worth a dedicated schema (an internal/low-stakes result, or one already validated upstream by the domain logic it wraps). */
36
+ export declare const passthroughVehicleSchema: VehicleSchemaCodec<unknown>;
37
+ /**
38
+ * A block of narrative text meant to be read by the model, not parsed as
39
+ * data -- same field name and shape MCP's own CallToolResult.content and
40
+ * Pi's own ToolDefinition.execute() return already use, so a Vehicle
41
+ * operation adopting this needs no translation layer at either boundary.
42
+ * Only the "text" variant exists here; there's no Vehicle use case yet for
43
+ * MCP's image/audio/resource-link block kinds.
44
+ */
45
+ export interface VehicleContentBlock {
46
+ readonly type: "text";
47
+ readonly text: string;
48
+ }
49
+ /**
50
+ * An operation's Output type can intersect this to carry its own
51
+ * model-facing narrative alongside its structured data, e.g.
52
+ * `type RunOutput = { runId: string; created: Task[] } & WithVehicleContent`.
53
+ * The operation itself builds `content` since it's the only code that
54
+ * actually knows how to describe what it computed -- never a per-consumer
55
+ * override bolted on wherever the operation happens to get registered.
56
+ */
57
+ export interface WithVehicleContent {
58
+ readonly content?: readonly VehicleContentBlock[];
59
+ }
60
+ /**
61
+ * Reads an operation's own `content` blocks off its output when present and
62
+ * well-formed, so a generic Vehicle client can prefer them over dumping raw
63
+ * JSON at the model -- without knowing anything about the operation's own
64
+ * domain shape. Returns undefined for a malformed or absent `content` field;
65
+ * the caller falls back to its own default (formatted JSON) rather than
66
+ * risk forwarding partial/garbled blocks.
67
+ */
68
+ export declare function extractVehicleContent(output: unknown): readonly VehicleContentBlock[] | undefined;
22
69
  export type VehicleEffect = "read" | "local-write" | "external-write" | "destructive" | "open-world";
23
70
  export type VehicleIdempotency = {
24
71
  readonly mode: "safe";
@@ -4,6 +4,71 @@ export function defineVehicleSchema(codec) {
4
4
  safeParse: codec.safeParse,
5
5
  });
6
6
  }
7
+ /**
8
+ * A VehicleRegistry only ever calls a schema's own safeParse -- jsonSchema is
9
+ * descriptive metadata surfaced to a client/Pi projection, never itself
10
+ * enforced at runtime -- so a declared `enum` has to be checked here for
11
+ * real, or it's a documentation gesture, not an honest contract. Every
12
+ * consumer projecting a plain-object input onto a VehicleOperation needs the
13
+ * same required/enum checks; this is that check written once.
14
+ */
15
+ export function defineLooseObjectSchema(properties, required = []) {
16
+ return defineVehicleSchema({
17
+ // LooseObjectProperty's named fields (type, enum) are all JSON-value-shaped
18
+ // at runtime, but TypeScript's structural check against the recursive
19
+ // JsonValue union doesn't see that through a plain interface -- the cast
20
+ // is a type-system limitation, not a runtime concern.
21
+ jsonSchema: { type: "object", properties: properties, required: [...required], additionalProperties: false },
22
+ safeParse(value) {
23
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
24
+ return { success: false, issues: [{ path: [], message: "input must be an object" }] };
25
+ }
26
+ const input = value;
27
+ for (const key of required) {
28
+ if (!(key in input))
29
+ return { success: false, issues: [{ path: [key], message: `${key} is required` }] };
30
+ }
31
+ for (const [key, schema] of Object.entries(properties)) {
32
+ if (!schema.enum || !(key in input))
33
+ continue;
34
+ if (!schema.enum.includes(input[key])) {
35
+ return { success: false, issues: [{ path: [key], message: `${key} must be one of ${schema.enum.join(", ")}` }] };
36
+ }
37
+ }
38
+ return { success: true, value: input };
39
+ },
40
+ });
41
+ }
42
+ /** Accepts any value unvalidated -- for an operation whose output shape isn't worth a dedicated schema (an internal/low-stakes result, or one already validated upstream by the domain logic it wraps). */
43
+ export const passthroughVehicleSchema = defineVehicleSchema({
44
+ jsonSchema: { type: "object" },
45
+ safeParse: (value) => ({ success: true, value }),
46
+ });
47
+ /**
48
+ * Reads an operation's own `content` blocks off its output when present and
49
+ * well-formed, so a generic Vehicle client can prefer them over dumping raw
50
+ * JSON at the model -- without knowing anything about the operation's own
51
+ * domain shape. Returns undefined for a malformed or absent `content` field;
52
+ * the caller falls back to its own default (formatted JSON) rather than
53
+ * risk forwarding partial/garbled blocks.
54
+ */
55
+ export function extractVehicleContent(output) {
56
+ if (typeof output !== "object" || output === null || Array.isArray(output))
57
+ return undefined;
58
+ const content = output.content;
59
+ if (!Array.isArray(content) || content.length === 0)
60
+ return undefined;
61
+ const blocks = [];
62
+ for (const block of content) {
63
+ if (typeof block !== "object" || block === null)
64
+ return undefined;
65
+ const { type, text } = block;
66
+ if (type !== "text" || typeof text !== "string")
67
+ return undefined;
68
+ blocks.push({ type: "text", text });
69
+ }
70
+ return blocks;
71
+ }
7
72
  export function defineVehicleOperation(options) {
8
73
  validateOperationMetadata(options);
9
74
  const descriptor = Object.freeze({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/vehicle-core",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Vehicle's runtime-neutral wire contract: operation descriptors, schema codecs, failure shapes. Zero runtime dependencies, zero Bun-specific code -- the one thing every Vehicle client and server package depends on.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -23,7 +23,7 @@
23
23
  },
24
24
  "repository": {
25
25
  "type": "git",
26
- "url": "git+https://github.com/DanyPops/daemon-kit.git",
26
+ "url": "git+https://github.com/DanyPops/vehicle.git",
27
27
  "directory": "packages/vehicle-core"
28
28
  },
29
29
  "keywords": ["vehicle", "agent-tools"],
@@ -23,6 +23,98 @@ export function defineVehicleSchema<T>(codec: VehicleSchemaCodec<T>): VehicleSch
23
23
  });
24
24
  }
25
25
 
26
+ export interface LooseObjectProperty {
27
+ readonly type: string;
28
+ readonly enum?: readonly string[];
29
+ }
30
+
31
+ /**
32
+ * A VehicleRegistry only ever calls a schema's own safeParse -- jsonSchema is
33
+ * descriptive metadata surfaced to a client/Pi projection, never itself
34
+ * enforced at runtime -- so a declared `enum` has to be checked here for
35
+ * real, or it's a documentation gesture, not an honest contract. Every
36
+ * consumer projecting a plain-object input onto a VehicleOperation needs the
37
+ * same required/enum checks; this is that check written once.
38
+ */
39
+ export function defineLooseObjectSchema(properties: Record<string, LooseObjectProperty>, required: readonly string[] = []): VehicleSchemaCodec<Record<string, unknown>> {
40
+ return defineVehicleSchema<Record<string, unknown>>({
41
+ // LooseObjectProperty's named fields (type, enum) are all JSON-value-shaped
42
+ // at runtime, but TypeScript's structural check against the recursive
43
+ // JsonValue union doesn't see that through a plain interface -- the cast
44
+ // is a type-system limitation, not a runtime concern.
45
+ jsonSchema: { type: "object", properties: properties as unknown as JsonValue, required: [...required], additionalProperties: false },
46
+ safeParse(value) {
47
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
48
+ return { success: false, issues: [{ path: [], message: "input must be an object" }] };
49
+ }
50
+ const input = value as Record<string, unknown>;
51
+ for (const key of required) {
52
+ if (!(key in input)) return { success: false, issues: [{ path: [key], message: `${key} is required` }] };
53
+ }
54
+ for (const [key, schema] of Object.entries(properties)) {
55
+ if (!schema.enum || !(key in input)) continue;
56
+ if (!schema.enum.includes(input[key] as string)) {
57
+ return { success: false, issues: [{ path: [key], message: `${key} must be one of ${schema.enum.join(", ")}` }] };
58
+ }
59
+ }
60
+ return { success: true, value: input };
61
+ },
62
+ });
63
+ }
64
+
65
+ /** Accepts any value unvalidated -- for an operation whose output shape isn't worth a dedicated schema (an internal/low-stakes result, or one already validated upstream by the domain logic it wraps). */
66
+ export const passthroughVehicleSchema: VehicleSchemaCodec<unknown> = defineVehicleSchema<unknown>({
67
+ jsonSchema: { type: "object" },
68
+ safeParse: (value) => ({ success: true, value }),
69
+ });
70
+
71
+ /**
72
+ * A block of narrative text meant to be read by the model, not parsed as
73
+ * data -- same field name and shape MCP's own CallToolResult.content and
74
+ * Pi's own ToolDefinition.execute() return already use, so a Vehicle
75
+ * operation adopting this needs no translation layer at either boundary.
76
+ * Only the "text" variant exists here; there's no Vehicle use case yet for
77
+ * MCP's image/audio/resource-link block kinds.
78
+ */
79
+ export interface VehicleContentBlock {
80
+ readonly type: "text";
81
+ readonly text: string;
82
+ }
83
+
84
+ /**
85
+ * An operation's Output type can intersect this to carry its own
86
+ * model-facing narrative alongside its structured data, e.g.
87
+ * `type RunOutput = { runId: string; created: Task[] } & WithVehicleContent`.
88
+ * The operation itself builds `content` since it's the only code that
89
+ * actually knows how to describe what it computed -- never a per-consumer
90
+ * override bolted on wherever the operation happens to get registered.
91
+ */
92
+ export interface WithVehicleContent {
93
+ readonly content?: readonly VehicleContentBlock[];
94
+ }
95
+
96
+ /**
97
+ * Reads an operation's own `content` blocks off its output when present and
98
+ * well-formed, so a generic Vehicle client can prefer them over dumping raw
99
+ * JSON at the model -- without knowing anything about the operation's own
100
+ * domain shape. Returns undefined for a malformed or absent `content` field;
101
+ * the caller falls back to its own default (formatted JSON) rather than
102
+ * risk forwarding partial/garbled blocks.
103
+ */
104
+ export function extractVehicleContent(output: unknown): readonly VehicleContentBlock[] | undefined {
105
+ if (typeof output !== "object" || output === null || Array.isArray(output)) return undefined;
106
+ const content = (output as { readonly content?: unknown }).content;
107
+ if (!Array.isArray(content) || content.length === 0) return undefined;
108
+ const blocks: VehicleContentBlock[] = [];
109
+ for (const block of content) {
110
+ if (typeof block !== "object" || block === null) return undefined;
111
+ const { type, text } = block as { readonly type?: unknown; readonly text?: unknown };
112
+ if (type !== "text" || typeof text !== "string") return undefined;
113
+ blocks.push({ type: "text", text });
114
+ }
115
+ return blocks;
116
+ }
117
+
26
118
  export type VehicleEffect = "read" | "local-write" | "external-write" | "destructive" | "open-world";
27
119
 
28
120
  export type VehicleIdempotency =