@spotpatch/agent 1.2.0 → 1.2.1

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.cjs CHANGED
@@ -69,9 +69,27 @@ function parseJsonRecord(value) {
69
69
  }
70
70
  function parseToolArguments(value) {
71
71
  if (typeof value !== "string") {
72
- throw new import_shared.SpotPatchError(import_shared.ERROR_CODES.PROVIDER_PROTOCOL_UNSUPPORTED);
72
+ throw new import_shared.SpotPatchError(import_shared.ERROR_CODES.TOOL_ARGUMENTS_INVALID);
73
+ }
74
+ let parsed;
75
+ try {
76
+ parsed = JSON.parse(value);
77
+ } catch {
78
+ throw new import_shared.SpotPatchError(import_shared.ERROR_CODES.TOOL_ARGUMENTS_INVALID);
79
+ }
80
+ if (!isRecord(parsed)) {
81
+ throw new import_shared.SpotPatchError(import_shared.ERROR_CODES.TOOL_ARGUMENTS_INVALID);
82
+ }
83
+ return parsed;
84
+ }
85
+ function assertUniqueToolCallIds(calls) {
86
+ const ids = /* @__PURE__ */ new Set();
87
+ for (const call of calls) {
88
+ if (ids.has(call.id)) {
89
+ throw new import_shared.SpotPatchError(import_shared.ERROR_CODES.TOOL_CALL_ID_CONFLICT);
90
+ }
91
+ ids.add(call.id);
73
92
  }
74
- return parseJsonRecord(value);
75
93
  }
76
94
  function requireString(record, field) {
77
95
  const value = record[field];
@@ -81,6 +99,7 @@ function requireString(record, field) {
81
99
  return value;
82
100
  }
83
101
  function validateToolResults(pendingCalls, results) {
102
+ assertUniqueToolCallIds(pendingCalls);
84
103
  if (pendingCalls.length === 0) {
85
104
  if (results !== void 0 && results.length > 0) {
86
105
  throw new import_shared.SpotPatchError(import_shared.ERROR_CODES.INTERNAL_ERROR);
@@ -478,6 +497,7 @@ function parseChatEvents(events) {
478
497
  }
479
498
  const finalText = content.join("");
480
499
  const toolCalls = finalizeToolCalls(calls);
500
+ assertUniqueToolCallIds(toolCalls);
481
501
  if (toolCalls.length === 0 && finalText.trim().length === 0) {
482
502
  throw new import_shared5.SpotPatchError(import_shared5.ERROR_CODES.PROVIDER_PROTOCOL_UNSUPPORTED);
483
503
  }
@@ -574,7 +594,7 @@ function collectFunctionCall(item, calls) {
574
594
  });
575
595
  const existing = calls.get(id);
576
596
  if (existing !== void 0 && (existing.name !== call.name || JSON.stringify(existing.arguments) !== JSON.stringify(call.arguments))) {
577
- throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.PROVIDER_PROTOCOL_UNSUPPORTED);
597
+ throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.TOOL_CALL_ID_CONFLICT);
578
598
  }
579
599
  calls.set(id, call);
580
600
  }
@@ -874,7 +894,7 @@ function encodeUtf8Text(content, includeByteOrderMark) {
874
894
  }
875
895
  async function writeAgentTextFileIfContentMatches(root, relativePath, expectedContent, nextContent, maximumBytes) {
876
896
  if (nextContent.includes("\0")) {
877
- throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.TOOL_INPUT_INVALID);
897
+ throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.TOOL_ARGUMENTS_INVALID);
878
898
  }
879
899
  const absolutePath = await resolveExistingAgentPath(root, relativePath);
