@a-dray/aglib 0.1.0 → 0.1.1
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 +55 -136
- package/dist/harness/adapters/acp/index.d.ts +27 -11
- package/dist/harness/adapters/acp/index.js +58 -24
- package/dist/harness/adapters/acp/index.js.map +1 -1
- package/dist/harness/adapters/native/loop.js +0 -1
- package/dist/harness/adapters/native/loop.js.map +1 -1
- package/dist/harness/harness.d.ts +17 -6
- package/dist/harness/harness.js.map +1 -1
- package/dist/model/adapters/anthropic/index.js +21 -4
- package/dist/model/adapters/anthropic/index.js.map +1 -1
- package/dist/model/index.d.ts +0 -1
- package/dist/model/index.js +1 -1
- package/dist/model/index.js.map +1 -1
- package/dist/model/model.d.ts +8 -1
- package/dist/model/model.js +8 -1
- package/dist/model/model.js.map +1 -1
- package/dist/render.d.ts +100 -0
- package/dist/render.js +370 -0
- package/dist/render.js.map +1 -0
- package/dist/store/adapters/sqlite.js +25 -2
- package/dist/store/adapters/sqlite.js.map +1 -1
- package/dist/terminal.d.ts +23 -0
- package/dist/terminal.js +90 -0
- package/dist/terminal.js.map +1 -0
- package/dist/tools/execute.js +3 -1
- package/dist/tools/execute.js.map +1 -1
- package/dist/tools/tool.d.ts +8 -1
- package/dist/tools/tool.js.map +1 -1
- package/package.json +10 -1
package/README.md
CHANGED
|
@@ -1,171 +1,90 @@
|
|
|
1
1
|
# aglib
|
|
2
2
|
|
|
3
|
-
A small TypeScript toolkit for building **
|
|
3
|
+
A small TypeScript toolkit for building **production agents** — the kind with a
|
|
4
|
+
durable log, a permission rule on every call, somewhere contained to run, and an
|
|
5
|
+
answer for what happens when the process dies mid-turn.
|
|
4
6
|
|
|
5
|
-
You bring the product — users, tenancy, channels, UI, deployment, policy. aglib
|
|
6
|
-
session log
|
|
7
|
-
somewhere contained to run them, and four seams
|
|
7
|
+
You bring the product — users, tenancy, channels, UI, deployment, policy. aglib
|
|
8
|
+
gives you a session log that *is* the state, a loop that runs against it, tools
|
|
9
|
+
under your own permission rule, somewhere contained to run them, and four seams
|
|
10
|
+
with adapters: **model, store, sandbox, harness**.
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
Whose loop does the reasoning is a decision here, not an assumption. Write your
|
|
13
|
+
own, compose a vendor's agent library, or drive one over a protocol — the log,
|
|
14
|
+
the rendering, the accounting and the handoff are the same in all three, and what
|
|
15
|
+
*differs* is declared rather than discovered.
|
|
10
16
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
```text
|
|
14
|
-
src/
|
|
15
|
-
json.ts result.ts content.ts agent.ts run.ts
|
|
16
|
-
|
|
17
|
-
session/ entry.ts log.ts messages.ts the log — this IS the session's state
|
|
18
|
-
tools/ tool.ts execute.ts declaring and running tools
|
|
19
|
-
|
|
20
|
-
model/ model.ts adapters/{openai-compatible,anthropic,fake}/ conformance.ts
|
|
21
|
-
store/ store.ts adapters/sqlite.ts conformance.ts
|
|
22
|
-
sandbox/ sandbox.ts adapters/{local,docker} conformance.ts
|
|
23
|
-
harness/ harness.ts adapters/{native,acp}/
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
Four ports, each `<port>.ts` → `adapters/`. Read one and you can predict the rest. Our own loop is
|
|
27
|
-
`adapters/native`, beside the others rather than above them: it is one implementation of the port,
|
|
28
|
-
and the only thing special about it is that it holds every control point.
|
|
29
|
-
|
|
30
|
-
**The log is the state, not a record of it.** The loop re-projects its context from committed
|
|
31
|
-
entries every turn and appends results back. There is one representation of a conversation, so
|
|
32
|
-
nothing can drift from it and no test has to prove two views agree.
|
|
33
|
-
|
|
34
|
-
**One session hands work to another in a single write.** `append({ entries, enqueue })` commits what
|
|
35
|
-
this session did and what another receives, together or not at all — so spawning a child, replying
|
|
36
|
-
to a parent and messaging a peer are one operation, and a handoff cannot be half-done. A delivery
|
|
37
|
-
names where in the recipient's loop it lands: `interrupt` ends the running turn, `turn` is folded into it before its next model call, `next` waits for the one after.
|
|
38
|
-
|
|
39
|
-
**A killed worker's session is finishable.** `store.next()` answers what has been asked for;
|
|
40
|
-
`store.interrupted()` answers what was being worked when a process stopped existing. Either hands back a
|
|
41
|
-
claim, and a claim is what a run takes:
|
|
42
|
-
|
|
43
|
-
```ts
|
|
44
|
-
runAgent({ agent, store, sessionId, input: "..." }); // a caller sends
|
|
45
|
-
runAgent({ agent, store, claim }); // a worker runs what it was handed
|
|
46
|
-
```
|
|
17
|
+
> **Pre-release.** The API changes without deprecation aliases, so pin an exact version: `bun add aglib@npm:@a-dray/aglib@<version>`. The unscoped name is a lookalike of an existing package; the alias keeps imports as `aglib`.
|
|
47
18
|
|
|
48
|
-
|
|
49
|
-
hand. A claim with an empty queue is a resumption: the log is continued rather than begun again, and
|
|
50
|
-
the tool call that already ran is read from it, never issued twice. A harness that cannot restart
|
|
51
|
-
from history ends that run instead of continuing it, so a session nothing can finish is closed once
|
|
52
|
-
rather than handed out for ever.
|
|
53
|
-
|
|
54
|
-
**A model is a value, not a name.** A `Model` is a provider, a credential and a model id together;
|
|
55
|
-
picking a different one is picking a different `Model`, and routing between them is a record lookup
|
|
56
|
-
you write. Nothing here resolves a name, so nothing here owns a naming convention — names come *out*
|
|
57
|
-
(`ModelResponse.model`, and what `price` reads) and never go in.
|
|
58
|
-
|
|
59
|
-
**You are told when to look, and you still keep a heartbeat.** `store.watch()` is an optional
|
|
60
|
-
interrupt line — which session moved, whether it has work owed, nothing else. A wake may be spurious
|
|
61
|
-
or lost by design, so it removes latency rather than the need to ask; without one, a worker polls and
|
|
62
|
-
that interval is the whole of a message's latency.
|
|
63
|
-
|
|
64
|
-
**Where a port has more than one implementation, it has one executable contract.**
|
|
65
|
-
`aglib/model/conformance`, `aglib/store/conformance` and `aglib/sandbox/conformance` are the cases an
|
|
66
|
-
adapter must pass, as an inert list you run under your own test framework. Point one at a model, a
|
|
67
|
-
store or a sandbox you wrote and find out whether it means what the interface says. A subject
|
|
68
|
-
declares what it actually does — whether its output streams arrive apart, how far its change feed
|
|
69
|
-
reaches — and the suite holds it to exactly that rather than assuming.
|
|
70
|
-
|
|
71
|
-
## Quickstart
|
|
19
|
+
## Run one
|
|
72
20
|
|
|
73
21
|
```bash
|
|
74
22
|
bun install --frozen-lockfile
|
|
75
23
|
echo 'OPENROUTER_API_KEY=sk-or-...' > .env
|
|
76
|
-
bun run recipe personal-agent "what did I decide about pricing?"
|
|
77
|
-
```
|
|
78
24
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
import { createNativeHarness } from "aglib/harness";
|
|
82
|
-
import { createOpenRouterModel } from "aglib/model/adapters/openai-compatible";
|
|
83
|
-
import { createSqliteStore } from "aglib/store/adapters/sqlite";
|
|
84
|
-
import { Database } from "bun:sqlite";
|
|
85
|
-
import { z } from "zod";
|
|
86
|
-
|
|
87
|
-
const bookkeeper = {
|
|
88
|
-
id: "bookkeeper", version: "1",
|
|
89
|
-
instructions: "Answer from the ledger.",
|
|
90
|
-
harness: createNativeHarness({
|
|
91
|
-
model: createOpenRouterModel({ apiKey: process.env.OPENROUTER_API_KEY!, model: "deepseek/deepseek-v4-flash" }),
|
|
92
|
-
}),
|
|
93
|
-
tools: [defineTool({
|
|
94
|
-
name: "read_ledger",
|
|
95
|
-
description: "Read the September ledger.",
|
|
96
|
-
annotations: { readOnly: true },
|
|
97
|
-
schema: z.object({}),
|
|
98
|
-
execute: () => ({ content: "September closes at 1250 GBP." }),
|
|
99
|
-
})],
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
const run = runAgent({
|
|
103
|
-
agent: bookkeeper,
|
|
104
|
-
store: createSqliteStore({ database: new Database("agent.db") }),
|
|
105
|
-
input: "What is the September balance?",
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
for await (const update of run) if (update.type === "text.delta") process.stdout.write(update.text);
|
|
109
|
-
const result = await run.result;
|
|
110
|
-
if (result.status === "completed") console.log(textOf(result.output));
|
|
25
|
+
bun run recipe native-agent # a conversation, in your terminal
|
|
26
|
+
bun run recipe native-agent "what did I decide?" # …or one shot, for a pipe
|
|
111
27
|
```
|
|
112
28
|
|
|
113
|
-
|
|
114
|
-
not a different program.
|
|
115
|
-
|
|
116
|
-
A run answers with `usage` on every outcome — completed, cancelled or failed — summed from the entries it committed, so a harness reports no total of its own and a run that burned tokens and then failed says so.
|
|
117
|
-
|
|
118
|
-
There is no price list here and no spend ceiling. The counts are the fact; the rates are yours, and so is what to do when a run gets expensive.
|
|
29
|
+
## The shape
|
|
119
30
|
|
|
120
|
-
|
|
31
|
+
```text
|
|
32
|
+
src/
|
|
33
|
+
json.ts result.ts content.ts agent.ts run.ts render.ts terminal.ts
|
|
121
34
|
|
|
122
|
-
|
|
123
|
-
|
|
35
|
+
session/ the log — this IS the session's state
|
|
36
|
+
tools/ declaring and running tools
|
|
37
|
+
model/ store/ sandbox/ harness/ each: <port>.ts → adapters/
|
|
38
|
+
```
|
|
124
39
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
| [`recipes/agent-service`](recipes/agent-service/README.md) | The queue, atomic cross-session handoff, a harness per session, a sandbox per session, live views. |
|
|
40
|
+
Four ports, each with adapters. Three ship an executable conformance suite;
|
|
41
|
+
`harness` does not, because what one must prove depends on what it declares. Our
|
|
42
|
+
own loop is `harness/adapters/native`, beside the others rather than above them.
|
|
129
43
|
|
|
130
|
-
|
|
44
|
+
**`src/` is the library** — everything published to npm, and nothing that knows
|
|
45
|
+
what your agent is for. **`recipes/` is where most of it earns its place** —
|
|
46
|
+
runnable programs that each answer one production question. An export needs a
|
|
47
|
+
consumer: a recipe that composes it, or a conformance suite run against an
|
|
48
|
+
implementation here. A surface with neither fails `bun run check`.
|
|
131
49
|
|
|
132
|
-
|
|
133
|
-
control plane, or finished agent. It does not make model output trustworthy, and a local
|
|
134
|
-
sandbox is a host process, not a sandbox — ask for `isolation: "required"` and a provider that
|
|
135
|
-
cannot deliver it fails rather than pretending. `adapters/docker` is the one that can: a container
|
|
136
|
-
from the local daemon, no account and no vendor SDK. A hosted box is yours to adapt, and
|
|
137
|
-
`agent-service` shows one.
|
|
50
|
+
## Three recipes
|
|
138
51
|
|
|
139
|
-
|
|
140
|
-
|
|
52
|
+
One per answer to "who does the reasoning". Each is the smallest thing that
|
|
53
|
+
still makes its point, and between them they exercise every seam.
|
|
141
54
|
|
|
142
|
-
|
|
55
|
+
| Recipe | Whose loop | What it shows |
|
|
56
|
+
| --- | --- | --- |
|
|
57
|
+
| [`native-agent`](recipes/native-agent/README.md) | ours | The whole surface in one program — memory, skills, subagents, a sandbox you choose — and the one thing only our own loop offers: a message reaching a busy session mid-turn. |
|
|
58
|
+
| [`vendored-agent`](recipes/vendored-agent/README.md) | a vendor's, from its library | What a vendor library must expose for the log to stay the state. Pi's tools are values you re-point at your sandbox and its transcript is a field you assign, so an interrupted run continues — `recovery: "history"`. Its README says what the tiers below that get you. |
|
|
59
|
+
| [`coding-agent`](recipes/coding-agent/README.md) | a vendor's, over a protocol | The cheapest containment on offer: any ACP agent started *inside* your sandbox, its work arriving as your entries, for one row of argv. |
|
|
143
60
|
|
|
144
|
-
|
|
145
|
-
orchestration — around whatever does the reasoning inside.
|
|
61
|
+
## Where to read next
|
|
146
62
|
|
|
147
|
-
|
|
|
63
|
+
| Question | Document |
|
|
148
64
|
| --- | --- |
|
|
149
|
-
|
|
|
150
|
-
|
|
|
151
|
-
|
|
|
152
|
-
|
|
|
65
|
+
| What is this, for whom, and why? | [`docs/PRODUCT.md`](docs/PRODUCT.md) |
|
|
66
|
+
| What is the outward surface? | [`docs/INTERFACE.md`](docs/INTERFACE.md) |
|
|
67
|
+
| How does it work, and who owns each part? | [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) |
|
|
68
|
+
| How is the source written? | [`docs/CODE.md`](docs/CODE.md) |
|
|
69
|
+
| How do I work in here? | [`AGENTS.md`](AGENTS.md) |
|
|
153
70
|
|
|
154
|
-
|
|
155
|
-
control points each one gives you.
|
|
71
|
+
[`docs/REFERENCE.md`](docs/REFERENCE.md) is generated by `bun run docs`.
|
|
156
72
|
|
|
157
73
|
## Development
|
|
158
74
|
|
|
159
75
|
```bash
|
|
160
76
|
bun run check # typecheck, tests, build, Node verification, recipes, docs, invariants
|
|
161
|
-
bun run recipe personal-agent "..."
|
|
162
77
|
```
|
|
163
78
|
|
|
164
|
-
`bun run check` is the green gate. A failing check is a decision, not an
|
|
165
|
-
or change the check deliberately in the same commit
|
|
79
|
+
`bun run check` is the green gate. A failing check is a decision, not an
|
|
80
|
+
obstacle: fix the code, or change the check deliberately in the same commit
|
|
81
|
+
with the reason in the message.
|
|
166
82
|
|
|
167
|
-
|
|
168
|
-
the
|
|
83
|
+
Live tests cost money and are opt-in twice — the credential, and a flag:
|
|
84
|
+
`AGLIB_LIVE_MODEL=1` for the cases that reach a provider, `AGLIB_LIVE_ACP=1` for
|
|
85
|
+
the ones that start a vendor's agent over `npx`. The Docker cases need no flag:
|
|
86
|
+
they run where a daemon and the image are already there, and name themselves
|
|
87
|
+
when they skip.
|
|
169
88
|
|
|
170
89
|
## License
|
|
171
90
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Harness } from "../../harness.js";
|
|
2
2
|
import type { Sandbox } from "../../../sandbox/sandbox.js";
|
|
3
3
|
import type { Decide } from "../../../tools/tool.js";
|
|
4
|
+
import { type Failure, type Result } from "../../../result.js";
|
|
4
5
|
/** An agent process: argv and the environment that selects its provider and model. */
|
|
5
6
|
export interface AcpAgent {
|
|
6
7
|
command: readonly string[];
|
|
@@ -51,21 +52,18 @@ export interface AcpHarnessOptions {
|
|
|
51
52
|
sandbox: Sandbox;
|
|
52
53
|
mcpServers?: readonly AcpMcpServer[];
|
|
53
54
|
/**
|
|
54
|
-
* What to set before prompting, by option id
|
|
55
|
-
*
|
|
56
|
-
*
|
|
55
|
+
* What to set before prompting, by option id — the agent's own ids, from
|
|
56
|
+
* `onConfig`. The only configuration door, deliberately: an option this
|
|
57
|
+
* package named would be a guess at another product's vocabulary, and one
|
|
58
|
+
* that goes stale the first time an agent ships an axis nobody thought of.
|
|
59
|
+
*
|
|
60
|
+
* Applied only where the agent published that option and, for a select, that
|
|
61
|
+
* value — otherwise the run fails naming what it does offer, rather than
|
|
62
|
+
* quietly running something else.
|
|
57
63
|
*/
|
|
58
64
|
select?: Readonly<Record<string, string | boolean>>;
|
|
59
|
-
/**
|
|
60
|
-
* Requested model, matched against whichever option the agent categorised as
|
|
61
|
-
* its model selector. A convenience over `select` for the one option every
|
|
62
|
-
* agent has, and it fails the same way.
|
|
63
|
-
*/
|
|
64
|
-
model?: string;
|
|
65
65
|
/** What the agent published. The caller persists it so a session can offer the agent's own choices. */
|
|
66
66
|
onConfig?(options: readonly AcpConfigOption[]): void;
|
|
67
|
-
/** Agent-defined mode. Choosing one that asks before acting is what routes its own tools through `decide`. */
|
|
68
|
-
mode?: string;
|
|
69
67
|
/**
|
|
70
68
|
* Applied to the agent's own tools, per call, before they run.
|
|
71
69
|
*
|
|
@@ -90,3 +88,21 @@ export interface AcpHarnessOptions {
|
|
|
90
88
|
* agents can dispatch and message each other.
|
|
91
89
|
*/
|
|
92
90
|
export declare function createAcpHarness(options: AcpHarnessOptions): Harness;
|
|
91
|
+
/**
|
|
92
|
+
* What an agent offers, before anyone has asked it anything.
|
|
93
|
+
*
|
|
94
|
+
* Over this protocol an agent publishes its options in the answer to
|
|
95
|
+
* `session/new`, so nothing knows what a model, a mode or a reasoning level is
|
|
96
|
+
* called until a session exists — and a session exists on the first prompt.
|
|
97
|
+
* That left a client with a menu it could not draw until after the choice it
|
|
98
|
+
* wanted to offer had already been made.
|
|
99
|
+
*
|
|
100
|
+
* So this opens one and asks. It spawns, initialises, opens a session, reads
|
|
101
|
+
* what came back and closes: a process and a handshake, no prompt, no
|
|
102
|
+
* generation, nothing billed. The session it opened is thrown away — the run
|
|
103
|
+
* opens its own, exactly as it did before.
|
|
104
|
+
*/
|
|
105
|
+
export declare function acpOptions(input: {
|
|
106
|
+
agent: AcpAgent;
|
|
107
|
+
sandbox: Sandbox;
|
|
108
|
+
}): Promise<Result<readonly AcpConfigOption[], Failure>>;
|
|
@@ -2,6 +2,18 @@ import { textOf } from "../../../content.js";
|
|
|
2
2
|
import { err, ok } from "../../../result.js";
|
|
3
3
|
import { createRpc } from "./rpc.js";
|
|
4
4
|
const PROTOCOL_VERSION = 1;
|
|
5
|
+
/**
|
|
6
|
+
* The handshake, and what it claims for us.
|
|
7
|
+
*
|
|
8
|
+
* Two callers open a connection — a turn, and `acpOptions` — and an agent
|
|
9
|
+
* decides what to offer from what the client says it can do. A second copy of
|
|
10
|
+
* this is a second answer to the same question, and the two would drift.
|
|
11
|
+
*/
|
|
12
|
+
const initialize = (rpc) => rpc.request("initialize", {
|
|
13
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
14
|
+
clientCapabilities: { fs: { readTextFile: true, writeTextFile: true }, terminal: true },
|
|
15
|
+
clientInfo: { name: "aglib", version: "0" },
|
|
16
|
+
});
|
|
5
17
|
/**
|
|
6
18
|
* Runs a foreign coding agent over the Agent Client Protocol.
|
|
7
19
|
*
|
|
@@ -17,13 +29,48 @@ export function createAcpHarness(options) {
|
|
|
17
29
|
id: options.id,
|
|
18
30
|
// Its tools are its own. We gate them and we record them; we do not claim
|
|
19
31
|
// to have validated arguments we never had a schema for.
|
|
20
|
-
toolUse: "harness",
|
|
21
32
|
// The agent owns its context. Our entries describe what it did, and cannot
|
|
22
33
|
// by themselves put it back mid-turn.
|
|
23
34
|
recovery: "none",
|
|
24
35
|
run: (context) => runTurn(options, context),
|
|
25
36
|
};
|
|
26
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* What an agent offers, before anyone has asked it anything.
|
|
40
|
+
*
|
|
41
|
+
* Over this protocol an agent publishes its options in the answer to
|
|
42
|
+
* `session/new`, so nothing knows what a model, a mode or a reasoning level is
|
|
43
|
+
* called until a session exists — and a session exists on the first prompt.
|
|
44
|
+
* That left a client with a menu it could not draw until after the choice it
|
|
45
|
+
* wanted to offer had already been made.
|
|
46
|
+
*
|
|
47
|
+
* So this opens one and asks. It spawns, initialises, opens a session, reads
|
|
48
|
+
* what came back and closes: a process and a handshake, no prompt, no
|
|
49
|
+
* generation, nothing billed. The session it opened is thrown away — the run
|
|
50
|
+
* opens its own, exactly as it did before.
|
|
51
|
+
*/
|
|
52
|
+
export async function acpOptions(input) {
|
|
53
|
+
const started = await input.sandbox.spawn({
|
|
54
|
+
command: input.agent.command,
|
|
55
|
+
cwd: input.sandbox.root,
|
|
56
|
+
...(input.agent.env ? { env: input.agent.env } : {}),
|
|
57
|
+
});
|
|
58
|
+
if (!started.ok)
|
|
59
|
+
return err(started.error);
|
|
60
|
+
const rpc = createRpc(started.value);
|
|
61
|
+
try {
|
|
62
|
+
const ready = await initialize(rpc);
|
|
63
|
+
if (!ready.ok)
|
|
64
|
+
return err(ready.error);
|
|
65
|
+
const opened = await rpc.request("session/new", { cwd: input.sandbox.root, mcpServers: [] });
|
|
66
|
+
if (!opened.ok)
|
|
67
|
+
return err(opened.error);
|
|
68
|
+
return ok(readOptions(opened.value).options);
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
rpc.close();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
27
74
|
async function runTurn(options, context) {
|
|
28
75
|
const started = await options.sandbox.spawn({
|
|
29
76
|
command: options.agent.command,
|
|
@@ -67,11 +114,7 @@ function createTurn(options, context, rpc) {
|
|
|
67
114
|
serveTerminals();
|
|
68
115
|
servePermission();
|
|
69
116
|
rpc.onNotify("session/update", (params) => { void receive(params); });
|
|
70
|
-
const ready = await rpc
|
|
71
|
-
protocolVersion: PROTOCOL_VERSION,
|
|
72
|
-
clientCapabilities: { fs: { readTextFile: true, writeTextFile: true }, terminal: true },
|
|
73
|
-
clientInfo: { name: "aglib", version: "0" },
|
|
74
|
-
});
|
|
117
|
+
const ready = await initialize(rpc);
|
|
75
118
|
if (!ready.ok)
|
|
76
119
|
return { status: "failed", error: ready.error };
|
|
77
120
|
const capabilities = field(ready.value, "agentCapabilities");
|
|
@@ -143,24 +186,15 @@ function createTurn(options, context, rpc) {
|
|
|
143
186
|
const { options: published, legacyModel } = readOptions(opened);
|
|
144
187
|
if (published.length)
|
|
145
188
|
options.onConfig?.(published);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return err({
|
|
156
|
-
code: "unsupported",
|
|
157
|
-
message: `${options.id} publishes no model selector, so '${options.model}' cannot be chosen over the protocol. Point it at a model through its environment instead.`,
|
|
158
|
-
retryable: false,
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
wanted[selector.id] = options.model;
|
|
162
|
-
}
|
|
163
|
-
for (const [option, value] of Object.entries(wanted)) {
|
|
189
|
+
// One door, and `select` is it. There were two more — `mode`, which sent
|
|
190
|
+
// `session/set_mode`, and `model`, which found whichever option the agent
|
|
191
|
+
// had categorised as its selector. Both were conveniences naming an axis,
|
|
192
|
+
// and naming an axis is guessing at a vocabulary that is not ours: the
|
|
193
|
+
// agents measured here publish five categories between them, and no
|
|
194
|
+
// shorthand was ever going to cover the next one. `session/set_config_option`
|
|
195
|
+
// with `configId: "mode"` was checked against `claude-agent-acp` and does
|
|
196
|
+
// what `session/set_mode` did, answering with the new state as well.
|
|
197
|
+
for (const [option, value] of Object.entries(options.select ?? {})) {
|
|
164
198
|
const published_ = published.find((candidate) => candidate.id === option);
|
|
165
199
|
if (!published_) {
|
|
166
200
|
return err({
|