la-machina-engine 0.11.2 → 0.12.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
@@ -1861,6 +1861,7 @@ All features ported 1:1 from La-Machina's production runtime. Pure JS, Workers-c
1861
1861
  - [x] Bash error cascading (AbortController aborts sibling tools)
1862
1862
  - [x] 22 built-in tools
1863
1863
  - [x] Custom tool registration via `defineTool()`
1864
+ - [x] Gated tools via `defineGatedTool()` — pause the run on tool_use, resume via `engine.resumeAsync({toolResult})`. Full transcript preserved across the pause. Use for human-in-the-loop inputs, out-of-band approvals, anything where the result comes from a caller decision.
1864
1865
  - [x] Device path blocking (/dev/zero, /dev/random, /proc/kcore)
1865
1866
  - [x] Knowledge base (`SearchKnowledge` + `ReadKnowledge`) — opt-in, per-tenant vault under `workspaces/{ws}/knowledge/`, section-level indexing, format extractors (md/txt/json/csv/html native; pdf/docx via optional deps), external link headers with non-persistence guarantee
1866
1867
 
@@ -96,6 +96,20 @@ interface Tool<TSchema extends z.ZodTypeAny = z.ZodTypeAny> {
96
96
  * Do not set this manually.
97
97
  */
98
98
  readonly isCapabilityStub?: boolean;
99
+ /**
100
+ * Plan 043 — when true, the engine pauses the run as soon as the
101
+ * model calls this tool instead of dispatching `execute()`. The
102
+ * pending tool call (id, name, input) is captured in the snapshot;
103
+ * resume via `engine.resumeAsync({toolResult: {...}})` injects a
104
+ * synthetic `tool_result` message and continues the same loop with
105
+ * full transcript intact.
106
+ *
107
+ * Pauses emit `pauseReason: 'awaiting_tool_result'`.
108
+ *
109
+ * `execute` is never invoked on a gated tool — `defineGatedTool()`
110
+ * fills in a no-op stub so the Tool type stays consistent.
111
+ */
112
+ readonly gated?: boolean;
99
113
  execute(input: z.infer<TSchema>, context: ToolContext): Promise<ToolResult>;
100
114
  }
101
115
  /**
@@ -114,6 +128,29 @@ interface Tool<TSchema extends z.ZodTypeAny = z.ZodTypeAny> {
114
128
  * ```
115
129
  */
116
130
  declare function defineTool<TSchema extends z.ZodTypeAny>(tool: Tool<TSchema>): Tool<TSchema>;
131
+ /**
132
+ * Plan 043 — define a tool that pauses the run instead of executing.
133
+ *
134
+ * The engine stops on `tool_use`, persists the pending call in the
135
+ * run snapshot, and waits for `engine.resumeAsync({toolResult: {...}})`
136
+ * to deliver the answer. Use this for human-in-the-loop inputs,
137
+ * out-of-band approvals, or any tool whose result comes from a
138
+ * caller decision rather than in-process computation.
139
+ *
140
+ * @example
141
+ * ```ts
142
+ * const AskHuman = defineGatedTool({
143
+ * name: 'AskHuman',
144
+ * description: 'Ask a human for information.',
145
+ * inputSchema: z.object({ question: z.string() }),
146
+ * })
147
+ * ```
148
+ *
149
+ * `execute` is filled in with a no-op stub that flags the tool as
150
+ * "should not have been executed" if it's ever called — that
151
+ * indicates a code-path bug where the gated-tool pause was bypassed.
152
+ */
153
+ declare function defineGatedTool<TSchema extends z.ZodTypeAny>(tool: Omit<Tool<TSchema>, 'execute' | 'gated'>): Tool<TSchema>;
117
154
  /**
118
155
  * In-memory registry of tools by name. Used by the engine's tool runtime
119
156
  * to dispatch tool calls. Re-registering the same name throws so typos
@@ -130,4 +167,4 @@ declare class ToolRegistry {
130
167
  count(): number;
131
168
  }
132
169
 
133
- export { type ToolResult as T, ToolRegistry as a, type Tool as b, type ToolContext as c, defineTool as d };
170
+ export { type ToolResult as T, ToolRegistry as a, type Tool as b, type ToolContext as c, defineGatedTool as d, defineTool as e };
@@ -96,6 +96,20 @@ interface Tool<TSchema extends z.ZodTypeAny = z.ZodTypeAny> {
96
96
  * Do not set this manually.
97
97
  */