880
900
  const [metadata, currentBytes] = await Promise.all([
@@ -1143,7 +1163,7 @@ async function runConfiguredCheck(options) {
1143
1163
  function requireConfiguredCheck(checkId, checks) {
1144
1164
  const check = checks[checkId];
1145
1165
  if (check === void 0) {
1146
- throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.TOOL_INPUT_INVALID);
1166
+ throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.TOOL_ARGUMENTS_INVALID);
1147
1167
  }
1148
1168
  return check;
1149
1169
  }
@@ -1455,7 +1475,7 @@ var MAX_DISCOVERED_FILES = 2e4;
1455
1475
  var TEXT_SAMPLE_BYTES = 8192;
1456
1476
  function compileGlob(glob) {
1457
1477
  if (glob.length === 0 || glob.length > 256 || glob.includes("\0") || glob.includes("\\") || glob.startsWith("/") || ["[", "]", "{", "}", "(", ")", "!"].some((character) => glob.includes(character)) || glob.split("/").some((segment) => segment === "..")) {
1458
- throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.TOOL_INPUT_INVALID);
1478
+ throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.TOOL_ARGUMENTS_INVALID);
1459
1479
  }
1460
1480
  let expression = "^";
1461
1481
  for (let index = 0; index < glob.length; index += 1) {
@@ -1694,7 +1714,7 @@ var runCheckSchema = import_zod.z.strictObject({
1694
1714
  checkId: import_zod.z.string().min(1).max(64).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u)
1695
1715
  });
1696
1716
  function invalidTool() {
1697
- throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.TOOL_INPUT_INVALID);
1717
+ throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.TOOL_ARGUMENTS_INVALID);
1698
1718
  }
1699
1719
  function parseArguments(schema, value) {
1700
1720
  const parsed = schema.safeParse(value);
@@ -1751,8 +1771,16 @@ function retryableWriteRejection(reason, guidance) {
1751
1771
  guidance
1752
1772
  });
1753
1773
  }
