@spotpatch/agent 1.2.0 → 1.2.2

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([
@@ -1036,7 +1056,10 @@ function minimalProcessEnvironment() {
1036
1056
  "LANG",
1037
1057
  "LC_ALL"
1038
1058
  ];
1039
- const environment = { CI: "1", NO_COLOR: "1" };
1059
+ const environment = {
1060
+ CI: "1",
1061
+ NO_COLOR: "1"
1062
+ };
1040
1063
  for (const name of allowedNames) {
1041
1064
  const value = process.env[name];
1042
1065
  if (value !== void 0) {
@@ -1113,7 +1136,7 @@ async function runConfiguredCheck(options) {
1113
1136
  function requireConfiguredCheck(checkId, checks) {
1114
1137
  const check = checks[checkId];
1115
1138
  if (check === void 0) {
1116
- throw new SpotPatchError10(ERROR_CODES10.TOOL_INPUT_INVALID);
1139
+ throw new SpotPatchError10(ERROR_CODES10.TOOL_ARGUMENTS_INVALID);
1117
1140
  }
1118
1141
  return check;
1119
1142
  }
@@ -1431,7 +1454,7 @@ var MAX_DISCOVERED_FILES = 2e4;
1431
1454
  var TEXT_SAMPLE_BYTES = 8192;
1432
1455
  function compileGlob(glob) {
1433
1456
  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);
1457
+ throw new SpotPatchError14(ERROR_CODES14.TOOL_ARGUMENTS_INVALID);
1435
1458
  }
1436
1459
  let expression = "^";
1437
1460
  for (let index = 0; index < glob.length; index += 1) {
@@ -1670,7 +1693,7 @@ var runCheckSchema = z.strictObject({
1670
1693
  checkId: z.string().min(1).max(64).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u)
1671
1694
  });
1672
1695
  function invalidTool() {
1673
- throw new SpotPatchError15(ERROR_CODES15.TOOL_INPUT_INVALID);
1696
+ throw new SpotPatchError15(ERROR_CODES15.TOOL_ARGUMENTS_INVALID);
1674
1697
  }
1675
1698
  function parseArguments(schema, value) {
1676
1699
  const parsed = schema.safeParse(value);
@@ -1727,8 +1750,16 @@ function retryableWriteRejection(reason, guidance) {
1727
1750
  guidance
1728
1751
  });
1729
1752
  }
1753
+ function retryableArgumentsRejection() {
1754
+ return Object.freeze({
1755
+ errorCode: ERROR_CODES15.TOOL_ARGUMENTS_INVALID,
1756
+ retryable: true,
1757
+ reason: "ARGUMENTS_DO_NOT_MATCH_CONTRACT",
1758
+ guidance: "No files changed. Retry once with a new tool call ID and only the declared fields and value types."
1759
+ });
1760
+ }
1730
1761
  function createAgentToolExecutor(options) {
1731
- const cache = /* @__PURE__ */ new Map();
1762
+ const cacheByTurn = /* @__PURE__ */ new Map();
1732
1763
  const touchedPaths = /* @__PURE__ */ new Set();
1733
1764
  const executeUncached = async (call, signal) => {
1734
1765
  switch (call.name) {
@@ -1944,20 +1975,32 @@ function createAgentToolExecutor(options) {
1944
1975
  }
1945
1976
  };
1946
1977
  return Object.freeze({
1947
- async execute(call, signal) {
1978
+ async execute(call, scope, signal) {
1979
+ if (!Number.isSafeInteger(scope.turn) || scope.turn < 1) {
1980
+ throw new SpotPatchError15(ERROR_CODES15.INTERNAL_ERROR);
1981
+ }
1982
+ const turnCache = cacheByTurn.get(scope.turn) ?? /* @__PURE__ */ new Map();
1983
+ cacheByTurn.set(scope.turn, turnCache);
1948
1984
  const signature = `${call.name}\0${JSON.stringify(call.arguments)}`;
1949
- const cached = cache.get(call.id);
1985
+ const cached = turnCache.get(call.id);
1950
1986
  if (cached !== void 0) {
1951
1987
  if (cached.signature !== signature) {
1952
- return invalidTool();
1988
+ throw new SpotPatchError15(ERROR_CODES15.TOOL_CALL_ID_CONFLICT);
1953
1989
  }
1954
1990
  return cached.result;
1955
1991
  }
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 }));
1992
+ let output;
1993
+ try {
1994
+ output = await executeUncached(call, signal);
1995
+ } catch (error) {
1996
+ if (error instanceof SpotPatchError15 && error.code === ERROR_CODES15.TOOL_ARGUMENTS_INVALID) {
1997
+ output = retryableArgumentsRejection();
1998
+ } else {
1999
+ throw error;
2000
+ }
2001
+ }
2002
+ const result = Object.freeze({ toolCallId: call.id, output });
2003
+ turnCache.set(call.id, Object.freeze({ signature, result }));
1961
2004
  return result;
1962
2005
  },
1963
2006
  touchedPaths() {
@@ -2546,6 +2589,7 @@ Follow these rules exactly:
2546
2589
  - 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
2590
  - 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
2591
  - 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.
2592
+ - 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
2593
  - Never modify credentials, environment files, lockfiles, generated output, Git metadata, or dependencies.
2550
2594
  - Do not claim a check passed unless run_check returned a passed status.
2551
2595
  - Finish with a concise factual summary after all needed tool calls. Do not include secrets or absolute paths.`;
@@ -2562,6 +2606,12 @@ function sliceText(value, maximum) {
2562
2606
  function createBoundedTarget(target, maximumCharacters) {
2563
2607
  const detailBudget = Math.max(192, maximumCharacters - 420);
2564
2608
  const bounded = {
2609
+ ...target.page === void 0 ? {} : {
2610
+ page: Object.freeze({
2611
+ ...target.page,
2612
+ url: sanitizeUrl(target.page.url, "http://spotpatch.invalid")
2613
+ })
2614
+ },
2565
2615
  source: target.source,
2566
2616
  react: Object.freeze({
2567
2617
  supported: target.react.supported,
@@ -2705,7 +2755,7 @@ function isRetryableToolFailure(result) {
2705
2755
  return false;
2706
2756
  }
2707
2757
  const candidate = output;
2708
- return candidate.errorCode === ERROR_CODES19.PATCH_REJECTED && candidate.retryable === true;
2758
+ return candidate.retryable === true && (candidate.errorCode === ERROR_CODES19.PATCH_REJECTED || candidate.errorCode === ERROR_CODES19.TOOL_ARGUMENTS_INVALID);
2709
2759
  }
2710
2760
  function throwIfCancelled(signal) {
2711
2761
  if (signal.aborted) {
@@ -2781,8 +2831,10 @@ async function executeAgentChange(options) {
2781
2831
  let summary;
2782
2832
  let toolCallCount = 0;
2783
2833
  for (let turn = 0; turn < options.execution.limits.maxTurns; turn += 1) {
2834
+ const turnNumber = turn + 1;
2784
2835
  throwIfCancelled(controller.signal);
2785
2836
  const response = await session.next(pendingResults, controller.signal);
2837
+ assertUniqueToolCallIds(response.toolCalls);
2786
2838
  if (response.toolCalls.length === 0) {
2787
2839
  summary = response.finalText.trim().slice(0, options.execution.limits.maxToolOutputCharacters);
2788
2840
  break;
@@ -2795,16 +2847,22 @@ async function executeAgentChange(options) {
2795
2847
  for (const call of response.toolCalls) {
2796
2848
  options.callbacks?.onTool?.(
2797
2849
  Object.freeze({
2850
+ turn: turnNumber,
2798
2851
  toolCallId: call.id,
2799
2852
  toolName: call.name,
2800
2853
  state: "started"
2801
2854
  })
2802
2855
  );
2803
2856
  try {
2804
- const result2 = await executor.execute(call, controller.signal);
2857
+ const result2 = await executor.execute(
2858
+ call,
2859
+ Object.freeze({ turn: turnNumber }),
2860
+ controller.signal
2861
+ );
2805
2862
  results.push(result2);
2806
2863
  options.callbacks?.onTool?.(
2807
2864
  Object.freeze({
2865
+ turn: turnNumber,
2808
2866
  toolCallId: call.id,
2809
2867
  toolName: call.name,
2810
2868
  state: isRetryableToolFailure(result2) ? "failed" : "succeeded"
@@ -2813,6 +2871,7 @@ async function executeAgentChange(options) {
2813
2871
  } catch (error) {
2814
2872
  options.callbacks?.onTool?.(
2815
2873
  Object.freeze({
2874
+ turn: turnNumber,
2816
2875
  toolCallId: call.id,
2817
2876
  toolName: call.name,
2818
2877
  state: "failed"