@mikode13/harness 4.0.1 → 5.1.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 +299 -17
- package/dist/agent/domain/agent.d.ts +32 -1
- package/dist/agent/domain/agent.js.map +1 -1
- package/dist/agent/domain/approval.d.ts +9 -0
- 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 +287 -31
- 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 +13 -5
- 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 +67 -17
- package/dist/llm/infrastructure/openAILLMClient.js.map +1 -1
- package/dist/orchestration/domain/model/orchestratorAgent.d.ts +17 -3
- package/dist/orchestration/domain/model/orchestratorAgent.js +65 -8
- 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 +44 -0
- package/dist/tools/domain/preparedCall.js +26 -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/tool.d.ts +14 -0
- package/dist/tools/domain/tool.js +7 -1
- package/dist/tools/domain/tool.js.map +1 -1
- 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 +285 -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 +18 -2
- package/dist/tools/infrastructure/defineTool.js +52 -6
- package/dist/tools/infrastructure/defineTool.js.map +1 -1
- package/dist/tools/infrastructure/editFormats.d.ts +22 -0
- package/dist/tools/infrastructure/editFormats.js +243 -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 +44 -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 +79 -0
- package/dist/tools/infrastructure/trackedReadFile.js.map +1 -0
- package/dist/tools/infrastructure/workspaceTools.d.ts +34 -0
- package/dist/tools/infrastructure/workspaceTools.js +69 -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:
|
|
@@ -148,10 +153,75 @@ releases, so render the ones you know and ignore the rest. Swapping the agent fo
|
|
|
148
153
|
reviewer workflow, changes nothing else in the snippet above.
|
|
149
154
|
`createOrchestrator({ provider: 'anthropic' })` runs every role on one provider, for
|
|
150
155
|
example when the other one is out of quota.
|
|
156
|
+
|
|
157
|
+
The optional `agent` field on an event identifies the orchestrator role that produced
|
|
158
|
+
it (`planner`, `executor`, or `reviewer`). The optional `summary` on a `tool` event
|
|
159
|
+
is narration only: it describes the model's call and is not the tool result. A failed
|
|
160
|
+
configured command may also carry its optional `exitCode`; its output and changes
|
|
161
|
+
still arrive in the tool result. A role label is applied by the orchestrator,
|
|
162
|
+
including retries and later rounds, so a nested agent cannot accidentally attribute
|
|
163
|
+
its events to another role. Consumers should continue to handle events without these
|
|
164
|
+
optional fields.
|
|
151
165
|
`systemPrompts: { planner, executor, reviewer }` replaces a role's instructions;
|
|
152
166
|
a role left out keeps the harness's own, and the reviewer must still answer with
|
|
153
167
|
the JSON decision described on the option.
|
|
154
168
|
|
|
169
|
+
### Validating factory options
|
|
170
|
+
|
|
171
|
+
The provider-aware guards validate command-line strings before a factory is built. Use the
|
|
172
|
+
catalog for help text rather than copying the accepted values; choose the catalog and guards for
|
|
173
|
+
the path you are using:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import {
|
|
177
|
+
agentModels,
|
|
178
|
+
agentReasoningEfforts,
|
|
179
|
+
createAgent,
|
|
180
|
+
createLLMAgent,
|
|
181
|
+
isAgentProvider,
|
|
182
|
+
isLLMModelOf,
|
|
183
|
+
isLLMReasoningEffortOf,
|
|
184
|
+
isModelOf,
|
|
185
|
+
isReasoningEffortOf,
|
|
186
|
+
llmModels,
|
|
187
|
+
llmReasoningEfforts,
|
|
188
|
+
} from '@mikode13/harness';
|
|
189
|
+
|
|
190
|
+
const providerInput: string = process.argv[2] ?? '';
|
|
191
|
+
const modelInput: string = process.argv[3] ?? '';
|
|
192
|
+
const effortInput: string = process.argv[4] ?? '';
|
|
193
|
+
const useModelApi = false;
|
|
194
|
+
|
|
195
|
+
if (!isAgentProvider(providerInput)) throw new Error('Unknown provider');
|
|
196
|
+
|
|
197
|
+
if (useModelApi) {
|
|
198
|
+
if (
|
|
199
|
+
!isLLMModelOf(providerInput, modelInput) ||
|
|
200
|
+
!isLLMReasoningEffortOf(providerInput, effortInput)
|
|
201
|
+
) {
|
|
202
|
+
throw new Error(
|
|
203
|
+
`Expected model ${llmModels[providerInput].join(', ')} and effort ${llmReasoningEfforts[providerInput].join(', ')}`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
createLLMAgent(providerInput, {
|
|
207
|
+
model: modelInput,
|
|
208
|
+
reasoningEffort: effortInput,
|
|
209
|
+
systemPrompt: 'Be concise.',
|
|
210
|
+
});
|
|
211
|
+
} else {
|
|
212
|
+
if (!isModelOf(providerInput, modelInput) || !isReasoningEffortOf(providerInput, effortInput)) {
|
|
213
|
+
throw new Error(
|
|
214
|
+
`Expected model ${agentModels[providerInput].join(', ')} and effort ${agentReasoningEfforts[providerInput].join(', ')}`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
createAgent(providerInput, { model: modelInput, reasoningEffort: effortInput });
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
These guards describe the provider-wide catalogs. A client can still impose a model-specific
|
|
222
|
+
constraint, such as Claude Haiku accepting no reasoning effort, and the factory's client or
|
|
223
|
+
engine validation remains the final boundary for values that bypass the guards.
|
|
224
|
+
|
|
155
225
|
Every agent a factory returns already retries recoverable failures. A model or
|
|
156
226
|
reasoning effort the chosen provider does not support throws
|
|
157
227
|
`InvalidAgentConfigError` when the agent is built. Building a Codex agent throws
|
|
@@ -164,24 +234,26 @@ The two paths authenticate differently:
|
|
|
164
234
|
|
|
165
235
|
- `createAgent` and `createOrchestrator` use the Agent SDKs, which use the login of
|
|
166
236
|
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
|
|
237
|
+
- `createLLMAgent` and every role of `createLLMOrchestrator`, the executor included, call
|
|
238
|
+
the model APIs, which bill per token and need the key of each provider they use in
|
|
169
239
|
the environment: `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, or both for the
|
|
170
240
|
orchestrator's default roles. A missing OpenAI key fails when the agent is built; a missing
|
|
171
241
|
Anthropic key fails its first run.
|
|
172
242
|
|
|
173
243
|
The Claude Agent SDK
|
|
174
244
|
[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.
|
|
245
|
+
it is set, so with that key in the environment a Claude agent from `createAgent` or `createOrchestrator`
|
|
246
|
+
bills the key instead of the subscription.
|
|
178
247
|
|
|
179
248
|
### Agents that own their conversation
|
|
180
249
|
|
|
181
250
|
`createLLMAgent` takes a system prompt and the tools the model may call, and
|
|
182
251
|
returns the same `Agent`. `defineTool` builds a tool from a Zod 4 schema; a field the
|
|
183
252
|
model may leave out is `.nullable()`, not optional, because both APIs call tools in
|
|
184
|
-
strict mode
|
|
253
|
+
strict mode. An optional `summarize` function receives the same input after Zod
|
|
254
|
+
validation and supplies the tool's short progress narration. It is called once per
|
|
255
|
+
model call; its output is never sent to the provider, and a throwing summarizer
|
|
256
|
+
ends the run as an `UnrecoverableError` before the tool is prepared or executed:
|
|
185
257
|
|
|
186
258
|
```ts
|
|
187
259
|
import {
|
|
@@ -197,6 +269,7 @@ const now = defineTool({
|
|
|
197
269
|
description: 'The current time, in ISO 8601.',
|
|
198
270
|
input: z.object({}),
|
|
199
271
|
risk: 'safe',
|
|
272
|
+
summarize: () => 'current time',
|
|
200
273
|
execute: async () => new Date().toISOString(),
|
|
201
274
|
});
|
|
202
275
|
|
|
@@ -236,9 +309,171 @@ neither run nor remembered. Its `key` option decides what counts as the same cal
|
|
|
236
309
|
such as the tool and its path. The memory lives as long as the approver you created,
|
|
237
310
|
so create one per session, per user or per run.
|
|
238
311
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
312
|
+
### Agents that change files
|
|
313
|
+
|
|
314
|
+
`createFileTools` gives an agent the file tools of its provider, over the folders you
|
|
315
|
+
declare, each `read` or `write`:
|
|
316
|
+
|
|
317
|
+
- **OpenAI** gets `listFiles`, `searchText`, `readFile` and its native `apply_patch`.
|
|
318
|
+
- **Anthropic** gets `listFiles`, `searchText`, its native text editor (whose `view`
|
|
319
|
+
reads) and `delete_file`.
|
|
320
|
+
|
|
321
|
+
Give the agent the same `workspace`, so it follows what the history undoes:
|
|
322
|
+
|
|
323
|
+
```ts
|
|
324
|
+
import { createFileTools, createLLMAgent, type WorkspaceOptions } from '@mikode13/harness';
|
|
325
|
+
|
|
326
|
+
const workspace: WorkspaceOptions = {
|
|
327
|
+
roots: [
|
|
328
|
+
{ path: process.cwd(), access: 'write' },
|
|
329
|
+
{ path: '/srv/standards', access: 'read' },
|
|
330
|
+
],
|
|
331
|
+
};
|
|
332
|
+
const agent = createLLMAgent('openai', {
|
|
333
|
+
systemPrompt: 'You fix bugs, with a test for each.',
|
|
334
|
+
tools: await createFileTools('openai', workspace),
|
|
335
|
+
workspace,
|
|
336
|
+
});
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Every tool goes through one access policy:
|
|
340
|
+
|
|
341
|
+
- it refuses a path outside the roots;
|
|
342
|
+
- it refuses a file `.gitignore` excludes;
|
|
343
|
+
- it refuses git's metadata, the history itself, and a secret such as `.env`. Add more
|
|
344
|
+
secrets with `secrets.protect`, or open exact files with `secrets.allow`.
|
|
345
|
+
|
|
346
|
+
A change happens only in a `write` root, and only from a version of the file the agent
|
|
347
|
+
read: editing a file it never read is `READ_REQUIRED`, and editing one that changed since
|
|
348
|
+
is `STALE_FILE`. Every change is recorded before it is made, so the history can undo the
|
|
349
|
+
run.
|
|
350
|
+
|
|
351
|
+
With `workspace`, the agent's system prompt is followed by the roots and how to name a file
|
|
352
|
+
in them. After the user undoes runs, the agent's next run starts from its conversation as it
|
|
353
|
+
was then, with a note on what was undone and why. By default that note carries a summary
|
|
354
|
+
from the provider's cheap model, Claude Haiku or `gpt-5.6-luna`, billed with the run;
|
|
355
|
+
`summarizeUndone: false` keeps to the undone prompts. A run that changed a file names its
|
|
356
|
+
run in `AgentResponse.runId`. Progress reports each change as a unified `diff` on the tool's
|
|
357
|
+
`completed` event.
|
|
358
|
+
|
|
359
|
+
`createLLMOrchestrator()` takes the options of `createOrchestrator()` plus `workspace`,
|
|
360
|
+
`maxSteps`, `commands` and `summarizeUndone`, and returns a promise. Every role runs on the
|
|
361
|
+
model APIs:
|
|
362
|
+
|
|
363
|
+
- the planner and the reviewer read, with a `showChanges` tool that gives the current task's
|
|
364
|
+
changes as far as each may read them;
|
|
365
|
+
- the executor gets its provider's file tools. `autoApprove` reaches only the executor;
|
|
366
|
+
- each role gets the commands whose `roles` name it; a command names only the executor unless
|
|
367
|
+
it says otherwise.
|
|
368
|
+
|
|
369
|
+
Each role's run stops after a number of calls to the model: 80 for the executor and 65 for
|
|
370
|
+
the planner and the reviewer by default. `maxSteps` changes it, as one number for every role
|
|
371
|
+
or per role, with no upper bound:
|
|
372
|
+
|
|
373
|
+
```ts
|
|
374
|
+
createLLMOrchestrator({ workspace, maxSteps: { executor: 120 } });
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
A run also stops sooner when five answers in a row ask for exactly the same tool calls,
|
|
378
|
+
which means the model is stuck. `createLLMAgent` takes one number, 65 by default.
|
|
379
|
+
|
|
380
|
+
Without `workspace` it works on the current directory, read only. One run of the
|
|
381
|
+
orchestrator is one run of the history, every round included.
|
|
382
|
+
|
|
383
|
+
### Running the project's commands
|
|
384
|
+
|
|
385
|
+
With `commands`, `createFileTools` and `createLLMOrchestrator` add `runCommand`, which runs
|
|
386
|
+
one of the commands you configure, by name. Nothing goes through a shell, and the model
|
|
387
|
+
fills only the parameters a command declares:
|
|
388
|
+
|
|
389
|
+
```ts
|
|
390
|
+
const tools = await createFileTools('openai', workspace, {
|
|
391
|
+
commands: {
|
|
392
|
+
test: {
|
|
393
|
+
description: 'Runs the tests. Give the test files to run only those.',
|
|
394
|
+
executable: 'pnpm',
|
|
395
|
+
args: ['vitest', 'run', '--', { type: 'paths', name: 'files', description: 'Test files' }],
|
|
396
|
+
},
|
|
397
|
+
format: { description: 'Formats the repository', executable: 'pnpm', args: ['format'] },
|
|
398
|
+
},
|
|
399
|
+
});
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
- **Parameters:** `paths` inside the roots that the agent may read; a `choice` among the
|
|
403
|
+
values you list; or `text` that matches your pattern and never starts with `-`.
|
|
404
|
+
- **Approval:** every call asks the approver, which sees the exact program, arguments and
|
|
405
|
+
folder in `request.command`. `autoApprove` skips it.
|
|
406
|
+
- **Roles:** in `createLLMOrchestrator`, a command goes to the executor, or to the roles its
|
|
407
|
+
`roles` lists, such as `['planner', 'executor']` for one that reads an issue. Whoever runs it,
|
|
408
|
+
its files are captured around it. `createFileTools` offers every command.
|
|
409
|
+
- **Environment:** a command inherits only `PATH`, `HOME`, `TMPDIR`, `USER`, `SHELL`, `LANG`
|
|
410
|
+
and `LC_*`, plus its own `env`. The provider keys never reach it.
|
|
411
|
+
- **Limits:** 10 minutes, then its whole process group is stopped (`timeoutMs`). The model
|
|
412
|
+
reads the last 16 KB of its output (`outputBytes`).
|
|
413
|
+
- **History:** the files of the `write` roots are captured before each command and compared
|
|
414
|
+
after it. What it changed is recorded in the history, so `undo` takes it back.
|
|
415
|
+
- Not covered: what `.gitignore` excludes (`node_modules`, build output), secrets, files
|
|
416
|
+
outside the roots, the network and remote services.
|
|
417
|
+
- A timeout or a cancellation is never reported as a finished command.
|
|
418
|
+
- **Not on Windows yet.**
|
|
419
|
+
|
|
420
|
+
A command is trusted execution, not a sandbox. A test runs the repository's code, including
|
|
421
|
+
code the model just wrote, with everything your account can reach. The secret rules of the
|
|
422
|
+
file tools do not apply to it, so a command can print a secret into the model's context.
|
|
423
|
+
Configure only commands you would run yourself on that repository.
|
|
424
|
+
|
|
425
|
+
### Going back through what an agent wrote
|
|
426
|
+
|
|
427
|
+
The harness's write tools record every run that changes files, outside the repository,
|
|
428
|
+
under the platform's state directory. `createHistory` reads that record and moves the
|
|
429
|
+
workspace through it:
|
|
430
|
+
|
|
431
|
+
```ts
|
|
432
|
+
import { createHistory, historyStart } from '@mikode13/harness';
|
|
433
|
+
|
|
434
|
+
const history = await createHistory({ root: process.cwd() });
|
|
435
|
+
const { runId } = await agent.run('Rename the helper.', { signal: controller.signal });
|
|
436
|
+
|
|
437
|
+
if (runId) console.log(await history.changes(runId)); // a git-style diff
|
|
438
|
+
await history.undo({ reason: 'keep the old name' });
|
|
439
|
+
await history.redo();
|
|
440
|
+
await history.goTo(historyStart); // as it was before the agent's first run
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
`list()` gives every run kept, oldest first, with its parent, status and the files it
|
|
444
|
+
changed, and the run the workspace is at (`head`), absent at the start. Runs form a tree:
|
|
445
|
+
going back and writing again starts a new branch, and `goTo` reaches the old one. `redo`
|
|
446
|
+
returns towards where the workspace was most recently. A run's `runId` is in its
|
|
447
|
+
`AgentResponse` when it changed a file, and a run that failed or was cancelled after
|
|
448
|
+
writing is still listed, with that status. A run that changed nothing is not kept.
|
|
449
|
+
|
|
450
|
+
A move writes a file only if it still holds what the history expects. A file you
|
|
451
|
+
changed since is left as it is and named in the move's `conflicts`, and the move is
|
|
452
|
+
then not `complete`. The reason is kept with the move.
|
|
453
|
+
|
|
454
|
+
A move can be tied to where the workspace is. Pass `from`, a run or `historyStart`, and
|
|
455
|
+
the move happens only if the workspace is still there; otherwise it fails with
|
|
456
|
+
`WorkspaceMovedError` and changes nothing. Pass it when you showed the user what a move
|
|
457
|
+
will do, so a run that ends while they answer cannot make it do something else:
|
|
458
|
+
|
|
459
|
+
```ts
|
|
460
|
+
const { head } = await history.list();
|
|
461
|
+
// … show the user what undoing `head` does, and wait for a yes …
|
|
462
|
+
await history.undo({ from: head ?? historyStart, reason });
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
A consumer branches on five errors: `WorkspaceBusyError` while a run or another move
|
|
466
|
+
holds the workspace, `WorkspaceMovedError` when it is not at `from`,
|
|
467
|
+
`NothingToMoveError` with nothing to undo or redo,
|
|
468
|
+
`UnknownRunError` for a run the workspace never had, and `HistoryExpiredError` for one
|
|
469
|
+
retention took. The history keeps 25 runs per workspace. Older runs are chained into
|
|
470
|
+
the next, so `historyStart` always restores the original state. For a chained run,
|
|
471
|
+
the error's `keptIn` names the run that now holds its changes, and that run's
|
|
472
|
+
`absorbed` in `list()` names it.
|
|
473
|
+
|
|
474
|
+
The history is not version control. It holds only what the harness's own tools wrote:
|
|
475
|
+
not your edits, not other programs', and not the Agent SDK engines' changes. The lock
|
|
476
|
+
keeps a second harness run out, but not an editor.
|
|
242
477
|
|
|
243
478
|
## Tests
|
|
244
479
|
|
|
@@ -258,19 +493,66 @@ pnpm run dev
|
|
|
258
493
|
```
|
|
259
494
|
|
|
260
495
|
The agents work on the repository root, which is the directory the script runs in.
|
|
261
|
-
`pnpm run dev --llm`
|
|
496
|
+
`pnpm run dev --llm` runs all three roles on the model APIs instead, through
|
|
262
497
|
`createLLMOrchestrator`, and needs the API keys in the environment (see
|
|
263
498
|
[Authentication](#authentication)).
|
|
264
499
|
|
|
265
500
|
Type your prompt at `>`. Press Ctrl+C while idle at the prompt to exit; pressing
|
|
266
501
|
it while an agent is running cancels only that turn and returns to the prompt.
|
|
267
502
|
|
|
503
|
+
A line that starts with `/` is a command for the CLI, not a prompt. Four commands
|
|
504
|
+
move through the [history of what the agents wrote](#going-back-through-what-an-agent-wrote),
|
|
505
|
+
without the model:
|
|
506
|
+
|
|
507
|
+
- `/history` shows the tree of runs and marks where the workspace is.
|
|
508
|
+
`/history --files` adds the files each run changed.
|
|
509
|
+
- `/undo` goes back to the run before the current one.
|
|
510
|
+
- `/redo` goes forward again.
|
|
511
|
+
- `/goto <run | start>` goes to a run, named by its id or its last six characters, or
|
|
512
|
+
to `start`, before any run.
|
|
513
|
+
|
|
514
|
+
Each move says what it will do and changes nothing until you answer `y`. It then
|
|
515
|
+
asks why, and records the reason, if you give one, with the move. A move that left
|
|
516
|
+
files as they were, because they changed since, names them. With `--llm` the executor
|
|
517
|
+
writes to the current directory through the harness's tools, so its runs are in that
|
|
518
|
+
history. The Agent SDK path changes files through its own tools, which the history does not
|
|
519
|
+
record.
|
|
520
|
+
|
|
268
521
|
The CLI asks in the terminal before a destructive tool call runs: yes, always for
|
|
269
|
-
that tool until the CLI exits, or no with an optional reason for the model.
|
|
270
|
-
|
|
271
|
-
|
|
522
|
+
that tool until the CLI exits, or no with an optional reason for the model. With
|
|
523
|
+
piped input nobody can answer, so such a call is denied: a line written for a later
|
|
524
|
+
question never approves one. Piped lines answer the prompt and the history commands in
|
|
525
|
+
order. With `--llm`, the executor asks before a destructive change, such as one that would
|
|
526
|
+
make `.gitignore` hide less.
|
|
527
|
+
|
|
528
|
+
With `--llm`, the agents can also [run commands](#running-the-projects-commands) you
|
|
529
|
+
configure for the repository, in `~/.config/mikode-harness/config.json` (or under
|
|
530
|
+
`$XDG_CONFIG_HOME`). The file maps each repository's real path to its `commands` and,
|
|
531
|
+
optionally, its `maxSteps`. It lives outside every repository, so neither a cloned repository
|
|
532
|
+
nor the agent decides what runs, or for how long:
|
|
533
|
+
|
|
534
|
+
```json
|
|
535
|
+
{
|
|
536
|
+
"/Users/me/code/app": {
|
|
537
|
+
"commands": {
|
|
538
|
+
"test": { "description": "Runs the tests", "executable": "pnpm", "args": ["test"] }
|
|
539
|
+
},
|
|
540
|
+
"maxSteps": { "executor": 120 }
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
```
|
|
272
544
|
|
|
273
|
-
|
|
545
|
+
The CLI shows each command as the line it will run before asking, and "always" allows
|
|
546
|
+
that command again, not every command. Tool calls finish as one dimmed line with an
|
|
547
|
+
icon, role, tool name, summary and duration; a failed configured command includes
|
|
548
|
+
`exit N` beside its duration, while a command event also includes its exit code.
|
|
549
|
+
A denied call is marked `denied`. Messages remain normal text under a bold role
|
|
550
|
+
heading. Reasoning is dim italic and complete by default; pass
|
|
551
|
+
`--thinking-headings` to show only Markdown bold headings as `∴ …`. A tool start
|
|
552
|
+
updates the spinner rather than printing a second line, and cancelling a turn clears
|
|
553
|
+
its pending spinner state.
|
|
554
|
+
|
|
555
|
+
`cli/cli.ts` currently enables `autoApprove` for its Agent SDK agents, not for `--llm`.
|
|
274
556
|
This maps to each provider's permission-bypass mode and grants those processes
|
|
275
557
|
unrestricted command access. Keep it disabled when the host may receive untrusted
|
|
276
558
|
prompts, or provide an approval workflow from the entry point.
|
|
@@ -14,9 +14,18 @@ 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
|
+
/**
|
|
26
|
+
* What a run needs besides its prompt. RetryingAgent and InstructedAgent pass it on whole;
|
|
27
|
+
* OrchestratorAgent copies its own enumerable fields and replaces only `onProgress` per role.
|
|
28
|
+
*/
|
|
20
29
|
export interface RunOptions {
|
|
21
30
|
/** Cancels the run, which then rejects with the signal's `AbortError`. */
|
|
22
31
|
signal: AbortSignal;
|
|
@@ -40,48 +49,70 @@ export type ProgressEvent = {
|
|
|
40
49
|
type: 'command';
|
|
41
50
|
command: string;
|
|
42
51
|
exitCode?: number;
|
|
52
|
+
agent?: ProgressAgent;
|
|
43
53
|
} | {
|
|
44
54
|
type: 'reasoning';
|
|
45
55
|
message: string;
|
|
56
|
+
agent?: ProgressAgent;
|
|
46
57
|
} | {
|
|
47
58
|
type: 'search';
|
|
48
59
|
query: string;
|
|
60
|
+
agent?: ProgressAgent;
|
|
49
61
|
} | {
|
|
50
62
|
type: 'fileChange';
|
|
51
63
|
changes: {
|
|
52
64
|
path: string;
|
|
53
65
|
kind: 'add' | 'update' | 'delete';
|
|
54
66
|
}[];
|
|
67
|
+
agent?: ProgressAgent;
|
|
55
68
|
} | {
|
|
56
69
|
type: 'mcpTool';
|
|
57
70
|
server: string;
|
|
58
71
|
tool: string;
|
|
59
72
|
status: string;
|
|
73
|
+
agent?: ProgressAgent;
|
|
60
74
|
} | {
|
|
61
75
|
type: 'agentMessage';
|
|
62
76
|
message: string;
|
|
77
|
+
agent?: ProgressAgent;
|
|
63
78
|
}
|
|
64
79
|
/** `id` pairs a call's end with its start when one step calls the same tool more than once. */
|
|
65
80
|
| {
|
|
66
81
|
type: 'tool';
|
|
67
82
|
id: string;
|
|
68
83
|
name: string;
|
|
84
|
+
agent?: ProgressAgent;
|
|
85
|
+
/** A short, tool-owned description of the call. Narration only. */
|
|
86
|
+
summary?: string;
|
|
87
|
+
/** The exit code of a configured command that ended with an error. Narration only. */
|
|
88
|
+
exitCode?: number;
|
|
69
89
|
/**
|
|
70
90
|
* `in_progress` only once the call is about to run, after any approval. `denied` when
|
|
71
91
|
* it never ran because no one allowed it, with no `in_progress` before it.
|
|
72
92
|
*/
|
|
73
93
|
status: 'in_progress' | 'completed' | 'error' | 'denied';
|
|
94
|
+
/**
|
|
95
|
+
* On `completed`, for a call that changed a file through the harness's write tools: the
|
|
96
|
+
* change as a unified diff, for the person watching. Narration only; the run's whole
|
|
97
|
+
* record is the history's `changes`.
|
|
98
|
+
*/
|
|
99
|
+
diff?: string;
|
|
74
100
|
} | {
|
|
75
101
|
type: 'todoList';
|
|
76
102
|
items: {
|
|
77
103
|
text: string;
|
|
78
104
|
completed: boolean;
|
|
79
105
|
}[];
|
|
106
|
+
agent?: ProgressAgent;
|
|
80
107
|
} | {
|
|
81
108
|
type: 'turnStarted';
|
|
109
|
+
agent?: ProgressAgent;
|
|
82
110
|
} | {
|
|
83
111
|
type: 'turnEnded';
|
|
112
|
+
agent?: ProgressAgent;
|
|
84
113
|
};
|
|
114
|
+
/** A role in the fixed planner → executor → reviewer workflow. */
|
|
115
|
+
export type ProgressAgent = 'planner' | 'executor' | 'reviewer';
|
|
85
116
|
/**
|
|
86
117
|
* The contract every engine (CodexAgent, ClaudeAgent, future providers) and every
|
|
87
118
|
* decorator (RetryingAgent, OrchestratorAgent) is built against.
|
|
@@ -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":"AA4CA,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/**\n * What a run needs besides its prompt. RetryingAgent and InstructedAgent pass it on whole;\n * OrchestratorAgent copies its own enumerable fields and replaces only `onProgress` per role.\n */\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; agent?: ProgressAgent }\n\t| { type: 'reasoning'; message: string; agent?: ProgressAgent }\n\t| { type: 'search'; query: string; agent?: ProgressAgent }\n\t| {\n\t\t\ttype: 'fileChange';\n\t\t\tchanges: { path: string; kind: 'add' | 'update' | 'delete' }[];\n\t\t\tagent?: ProgressAgent;\n\t }\n\t| { type: 'mcpTool'; server: string; tool: string; status: string; agent?: ProgressAgent }\n\t| { type: 'agentMessage'; message: string; agent?: ProgressAgent }\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\tagent?: ProgressAgent;\n\t\t\t/** A short, tool-owned description of the call. Narration only. */\n\t\t\tsummary?: string;\n\t\t\t/** The exit code of a configured command that ended with an error. Narration only. */\n\t\t\texitCode?: number;\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 }[]; agent?: ProgressAgent }\n\t| { type: 'turnStarted'; agent?: ProgressAgent }\n\t| { type: 'turnEnded'; agent?: ProgressAgent };\n\n/** A role in the fixed planner → executor → reviewer workflow. */\nexport type ProgressAgent = 'planner' | 'executor' | 'reviewer';\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 = {
|
|
@@ -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"]}
|