98
98
  readonly isCapabilityStub?: boolean;
99
+ /**
100
+ * Plan 043 — when true, the engine pauses the run as soon as the
101
+ * model calls this tool instead of dispatching `execute()`. The
102
+ * pending tool call (id, name, input) is captured in the snapshot;
103
+ * resume via `engine.resumeAsync({toolResult: {...}})` injects a
104
+ * synthetic `tool_result` message and continues the same loop with
105
+ * full transcript intact.
106
+ *
107
+ * Pauses emit `pauseReason: 'awaiting_tool_result'`.
108
+ *
109
+ * `execute` is never invoked on a gated tool — `defineGatedTool()`
110
+ * fills in a no-op stub so the Tool type stays consistent.
111
+ */
112
+ readonly gated?: boolean;
99
113
  execute(input: z.infer<TSchema>, context: ToolContext): Promise<ToolResult>;
100
114
  }
101
115
  /**
@@ -114,6 +128,29 @@ interface Tool<TSchema extends z.ZodTypeAny = z.ZodTypeAny> {
114
128
  * ```
115
129
  */
116
130
  declare function defineTool<TSchema extends z.ZodTypeAny>(tool: Tool<TSchema>): Tool<TSchema>;
131
+ /**
132
+ * Plan 043 — define a tool that pauses the run instead of executing.
133
+ *
134
+ * The engine stops on `tool_use`, persists the pending call in the
135
+ * run snapshot, and waits for `engine.resumeAsync({toolResult: {...}})`
136
+ * to deliver the answer. Use this for human-in-the-loop inputs,
137
+ * out-of-band approvals, or any tool whose result comes from a
138
+ * caller decision rather than in-process computation.
139
+ *
140
+ * @example
141
+ * ```ts
142
+ * const AskHuman = defineGatedTool({
143
+ * name: 'AskHuman',
144
+ * description: 'Ask a human for information.',
145
+ * inputSchema: z.object({ question: z.string() }),
146
+ * })
147
+ * ```
148
+ *
149
+ * `execute` is filled in with a no-op stub that flags the tool as
150
+ * "should not have been executed" if it's ever called — that
151
+ * indicates a code-path bug where the gated-tool pause was bypassed.
152
+ */
153
+ declare function defineGatedTool<TSchema extends z.ZodTypeAny>(tool: Omit<Tool<TSchema>, 'execute' | 'gated'>): Tool<TSchema>;
117
154
  /**
118
155
  * In-memory registry of tools by name. Used by the engine's tool runtime
119
156
  * to dispatch tool calls. Re-registering the same name throws so typos
@@ -130,4 +167,4 @@ declare class ToolRegistry {
130
167
  count(): number;
131
168
  }
132
169
 
133
- export { type ToolResult as T, ToolRegistry as a, type Tool as b, type ToolContext as c, defineTool as d };
170
+ export { type ToolResult as T, ToolRegistry as a, type Tool as b, type ToolContext as c, defineGatedTool as d, defineTool as e };
package/dist/index.cjs CHANGED
@@ -41,6 +41,16 @@ var init_cjs_shims = __esm({
41
41
  function defineTool(tool) {
42
42
  return tool;
43
43
  }
44
+ function defineGatedTool(tool) {
45
+ return {
46
+ ...tool,
47
+ gated: true,
48
+ execute: async () => ({
49
+ content: `gated tool "${tool.name}" should not have been executed \u2014 the engine was supposed to pause on its invocation (Plan 043). File a bug against la-machina-engine.`,
50
+ isError: true
51
+ })
52
+ };
53
+ }
44
54
  var ToolRegistry;
45
55
  var init_contract = __esm({
46
56
  "src/tools/contract.ts"() {
@@ -889,6 +899,7 @@ __export(src_exports, {
889
899
  createSmartMemory: () => createSmartMemory,
890
900
  defaultSamplingHandler: () => defaultSamplingHandler,
891
901
  defaultToolResultSummarizer: () => defaultToolResultSummarizer,
902
+ defineGatedTool: () => defineGatedTool,
892
903
  defineTool: () => defineTool,
893
904
  detectRuntime: () => detectRuntime,
894
905
  getCoordinatorBasePrompt: () => getCoordinatorBasePrompt,
@@ -2389,6 +2400,8 @@ var RunSnapshotSchema = import_zod2.z.lazy(
2389
2400
  "gate_required",
2390
2401
  "subagent_gate_required",
2391
2402
  "handoff_to_runner",
2403
+ // Plan 043 — caller pauses for a gated tool's human-supplied result.
2404
+ "awaiting_tool_result",
2392
2405
  "max_turns",
2393
2406
  "explicit",
2394
2407
  "timeout"
@@ -3345,6 +3358,25 @@ async function agentLoop(options) {
3345
3358
  }
3346
3359
  }
3347
3360
  }
3361
+ for (const call of toolCallsToDispatch) {
3362
+ const tool = options.registry?.get(call.name);
3363
+ if (tool?.gated === true) {
3364
+ const paused = await pauseHere({
3365
+ ctx,
3366
+ transcript,
3367
+ reason: "awaiting_tool_result",
3368
+ pendingToolCall: {
3369
+ toolName: call.name,
3370
+ toolUseId: call.id,
3371
+ input: call.input,
3372
+ calledAt: (/* @__PURE__ */ new Date()).toISOString()
3373
+ },
3374
+ storage: options.storage,
3375
+ subagentRegistry: options.subagentRegistry
3376
+ });
3377
+ return paused;
3378
+ }
3379
+ }
3348
3380
  if (options.handoffToRunner === true) {
3349
3381
  for (const call of toolCallsToDispatch) {
3350
3382
  const tool = options.registry?.get(call.name);
@@ -8296,7 +8328,7 @@ function createApiCallTool(opts) {
8296
8328
  bodyText = input.body;
8297
8329
  defaultContentType = "text/plain";
8298
8330
  } else {
8299
- bodyText = JSON.stringify(input.body);
8331
+ bodyText = typeof input.body === "string" ? input.body : JSON.stringify(input.body);
8300
8332
  defaultContentType = "application/json";
8301
8333
  }
8302
8334
  const cap = svc.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
@@ -10883,7 +10915,25 @@ var Engine = class {
10883
10915
  });
10884
10916
  if (snapshot.pendingToolCall) {
10885
10917
  const pending = snapshot.pendingToolCall;
10886
- if (options.gateAnswer !== void 0) {
10918
+ if (snapshot.pauseReason === "awaiting_tool_result") {
10919
+ if (options.toolResult === void 0) {
10920
+ throw new EngineError(
10921
+ "ERR_GATED_RESUME_NO_RESULT",
10922
+ "resume of `awaiting_tool_result` pause requires `toolResult` in ResumeOptions"
10923
+ );
10924
+ }
10925
+ if (options.toolResult.id !== pending.toolUseId) {
10926
+ throw new EngineError(
10927
+ "ERR_GATED_RESUME_ID_MISMATCH",
10928
+ `toolResult.id "${options.toolResult.id}" does not match pending toolUseId "${pending.toolUseId}" \u2014 resume aborted`
10929
+ );
10930
+ }
10931
+ await ctx.addToolResult(
10932
+ pending.toolUseId,
10933
+ options.toolResult.content,
10934
+ options.toolResult.isError ?? false
10935
+ );
10936
+ } else if (options.gateAnswer !== void 0) {
10887
10937
  const answer = typeof options.gateAnswer === "string" ? options.gateAnswer : JSON.stringify(options.gateAnswer);
10888
10938
  await ctx.addToolResult(pending.toolUseId, answer, false);
10889
10939
  } else {
@@ -12255,6 +12305,7 @@ function resolveApiKey(config) {
12255
12305
  createSmartMemory,
12256
12306
  defaultSamplingHandler,
12257
12307
  defaultToolResultSummarizer,
12308
+ defineGatedTool,
12258
12309
  defineTool,
12259
12310
  detectRuntime,
12260
12311
  getCoordinatorBasePrompt,