@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.d.cts CHANGED
@@ -22,6 +22,7 @@ interface AgentExecutionCallbacks {
22
22
  message: string;
23
23
  }>) => void;
24
24
  readonly onTool?: (event: Readonly<{
25
+ turn: number;
25
26
  toolCallId: string;
26
27
  toolName: string;
27
28
  state: "started" | "succeeded" | "failed";
package/dist/index.d.ts CHANGED
@@ -22,6 +22,7 @@ interface AgentExecutionCallbacks {
22
22
  message: string;
23
23
  }>) => void;
24
24
  readonly onTool?: (event: Readonly<{
25
+ turn: number;
25
26
  toolCallId: string;
26
27
  toolName: string;
27
28
  state: "started" | "succeeded" | "failed";
package/dist/index.js CHANGED
@@ -29,9 +29,27 @@ function parseJsonRecord(value) {
29
29
  }
30
30
  function parseToolArguments(value) {
31
31
  if (typeof value !== "string") {
32
- throw new SpotPatchError(ERROR_CODES.PROVIDER_PROTOCOL_UNSUPPORTED);
32
+ throw new SpotPatchError(ERROR_CODES.TOOL_ARGUMENTS_INVALID);
33
+ }
34
+ let parsed;
35
+ try {
36
+ parsed = JSON.parse(value);
37
+ } catch {
38
+ throw new SpotPatchError(ERROR_CODES.TOOL_ARGUMENTS_INVALID);
39
+ }
40
+ if (!isRecord(parsed)) {
41
+ throw new SpotPatchError(ERROR_CODES.TOOL_ARGUMENTS_INVALID);
42
+ }
43
+ return parsed;
44
+ }
45
+ function assertUniqueToolCallIds(calls) {
46
+ const ids = /* @__PURE__ */ new Set();
47
+ for (const call of calls) {
48
+ if (ids.has(call.id)) {
49
+ throw new SpotPatchError(ERROR_CODES.TOOL_CALL_ID_CONFLICT);
50
+ }
51
+ ids.add(call.id);
33
52
  }
34
- return parseJsonRecord(value);
35
53
  }
36
54
  function requireString(record, field) {
37
55
  const value = record[field];
@@ -41,6 +59,7 @@ function requireString(record, field) {
41
59
  return value;
42
60
  }
43
61
  function validateToolResults(pendingCalls, results) {
62
+ assertUniqueToolCallIds(pendingCalls);
44
63
  if (pendingCalls.length === 0) {
45
64
  if (results !== void 0 && results.length > 0) {
46
65
  throw new SpotPatchError(ERROR_CODES.INTERNAL_ERROR);
@@ -441,6 +460,7 @@ function parseChatEvents(events) {
441
460
  }
442
461
  const finalText = content.join("");
443
462
  const toolCalls = finalizeToolCalls(calls);
463
+ assertUniqueToolCallIds(toolCalls);
444
464
  if (toolCalls.length === 0 && finalText.trim().length === 0) {
445
465
  throw new SpotPatchError5(ERROR_CODES5.PROVIDER_PROTOCOL_UNSUPPORTED);
446
466
  }
@@ -537,7 +557,7 @@ function collectFunctionCall(item, calls) {
537
557
  });
538
558
  const existing = calls.get(id);
539
559
  if (existing !== void 0 && (existing.name !== call.name || JSON.stringify(existing.arguments) !== JSON.stringify(call.arguments))) {
540
- throw new SpotPatchError6(ERROR_CODES6.PROVIDER_PROTOCOL_UNSUPPORTED);
560
+ throw new SpotPatchError6(ERROR_CODES6.TOOL_CALL_ID_CONFLICT);
541
561
  }
542
562
  calls.set(id, call);
543
563
  }
@@ -840,7 +860,7 @@ function encodeUtf8Text(content, includeByteOrderMark) {
840
860
  }
841
861
  async function writeAgentTextFileIfContentMatches(root, relativePath, expectedContent, nextContent, maximumBytes) {
842
862
  if (nextContent.includes("\0")) {
843
- throw new SpotPatchError9(ERROR_CODES9.TOOL_INPUT_INVALID);
863
+ throw new SpotPatchError9(ERROR_CODES9.TOOL_ARGUMENTS_INVALID);
844
864
  }
845
865
  const absolutePath = await resolveExistingAgentPath(root, relativePath);
846
866
  const [metadata, currentBytes] = await Promise.all([
@@ -1113,7 +1133,7 @@ async function runConfiguredCheck(options) {
1113
1133
  function requireConfiguredCheck(checkId, checks) {
1114
1134
  const check = checks[checkId];
1115
1135
  if (check === void 0) {
1116
- throw new SpotPatchError10(ERROR_CODES10.TOOL_INPUT_INVALID);
1136
+ throw new SpotPatchError10(ERROR_CODES10.TOOL_ARGUMENTS_INVALID);
1117
1137
  }
1118
1138
  return check;
1119
1139
  }
@@ -1431,7 +1451,7 @@ var MAX_DISCOVERED_FILES = 2e4;
1431
1451
  var TEXT_SAMPLE_BYTES = 8192;
1432
1452
  function compileGlob(glob) {
1433
1453
  if (glob.length === 0 || glob.length > 256 || glob.includes("\0") || glob.includes("\\") || glob.startsWith("/") || ["[", "]", "{", "}", "(", ")", "!"].some((character) => glob.includes(character)) || glob.split("/").some((segment) => segment === "..")) {
1434
- throw new SpotPatchError14(ERROR_CODES14.TOOL_INPUT_INVALID);
1454
+ throw new SpotPatchError14(ERROR_CODES14.TOOL_ARGUMENTS_INVALID);
1435
1455
  }
1436
1456
  let expression = "^";
1437
1457
  for (let index = 0; index < glob.length; index += 1) {
@@ -1670,7 +1690,7 @@ var runCheckSchema = z.strictObject({
1670
1690
  checkId: z.string().min(1).max(64).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u)
1671
1691
  });
1672
1692
  function invalidTool() {
1673
- throw new SpotPatchError15(ERROR_CODES15.TOOL_INPUT_INVALID);
1693
+ throw new SpotPatchError15(ERROR_CODES15.TOOL_ARGUMENTS_INVALID);
1674
1694
  }
1675
1695
  function parseArguments(schema, value) {
1676
1696
  const parsed = schema.safeParse(value);
@@ -1727,8 +1747,16 @@ function retryableWriteRejection(reason, guidance) {
1727
1747
  guidance
1728
1748
  });
1729
1749
  }
1750
+ function retryableArgumentsRejection() {
1751
+ return Object.freeze({
1752
+ errorCode: ERROR_CODES15.TOOL_ARGUMENTS_INVALID,
1753
+ retryable: true,
1754
+ reason: "ARGUMENTS_DO_NOT_MATCH_CONTRACT",
1755
+ guidance: "No files changed. Retry once with a new tool call ID and only the declared fields and value types."
1756
+ });
1757
+ }
1730
1758
  function createAgentToolExecutor(options) {
1731
- const cache = /* @__PURE__ */ new Map();
1759
+ const cacheByTurn = /* @__PURE__ */ new Map();
1732
1760
  const touchedPaths = /* @__PURE__ */ new Set();
1733
1761
  const executeUncached = async (call, signal) => {
1734
1762
  switch (call.name) {
@@ -1944,20 +1972,32 @@ function createAgentToolExecutor(options) {
1944
1972
  }
1945
1973
  };
1946
1974
  return Object.freeze({
1947
- async execute(call, signal) {
1975
+ async execute(call, scope, signal) {
1976
+ if (!Number.isSafeInteger(scope.turn) || scope.turn < 1) {
1977
+ throw new SpotPatchError15(ERROR_CODES15.INTERNAL_ERROR);
1978
+ }
1979
+ const turnCache = cacheByTurn.get(scope.turn) ?? /* @__PURE__ */ new Map();
1980
+ cacheByTurn.set(scope.turn, turnCache);
1948
1981
  const signature = `${call.name}\0${JSON.stringify(call.arguments)}`;
1949
- const cached = cache.get(call.id);
1982
+ const cached = turnCache.get(call.id);
1950
1983
  if (cached !== void 0) {
1951
1984
  if (cached.signature !== signature) {
1952
- return invalidTool();
1985
+ throw new SpotPatchError15(ERROR_CODES15.TOOL_CALL_ID_CONFLICT);
1953
1986
  }
1954
1987
  return cached.result;
1955
1988
  }
1956
- const result = Object.freeze({
1957
- toolCallId: call.id,
1958
- output: await executeUncached(call, signal)
1959
- });
1960
- cache.set(call.id, Object.freeze({ signature, result }));
1989
+ let output;
1990
+ try {
1991
+ output = await executeUncached(call, signal);
1992
+ } catch (error) {
1993
+ if (error instanceof SpotPatchError15 && error.code === ERROR_CODES15.TOOL_ARGUMENTS_INVALID) {
1994
+ output = retryableArgumentsRejection();
1995
+ } else {
1996
+ throw error;
1997
+ }
1998
+ }
1999
+ const result = Object.freeze({ toolCallId: call.id, output });
2000
+ turnCache.set(call.id, Object.freeze({ signature, result }));
1961
2001
  return result;
1962
2002
  },
1963
2003
  touchedPaths() {
@@ -2546,6 +2586,7 @@ Follow these rules exactly:
2546
2586
  - 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.
2547
2587
  - 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.
2548
2588
  - 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.
2589
+ - 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.
2549
2590
  - Never modify credentials, environment files, lockfiles, generated output, Git metadata, or dependencies.
2550
2591
  - Do not claim a check passed unless run_check returned a passed status.
2551
2592
  - Finish with a concise factual summary after all needed tool calls. Do not include secrets or absolute paths.`;
@@ -2705,7 +2746,7 @@ function isRetryableToolFailure(result) {
2705
2746
  return false;
2706
2747
  }
2707
2748
  const candidate = output;
2708
- return candidate.errorCode === ERROR_CODES19.PATCH_REJECTED && candidate.retryable === true;
2749
+ return candidate.retryable === true && (candidate.errorCode === ERROR_CODES19.PATCH_REJECTED || candidate.errorCode === ERROR_CODES19.TOOL_ARGUMENTS_INVALID);
2709
2750
  }
2710
2751
  function throwIfCancelled(signal) {
2711
2752
  if (signal.aborted) {
@@ -2781,8 +2822,10 @@ async function executeAgentChange(options) {
2781
2822
  let summary;
2782
2823
  let toolCallCount = 0;
2783
2824
  for (let turn = 0; turn < options.execution.limits.maxTurns; turn += 1) {
2825
+ const turnNumber = turn + 1;
2784
2826
  throwIfCancelled(controller.signal);
2785
2827
  const response = await session.next(pendingResults, controller.signal);
2828
+ assertUniqueToolCallIds(response.toolCalls);
2786
2829
  if (response.toolCalls.length === 0) {
2787
2830
  summary = response.finalText.trim().slice(0, options.execution.limits.maxToolOutputCharacters);
2788
2831
  break;
@@ -2795,16 +2838,22 @@ async function executeAgentChange(options) {
2795
2838
  for (const call of response.toolCalls) {
2796
2839
  options.callbacks?.onTool?.(
2797
2840
  Object.freeze({
2841
+ turn: turnNumber,
2798
2842
  toolCallId: call.id,
2799
2843
  toolName: call.name,
2800
2844
  state: "started"
2801
2845
  })
2802
2846
  );
2803
2847
  try {
2804
- const result2 = await executor.execute(call, controller.signal);
2848
+ const result2 = await executor.execute(
2849
+ call,
2850
+ Object.freeze({ turn: turnNumber }),
2851
+ controller.signal
2852
+ );
2805
2853
  results.push(result2);
2806
2854
  options.callbacks?.onTool?.(
2807
2855
  Object.freeze({
2856
+ turn: turnNumber,
2808
2857
  toolCallId: call.id,
2809
2858
  toolName: call.name,
2810
2859
  state: isRetryableToolFailure(result2) ? "failed" : "succeeded"
@@ -2813,6 +2862,7 @@ async function executeAgentChange(options) {
2813
2862
  } catch (error) {
2814
2863
  options.callbacks?.onTool?.(
2815
2864
  Object.freeze({
2865
+ turn: turnNumber,
2816
2866
  toolCallId: call.id,
2817
2867
  toolName: call.name,
2818
2868
  state: "failed"