@mikode13/harness 4.0.0 → 5.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 +285 -20
- package/dist/agent/domain/agent.d.ts +12 -0
- package/dist/agent/domain/agent.js.map +1 -1
- package/dist/agent/domain/approval.d.ts +14 -0
- package/dist/agent/domain/approval.js +28 -3
- package/dist/agent/domain/approval.js.map +1 -1
- package/dist/agent/domain/runContext.d.ts +31 -0
- package/dist/agent/domain/runContext.js +47 -0
- package/dist/agent/domain/runContext.js.map +1 -0
- package/dist/diff/domain/unifiedDiff.d.ts +19 -0
- package/dist/diff/domain/unifiedDiff.js +166 -0
- package/dist/diff/domain/unifiedDiff.js.map +1 -0
- package/dist/engines/domain/model/historyFollower.d.ts +41 -0
- package/dist/engines/domain/model/historyFollower.js +158 -0
- package/dist/engines/domain/model/historyFollower.js.map +1 -0
- package/dist/engines/domain/model/llmAgent.d.ts +42 -4
- package/dist/engines/domain/model/llmAgent.js +224 -36
- package/dist/engines/domain/model/llmAgent.js.map +1 -1
- package/dist/factory/infrastructure/agentFactory.d.ts +14 -0
- package/dist/factory/infrastructure/agentFactory.js +20 -2
- package/dist/factory/infrastructure/agentFactory.js.map +1 -1
- package/dist/factory/infrastructure/agentLLMFactory.d.ts +119 -14
- package/dist/factory/infrastructure/agentLLMFactory.js +284 -57
- package/dist/factory/infrastructure/agentLLMFactory.js.map +1 -1
- package/dist/index.d.ts +11 -3
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/llm/domain/conversation.d.ts +44 -4
- package/dist/llm/domain/conversation.js +56 -7
- package/dist/llm/domain/conversation.js.map +1 -1
- package/dist/llm/domain/llm.d.ts +3 -1
- package/dist/llm/domain/llm.js.map +1 -1
- package/dist/llm/domain/tool.d.ts +13 -0
- package/dist/llm/domain/tool.js +12 -1
- package/dist/llm/domain/tool.js.map +1 -1
- package/dist/llm/infrastructure/claudeLLMClient.d.ts +3 -2
- package/dist/llm/infrastructure/claudeLLMClient.js +38 -10
- package/dist/llm/infrastructure/claudeLLMClient.js.map +1 -1
- package/dist/llm/infrastructure/openAILLMClient.d.ts +2 -1
- package/dist/llm/infrastructure/openAILLMClient.js +66 -16
- package/dist/llm/infrastructure/openAILLMClient.js.map +1 -1
- package/dist/orchestration/domain/model/orchestratorAgent.d.ts +11 -2
- package/dist/orchestration/domain/model/orchestratorAgent.js +43 -4
- package/dist/orchestration/domain/model/orchestratorAgent.js.map +1 -1
- package/dist/recovery/domain/fileChanges.d.ts +31 -0
- package/dist/recovery/domain/fileChanges.js +48 -0
- package/dist/recovery/domain/fileChanges.js.map +1 -0
- package/dist/recovery/domain/history.d.ts +114 -0
- package/dist/recovery/domain/history.js +62 -0
- package/dist/recovery/domain/history.js.map +1 -0
- package/dist/recovery/domain/recoveryStore.d.ts +233 -0
- package/dist/recovery/domain/recoveryStore.js +32 -0
- package/dist/recovery/domain/recoveryStore.js.map +1 -0
- package/dist/recovery/domain/retention.d.ts +33 -0
- package/dist/recovery/domain/retention.js +90 -0
- package/dist/recovery/domain/retention.js.map +1 -0
- package/dist/recovery/domain/runChanges.d.ts +37 -0
- package/dist/recovery/domain/runChanges.js +109 -0
- package/dist/recovery/domain/runChanges.js.map +1 -0
- package/dist/recovery/domain/runTree.d.ts +32 -0
- package/dist/recovery/domain/runTree.js +90 -0
- package/dist/recovery/domain/runTree.js.map +1 -0
- package/dist/recovery/infrastructure/createHistory.d.ts +11 -0
- package/dist/recovery/infrastructure/createHistory.js +17 -0
- package/dist/recovery/infrastructure/createHistory.js.map +1 -0
- package/dist/recovery/infrastructure/fileRecoveryStore.d.ts +133 -0
- package/dist/recovery/infrastructure/fileRecoveryStore.js +1043 -0
- package/dist/recovery/infrastructure/fileRecoveryStore.js.map +1 -0
- package/dist/recovery/infrastructure/workspaceFiles.d.ts +30 -0
- package/dist/recovery/infrastructure/workspaceFiles.js +125 -0
- package/dist/recovery/infrastructure/workspaceFiles.js.map +1 -0
- package/dist/tools/domain/accessPolicy.d.ts +30 -0
- package/dist/tools/domain/accessPolicy.js +11 -0
- package/dist/tools/domain/accessPolicy.js.map +1 -0
- package/dist/tools/domain/commands.d.ts +93 -0
- package/dist/tools/domain/commands.js +191 -0
- package/dist/tools/domain/commands.js.map +1 -0
- package/dist/tools/domain/editResponse.d.ts +14 -0
- package/dist/tools/domain/editResponse.js +79 -0
- package/dist/tools/domain/editResponse.js.map +1 -0
- package/dist/tools/domain/fileEdits.d.ts +64 -0
- package/dist/tools/domain/fileEdits.js +21 -0
- package/dist/tools/domain/fileEdits.js.map +1 -0
- package/dist/tools/domain/lineLength.d.ts +7 -0
- package/dist/tools/domain/lineLength.js +26 -0
- package/dist/tools/domain/lineLength.js.map +1 -0
- package/dist/tools/domain/preparedCall.d.ts +31 -0
- package/dist/tools/domain/preparedCall.js +14 -0
- package/dist/tools/domain/preparedCall.js.map +1 -0
- package/dist/tools/domain/readRegistry.d.ts +46 -0
- package/dist/tools/domain/readRegistry.js +62 -0
- package/dist/tools/domain/readRegistry.js.map +1 -0
- package/dist/tools/domain/textLines.d.ts +26 -0
- package/dist/tools/domain/textLines.js +44 -0
- package/dist/tools/domain/textLines.js.map +1 -0
- package/dist/tools/domain/v4aPatch.d.ts +17 -0
- package/dist/tools/domain/v4aPatch.js +236 -0
- package/dist/tools/domain/v4aPatch.js.map +1 -0
- package/dist/tools/infrastructure/boundedWorkspace.d.ts +11 -2
- package/dist/tools/infrastructure/boundedWorkspace.js +9 -1
- package/dist/tools/infrastructure/boundedWorkspace.js.map +1 -1
- package/dist/tools/infrastructure/commandCheckpoint.d.ts +55 -0
- package/dist/tools/infrastructure/commandCheckpoint.js +178 -0
- package/dist/tools/infrastructure/commandCheckpoint.js.map +1 -0
- package/dist/tools/infrastructure/commandProcess.d.ts +47 -0
- package/dist/tools/infrastructure/commandProcess.js +217 -0
- package/dist/tools/infrastructure/commandProcess.js.map +1 -0
- package/dist/tools/infrastructure/commandTool.d.ts +36 -0
- package/dist/tools/infrastructure/commandTool.js +254 -0
- package/dist/tools/infrastructure/commandTool.js.map +1 -0
- package/dist/tools/infrastructure/createWorkspace.d.ts +10 -0
- package/dist/tools/infrastructure/createWorkspace.js +11 -3
- package/dist/tools/infrastructure/createWorkspace.js.map +1 -1
- package/dist/tools/infrastructure/defineTool.d.ts +14 -0
- package/dist/tools/infrastructure/defineTool.js +28 -5
- package/dist/tools/infrastructure/defineTool.js.map +1 -1
- package/dist/tools/infrastructure/editFormats.d.ts +22 -0
- package/dist/tools/infrastructure/editFormats.js +211 -0
- package/dist/tools/infrastructure/editFormats.js.map +1 -0
- package/dist/tools/infrastructure/fileEditor.d.ts +64 -0
- package/dist/tools/infrastructure/fileEditor.js +306 -0
- package/dist/tools/infrastructure/fileEditor.js.map +1 -0
- package/dist/tools/infrastructure/fileTools.d.ts +81 -0
- package/dist/tools/infrastructure/fileTools.js +152 -0
- package/dist/tools/infrastructure/fileTools.js.map +1 -0
- package/dist/tools/infrastructure/gitIgnoreRules.d.ts +15 -0
- package/dist/tools/infrastructure/gitIgnoreRules.js +74 -0
- package/dist/tools/infrastructure/gitIgnoreRules.js.map +1 -0
- package/dist/tools/infrastructure/gitWorkspace.d.ts +1 -1
- package/dist/tools/infrastructure/gitWorkspace.js +1 -1
- package/dist/tools/infrastructure/gitWorkspace.js.map +1 -1
- package/dist/tools/infrastructure/ignoreFileChanges.d.ts +14 -0
- package/dist/tools/infrastructure/ignoreFileChanges.js +62 -0
- package/dist/tools/infrastructure/ignoreFileChanges.js.map +1 -0
- package/dist/tools/infrastructure/policyWorkspace.d.ts +52 -0
- package/dist/tools/infrastructure/policyWorkspace.js +32 -0
- package/dist/tools/infrastructure/policyWorkspace.js.map +1 -0
- package/dist/tools/infrastructure/ripgrepWorkspace.d.ts +2 -1
- package/dist/tools/infrastructure/ripgrepWorkspace.js +1 -1
- package/dist/tools/infrastructure/ripgrepWorkspace.js.map +1 -1
- package/dist/tools/infrastructure/rootsAccessPolicy.d.ts +100 -0
- package/dist/tools/infrastructure/rootsAccessPolicy.js +301 -0
- package/dist/tools/infrastructure/rootsAccessPolicy.js.map +1 -0
- package/dist/tools/infrastructure/runProcess.d.ts +4 -1
- package/dist/tools/infrastructure/runProcess.js +4 -2
- package/dist/tools/infrastructure/runProcess.js.map +1 -1
- package/dist/tools/infrastructure/secretPaths.d.ts +8 -0
- package/dist/tools/infrastructure/secretPaths.js +76 -0
- package/dist/tools/infrastructure/secretPaths.js.map +1 -0
- package/dist/tools/infrastructure/showChangesTool.d.ts +12 -0
- package/dist/tools/infrastructure/showChangesTool.js +43 -0
- package/dist/tools/infrastructure/showChangesTool.js.map +1 -0
- package/dist/tools/infrastructure/trackedEdits.d.ts +33 -0
- package/dist/tools/infrastructure/trackedEdits.js +70 -0
- package/dist/tools/infrastructure/trackedEdits.js.map +1 -0
- package/dist/tools/infrastructure/trackedReadFile.d.ts +21 -0
- package/dist/tools/infrastructure/trackedReadFile.js +78 -0
- package/dist/tools/infrastructure/trackedReadFile.js.map +1 -0
- package/dist/tools/infrastructure/workspaceTools.d.ts +28 -0
- package/dist/tools/infrastructure/workspaceTools.js +53 -40
- package/dist/tools/infrastructure/workspaceTools.js.map +1 -1
- package/dist/tools/infrastructure/workspaceWrites.d.ts +23 -0
- package/dist/tools/infrastructure/workspaceWrites.js +42 -0
- package/dist/tools/infrastructure/workspaceWrites.js.map +1 -0
- package/dist/tools/infrastructure/writeSession.d.ts +35 -0
- package/dist/tools/infrastructure/writeSession.js +81 -0
- package/dist/tools/infrastructure/writeSession.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,9 +39,11 @@ chat loop itself never knows the difference.
|
|
|
39
39
|
captured `session_id`, so the provider keeps the context.
|
|
40
40
|
- **The model API path**: `LLMAgent` calls the OpenAI Responses API or the
|
|
41
41
|
Anthropic Messages API and keeps the conversation itself, in a form that can move
|
|
42
|
-
between providers. It runs its own tool loop over the tools it is given
|
|
43
|
-
harness ships
|
|
44
|
-
|
|
42
|
+
between providers. It runs its own tool loop over the tools it is given. The
|
|
43
|
+
harness ships tools that read the repository (list, search, read), bounded by
|
|
44
|
+
`.gitignore`, and `createFileTools`, which also change files in each provider's
|
|
45
|
+
native format, every change recorded so it can be undone, and run the commands you
|
|
46
|
+
configure, such as the tests, recording what each one changed too.
|
|
45
47
|
- **`OrchestratorAgent`**: coordinates a planner, an executor, and a reviewer
|
|
46
48
|
(each an injected `Agent`) in a plan → execute → review loop. The reviewer's
|
|
47
49
|
decision is a Zod-validated structured `{decision, feedback}`, not free text —
|
|
@@ -112,6 +114,9 @@ internal modules through the `imports` field of its `package.json`, so a TypeScr
|
|
|
112
114
|
needs `moduleResolution` set to `node16`, `nodenext` or `bundler`; the legacy `node10`
|
|
113
115
|
resolution cannot read that field.
|
|
114
116
|
|
|
117
|
+
It supports macOS and Linux. Windows is not supported for now: the tools that write files,
|
|
118
|
+
undo a run and run commands are built and tested on macOS and Linux only.
|
|
119
|
+
|
|
115
120
|
An agent is built by a factory, then driven. The only output the package produces
|
|
116
121
|
on its own is diagnostic warnings on stderr, from a default logger that both
|
|
117
122
|
factories let you replace through their `logger` option:
|
|
@@ -152,6 +157,62 @@ example when the other one is out of quota.
|
|
|
152
157
|
a role left out keeps the harness's own, and the reviewer must still answer with
|
|
153
158
|
the JSON decision described on the option.
|
|
154
159
|
|
|
160
|
+
### Validating factory options
|
|
161
|
+
|
|
162
|
+
The provider-aware guards validate command-line strings before a factory is built. Use the
|
|
163
|
+
catalog for help text rather than copying the accepted values; choose the catalog and guards for
|
|
164
|
+
the path you are using:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import {
|
|
168
|
+
agentModels,
|
|
169
|
+
agentReasoningEfforts,
|
|
170
|
+
createAgent,
|
|
171
|
+
createLLMAgent,
|
|
172
|
+
isAgentProvider,
|
|
173
|
+
isLLMModelOf,
|
|
174
|
+
isLLMReasoningEffortOf,
|
|
175
|
+
isModelOf,
|
|
176
|
+
isReasoningEffortOf,
|
|
177
|
+
llmModels,
|
|
178
|
+
llmReasoningEfforts,
|
|
179
|
+
} from '@mikode13/harness';
|
|
180
|
+
|
|
181
|
+
const providerInput: string = process.argv[2] ?? '';
|
|
182
|
+
const modelInput: string = process.argv[3] ?? '';
|
|
183
|
+
const effortInput: string = process.argv[4] ?? '';
|
|
184
|
+
const useModelApi = false;
|
|
185
|
+
|
|
186
|
+
if (!isAgentProvider(providerInput)) throw new Error('Unknown provider');
|
|
187
|
+
|
|
188
|
+
if (useModelApi) {
|
|
189
|
+
if (
|
|
190
|
+
!isLLMModelOf(providerInput, modelInput) ||
|
|
191
|
+
!isLLMReasoningEffortOf(providerInput, effortInput)
|
|
192
|
+
) {
|
|
193
|
+
throw new Error(
|
|
194
|
+
`Expected model ${llmModels[providerInput].join(', ')} and effort ${llmReasoningEfforts[providerInput].join(', ')}`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
createLLMAgent(providerInput, {
|
|
198
|
+
model: modelInput,
|
|
199
|
+
reasoningEffort: effortInput,
|
|
200
|
+
systemPrompt: 'Be concise.',
|
|
201
|
+
});
|
|
202
|
+
} else {
|
|
203
|
+
if (!isModelOf(providerInput, modelInput) || !isReasoningEffortOf(providerInput, effortInput)) {
|
|
204
|
+
throw new Error(
|
|
205
|
+
`Expected model ${agentModels[providerInput].join(', ')} and effort ${agentReasoningEfforts[providerInput].join(', ')}`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
createAgent(providerInput, { model: modelInput, reasoningEffort: effortInput });
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
These guards describe the provider-wide catalogs. A client can still impose a model-specific
|
|
213
|
+
constraint, such as Claude Haiku accepting no reasoning effort, and the factory's client or
|
|
214
|
+
engine validation remains the final boundary for values that bypass the guards.
|
|
215
|
+
|
|
155
216
|
Every agent a factory returns already retries recoverable failures. A model or
|
|
156
217
|
reasoning effort the chosen provider does not support throws
|
|
157
218
|
`InvalidAgentConfigError` when the agent is built. Building a Codex agent throws
|
|
@@ -164,17 +225,16 @@ The two paths authenticate differently:
|
|
|
164
225
|
|
|
165
226
|
- `createAgent` and `createOrchestrator` use the Agent SDKs, which use the login of
|
|
166
227
|
the Codex CLI and of Claude Code, so a run counts against that subscription.
|
|
167
|
-
- `createLLMAgent` and
|
|
168
|
-
model APIs, which bill per token and need the key of each provider they use in
|
|
228
|
+
- `createLLMAgent` and every role of `createLLMOrchestrator`, the executor included, call
|
|
229
|
+
the model APIs, which bill per token and need the key of each provider they use in
|
|
169
230
|
the environment: `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, or both for the
|
|
170
231
|
orchestrator's default roles. A missing OpenAI key fails when the agent is built; a missing
|
|
171
232
|
Anthropic key fails its first run.
|
|
172
233
|
|
|
173
234
|
The Claude Agent SDK
|
|
174
235
|
[authenticates with `ANTHROPIC_API_KEY`](https://code.claude.com/docs/en/agent-sdk) whenever
|
|
175
|
-
it is set, so with that key in the environment a Claude agent from `createAgent
|
|
176
|
-
|
|
177
|
-
subscription.
|
|
236
|
+
it is set, so with that key in the environment a Claude agent from `createAgent` or `createOrchestrator`
|
|
237
|
+
bills the key instead of the subscription.
|
|
178
238
|
|
|
179
239
|
### Agents that own their conversation
|
|
180
240
|
|
|
@@ -224,18 +284,183 @@ await agent.run('Tidy the docs folder.', {
|
|
|
224
284
|
|
|
225
285
|
With no `approve`, a `destructive` call is denied. A denial is not a failure: the
|
|
226
286
|
model receives it, with the reason when there is one, and carries on. Progress
|
|
227
|
-
reports the call with `status: 'denied'`.
|
|
287
|
+
reports the call with `status: 'denied'`. An approver that throws, or answers
|
|
288
|
+
anything but a boolean `approved`, ends the run with an `UnrecoverableError`, unless
|
|
289
|
+
the run was cancelled, and the call never runs.
|
|
228
290
|
|
|
229
291
|
The harness keeps no state between runs, so "don't ask again" belongs to your
|
|
230
292
|
approver. `rememberApprovals(ask)` builds one: when `ask` answers
|
|
231
293
|
`{ approved: true, remember: true }`, later calls to that tool are allowed without
|
|
232
|
-
asking.
|
|
233
|
-
|
|
234
|
-
|
|
294
|
+
asking. An answer whose `approved` or `remember` is not a boolean throws, so it is
|
|
295
|
+
neither run nor remembered. Its `key` option decides what counts as the same call,
|
|
296
|
+
such as the tool and its path. The memory lives as long as the approver you created,
|
|
297
|
+
so create one per session, per user or per run.
|
|
298
|
+
|
|
299
|
+
### Agents that change files
|
|
300
|
+
|
|
301
|
+
`createFileTools` gives an agent the file tools of its provider, over the folders you
|
|
302
|
+
declare, each `read` or `write`:
|
|
303
|
+
|
|
304
|
+
- **OpenAI** gets `listFiles`, `searchText`, `readFile` and its native `apply_patch`.
|
|
305
|
+
- **Anthropic** gets `listFiles`, `searchText`, its native text editor (whose `view`
|
|
306
|
+
reads) and `delete_file`.
|
|
235
307
|
|
|
236
|
-
`
|
|
237
|
-
|
|
238
|
-
|
|
308
|
+
Give the agent the same `workspace`, so it follows what the history undoes:
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
import { createFileTools, createLLMAgent, type WorkspaceOptions } from '@mikode13/harness';
|
|
312
|
+
|
|
313
|
+
const workspace: WorkspaceOptions = {
|
|
314
|
+
roots: [
|
|
315
|
+
{ path: process.cwd(), access: 'write' },
|
|
316
|
+
{ path: '/srv/standards', access: 'read' },
|
|
317
|
+
],
|
|
318
|
+
};
|
|
319
|
+
const agent = createLLMAgent('openai', {
|
|
320
|
+
systemPrompt: 'You fix bugs, with a test for each.',
|
|
321
|
+
tools: await createFileTools('openai', workspace),
|
|
322
|
+
workspace,
|
|
323
|
+
});
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Every tool goes through one access policy:
|
|
327
|
+
|
|
328
|
+
- it refuses a path outside the roots;
|
|
329
|
+
- it refuses a file `.gitignore` excludes;
|
|
330
|
+
- it refuses git's metadata, the history itself, and a secret such as `.env`. Add more
|
|
331
|
+
secrets with `secrets.protect`, or open exact files with `secrets.allow`.
|
|
332
|
+
|
|
333
|
+
A change happens only in a `write` root, and only from a version of the file the agent
|
|
334
|
+
read: editing a file it never read is `READ_REQUIRED`, and editing one that changed since
|
|
335
|
+
is `STALE_FILE`. Every change is recorded before it is made, so the history can undo the
|
|
336
|
+
run.
|
|
337
|
+
|
|
338
|
+
With `workspace`, the agent's system prompt is followed by the roots and how to name a file
|
|
339
|
+
in them. After the user undoes runs, the agent's next run starts from its conversation as it
|
|
340
|
+
was then, with a note on what was undone and why. By default that note carries a summary
|
|
341
|
+
from the provider's cheap model, Claude Haiku or `gpt-5.6-luna`, billed with the run;
|
|
342
|
+
`summarizeUndone: false` keeps to the undone prompts. A run that changed a file names its
|
|
343
|
+
run in `AgentResponse.runId`. Progress reports each change as a unified `diff` on the tool's
|
|
344
|
+
`completed` event.
|
|
345
|
+
|
|
346
|
+
`createLLMOrchestrator()` takes the options of `createOrchestrator()` plus `workspace`,
|
|
347
|
+
`maxSteps`, `commands` and `summarizeUndone`, and returns a promise. Every role runs on the
|
|
348
|
+
model APIs:
|
|
349
|
+
|
|
350
|
+
- the planner and the reviewer read, with a `showChanges` tool that gives the current task's
|
|
351
|
+
changes as far as each may read them;
|
|
352
|
+
- the executor gets its provider's file tools. `autoApprove` reaches only the executor;
|
|
353
|
+
- each role gets the commands whose `roles` name it; a command names only the executor unless
|
|
354
|
+
it says otherwise.
|
|
355
|
+
|
|
356
|
+
Each role's run stops after a number of calls to the model: 80 for the executor and 65 for
|
|
357
|
+
the planner and the reviewer by default. `maxSteps` changes it, as one number for every role
|
|
358
|
+
or per role, with no upper bound:
|
|
359
|
+
|
|
360
|
+
```ts
|
|
361
|
+
createLLMOrchestrator({ workspace, maxSteps: { executor: 120 } });
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
A run also stops sooner when five answers in a row ask for exactly the same tool calls,
|
|
365
|
+
which means the model is stuck. `createLLMAgent` takes one number, 65 by default.
|
|
366
|
+
|
|
367
|
+
Without `workspace` it works on the current directory, read only. One run of the
|
|
368
|
+
orchestrator is one run of the history, every round included.
|
|
369
|
+
|
|
370
|
+
### Running the project's commands
|
|
371
|
+
|
|
372
|
+
With `commands`, `createFileTools` and `createLLMOrchestrator` add `runCommand`, which runs
|
|
373
|
+
one of the commands you configure, by name. Nothing goes through a shell, and the model
|
|
374
|
+
fills only the parameters a command declares:
|
|
375
|
+
|
|
376
|
+
```ts
|
|
377
|
+
const tools = await createFileTools('openai', workspace, {
|
|
378
|
+
commands: {
|
|
379
|
+
test: {
|
|
380
|
+
description: 'Runs the tests. Give the test files to run only those.',
|
|
381
|
+
executable: 'pnpm',
|
|
382
|
+
args: ['vitest', 'run', '--', { type: 'paths', name: 'files', description: 'Test files' }],
|
|
383
|
+
},
|
|
384
|
+
format: { description: 'Formats the repository', executable: 'pnpm', args: ['format'] },
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
- **Parameters:** `paths` inside the roots that the agent may read; a `choice` among the
|
|
390
|
+
values you list; or `text` that matches your pattern and never starts with `-`.
|
|
391
|
+
- **Approval:** every call asks the approver, which sees the exact program, arguments and
|
|
392
|
+
folder in `request.command`. `autoApprove` skips it.
|
|
393
|
+
- **Roles:** in `createLLMOrchestrator`, a command goes to the executor, or to the roles its
|
|
394
|
+
`roles` lists, such as `['planner', 'executor']` for one that reads an issue. Whoever runs it,
|
|
395
|
+
its files are captured around it. `createFileTools` offers every command.
|
|
396
|
+
- **Environment:** a command inherits only `PATH`, `HOME`, `TMPDIR`, `USER`, `SHELL`, `LANG`
|
|
397
|
+
and `LC_*`, plus its own `env`. The provider keys never reach it.
|
|
398
|
+
- **Limits:** 10 minutes, then its whole process group is stopped (`timeoutMs`). The model
|
|
399
|
+
reads the last 16 KB of its output (`outputBytes`).
|
|
400
|
+
- **History:** the files of the `write` roots are captured before each command and compared
|
|
401
|
+
after it. What it changed is recorded in the history, so `undo` takes it back.
|
|
402
|
+
- Not covered: what `.gitignore` excludes (`node_modules`, build output), secrets, files
|
|
403
|
+
outside the roots, the network and remote services.
|
|
404
|
+
- A timeout or a cancellation is never reported as a finished command.
|
|
405
|
+
- **Not on Windows yet.**
|
|
406
|
+
|
|
407
|
+
A command is trusted execution, not a sandbox. A test runs the repository's code, including
|
|
408
|
+
code the model just wrote, with everything your account can reach. The secret rules of the
|
|
409
|
+
file tools do not apply to it, so a command can print a secret into the model's context.
|
|
410
|
+
Configure only commands you would run yourself on that repository.
|
|
411
|
+
|
|
412
|
+
### Going back through what an agent wrote
|
|
413
|
+
|
|
414
|
+
The harness's write tools record every run that changes files, outside the repository,
|
|
415
|
+
under the platform's state directory. `createHistory` reads that record and moves the
|
|
416
|
+
workspace through it:
|
|
417
|
+
|
|
418
|
+
```ts
|
|
419
|
+
import { createHistory, historyStart } from '@mikode13/harness';
|
|
420
|
+
|
|
421
|
+
const history = await createHistory({ root: process.cwd() });
|
|
422
|
+
const { runId } = await agent.run('Rename the helper.', { signal: controller.signal });
|
|
423
|
+
|
|
424
|
+
if (runId) console.log(await history.changes(runId)); // a git-style diff
|
|
425
|
+
await history.undo({ reason: 'keep the old name' });
|
|
426
|
+
await history.redo();
|
|
427
|
+
await history.goTo(historyStart); // as it was before the agent's first run
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
`list()` gives every run kept, oldest first, with its parent, status and the files it
|
|
431
|
+
changed, and the run the workspace is at (`head`), absent at the start. Runs form a tree:
|
|
432
|
+
going back and writing again starts a new branch, and `goTo` reaches the old one. `redo`
|
|
433
|
+
returns towards where the workspace was most recently. A run's `runId` is in its
|
|
434
|
+
`AgentResponse` when it changed a file, and a run that failed or was cancelled after
|
|
435
|
+
writing is still listed, with that status. A run that changed nothing is not kept.
|
|
436
|
+
|
|
437
|
+
A move writes a file only if it still holds what the history expects. A file you
|
|
438
|
+
changed since is left as it is and named in the move's `conflicts`, and the move is
|
|
439
|
+
then not `complete`. The reason is kept with the move.
|
|
440
|
+
|
|
441
|
+
A move can be tied to where the workspace is. Pass `from`, a run or `historyStart`, and
|
|
442
|
+
the move happens only if the workspace is still there; otherwise it fails with
|
|
443
|
+
`WorkspaceMovedError` and changes nothing. Pass it when you showed the user what a move
|
|
444
|
+
will do, so a run that ends while they answer cannot make it do something else:
|
|
445
|
+
|
|
446
|
+
```ts
|
|
447
|
+
const { head } = await history.list();
|
|
448
|
+
// … show the user what undoing `head` does, and wait for a yes …
|
|
449
|
+
await history.undo({ from: head ?? historyStart, reason });
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
A consumer branches on five errors: `WorkspaceBusyError` while a run or another move
|
|
453
|
+
holds the workspace, `WorkspaceMovedError` when it is not at `from`,
|
|
454
|
+
`NothingToMoveError` with nothing to undo or redo,
|
|
455
|
+
`UnknownRunError` for a run the workspace never had, and `HistoryExpiredError` for one
|
|
456
|
+
retention took. The history keeps 25 runs per workspace. Older runs are chained into
|
|
457
|
+
the next, so `historyStart` always restores the original state. For a chained run,
|
|
458
|
+
the error's `keptIn` names the run that now holds its changes, and that run's
|
|
459
|
+
`absorbed` in `list()` names it.
|
|
460
|
+
|
|
461
|
+
The history is not version control. It holds only what the harness's own tools wrote:
|
|
462
|
+
not your edits, not other programs', and not the Agent SDK engines' changes. The lock
|
|
463
|
+
keeps a second harness run out, but not an editor.
|
|
239
464
|
|
|
240
465
|
## Tests
|
|
241
466
|
|
|
@@ -255,19 +480,59 @@ pnpm run dev
|
|
|
255
480
|
```
|
|
256
481
|
|
|
257
482
|
The agents work on the repository root, which is the directory the script runs in.
|
|
258
|
-
`pnpm run dev --llm`
|
|
483
|
+
`pnpm run dev --llm` runs all three roles on the model APIs instead, through
|
|
259
484
|
`createLLMOrchestrator`, and needs the API keys in the environment (see
|
|
260
485
|
[Authentication](#authentication)).
|
|
261
486
|
|
|
262
487
|
Type your prompt at `>`. Press Ctrl+C while idle at the prompt to exit; pressing
|
|
263
488
|
it while an agent is running cancels only that turn and returns to the prompt.
|
|
264
489
|
|
|
490
|
+
A line that starts with `/` is a command for the CLI, not a prompt. Four commands
|
|
491
|
+
move through the [history of what the agents wrote](#going-back-through-what-an-agent-wrote),
|
|
492
|
+
without the model:
|
|
493
|
+
|
|
494
|
+
- `/history` shows the tree of runs and marks where the workspace is.
|
|
495
|
+
`/history --files` adds the files each run changed.
|
|
496
|
+
- `/undo` goes back to the run before the current one.
|
|
497
|
+
- `/redo` goes forward again.
|
|
498
|
+
- `/goto <run | start>` goes to a run, named by its id or its last six characters, or
|
|
499
|
+
to `start`, before any run.
|
|
500
|
+
|
|
501
|
+
Each move says what it will do and changes nothing until you answer `y`. It then
|
|
502
|
+
asks why, and records the reason, if you give one, with the move. A move that left
|
|
503
|
+
files as they were, because they changed since, names them. With `--llm` the executor
|
|
504
|
+
writes to the current directory through the harness's tools, so its runs are in that
|
|
505
|
+
history. The Agent SDK path changes files through its own tools, which the history does not
|
|
506
|
+
record.
|
|
507
|
+
|
|
265
508
|
The CLI asks in the terminal before a destructive tool call runs: yes, always for
|
|
266
|
-
that tool until the CLI exits, or no with an optional reason for the model.
|
|
267
|
-
|
|
268
|
-
|
|
509
|
+
that tool until the CLI exits, or no with an optional reason for the model. With
|
|
510
|
+
piped input nobody can answer, so such a call is denied: a line written for a later
|
|
511
|
+
question never approves one. Piped lines answer the prompt and the history commands in
|
|
512
|
+
order. With `--llm`, the executor asks before a destructive change, such as one that would
|
|
513
|
+
make `.gitignore` hide less.
|
|
514
|
+
|
|
515
|
+
With `--llm`, the agents can also [run commands](#running-the-projects-commands) you
|
|
516
|
+
configure for the repository, in `~/.config/mikode-harness/config.json` (or under
|
|
517
|
+
`$XDG_CONFIG_HOME`). The file maps each repository's real path to its `commands` and,
|
|
518
|
+
optionally, its `maxSteps`. It lives outside every repository, so neither a cloned repository
|
|
519
|
+
nor the agent decides what runs, or for how long:
|
|
520
|
+
|
|
521
|
+
```json
|
|
522
|
+
{
|
|
523
|
+
"/Users/me/code/app": {
|
|
524
|
+
"commands": {
|
|
525
|
+
"test": { "description": "Runs the tests", "executable": "pnpm", "args": ["test"] }
|
|
526
|
+
},
|
|
527
|
+
"maxSteps": { "executor": 120 }
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
The CLI shows each command as the line it will run before asking, and "always" allows
|
|
533
|
+
that command again, not every command.
|
|
269
534
|
|
|
270
|
-
`cli/cli.ts` currently enables `autoApprove` for its
|
|
535
|
+
`cli/cli.ts` currently enables `autoApprove` for its Agent SDK agents, not for `--llm`.
|
|
271
536
|
This maps to each provider's permission-bypass mode and grants those processes
|
|
272
537
|
unrestricted command access. Keep it disabled when the host may receive untrusted
|
|
273
538
|
prompts, or provide an approval workflow from the entry point.
|
|
@@ -14,6 +14,12 @@ export interface AgentResponse {
|
|
|
14
14
|
tokens?: Tokens;
|
|
15
15
|
/** Wall-clock seconds the consumer waited for the run, retries and every role included. */
|
|
16
16
|
duration: number;
|
|
17
|
+
/**
|
|
18
|
+
* The run the workspace's history recorded, when this run wrote through the harness's write
|
|
19
|
+
* tools. Absent for a run that wrote nothing, and from the Agent SDK engines, whose changes
|
|
20
|
+
* the history does not record.
|
|
21
|
+
*/
|
|
22
|
+
runId?: string;
|
|
17
23
|
}
|
|
18
24
|
export type Callback = (item: ProgressEvent) => void;
|
|
19
25
|
/** What a run needs besides its prompt. Decorators pass it on whole, so a new field reaches every agent. */
|
|
@@ -71,6 +77,12 @@ export type ProgressEvent = {
|
|
|
71
77
|
* it never ran because no one allowed it, with no `in_progress` before it.
|
|
72
78
|
*/
|
|
73
79
|
status: 'in_progress' | 'completed' | 'error' | 'denied';
|
|
80
|
+
/**
|
|
81
|
+
* On `completed`, for a call that changed a file through the harness's write tools: the
|
|
82
|
+
* change as a unified diff, for the person watching. Narration only; the run's whole
|
|
83
|
+
* record is the history's `changes`.
|
|
84
|
+
*/
|
|
85
|
+
diff?: string;
|
|
74
86
|
} | {
|
|
75
87
|
type: 'todoList';
|
|
76
88
|
items: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent.js","sourceRoot":"","sources":["../../../src/agent/domain/agent.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"agent.js","sourceRoot":"","sources":["../../../src/agent/domain/agent.ts"],"names":[],"mappings":"AAyCA,gDAAgD;AAChD,MAAM,CAAC,MAAM,cAAc,GAAa,GAAG,EAAE,CAAC,SAAS,CAAC","sourcesContent":["import type { Tokens } from '#src/shared/domain/tokens';\nimport type { Approver } from './approval.ts';\n\n/**\n * How a run ended when it did not fail. Every token a run spends travels with its end: here\n * when it finishes, in `RecoverableError.tokens` or `UnrecoverableError.tokens` when it fails.\n */\nexport interface AgentResponse {\n\t/** The final text. Empty when the run produced none, for example a turn that only edited files. */\n\tresponse: string;\n\t/**\n\t * Missing when any call in the run completed without reporting usage, which is not the same\n\t * as zero: a partial sum would look complete.\n\t */\n\ttokens?: Tokens;\n\t/** Wall-clock seconds the consumer waited for the run, retries and every role included. */\n\tduration: number;\n\t/**\n\t * The run the workspace's history recorded, when this run wrote through the harness's write\n\t * tools. Absent for a run that wrote nothing, and from the Agent SDK engines, whose changes\n\t * the history does not record.\n\t */\n\trunId?: string;\n}\n\nexport type Callback = (item: ProgressEvent) => void;\n\n/** What a run needs besides its prompt. Decorators pass it on whole, so a new field reaches every agent. */\nexport interface RunOptions {\n\t/** Cancels the run, which then rejects with the signal's `AbortError`. */\n\tsignal: AbortSignal;\n\t/** Receives the run's live activity. Without it, the run is silent. */\n\tonProgress?: Callback | undefined;\n\t/**\n\t * Asked before a model-backed agent runs a `destructive` tool call. Without it, such a call\n\t * is denied. An agent built with `autoApprove` never asks, and the Agent SDK engines ignore\n\t * it: their own permission systems decide.\n\t */\n\tapprove?: Approver | undefined;\n}\n\n/** The `onProgress` of a run that gave none. */\nexport const ignoreProgress: Callback = () => undefined;\n\n/**\n * Live activity during a run, for narration only: the result travels in `AgentResponse`.\n * New event types can arrive in a minor release, so render the types you know and ignore\n * the rest instead of switching over them exhaustively with a `never` check.\n */\nexport type ProgressEvent =\n\t| { type: 'command'; command: string; exitCode?: number }\n\t| { type: 'reasoning'; message: string }\n\t| { type: 'search'; query: string }\n\t| { type: 'fileChange'; changes: { path: string; kind: 'add' | 'update' | 'delete' }[] }\n\t| { type: 'mcpTool'; server: string; tool: string; status: string }\n\t| { type: 'agentMessage'; message: string }\n\t/** `id` pairs a call's end with its start when one step calls the same tool more than once. */\n\t| {\n\t\t\ttype: 'tool';\n\t\t\tid: string;\n\t\t\tname: string;\n\t\t\t/**\n\t\t\t * `in_progress` only once the call is about to run, after any approval. `denied` when\n\t\t\t * it never ran because no one allowed it, with no `in_progress` before it.\n\t\t\t */\n\t\t\tstatus: 'in_progress' | 'completed' | 'error' | 'denied';\n\t\t\t/**\n\t\t\t * On `completed`, for a call that changed a file through the harness's write tools: the\n\t\t\t * change as a unified diff, for the person watching. Narration only; the run's whole\n\t\t\t * record is the history's `changes`.\n\t\t\t */\n\t\t\tdiff?: string;\n\t }\n\t| { type: 'todoList'; items: { text: string; completed: boolean }[] }\n\t| { type: 'turnStarted' }\n\t| { type: 'turnEnded' };\n\n/**\n * The contract every engine (CodexAgent, ClaudeAgent, future providers) and every\n * decorator (RetryingAgent, OrchestratorAgent) is built against.\n *\n * Implementers MUST only ever reject with `RecoverableError` or `UnrecoverableError`\n * (see src/shared/domain/errors.ts) — never a raw SDK error, a plain `Error`, or anything else leaked\n * unclassified. Every consumer of `Agent` (RetryingAgent's retry decision,\n * OrchestratorAgent's failure handling) `instanceof`-checks against those two types to\n * decide what to do next; a leaked, unclassified error bypasses that decision\n * entirely — it gets retried when it shouldn't be, or crashes a run that a retry\n * would have recovered. Wrap every call into the underlying SDK so nothing escapes\n * unclassified, including failures the SDK itself doesn't model as a domain error\n * (network errors, malformed responses, etc.); `classifyProviderFailure` does this.\n *\n * Cancellation is the one exception: an `AbortError` must propagate unchanged, because\n * consumers check for it before either error type.\n */\nexport interface Agent {\n\trun(prompt: string, options: RunOptions): Promise<AgentResponse>;\n}\n"]}
|
|
@@ -5,12 +5,21 @@
|
|
|
5
5
|
* - `destructive` loses something, as with overwriting or deleting a file.
|
|
6
6
|
*/
|
|
7
7
|
export type ToolRisk = 'safe' | 'mutating' | 'destructive';
|
|
8
|
+
/** A program a tool call will run, exactly as it will run: no shell reads it. */
|
|
9
|
+
export interface CommandLine {
|
|
10
|
+
executable: string;
|
|
11
|
+
args: string[];
|
|
12
|
+
/** Where it runs, as a real path. */
|
|
13
|
+
cwd: string;
|
|
14
|
+
}
|
|
8
15
|
/** A tool call waiting for someone to allow it. */
|
|
9
16
|
export interface ApprovalRequest {
|
|
10
17
|
tool: string;
|
|
11
18
|
/** What the model sent, unvalidated: the tool validates it only if the call is allowed. */
|
|
12
19
|
input: unknown;
|
|
13
20
|
risk: ToolRisk;
|
|
21
|
+
/** For a call that runs a configured command: the program, worked out from the input. */
|
|
22
|
+
command?: CommandLine;
|
|
14
23
|
}
|
|
15
24
|
/** A denial's `reason` reaches the model, so it can try something else. */
|
|
16
25
|
export type ApprovalDecision = {
|
|
@@ -42,3 +51,8 @@ export type RememberableDecision = {
|
|
|
42
51
|
export declare function rememberApprovals(ask: (request: ApprovalRequest, signal: AbortSignal) => RememberableDecision | Promise<RememberableDecision>, { key }?: {
|
|
43
52
|
key?: (request: ApprovalRequest) => string;
|
|
44
53
|
}): Approver;
|
|
54
|
+
/**
|
|
55
|
+
* Reads an approver's answer without trusting its type, since a JavaScript consumer can return
|
|
56
|
+
* anything: only an `approved` that is a boolean decides, and anything else throws.
|
|
57
|
+
*/
|
|
58
|
+
export declare function readDecision(answer: unknown): ApprovalDecision;
|
|
@@ -10,14 +10,39 @@ export function rememberApprovals(ask, { key = request => request.tool } = {}) {
|
|
|
10
10
|
const id = key(request);
|
|
11
11
|
if (allowed.has(id))
|
|
12
12
|
return { approved: true };
|
|
13
|
-
const
|
|
13
|
+
const answer = await ask(request, signal);
|
|
14
14
|
// The agent discards an answer that arrives after a cancellation, so it must not be kept.
|
|
15
15
|
signal.throwIfAborted();
|
|
16
|
+
// Read before keeping anything: a truthy `'false'` would otherwise be allowed and remembered.
|
|
17
|
+
const decision = readDecision(answer);
|
|
16
18
|
if (!decision.approved)
|
|
17
19
|
return decision;
|
|
18
|
-
if (
|
|
20
|
+
if (readRemember(answer))
|
|
19
21
|
allowed.add(id);
|
|
20
|
-
return
|
|
22
|
+
return decision;
|
|
21
23
|
};
|
|
22
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Reads an approver's answer without trusting its type, since a JavaScript consumer can return
|
|
27
|
+
* anything: only an `approved` that is a boolean decides, and anything else throws.
|
|
28
|
+
*/
|
|
29
|
+
export function readDecision(answer) {
|
|
30
|
+
if (typeof answer !== 'object' || answer === null) {
|
|
31
|
+
throw new TypeError('The approver answered without a decision');
|
|
32
|
+
}
|
|
33
|
+
const { approved, reason } = answer;
|
|
34
|
+
if (typeof approved !== 'boolean') {
|
|
35
|
+
throw new TypeError('The approver answered without a boolean `approved`');
|
|
36
|
+
}
|
|
37
|
+
return approved
|
|
38
|
+
? { approved }
|
|
39
|
+
: { approved, reason: typeof reason === 'string' ? reason : undefined };
|
|
40
|
+
}
|
|
41
|
+
function readRemember(answer) {
|
|
42
|
+
const { remember } = answer;
|
|
43
|
+
if (remember !== undefined && typeof remember !== 'boolean') {
|
|
44
|
+
throw new TypeError('The approver answered without a boolean `remember`');
|
|
45
|
+
}
|
|
46
|
+
return remember === true;
|
|
47
|
+
}
|
|
23
48
|
//# sourceMappingURL=approval.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"approval.js","sourceRoot":"","sources":["../../../src/agent/domain/approval.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"approval.js","sourceRoot":"","sources":["../../../src/agent/domain/approval.ts"],"names":[],"mappings":"AA6CA;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAChC,GAGyD,EACzD,EAAE,GAAG,GAAG,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,KAAqD,EAAE;IAEtF,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAElC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE;QAChC,MAAM,EAAE,GAAG,GAAG,CAAC,OAAO,CAAC,CAAC;QACxB,IAAI,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;QAE/C,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC1C,0FAA0F;QAC1F,MAAM,CAAC,cAAc,EAAE,CAAC;QACxB,8FAA8F;QAC9F,MAAM,QAAQ,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;QACtC,IAAI,CAAC,QAAQ,CAAC,QAAQ;YAAE,OAAO,QAAQ,CAAC;QACxC,IAAI,YAAY,CAAC,MAAM,CAAC;YAAE,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC1C,OAAO,QAAQ,CAAC;IACjB,CAAC,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,MAAe;IAC3C,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACnD,MAAM,IAAI,SAAS,CAAC,0CAA0C,CAAC,CAAC;IACjE,CAAC;IACD,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAkD,CAAC;IAChF,IAAI,OAAO,QAAQ,KAAK,SAAS,EAAE,CAAC;QACnC,MAAM,IAAI,SAAS,CAAC,oDAAoD,CAAC,CAAC;IAC3E,CAAC;IAED,OAAO,QAAQ;QACd,CAAC,CAAC,EAAE,QAAQ,EAAE;QACd,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC;AAC1E,CAAC;AAED,SAAS,YAAY,CAAC,MAAe;IACpC,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAgC,CAAC;IACtD,IAAI,QAAQ,KAAK,SAAS,IAAI,OAAO,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC7D,MAAM,IAAI,SAAS,CAAC,oDAAoD,CAAC,CAAC;IAC3E,CAAC;IACD,OAAO,QAAQ,KAAK,IAAI,CAAC;AAC1B,CAAC","sourcesContent":["/**\n * How much one tool call can hurt, judged by the tool from that call's input:\n * - `safe` changes nothing, as with a read.\n * - `mutating` makes a change git can undo, as with creating a file.\n * - `destructive` loses something, as with overwriting or deleting a file.\n */\nexport type ToolRisk = 'safe' | 'mutating' | 'destructive';\n\n/** A program a tool call will run, exactly as it will run: no shell reads it. */\nexport interface CommandLine {\n\texecutable: string;\n\targs: string[];\n\t/** Where it runs, as a real path. */\n\tcwd: string;\n}\n\n/** A tool call waiting for someone to allow it. */\nexport interface ApprovalRequest {\n\ttool: string;\n\t/** What the model sent, unvalidated: the tool validates it only if the call is allowed. */\n\tinput: unknown;\n\trisk: ToolRisk;\n\t/** For a call that runs a configured command: the program, worked out from the input. */\n\tcommand?: CommandLine;\n}\n\n/** A denial's `reason` reaches the model, so it can try something else. */\nexport type ApprovalDecision =\n\t{ approved: true } | { approved: false; reason?: string | undefined };\n\n/**\n * Decides one tool call at a time. A consumer that wants \"don't ask again\" remembers its own\n * answers, so the harness keeps no session state. A throw ends the run as an\n * `UnrecoverableError`.\n */\nexport type Approver = (\n\trequest: ApprovalRequest,\n\tsignal: AbortSignal,\n) => ApprovalDecision | Promise<ApprovalDecision>;\n\n/** What `rememberApprovals` asks for: `remember` allows the same call again without asking. */\nexport type RememberableDecision =\n\t| { approved: true; remember?: boolean | undefined }\n\t| { approved: false; reason?: string | undefined };\n\n/**\n * Wraps `ask` so a call the user allowed with `remember` is not asked about again. `key` decides\n * what counts as the same call; it defaults to the tool's name, so \"always\" allows every\n * destructive call to that tool. The memory lives in the returned approver, so its lifetime is\n * the consumer's choice: one per session, one per user, or one per run.\n */\nexport function rememberApprovals(\n\task: (\n\t\trequest: ApprovalRequest,\n\t\tsignal: AbortSignal,\n\t) => RememberableDecision | Promise<RememberableDecision>,\n\t{ key = request => request.tool }: { key?: (request: ApprovalRequest) => string } = {},\n): Approver {\n\tconst allowed = new Set<string>();\n\n\treturn async (request, signal) => {\n\t\tconst id = key(request);\n\t\tif (allowed.has(id)) return { approved: true };\n\n\t\tconst answer = await ask(request, signal);\n\t\t// The agent discards an answer that arrives after a cancellation, so it must not be kept.\n\t\tsignal.throwIfAborted();\n\t\t// Read before keeping anything: a truthy `'false'` would otherwise be allowed and remembered.\n\t\tconst decision = readDecision(answer);\n\t\tif (!decision.approved) return decision;\n\t\tif (readRemember(answer)) allowed.add(id);\n\t\treturn decision;\n\t};\n}\n\n/**\n * Reads an approver's answer without trusting its type, since a JavaScript consumer can return\n * anything: only an `approved` that is a boolean decides, and anything else throws.\n */\nexport function readDecision(answer: unknown): ApprovalDecision {\n\tif (typeof answer !== 'object' || answer === null) {\n\t\tthrow new TypeError('The approver answered without a decision');\n\t}\n\tconst { approved, reason } = answer as { approved?: unknown; reason?: unknown };\n\tif (typeof approved !== 'boolean') {\n\t\tthrow new TypeError('The approver answered without a boolean `approved`');\n\t}\n\n\treturn approved\n\t\t? { approved }\n\t\t: { approved, reason: typeof reason === 'string' ? reason : undefined };\n}\n\nfunction readRemember(answer: unknown): boolean {\n\tconst { remember } = answer as { remember?: unknown };\n\tif (remember !== undefined && typeof remember !== 'boolean') {\n\t\tthrow new TypeError('The approver answered without a boolean `remember`');\n\t}\n\treturn remember === true;\n}\n"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { RunOptions } from './agent.ts';
|
|
2
|
+
export type RunEnd = 'completed' | 'failed' | 'cancelled';
|
|
3
|
+
/**
|
|
4
|
+
* What one top-level run shares with every agent and tool inside it, across all its roles,
|
|
5
|
+
* rounds and retries, such as the record of what it wrote. Whatever a tool starts for the run
|
|
6
|
+
* registers how to end it, and the agent that created the context ends it with the run.
|
|
7
|
+
*
|
|
8
|
+
* Internal: it travels in `RunOptions` under a symbol, so decorators, which pass the options on
|
|
9
|
+
* whole, carry it without knowing it exists, and no consumer can see or set it.
|
|
10
|
+
*/
|
|
11
|
+
export declare class RunContext {
|
|
12
|
+
private readonly finishers;
|
|
13
|
+
/**
|
|
14
|
+
* The run the workspace's history recorded for this run, once it first wrote. The object is
|
|
15
|
+
* shared, not copied: a turn recorded before the first write still learns its run.
|
|
16
|
+
*/
|
|
17
|
+
readonly historyRun: {
|
|
18
|
+
runId?: string;
|
|
19
|
+
};
|
|
20
|
+
/** Ends something the run started, when the run ends. */
|
|
21
|
+
onFinish(finisher: (end: RunEnd) => Promise<void>): void;
|
|
22
|
+
/**
|
|
23
|
+
* Ends everything registered, all of it even when one fails, and then throws the first
|
|
24
|
+
* failure.
|
|
25
|
+
*/
|
|
26
|
+
finish(end: RunEnd): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
/** The context of the run these options belong to, if an outer agent created one. */
|
|
29
|
+
export declare function runContextOf(options: RunOptions): RunContext | undefined;
|
|
30
|
+
/** The same options, carrying `context` for every agent the run reaches. */
|
|
31
|
+
export declare function withRunContext(options: RunOptions, context: RunContext): RunOptions;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What one top-level run shares with every agent and tool inside it, across all its roles,
|
|
3
|
+
* rounds and retries, such as the record of what it wrote. Whatever a tool starts for the run
|
|
4
|
+
* registers how to end it, and the agent that created the context ends it with the run.
|
|
5
|
+
*
|
|
6
|
+
* Internal: it travels in `RunOptions` under a symbol, so decorators, which pass the options on
|
|
7
|
+
* whole, carry it without knowing it exists, and no consumer can see or set it.
|
|
8
|
+
*/
|
|
9
|
+
export class RunContext {
|
|
10
|
+
finishers = [];
|
|
11
|
+
/**
|
|
12
|
+
* The run the workspace's history recorded for this run, once it first wrote. The object is
|
|
13
|
+
* shared, not copied: a turn recorded before the first write still learns its run.
|
|
14
|
+
*/
|
|
15
|
+
historyRun = {};
|
|
16
|
+
/** Ends something the run started, when the run ends. */
|
|
17
|
+
onFinish(finisher) {
|
|
18
|
+
this.finishers.push(finisher);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Ends everything registered, all of it even when one fails, and then throws the first
|
|
22
|
+
* failure.
|
|
23
|
+
*/
|
|
24
|
+
async finish(end) {
|
|
25
|
+
const failures = [];
|
|
26
|
+
for (const finisher of this.finishers.splice(0)) {
|
|
27
|
+
try {
|
|
28
|
+
await finisher(end);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
failures.push(error);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (failures.length > 0)
|
|
35
|
+
throw failures[0];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const runContextKey = Symbol('mikode-harness.runContext');
|
|
39
|
+
/** The context of the run these options belong to, if an outer agent created one. */
|
|
40
|
+
export function runContextOf(options) {
|
|
41
|
+
return options[runContextKey];
|
|
42
|
+
}
|
|
43
|
+
/** The same options, carrying `context` for every agent the run reaches. */
|
|
44
|
+
export function withRunContext(options, context) {
|
|
45
|
+
return { ...options, [runContextKey]: context };
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=runContext.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runContext.js","sourceRoot":"","sources":["../../../src/agent/domain/runContext.ts"],"names":[],"mappings":"AAIA;;;;;;;GAOG;AACH,MAAM,OAAO,UAAU;IACL,SAAS,GAAuC,EAAE,CAAC;IAEpE;;;OAGG;IACM,UAAU,GAAuB,EAAE,CAAC;IAE7C,yDAAyD;IACzD,QAAQ,CAAC,QAAwC;QAChD,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC/B,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,MAAM,CAAC,GAAW;QACvB,MAAM,QAAQ,GAAc,EAAE,CAAC;QAC/B,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;YACjD,IAAI,CAAC;gBACJ,MAAM,QAAQ,CAAC,GAAG,CAAC,CAAC;YACrB,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAChB,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACtB,CAAC;QACF,CAAC;QACD,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;YAAE,MAAM,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC5C,CAAC;CACD;AAED,MAAM,aAAa,GAAG,MAAM,CAAC,2BAA2B,CAAC,CAAC;AAM1D,qFAAqF;AACrF,MAAM,UAAU,YAAY,CAAC,OAAmB;IAC/C,OAAQ,OAAgC,CAAC,aAAa,CAAC,CAAC;AACzD,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,cAAc,CAAC,OAAmB,EAAE,OAAmB;IACtE,OAAO,EAAE,GAAG,OAAO,EAAE,CAAC,aAAa,CAAC,EAAE,OAAO,EAA0B,CAAC;AACzE,CAAC","sourcesContent":["import type { RunOptions } from './agent.ts';\n\nexport type RunEnd = 'completed' | 'failed' | 'cancelled';\n\n/**\n * What one top-level run shares with every agent and tool inside it, across all its roles,\n * rounds and retries, such as the record of what it wrote. Whatever a tool starts for the run\n * registers how to end it, and the agent that created the context ends it with the run.\n *\n * Internal: it travels in `RunOptions` under a symbol, so decorators, which pass the options on\n * whole, carry it without knowing it exists, and no consumer can see or set it.\n */\nexport class RunContext {\n\tprivate readonly finishers: ((end: RunEnd) => Promise<void>)[] = [];\n\n\t/**\n\t * The run the workspace's history recorded for this run, once it first wrote. The object is\n\t * shared, not copied: a turn recorded before the first write still learns its run.\n\t */\n\treadonly historyRun: { runId?: string } = {};\n\n\t/** Ends something the run started, when the run ends. */\n\tonFinish(finisher: (end: RunEnd) => Promise<void>): void {\n\t\tthis.finishers.push(finisher);\n\t}\n\n\t/**\n\t * Ends everything registered, all of it even when one fails, and then throws the first\n\t * failure.\n\t */\n\tasync finish(end: RunEnd): Promise<void> {\n\t\tconst failures: unknown[] = [];\n\t\tfor (const finisher of this.finishers.splice(0)) {\n\t\t\ttry {\n\t\t\t\tawait finisher(end);\n\t\t\t} catch (error) {\n\t\t\t\tfailures.push(error);\n\t\t\t}\n\t\t}\n\t\tif (failures.length > 0) throw failures[0];\n\t}\n}\n\nconst runContextKey = Symbol('mikode-harness.runContext');\n\ninterface ContextualRunOptions extends RunOptions {\n\t[runContextKey]?: RunContext;\n}\n\n/** The context of the run these options belong to, if an outer agent created one. */\nexport function runContextOf(options: RunOptions): RunContext | undefined {\n\treturn (options as ContextualRunOptions)[runContextKey];\n}\n\n/** The same options, carrying `context` for every agent the run reaches. */\nexport function withRunContext(options: RunOptions, context: RunContext): RunOptions {\n\treturn { ...options, [runContextKey]: context } as ContextualRunOptions;\n}\n"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Line diffs in the unified format, with Myers' algorithm. This module imports nothing, so it can
|
|
3
|
+
* move to a package of its own unchanged.
|
|
4
|
+
*/
|
|
5
|
+
export interface LineDiffOptions {
|
|
6
|
+
/** Unchanged lines shown around each change. Defaults to 3, as in git. */
|
|
7
|
+
context?: number;
|
|
8
|
+
/**
|
|
9
|
+
* The most lines two texts may differ by before the diff stops looking for the smallest
|
|
10
|
+
* change and shows the differing middle as removed, then added. Memory grows with its square.
|
|
11
|
+
*/
|
|
12
|
+
maxDistance?: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The hunks that turn `before` into `after`, each with its `@@` header, or `''` when the texts
|
|
16
|
+
* are equal. Lines are compared whole, a `\r` included, and a last line without a newline
|
|
17
|
+
* differs from the same line with one, as git shows it.
|
|
18
|
+
*/
|
|
19
|
+
export declare function unifiedDiff(before: string, after: string, options?: LineDiffOptions): string;
|