la-machina-engine 0.7.10 → 0.9.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
@@ -73,7 +73,38 @@ npm install la-machina-engine
73
73
 
74
74
  Requires Node 20+. Works in Bun, Cloudflare Workers (with R2 storage).
75
75
 
76
- The package is published as a single bundled module (~330 KB ESM, ~340 KB CJS) with full TypeScript types. Built with [tsup](https://tsup.egoist.dev/), CI publishes with [Sigstore provenance](https://docs.npmjs.com/generating-provenance-statements).
76
+ The package is published as a single bundled module (~330 KB ESM, ~340 KB CJS) with full TypeScript types plus two Node-only subpaths (`/node-tools`, `/node-mcp`). Built with [tsup](https://tsup.egoist.dev/), CI publishes with [Sigstore provenance](https://docs.npmjs.com/generating-provenance-statements).
77
+
78
+ ### 0.7.x → 0.8.0 migration
79
+
80
+ 0.8.0 makes the engine fully runtime-agnostic. The default bundle no longer auto-registers `Bash` or auto-stubs MCP stdio servers based on `process.versions.node` probing — the caller now decides which Tools the engine sees per environment. See `plans/031-runtime-agnostic-engine.md` for the full design.
81
+
82
+ Two changes consumers may need:
83
+
84
+ ```ts
85
+ // Workers callers — Bash via capabilityStub (model sees it; engine
86
+ // pauses with handoff_to_runner when called):
87
+ import { initEngine, capabilityStub } from 'la-machina-engine'
88
+ initEngine({
89
+ tools: { custom: [capabilityStub({ name: 'Bash', description: '...' })] },
90
+ })
91
+
92
+ // Node callers — real Bash via the new node-tools subpath:
93
+ import { initEngine } from 'la-machina-engine'
94
+ import { createBashTool } from 'la-machina-engine/node-tools'
95
+ initEngine({ tools: { custom: [createBashTool()] } })
96
+
97
+ // Stdio MCP on Node — pass a transport factory:
98
+ import { createStdioTransportFactory } from 'la-machina-engine/node-mcp'
99
+ initEngine({
100
+ mcp: {
101
+ servers: { local: { type: 'stdio', command: 'npx', args: ['mcp-server-x'], ... } },
102
+ stdioTransport: createStdioTransportFactory(),
103
+ },
104
+ })
105
+ ```
106
+
107
+ Without `mcp.stdioTransport`, configuring a stdio MCP server fails at connect with a clean error pointing at the subpath (see `plans/031-runtime-agnostic-engine.md`).
77
108
 
78
109
  ---
79
110
 
@@ -1270,6 +1301,28 @@ model cannot spoof `Authorization` via `input.headers`).
1270
1301
  dispatch with `{ service, method, path, status, latencyMs, bytesIn }`
1271
1302
  — no secrets — for metering, billing, audit logs.
1272
1303
 
1304
+ **Raw-body uploads (0.9.0):** set `bodyEncoding: 'raw'` on the call to
1305
+ send a string body verbatim — no `JSON.stringify`. Use for upload APIs
1306
+ that take binary or text-blob bodies (Drive media upload, S3
1307
+ PutObject, image push, log ingestion, etc.). The caller-supplied
1308
+ `Content-Type` header wins; default falls back to `text/plain`.
1309
+
1310
+ ```ts
1311
+ ApiCall({
1312
+ service: 'gdrive',
1313
+ method: 'PATCH',
1314
+ path: `/upload/drive/v3/files/${fileId}?uploadType=media`,
1315
+ headers: { 'Content-Type': 'text/markdown' },
1316
+ body: '# Hello world',
1317
+ bodyEncoding: 'raw',
1318
+ })
1319
+ ```
1320
+
1321
+ `bodyEncoding: 'raw'` requires the body to be a string — passing an
1322
+ object returns `ERR_API_RAW_BODY_NOT_STRING` and never makes the
1323
+ fetch. The default `'json'` encoding behaves exactly as in 0.8.x.
1324
+ `maxBodyBytes` is enforced under both encodings.
1325
+
1273
1326
  **Disabling:** `tools.disabled: ['ApiCall']` turns it off even when
1274
1327
  services are configured. Absent `config.api` → tool never registered,
1275
1328
  no prompt mention.
@@ -0,0 +1,133 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Tool contract — the shape every tool implements, plus a registry to
5
+ * collect them by name.
6
+ *
7
+ * The contract is intentionally minimal:
8
+ *
9
+ * 1. A tool has a `name`, a `description`, and a Zod `inputSchema`.
10
+ * Zod gives both runtime validation and a TypeScript-inferred
11
+ * input type for `execute()`.
12
+ * 2. `execute(input, context)` returns a `ToolResult` (or rejects).
13
+ * The runtime catches rejections and turns them into error
14
+ * results — a misbehaving tool can never crash the agent loop.
15
+ * 3. The runtime supplies a `ToolContext` carrying at least an
16
+ * `AbortSignal` for cancellation.
17
+ *
18
+ * Phase 5 ships the types and the registry. Phase 6 fills the registry
19
+ * with the core tool set (Bash, Read, Write, Edit, Glob, Grep, WebFetch).
20
+ * Custom tools come from `config.tools.custom` at engine init time.
21
+ */
22
+
23
+ /**
24
+ * The execution context passed to every tool. Phase 5 keeps this
25
+ * minimal — only an AbortSignal — so the contract stays portable.
26
+ * Later phases extend with storage, run/node ids, hook handles, etc.
27
+ */
28
+ interface ToolContext {
29
+ readonly signal: AbortSignal;
30
+ }
31
+ /**
32
+ * The result of a single tool invocation. The runtime wraps thrown
33
+ * errors in this shape so the agent loop only ever sees success/error
34
+ * as data, not exceptions.
35
+ */
36
+ interface ToolResult {
37
+ /** Human- and model-readable output. The agent's next turn sees this. */
38
+ readonly content: string;
39
+ /** True when the tool failed. */
40
+ readonly isError?: boolean;
41
+ /** Optional structured metadata for hooks / observability. */
42
+ readonly metadata?: Readonly<Record<string, unknown>>;
43
+ }
44
+ /**
45
+ * A tool definition. Generic on a Zod schema so that `execute()`'s
46
+ * input parameter is statically typed via `z.infer<TSchema>`.
47
+ *
48
+ * Tool authors normally use `defineTool()` to get the inference
49
+ * without writing the generic by hand.
50
+ */
51
+ interface Tool<TSchema extends z.ZodTypeAny = z.ZodTypeAny> {
52
+ readonly name: string;
53
+ readonly description: string;
54
+ readonly inputSchema: TSchema;
55
+ /**
56
+ * Optional raw JSON Schema to send to the Anthropic API as
57
+ * `input_schema`, bypassing Zod-to-JSON-Schema conversion. Used by
58
+ * MCP-sourced tools whose schema is authored in JSON Schema directly
59
+ * and whose Zod schema is a `z.unknown()` passthrough.
60
+ *
61
+ * When set, `toAnthropicTool()` in the agent loop prefers this over
62
+ * running `zodToJsonSchema(inputSchema)`. Normal in-process tools
63
+ * defined via `defineTool()` should leave this undefined.
64
+ */
65
+ readonly anthropicSchemaOverride?: Record<string, unknown>;
66
+ /**
67
+ * Returns true if this tool is safe to run concurrently with other
68
+ * concurrency-safe tools. Read-only tools (Read, Glob, Grep, etc.)
69
+ * should return true; mutating tools (Write, Edit, Bash) should
70
+ * return false. Default: false (fail-closed).
71
+ *
72
+ * The agent loop uses this to batch consecutive safe tool calls
73
+ * into a single `Promise.all()` for parallel execution, while
74
+ * running unsafe tools serially.
75
+ *
76
+ * Input is typed as `unknown` (not `z.infer<TSchema>`) so that
77
+ * `Tool<ZodObject<...>>` remains assignable to `Tool<ZodTypeAny>`
78
+ * under `exactOptionalPropertyTypes`.
79
+ */
80
+ isConcurrencySafe?: ((input: unknown) => boolean) | undefined;
81
+ /**
82
+ * Declares that this tool needs Node-only capabilities
83
+ * (`node:child_process`, the real filesystem, stdio-based MCP, etc.)
84
+ * When true AND the runtime can't support them, the engine replaces
85
+ * this tool with a capability stub (see `src/tools/capabilityStub.ts`)
86
+ * that returns a structured `isError` result instead of crashing.
87
+ *
88
+ * Leave undefined / false for tools that work anywhere (Read, Write,
89
+ * WebFetch, http-based MCP). Default: false.
90
+ */
91
+ readonly requiresNode?: boolean;
92
+ /**
93
+ * Internal marker set by `capabilityStub()` so the agent-loop's
94
+ * runner-handoff check (Plan 019 §4) can distinguish a stubbed tool
95
+ * from a real one without reflecting on the implementation.
96
+ * Do not set this manually.
97
+ */
98
+ readonly isCapabilityStub?: boolean;
99
+ execute(input: z.infer<TSchema>, context: ToolContext): Promise<ToolResult>;
100
+ }
101
+ /**
102
+ * Identity helper that locks in the schema generic so callers don't have
103
+ * to write it. Use this whenever defining a tool — it costs nothing at
104
+ * runtime and gives you full inference inside `execute()`.
105
+ *
106
+ * @example
107
+ * ```ts
108
+ * const Echo = defineTool({
109
+ * name: 'Echo',
110
+ * description: 'Echoes its input back',
111
+ * inputSchema: z.object({ msg: z.string() }),
112
+ * execute: async ({ msg }) => ({ content: msg }),
113
+ * })
114
+ * ```
115
+ */
116
+ declare function defineTool<TSchema extends z.ZodTypeAny>(tool: Tool<TSchema>): Tool<TSchema>;
117
+ /**
118
+ * In-memory registry of tools by name. Used by the engine's tool runtime
119
+ * to dispatch tool calls. Re-registering the same name throws so typos
120
+ * surface immediately rather than silently shadowing.
121
+ */
122
+ declare class ToolRegistry {
123
+ private readonly tools;
124
+ register(tool: Tool): void;
125
+ registerAll(tools: ReadonlyArray<Tool>): void;
126
+ unregister(name: string): void;
127
+ get(name: string): Tool | undefined;
128
+ has(name: string): boolean;
129
+ list(): Tool[];
130
+ count(): number;
131
+ }
132
+
133
+ export { type ToolResult as T, ToolRegistry as a, type Tool as b, type ToolContext as c, defineTool as d };
@@ -0,0 +1,133 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Tool contract — the shape every tool implements, plus a registry to
5
+ * collect them by name.
6
+ *
7
+ * The contract is intentionally minimal:
8
+ *
9
+ * 1. A tool has a `name`, a `description`, and a Zod `inputSchema`.
10
+ * Zod gives both runtime validation and a TypeScript-inferred
11
+ * input type for `execute()`.
12
+ * 2. `execute(input, context)` returns a `ToolResult` (or rejects).
13
+ * The runtime catches rejections and turns them into error
14
+ * results — a misbehaving tool can never crash the agent loop.
15
+ * 3. The runtime supplies a `ToolContext` carrying at least an
16
+ * `AbortSignal` for cancellation.
17
+ *
18
+ * Phase 5 ships the types and the registry. Phase 6 fills the registry
19
+ * with the core tool set (Bash, Read, Write, Edit, Glob, Grep, WebFetch).
20
+ * Custom tools come from `config.tools.custom` at engine init time.
21
+ */
22
+
23
+ /**
24
+ * The execution context passed to every tool. Phase 5 keeps this
25
+ * minimal — only an AbortSignal — so the contract stays portable.
26
+ * Later phases extend with storage, run/node ids, hook handles, etc.
27
+ */
28
+ interface ToolContext {
29
+ readonly signal: AbortSignal;
30
+ }
31
+ /**
32
+ * The result of a single tool invocation. The runtime wraps thrown
33
+ * errors in this shape so the agent loop only ever sees success/error
34
+ * as data, not exceptions.
35
+ */
36
+ interface ToolResult {
37
+ /** Human- and model-readable output. The agent's next turn sees this. */
38
+ readonly content: string;
39
+ /** True when the tool failed. */
40
+ readonly isError?: boolean;
41
+ /** Optional structured metadata for hooks / observability. */
42
+ readonly metadata?: Readonly<Record<string, unknown>>;
43
+ }
44
+ /**
45
+ * A tool definition. Generic on a Zod schema so that `execute()`'s
46
+ * input parameter is statically typed via `z.infer<TSchema>`.
47
+ *
48
+ * Tool authors normally use `defineTool()` to get the inference
49
+ * without writing the generic by hand.
50
+ */
51
+ interface Tool<TSchema extends z.ZodTypeAny = z.ZodTypeAny> {
52
+ readonly name: string;
53
+ readonly description: string;
54
+ readonly inputSchema: TSchema;
55
+ /**
56
+ * Optional raw JSON Schema to send to the Anthropic API as
57
+ * `input_schema`, bypassing Zod-to-JSON-Schema conversion. Used by
58
+ * MCP-sourced tools whose schema is authored in JSON Schema directly
59
+ * and whose Zod schema is a `z.unknown()` passthrough.
60
+ *
61
+ * When set, `toAnthropicTool()` in the agent loop prefers this over
62
+ * running `zodToJsonSchema(inputSchema)`. Normal in-process tools
63
+ * defined via `defineTool()` should leave this undefined.
64
+ */
65
+ readonly anthropicSchemaOverride?: Record<string, unknown>;
66
+ /**
67
+ * Returns true if this tool is safe to run concurrently with other
68
+ * concurrency-safe tools. Read-only tools (Read, Glob, Grep, etc.)
69
+ * should return true; mutating tools (Write, Edit, Bash) should
70
+ * return false. Default: false (fail-closed).
71
+ *
72
+ * The agent loop uses this to batch consecutive safe tool calls
73
+ * into a single `Promise.all()` for parallel execution, while
74
+ * running unsafe tools serially.
75
+ *
76
+ * Input is typed as `unknown` (not `z.infer<TSchema>`) so that
77
+ * `Tool<ZodObject<...>>` remains assignable to `Tool<ZodTypeAny>`
78
+ * under `exactOptionalPropertyTypes`.
79
+ */
80
+ isConcurrencySafe?: ((input: unknown) => boolean) | undefined;
81
+ /**
82
+ * Declares that this tool needs Node-only capabilities
83
+ * (`node:child_process`, the real filesystem, stdio-based MCP, etc.)
84
+ * When true AND the runtime can't support them, the engine replaces
85
+ * this tool with a capability stub (see `src/tools/capabilityStub.ts`)
86
+ * that returns a structured `isError` result instead of crashing.
87
+ *
88
+ * Leave undefined / false for tools that work anywhere (Read, Write,
89
+ * WebFetch, http-based MCP). Default: false.
90
+ */
91
+ readonly requiresNode?: boolean;
92
+ /**
93
+ * Internal marker set by `capabilityStub()` so the agent-loop's
94
+ * runner-handoff check (Plan 019 §4) can distinguish a stubbed tool
95
+ * from a real one without reflecting on the implementation.
96
+ * Do not set this manually.
97
+ */
98
+ readonly isCapabilityStub?: boolean;
99
+ execute(input: z.infer<TSchema>, context: ToolContext): Promise<ToolResult>;
100
+ }
101
+ /**
102
+ * Identity helper that locks in the schema generic so callers don't have
103
+ * to write it. Use this whenever defining a tool — it costs nothing at
104
+ * runtime and gives you full inference inside `execute()`.
105
+ *
106
+ * @example
107
+ * ```ts
108
+ * const Echo = defineTool({
109
+ * name: 'Echo',
110
+ * description: 'Echoes its input back',
111
+ * inputSchema: z.object({ msg: z.string() }),
112
+ * execute: async ({ msg }) => ({ content: msg }),
113
+ * })
114
+ * ```
115
+ */
116
+ declare function defineTool<TSchema extends z.ZodTypeAny>(tool: Tool<TSchema>): Tool<TSchema>;
117
+ /**
118
+ * In-memory registry of tools by name. Used by the engine's tool runtime
119
+ * to dispatch tool calls. Re-registering the same name throws so typos
120
+ * surface immediately rather than silently shadowing.
121
+ */
122
+ declare class ToolRegistry {
123
+ private readonly tools;
124
+ register(tool: Tool): void;
125
+ registerAll(tools: ReadonlyArray<Tool>): void;
126
+ unregister(name: string): void;
127
+ get(name: string): Tool | undefined;
128
+ has(name: string): boolean;
129
+ list(): Tool[];
130
+ count(): number;
131
+ }
132
+
133
+ export { type ToolResult as T, ToolRegistry as a, type Tool as b, type ToolContext as c, defineTool as d };