amness 0.0.0-stage → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +237 -2
- package/dist/cli/acpcli.d.ts +7 -0
- package/dist/cli/acpcli.js +14 -0
- package/dist/cli/amness.d.ts +2 -0
- package/dist/cli/amness.js +694 -0
- package/dist/cli/benchcli.d.ts +5 -0
- package/dist/cli/benchcli.js +160 -0
- package/dist/cli/config.d.ts +79 -0
- package/dist/cli/config.js +111 -0
- package/dist/cli/demo.d.ts +13 -0
- package/dist/cli/demo.js +44 -0
- package/dist/cli/detect.d.ts +2 -0
- package/dist/cli/detect.js +20 -0
- package/dist/cli/evalcli.d.ts +15 -0
- package/dist/cli/evalcli.js +198 -0
- package/dist/cli/format.d.ts +5 -0
- package/dist/cli/format.js +55 -0
- package/dist/cli/headless.d.ts +2 -0
- package/dist/cli/headless.js +169 -0
- package/dist/cli/keystore.d.ts +9 -0
- package/dist/cli/keystore.js +108 -0
- package/dist/cli/model.d.ts +11 -0
- package/dist/cli/model.js +19 -0
- package/dist/cli/plugincli.d.ts +2 -0
- package/dist/cli/plugincli.js +176 -0
- package/dist/cli/schedulecli.d.ts +2 -0
- package/dist/cli/schedulecli.js +66 -0
- package/dist/cli/state.d.ts +11 -0
- package/dist/cli/state.js +19 -0
- package/dist/cli/tui/ansi.d.ts +38 -0
- package/dist/cli/tui/ansi.js +142 -0
- package/dist/cli/tui/app.d.ts +328 -0
- package/dist/cli/tui/app.js +1358 -0
- package/dist/cli/tui/blocks.d.ts +151 -0
- package/dist/cli/tui/blocks.js +128 -0
- package/dist/cli/tui/brand.d.ts +9 -0
- package/dist/cli/tui/brand.js +16 -0
- package/dist/cli/tui/canvas.d.ts +92 -0
- package/dist/cli/tui/canvas.js +199 -0
- package/dist/cli/tui/cards.d.ts +44 -0
- package/dist/cli/tui/cards.js +215 -0
- package/dist/cli/tui/clipboard.d.ts +5 -0
- package/dist/cli/tui/clipboard.js +29 -0
- package/dist/cli/tui/commands.d.ts +10 -0
- package/dist/cli/tui/commands.js +650 -0
- package/dist/cli/tui/complete.d.ts +39 -0
- package/dist/cli/tui/complete.js +113 -0
- package/dist/cli/tui/custom.d.ts +9 -0
- package/dist/cli/tui/custom.js +34 -0
- package/dist/cli/tui/diffview.d.ts +25 -0
- package/dist/cli/tui/diffview.js +130 -0
- package/dist/cli/tui/editor.d.ts +60 -0
- package/dist/cli/tui/editor.js +230 -0
- package/dist/cli/tui/full.d.ts +86 -0
- package/dist/cli/tui/full.js +630 -0
- package/dist/cli/tui/keys.d.ts +31 -0
- package/dist/cli/tui/keys.js +134 -0
- package/dist/cli/tui/markdown.d.ts +37 -0
- package/dist/cli/tui/markdown.js +155 -0
- package/dist/cli/tui/mentions.d.ts +15 -0
- package/dist/cli/tui/mentions.js +48 -0
- package/dist/cli/tui/modal.d.ts +26 -0
- package/dist/cli/tui/modal.js +107 -0
- package/dist/cli/tui/overlays.d.ts +98 -0
- package/dist/cli/tui/overlays.js +237 -0
- package/dist/cli/tui/pager.d.ts +15 -0
- package/dist/cli/tui/pager.js +66 -0
- package/dist/cli/tui/pricing.d.ts +9 -0
- package/dist/cli/tui/pricing.js +23 -0
- package/dist/cli/tui/rich.d.ts +16 -0
- package/dist/cli/tui/rich.js +72 -0
- package/dist/cli/tui/screen.d.ts +30 -0
- package/dist/cli/tui/screen.js +62 -0
- package/dist/cli/tui/session.d.ts +77 -0
- package/dist/cli/tui/session.js +103 -0
- package/dist/cli/tui/sidebar.d.ts +84 -0
- package/dist/cli/tui/sidebar.js +114 -0
- package/dist/cli/tui/text.d.ts +15 -0
- package/dist/cli/tui/text.js +94 -0
- package/dist/cli/tui/theme.d.ts +59 -0
- package/dist/cli/tui/theme.js +148 -0
- package/dist/cli/tui/themes.d.ts +56 -0
- package/dist/cli/tui/themes.js +80 -0
- package/dist/cli/tui/tokens.d.ts +39 -0
- package/dist/cli/tui/tokens.js +71 -0
- package/dist/cli/tui/view.d.ts +60 -0
- package/dist/cli/tui/view.js +178 -0
- package/dist/cli/ui.d.ts +34 -0
- package/dist/cli/ui.js +224 -0
- package/dist/src/acp.d.ts +28 -0
- package/dist/src/acp.js +111 -0
- package/dist/src/agents.d.ts +23 -0
- package/dist/src/agents.js +71 -0
- package/dist/src/anthropic.d.ts +30 -0
- package/dist/src/anthropic.js +91 -0
- package/dist/src/approver.d.ts +15 -0
- package/dist/src/approver.js +40 -0
- package/dist/src/bench.d.ts +67 -0
- package/dist/src/bench.js +201 -0
- package/dist/src/bundle.d.ts +36 -0
- package/dist/src/bundle.js +55 -0
- package/dist/src/chaos.d.ts +15 -0
- package/dist/src/chaos.js +26 -0
- package/dist/src/code.d.ts +187 -0
- package/dist/src/code.js +590 -0
- package/dist/src/codeindex.d.ts +65 -0
- package/dist/src/codeindex.js +300 -0
- package/dist/src/deep.d.ts +62 -0
- package/dist/src/deep.js +177 -0
- package/dist/src/diff.d.ts +8 -0
- package/dist/src/diff.js +74 -0
- package/dist/src/dynamic.d.ts +42 -0
- package/dist/src/dynamic.js +133 -0
- package/dist/src/embed.d.ts +8 -0
- package/dist/src/embed.js +25 -0
- package/dist/src/evalcases.d.ts +11 -0
- package/dist/src/evalcases.js +196 -0
- package/dist/src/evals.d.ts +197 -0
- package/dist/src/evals.js +340 -0
- package/dist/src/gitflow.d.ts +27 -0
- package/dist/src/gitflow.js +56 -0
- package/dist/src/grade.d.ts +42 -0
- package/dist/src/grade.js +96 -0
- package/dist/src/guidance.d.ts +15 -0
- package/dist/src/guidance.js +68 -0
- package/dist/src/hardcases.d.ts +2 -0
- package/dist/src/hardcases.js +94 -0
- package/dist/src/harness.d.ts +411 -0
- package/dist/src/harness.js +725 -0
- package/dist/src/hooks.d.ts +44 -0
- package/dist/src/hooks.js +116 -0
- package/dist/src/images.d.ts +13 -0
- package/dist/src/images.js +27 -0
- package/dist/src/index.d.ts +57 -0
- package/dist/src/index.js +57 -0
- package/dist/src/ledger.d.ts +58 -0
- package/dist/src/ledger.js +78 -0
- package/dist/src/live.d.ts +16 -0
- package/dist/src/live.js +80 -0
- package/dist/src/lsp.d.ts +31 -0
- package/dist/src/lsp.js +147 -0
- package/dist/src/memory.d.ts +128 -0
- package/dist/src/memory.js +390 -0
- package/dist/src/metrics.d.ts +3 -0
- package/dist/src/metrics.js +42 -0
- package/dist/src/mining.d.ts +49 -0
- package/dist/src/mining.js +126 -0
- package/dist/src/openai.d.ts +50 -0
- package/dist/src/openai.js +307 -0
- package/dist/src/parallel.d.ts +66 -0
- package/dist/src/parallel.js +135 -0
- package/dist/src/patch.d.ts +45 -0
- package/dist/src/patch.js +219 -0
- package/dist/src/permissions.d.ts +46 -0
- package/dist/src/permissions.js +245 -0
- package/dist/src/plugins.d.ts +148 -0
- package/dist/src/plugins.js +225 -0
- package/dist/src/pluginscan.d.ts +8 -0
- package/dist/src/pluginscan.js +20 -0
- package/dist/src/procs.d.ts +41 -0
- package/dist/src/procs.js +94 -0
- package/dist/src/profiles.d.ts +36 -0
- package/dist/src/profiles.js +61 -0
- package/dist/src/progress.d.ts +29 -0
- package/dist/src/progress.js +75 -0
- package/dist/src/prompts.d.ts +10 -0
- package/dist/src/prompts.js +28 -0
- package/dist/src/ratelimit.d.ts +34 -0
- package/dist/src/ratelimit.js +116 -0
- package/dist/src/replay.d.ts +25 -0
- package/dist/src/replay.js +99 -0
- package/dist/src/review.d.ts +20 -0
- package/dist/src/review.js +49 -0
- package/dist/src/route.d.ts +9 -0
- package/dist/src/route.js +30 -0
- package/dist/src/router.d.ts +61 -0
- package/dist/src/router.js +126 -0
- package/dist/src/sandbox.d.ts +13 -0
- package/dist/src/sandbox.js +68 -0
- package/dist/src/schedule.d.ts +20 -0
- package/dist/src/schedule.js +59 -0
- package/dist/src/schema.d.ts +13 -0
- package/dist/src/schema.js +36 -0
- package/dist/src/secrets.d.ts +10 -0
- package/dist/src/secrets.js +41 -0
- package/dist/src/shell.d.ts +34 -0
- package/dist/src/shell.js +95 -0
- package/dist/src/skills.d.ts +20 -0
- package/dist/src/skills.js +112 -0
- package/dist/src/snapshot.d.ts +20 -0
- package/dist/src/snapshot.js +93 -0
- package/dist/src/stats.d.ts +24 -0
- package/dist/src/stats.js +62 -0
- package/dist/src/strategies.d.ts +16 -0
- package/dist/src/strategies.js +35 -0
- package/dist/src/suites.d.ts +30 -0
- package/dist/src/suites.js +112 -0
- package/dist/src/trace.d.ts +63 -0
- package/dist/src/trace.js +153 -0
- package/dist/src/tune.d.ts +38 -0
- package/dist/src/tune.js +49 -0
- package/dist/src/web.d.ts +19 -0
- package/dist/src/web.js +56 -0
- package/dist/src/worktree.d.ts +24 -0
- package/dist/src/worktree.js +85 -0
- package/docs/agent-loop.md +222 -0
- package/docs/architecture.md +228 -0
- package/docs/bench.md +292 -0
- package/docs/budgets.md +185 -0
- package/docs/build-your-own-harness.md +424 -0
- package/docs/cookbook.md +375 -0
- package/docs/development.md +150 -0
- package/docs/evaluation.md +450 -0
- package/docs/features.md +1320 -0
- package/docs/getting-started.md +205 -0
- package/docs/headless.md +188 -0
- package/docs/images/approval.png +0 -0
- package/docs/images/chat.png +0 -0
- package/docs/images/daylight.png +0 -0
- package/docs/images/loop.png +0 -0
- package/docs/images/palette.png +0 -0
- package/docs/images/running.png +0 -0
- package/docs/images/subagent-done.png +0 -0
- package/docs/images/subagent-running.png +0 -0
- package/docs/images/welcome.png +0 -0
- package/docs/index.md +25 -0
- package/docs/library.md +267 -0
- package/docs/plugins.md +691 -0
- package/docs/providers.md +175 -0
- package/docs/safety.md +250 -0
- package/docs/usage.md +348 -0
- package/docs/why.md +155 -0
- package/docs/writing-guide.md +39 -0
- package/package.json +66 -4
package/docs/plugins.md
ADDED
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
# Plugins: the harness as code
|
|
2
|
+
|
|
3
|
+
A plugin is a TypeScript or JavaScript file that runs inside Amness and can change how the agent behaves, and `amness eval` can measure it against the built-in harness.
|
|
4
|
+
|
|
5
|
+
New to plugins? Start with the [tutorial: Build your own harness](build-your-own-harness.md).
|
|
6
|
+
|
|
7
|
+
In this page you will learn:
|
|
8
|
+
|
|
9
|
+
- what a plugin can add, and where plugin files live
|
|
10
|
+
- how to write, load and test your first plugin
|
|
11
|
+
- the eight hook events and what each one receives
|
|
12
|
+
- how to write eval tasks and variants as code
|
|
13
|
+
- how to add slash commands, finish gates, policies, loop strategies, context strategies, metrics and graders
|
|
14
|
+
- what a plugin can do to your machine
|
|
15
|
+
|
|
16
|
+
## What a plugin is
|
|
17
|
+
|
|
18
|
+
Hooks in a config file run shell commands at fixed points. A plugin goes further. It is code that Amness loads into its own process. A plugin can contribute:
|
|
19
|
+
|
|
20
|
+
| What | How | Use it for |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| **Loop rules** | `hooks`: one function per event | Block a tool call, rewrite its arguments, add a note before the next model request, refuse to finish, log anything. |
|
|
23
|
+
| **Guardrails** | `PreToolUse` and `Stop` hooks, with `failClosed: true` | House rules in code. With `failClosed`, a crash in your rule blocks the action instead of letting it pass. |
|
|
24
|
+
| **Tools** | `tools: [defineTool({...})]` | Your own tools for the agent. |
|
|
25
|
+
| **Prompt** | `system: (base, ctx) => ...` | Extend or rewrite the system prompt. |
|
|
26
|
+
| **Eval tasks** | `cases: [...]` | Tasks whose check is a function. |
|
|
27
|
+
| **Eval variants** | `variants: { name: {...} }` | A harness setup to compare with others. |
|
|
28
|
+
| **Slash commands** | `commands: [...]` | Your own `/name` commands in the chat. |
|
|
29
|
+
| **Finish gates** | `gates: [...]` | Checks that must pass before the agent may finish. |
|
|
30
|
+
| **Policy** | `policy: (call) => ...` | Allow, ask or deny each tool call, in code. |
|
|
31
|
+
| **Loop strategy** | `PreModel` returns `tools` and `then` | Limit which tools the model may use in a turn. |
|
|
32
|
+
| **Context strategy** | `context: (msgs, info) => ...` | Your own way to trim or rewrite the message list. |
|
|
33
|
+
| **Memory backend** | `memory: ...` | Keep notes in your own store instead of Markdown files. |
|
|
34
|
+
| **Model routing** | `route: (info) => id` | Choose the model for each request. |
|
|
35
|
+
| **Status line** | `status: (ctx) => text` | Show a short text in the sidebar. |
|
|
36
|
+
| **Metrics and graders** | `metrics`, `graders` | Extra numbers for evals, and scores for real sessions. |
|
|
37
|
+
|
|
38
|
+
Status: all of this is **tested** with scripted models (no real model), in `test/plugins.test.ts` and `test/trackc.test.ts`. Only the two built-in loop strategies were also run once on a real model (see [Evaluating the harness](evaluation.md)). No other feature on this page is **measured** on a real model.
|
|
39
|
+
|
|
40
|
+
The tested code is in `src/plugins.ts` and `test/plugins.test.ts`. A full example with a tool, a prompt rewrite, an eval task and two variants is in [examples/plugins/house-rules.ts](../examples/plugins/house-rules.ts).
|
|
41
|
+
|
|
42
|
+
## Write your first plugin
|
|
43
|
+
|
|
44
|
+
This plugin stops the agent from editing lockfiles by hand.
|
|
45
|
+
|
|
46
|
+
1. Create the folder and the file in your project:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
mkdir -p .amness/plugins
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
2. Save this as `.amness/plugins/no-lockfiles.ts`:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
export default {
|
|
56
|
+
name: "no-lockfiles",
|
|
57
|
+
description: "never hand-edit package-lock.json",
|
|
58
|
+
hooks: {
|
|
59
|
+
PreToolUse: ({ tool, args }: { tool: string; args: any }) =>
|
|
60
|
+
(tool === "write_file" || tool === "edit_file") && /package-lock\.json$/.test(String(args.path))
|
|
61
|
+
? { block: "lockfiles are generated: run the package manager instead" }
|
|
62
|
+
: undefined,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
A plain exported object works. If you want type checking, import `definePlugin` from `src/index.ts` of Amness and wrap the object. `definePlugin` only returns the object you give it.
|
|
68
|
+
|
|
69
|
+
3. Start Amness in the project folder:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
amness
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Amness asks if you trust the plugin files in `.amness/plugins`. Choose "Yes, trust this version". Amness remembers this for these exact files. If you change a file, it asks again.
|
|
76
|
+
|
|
77
|
+
4. See the plugin:
|
|
78
|
+
|
|
79
|
+
```text
|
|
80
|
+
/plugins
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
You will see a line with the name `no-lockfiles`, `hooks: PreToolUse` and your description.
|
|
84
|
+
|
|
85
|
+
5. Trigger it. Ask the agent to make a change that needs a lockfile edit:
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
Edit package-lock.json by hand and change the version of the root package to 9.9.9.
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
6. When the agent tries `write_file` or `edit_file` on that path, the hook blocks the call. The agent gets this text as the tool result:
|
|
92
|
+
|
|
93
|
+
```text
|
|
94
|
+
BLOCKED BY A HOOK: lockfiles are generated: run the package manager instead
|
|
95
|
+
Do not retry the same call; choose another approach or explain what you could not do.
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The chat marks the call as denied. The words the agent then writes will differ from run to run.
|
|
99
|
+
|
|
100
|
+
If the file has a mistake, Amness shows a warning such as `plugin: <file>: could not load: ...` and starts anyway.
|
|
101
|
+
|
|
102
|
+
### Hook return values
|
|
103
|
+
|
|
104
|
+
A hook may return:
|
|
105
|
+
|
|
106
|
+
- nothing (`undefined`): continue
|
|
107
|
+
- a string: added as context for the agent
|
|
108
|
+
- an object `{ block, context, args }`: stop the action with a message, add context, or replace the tool arguments
|
|
109
|
+
|
|
110
|
+
The first `block` stops all later hooks. If several hooks return `context`, Amness joins them. The last `args` win. Plugin hooks run after your shell hooks. A hook may be `async`.
|
|
111
|
+
|
|
112
|
+
A hook that throws only gives a warning. If the plugin sets `failClosed: true`, a throw blocks instead.
|
|
113
|
+
|
|
114
|
+
## The eight hook events
|
|
115
|
+
|
|
116
|
+
Every payload also has `event` (the event name) and `cwd` (the working folder). The table lists the other fields. They are taken from `src/harness.ts` and `src/code.ts`.
|
|
117
|
+
|
|
118
|
+
| Event | When it runs | Extra payload fields | What a return value does |
|
|
119
|
+
|---|---|---|---|
|
|
120
|
+
| `SessionStart` | With the first message of a conversation | none | `context` is added to the message. `block` refuses the message. |
|
|
121
|
+
| `UserPromptSubmit` | Each time you send a message | `prompt` | `context` is added to the message. `block` refuses the message. |
|
|
122
|
+
| `PreModel` | Before every model request | `step`, `max`, `messages`, `promptTokens`, `budget` | A string or `context` is added as a user note. `block` ends the run with `stopped: <message>`. |
|
|
123
|
+
| `PostModel` | After every model reply | `step`, `text` (first 4000 characters), `tools` (names of the tools the model called), `usage` | Ignored. Use it to log or measure. |
|
|
124
|
+
| `PreToolUse` | Before a tool runs | `tool`, `args` | `block` stops the call. `args` replaces the arguments (they must still match the tool schema). `context` is not read for this event in the harness. |
|
|
125
|
+
| `PostToolUse` | After a tool runs | `tool`, `args`, `result` (clipped to 4000 characters), `ok` | `context` is added to the end of the result. `block` is not used. |
|
|
126
|
+
| `PreCompact` | Before old tool output is trimmed | `chars` (size of the history) | `block` cancels the trim for this step. |
|
|
127
|
+
| `Stop` | When the agent calls `finish` | `summary` | `block` rejects the finish. The agent sees `REJECTED by a hook: <message>` and must continue. |
|
|
128
|
+
|
|
129
|
+
Notes:
|
|
130
|
+
|
|
131
|
+
- `budget` in `PreModel` is the share of the token cap used so far, from 0 to 1.
|
|
132
|
+
- `ok` in `PostToolUse` is false when the result starts with `ERROR` or `DENIED`.
|
|
133
|
+
- `step` starts at 0.
|
|
134
|
+
- `SessionStart` and `UserPromptSubmit` run in the code agent (`src/code.ts`). The other six run in the loop (`src/harness.ts`).
|
|
135
|
+
|
|
136
|
+
The example plugin uses three of these events:
|
|
137
|
+
|
|
138
|
+
- `PreToolUse` blocks lockfile edits.
|
|
139
|
+
- `Stop` makes the agent say which command it ran before it finishes.
|
|
140
|
+
- `PreModel` reminds the agent at step 10 if it has used less than half of its budget and is still reading.
|
|
141
|
+
|
|
142
|
+
## Add a tool or change the prompt
|
|
143
|
+
|
|
144
|
+
A tool uses `defineTool`:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import { definePlugin, defineTool } from "<path to Amness>/src/index.ts";
|
|
148
|
+
|
|
149
|
+
export default definePlugin({
|
|
150
|
+
name: "changelog",
|
|
151
|
+
tools: [defineTool({
|
|
152
|
+
name: "changelog_hint",
|
|
153
|
+
description: "Returns the repository's changelog convention.",
|
|
154
|
+
readOnly: true,
|
|
155
|
+
schema: { type: "object" },
|
|
156
|
+
run: () => "Add one line under 'Unreleased' in CHANGELOG.md.",
|
|
157
|
+
})],
|
|
158
|
+
system: (base) => base + "\nMention any file you could not verify.",
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`system` runs once when a conversation starts. Plugins apply in load order. Set `readOnly: true` only for tools that change nothing.
|
|
163
|
+
|
|
164
|
+
## Write eval cases and variants
|
|
165
|
+
|
|
166
|
+
A plugin can bring its own eval tasks and harness variants, so you can test a rule before you trust it. For how to read the results, see [Evaluating the harness](evaluation.md).
|
|
167
|
+
|
|
168
|
+
### A case with a function grader
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
cases: [{
|
|
172
|
+
name: "lockfile-trap",
|
|
173
|
+
files: { "package-lock.json": "{}\n", "package.json": '{"name":"x"}\n', "a.txt": "one\n" },
|
|
174
|
+
task: "Change the text in a.txt from 'one' to 'two'. Do not touch anything else.",
|
|
175
|
+
check: async (dir: string) => {
|
|
176
|
+
const fs = await import("node:fs");
|
|
177
|
+
return fs.readFileSync(`${dir}/a.txt`, "utf8").trim() === "two"
|
|
178
|
+
&& fs.readFileSync(`${dir}/package-lock.json`, "utf8") === "{}\n"
|
|
179
|
+
? true
|
|
180
|
+
: "wrong a.txt or lockfile changed";
|
|
181
|
+
},
|
|
182
|
+
solution: { "a.txt": "two\n" },
|
|
183
|
+
}],
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
- `check` gets the workspace folder. It returns `true` to pass, or text that explains the failure. It can also be a shell command string.
|
|
187
|
+
- `solution` is a known good fix. Without it, `--selftest` only checks that the check fails at the start.
|
|
188
|
+
- A case may also have `verify` (a command the agent may run), `tools` (extra tools for this case) and `setup` (prepare the folder; if it fails, the case is excluded instead of counted as a failure).
|
|
189
|
+
|
|
190
|
+
### A variant
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
variants: {
|
|
194
|
+
"house-rules": { off: [], verify: true },
|
|
195
|
+
"lean": { off: ["nudge", "condense", "wrapup"], verify: true, system: (b) => b + "\nBe brief." },
|
|
196
|
+
},
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
| Field | Meaning |
|
|
200
|
+
|---|---|
|
|
201
|
+
| `off` | Guards to switch off. The names are `nudge`, `loop`, `compact`, `stuck`, `wrapup`, `condense`, `schedule`, `ledger` and `stagnation`. |
|
|
202
|
+
| `maxSteps` | Step limit for this variant. |
|
|
203
|
+
| `verify` | `false` removes the case's verify command as the finish gate. The default is on. |
|
|
204
|
+
| `review` | `true` adds the independent reviewer. |
|
|
205
|
+
| `plugins` | Names of other loaded plugins that are active in this variant. The plugin that defines the variant is always active. |
|
|
206
|
+
| `system` | Rewrites the system prompt for this variant. |
|
|
207
|
+
| `build` | `build(model, { maxSteps, maxTokens })` returns a complete custom `Variant`. When set, the other fields are not used. |
|
|
208
|
+
|
|
209
|
+
### Run it
|
|
210
|
+
|
|
211
|
+
1. Check that your cases are sound. This calls no model.
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
amness eval --plugin examples/plugins/house-rules.ts --selftest
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
You will see the built-in tasks and then `ok lockfile-trap`.
|
|
218
|
+
|
|
219
|
+
2. Compare variants. This calls your model and asks before it starts.
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
amness eval --plugin examples/plugins/house-rules.ts --variants full,house-rules,lean --trials 3
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
3. Put all loaded plugins into the `full` harness:
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
amness eval --plugin my.ts --variants full,full+plugins
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
4. Use a module of cases with function graders, without a plugin:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
amness eval --cases my-tasks.ts
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Use a variant by `name` or by `plugin:name`, for example `house-rules:lean`. `--plugin` takes a comma-separated list of files. Passing a file with `--plugin` counts as your approval of that file.
|
|
238
|
+
|
|
239
|
+
## Add slash commands
|
|
240
|
+
|
|
241
|
+
**What it is.** A plugin can add a slash command to the chat. The command is a function with a name and a help text.
|
|
242
|
+
|
|
243
|
+
**Why.** You can start a repeated job, such as "run the linter and ask the agent to fix it", with one short command.
|
|
244
|
+
|
|
245
|
+
**How to use it.**
|
|
246
|
+
|
|
247
|
+
1. Create a plugin from a template. It already has one command:
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
amness plugin new house-rules
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Amness writes `.amness/plugins/house-rules.ts`. It never overwrites a file that exists. The name may use letters, numbers, `.`, `_` and `-`.
|
|
254
|
+
|
|
255
|
+
2. Add or edit a command in the `commands` list:
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
commands: [{
|
|
259
|
+
name: "lint-fix",
|
|
260
|
+
help: "run the linter and ask the agent to fix what it finds",
|
|
261
|
+
run: async (ctx) => {
|
|
262
|
+
const r = await ctx.run("npm run lint", 120_000);
|
|
263
|
+
if (r.code === 0) return ctx.say("lint is clean");
|
|
264
|
+
await ctx.ask("Fix these lint errors:\n" + r.output);
|
|
265
|
+
},
|
|
266
|
+
}],
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
3. Load the change without a restart:
|
|
270
|
+
|
|
271
|
+
```text
|
|
272
|
+
/reload-plugins
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
4. List what loads, from the shell:
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
amness plugin list
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
The `ctx` object has four things and nothing else:
|
|
282
|
+
|
|
283
|
+
| Member | What it does |
|
|
284
|
+
|---|---|
|
|
285
|
+
| `ctx.cwd` | The working folder. |
|
|
286
|
+
| `ctx.say(text)` | Prints a line in the chat. The agent does not see it. |
|
|
287
|
+
| `ctx.ask(text)` | Sends a message to the agent, as if you had typed it. |
|
|
288
|
+
| `ctx.run(cmd, timeoutMs?)` | Runs a shell command. It returns `{ code, output }`. The default timeout is 60 seconds. |
|
|
289
|
+
|
|
290
|
+
A command can also take an argument: `run(ctx, arg)` gets the text after the command name.
|
|
291
|
+
|
|
292
|
+
**Example.** After step 3, `/reload-plugins` prints a line such as `1 plugin loaded: house-rules.` Then `/lint-fix` appears in the `/` popup, with your help text and `(plugin house-rules)` after it.
|
|
293
|
+
|
|
294
|
+
**Limits.**
|
|
295
|
+
|
|
296
|
+
- A plugin command whose name is already used by a built-in command is skipped. Your own `.amness/commands/*.md` files also win over plugin commands.
|
|
297
|
+
- If the command throws, the chat shows `plugin <name>: /<command> failed: <message>`. Nothing else breaks.
|
|
298
|
+
- `/reload-plugins` loads your user plugins and the project plugins you already approved. If you edited a project plugin file, the approval no longer matches, so Amness shows the same approval question again (with what the file appears to touch). If you say no, the edited plugin is skipped.
|
|
299
|
+
- `amness plugin list` shows project plugins as loaded only with `--trust-project`. Without it, the list says how many files were skipped.
|
|
300
|
+
- Tested in `test/trackc.test.ts` with scripted input. The full-screen chat was not driven by a person in these tests.
|
|
301
|
+
|
|
302
|
+
## Add a finish gate
|
|
303
|
+
|
|
304
|
+
**What it is.** A *finish gate* is a check that runs when the agent calls `finish`. If the check fails, the agent must keep working.
|
|
305
|
+
|
|
306
|
+
**Why.** A `Stop` hook sees only the summary text. A gate also sees what really happened: which files changed, and whether tests ran after the last edit.
|
|
307
|
+
|
|
308
|
+
**How to use it.** Add a `gates` list to your plugin:
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
gates: [{
|
|
312
|
+
name: "tests-after-edit",
|
|
313
|
+
check: (ctx) =>
|
|
314
|
+
ctx.changed.length && ctx.testsAfterLastEdit === false
|
|
315
|
+
? "you changed files but did not run the tests after the last edit"
|
|
316
|
+
: true,
|
|
317
|
+
}],
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
The `check` function gets a `GateCtx` object:
|
|
321
|
+
|
|
322
|
+
| Field | Meaning |
|
|
323
|
+
|---|---|
|
|
324
|
+
| `summary` | The text the agent gave to `finish`. |
|
|
325
|
+
| `task` | The task text. |
|
|
326
|
+
| `changed` | The list of files changed in this task. |
|
|
327
|
+
| `testsAfterLastEdit` | Whether a test or build command ran after the last edit. It can be `undefined`. |
|
|
328
|
+
| `notes` | Notes from the change ledger about this task. |
|
|
329
|
+
| `cwd` | The working folder. |
|
|
330
|
+
| `run(cmd, ms?)` | Runs a shell command. It returns `{ code, output }`. |
|
|
331
|
+
| `rejections` | How many times this gate has already rejected this task. |
|
|
332
|
+
|
|
333
|
+
What `check` may return:
|
|
334
|
+
|
|
335
|
+
| Return value | Meaning |
|
|
336
|
+
|---|---|
|
|
337
|
+
| `true` or nothing | Pass. |
|
|
338
|
+
| `false` | Fail, with the finding "the check failed". |
|
|
339
|
+
| a string | Fail, with this finding. |
|
|
340
|
+
| a list of strings | Fail if the list is not empty. Each string is a finding. |
|
|
341
|
+
| `{ pass, findings }` | Fail when `pass` is false. |
|
|
342
|
+
|
|
343
|
+
**The `maxRejections` rule.** A gate may reject the same task only a limited number of times. The default is 2. After that, the run finishes anyway and Amness adds a warning `finishing despite gate ...`. This stops a wrong gate from trapping the agent. Set `maxRejections` on the gate to change it. The count restarts when the task text changes.
|
|
344
|
+
|
|
345
|
+
**Several reviewers.** `votingGate(name, reviewers, quorum, maxRejections)` joins several checks. `quorum` is `"all"`, `"majority"` (the default) or a number. Each reviewer has a `name` and a `review(ctx)` function that returns the same values as `check`. A reviewer that throws counts as a pass, and its error is added as a finding. A rejection lists which reviewers said no.
|
|
346
|
+
|
|
347
|
+
**Example.** The agent calls `finish` after editing `a.txt` without a test run. It sees:
|
|
348
|
+
|
|
349
|
+
```text
|
|
350
|
+
REJECTED by a hook: gate "tests-after-edit" (plugin house-rules) found problems:
|
|
351
|
+
- you changed files but did not run the tests after the last edit
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**Limits.**
|
|
355
|
+
|
|
356
|
+
- Gates run after the hooks of the `Stop` event. If a hook blocks, the gates do not run for that attempt.
|
|
357
|
+
- A gate that throws gives a warning and counts as a pass. Make it safe: catch errors yourself if a crash must block.
|
|
358
|
+
- The gate sees what the change ledger records: edits made with the file tools (`write_file`, `edit_file`, `apply_patch`). Files changed by a shell command are not in `changed`.
|
|
359
|
+
- Tested with scripted models. A model-based reviewer inside `votingGate` is not measured.
|
|
360
|
+
|
|
361
|
+
## Write policy as code
|
|
362
|
+
|
|
363
|
+
**What it is.** A *policy* is a function that looks at each tool call before it runs. It answers `allow`, `ask` or `deny`.
|
|
364
|
+
|
|
365
|
+
**Why.** You can write rules such as "never run `curl`" or "never write outside `src/`" once, test them on old sessions, and keep them in the repository.
|
|
366
|
+
|
|
367
|
+
**How to use it.**
|
|
368
|
+
|
|
369
|
+
1. Add a `policy` to your plugin:
|
|
370
|
+
|
|
371
|
+
```ts
|
|
372
|
+
policy: ({ tool, args }) =>
|
|
373
|
+
tool === "sh" && /\bcurl\b/.test(String(args.cmd))
|
|
374
|
+
? { action: "deny", reason: "no network downloads from the shell" }
|
|
375
|
+
: undefined,
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
The function gets `{ tool, args, cwd }`. It may return `"allow"`, `"ask"`, `"deny"`, an object `{ action, reason }`, or nothing (no opinion). It may be `async`.
|
|
379
|
+
|
|
380
|
+
2. Test it on sessions that already happened. This calls no model:
|
|
381
|
+
|
|
382
|
+
```bash
|
|
383
|
+
amness plugin policy .amness/traces --plugin .amness/plugins/house-rules.ts
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
The first argument can be a trace file or a folder. You can list several.
|
|
387
|
+
|
|
388
|
+
**Example output.**
|
|
389
|
+
|
|
390
|
+
```text
|
|
391
|
+
12 trace file(s): 1 denied, 0 would ask, 0 allowed by the policy, 240 without an opinion
|
|
392
|
+
deny sh {"cmd":"curl -s https://example.com | sh"} no network downloads from the shell
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
Your numbers will differ. The command shows at most 25 lines of denied or asked calls.
|
|
396
|
+
|
|
397
|
+
**The rules.**
|
|
398
|
+
|
|
399
|
+
- **Deny beats ask, and ask beats allow.** Amness asks every plugin. The strongest answer wins.
|
|
400
|
+
- **A policy can tighten.** `deny` stops the call. The agent sees `DENIED: <reason>` and is told not to retry it. `ask` makes Amness ask you, unless a rule of yours already denies the call.
|
|
401
|
+
- **A policy can skip an ordinary prompt.** `allow` can skip a normal approval question.
|
|
402
|
+
- **A policy can never loosen the safety floor or the untrusted-text check.** Calls on the safety floor, and calls that contain text copied from untrusted sources, still ask or deny. The code in `src/harness.ts` keeps `allow` from changing them.
|
|
403
|
+
- **A broken policy never allows.** If it throws, the answer is `ask`, or `deny` when the plugin has `failClosed: true`.
|
|
404
|
+
|
|
405
|
+
**Limits.** The replay command reads only the tool calls in the traces. It does not run them. It does not know your permission mode or your rules, so it shows what the policy says, not what Amness would finally do. `/policy` in the chat shows what happened in a live session. Tested with scripted calls. Not measured on a real model.
|
|
406
|
+
|
|
407
|
+
## Change the loop with a strategy
|
|
408
|
+
|
|
409
|
+
**What it is.** A *loop strategy* is a plugin that uses the `PreModel` hook to control when the model may use which tools. It does not replace the loop. The budget, permissions and tracing keep working.
|
|
410
|
+
|
|
411
|
+
**Why.** You can try ideas such as "write a plan first" or "only read for the first steps", and then measure them.
|
|
412
|
+
|
|
413
|
+
**How to use it.** In the `PreModel` hook, you can return these extra fields:
|
|
414
|
+
|
|
415
|
+
| Field | Meaning |
|
|
416
|
+
|---|---|
|
|
417
|
+
| `tools` | A list of tool names. Only these tools may be called in this turn. An empty list means no tools. Another tool call gets `ERROR: <tool> is not allowed in this phase`. |
|
|
418
|
+
| `then` | A message. If the model answers with text only in this turn, Amness adds `then` as a user message and asks again. It does not end the run. |
|
|
419
|
+
| `context` | A note added as a user message (as before). |
|
|
420
|
+
|
|
421
|
+
The `then` field is for a planning turn. Without it, a text-only reply ends the run (the model has no tool call, so the loop thinks it is done).
|
|
422
|
+
|
|
423
|
+
Two strategies are built in, in `src/strategies.ts`:
|
|
424
|
+
|
|
425
|
+
| Name | What it does |
|
|
426
|
+
|---|---|
|
|
427
|
+
| `plan-first` | At step 0, the model must write a numbered plan and may not call tools. Then it works as usual. |
|
|
428
|
+
| `explore-then-act` | For the first 6 steps, only read-only tools are allowed. After that, all tools. |
|
|
429
|
+
|
|
430
|
+
To use one in the chat:
|
|
431
|
+
|
|
432
|
+
```text
|
|
433
|
+
/strategy plan-first
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
`/strategy` with no argument shows the current choice. `/strategy off` turns it off. The choice is saved in your config.
|
|
437
|
+
|
|
438
|
+
To measure one, compare it with `full`. This calls your model and asks first:
|
|
439
|
+
|
|
440
|
+
```bash
|
|
441
|
+
amness eval --variants full,strategy:plan-first --builtin hard --trials 3
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
Copy `src/strategies.ts` as a start for your own strategy.
|
|
445
|
+
|
|
446
|
+
**Limits.**
|
|
447
|
+
|
|
448
|
+
- `tools` is checked when the model calls a tool. A call to a tool that is not in the list gets an error result. The model still sees the full tool list (this keeps the provider's prompt cache valid); the limit is enforced when it calls a tool.
|
|
449
|
+
- `plan-first` costs one extra model call per request.
|
|
450
|
+
- **Measured once:** a run on `gpt-4.1-mini` found no detectable difference to `full`. See [Evaluating the harness](evaluation.md). Do not read this as proof that a strategy helps or hurts.
|
|
451
|
+
|
|
452
|
+
## Your own context strategy
|
|
453
|
+
|
|
454
|
+
**What it is.** The `context` function runs before every model request. It gets the message list and `{ step, promptTokens, budget }`. It may return a new list that replaces the old one.
|
|
455
|
+
|
|
456
|
+
**Why.** You can drop old tool output, add a summary, or move notes. This is how you test your own compaction idea.
|
|
457
|
+
|
|
458
|
+
**How to use it.**
|
|
459
|
+
|
|
460
|
+
```ts
|
|
461
|
+
context: (msgs, { step, budget }) =>
|
|
462
|
+
budget > 0.7 ? msgs.filter((m, i) => i < 2 || i > msgs.length - 12) : undefined,
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
Return nothing to keep the list. An empty list is ignored. If several plugins define `context`, they run in load order and each gets the result of the one before. If your function throws, Amness ignores it for that turn (in `src/harness.ts` the error is caught).
|
|
466
|
+
|
|
467
|
+
**Limits.** Amness does not check that your new list is valid. If you cut a tool result away from its tool call, the provider may reject the request. Keep each tool call and its result together. Tested with a scripted model. Not measured.
|
|
468
|
+
|
|
469
|
+
## Replace the memory backend
|
|
470
|
+
|
|
471
|
+
**What it is.** *Memory* is the set of notes that Amness saves in one session and recalls in the next. The built-in memory keeps Markdown files in `.amness/memory`. A plugin can provide its own backend with the `memory` field. The backend can use a database, a wiki or a ticket system.
|
|
472
|
+
|
|
473
|
+
**Why.** Your team may already keep knowledge somewhere else. A backend lets the agent read and write it.
|
|
474
|
+
|
|
475
|
+
**How to use it.** Set `memory` to an object, or to a function that gets the working folder and returns the object. The object must have the seven methods of the `MemoryBackend` interface in `src/memory.ts`:
|
|
476
|
+
|
|
477
|
+
| Method | What it is for |
|
|
478
|
+
|---|---|
|
|
479
|
+
| `list()` | Returns all notes. |
|
|
480
|
+
| `save(entry)` | Saves one note (`name`, `type`, `description`, `content`, optional `scope`). Returns the saved note. |
|
|
481
|
+
| `forget(name, scope?)` | Deletes a note. Returns `true` if it existed. |
|
|
482
|
+
| `search(query, n?)` | Finds notes for the `memory_search` tool. It may be `async`. |
|
|
483
|
+
| `context(task, maxChars?, seen?)` | Returns text for the first message of a conversation. Return `""` for none. |
|
|
484
|
+
| `delta(text, seen)` | Returns text for a later message, with only notes that are new and relevant. Return `""` for none. |
|
|
485
|
+
| `endTurn(success)` | Tells the backend how the run ended. |
|
|
486
|
+
|
|
487
|
+
`endTurn` gets `true` when the agent finished and the result was verified, `false` when it gave up or got stuck, and `undefined` when the outcome is unknown (for example, a chat answer). A backend can use this to rank notes that helped higher. The built-in store does this. `warmQuery(query)` is optional. It lets a backend prepare a semantic search.
|
|
488
|
+
|
|
489
|
+
This backend keeps notes in memory only. It is small, but complete:
|
|
490
|
+
|
|
491
|
+
```ts
|
|
492
|
+
const notes = new Map<string, any>();
|
|
493
|
+
|
|
494
|
+
export default {
|
|
495
|
+
name: "scratch-memory",
|
|
496
|
+
description: "notes kept in memory for one session",
|
|
497
|
+
memory: () => ({
|
|
498
|
+
list: () => [...notes.values()],
|
|
499
|
+
save: (e: any) => {
|
|
500
|
+
const entry = { scope: "project", ...e, updated: new Date().toISOString().slice(0, 10) };
|
|
501
|
+
notes.set(e.name, entry);
|
|
502
|
+
return entry;
|
|
503
|
+
},
|
|
504
|
+
forget: (name: string) => notes.delete(name),
|
|
505
|
+
search: (q: string, n = 5) =>
|
|
506
|
+
[...notes.values()].filter((e) => `${e.name} ${e.description} ${e.content}`.toLowerCase().includes(q.toLowerCase())).slice(0, n),
|
|
507
|
+
context: (task: string) =>
|
|
508
|
+
notes.size ? "## Notes\n" + [...notes.values()].map((e) => `- ${e.name}: ${e.description}`).join("\n") : "",
|
|
509
|
+
delta: () => "",
|
|
510
|
+
endTurn: (success?: boolean) => { console.error("run ended, success:", success); },
|
|
511
|
+
}),
|
|
512
|
+
};
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
**Limits.**
|
|
516
|
+
|
|
517
|
+
- The first plugin that provides a memory wins. A `memory` option passed to `createCodeAgent` wins over a plugin.
|
|
518
|
+
- Amness checks that the seven methods exist (`isMemoryBackend`). It does not check what they return.
|
|
519
|
+
- The built-in store has extras (embeddings, consolidation, a note graph). A custom backend does not need them. Not verified: how each `/memory` subcommand behaves with a custom backend.
|
|
520
|
+
- Evals run with memory off, so `amness eval` does not test a backend.
|
|
521
|
+
- Tested in `test/trackc.test.ts` with scripted models only.
|
|
522
|
+
|
|
523
|
+
## Route requests to different models
|
|
524
|
+
|
|
525
|
+
**What it is.** The `route` function picks the model for each request. It gets `{ messages, promptTokens, tail }` and returns a model id, or nothing to use the default model. `tail` describes the end of the conversation: `streak` (read-only steps in a row), `trouble` (something just failed), `first` (no model answer yet) and `steering` (a new user message).
|
|
526
|
+
|
|
527
|
+
**Why.** Reading files rarely needs the strongest model. You can send those steps to a cheaper one. Remember that each model has its own prompt cache. A switch can cost more than it saves. The built-in `/router` weighs this. Your function does not, unless you write it.
|
|
528
|
+
|
|
529
|
+
**How to use it.**
|
|
530
|
+
|
|
531
|
+
```ts
|
|
532
|
+
export default {
|
|
533
|
+
name: "cheap-reads",
|
|
534
|
+
description: "use a cheaper model after three read-only steps in a row",
|
|
535
|
+
route: ({ tail }: { tail: { streak: number; trouble: boolean } }) =>
|
|
536
|
+
tail.streak >= 3 && !tail.trouble ? "gpt-4.1-mini" : undefined,
|
|
537
|
+
};
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
Use an id that your provider accepts. Amness turns the id into a model with `modelById`. The chat and `amness run` provide it. They build a new model for the id with your current provider settings, and create it once per id. If several plugins have a `route`, Amness uses the first answer that is not empty (`routedByFunction` in `src/router.ts`).
|
|
541
|
+
|
|
542
|
+
**Limits.**
|
|
543
|
+
|
|
544
|
+
- The id must belong to the **same provider** as the current model.
|
|
545
|
+
- Routing works only where `modelById` exists. When you use `createCodeAgent` as a library, pass `modelById` yourself, or `route` is ignored.
|
|
546
|
+
- If `route` throws, Amness uses the default model.
|
|
547
|
+
- Tested with scripted models. Not measured: whether any routing rule saves money or keeps quality.
|
|
548
|
+
|
|
549
|
+
## Show a status line
|
|
550
|
+
|
|
551
|
+
**What it is.** `status` is a function that returns a short text. The chat shows it in the sidebar as `<plugin name>: <text>`, cut at 60 characters.
|
|
552
|
+
|
|
553
|
+
**How to use it.**
|
|
554
|
+
|
|
555
|
+
```ts
|
|
556
|
+
import { readFileSync } from "node:fs";
|
|
557
|
+
|
|
558
|
+
export default {
|
|
559
|
+
name: "branch",
|
|
560
|
+
description: "show the git branch",
|
|
561
|
+
status: ({ cwd, running }: { cwd: string; running: boolean }) => {
|
|
562
|
+
const head = readFileSync(cwd + "/.git/HEAD", "utf8").trim().replace("ref: refs/heads/", "");
|
|
563
|
+
return running ? `${head} (working)` : head;
|
|
564
|
+
},
|
|
565
|
+
};
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
The function gets the working folder and whether the agent is running. Return nothing to show no line.
|
|
569
|
+
|
|
570
|
+
**Limits.** The function runs on every redraw, and Amness caches the result for one second. Keep it fast and do not run slow commands. If it throws, the line is left out. If you do not see the sidebar, type `/sidebar`.
|
|
571
|
+
|
|
572
|
+
## Choose a system prompt
|
|
573
|
+
|
|
574
|
+
**What it is.** The *prompt registry* is a folder of Markdown files. Each file `.amness/prompts/<id>.md` (or `~/.amness/prompts/<id>.md`) is a complete system prompt. The id `base` is the built-in prompt. The code is in `src/prompts.ts`.
|
|
575
|
+
|
|
576
|
+
**Why.** A prompt in a file has a name. You can keep it in git, compare it with others and measure it.
|
|
577
|
+
|
|
578
|
+
**How to use it.**
|
|
579
|
+
|
|
580
|
+
1. Create a prompt:
|
|
581
|
+
|
|
582
|
+
```bash
|
|
583
|
+
mkdir -p .amness/prompts
|
|
584
|
+
echo "You are a careful coding agent. Make the smallest change that works." > .amness/prompts/terse.md
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
2. In the chat, choose it:
|
|
588
|
+
|
|
589
|
+
```text
|
|
590
|
+
/prompt terse
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
`/prompt` or `/prompt list` shows the ids and the current choice. `/prompt off` returns to the built-in prompt. The choice is saved in your config and applies to new conversations.
|
|
594
|
+
|
|
595
|
+
3. Measure it. This calls your model and asks first:
|
|
596
|
+
|
|
597
|
+
```bash
|
|
598
|
+
amness eval --variants full,prompt:terse --trials 3
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
**Limits.**
|
|
602
|
+
|
|
603
|
+
- The prompt replaces the **whole** system prompt, including the built-in rules for tools. Test it before you rely on it.
|
|
604
|
+
- A block of front matter between `---` lines at the top of the file is removed.
|
|
605
|
+
- An id may use letters, numbers, `.`, `_` and `-`, up to 40 characters. A project prompt wins over a user prompt with the same id.
|
|
606
|
+
- Tested with scripted models. Not measured on a real model.
|
|
607
|
+
|
|
608
|
+
## Add a plugin from a file or a URL
|
|
609
|
+
|
|
610
|
+
**What it is.** `amness plugin add` copies a plugin file into `.amness/plugins/`. The code is in `cli/plugincli.ts`.
|
|
611
|
+
|
|
612
|
+
**How to use it.**
|
|
613
|
+
|
|
614
|
+
```bash
|
|
615
|
+
amness plugin add ./their-plugin.ts
|
|
616
|
+
amness plugin add https://example.com/their-plugin.ts
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
Amness then:
|
|
620
|
+
|
|
621
|
+
- accepts a local file or an `https` URL (it rejects `http`)
|
|
622
|
+
- rejects a file over 300 KB, or a file with no `export default`
|
|
623
|
+
- never overwrites a file that exists
|
|
624
|
+
- prints what the file seems to touch (a text scan, see below)
|
|
625
|
+
|
|
626
|
+
The command does not run the plugin. The plugin still needs your approval the first time you start Amness in the project. Read the file before you approve it.
|
|
627
|
+
|
|
628
|
+
**Limits.** It does not check that the plugin works, or who wrote it. It cannot install a package from npm.
|
|
629
|
+
|
|
630
|
+
## Metrics and graders
|
|
631
|
+
|
|
632
|
+
Two more fields let a plugin measure things.
|
|
633
|
+
|
|
634
|
+
- `metrics: { name: (ctx) => number }` adds a number to every eval run. The function gets `{ case, row, outcome, dir }`. A metric that throws or returns a value that is not a finite number is left out of that run. Plugin metrics are recorded whenever the plugin is loaded with `--plugin`. See [Custom metrics and the regression gate](evaluation.md#custom-metrics-and-the-regression-gate).
|
|
635
|
+
- `graders: { name: (session) => score }` scores real sessions from your traces. A score is a number from 0 to 1, a boolean, or `{ score, note }`. Return nothing to skip a session. Run them with `amness plugin grade`. See [Grade real sessions](evaluation.md#grade-real-sessions).
|
|
636
|
+
|
|
637
|
+
## The capability scan at the trust prompt
|
|
638
|
+
|
|
639
|
+
When a project has plugin files that you did not approve, Amness shows one line per file before it asks. Each line says what the source text seems to touch. For example:
|
|
640
|
+
|
|
641
|
+
```text
|
|
642
|
+
house-rules.ts: runs shell commands, reads files
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
Amness looks for these things: shell commands, network use, file writes, file reads, environment variables, generated code (`eval`, `new Function`, dynamic `import`), and paths outside the project. If it finds none, it says `no shell, network, file or environment access found`. `amness plugin list` shows the same lines for skipped files.
|
|
646
|
+
|
|
647
|
+
**This is a text scan, not a sandbox.** It searches the source for words (in `src/pluginscan.ts`) and removes comments first. A plugin can hide what it does, for example by building a word from pieces. A plugin that passes the scan still runs with your full permissions. Read the file before you trust it.
|
|
648
|
+
|
|
649
|
+
## Safety: what a plugin can do
|
|
650
|
+
|
|
651
|
+
- **A plugin has your full permissions.** It is ordinary code in the Amness process. It can read files, run commands and use the network. Read a plugin before you trust it.
|
|
652
|
+
- **User plugins** in `~/.amness/plugins/` always load. Put only files there that you wrote or read.
|
|
653
|
+
- **Project plugins** in `.amness/plugins/` load only after you approve those exact files. Amness stores a fingerprint of the files. Any change to a file asks again. This protects you from a cloned repository that brings its own plugin.
|
|
654
|
+
- **Headless runs** do not load project plugins unless you pass `--trust-project`. Without it, Amness prints a note that tells you how many files it skipped.
|
|
655
|
+
- **A broken plugin is a warning**, never a crash. Two plugins with the same name: the second is skipped with a warning.
|
|
656
|
+
- **A hook that throws** only warns, unless the plugin sets `failClosed: true`. Use `failClosed` for rules that must always hold.
|
|
657
|
+
- **Names** may use letters, numbers, `.`, `_` and `-`, up to 40 characters.
|
|
658
|
+
- **Plugin tools** are added to the same tool list as built-in tools, so the harness handles their calls like other tool calls. Set `risky` on a tool that changes things. Not verified: how each permission mode treats every plugin tool.
|
|
659
|
+
- **Do not rely on the sandbox to limit plugin code.** The sandbox setting wraps the agent's shell commands only. Plugin code runs inside the Amness process, outside the sandbox, with your full permissions.
|
|
660
|
+
|
|
661
|
+
## What is not (yet) pluggable
|
|
662
|
+
|
|
663
|
+
- The loop order (steer, budget, compact, model, tools, verify) and the permission engine are fixed. Plugins act at the events around them. A strategy limits tools and adds notes. It does not replace the loop.
|
|
664
|
+
- A policy cannot loosen the safety floor or the untrusted-text check.
|
|
665
|
+
- Replacing the turn order itself. A plugin cannot change the order of the steps in the loop.
|
|
666
|
+
- Installing a plugin from npm. `amness plugin add` copies one file from a path or an `https` URL.
|
|
667
|
+
- Isolation of plugin code. Plugins run in the Amness process with your permissions.
|
|
668
|
+
- `PostModel` cannot change anything. `PostToolUse` cannot block.
|
|
669
|
+
- A plugin cannot add UI panels. A slash command can only use `say`, `ask` and `run`.
|
|
670
|
+
- Memory backends and model routing **are** pluggable now (see the sections above). The built-in `/router` is still separate from a plugin `route`.
|
|
671
|
+
|
|
672
|
+
## Limits and common problems
|
|
673
|
+
|
|
674
|
+
- The loop order (steer, budget, compact, model, tools, verify) and the permission engine are fixed. Plugins act at the events around them.
|
|
675
|
+
- The approval question appears in the chat. In headless mode, use `--trust-project`.
|
|
676
|
+
- No plugin has been measured to improve results. The `house-rules` example was run once against a real model, and it is not a benchmark.
|
|
677
|
+
- If your plugin does not show in `/plugins`, check the warning lines at the top of the chat. Common causes: no `name`, a name with spaces, `hooks` that is not a map of functions, or a syntax error.
|
|
678
|
+
|
|
679
|
+
## Related pages
|
|
680
|
+
|
|
681
|
+
- [Evaluating the harness](evaluation.md): how to measure a plugin.
|
|
682
|
+
- [Features in depth](features.md): shell hooks, permissions and the safety floor.
|
|
683
|
+
- [Use it as a library](library.md): `createCodeAgent` and `createHarness`.
|
|
684
|
+
- [Safety and privacy](safety.md)
|
|
685
|
+
|
|
686
|
+
## Next steps
|
|
687
|
+
|
|
688
|
+
- Copy [examples/plugins/house-rules.ts](../examples/plugins/house-rules.ts) to `.amness/plugins/` and change it.
|
|
689
|
+
- Measure your rule with `amness eval`, as shown in the [Cookbook](cookbook.md).
|
|
690
|
+
- Read the [roadmap](../ROADMAP.md) for what plugins cannot do yet.
|
|
691
|
+
- Try the new recipes in the [Cookbook](cookbook.md): finish gate, policy, strategy, CI gate and chaos.
|