1774
+ function retryableArgumentsRejection() {
1775
+ return Object.freeze({
1776
+ errorCode: import_shared15.ERROR_CODES.TOOL_ARGUMENTS_INVALID,
1777
+ retryable: true,
1778
+ reason: "ARGUMENTS_DO_NOT_MATCH_CONTRACT",
1779
+ guidance: "No files changed. Retry once with a new tool call ID and only the declared fields and value types."
1780
+ });
1781
+ }
1754
1782
  function createAgentToolExecutor(options) {
1755
- const cache = /* @__PURE__ */ new Map();
1783
+ const cacheByTurn = /* @__PURE__ */ new Map();
1756
1784
  const touchedPaths = /* @__PURE__ */ new Set();
1757
1785
  const executeUncached = async (call, signal) => {
1758
1786
  switch (call.name) {
@@ -1968,20 +1996,32 @@ function createAgentToolExecutor(options) {
1968
1996
  }
1969
1997
  };
1970
1998
  return Object.freeze({
1971
- async execute(call, signal) {
1999
+ async execute(call, scope, signal) {
2000
+ if (!Number.isSafeInteger(scope.turn) || scope.turn < 1) {
2001
+ throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.INTERNAL_ERROR);
2002
+ }
2003
+ const turnCache = cacheByTurn.get(scope.turn) ?? /* @__PURE__ */ new Map();
2004
+ cacheByTurn.set(scope.turn, turnCache);
1972
2005
  const signature = `${call.name}\0${JSON.stringify(call.arguments)}`;
1973
- const cached = cache.get(call.id);
2006
+ const cached = turnCache.get(call.id);
1974
2007
  if (cached !== void 0) {
1975
2008
  if (cached.signature !== signature) {
1976
- return invalidTool();
2009
+ throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.TOOL_CALL_ID_CONFLICT);
1977
2010
  }
1978
2011
  return cached.result;
1979
2012
  }
1980
- const result = Object.freeze({
1981
- toolCallId: call.id,
1982
- output: await executeUncached(call, signal)
1983
- });
1984
- cache.set(call.id, Object.freeze({ signature, result }));
2013
+ let output;
2014
+ try {
2015
+ output = await executeUncached(call, signal);
2016
+ } catch (error) {
2017
+ if (error instanceof import_shared15.SpotPatchError && error.code === import_shared15.ERROR_CODES.TOOL_ARGUMENTS_INVALID) {
2018
+ output = retryableArgumentsRejection();
2019
+ } else {
2020
+ throw error;
2021
+ }
2022
+ }
2023
+ const result = Object.freeze({ toolCallId: call.id, output });
2024
+ turnCache.set(call.id, Object.freeze({ signature, result }));
1985
2025
  return result;
1986
2026
  },
1987
2027
  touchedPaths() {
@@ -2552,6 +2592,7 @@ Follow these rules exactly:
2552
2592
  - Use apply_patch only when creating or deleting a file, or when the change cannot be expressed as one exact replacement. apply_patch accepts only a raw canonical unified Git diff.
2553
2593
  - Every patch must begin with 'diff --git a/<path> b/<path>', include matching '--- a/<path>' and '+++ b/<path>' headers and valid '@@' hunks. Send only the raw diff: no Markdown fences, prose, shell commands, or '*** Begin Patch' markers.
2554
2594
  - If a write tool returns a retryable PATCH_REJECTED result, no file changed. Follow its guidance, re-read the current file, and retry once with a new tool call ID.
2595
+ - If any tool returns a retryable TOOL_ARGUMENTS_INVALID result, no file changed. Retry once with a new tool call ID using only the declared fields and value types.
2555
2596
  - Never modify credentials, environment files, lockfiles, generated output, Git metadata, or dependencies.
2556
2597
  - Do not claim a check passed unless run_check returned a passed status.
2557
2598
  - Finish with a concise factual summary after all needed tool calls. Do not include secrets or absolute paths.`;
@@ -2711,7 +2752,7 @@ function isRetryableToolFailure(result) {
2711
2752
  return false;
2712
2753
  }
2713
2754
  const candidate = output;
2714
- return candidate.errorCode === import_shared20.ERROR_CODES.PATCH_REJECTED && candidate.retryable === true;
2755
+ return candidate.retryable === true && (candidate.errorCode === import_shared20.ERROR_CODES.PATCH_REJECTED || candidate.errorCode === import_shared20.ERROR_CODES.TOOL_ARGUMENTS_INVALID);
2715
2756
  }
2716
2757
  function throwIfCancelled(signal) {
2717
2758
  if (signal.aborted) {
@@ -2787,8 +2828,10 @@ async function executeAgentChange(options) {
2787
2828
  let summary;
2788
2829
  let toolCallCount = 0;
2789
2830
  for (let turn = 0; turn < options.execution.limits.maxTurns; turn += 1) {
2831
+ const turnNumber = turn + 1;
2790
2832
  throwIfCancelled(controller.signal);
2791
2833
  const response = await session.next(pendingResults, controller.signal);
2834
+ assertUniqueToolCallIds(response.toolCalls);
2792
2835
  if (response.toolCalls.length === 0) {
2793
2836
  summary = response.finalText.trim().slice(0, options.execution.limits.maxToolOutputCharacters);
2794
2837
  break;
@@ -2801,16 +2844,22 @@ async function executeAgentChange(options) {
2801
2844
  for (const call of response.toolCalls) {
2802
2845
  options.callbacks?.onTool?.(
2803
2846
  Object.freeze({
2847
+ turn: turnNumber,
2804
2848
  toolCallId: call.id,
2805
2849
  toolName: call.name,
2806
2850
  state: "started"
2807
2851
  })
2808
2852
  );
2809
2853
  try {
2810
- const result2 = await executor.execute(call, controller.signal);
2854
+ const result2 = await executor.execute(
2855
+ call,
2856
+ Object.freeze({ turn: turnNumber }),
2857
+ controller.signal
2858
+ );
2811
2859
  results.push(result2);
2812
2860
  options.callbacks?.onTool?.(
2813
2861
  Object.freeze({
2862
+ turn: turnNumber,
2814
2863
  toolCallId: call.id,
2815
2864
  toolName: call.name,
2816
2865
  state: isRetryableToolFailure(result2) ? "failed" : "succeeded"
@@ -2819,6 +2868,7 @@ async function executeAgentChange(options) {
2819
2868
  } catch (error) {
2820
2869
  options.callbacks?.onTool?.(
2821
2870
  Object.freeze({
2871
+ turn: turnNumber,
2822
2872
  toolCallId: call.id,
2823
2873
  toolName: call.name,
2824
2874
  state: "failed"