@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/README.md +31 -8
- package/dist/index.cjs +78 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +78 -19
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,10 +1,33 @@
|
|
|
1
|
-
|
|
1
|
+
<h1><a href="https://github.com/huanglvjing/spotpatch"><img src="https://raw.githubusercontent.com/huanglvjing/spotpatch/main/docs/assets/spotpatch-npm-icon.png" alt="SpotPatch" width="48" height="48" align="absmiddle" /></a> <code>@spotpatch/agent</code></h1>
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
isolated Git worktrees, validation checks, review, Apply, and conflict-safe
|
|
5
|
-
Revert.
|
|
3
|
+
## English
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
The Node-only Agent engine used by SpotPatch framework adapters. It owns OpenAI-compatible provider sessions, capability probes, bounded file tools, isolated Git worktrees, validation checks, Diff review, Apply, and conflict-safe Revert.
|
|
6
|
+
|
|
7
|
+
Applications should enable this capability through a framework adapter such as [`@spotpatch/vite`](https://www.npmjs.com/package/@spotpatch/vite). This package is public so the adapter dependency graph can be installed and versioned; it is not a standalone UI integration.
|
|
8
|
+
|
|
9
|
+
Security boundaries:
|
|
10
|
+
|
|
11
|
+
- no model-controlled arbitrary shell;
|
|
12
|
+
- no browser-side API keys;
|
|
13
|
+
- bounded paths, reads, writes, Diff sizes, turns, and tool calls;
|
|
14
|
+
- no implicit stash, reset, commit, push, publish, or deployment;
|
|
15
|
+
- review is the default apply mode.
|
|
16
|
+
|
|
17
|
+
Requires Node.js `>=20.19.0`.
|
|
18
|
+
|
|
19
|
+
## 简体中文
|
|
20
|
+
|
|
21
|
+
这是 SpotPatch 框架适配器使用的 Node-only Agent 引擎,负责 OpenAI-compatible Provider 会话、能力探测、有界文件工具、隔离 Git worktree、项目检查、Diff 审阅、Apply 和冲突安全的 Revert。
|
|
22
|
+
|
|
23
|
+
业务应用应通过 [`@spotpatch/vite`](https://www.npmjs.com/package/@spotpatch/vite) 等框架适配器启用该能力。本包公开发布是为了形成可安装、可版本化的依赖图,不是独立 UI 接入入口。
|
|
24
|
+
|
|
25
|
+
安全边界包括:不向模型开放任意 Shell、不把 API Key 放入浏览器、限制路径/读写/Diff/轮次/工具调用,并且不隐式执行 stash、reset、commit、push、发包或部署。默认应用模式必须经过审阅。
|
|
26
|
+
|
|
27
|
+
要求 Node.js `>=20.19.0`。
|
|
28
|
+
|
|
29
|
+
## Links / 链接
|
|
30
|
+
|
|
31
|
+
- [Repository / 仓库](https://github.com/huanglvjing/spotpatch)
|
|
32
|
+
- [AI execution model / AI 执行模型](https://github.com/huanglvjing/spotpatch/blob/main/docs/%E6%8A%80%E6%9C%AF%E6%96%B9%E6%A1%88/16-AIAgent%E6%89%A7%E8%A1%8C%E4%B8%8E%E5%8F%98%E6%9B%B4%E5%AE%A1%E9%98%85.md)
|
|
33
|
+
- [MIT License / 许可证](https://github.com/huanglvjing/spotpatch/blob/main/LICENSE)
|
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.
|
|
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.
|
|
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.
|
|
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([
|
|
@@ -1066,7 +1086,10 @@ function minimalProcessEnvironment() {
|
|
|
1066
1086
|
"LANG",
|
|
1067
1087
|
"LC_ALL"
|
|
1068
1088
|
];
|
|
1069
|
-
const environment = {
|
|
1089
|
+
const environment = {
|
|
1090
|
+
CI: "1",
|
|
1091
|
+
NO_COLOR: "1"
|
|
1092
|
+
};
|
|
1070
1093
|
for (const name of allowedNames) {
|
|
1071
1094
|
const value = process.env[name];
|
|
1072
1095
|
if (value !== void 0) {
|
|
@@ -1143,7 +1166,7 @@ async function runConfiguredCheck(options) {
|
|
|
1143
1166
|
function requireConfiguredCheck(checkId, checks) {
|
|
1144
1167
|
const check = checks[checkId];
|
|
1145
1168
|
if (check === void 0) {
|
|
1146
|
-
throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.
|
|
1169
|
+
throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.TOOL_ARGUMENTS_INVALID);
|
|
1147
1170
|
}
|
|
1148
1171
|
return check;
|
|
1149
1172
|
}
|
|
@@ -1455,7 +1478,7 @@ var MAX_DISCOVERED_FILES = 2e4;
|
|
|
1455
1478
|
var TEXT_SAMPLE_BYTES = 8192;
|
|
1456
1479
|
function compileGlob(glob) {
|
|
1457
1480
|
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.
|
|
1481
|
+
throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.TOOL_ARGUMENTS_INVALID);
|
|
1459
1482
|
}
|
|
1460
1483
|
let expression = "^";
|
|
1461
1484
|
for (let index = 0; index < glob.length; index += 1) {
|
|
@@ -1694,7 +1717,7 @@ var runCheckSchema = import_zod.z.strictObject({
|
|
|
1694
1717
|
checkId: import_zod.z.string().min(1).max(64).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u)
|
|
1695
1718
|
});
|
|
1696
1719
|
function invalidTool() {
|
|
1697
|
-
throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.
|
|
1720
|
+
throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.TOOL_ARGUMENTS_INVALID);
|
|
1698
1721
|
}
|
|
1699
1722
|
function parseArguments(schema, value) {
|
|
1700
1723
|
const parsed = schema.safeParse(value);
|
|
@@ -1751,8 +1774,16 @@ function retryableWriteRejection(reason, guidance) {
|
|
|
1751
1774
|
guidance
|
|
1752
1775
|
});
|
|
1753
1776
|
}
|
|
1777
|
+
function retryableArgumentsRejection() {
|
|
1778
|
+
return Object.freeze({
|
|
1779
|
+
errorCode: import_shared15.ERROR_CODES.TOOL_ARGUMENTS_INVALID,
|
|
1780
|
+
retryable: true,
|
|
1781
|
+
reason: "ARGUMENTS_DO_NOT_MATCH_CONTRACT",
|
|
1782
|
+
guidance: "No files changed. Retry once with a new tool call ID and only the declared fields and value types."
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1754
1785
|
function createAgentToolExecutor(options) {
|
|
1755
|
-
const
|
|
1786
|
+
const cacheByTurn = /* @__PURE__ */ new Map();
|
|
1756
1787
|
const touchedPaths = /* @__PURE__ */ new Set();
|
|
1757
1788
|
const executeUncached = async (call, signal) => {
|
|
1758
1789
|
switch (call.name) {
|
|
@@ -1968,20 +1999,32 @@ function createAgentToolExecutor(options) {
|
|
|
1968
1999
|
}
|
|
1969
2000
|
};
|
|
1970
2001
|
return Object.freeze({
|
|
1971
|
-
async execute(call, signal) {
|
|
2002
|
+
async execute(call, scope, signal) {
|
|
2003
|
+
if (!Number.isSafeInteger(scope.turn) || scope.turn < 1) {
|
|
2004
|
+
throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.INTERNAL_ERROR);
|
|
2005
|
+
}
|
|
2006
|
+
const turnCache = cacheByTurn.get(scope.turn) ?? /* @__PURE__ */ new Map();
|
|
2007
|
+
cacheByTurn.set(scope.turn, turnCache);
|
|
1972
2008
|
const signature = `${call.name}\0${JSON.stringify(call.arguments)}`;
|
|
1973
|
-
const cached =
|
|
2009
|
+
const cached = turnCache.get(call.id);
|
|
1974
2010
|
if (cached !== void 0) {
|
|
1975
2011
|
if (cached.signature !== signature) {
|
|
1976
|
-
|
|
2012
|
+
throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.TOOL_CALL_ID_CONFLICT);
|
|
1977
2013
|
}
|
|
1978
2014
|
return cached.result;
|
|
1979
2015
|
}
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
output
|
|
1983
|
-
})
|
|
1984
|
-
|
|
2016
|
+
let output;
|
|
2017
|
+
try {
|
|
2018
|
+
output = await executeUncached(call, signal);
|
|
2019
|
+
} catch (error) {
|
|
2020
|
+
if (error instanceof import_shared15.SpotPatchError && error.code === import_shared15.ERROR_CODES.TOOL_ARGUMENTS_INVALID) {
|
|
2021
|
+
output = retryableArgumentsRejection();
|
|
2022
|
+
} else {
|
|
2023
|
+
throw error;
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
const result = Object.freeze({ toolCallId: call.id, output });
|
|
2027
|
+
turnCache.set(call.id, Object.freeze({ signature, result }));
|
|
1985
2028
|
return result;
|
|
1986
2029
|
},
|
|
1987
2030
|
touchedPaths() {
|
|
@@ -2552,6 +2595,7 @@ Follow these rules exactly:
|
|
|
2552
2595
|
- 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
2596
|
- 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
2597
|
- 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.
|
|
2598
|
+
- 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
2599
|
- Never modify credentials, environment files, lockfiles, generated output, Git metadata, or dependencies.
|
|
2556
2600
|
- Do not claim a check passed unless run_check returned a passed status.
|
|
2557
2601
|
- Finish with a concise factual summary after all needed tool calls. Do not include secrets or absolute paths.`;
|
|
@@ -2568,6 +2612,12 @@ function sliceText(value, maximum) {
|
|
|
2568
2612
|
function createBoundedTarget(target, maximumCharacters) {
|
|
2569
2613
|
const detailBudget = Math.max(192, maximumCharacters - 420);
|
|
2570
2614
|
const bounded = {
|
|
2615
|
+
...target.page === void 0 ? {} : {
|
|
2616
|
+
page: Object.freeze({
|
|
2617
|
+
...target.page,
|
|
2618
|
+
url: (0, import_shared19.sanitizeUrl)(target.page.url, "http://spotpatch.invalid")
|
|
2619
|
+
})
|
|
2620
|
+
},
|
|
2571
2621
|
source: target.source,
|
|
2572
2622
|
react: Object.freeze({
|
|
2573
2623
|
supported: target.react.supported,
|
|
@@ -2711,7 +2761,7 @@ function isRetryableToolFailure(result) {
|
|
|
2711
2761
|
return false;
|
|
2712
2762
|
}
|
|
2713
2763
|
const candidate = output;
|
|
2714
|
-
return candidate.errorCode === import_shared20.ERROR_CODES.PATCH_REJECTED
|
|
2764
|
+
return candidate.retryable === true && (candidate.errorCode === import_shared20.ERROR_CODES.PATCH_REJECTED || candidate.errorCode === import_shared20.ERROR_CODES.TOOL_ARGUMENTS_INVALID);
|
|
2715
2765
|
}
|
|
2716
2766
|
function throwIfCancelled(signal) {
|
|
2717
2767
|
if (signal.aborted) {
|
|
@@ -2787,8 +2837,10 @@ async function executeAgentChange(options) {
|
|
|
2787
2837
|
let summary;
|
|
2788
2838
|
let toolCallCount = 0;
|
|
2789
2839
|
for (let turn = 0; turn < options.execution.limits.maxTurns; turn += 1) {
|
|
2840
|
+
const turnNumber = turn + 1;
|
|
2790
2841
|
throwIfCancelled(controller.signal);
|
|
2791
2842
|
const response = await session.next(pendingResults, controller.signal);
|
|
2843
|
+
assertUniqueToolCallIds(response.toolCalls);
|
|
2792
2844
|
if (response.toolCalls.length === 0) {
|
|
2793
2845
|
summary = response.finalText.trim().slice(0, options.execution.limits.maxToolOutputCharacters);
|
|
2794
2846
|
break;
|
|
@@ -2801,16 +2853,22 @@ async function executeAgentChange(options) {
|
|
|
2801
2853
|
for (const call of response.toolCalls) {
|
|
2802
2854
|
options.callbacks?.onTool?.(
|
|
2803
2855
|
Object.freeze({
|
|
2856
|
+
turn: turnNumber,
|
|
2804
2857
|
toolCallId: call.id,
|
|
2805
2858
|
toolName: call.name,
|
|
2806
2859
|
state: "started"
|
|
2807
2860
|
})
|
|
2808
2861
|
);
|
|
2809
2862
|
try {
|
|
2810
|
-
const result2 = await executor.execute(
|
|
2863
|
+
const result2 = await executor.execute(
|
|
2864
|
+
call,
|
|
2865
|
+
Object.freeze({ turn: turnNumber }),
|
|
2866
|
+
controller.signal
|
|
2867
|
+
);
|
|
2811
2868
|
results.push(result2);
|
|
2812
2869
|
options.callbacks?.onTool?.(
|
|
2813
2870
|
Object.freeze({
|
|
2871
|
+
turn: turnNumber,
|
|
2814
2872
|
toolCallId: call.id,
|
|
2815
2873
|
toolName: call.name,
|
|
2816
2874
|
state: isRetryableToolFailure(result2) ? "failed" : "succeeded"
|
|
@@ -2819,6 +2877,7 @@ async function executeAgentChange(options) {
|
|
|
2819
2877
|
} catch (error) {
|
|
2820
2878
|
options.callbacks?.onTool?.(
|
|
2821
2879
|
Object.freeze({
|
|
2880
|
+
turn: turnNumber,
|
|
2822
2881
|
toolCallId: call.id,
|
|
2823
2882
|
toolName: call.name,
|
|
2824
2883
|
state: "failed"
|