la-machina-engine 0.7.2 → 0.7.4

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/dist/index.d.cts CHANGED
@@ -1280,6 +1280,18 @@ interface StreamRequest {
1280
1280
  readonly tools?: readonly ModelToolDefinition[] | undefined;
1281
1281
  readonly maxTokens?: number | undefined;
1282
1282
  readonly temperature?: number | undefined;
1283
+ /**
1284
+ * Tool-call policy (Plan 025). Default: `'auto'` (model decides).
1285
+ * - `'auto'` — model picks; pass-through behavior
1286
+ * - `'required'` — model MUST call a tool on its next response;
1287
+ * translated per-provider (Anthropic:
1288
+ * `tool_choice: { type: 'any' }`; OpenAI / AI SDK:
1289
+ * `toolChoice: 'required'`).
1290
+ *
1291
+ * Note: `'none'` is handled engine-side by stripping `tools` before
1292
+ * the request reaches the adapter, so adapters never see it.
1293
+ */
1294
+ readonly toolChoice?: 'auto' | 'required' | undefined;
1283
1295
  }
1284
1296
 
1285
1297
  interface ModelAdapter {
@@ -2000,7 +2012,30 @@ interface RunOptions {
2000
2012
  readonly runId?: string;
2001
2013
  readonly nodeId: string;
2002
2014
  readonly task: string;
2015
+ /**
2016
+ * Per-run tool allowlist (Plan 025). When undefined, the run sees
2017
+ * every tool registered at engine init (subject to
2018
+ * `config.tools.enabled / disabled`). When provided, the visible
2019
+ * tool set is further restricted to this exact list — names that
2020
+ * aren't registered are silently ignored. Pass `[]` to disable
2021
+ * all tools for this run; equivalent to `toolChoice: 'none'`.
2022
+ */
2003
2023
  readonly tools?: readonly string[] | undefined;
2024
+ /**
2025
+ * Tool-call policy for this run (Plan 025). Default: `'auto'`.
2026
+ * - `'auto'` — model decides whether to call tools
2027
+ * - `'none'` — model is told it has no tools; tools list is
2028
+ * stripped before the model adapter sees it
2029
+ * - `'required'` — model MUST call a tool on its next response;
2030
+ * plumbed to the provider as the "force tool use"
2031
+ * flag (Anthropic: `tool_choice: { type: 'any' }`,
2032
+ * OpenAI / AI SDK: `toolChoice: 'required'`)
2033
+ *
2034
+ * Precedence: `'none'` wins over any `tools: [...]` allowlist.
2035
+ * `'required'` paired with an empty effective tool set throws
2036
+ * `ERR_TOOL_CHOICE_CONFLICT` at run start.
2037
+ */
2038
+ readonly toolChoice?: 'auto' | 'none' | 'required' | undefined;
2004
2039
  readonly context?: Readonly<Record<string, unknown>> | undefined;
2005
2040
  /** Override maxTurns for this run only. Used by the orchestrator for per-phase budgets. */
2006
2041
  readonly maxTurns?: number | undefined;
@@ -2020,11 +2055,19 @@ interface RunOptions {
2020
2055
  */
2021
2056
  readonly outputFormat?: 'text' | 'json' | undefined;
2022
2057
  /**
2023
- * Zod schema for structured output. Only used when outputFormat is 'json'.
2024
- * Injected into system prompt as JSON Schema description, used to
2025
- * validate the model's response. If validation fails, retries once.
2058
+ * Schema for structured output. Only used when outputFormat is 'json'.
2059
+ * Injected into the system prompt and (for Zod schemas) used to
2060
+ * validate the model's response.
2061
+ *
2062
+ * Two shapes accepted:
2063
+ * - A Zod schema — strictly validated via `safeParse`; retries
2064
+ * once on failure with a corrective prompt.
2065
+ * - A plain JSON Schema object — used verbatim in the prompt; no
2066
+ * server-side strict validation (callers wanting that should
2067
+ * pass Zod). This shape is what serialized workflow definitions
2068
+ * (e.g. nikaido) carry, since they can't embed Zod instances.
2026
2069
  */
2027
- readonly outputSchema?: zod.ZodTypeAny | undefined;
2070
+ readonly outputSchema?: zod.ZodTypeAny | Record<string, unknown> | undefined;
2028
2071
  /**
2029
2072
  * Per-run skill override (Plan 017). When present, the engine IGNORES
2030
2073
  * `config.skills.autoload` + `config.skills.path` and uses this list
@@ -3047,11 +3090,21 @@ declare const defaultToolResultSummarizer: ToolResultSummarizerV1;
3047
3090
  * All pure JS — no `node:` imports, Workers-compatible.
3048
3091
  */
3049
3092
 
3093
+ /**
3094
+ * Schema users can supply for structured output. Either:
3095
+ * - a Zod schema (typed validation, runs `safeParse` post-parse), or
3096
+ * - a plain JSON Schema object (used verbatim in the system prompt;
3097
+ * no strict server-side validation in v1).
3098
+ *
3099
+ * Workflow definitions stored as JSON (e.g. nikaido) can only carry
3100
+ * the JSON Schema variant; native TS callers may use either.
3101
+ */
3102
+ type OutputSchema = ZodTypeAny | Record<string, unknown>;
3050
3103
  /**
3051
3104
  * Build the output format section to append to the system prompt.
3052
3105
  * Called when outputFormat is 'json'.
3053
3106
  */
3054
- declare function buildSchemaPrompt(schema?: ZodTypeAny): string;
3107
+ declare function buildSchemaPrompt(schema?: OutputSchema): string;
3055
3108
  interface ParseResult {
3056
3109
  readonly ok: boolean;
3057
3110
  readonly value?: unknown;
@@ -3065,10 +3118,14 @@ interface ParseResult {
3065
3118
  */
3066
3119
  declare function tryParseJSON(text: string): ParseResult;
3067
3120
  /**
3068
- * Validate parsed JSON against a Zod schema.
3069
- * Returns the validated data or an error message.
3121
+ * Validate parsed JSON against a schema.
3122
+ *
3123
+ * Zod schemas get strict `safeParse` validation. Plain JSON Schema
3124
+ * objects are NOT strictly validated (skipping ajv keeps the bundle
3125
+ * small for Workers); the prompt-injected schema is the contract and
3126
+ * the caller can validate downstream if needed.
3070
3127
  */
3071
- declare function validateOutput(value: unknown, schema: ZodTypeAny): {
3128
+ declare function validateOutput(value: unknown, schema: OutputSchema): {
3072
3129
  ok: true;
3073
3130
  data: unknown;
3074
3131
  } | {
package/dist/index.d.ts CHANGED
@@ -1280,6 +1280,18 @@ interface StreamRequest {
1280
1280
  readonly tools?: readonly ModelToolDefinition[] | undefined;
1281
1281
  readonly maxTokens?: number | undefined;
1282
1282
  readonly temperature?: number | undefined;
1283
+ /**
1284
+ * Tool-call policy (Plan 025). Default: `'auto'` (model decides).
1285
+ * - `'auto'` — model picks; pass-through behavior
1286
+ * - `'required'` — model MUST call a tool on its next response;
1287
+ * translated per-provider (Anthropic:
1288
+ * `tool_choice: { type: 'any' }`; OpenAI / AI SDK:
1289
+ * `toolChoice: 'required'`).
1290
+ *
1291
+ * Note: `'none'` is handled engine-side by stripping `tools` before
1292
+ * the request reaches the adapter, so adapters never see it.
1293
+ */
1294
+ readonly toolChoice?: 'auto' | 'required' | undefined;
1283
1295
  }
1284
1296
 
1285
1297
  interface ModelAdapter {
@@ -2000,7 +2012,30 @@ interface RunOptions {
2000
2012
  readonly runId?: string;
2001
2013
  readonly nodeId: string;
2002
2014
  readonly task: string;
2015
+ /**
2016
+ * Per-run tool allowlist (Plan 025). When undefined, the run sees
2017
+ * every tool registered at engine init (subject to
2018
+ * `config.tools.enabled / disabled`). When provided, the visible
2019
+ * tool set is further restricted to this exact list — names that
2020
+ * aren't registered are silently ignored. Pass `[]` to disable
2021
+ * all tools for this run; equivalent to `toolChoice: 'none'`.
2022
+ */
2003
2023
  readonly tools?: readonly string[] | undefined;
2024
+ /**
2025
+ * Tool-call policy for this run (Plan 025). Default: `'auto'`.
2026
+ * - `'auto'` — model decides whether to call tools
2027
+ * - `'none'` — model is told it has no tools; tools list is
2028
+ * stripped before the model adapter sees it
2029
+ * - `'required'` — model MUST call a tool on its next response;
2030
+ * plumbed to the provider as the "force tool use"
2031
+ * flag (Anthropic: `tool_choice: { type: 'any' }`,
2032
+ * OpenAI / AI SDK: `toolChoice: 'required'`)
2033
+ *
2034
+ * Precedence: `'none'` wins over any `tools: [...]` allowlist.
2035
+ * `'required'` paired with an empty effective tool set throws
2036
+ * `ERR_TOOL_CHOICE_CONFLICT` at run start.
2037
+ */
2038
+ readonly toolChoice?: 'auto' | 'none' | 'required' | undefined;
2004
2039
  readonly context?: Readonly<Record<string, unknown>> | undefined;
2005
2040
  /** Override maxTurns for this run only. Used by the orchestrator for per-phase budgets. */
2006
2041
  readonly maxTurns?: number | undefined;
@@ -2020,11 +2055,19 @@ interface RunOptions {
2020
2055
  */
2021
2056
  readonly outputFormat?: 'text' | 'json' | undefined;
2022
2057
  /**
2023
- * Zod schema for structured output. Only used when outputFormat is 'json'.
2024
- * Injected into system prompt as JSON Schema description, used to
2025
- * validate the model's response. If validation fails, retries once.
2058
+ * Schema for structured output. Only used when outputFormat is 'json'.
2059
+ * Injected into the system prompt and (for Zod schemas) used to
2060
+ * validate the model's response.
2061
+ *
2062
+ * Two shapes accepted:
2063
+ * - A Zod schema — strictly validated via `safeParse`; retries
2064
+ * once on failure with a corrective prompt.
2065
+ * - A plain JSON Schema object — used verbatim in the prompt; no
2066
+ * server-side strict validation (callers wanting that should
2067
+ * pass Zod). This shape is what serialized workflow definitions
2068
+ * (e.g. nikaido) carry, since they can't embed Zod instances.
2026
2069
  */
2027
- readonly outputSchema?: zod.ZodTypeAny | undefined;
2070
+ readonly outputSchema?: zod.ZodTypeAny | Record<string, unknown> | undefined;
2028
2071
  /**
2029
2072
  * Per-run skill override (Plan 017). When present, the engine IGNORES
2030
2073
  * `config.skills.autoload` + `config.skills.path` and uses this list
@@ -3047,11 +3090,21 @@ declare const defaultToolResultSummarizer: ToolResultSummarizerV1;
3047
3090
  * All pure JS — no `node:` imports, Workers-compatible.
3048
3091
  */
3049
3092
 
3093
+ /**
3094
+ * Schema users can supply for structured output. Either:
3095
+ * - a Zod schema (typed validation, runs `safeParse` post-parse), or
3096
+ * - a plain JSON Schema object (used verbatim in the system prompt;
3097
+ * no strict server-side validation in v1).
3098
+ *
3099
+ * Workflow definitions stored as JSON (e.g. nikaido) can only carry
3100
+ * the JSON Schema variant; native TS callers may use either.
3101
+ */
3102
+ type OutputSchema = ZodTypeAny | Record<string, unknown>;
3050
3103
  /**
3051
3104
  * Build the output format section to append to the system prompt.
3052
3105
  * Called when outputFormat is 'json'.
3053
3106
  */
3054
- declare function buildSchemaPrompt(schema?: ZodTypeAny): string;
3107
+ declare function buildSchemaPrompt(schema?: OutputSchema): string;
3055
3108
  interface ParseResult {
3056
3109
  readonly ok: boolean;
3057
3110
  readonly value?: unknown;
@@ -3065,10 +3118,14 @@ interface ParseResult {
3065
3118
  */
3066
3119
  declare function tryParseJSON(text: string): ParseResult;
3067
3120
  /**
3068
- * Validate parsed JSON against a Zod schema.
3069
- * Returns the validated data or an error message.
3121
+ * Validate parsed JSON against a schema.
3122
+ *
3123
+ * Zod schemas get strict `safeParse` validation. Plain JSON Schema
3124
+ * objects are NOT strictly validated (skipping ajv keeps the bundle
3125
+ * small for Workers); the prompt-injected schema is the contract and
3126
+ * the caller can validate downstream if needed.
3070
3127
  */
3071
- declare function validateOutput(value: unknown, schema: ZodTypeAny): {
3128
+ declare function validateOutput(value: unknown, schema: OutputSchema): {
3072
3129
  ok: true;
3073
3130
  data: unknown;
3074
3131
  } | {
package/dist/index.js CHANGED
@@ -1716,7 +1716,11 @@ var AnthropicClient = class {
1716
1716
  messages: request.messages,
1717
1717
  stream: true,
1718
1718
  ...request.system !== void 0 ? { system: request.system } : {},
1719
- ...request.tools !== void 0 ? { tools: request.tools } : {}
1719
+ ...request.tools !== void 0 ? { tools: request.tools } : {},
1720
+ // Plan 025 — `'required'` maps to Anthropic's `tool_choice: { type: 'any' }`,
1721
+ // which forces the model to call SOME tool but doesn't pin which.
1722
+ // `'auto'` is the SDK default — omit to let it through unchanged.
1723
+ ...request.toolChoice === "required" ? { tool_choice: { type: "any" } } : {}
1720
1724
  };
1721
1725
  const requestOptions = {};
1722
1726
  if (betas.length > 0) {
@@ -1898,6 +1902,9 @@ var AISdkAdapter = class {
1898
1902
  tools,
1899
1903
  ...request.maxTokens !== void 0 ? { maxOutputTokens: request.maxTokens } : {},
1900
1904
  ...request.temperature !== void 0 ? { temperature: request.temperature } : {},
1905
+ // Plan 025 — pass through `'required'` so the AI SDK forwards it
1906
+ // to the provider as that provider's "force tool call" flag.
1907
+ ...request.toolChoice === "required" ? { toolChoice: "required" } : {},
1901
1908
  maxRetries: this.options.maxRetries ?? 2
1902
1909
  });
1903
1910
  for await (const event of result.fullStream) {
@@ -2517,6 +2524,17 @@ function ensureToolResultPairing(messages) {
2517
2524
  return messages;
2518
2525
  }
2519
2526
 
2527
+ // src/engine/lastTurnGuard.ts
2528
+ init_esm_shims();
2529
+ var LAST_TURN_INSTRUCTION_JSON = "SYSTEM NOTE: This is your final allowed turn. Emit ONLY the JSON object that satisfies the output schema in the system prompt. Do not call any more tools. Do not write any explanation, narration, or markdown \u2014 only the raw JSON.";
2530
+ var LAST_TURN_INSTRUCTION_TEXT = "SYSTEM NOTE: This is your final allowed turn. Stop calling tools and deliver your final answer now. The next turn will not happen.";
2531
+ function lastTurnInstruction(outputFormat) {
2532
+ return outputFormat === "json" ? LAST_TURN_INSTRUCTION_JSON : LAST_TURN_INSTRUCTION_TEXT;
2533
+ }
2534
+ function shouldInjectLastTurnInstruction(opts) {
2535
+ return opts.turnCount + 1 === opts.maxTurns;
2536
+ }
2537
+
2520
2538
  // src/compact/compactor.ts
2521
2539
  init_esm_shims();
2522
2540
 
@@ -3008,15 +3026,29 @@ async function agentLoop(options) {
3008
3026
  const toolCalls = [];
3009
3027
  let stopReason = null;
3010
3028
  let turnUsage = { input: 0, output: 0 };
3029
+ let messagesForApi = messages;
3030
+ if (shouldInjectLastTurnInstruction({
3031
+ turnCount: ctx.getTurnCount(),
3032
+ maxTurns: ctx.getMaxTurns()
3033
+ })) {
3034
+ messagesForApi = [
3035
+ ...messages,
3036
+ {
3037
+ role: "user",
3038
+ content: [{ type: "text", text: lastTurnInstruction(options.outputFormat) }]
3039
+ }
3040
+ ];
3041
+ }
3011
3042
  const normalizedMessages = normalizeMessages(
3012
- messages
3043
+ messagesForApi
3013
3044
  );
3014
3045
  try {
3015
3046
  for await (const event of client.streamMessage({
3016
3047
  messages: normalizedMessages,
3017
3048
  system,
3018
3049
  tools: anthropicTools,
3019
- ...escalatedMaxTokens !== void 0 ? { maxTokens: escalatedMaxTokens } : {}
3050
+ ...escalatedMaxTokens !== void 0 ? { maxTokens: escalatedMaxTokens } : {},
3051
+ ...options.toolChoice !== void 0 ? { toolChoice: options.toolChoice } : {}
3020
3052
  })) {
3021
3053
  const handled = consumeEvent(event);
3022
3054
  if (handled.text !== void 0) textBlocks.push(handled.text);
@@ -3567,6 +3599,9 @@ var RunContext = class {
3567
3599
  getTurnCount() {
3568
3600
  return this.turnCount;
3569
3601
  }
3602
+ getMaxTurns() {
3603
+ return this.maxTurns;
3604
+ }
3570
3605
  getTokensUsed() {
3571
3606
  return this.tokensUsed;
3572
3607
  }
@@ -7767,6 +7802,9 @@ async function collectSkills(storage, skillsDir) {
7767
7802
  // src/engine/jsonOutput.ts
7768
7803
  init_esm_shims();
7769
7804
  import { zodToJsonSchema as zodToJsonSchema2 } from "zod-to-json-schema";
7805
+ function isZodSchema(s) {
7806
+ return s !== null && typeof s === "object" && "_def" in s && typeof s.safeParse === "function";
7807
+ }
7770
7808
  function buildSchemaPrompt(schema) {
7771
7809
  const lines = [
7772
7810
  "# Output Format",
@@ -7776,11 +7814,18 @@ function buildSchemaPrompt(schema) {
7776
7814
  "Do NOT wrap in ```json ... ```. Just raw JSON."
7777
7815
  ];
7778
7816
  if (schema) {
7779
- const jsonSchema2 = zodToJsonSchema2(schema, {
7780
- target: "jsonSchema7",
7781
- $refStrategy: "none"
7782
- });
7783
- const { $schema: _, ...clean } = jsonSchema2;
7817
+ let clean;
7818
+ if (isZodSchema(schema)) {
7819
+ const jsonSchema2 = zodToJsonSchema2(schema, {
7820
+ target: "jsonSchema7",
7821
+ $refStrategy: "none"
7822
+ });
7823
+ const { $schema: _z, ...rest } = jsonSchema2;
7824
+ clean = rest;
7825
+ } else {
7826
+ const { $schema: _j, ...rest } = schema;
7827
+ clean = rest;
7828
+ }
7784
7829
  lines.push("", "The JSON MUST conform to this schema:", JSON.stringify(clean, null, 2));
7785
7830
  } else {
7786
7831
  lines.push("", "Return a JSON object with the relevant data.");
@@ -7816,6 +7861,9 @@ function tryParseJSON2(text2) {
7816
7861
  return { ok: false, error: "No valid JSON found in response" };
7817
7862
  }
7818
7863
  function validateOutput(value, schema) {
7864
+ if (!isZodSchema(schema)) {
7865
+ return { ok: true, data: value };
7866
+ }
7819
7867
  const result = schema.safeParse(value);
7820
7868
  if (result.success) {
7821
7869
  return { ok: true, data: result.data };
@@ -9552,6 +9600,35 @@ var TranscriptReader = class {
9552
9600
  }
9553
9601
  };
9554
9602
 
9603
+ // src/engine/runToolFilter.ts
9604
+ init_esm_shims();
9605
+ function applyRunToolFilter(registry, options) {
9606
+ const stripAll = options.toolChoice === "none" || options.tools !== void 0 && options.tools.length === 0;
9607
+ if (stripAll) {
9608
+ if (options.toolChoice === "required") {
9609
+ throw new EngineError(
9610
+ "ERR_TOOL_CHOICE_CONFLICT",
9611
+ "toolChoice: 'required' is incompatible with an empty tool set (received tools: [] or toolChoice: 'none')."
9612
+ );
9613
+ }
9614
+ for (const tool of registry.list()) {
9615
+ registry.unregister(tool.name);
9616
+ }
9617
+ return;
9618
+ }
9619
+ if (options.tools === void 0) return;
9620
+ const allow = new Set(options.tools);
9621
+ for (const tool of registry.list()) {
9622
+ if (!allow.has(tool.name)) registry.unregister(tool.name);
9623
+ }
9624
+ if (options.toolChoice === "required" && registry.count() === 0) {
9625
+ throw new EngineError(
9626
+ "ERR_TOOL_CHOICE_CONFLICT",
9627
+ "toolChoice: 'required' was requested but no tools matched the per-run allowlist after applying config filters."
9628
+ );
9629
+ }
9630
+ }
9631
+
9555
9632
  // src/engine/rehydrate.ts
9556
9633
  init_esm_shims();
9557
9634
  function rebuildMessagesFromEntries(entries) {
@@ -10028,6 +10105,7 @@ var Engine = class {
10028
10105
  ...knowledgeRuntime !== void 0 ? { knowledge: knowledgeRuntime } : {},
10029
10106
  ...this.internals.fetch !== void 0 ? { fetch: this.internals.fetch } : {}
10030
10107
  });
10108
+ applyRunToolFilter(registry, options);
10031
10109
  const writer = new TranscriptWriter({
10032
10110
  storage: storage.workspace,
10033
10111
  logPath,
@@ -10082,7 +10160,14 @@ var Engine = class {
10082
10160
  ...runTimeout.signal !== void 0 ? { runSignal: runTimeout.signal, runTimeoutMs: this.config.execution.runTimeoutMs } : {},
10083
10161
  ...gate !== void 0 ? { gateBeforeTool: gate } : {},
10084
10162
  ..._internal?.handoffToRunner === true ? { handoffToRunner: true } : {},
10085
- ...offloadConfig !== void 0 ? { toolResultOffload: offloadConfig } : {}
10163
+ ...offloadConfig !== void 0 ? { toolResultOffload: offloadConfig } : {},
10164
+ // Plan 025 — output mode + tool-choice plumbed down so the
10165
+ // last-turn guard picks the right instruction text and the
10166
+ // model adapter can pass `'required'` to the provider. `'none'`
10167
+ // is already handled above by stripping the tool list, so the
10168
+ // loop only ever sees `'auto'` or `'required'`.
10169
+ ...options.outputFormat !== void 0 ? { outputFormat: options.outputFormat } : {},
10170
+ ...options.toolChoice === "required" ? { toolChoice: "required" } : {}
10086
10171
  });
10087
10172
  const result = await this.finalizeResult(loopResult, writer, logPath, {
10088
10173
  ...options.outputFormat !== void 0 ? { outputFormat: options.outputFormat } : {},