la-machina-engine 0.11.3 → 0.13.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);
@@ -10395,16 +10427,8 @@ var NodeBackgroundExecutor = class {
10395
10427
  // src/engine/webhook.ts
10396
10428
  init_cjs_shims();
10397
10429
  var RETRY_DELAYS_MS = [
10398
- 0,
10399
- // attempt 1: immediate
10400
- 1e4,
10401
- // attempt 2: 10s after failure
10402
- 6e4,
10403
- // attempt 3: 60s after failure
10404
- 5 * 6e4,
10405
- // attempt 4: 5min after failure
10406
- 30 * 6e4
10407
- // attempt 5: 30min after failure
10430
+ 0
10431
+ // attempt 1: immediate (Plan 046 — single attempt only)
10408
10432
  ];
10409
10433
  var MAX_ATTEMPTS = RETRY_DELAYS_MS.length;
10410
10434
  async function signPayload(secret, timestamp, body) {
@@ -10883,7 +10907,25 @@ var Engine = class {
10883
10907
  });
10884
10908
  if (snapshot.pendingToolCall) {
10885
10909
  const pending = snapshot.pendingToolCall;
10886
- if (options.gateAnswer !== void 0) {
10910
+ if (snapshot.pauseReason === "awaiting_tool_result") {
10911
+ if (options.toolResult === void 0) {
10912
+ throw new EngineError(
10913
+ "ERR_GATED_RESUME_NO_RESULT",
10914
+ "resume of `awaiting_tool_result` pause requires `toolResult` in ResumeOptions"
10915
+ );
10916
+ }
10917
+ if (options.toolResult.id !== pending.toolUseId) {
10918
+ throw new EngineError(
10919
+ "ERR_GATED_RESUME_ID_MISMATCH",
10920
+ `toolResult.id "${options.toolResult.id}" does not match pending toolUseId "${pending.toolUseId}" \u2014 resume aborted`
10921
+ );
10922
+ }
10923
+ await ctx.addToolResult(
10924
+ pending.toolUseId,
10925
+ options.toolResult.content,
10926
+ options.toolResult.isError ?? false
10927
+ );
10928
+ } else if (options.gateAnswer !== void 0) {
10887
10929
  const answer = typeof options.gateAnswer === "string" ? options.gateAnswer : JSON.stringify(options.gateAnswer);
10888
10930
  await ctx.addToolResult(pending.toolUseId, answer, false);
10889
10931
  } else {
@@ -12255,6 +12297,7 @@ function resolveApiKey(config) {
12255
12297
  createSmartMemory,
12256
12298
  defaultSamplingHandler,
12257
12299
  defaultToolResultSummarizer,
12300
+ defineGatedTool,
12258
12301
  defineTool,
12259
12302
  detectRuntime,
12260
12303
  getCoordinatorBasePrompt,