@mikode13/harness 9.3.0 → 10.0.0
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 +66 -8
- package/dist/agent/domain/agent.d.ts +11 -5
- package/dist/agent/domain/agent.js.map +1 -1
- package/dist/agent/domain/approval.d.ts +2 -1
- package/dist/agent/domain/approval.js +2 -1
- package/dist/agent/domain/approval.js.map +1 -1
- package/dist/agent/domain/question.d.ts +5 -0
- package/dist/agent/domain/question.js +11 -0
- package/dist/agent/domain/question.js.map +1 -1
- package/dist/agent/domain/runContext.d.ts +17 -0
- package/dist/agent/domain/runContext.js +9 -0
- package/dist/agent/domain/runContext.js.map +1 -1
- package/dist/agent/domain/transcript.d.ts +10 -0
- package/dist/agent/domain/transcript.js.map +1 -1
- package/dist/engines/domain/model/askUser.d.ts +0 -5
- package/dist/engines/domain/model/askUser.js +0 -9
- package/dist/engines/domain/model/askUser.js.map +1 -1
- package/dist/engines/domain/model/historyFollower.d.ts +1 -1
- package/dist/engines/domain/model/historyFollower.js +14 -3
- package/dist/engines/domain/model/historyFollower.js.map +1 -1
- package/dist/engines/domain/model/llmAgent.d.ts +19 -3
- package/dist/engines/domain/model/llmAgent.js +141 -51
- package/dist/engines/domain/model/llmAgent.js.map +1 -1
- package/dist/factory/infrastructure/agentLLMFactory.d.ts +6 -2
- package/dist/factory/infrastructure/agentLLMFactory.js +4 -4
- package/dist/factory/infrastructure/agentLLMFactory.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/recovery/domain/history.d.ts +3 -2
- package/dist/recovery/domain/history.js +6 -2
- package/dist/recovery/domain/history.js.map +1 -1
- package/dist/recovery/domain/recoveryStore.d.ts +47 -5
- package/dist/recovery/domain/recoveryStore.js.map +1 -1
- package/dist/recovery/domain/runChanges.js +11 -2
- package/dist/recovery/domain/runChanges.js.map +1 -1
- package/dist/recovery/infrastructure/fileRecoveryStore.d.ts +41 -9
- package/dist/recovery/infrastructure/fileRecoveryStore.js +217 -65
- package/dist/recovery/infrastructure/fileRecoveryStore.js.map +1 -1
- package/dist/tools/domain/preparedCall.d.ts +14 -0
- package/dist/tools/domain/preparedCall.js +12 -0
- package/dist/tools/domain/preparedCall.js.map +1 -1
- package/dist/tools/domain/readRegistry.d.ts +7 -0
- package/dist/tools/domain/readRegistry.js.map +1 -1
- package/dist/tools/infrastructure/agentAsTool.d.ts +61 -0
- package/dist/tools/infrastructure/agentAsTool.js +144 -0
- package/dist/tools/infrastructure/agentAsTool.js.map +1 -0
- package/dist/tools/infrastructure/commandCheckpoint.d.ts +3 -2
- package/dist/tools/infrastructure/commandCheckpoint.js +21 -9
- package/dist/tools/infrastructure/commandCheckpoint.js.map +1 -1
- package/dist/tools/infrastructure/commandTool.js +7 -3
- package/dist/tools/infrastructure/commandTool.js.map +1 -1
- package/dist/tools/infrastructure/exploreTool.js +9 -22
- package/dist/tools/infrastructure/exploreTool.js.map +1 -1
- package/dist/tools/infrastructure/fileEditor.d.ts +2 -0
- package/dist/tools/infrastructure/fileEditor.js +5 -1
- package/dist/tools/infrastructure/fileEditor.js.map +1 -1
- package/dist/tools/infrastructure/fileFormatter.d.ts +1 -0
- package/dist/tools/infrastructure/fileFormatter.js +5 -1
- package/dist/tools/infrastructure/fileFormatter.js.map +1 -1
- package/dist/tools/infrastructure/fileTools.d.ts +1 -1
- package/dist/tools/infrastructure/fileTools.js +3 -3
- package/dist/tools/infrastructure/fileTools.js.map +1 -1
- package/dist/tools/infrastructure/subagents.d.ts +19 -0
- package/dist/tools/infrastructure/subagents.js +29 -0
- package/dist/tools/infrastructure/subagents.js.map +1 -0
- package/dist/tools/infrastructure/workspaceWrites.d.ts +8 -5
- package/dist/tools/infrastructure/workspaceWrites.js +19 -7
- package/dist/tools/infrastructure/workspaceWrites.js.map +1 -1
- package/dist/tools/infrastructure/writeSession.d.ts +8 -0
- package/dist/tools/infrastructure/writeSession.js +37 -0
- package/dist/tools/infrastructure/writeSession.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Agent } from '#src/agent/domain/agent';
|
|
2
|
+
import { type ToolRisk } from '#src/agent/domain/approval';
|
|
3
|
+
import type { ToolDefinition } from '#src/llm/domain/tool';
|
|
4
|
+
declare const delegationToolBrand: unique symbol;
|
|
5
|
+
/**
|
|
6
|
+
* A tool from `agentAsTool`, which hands a task to another agent. It goes to an agent from
|
|
7
|
+
* `createLLMAgent` beside its other tools; what it does inside is the harness's.
|
|
8
|
+
*/
|
|
9
|
+
export type DelegationTool = ToolDefinition & {
|
|
10
|
+
readonly [delegationToolBrand]: true;
|
|
11
|
+
};
|
|
12
|
+
/** How deep agents may delegate unless a tool says otherwise: planner → coder → committer. */
|
|
13
|
+
export declare const defaultMaxDepth = 2;
|
|
14
|
+
export interface AgentAsToolOptions {
|
|
15
|
+
/** The name the model calls it by, such as `commit`. */
|
|
16
|
+
name: string;
|
|
17
|
+
/** What the agent does and when to hand it a task, for the model that may call it. */
|
|
18
|
+
description: string;
|
|
19
|
+
/**
|
|
20
|
+
* A new agent for each call, such as one from `createLLMAgent`: it starts from the task alone
|
|
21
|
+
* and never shares a conversation with another call.
|
|
22
|
+
*/
|
|
23
|
+
createAgent: () => Agent;
|
|
24
|
+
/**
|
|
25
|
+
* How the call itself is judged before it runs. `mutating` unless given: handing over a task
|
|
26
|
+
* asks no one, and the agent's own `destructive` calls are approved through the run's
|
|
27
|
+
* `approve`, as the calling agent's would be. `destructive` asks before every task is handed
|
|
28
|
+
* over.
|
|
29
|
+
*/
|
|
30
|
+
risk?: ToolRisk;
|
|
31
|
+
/**
|
|
32
|
+
* How many delegations deep the agent may run, 1 for one the top-level agent calls. A call
|
|
33
|
+
* that would go deeper is refused, and the model is told why. Defaults to 2.
|
|
34
|
+
*/
|
|
35
|
+
maxDepth?: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Offers an agent to another agent as a tool. The calling model writes the task; the agent runs
|
|
39
|
+
* it as part of the caller's run, and its answer is the call's result:
|
|
40
|
+
*
|
|
41
|
+
* - its changes go into the same run of the workspace's history, so one undo reverts both, and
|
|
42
|
+
* each agent keeps its own tools' access policy and its own reads;
|
|
43
|
+
* - what it spends counts in the caller's `tokens`, a failed attempt included;
|
|
44
|
+
* - its progress arrives as `subagent` events of the call, and a transcript it gives names the
|
|
45
|
+
* call in `delegation`;
|
|
46
|
+
* - it is asked and approved through the run's `ask` and `approve`. Delegating never grants
|
|
47
|
+
* `autoApprove`: the agent has it only if the `createAgent` option gave it.
|
|
48
|
+
*
|
|
49
|
+
* Calls of these tools next to each other in one step run at once, and any other call runs
|
|
50
|
+
* alone. Every change and command of the run is made one at a time, so the history stays exact,
|
|
51
|
+
* but agents working at once can see each other's unfinished work. An agent that fails is the
|
|
52
|
+
* call's error result, for the model to act on; a cancellation, or a run's approver or asker that
|
|
53
|
+
* failed inside it, ends the run.
|
|
54
|
+
*
|
|
55
|
+
* Only for agents on the model APIs, from `createLLMAgent`: an Agent SDK agent from the
|
|
56
|
+
* `createAgent` factory runs as its own run, outside these guarantees.
|
|
57
|
+
*
|
|
58
|
+
* @throws {InvalidAgentConfigError} when `maxDepth` is not a positive integer.
|
|
59
|
+
*/
|
|
60
|
+
export declare function agentAsTool({ name, description, createAgent, risk, maxDepth, }: AgentAsToolOptions): DelegationTool;
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { readDecision } from '#src/agent/domain/approval';
|
|
3
|
+
import { readAnswers } from '#src/agent/domain/question';
|
|
4
|
+
import { InvalidAgentConfigError, RecoverableError, UnrecoverableError, withSpentTokens, } from '#src/shared/domain/errors';
|
|
5
|
+
import { isAbortError } from '#src/shared/domain/isAbortError';
|
|
6
|
+
import { classifyHostFailure } from '#src/shared/domain/providerFailure';
|
|
7
|
+
import { RunEndingToolError } from "../domain/preparedCall.js";
|
|
8
|
+
import { definePreparingTool } from "./defineTool.js";
|
|
9
|
+
import { cutAnswer, failureOf, subagentOptions } from "./subagents.js";
|
|
10
|
+
/** How deep agents may delegate unless a tool says otherwise: planner → coder → committer. */
|
|
11
|
+
export const defaultMaxDepth = 2;
|
|
12
|
+
/**
|
|
13
|
+
* Offers an agent to another agent as a tool. The calling model writes the task; the agent runs
|
|
14
|
+
* it as part of the caller's run, and its answer is the call's result:
|
|
15
|
+
*
|
|
16
|
+
* - its changes go into the same run of the workspace's history, so one undo reverts both, and
|
|
17
|
+
* each agent keeps its own tools' access policy and its own reads;
|
|
18
|
+
* - what it spends counts in the caller's `tokens`, a failed attempt included;
|
|
19
|
+
* - its progress arrives as `subagent` events of the call, and a transcript it gives names the
|
|
20
|
+
* call in `delegation`;
|
|
21
|
+
* - it is asked and approved through the run's `ask` and `approve`. Delegating never grants
|
|
22
|
+
* `autoApprove`: the agent has it only if the `createAgent` option gave it.
|
|
23
|
+
*
|
|
24
|
+
* Calls of these tools next to each other in one step run at once, and any other call runs
|
|
25
|
+
* alone. Every change and command of the run is made one at a time, so the history stays exact,
|
|
26
|
+
* but agents working at once can see each other's unfinished work. An agent that fails is the
|
|
27
|
+
* call's error result, for the model to act on; a cancellation, or a run's approver or asker that
|
|
28
|
+
* failed inside it, ends the run.
|
|
29
|
+
*
|
|
30
|
+
* Only for agents on the model APIs, from `createLLMAgent`: an Agent SDK agent from the
|
|
31
|
+
* `createAgent` factory runs as its own run, outside these guarantees.
|
|
32
|
+
*
|
|
33
|
+
* @throws {InvalidAgentConfigError} when `maxDepth` is not a positive integer.
|
|
34
|
+
*/
|
|
35
|
+
export function agentAsTool({ name, description, createAgent, risk = 'mutating', maxDepth = defaultMaxDepth, }) {
|
|
36
|
+
if (!Number.isInteger(maxDepth) || maxDepth < 1) {
|
|
37
|
+
throw new InvalidAgentConfigError(`maxDepth of "${name}" must be a positive integer; got ${String(maxDepth)}`);
|
|
38
|
+
}
|
|
39
|
+
const tool = {
|
|
40
|
+
...definePreparingTool({
|
|
41
|
+
name,
|
|
42
|
+
description,
|
|
43
|
+
input: z.object({
|
|
44
|
+
task: z
|
|
45
|
+
.string()
|
|
46
|
+
.describe('The whole task. The agent starts from it alone, so say what to do and what to report back.'),
|
|
47
|
+
}),
|
|
48
|
+
summarize: ({ task }) => task,
|
|
49
|
+
prepare: ({ task }, _, call) => {
|
|
50
|
+
const asked = task.trim();
|
|
51
|
+
if (asked === '')
|
|
52
|
+
return Promise.reject(new Error('The task needs some text.'));
|
|
53
|
+
if (call.depth >= maxDepth) {
|
|
54
|
+
return Promise.reject(new Error(`"${name}" may run at most ${String(maxDepth)} delegations deep, and this agent already runs ${String(call.depth)} deep. Do the work yourself, or answer with what you have.`));
|
|
55
|
+
}
|
|
56
|
+
return Promise.resolve({
|
|
57
|
+
risk,
|
|
58
|
+
// What the agent changes is in the history, but it may run tools of the consumer's
|
|
59
|
+
// that change what the history never sees.
|
|
60
|
+
unrecorded: true,
|
|
61
|
+
run: signal => delegate(asked, signal, call),
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
}),
|
|
65
|
+
parallel: true,
|
|
66
|
+
};
|
|
67
|
+
async function delegate(task, signal, call) {
|
|
68
|
+
// The run's approver and asker are host code: one that fails inside the agent ends the
|
|
69
|
+
// run, not just the agent's task.
|
|
70
|
+
const failed = { host: false };
|
|
71
|
+
const options = subagentOptions(call, {
|
|
72
|
+
tool: name,
|
|
73
|
+
name,
|
|
74
|
+
subagent: 1,
|
|
75
|
+
task,
|
|
76
|
+
options: {
|
|
77
|
+
signal,
|
|
78
|
+
...(call.approve ? { approve: watchedApprover(call.approve, failed) } : {}),
|
|
79
|
+
...(call.ask ? { ask: watchedAsker(call.ask, failed) } : {}),
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
let agent;
|
|
83
|
+
try {
|
|
84
|
+
agent = createAgent();
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
throw new RunEndingToolError(classifyHostFailure(error, `The agent for "${name}" could not be created`));
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const { response, tokens } = await agent.run(task, options);
|
|
91
|
+
call.spend(tokens, tokens === undefined);
|
|
92
|
+
return cutAnswer(response || 'It finished without an answer.');
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (isAbortError(error))
|
|
96
|
+
throw error;
|
|
97
|
+
const classified = error instanceof RecoverableError || error instanceof UnrecoverableError;
|
|
98
|
+
// Its failure carries what it spent, which the run's failure keeps.
|
|
99
|
+
if (classified && failed.host)
|
|
100
|
+
throw new RunEndingToolError(error);
|
|
101
|
+
if (!classified) {
|
|
102
|
+
// It broke the contract of `Agent`, so what it spent is unknown, not nothing.
|
|
103
|
+
throw new RunEndingToolError(withSpentTokens(classifyHostFailure(error, `The agent for "${name}" failed`), undefined, true));
|
|
104
|
+
}
|
|
105
|
+
call.spend(error.tokens, error.usageUnreported);
|
|
106
|
+
throw new Error(`The agent failed: ${failureOf(error)}`, { cause: error });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// A file tool's kind of brand: the harness built it, and only `createLLMAgent` takes it.
|
|
110
|
+
return tool;
|
|
111
|
+
}
|
|
112
|
+
/** `approve`, noting in `failed` when it throws or answers with no decision. */
|
|
113
|
+
function watchedApprover(approve, failed) {
|
|
114
|
+
return async (request, signal) => {
|
|
115
|
+
try {
|
|
116
|
+
const answer = await approve(request, signal);
|
|
117
|
+
readDecision(answer);
|
|
118
|
+
return answer;
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
// One that gives up on a cancellation did not fail.
|
|
122
|
+
if (!signal.aborted)
|
|
123
|
+
failed.host = true;
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/** `ask`, noting in `failed` when it throws or answers in a shape no agent accepts. */
|
|
129
|
+
function watchedAsker(ask, failed) {
|
|
130
|
+
return async (questions, signal) => {
|
|
131
|
+
try {
|
|
132
|
+
const answers = await ask(questions, signal);
|
|
133
|
+
if (!readAnswers(answers, questions))
|
|
134
|
+
failed.host = true;
|
|
135
|
+
return answers;
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (!signal.aborted)
|
|
139
|
+
failed.host = true;
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=agentAsTool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agentAsTool.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/agentAsTool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAiB,YAAY,EAAiB,MAAM,4BAA4B,CAAC;AACxF,OAAO,EAAc,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACrE,OAAO,EACN,uBAAuB,EACvB,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,GACf,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAC/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,oCAAoC,CAAC;AAEzE,OAAO,EAAsB,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAEnF,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAUvE,8FAA8F;AAC9F,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC;AA0BjC;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,WAAW,CAAC,EAC3B,IAAI,EACJ,WAAW,EACX,WAAW,EACX,IAAI,GAAG,UAAU,EACjB,QAAQ,GAAG,eAAe,GACN;IACpB,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;QACjD,MAAM,IAAI,uBAAuB,CAChC,gBAAgB,IAAI,qCAAqC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAC3E,CAAC;IACH,CAAC;IACD,MAAM,IAAI,GAAkB;QAC3B,GAAG,mBAAmB,CAAC;YACtB,IAAI;YACJ,WAAW;YACX,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC;gBACf,IAAI,EAAE,CAAC;qBACL,MAAM,EAAE;qBACR,QAAQ,CACR,4FAA4F,CAC5F;aACF,CAAC;YACF,SAAS,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,IAAI;YAC7B,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;gBAC9B,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;gBAC1B,IAAI,KAAK,KAAK,EAAE;oBAAE,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,2BAA2B,CAAC,CAAC,CAAC;gBAChF,IAAI,IAAI,CAAC,KAAK,IAAI,QAAQ,EAAE,CAAC;oBAC5B,OAAO,OAAO,CAAC,MAAM,CACpB,IAAI,KAAK,CACR,IAAI,IAAI,qBAAqB,MAAM,CAAC,QAAQ,CAAC,kDAAkD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,4DAA4D,CAC7K,CACD,CAAC;gBACH,CAAC;gBACD,OAAO,OAAO,CAAC,OAAO,CAAC;oBACtB,IAAI;oBACJ,mFAAmF;oBACnF,2CAA2C;oBAC3C,UAAU,EAAE,IAAI;oBAChB,GAAG,EAAE,MAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC;iBAC5C,CAAC,CAAC;YACJ,CAAC;SACD,CAAC;QACF,QAAQ,EAAE,IAAI;KACd,CAAC;IAEF,KAAK,UAAU,QAAQ,CAAC,IAAY,EAAE,MAAmB,EAAE,IAAqB;QAC/E,uFAAuF;QACvF,kCAAkC;QAClC,MAAM,MAAM,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;QAC/B,MAAM,OAAO,GAAG,eAAe,CAAC,IAAI,EAAE;YACrC,IAAI,EAAE,IAAI;YACV,IAAI;YACJ,QAAQ,EAAE,CAAC;YACX,IAAI;YACJ,OAAO,EAAE;gBACR,MAAM;gBACN,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,eAAe,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC3E,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,YAAY,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACvC;SACtB,CAAC,CAAC;QAEH,IAAI,KAAY,CAAC;QACjB,IAAI,CAAC;YACJ,KAAK,GAAG,WAAW,EAAE,CAAC;QACvB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,MAAM,IAAI,kBAAkB,CAC3B,mBAAmB,CAAC,KAAK,EAAE,kBAAkB,IAAI,wBAAwB,CAAC,CAC1E,CAAC;QACH,CAAC;QAED,IAAI,CAAC;YACJ,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;YAC5D,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,KAAK,SAAS,CAAC,CAAC;YACzC,OAAO,SAAS,CAAC,QAAQ,IAAI,gCAAgC,CAAC,CAAC;QAChE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,IAAI,YAAY,CAAC,KAAK,CAAC;gBAAE,MAAM,KAAK,CAAC;YACrC,MAAM,UAAU,GAAG,KAAK,YAAY,gBAAgB,IAAI,KAAK,YAAY,kBAAkB,CAAC;YAC5F,oEAAoE;YACpE,IAAI,UAAU,IAAI,MAAM,CAAC,IAAI;gBAAE,MAAM,IAAI,kBAAkB,CAAC,KAAK,CAAC,CAAC;YACnE,IAAI,CAAC,UAAU,EAAE,CAAC;gBACjB,8EAA8E;gBAC9E,MAAM,IAAI,kBAAkB,CAC3B,eAAe,CACd,mBAAmB,CAAC,KAAK,EAAE,kBAAkB,IAAI,UAAU,CAAC,EAC5D,SAAS,EACT,IAAI,CACJ,CACD,CAAC;YACH,CAAC;YACD,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,eAAe,CAAC,CAAC;YAChD,MAAM,IAAI,KAAK,CAAC,qBAAqB,SAAS,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QAC5E,CAAC;IACF,CAAC;IAED,yFAAyF;IACzF,OAAO,IAAiC,CAAC;AAC1C,CAAC;AAED,gFAAgF;AAChF,SAAS,eAAe,CAAC,OAAiB,EAAE,MAAyB;IACpE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE;QAChC,IAAI,CAAC;YACJ,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YAC9C,YAAY,CAAC,MAAM,CAAC,CAAC;YACrB,OAAO,MAAM,CAAC;QACf,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,oDAAoD;YACpD,IAAI,CAAC,MAAM,CAAC,OAAO;gBAAE,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC;YACxC,MAAM,KAAK,CAAC;QACb,CAAC;IACF,CAAC,CAAC;AACH,CAAC;AAED,uFAAuF;AACvF,SAAS,YAAY,CAAC,GAAU,EAAE,MAAyB;IAC1D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE;QAClC,IAAI,CAAC;YACJ,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;YAC7C,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,SAAS,CAAC;gBAAE,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC;YACzD,OAAO,OAAO,CAAC;QAChB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,IAAI,CAAC,MAAM,CAAC,OAAO;gBAAE,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC;YACxC,MAAM,KAAK,CAAC;QACb,CAAC;IACF,CAAC,CAAC;AACH,CAAC","sourcesContent":["import { z } from 'zod';\nimport type { Agent, RunOptions } from '#src/agent/domain/agent';\nimport { type Approver, readDecision, type ToolRisk } from '#src/agent/domain/approval';\nimport { type Asker, readAnswers } from '#src/agent/domain/question';\nimport {\n\tInvalidAgentConfigError,\n\tRecoverableError,\n\tUnrecoverableError,\n\twithSpentTokens,\n} from '#src/shared/domain/errors';\nimport { isAbortError } from '#src/shared/domain/isAbortError';\nimport { classifyHostFailure } from '#src/shared/domain/providerFailure';\nimport type { ToolDefinition } from '#src/llm/domain/tool';\nimport { type PreparingTool, RunEndingToolError } from '../domain/preparedCall.ts';\nimport type { ToolCallContext } from '../domain/readRegistry.ts';\nimport { definePreparingTool } from './defineTool.ts';\nimport { cutAnswer, failureOf, subagentOptions } from './subagents.ts';\n\ndeclare const delegationToolBrand: unique symbol;\n\n/**\n * A tool from `agentAsTool`, which hands a task to another agent. It goes to an agent from\n * `createLLMAgent` beside its other tools; what it does inside is the harness's.\n */\nexport type DelegationTool = ToolDefinition & { readonly [delegationToolBrand]: true };\n\n/** How deep agents may delegate unless a tool says otherwise: planner → coder → committer. */\nexport const defaultMaxDepth = 2;\n\nexport interface AgentAsToolOptions {\n\t/** The name the model calls it by, such as `commit`. */\n\tname: string;\n\t/** What the agent does and when to hand it a task, for the model that may call it. */\n\tdescription: string;\n\t/**\n\t * A new agent for each call, such as one from `createLLMAgent`: it starts from the task alone\n\t * and never shares a conversation with another call.\n\t */\n\tcreateAgent: () => Agent;\n\t/**\n\t * How the call itself is judged before it runs. `mutating` unless given: handing over a task\n\t * asks no one, and the agent's own `destructive` calls are approved through the run's\n\t * `approve`, as the calling agent's would be. `destructive` asks before every task is handed\n\t * over.\n\t */\n\trisk?: ToolRisk;\n\t/**\n\t * How many delegations deep the agent may run, 1 for one the top-level agent calls. A call\n\t * that would go deeper is refused, and the model is told why. Defaults to 2.\n\t */\n\tmaxDepth?: number;\n}\n\n/**\n * Offers an agent to another agent as a tool. The calling model writes the task; the agent runs\n * it as part of the caller's run, and its answer is the call's result:\n *\n * - its changes go into the same run of the workspace's history, so one undo reverts both, and\n * each agent keeps its own tools' access policy and its own reads;\n * - what it spends counts in the caller's `tokens`, a failed attempt included;\n * - its progress arrives as `subagent` events of the call, and a transcript it gives names the\n * call in `delegation`;\n * - it is asked and approved through the run's `ask` and `approve`. Delegating never grants\n * `autoApprove`: the agent has it only if the `createAgent` option gave it.\n *\n * Calls of these tools next to each other in one step run at once, and any other call runs\n * alone. Every change and command of the run is made one at a time, so the history stays exact,\n * but agents working at once can see each other's unfinished work. An agent that fails is the\n * call's error result, for the model to act on; a cancellation, or a run's approver or asker that\n * failed inside it, ends the run.\n *\n * Only for agents on the model APIs, from `createLLMAgent`: an Agent SDK agent from the\n * `createAgent` factory runs as its own run, outside these guarantees.\n *\n * @throws {InvalidAgentConfigError} when `maxDepth` is not a positive integer.\n */\nexport function agentAsTool({\n\tname,\n\tdescription,\n\tcreateAgent,\n\trisk = 'mutating',\n\tmaxDepth = defaultMaxDepth,\n}: AgentAsToolOptions): DelegationTool {\n\tif (!Number.isInteger(maxDepth) || maxDepth < 1) {\n\t\tthrow new InvalidAgentConfigError(\n\t\t\t`maxDepth of \"${name}\" must be a positive integer; got ${String(maxDepth)}`,\n\t\t);\n\t}\n\tconst tool: PreparingTool = {\n\t\t...definePreparingTool({\n\t\t\tname,\n\t\t\tdescription,\n\t\t\tinput: z.object({\n\t\t\t\ttask: z\n\t\t\t\t\t.string()\n\t\t\t\t\t.describe(\n\t\t\t\t\t\t'The whole task. The agent starts from it alone, so say what to do and what to report back.',\n\t\t\t\t\t),\n\t\t\t}),\n\t\t\tsummarize: ({ task }) => task,\n\t\t\tprepare: ({ task }, _, call) => {\n\t\t\t\tconst asked = task.trim();\n\t\t\t\tif (asked === '') return Promise.reject(new Error('The task needs some text.'));\n\t\t\t\tif (call.depth >= maxDepth) {\n\t\t\t\t\treturn Promise.reject(\n\t\t\t\t\t\tnew Error(\n\t\t\t\t\t\t\t`\"${name}\" may run at most ${String(maxDepth)} delegations deep, and this agent already runs ${String(call.depth)} deep. Do the work yourself, or answer with what you have.`,\n\t\t\t\t\t\t),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\treturn Promise.resolve({\n\t\t\t\t\trisk,\n\t\t\t\t\t// What the agent changes is in the history, but it may run tools of the consumer's\n\t\t\t\t\t// that change what the history never sees.\n\t\t\t\t\tunrecorded: true,\n\t\t\t\t\trun: signal => delegate(asked, signal, call),\n\t\t\t\t});\n\t\t\t},\n\t\t}),\n\t\tparallel: true,\n\t};\n\n\tasync function delegate(task: string, signal: AbortSignal, call: ToolCallContext) {\n\t\t// The run's approver and asker are host code: one that fails inside the agent ends the\n\t\t// run, not just the agent's task.\n\t\tconst failed = { host: false };\n\t\tconst options = subagentOptions(call, {\n\t\t\ttool: name,\n\t\t\tname,\n\t\t\tsubagent: 1,\n\t\t\ttask,\n\t\t\toptions: {\n\t\t\t\tsignal,\n\t\t\t\t...(call.approve ? { approve: watchedApprover(call.approve, failed) } : {}),\n\t\t\t\t...(call.ask ? { ask: watchedAsker(call.ask, failed) } : {}),\n\t\t\t} satisfies RunOptions,\n\t\t});\n\n\t\tlet agent: Agent;\n\t\ttry {\n\t\t\tagent = createAgent();\n\t\t} catch (error) {\n\t\t\tthrow new RunEndingToolError(\n\t\t\t\tclassifyHostFailure(error, `The agent for \"${name}\" could not be created`),\n\t\t\t);\n\t\t}\n\n\t\ttry {\n\t\t\tconst { response, tokens } = await agent.run(task, options);\n\t\t\tcall.spend(tokens, tokens === undefined);\n\t\t\treturn cutAnswer(response || 'It finished without an answer.');\n\t\t} catch (error) {\n\t\t\tif (isAbortError(error)) throw error;\n\t\t\tconst classified = error instanceof RecoverableError || error instanceof UnrecoverableError;\n\t\t\t// Its failure carries what it spent, which the run's failure keeps.\n\t\t\tif (classified && failed.host) throw new RunEndingToolError(error);\n\t\t\tif (!classified) {\n\t\t\t\t// It broke the contract of `Agent`, so what it spent is unknown, not nothing.\n\t\t\t\tthrow new RunEndingToolError(\n\t\t\t\t\twithSpentTokens(\n\t\t\t\t\t\tclassifyHostFailure(error, `The agent for \"${name}\" failed`),\n\t\t\t\t\t\tundefined,\n\t\t\t\t\t\ttrue,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t\tcall.spend(error.tokens, error.usageUnreported);\n\t\t\tthrow new Error(`The agent failed: ${failureOf(error)}`, { cause: error });\n\t\t}\n\t}\n\n\t// A file tool's kind of brand: the harness built it, and only `createLLMAgent` takes it.\n\treturn tool as unknown as DelegationTool;\n}\n\n/** `approve`, noting in `failed` when it throws or answers with no decision. */\nfunction watchedApprover(approve: Approver, failed: { host: boolean }): Approver {\n\treturn async (request, signal) => {\n\t\ttry {\n\t\t\tconst answer = await approve(request, signal);\n\t\t\treadDecision(answer);\n\t\t\treturn answer;\n\t\t} catch (error) {\n\t\t\t// One that gives up on a cancellation did not fail.\n\t\t\tif (!signal.aborted) failed.host = true;\n\t\t\tthrow error;\n\t\t}\n\t};\n}\n\n/** `ask`, noting in `failed` when it throws or answers in a shape no agent accepts. */\nfunction watchedAsker(ask: Asker, failed: { host: boolean }): Asker {\n\treturn async (questions, signal) => {\n\t\ttry {\n\t\t\tconst answers = await ask(questions, signal);\n\t\t\tif (!readAnswers(answers, questions)) failed.host = true;\n\t\t\treturn answers;\n\t\t} catch (error) {\n\t\t\tif (!signal.aborted) failed.host = true;\n\t\t\tthrow error;\n\t\t}\n\t};\n}\n"]}
|
|
@@ -43,8 +43,9 @@ export declare class CommandCheckpoint {
|
|
|
43
43
|
* cancelled: once the command has run, what it changed must reach the history. Unless
|
|
44
44
|
* `complete`, the observation stays open, because the command may still be changing files.
|
|
45
45
|
*
|
|
46
|
-
* @returns the changes, and the paths it could not record because they are now
|
|
47
|
-
* history cannot copy, such as a file the command replaced with a folder, or
|
|
46
|
+
* @returns the changes, and the paths it could not record because they are now, or were before,
|
|
47
|
+
* something the history cannot copy, such as a file the command replaced with a folder, or one
|
|
48
|
+
* too large that it replaced with a small one. The journal names them, so undo leaves them.
|
|
48
49
|
*/
|
|
49
50
|
end({ complete }: {
|
|
50
51
|
complete: boolean;
|
|
@@ -64,7 +64,10 @@ export class CommandCheckpoint {
|
|
|
64
64
|
if (state)
|
|
65
65
|
files[path] = state;
|
|
66
66
|
});
|
|
67
|
-
const manifest = {
|
|
67
|
+
const manifest = {
|
|
68
|
+
files,
|
|
69
|
+
...(uncopyable.size > 0 ? { uncopyable: [...uncopyable] } : {}),
|
|
70
|
+
};
|
|
68
71
|
const hash = await journal.saveContent(Buffer.from(JSON.stringify(manifest)));
|
|
69
72
|
signal.throwIfAborted();
|
|
70
73
|
const sequence = await journal.observe(hash, by);
|
|
@@ -83,26 +86,35 @@ export class CommandCheckpoint {
|
|
|
83
86
|
* cancelled: once the command has run, what it changed must reach the history. Unless
|
|
84
87
|
* `complete`, the observation stays open, because the command may still be changing files.
|
|
85
88
|
*
|
|
86
|
-
* @returns the changes, and the paths it could not record because they are now
|
|
87
|
-
* history cannot copy, such as a file the command replaced with a folder, or
|
|
89
|
+
* @returns the changes, and the paths it could not record because they are now, or were before,
|
|
90
|
+
* something the history cannot copy, such as a file the command replaced with a folder, or one
|
|
91
|
+
* too large that it replaced with a small one. The journal names them, so undo leaves them.
|
|
88
92
|
*/
|
|
89
93
|
async end({ complete }) {
|
|
90
94
|
const started = performance.now();
|
|
91
95
|
const signal = new AbortController().signal;
|
|
92
|
-
const paths = new Set([
|
|
96
|
+
const paths = new Set([
|
|
97
|
+
...Object.keys(this.manifest.files),
|
|
98
|
+
...this.uncopyable,
|
|
99
|
+
...(await this.covered(signal)),
|
|
100
|
+
]);
|
|
93
101
|
const changes = [];
|
|
94
102
|
const unrecorded = [];
|
|
95
103
|
let bytesRead = this.measure.bytesRead;
|
|
96
104
|
for (const path of paths) {
|
|
97
|
-
const before = this.manifest.files[path] ?? { exists: false };
|
|
98
105
|
const read = await readState(path);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
106
|
+
// One it could not copy before or after, such as a file over the size limit, is not
|
|
107
|
+
// covered at all. One it could copy only before, or only after, changed in a way the
|
|
108
|
+
// history cannot take back: what it held on the other side is unknown.
|
|
109
|
+
const copiedBefore = !this.uncopyable.has(path);
|
|
110
|
+
if (!read || !copiedBefore) {
|
|
111
|
+
if (read || copiedBefore) {
|
|
112
|
+
await this.journal.unrecorded(path, this.by);
|
|
103
113
|
unrecorded.push(path);
|
|
114
|
+
}
|
|
104
115
|
continue;
|
|
105
116
|
}
|
|
117
|
+
const before = this.manifest.files[path] ?? { exists: false };
|
|
106
118
|
let after = { exists: false };
|
|
107
119
|
if (read.content) {
|
|
108
120
|
bytesRead += read.content.length;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commandCheckpoint.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/commandCheckpoint.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from 'node:crypto';\nimport { lstat, readFile } from 'node:fs/promises';\nimport { sameState } from '#src/recovery/domain/fileChanges';\nimport type { FileState, Manifest, RunJournal } from '#src/recovery/domain/recoveryStore';\n\n// The same bound the history uses for a file it records: a larger one is not copied.\nconst maxCoveredBytes = 64 * 1024 * 1024;\nconst captureConcurrency = 16;\n\n/**\n * Every file a command's checkpoint covers now, by its real path, absolute: what `.gitignore`\n * does not exclude in the `write` roots, less what the access policy closes to writing by name.\n */\nexport type CoveredFiles = (signal: AbortSignal) => Promise<string[]>;\n\n/** A file a command changed, as the checkpoint found it. */\nexport interface ObservedChange {\n\tpath: string;\n\tbefore: FileState;\n\tafter: FileState;\n}\n\nfunction code(error: unknown): string | undefined {\n\treturn (error as NodeJS.ErrnoException).code;\n}\n\n/**\n * The files a command may change, captured right before it runs and compared right after, so\n * what it changed is in the run's history like any tool's change, and can be undone.\n *\n * `begin` stores the content of every covered file and a manifest of them, then records the\n * observation in the journal; if any of that fails, the command must not run. `end` lists the\n * covered files again, and records each one that differs from the manifest as a change made by\n * the command. This first version reads and hashes everything both times, trusting neither size\n * nor modification time, and keeps what that cost with the observation.\n */\nexport class CommandCheckpoint {\n\tprivate readonly journal: RunJournal;\n\tprivate readonly covered: CoveredFiles;\n\tprivate readonly by: string;\n\tprivate readonly manifest: Manifest;\n\t/** Covered files it could not copy before, which it leaves out after too. */\n\tprivate readonly uncopyable: ReadonlySet<string>;\n\tprivate readonly sequence: number;\n\tprivate readonly measure: { bytesRead: number; durationMs: number; storedBefore: number };\n\n\tprivate constructor(fields: {\n\t\tjournal: RunJournal;\n\t\tcovered: CoveredFiles;\n\t\tby: string;\n\t\tmanifest: Manifest;\n\t\tuncopyable: ReadonlySet<string>;\n\t\tsequence: number;\n\t\tmeasure: { bytesRead: number; durationMs: number; storedBefore: number };\n\t}) {\n\t\tthis.journal = fields.journal;\n\t\tthis.covered = fields.covered;\n\t\tthis.by = fields.by;\n\t\tthis.manifest = fields.manifest;\n\t\tthis.uncopyable = fields.uncopyable;\n\t\tthis.sequence = fields.sequence;\n\t\tthis.measure = fields.measure;\n\t}\n\n\t/** Captures every covered file. Cancelling it, or any failure, leaves the command unstarted. */\n\tstatic async begin({\n\t\tjournal,\n\t\tcovered,\n\t\tby,\n\t\tsignal,\n\t}: {\n\t\tjournal: RunJournal;\n\t\tcovered: CoveredFiles;\n\t\t/** The command, as the history will name it. */\n\t\tby: string;\n\t\tsignal: AbortSignal;\n\t}): Promise<CommandCheckpoint> {\n\t\tconst started = performance.now();\n\t\tconst storedBefore = journal.storedBytes;\n\t\tconst paths = await covered(signal);\n\t\tconst states: (FileState | undefined)[] = [];\n\t\tconst uncopyable = new Set<string>();\n\t\tlet bytesRead = 0;\n\t\t// Several at once: each stored content waits for its own sync, which dominates the time.\n\t\tawait eachAtOnce(paths, captureConcurrency, async (path, index) => {\n\t\t\tsignal.throwIfAborted();\n\t\t\tconst read = await readState(path);\n\t\t\t// A folder, a link or a file too large: no entry records it, so it is not covered.\n\t\t\tif (!read) {\n\t\t\t\tuncopyable.add(path);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (read.content) {\n\t\t\t\tbytesRead += read.content.length;\n\t\t\t\tconst hash = await journal.saveContent(read.content);\n\t\t\t\tstates[index] = { exists: true, hash, mode: read.mode };\n\t\t\t}\n\t\t});\n\t\tconst files: Manifest['files'] = {};\n\t\tpaths.forEach((path, index) => {\n\t\t\tconst state = states[index];\n\t\t\tif (state) files[path] = state;\n\t\t});\n\t\tconst manifest: Manifest = { files };\n\t\tconst hash = await journal.saveContent(Buffer.from(JSON.stringify(manifest)));\n\t\tsignal.throwIfAborted();\n\t\tconst sequence = await journal.observe(hash, by);\n\t\treturn new CommandCheckpoint({\n\t\t\tjournal,\n\t\t\tcovered,\n\t\t\tby,\n\t\t\tmanifest,\n\t\t\tuncopyable,\n\t\t\tsequence,\n\t\t\tmeasure: { bytesRead, durationMs: performance.now() - started, storedBefore },\n\t\t});\n\t}\n\n\t/**\n\t * Compares the covered files with the capture and records every change found. It is never\n\t * cancelled: once the command has run, what it changed must reach the history. Unless\n\t * `complete`, the observation stays open, because the command may still be changing files.\n\t *\n\t * @returns the changes, and the paths it could not record because they are now something the\n\t * history cannot copy, such as a file the command replaced with a folder, or made too large.\n\t */\n\tasync end({ complete }: { complete: boolean }): Promise<{\n\t\tchanges: ObservedChange[];\n\t\tunrecorded: string[];\n\t}> {\n\t\tconst started = performance.now();\n\t\tconst signal = new AbortController().signal;\n\t\tconst paths = new Set([...Object.keys(this.manifest.files), ...(await this.covered(signal))]);\n\t\tconst changes: ObservedChange[] = [];\n\t\tconst unrecorded: string[] = [];\n\t\tlet bytesRead = this.measure.bytesRead;\n\n\t\tfor (const path of paths) {\n\t\t\tconst before: FileState = this.manifest.files[path] ?? { exists: false };\n\t\t\tconst read = await readState(path);\n\t\t\tif (!read) {\n\t\t\t\t// One it could not copy before either, such as a file over the size limit, is not\n\t\t\t\t// covered at all; one it could is now something no entry records.\n\t\t\t\tif (!this.uncopyable.has(path)) unrecorded.push(path);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tlet after: FileState = { exists: false };\n\t\t\tif (read.content) {\n\t\t\t\tbytesRead += read.content.length;\n\t\t\t\tconst hash = createHash('sha256').update(read.content).digest('hex');\n\t\t\t\tafter = { exists: true, hash, mode: read.mode };\n\t\t\t}\n\t\t\tif (sameState(before, after)) continue;\n\t\t\t// Stored only now that it changed, so an unchanged file costs no write.\n\t\t\tif (read.content) await this.journal.saveContent(read.content);\n\t\t\tconst sequence = await this.journal.prepare({ path, before, after, observedBy: this.by });\n\t\t\tawait this.journal.applied(sequence);\n\t\t\tchanges.push({ path, before, after });\n\t\t}\n\n\t\t// Left open when something the command started may still be running: the record then says\n\t\t// that what it changed may not all be here.\n\t\tif (!complete) return { changes, unrecorded };\n\t\tawait this.journal.observed(this.sequence, {\n\t\t\tfiles: paths.size,\n\t\t\tbytesRead,\n\t\t\tbytesStored: this.journal.storedBytes - this.measure.storedBefore,\n\t\t\tdurationMs: Math.round(this.measure.durationMs + performance.now() - started),\n\t\t});\n\t\treturn { changes, unrecorded };\n\t}\n}\n\n/**\n * What `path` holds: nothing, or a regular file's content and mode. `undefined` for anything an\n * entry cannot record: a folder, a link, or a file too large to copy.\n */\nasync function readState(path: string): Promise<{ content?: Buffer; mode: number } | undefined> {\n\tlet stats;\n\ttry {\n\t\tstats = await lstat(path);\n\t} catch (error) {\n\t\tif (code(error) === 'ENOENT') return { mode: 0 };\n\t\tthrow error;\n\t}\n\tif (!stats.isFile() || stats.size > maxCoveredBytes) return undefined;\n\ttry {\n\t\treturn { content: await readFile(path), mode: stats.mode & 0o7777 };\n\t} catch (error) {\n\t\t// Deleted between the two calls.\n\t\tif (code(error) === 'ENOENT') return { mode: 0 };\n\t\tthrow error;\n\t}\n}\n\n/** Runs `task` on every item, at most `limit` at a time, and fails with the first failure. */\nasync function eachAtOnce<Item>(\n\titems: readonly Item[],\n\tlimit: number,\n\ttask: (item: Item, index: number) => Promise<void>,\n): Promise<void> {\n\tlet next = 0;\n\tlet failed = false;\n\tconst worker = async () => {\n\t\twhile (!failed && next < items.length) {\n\t\t\tconst index = next++;\n\t\t\ttry {\n\t\t\t\tawait task(items[index] as Item, index);\n\t\t\t} catch (error) {\n\t\t\t\tfailed = true;\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t}\n\t};\n\tawait Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"commandCheckpoint.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/commandCheckpoint.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from 'node:crypto';\nimport { lstat, readFile } from 'node:fs/promises';\nimport { sameState } from '#src/recovery/domain/fileChanges';\nimport type { FileState, Manifest, RunJournal } from '#src/recovery/domain/recoveryStore';\n\n// The same bound the history uses for a file it records: a larger one is not copied.\nconst maxCoveredBytes = 64 * 1024 * 1024;\nconst captureConcurrency = 16;\n\n/**\n * Every file a command's checkpoint covers now, by its real path, absolute: what `.gitignore`\n * does not exclude in the `write` roots, less what the access policy closes to writing by name.\n */\nexport type CoveredFiles = (signal: AbortSignal) => Promise<string[]>;\n\n/** A file a command changed, as the checkpoint found it. */\nexport interface ObservedChange {\n\tpath: string;\n\tbefore: FileState;\n\tafter: FileState;\n}\n\nfunction code(error: unknown): string | undefined {\n\treturn (error as NodeJS.ErrnoException).code;\n}\n\n/**\n * The files a command may change, captured right before it runs and compared right after, so\n * what it changed is in the run's history like any tool's change, and can be undone.\n *\n * `begin` stores the content of every covered file and a manifest of them, then records the\n * observation in the journal; if any of that fails, the command must not run. `end` lists the\n * covered files again, and records each one that differs from the manifest as a change made by\n * the command. This first version reads and hashes everything both times, trusting neither size\n * nor modification time, and keeps what that cost with the observation.\n */\nexport class CommandCheckpoint {\n\tprivate readonly journal: RunJournal;\n\tprivate readonly covered: CoveredFiles;\n\tprivate readonly by: string;\n\tprivate readonly manifest: Manifest;\n\t/** Covered files it could not copy before, which it leaves out after too. */\n\tprivate readonly uncopyable: ReadonlySet<string>;\n\tprivate readonly sequence: number;\n\tprivate readonly measure: { bytesRead: number; durationMs: number; storedBefore: number };\n\n\tprivate constructor(fields: {\n\t\tjournal: RunJournal;\n\t\tcovered: CoveredFiles;\n\t\tby: string;\n\t\tmanifest: Manifest;\n\t\tuncopyable: ReadonlySet<string>;\n\t\tsequence: number;\n\t\tmeasure: { bytesRead: number; durationMs: number; storedBefore: number };\n\t}) {\n\t\tthis.journal = fields.journal;\n\t\tthis.covered = fields.covered;\n\t\tthis.by = fields.by;\n\t\tthis.manifest = fields.manifest;\n\t\tthis.uncopyable = fields.uncopyable;\n\t\tthis.sequence = fields.sequence;\n\t\tthis.measure = fields.measure;\n\t}\n\n\t/** Captures every covered file. Cancelling it, or any failure, leaves the command unstarted. */\n\tstatic async begin({\n\t\tjournal,\n\t\tcovered,\n\t\tby,\n\t\tsignal,\n\t}: {\n\t\tjournal: RunJournal;\n\t\tcovered: CoveredFiles;\n\t\t/** The command, as the history will name it. */\n\t\tby: string;\n\t\tsignal: AbortSignal;\n\t}): Promise<CommandCheckpoint> {\n\t\tconst started = performance.now();\n\t\tconst storedBefore = journal.storedBytes;\n\t\tconst paths = await covered(signal);\n\t\tconst states: (FileState | undefined)[] = [];\n\t\tconst uncopyable = new Set<string>();\n\t\tlet bytesRead = 0;\n\t\t// Several at once: each stored content waits for its own sync, which dominates the time.\n\t\tawait eachAtOnce(paths, captureConcurrency, async (path, index) => {\n\t\t\tsignal.throwIfAborted();\n\t\t\tconst read = await readState(path);\n\t\t\t// A folder, a link or a file too large: no entry records it, so it is not covered.\n\t\t\tif (!read) {\n\t\t\t\tuncopyable.add(path);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (read.content) {\n\t\t\t\tbytesRead += read.content.length;\n\t\t\t\tconst hash = await journal.saveContent(read.content);\n\t\t\t\tstates[index] = { exists: true, hash, mode: read.mode };\n\t\t\t}\n\t\t});\n\t\tconst files: Manifest['files'] = {};\n\t\tpaths.forEach((path, index) => {\n\t\t\tconst state = states[index];\n\t\t\tif (state) files[path] = state;\n\t\t});\n\t\tconst manifest: Manifest = {\n\t\t\tfiles,\n\t\t\t...(uncopyable.size > 0 ? { uncopyable: [...uncopyable] } : {}),\n\t\t};\n\t\tconst hash = await journal.saveContent(Buffer.from(JSON.stringify(manifest)));\n\t\tsignal.throwIfAborted();\n\t\tconst sequence = await journal.observe(hash, by);\n\t\treturn new CommandCheckpoint({\n\t\t\tjournal,\n\t\t\tcovered,\n\t\t\tby,\n\t\t\tmanifest,\n\t\t\tuncopyable,\n\t\t\tsequence,\n\t\t\tmeasure: { bytesRead, durationMs: performance.now() - started, storedBefore },\n\t\t});\n\t}\n\n\t/**\n\t * Compares the covered files with the capture and records every change found. It is never\n\t * cancelled: once the command has run, what it changed must reach the history. Unless\n\t * `complete`, the observation stays open, because the command may still be changing files.\n\t *\n\t * @returns the changes, and the paths it could not record because they are now, or were before,\n\t * something the history cannot copy, such as a file the command replaced with a folder, or one\n\t * too large that it replaced with a small one. The journal names them, so undo leaves them.\n\t */\n\tasync end({ complete }: { complete: boolean }): Promise<{\n\t\tchanges: ObservedChange[];\n\t\tunrecorded: string[];\n\t}> {\n\t\tconst started = performance.now();\n\t\tconst signal = new AbortController().signal;\n\t\tconst paths = new Set([\n\t\t\t...Object.keys(this.manifest.files),\n\t\t\t...this.uncopyable,\n\t\t\t...(await this.covered(signal)),\n\t\t]);\n\t\tconst changes: ObservedChange[] = [];\n\t\tconst unrecorded: string[] = [];\n\t\tlet bytesRead = this.measure.bytesRead;\n\n\t\tfor (const path of paths) {\n\t\t\tconst read = await readState(path);\n\t\t\t// One it could not copy before or after, such as a file over the size limit, is not\n\t\t\t// covered at all. One it could copy only before, or only after, changed in a way the\n\t\t\t// history cannot take back: what it held on the other side is unknown.\n\t\t\tconst copiedBefore = !this.uncopyable.has(path);\n\t\t\tif (!read || !copiedBefore) {\n\t\t\t\tif (read || copiedBefore) {\n\t\t\t\t\tawait this.journal.unrecorded(path, this.by);\n\t\t\t\t\tunrecorded.push(path);\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst before: FileState = this.manifest.files[path] ?? { exists: false };\n\t\t\tlet after: FileState = { exists: false };\n\t\t\tif (read.content) {\n\t\t\t\tbytesRead += read.content.length;\n\t\t\t\tconst hash = createHash('sha256').update(read.content).digest('hex');\n\t\t\t\tafter = { exists: true, hash, mode: read.mode };\n\t\t\t}\n\t\t\tif (sameState(before, after)) continue;\n\t\t\t// Stored only now that it changed, so an unchanged file costs no write.\n\t\t\tif (read.content) await this.journal.saveContent(read.content);\n\t\t\tconst sequence = await this.journal.prepare({ path, before, after, observedBy: this.by });\n\t\t\tawait this.journal.applied(sequence);\n\t\t\tchanges.push({ path, before, after });\n\t\t}\n\n\t\t// Left open when something the command started may still be running: the record then says\n\t\t// that what it changed may not all be here.\n\t\tif (!complete) return { changes, unrecorded };\n\t\tawait this.journal.observed(this.sequence, {\n\t\t\tfiles: paths.size,\n\t\t\tbytesRead,\n\t\t\tbytesStored: this.journal.storedBytes - this.measure.storedBefore,\n\t\t\tdurationMs: Math.round(this.measure.durationMs + performance.now() - started),\n\t\t});\n\t\treturn { changes, unrecorded };\n\t}\n}\n\n/**\n * What `path` holds: nothing, or a regular file's content and mode. `undefined` for anything an\n * entry cannot record: a folder, a link, or a file too large to copy.\n */\nasync function readState(path: string): Promise<{ content?: Buffer; mode: number } | undefined> {\n\tlet stats;\n\ttry {\n\t\tstats = await lstat(path);\n\t} catch (error) {\n\t\tif (code(error) === 'ENOENT') return { mode: 0 };\n\t\tthrow error;\n\t}\n\tif (!stats.isFile() || stats.size > maxCoveredBytes) return undefined;\n\ttry {\n\t\treturn { content: await readFile(path), mode: stats.mode & 0o7777 };\n\t} catch (error) {\n\t\t// Deleted between the two calls.\n\t\tif (code(error) === 'ENOENT') return { mode: 0 };\n\t\tthrow error;\n\t}\n}\n\n/** Runs `task` on every item, at most `limit` at a time, and fails with the first failure. */\nasync function eachAtOnce<Item>(\n\titems: readonly Item[],\n\tlimit: number,\n\ttask: (item: Item, index: number) => Promise<void>,\n): Promise<void> {\n\tlet next = 0;\n\tlet failed = false;\n\tconst worker = async () => {\n\t\twhile (!failed && next < items.length) {\n\t\t\tconst index = next++;\n\t\t\ttry {\n\t\t\t\tawait task(items[index] as Item, index);\n\t\t\t} catch (error) {\n\t\t\t\tfailed = true;\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t}\n\t};\n\tawait Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));\n}\n"]}
|
|
@@ -91,9 +91,13 @@ export async function createCommandTool({ commands, policy, writes, covered, bas
|
|
|
91
91
|
};
|
|
92
92
|
},
|
|
93
93
|
});
|
|
94
|
-
|
|
94
|
+
// Alone: a change another agent of the run made while it ran would be taken for its own.
|
|
95
|
+
function runCommand(run) {
|
|
96
|
+
const session = writes.sessionFor(run.context);
|
|
97
|
+
return session.exclusive(() => runCommandAlone(run, session), run.signal);
|
|
98
|
+
}
|
|
99
|
+
async function runCommandAlone({ name, line, definition, signal, ended }, session) {
|
|
95
100
|
signal.throwIfAborted();
|
|
96
|
-
const session = writes.sessionFor(context);
|
|
97
101
|
const journal = await session.journalForChange();
|
|
98
102
|
let checkpoint;
|
|
99
103
|
try {
|
|
@@ -180,7 +184,7 @@ export async function createCommandTool({ commands, policy, writes, covered, bas
|
|
|
180
184
|
? 'It changed no file the history covers.'
|
|
181
185
|
: `Files it changed, recorded in the history: ${changes.map(change => shown(change.path)).join(', ')}.`);
|
|
182
186
|
if (unrecorded.length > 0) {
|
|
183
|
-
lines.push(`Not recorded, because the history cannot copy them (a folder, a link, or a file over 64 MB): ${unrecorded.map(shown).join(', ')}.`);
|
|
187
|
+
lines.push(`Not recorded, because the history cannot copy them (a folder, a link, or a file over 64 MB) before or after: ${unrecorded.map(shown).join(', ')}. Undo leaves them as they are.`);
|
|
184
188
|
}
|
|
185
189
|
const limit = definition.outputBytes ?? defaultCommandOutputBytes;
|
|
186
190
|
if (result.printedBytes === 0) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commandTool.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/commandTool.ts"],"names":[],"mappings":"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","sourcesContent":["import { realpath } from 'node:fs/promises';\nimport { isAbsolute, relative, resolve } from 'node:path';\nimport { z } from 'zod';\nimport type { CommandLine } from '#src/agent/domain/approval';\nimport type { RunContext } from '#src/agent/domain/runContext';\nimport { displayPath } from '#src/recovery/domain/runChanges';\nimport { InvalidAgentConfigError } from '#src/shared/domain/errors';\nimport type { AccessPolicy } from '../domain/accessPolicy.ts';\nimport {\n\tbuildArguments,\n\ttype CommandDefinition,\n\ttype CommandParameter,\n\ttype Commands,\n\tdefaultCommandOutputBytes,\n\tdefaultCommandTimeoutMs,\n\ttype ParameterValue,\n\tparametersOf,\n\tvalidateCommands,\n} from '../domain/commands.ts';\nimport { EditRefusedError, EditUnconfirmedError } from '../domain/fileEdits.ts';\nimport {\n\ttype CommandEnding,\n\tToolExecutionError,\n\ttype PreparingTool,\n} from '../domain/preparedCall.ts';\nimport { CommandCheckpoint, type CoveredFiles } from './commandCheckpoint.ts';\nimport { OutputFingerprint, withoutColors } from './commandOutput.ts';\nimport { type CommandProcessResult, runCommandProcess } from './commandProcess.ts';\nimport { definePreparingTool } from './defineTool.ts';\nimport type { WorkspaceWrites } from './workspaceWrites.ts';\n\n// Only what a toolchain needs to find itself and its locale. Everything else, the provider\n// keys included, stays in this process.\nconst inheritedVariables = ['PATH', 'HOME', 'TMPDIR', 'USER', 'SHELL', 'LANG'];\n// Long enough for a slow disk, short enough not to hold up building an agent.\nconst cwdCheckMs = 10_000;\n\n/** A command ready to run: its definition, and where it runs as a real path. */\ninterface ResolvedCommand {\n\tdefinition: CommandDefinition;\n\tcwd: string;\n}\n\n/**\n * `runCommand`, which runs one of the commands the consumer configured, by name, with the\n * parameters each one declares. Every call asks the approver, showing the program exactly as\n * it will run; `autoApprove` skips that.\n *\n * Around each command, the covered files are captured before and compared after, so what it\n * changed is in the run's history and can be undone: see `CommandCheckpoint`. If the capture\n * fails, the command does not run. A timeout or a cancellation stops its whole process group,\n * and is never reported as a completed command. If the group cannot be confirmed stopped, what\n * changed so far is recorded, the observation stays open, and writing stops for the run.\n *\n * This is not a sandbox: a command runs the repository's code, with everything this user can\n * reach. See \"Configured commands are trusted, not sandboxed\" in decisions.md.\n *\n * @throws {InvalidAgentConfigError} for an invalid configuration, a working directory outside\n * every `write` root, or on Windows, where a process group cannot be stopped.\n */\nexport async function createCommandTool({\n\tcommands,\n\tpolicy,\n\twrites,\n\tcovered,\n\tbase,\n\tplatform = process.platform,\n\trunProcess = runCommandProcess,\n}: {\n\tcommands: Commands;\n\tpolicy: AccessPolicy;\n\twrites: WorkspaceWrites;\n\tcovered: CoveredFiles;\n\t/** The real path of the first root, where a relative `cwd` starts. */\n\tbase: string;\n\tplatform?: NodeJS.Platform;\n\t/** Runs the program. Replaced in tests, to reach what a real process rarely does. */\n\trunProcess?: typeof runCommandProcess;\n}): Promise<PreparingTool> {\n\tif (platform === 'win32') {\n\t\tthrow new InvalidAgentConfigError(\n\t\t\t'Configured commands are not supported on Windows yet: the harness cannot stop a command and everything it started there',\n\t\t);\n\t}\n\tvalidateCommands(commands);\n\tconst resolved = new Map<string, ResolvedCommand>();\n\tfor (const [name, definition] of Object.entries(commands)) {\n\t\tresolved.set(name, {\n\t\t\tdefinition,\n\t\t\tcwd: await resolveCwd(`Command \"${name}\"`, definition.cwd, policy, base),\n\t\t});\n\t}\n\n\treturn definePreparingTool({\n\t\tname: 'runCommand',\n\t\tdescription: describeTool(commands),\n\t\tinput: inputSchema(commands),\n\t\tsummarize: ({ command: { name, ...values } }) => {\n\t\t\tconst definition = commands[name];\n\t\t\t// The schema only permits configured names, but keep this defensive for callers that\n\t\t\t// use a tool outside LLMAgent. This description never uses the resolved cwd.\n\t\t\treturn definition ? visibleCommand(definition, values) : name;\n\t\t},\n\t\tprepare: async ({ command: { name, ...values } }, signal, { run }) => {\n\t\t\tconst command = resolved.get(name);\n\t\t\t// The schema allows only the configured names.\n\t\t\tif (!command) throw new Error(`There is no command named \"${name}\"`);\n\t\t\tconst paramValues: Record<string, ParameterValue> = {};\n\t\t\tfor (const parameter of parametersOf(command.definition)) {\n\t\t\t\tconst value =\n\t\t\t\t\t(values as Record<string, ParameterValue | undefined>)[parameter.name] ?? null;\n\t\t\t\tparamValues[parameter.name] =\n\t\t\t\t\tparameter.type === 'paths' && Array.isArray(value)\n\t\t\t\t\t\t? await resolvePaths(value, command.cwd, policy, signal)\n\t\t\t\t\t\t: value;\n\t\t\t}\n\t\t\tconst line: CommandLine = Object.freeze({\n\t\t\t\texecutable: command.definition.executable,\n\t\t\t\targs: Object.freeze(buildArguments(command.definition, paramValues)) as string[],\n\t\t\t\tcwd: command.cwd,\n\t\t\t});\n\n\t\t\tlet ending: CommandEnding | undefined;\n\t\t\treturn {\n\t\t\t\t// A command can change anything its user can: it is always asked about.\n\t\t\t\trisk: 'destructive',\n\t\t\t\tcommand: line,\n\t\t\t\tending: () => ending,\n\t\t\t\trun: runSignal =>\n\t\t\t\t\trunCommand({\n\t\t\t\t\t\tname,\n\t\t\t\t\t\tline,\n\t\t\t\t\t\tdefinition: command.definition,\n\t\t\t\t\t\tcontext: run,\n\t\t\t\t\t\tsignal: runSignal,\n\t\t\t\t\t\tended: end => {\n\t\t\t\t\t\t\tending = end;\n\t\t\t\t\t\t},\n\t\t\t\t\t}),\n\t\t\t};\n\t\t},\n\t});\n\n\tasync function runCommand({\n\t\tname,\n\t\tline,\n\t\tdefinition,\n\t\tcontext,\n\t\tsignal,\n\t\tended,\n\t}: {\n\t\tname: string;\n\t\tline: CommandLine;\n\t\tdefinition: CommandDefinition;\n\t\tcontext: RunContext;\n\t\tsignal: AbortSignal;\n\t\t/** Told how the command ended, once it ran to an end and everything it changed is recorded. */\n\t\tended: (ending: CommandEnding) => void;\n\t}): Promise<string> {\n\t\tsignal.throwIfAborted();\n\t\tconst session = writes.sessionFor(context);\n\t\tconst journal = await session.journalForChange();\n\t\tlet checkpoint: CommandCheckpoint;\n\t\ttry {\n\t\t\tcheckpoint = await CommandCheckpoint.begin({ journal, covered, by: name, signal });\n\t\t} catch (error) {\n\t\t\tsignal.throwIfAborted();\n\t\t\tthrow new EditRefusedError(\n\t\t\t\t`The harness could not capture the files before \"${name}\", so it did not run`,\n\t\t\t\t{ cause: error },\n\t\t\t);\n\t\t}\n\n\t\tlet result: CommandProcessResult | undefined;\n\t\tlet startFailure: unknown;\n\t\tconst fingerprint = new OutputFingerprint();\n\t\ttry {\n\t\t\tresult = await runProcess(line.executable, line.args, {\n\t\t\t\tcwd: line.cwd,\n\t\t\t\tenv: environmentFor(definition),\n\t\t\t\ttimeoutMs: definition.timeoutMs ?? defaultCommandTimeoutMs,\n\t\t\t\toutputBytes: definition.outputBytes ?? defaultCommandOutputBytes,\n\t\t\t\tsignal,\n\t\t\t\tonOutput: (stream, chunk) => {\n\t\t\t\t\tfingerprint.push(stream, chunk);\n\t\t\t\t},\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tstartFailure = error;\n\t\t}\n\n\t\tconst complete = result?.stopped ?? true;\n\t\tlet observed: Awaited<ReturnType<CommandCheckpoint['end']>>;\n\t\ttry {\n\t\t\tobserved = await checkpoint.end({ complete });\n\t\t} catch (error) {\n\t\t\tsession.stop('Writing stopped: the harness could not record what a command changed');\n\t\t\tthrow new EditUnconfirmedError(\n\t\t\t\t`\"${name}\" ran, but the harness could not record what it changed; writing has stopped for this run`,\n\t\t\t\t{ cause: error },\n\t\t\t);\n\t\t}\n\n\t\tif (!result) {\n\t\t\tsignal.throwIfAborted();\n\t\t\tthrow new EditRefusedError(`\"${name}\" could not be started`, { cause: startFailure });\n\t\t}\n\t\tconst report = describeRun(name, definition, result, observed);\n\t\tif (!complete) {\n\t\t\tsession.stop('Writing stopped: a command could not be stopped');\n\t\t\tthrow new EditUnconfirmedError(\n\t\t\t\t`${report}\\n\"${name}\" could not be stopped: something it started may still be changing files, and the history may not hold all of it. Writing has stopped for this run.`,\n\t\t\t);\n\t\t}\n\t\t// After its changes are recorded: a cancelled command is never reported as finished.\n\t\tif (result.outcome === 'cancelled') signal.throwIfAborted();\n\t\tended({\n\t\t\tpassed: result.outcome === 'exited' && result.exitCode === 0,\n\t\t\thow: howItEnded(result),\n\t\t\toutput: withoutColors(result.output),\n\t\t\tfingerprint: fingerprint.digest(),\n\t\t\t...(observed.unrecorded.length === 0 ? { state: journal.contentState() } : {}),\n\t\t\tchanged: journal.appliedPaths.map(path => displayPath(base, path)),\n\t\t});\n\t\tif (result.outcome !== 'exited') throw new ToolExecutionError(report);\n\t\tif (result.exitCode !== 0) {\n\t\t\tthrow new ToolExecutionError(report, {\n\t\t\t\t...(result.exitCode === null ? {} : { exitCode: result.exitCode }),\n\t\t\t});\n\t\t}\n\t\treturn report;\n\t}\n\n\tfunction describeRun(\n\t\tname: string,\n\t\tdefinition: CommandDefinition,\n\t\tresult: CommandProcessResult,\n\t\t{ changes, unrecorded }: Awaited<ReturnType<CommandCheckpoint['end']>>,\n\t): string {\n\t\tconst seconds = (result.durationMs / 1000).toFixed(1);\n\t\tconst lines: string[] = [];\n\t\tif (result.outcome === 'timedOut') {\n\t\t\tconst limit = (definition.timeoutMs ?? defaultCommandTimeoutMs) / 1000;\n\t\t\tlines.push(`\"${name}\" ran past its limit of ${String(limit)} s and was stopped.`);\n\t\t} else if (result.outcome === 'cancelled') {\n\t\t\tlines.push(`\"${name}\" was stopped: the run was cancelled.`);\n\t\t} else if (result.exitCode !== null) {\n\t\t\tlines.push(`\"${name}\" exited with code ${String(result.exitCode)} after ${seconds} s.`);\n\t\t} else {\n\t\t\tlines.push(`\"${name}\" was killed by ${result.signal ?? 'a signal'} after ${seconds} s.`);\n\t\t}\n\n\t\tconst shown = (path: string) => displayPath(base, path);\n\t\tlines.push(\n\t\t\tchanges.length === 0\n\t\t\t\t? 'It changed no file the history covers.'\n\t\t\t\t: `Files it changed, recorded in the history: ${changes.map(change => shown(change.path)).join(', ')}.`,\n\t\t);\n\t\tif (unrecorded.length > 0) {\n\t\t\tlines.push(\n\t\t\t\t`Not recorded, because the history cannot copy them (a folder, a link, or a file over 64 MB): ${unrecorded.map(shown).join(', ')}.`,\n\t\t\t);\n\t\t}\n\n\t\tconst limit = definition.outputBytes ?? defaultCommandOutputBytes;\n\t\tif (result.printedBytes === 0) {\n\t\t\tlines.push('It printed nothing.');\n\t\t} else {\n\t\t\tlines.push(\n\t\t\t\tresult.printedBytes > limit\n\t\t\t\t\t? `Output, the last ${kilobytes(limit)} of ${kilobytes(result.printedBytes)}:`\n\t\t\t\t\t: 'Output:',\n\t\t\t\tresult.output,\n\t\t\t);\n\t\t}\n\t\treturn lines.join('\\n');\n\t}\n}\n\nfunction howItEnded(result: CommandProcessResult): string {\n\tif (result.outcome === 'timedOut') return 'ran past its time limit';\n\tif (result.outcome === 'cancelled') return 'was cancelled';\n\tif (result.exitCode !== null) return `exited with code ${String(result.exitCode)}`;\n\treturn `was killed by ${result.signal ?? 'a signal'}`;\n}\n\nfunction kilobytes(bytes: number): string {\n\treturn `${String(Math.ceil(bytes / 1024))} KB`;\n}\n\n/** The command line the model can see, before paths are resolved against the host workspace. */\nfunction visibleCommand(\n\tdefinition: CommandDefinition,\n\tvalues: Record<string, ParameterValue | undefined>,\n): string {\n\tconst args: string[] = [];\n\tfor (const argument of definition.args ?? []) {\n\t\tif (typeof argument === 'string') {\n\t\t\targs.push(argument);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst value = values[argument.name] ?? null;\n\t\tif (argument.type === 'paths') {\n\t\t\tif (Array.isArray(value)) args.push(...value);\n\t\t} else if (value !== null && typeof value === 'string') {\n\t\t\targs.push(value);\n\t\t}\n\t}\n\n\treturn [definition.executable, ...args].join(' ');\n}\n\n/**\n * Where a configured program runs, as a real path inside a `write` root. `program` names it in\n * the error, as `Command \"test\"`.\n */\nexport async function resolveCwd(\n\tprogram: string,\n\tcwd: string | undefined,\n\tpolicy: AccessPolicy,\n\tbase: string,\n): Promise<string> {\n\tconst named = resolve(base, cwd ?? '.');\n\tlet real: string;\n\ttry {\n\t\treal = await realpath(named);\n\t\tawait policy.check(real, 'write', AbortSignal.timeout(cwdCheckMs));\n\t} catch (error) {\n\t\tthrow new InvalidAgentConfigError(\n\t\t\t`${program} needs a cwd inside a write root of the workspace`,\n\t\t\t{ cause: error },\n\t\t);\n\t}\n\treturn real;\n}\n\n/**\n * Each path, once the policy lets the agent read it, relative to where the command runs. One\n * that would read as a flag is given as `./` and its name.\n */\nasync function resolvePaths(\n\tpaths: string[],\n\tcwd: string,\n\tpolicy: AccessPolicy,\n\tsignal: AbortSignal,\n): Promise<string[]> {\n\tconst resolved: string[] = [];\n\tfor (const path of paths) {\n\t\tconst { absolute } = await policy.check(path, 'read', signal);\n\t\tconst fromCwd = relative(cwd, absolute) || '.';\n\t\tresolved.push(fromCwd.startsWith('-') && !isAbsolute(fromCwd) ? `./${fromCwd}` : fromCwd);\n\t}\n\treturn resolved;\n}\n\n/** A small environment of its own: the inherited few, then what the consumer set. */\nexport function environmentFor(\n\tdefinition: Pick<CommandDefinition, 'env'>,\n\tenv: NodeJS.ProcessEnv = process.env,\n): NodeJS.ProcessEnv {\n\tconst inherited: NodeJS.ProcessEnv = {};\n\tfor (const [variable, value] of Object.entries(env)) {\n\t\tif (value === undefined) continue;\n\t\tif (inheritedVariables.includes(variable) || variable.startsWith('LC_')) {\n\t\t\tinherited[variable] = value;\n\t\t}\n\t}\n\treturn { ...inherited, ...definition.env };\n}\n\nfunction describeTool(commands: Commands): string {\n\tconst list = Object.entries(commands)\n\t\t.map(([name, { description }]) => `- ${name}: ${description}`)\n\t\t.join('\\n');\n\treturn [\n\t\t\"Runs one of the project's configured commands, by name, such as its tests or its formatter.\",\n\t\t'Files it changes are recorded in the history, so they can be undone. Its output is cut to its end.',\n\t\t'Commands:',\n\t\tlist,\n\t].join('\\n');\n}\n\n/**\n * One object per command, told apart by `name`, each with the parameters that command takes.\n * Strict mode needs every key present, so a parameter that may be left out takes `null`.\n */\nfunction inputSchema(commands: Commands) {\n\tconst variants = Object.entries(commands).map(([name, definition]) =>\n\t\tz.object({\n\t\t\tname: z.enum([name]),\n\t\t\t...Object.fromEntries(\n\t\t\t\tparametersOf(definition).map(parameter => [parameter.name, parameterSchema(parameter)]),\n\t\t\t),\n\t\t}),\n\t);\n\tconst [first, ...rest] = variants;\n\t// `validateCommands` refuses an empty configuration.\n\tif (!first) throw new InvalidAgentConfigError('No command is configured');\n\treturn z.object({\n\t\tcommand: (rest.length === 0 ? first : z.union([first, ...rest])).describe(\n\t\t\t'The command to run, and its parameters.',\n\t\t),\n\t});\n}\n\nfunction parameterSchema(parameter: CommandParameter): z.ZodType {\n\tswitch (parameter.type) {\n\t\tcase 'paths':\n\t\t\treturn z\n\t\t\t\t.array(z.string())\n\t\t\t\t.describe(`${parameter.description} Paths inside the workspace; [] for none.`);\n\t\tcase 'choice': {\n\t\t\tconst values = z.enum(parameter.values as [string, ...string[]]);\n\t\t\treturn parameter.required === true\n\t\t\t\t? values.describe(parameter.description)\n\t\t\t\t: values.nullable().describe(`${parameter.description} null to leave it out.`);\n\t\t}\n\t\tcase 'text': {\n\t\t\tconst text = z.string();\n\t\t\tconst description = `${parameter.description} Must match the regular expression ${parameter.pattern}, and not start with \"-\".`;\n\t\t\treturn parameter.required === true\n\t\t\t\t? text.describe(description)\n\t\t\t\t: text.nullable().describe(`${description} null to leave it out.`);\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"commandTool.js","sourceRoot":"","sources":["../../../src/tools/infrastructure/commandTool.ts"],"names":[],"mappings":"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","sourcesContent":["import { realpath } from 'node:fs/promises';\nimport { isAbsolute, relative, resolve } from 'node:path';\nimport { z } from 'zod';\nimport type { CommandLine } from '#src/agent/domain/approval';\nimport type { RunContext } from '#src/agent/domain/runContext';\nimport { displayPath } from '#src/recovery/domain/runChanges';\nimport { InvalidAgentConfigError } from '#src/shared/domain/errors';\nimport type { AccessPolicy } from '../domain/accessPolicy.ts';\nimport {\n\tbuildArguments,\n\ttype CommandDefinition,\n\ttype CommandParameter,\n\ttype Commands,\n\tdefaultCommandOutputBytes,\n\tdefaultCommandTimeoutMs,\n\ttype ParameterValue,\n\tparametersOf,\n\tvalidateCommands,\n} from '../domain/commands.ts';\nimport { EditRefusedError, EditUnconfirmedError } from '../domain/fileEdits.ts';\nimport {\n\ttype CommandEnding,\n\tToolExecutionError,\n\ttype PreparingTool,\n} from '../domain/preparedCall.ts';\nimport { CommandCheckpoint, type CoveredFiles } from './commandCheckpoint.ts';\nimport { OutputFingerprint, withoutColors } from './commandOutput.ts';\nimport { type CommandProcessResult, runCommandProcess } from './commandProcess.ts';\nimport { definePreparingTool } from './defineTool.ts';\nimport type { WorkspaceWrites } from './workspaceWrites.ts';\nimport type { WriteSession } from './writeSession.ts';\n\n// Only what a toolchain needs to find itself and its locale. Everything else, the provider\n// keys included, stays in this process.\nconst inheritedVariables = ['PATH', 'HOME', 'TMPDIR', 'USER', 'SHELL', 'LANG'];\n// Long enough for a slow disk, short enough not to hold up building an agent.\nconst cwdCheckMs = 10_000;\n\n/** A command ready to run: its definition, and where it runs as a real path. */\ninterface ResolvedCommand {\n\tdefinition: CommandDefinition;\n\tcwd: string;\n}\n\n/**\n * `runCommand`, which runs one of the commands the consumer configured, by name, with the\n * parameters each one declares. Every call asks the approver, showing the program exactly as\n * it will run; `autoApprove` skips that.\n *\n * Around each command, the covered files are captured before and compared after, so what it\n * changed is in the run's history and can be undone: see `CommandCheckpoint`. If the capture\n * fails, the command does not run. A timeout or a cancellation stops its whole process group,\n * and is never reported as a completed command. If the group cannot be confirmed stopped, what\n * changed so far is recorded, the observation stays open, and writing stops for the run.\n *\n * This is not a sandbox: a command runs the repository's code, with everything this user can\n * reach. See \"Configured commands are trusted, not sandboxed\" in decisions.md.\n *\n * @throws {InvalidAgentConfigError} for an invalid configuration, a working directory outside\n * every `write` root, or on Windows, where a process group cannot be stopped.\n */\nexport async function createCommandTool({\n\tcommands,\n\tpolicy,\n\twrites,\n\tcovered,\n\tbase,\n\tplatform = process.platform,\n\trunProcess = runCommandProcess,\n}: {\n\tcommands: Commands;\n\tpolicy: AccessPolicy;\n\twrites: WorkspaceWrites;\n\tcovered: CoveredFiles;\n\t/** The real path of the first root, where a relative `cwd` starts. */\n\tbase: string;\n\tplatform?: NodeJS.Platform;\n\t/** Runs the program. Replaced in tests, to reach what a real process rarely does. */\n\trunProcess?: typeof runCommandProcess;\n}): Promise<PreparingTool> {\n\tif (platform === 'win32') {\n\t\tthrow new InvalidAgentConfigError(\n\t\t\t'Configured commands are not supported on Windows yet: the harness cannot stop a command and everything it started there',\n\t\t);\n\t}\n\tvalidateCommands(commands);\n\tconst resolved = new Map<string, ResolvedCommand>();\n\tfor (const [name, definition] of Object.entries(commands)) {\n\t\tresolved.set(name, {\n\t\t\tdefinition,\n\t\t\tcwd: await resolveCwd(`Command \"${name}\"`, definition.cwd, policy, base),\n\t\t});\n\t}\n\n\treturn definePreparingTool({\n\t\tname: 'runCommand',\n\t\tdescription: describeTool(commands),\n\t\tinput: inputSchema(commands),\n\t\tsummarize: ({ command: { name, ...values } }) => {\n\t\t\tconst definition = commands[name];\n\t\t\t// The schema only permits configured names, but keep this defensive for callers that\n\t\t\t// use a tool outside LLMAgent. This description never uses the resolved cwd.\n\t\t\treturn definition ? visibleCommand(definition, values) : name;\n\t\t},\n\t\tprepare: async ({ command: { name, ...values } }, signal, { run }) => {\n\t\t\tconst command = resolved.get(name);\n\t\t\t// The schema allows only the configured names.\n\t\t\tif (!command) throw new Error(`There is no command named \"${name}\"`);\n\t\t\tconst paramValues: Record<string, ParameterValue> = {};\n\t\t\tfor (const parameter of parametersOf(command.definition)) {\n\t\t\t\tconst value =\n\t\t\t\t\t(values as Record<string, ParameterValue | undefined>)[parameter.name] ?? null;\n\t\t\t\tparamValues[parameter.name] =\n\t\t\t\t\tparameter.type === 'paths' && Array.isArray(value)\n\t\t\t\t\t\t? await resolvePaths(value, command.cwd, policy, signal)\n\t\t\t\t\t\t: value;\n\t\t\t}\n\t\t\tconst line: CommandLine = Object.freeze({\n\t\t\t\texecutable: command.definition.executable,\n\t\t\t\targs: Object.freeze(buildArguments(command.definition, paramValues)) as string[],\n\t\t\t\tcwd: command.cwd,\n\t\t\t});\n\n\t\t\tlet ending: CommandEnding | undefined;\n\t\t\treturn {\n\t\t\t\t// A command can change anything its user can: it is always asked about.\n\t\t\t\trisk: 'destructive',\n\t\t\t\tcommand: line,\n\t\t\t\tending: () => ending,\n\t\t\t\trun: runSignal =>\n\t\t\t\t\trunCommand({\n\t\t\t\t\t\tname,\n\t\t\t\t\t\tline,\n\t\t\t\t\t\tdefinition: command.definition,\n\t\t\t\t\t\tcontext: run,\n\t\t\t\t\t\tsignal: runSignal,\n\t\t\t\t\t\tended: end => {\n\t\t\t\t\t\t\tending = end;\n\t\t\t\t\t\t},\n\t\t\t\t\t}),\n\t\t\t};\n\t\t},\n\t});\n\n\tinterface CommandRun {\n\t\tname: string;\n\t\tline: CommandLine;\n\t\tdefinition: CommandDefinition;\n\t\tcontext: RunContext;\n\t\tsignal: AbortSignal;\n\t\t/** Told how the command ended, once it ran to an end and everything it changed is recorded. */\n\t\tended: (ending: CommandEnding) => void;\n\t}\n\n\t// Alone: a change another agent of the run made while it ran would be taken for its own.\n\tfunction runCommand(run: CommandRun): Promise<string> {\n\t\tconst session = writes.sessionFor(run.context);\n\t\treturn session.exclusive(() => runCommandAlone(run, session), run.signal);\n\t}\n\n\tasync function runCommandAlone(\n\t\t{ name, line, definition, signal, ended }: CommandRun,\n\t\tsession: WriteSession,\n\t): Promise<string> {\n\t\tsignal.throwIfAborted();\n\t\tconst journal = await session.journalForChange();\n\t\tlet checkpoint: CommandCheckpoint;\n\t\ttry {\n\t\t\tcheckpoint = await CommandCheckpoint.begin({ journal, covered, by: name, signal });\n\t\t} catch (error) {\n\t\t\tsignal.throwIfAborted();\n\t\t\tthrow new EditRefusedError(\n\t\t\t\t`The harness could not capture the files before \"${name}\", so it did not run`,\n\t\t\t\t{ cause: error },\n\t\t\t);\n\t\t}\n\n\t\tlet result: CommandProcessResult | undefined;\n\t\tlet startFailure: unknown;\n\t\tconst fingerprint = new OutputFingerprint();\n\t\ttry {\n\t\t\tresult = await runProcess(line.executable, line.args, {\n\t\t\t\tcwd: line.cwd,\n\t\t\t\tenv: environmentFor(definition),\n\t\t\t\ttimeoutMs: definition.timeoutMs ?? defaultCommandTimeoutMs,\n\t\t\t\toutputBytes: definition.outputBytes ?? defaultCommandOutputBytes,\n\t\t\t\tsignal,\n\t\t\t\tonOutput: (stream, chunk) => {\n\t\t\t\t\tfingerprint.push(stream, chunk);\n\t\t\t\t},\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tstartFailure = error;\n\t\t}\n\n\t\tconst complete = result?.stopped ?? true;\n\t\tlet observed: Awaited<ReturnType<CommandCheckpoint['end']>>;\n\t\ttry {\n\t\t\tobserved = await checkpoint.end({ complete });\n\t\t} catch (error) {\n\t\t\tsession.stop('Writing stopped: the harness could not record what a command changed');\n\t\t\tthrow new EditUnconfirmedError(\n\t\t\t\t`\"${name}\" ran, but the harness could not record what it changed; writing has stopped for this run`,\n\t\t\t\t{ cause: error },\n\t\t\t);\n\t\t}\n\n\t\tif (!result) {\n\t\t\tsignal.throwIfAborted();\n\t\t\tthrow new EditRefusedError(`\"${name}\" could not be started`, { cause: startFailure });\n\t\t}\n\t\tconst report = describeRun(name, definition, result, observed);\n\t\tif (!complete) {\n\t\t\tsession.stop('Writing stopped: a command could not be stopped');\n\t\t\tthrow new EditUnconfirmedError(\n\t\t\t\t`${report}\\n\"${name}\" could not be stopped: something it started may still be changing files, and the history may not hold all of it. Writing has stopped for this run.`,\n\t\t\t);\n\t\t}\n\t\t// After its changes are recorded: a cancelled command is never reported as finished.\n\t\tif (result.outcome === 'cancelled') signal.throwIfAborted();\n\t\tended({\n\t\t\tpassed: result.outcome === 'exited' && result.exitCode === 0,\n\t\t\thow: howItEnded(result),\n\t\t\toutput: withoutColors(result.output),\n\t\t\tfingerprint: fingerprint.digest(),\n\t\t\t...(observed.unrecorded.length === 0 ? { state: journal.contentState() } : {}),\n\t\t\tchanged: journal.appliedPaths.map(path => displayPath(base, path)),\n\t\t});\n\t\tif (result.outcome !== 'exited') throw new ToolExecutionError(report);\n\t\tif (result.exitCode !== 0) {\n\t\t\tthrow new ToolExecutionError(report, {\n\t\t\t\t...(result.exitCode === null ? {} : { exitCode: result.exitCode }),\n\t\t\t});\n\t\t}\n\t\treturn report;\n\t}\n\n\tfunction describeRun(\n\t\tname: string,\n\t\tdefinition: CommandDefinition,\n\t\tresult: CommandProcessResult,\n\t\t{ changes, unrecorded }: Awaited<ReturnType<CommandCheckpoint['end']>>,\n\t): string {\n\t\tconst seconds = (result.durationMs / 1000).toFixed(1);\n\t\tconst lines: string[] = [];\n\t\tif (result.outcome === 'timedOut') {\n\t\t\tconst limit = (definition.timeoutMs ?? defaultCommandTimeoutMs) / 1000;\n\t\t\tlines.push(`\"${name}\" ran past its limit of ${String(limit)} s and was stopped.`);\n\t\t} else if (result.outcome === 'cancelled') {\n\t\t\tlines.push(`\"${name}\" was stopped: the run was cancelled.`);\n\t\t} else if (result.exitCode !== null) {\n\t\t\tlines.push(`\"${name}\" exited with code ${String(result.exitCode)} after ${seconds} s.`);\n\t\t} else {\n\t\t\tlines.push(`\"${name}\" was killed by ${result.signal ?? 'a signal'} after ${seconds} s.`);\n\t\t}\n\n\t\tconst shown = (path: string) => displayPath(base, path);\n\t\tlines.push(\n\t\t\tchanges.length === 0\n\t\t\t\t? 'It changed no file the history covers.'\n\t\t\t\t: `Files it changed, recorded in the history: ${changes.map(change => shown(change.path)).join(', ')}.`,\n\t\t);\n\t\tif (unrecorded.length > 0) {\n\t\t\tlines.push(\n\t\t\t\t`Not recorded, because the history cannot copy them (a folder, a link, or a file over 64 MB) before or after: ${unrecorded.map(shown).join(', ')}. Undo leaves them as they are.`,\n\t\t\t);\n\t\t}\n\n\t\tconst limit = definition.outputBytes ?? defaultCommandOutputBytes;\n\t\tif (result.printedBytes === 0) {\n\t\t\tlines.push('It printed nothing.');\n\t\t} else {\n\t\t\tlines.push(\n\t\t\t\tresult.printedBytes > limit\n\t\t\t\t\t? `Output, the last ${kilobytes(limit)} of ${kilobytes(result.printedBytes)}:`\n\t\t\t\t\t: 'Output:',\n\t\t\t\tresult.output,\n\t\t\t);\n\t\t}\n\t\treturn lines.join('\\n');\n\t}\n}\n\nfunction howItEnded(result: CommandProcessResult): string {\n\tif (result.outcome === 'timedOut') return 'ran past its time limit';\n\tif (result.outcome === 'cancelled') return 'was cancelled';\n\tif (result.exitCode !== null) return `exited with code ${String(result.exitCode)}`;\n\treturn `was killed by ${result.signal ?? 'a signal'}`;\n}\n\nfunction kilobytes(bytes: number): string {\n\treturn `${String(Math.ceil(bytes / 1024))} KB`;\n}\n\n/** The command line the model can see, before paths are resolved against the host workspace. */\nfunction visibleCommand(\n\tdefinition: CommandDefinition,\n\tvalues: Record<string, ParameterValue | undefined>,\n): string {\n\tconst args: string[] = [];\n\tfor (const argument of definition.args ?? []) {\n\t\tif (typeof argument === 'string') {\n\t\t\targs.push(argument);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst value = values[argument.name] ?? null;\n\t\tif (argument.type === 'paths') {\n\t\t\tif (Array.isArray(value)) args.push(...value);\n\t\t} else if (value !== null && typeof value === 'string') {\n\t\t\targs.push(value);\n\t\t}\n\t}\n\n\treturn [definition.executable, ...args].join(' ');\n}\n\n/**\n * Where a configured program runs, as a real path inside a `write` root. `program` names it in\n * the error, as `Command \"test\"`.\n */\nexport async function resolveCwd(\n\tprogram: string,\n\tcwd: string | undefined,\n\tpolicy: AccessPolicy,\n\tbase: string,\n): Promise<string> {\n\tconst named = resolve(base, cwd ?? '.');\n\tlet real: string;\n\ttry {\n\t\treal = await realpath(named);\n\t\tawait policy.check(real, 'write', AbortSignal.timeout(cwdCheckMs));\n\t} catch (error) {\n\t\tthrow new InvalidAgentConfigError(\n\t\t\t`${program} needs a cwd inside a write root of the workspace`,\n\t\t\t{ cause: error },\n\t\t);\n\t}\n\treturn real;\n}\n\n/**\n * Each path, once the policy lets the agent read it, relative to where the command runs. One\n * that would read as a flag is given as `./` and its name.\n */\nasync function resolvePaths(\n\tpaths: string[],\n\tcwd: string,\n\tpolicy: AccessPolicy,\n\tsignal: AbortSignal,\n): Promise<string[]> {\n\tconst resolved: string[] = [];\n\tfor (const path of paths) {\n\t\tconst { absolute } = await policy.check(path, 'read', signal);\n\t\tconst fromCwd = relative(cwd, absolute) || '.';\n\t\tresolved.push(fromCwd.startsWith('-') && !isAbsolute(fromCwd) ? `./${fromCwd}` : fromCwd);\n\t}\n\treturn resolved;\n}\n\n/** A small environment of its own: the inherited few, then what the consumer set. */\nexport function environmentFor(\n\tdefinition: Pick<CommandDefinition, 'env'>,\n\tenv: NodeJS.ProcessEnv = process.env,\n): NodeJS.ProcessEnv {\n\tconst inherited: NodeJS.ProcessEnv = {};\n\tfor (const [variable, value] of Object.entries(env)) {\n\t\tif (value === undefined) continue;\n\t\tif (inheritedVariables.includes(variable) || variable.startsWith('LC_')) {\n\t\t\tinherited[variable] = value;\n\t\t}\n\t}\n\treturn { ...inherited, ...definition.env };\n}\n\nfunction describeTool(commands: Commands): string {\n\tconst list = Object.entries(commands)\n\t\t.map(([name, { description }]) => `- ${name}: ${description}`)\n\t\t.join('\\n');\n\treturn [\n\t\t\"Runs one of the project's configured commands, by name, such as its tests or its formatter.\",\n\t\t'Files it changes are recorded in the history, so they can be undone. Its output is cut to its end.',\n\t\t'Commands:',\n\t\tlist,\n\t].join('\\n');\n}\n\n/**\n * One object per command, told apart by `name`, each with the parameters that command takes.\n * Strict mode needs every key present, so a parameter that may be left out takes `null`.\n */\nfunction inputSchema(commands: Commands) {\n\tconst variants = Object.entries(commands).map(([name, definition]) =>\n\t\tz.object({\n\t\t\tname: z.enum([name]),\n\t\t\t...Object.fromEntries(\n\t\t\t\tparametersOf(definition).map(parameter => [parameter.name, parameterSchema(parameter)]),\n\t\t\t),\n\t\t}),\n\t);\n\tconst [first, ...rest] = variants;\n\t// `validateCommands` refuses an empty configuration.\n\tif (!first) throw new InvalidAgentConfigError('No command is configured');\n\treturn z.object({\n\t\tcommand: (rest.length === 0 ? first : z.union([first, ...rest])).describe(\n\t\t\t'The command to run, and its parameters.',\n\t\t),\n\t});\n}\n\nfunction parameterSchema(parameter: CommandParameter): z.ZodType {\n\tswitch (parameter.type) {\n\t\tcase 'paths':\n\t\t\treturn z\n\t\t\t\t.array(z.string())\n\t\t\t\t.describe(`${parameter.description} Paths inside the workspace; [] for none.`);\n\t\tcase 'choice': {\n\t\t\tconst values = z.enum(parameter.values as [string, ...string[]]);\n\t\t\treturn parameter.required === true\n\t\t\t\t? values.describe(parameter.description)\n\t\t\t\t: values.nullable().describe(`${parameter.description} null to leave it out.`);\n\t\t}\n\t\tcase 'text': {\n\t\t\tconst text = z.string();\n\t\t\tconst description = `${parameter.description} Must match the regular expression ${parameter.pattern}, and not start with \"-\".`;\n\t\t\treturn parameter.required === true\n\t\t\t\t? text.describe(description)\n\t\t\t\t: text.nullable().describe(`${description} null to leave it out.`);\n\t\t}\n\t}\n}\n"]}
|
|
@@ -1,19 +1,9 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { withRunContext } from '#src/agent/domain/runContext';
|
|
3
2
|
import { RecoverableError, UnrecoverableError } from '#src/shared/domain/errors';
|
|
4
3
|
import { definePreparingTool } from "./defineTool.js";
|
|
4
|
+
import { cutAnswer, failureOf, subagentOptions } from "./subagents.js";
|
|
5
5
|
/** The most explorers one call sends, so a single call cannot fan out without bound. */
|
|
6
6
|
export const maxExplorers = 4;
|
|
7
|
-
// An answer is read whole, like a read, which returns at most 16 KB.
|
|
8
|
-
const maxAnswerCharacters = 16_000;
|
|
9
|
-
function cut(answer) {
|
|
10
|
-
if (answer.length <= maxAnswerCharacters)
|
|
11
|
-
return answer;
|
|
12
|
-
return `${answer.slice(0, maxAnswerCharacters)}\n[The answer was cut at ${String(maxAnswerCharacters)} characters.]`;
|
|
13
|
-
}
|
|
14
|
-
function failureOf(error) {
|
|
15
|
-
return typeof error.cause === 'string' ? `${error.message}: ${error.cause}` : error.message;
|
|
16
|
-
}
|
|
17
7
|
/**
|
|
18
8
|
* Runs one explorer per question, all at once, each a new agent that starts from its question
|
|
19
9
|
* alone. Every explorer's usage counts in the run's, whether it answered or failed; a failure
|
|
@@ -23,21 +13,18 @@ async function explore(questions, explorer, signal, call) {
|
|
|
23
13
|
const answers = await Promise.allSettled(questions.map(async (question, index) => {
|
|
24
14
|
const subagent = index + 1;
|
|
25
15
|
const heading = `Explorer ${String(subagent)}: ${question}`;
|
|
26
|
-
const onProgress = (event) => {
|
|
27
|
-
call.onProgress({
|
|
28
|
-
type: 'subagent',
|
|
29
|
-
callId: call.callId,
|
|
30
|
-
subagent,
|
|
31
|
-
task: question,
|
|
32
|
-
event,
|
|
33
|
-
});
|
|
34
|
-
};
|
|
35
16
|
// Within the run that made the call, so the explorers' reads belong to it.
|
|
36
|
-
const options =
|
|
17
|
+
const options = subagentOptions(call, {
|
|
18
|
+
tool: 'explore',
|
|
19
|
+
name: 'explorer',
|
|
20
|
+
subagent,
|
|
21
|
+
task: question,
|
|
22
|
+
options: { signal },
|
|
23
|
+
});
|
|
37
24
|
try {
|
|
38
25
|
const { response, tokens } = await explorer().run(question, options);
|
|
39
26
|
call.spend(tokens, tokens === undefined);
|
|
40
|
-
return `${heading}\n${
|
|
27
|
+
return `${heading}\n${cutAnswer(response || 'It found nothing to report.')}`;
|
|
41
28
|
}
|
|
42
29
|
catch (error) {
|
|
43
30
|
if (!(error instanceof RecoverableError || error instanceof UnrecoverableError)) {
|