@a-dray/aglib 0.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/LICENSE +21 -0
- package/README.md +172 -0
- package/dist/agent.d.ts +158 -0
- package/dist/agent.js +2 -0
- package/dist/agent.js.map +1 -0
- package/dist/content.d.ts +45 -0
- package/dist/content.js +11 -0
- package/dist/content.js.map +1 -0
- package/dist/harness/adapters/acp/index.d.ts +92 -0
- package/dist/harness/adapters/acp/index.js +479 -0
- package/dist/harness/adapters/acp/index.js.map +1 -0
- package/dist/harness/adapters/acp/rpc.d.ts +23 -0
- package/dist/harness/adapters/acp/rpc.js +103 -0
- package/dist/harness/adapters/acp/rpc.js.map +1 -0
- package/dist/harness/adapters/native/compaction.d.ts +16 -0
- package/dist/harness/adapters/native/compaction.js +55 -0
- package/dist/harness/adapters/native/compaction.js.map +1 -0
- package/dist/harness/adapters/native/index.d.ts +2 -0
- package/dist/harness/adapters/native/index.js +2 -0
- package/dist/harness/adapters/native/index.js.map +1 -0
- package/dist/harness/adapters/native/loop.d.ts +25 -0
- package/dist/harness/adapters/native/loop.js +153 -0
- package/dist/harness/adapters/native/loop.js.map +1 -0
- package/dist/harness/harness.d.ts +129 -0
- package/dist/harness/harness.js +2 -0
- package/dist/harness/harness.js.map +1 -0
- package/dist/harness/index.d.ts +3 -0
- package/dist/harness/index.js +2 -0
- package/dist/harness/index.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/json.d.ts +4 -0
- package/dist/json.js +2 -0
- package/dist/json.js.map +1 -0
- package/dist/model/adapters/anthropic/index.d.ts +34 -0
- package/dist/model/adapters/anthropic/index.js +271 -0
- package/dist/model/adapters/anthropic/index.js.map +1 -0
- package/dist/model/adapters/fake/index.d.ts +27 -0
- package/dist/model/adapters/fake/index.js +37 -0
- package/dist/model/adapters/fake/index.js.map +1 -0
- package/dist/model/adapters/openai-compatible/index.d.ts +29 -0
- package/dist/model/adapters/openai-compatible/index.js +226 -0
- package/dist/model/adapters/openai-compatible/index.js.map +1 -0
- package/dist/model/conformance.d.ts +210 -0
- package/dist/model/conformance.js +427 -0
- package/dist/model/conformance.js.map +1 -0
- package/dist/model/index.d.ts +2 -0
- package/dist/model/index.js +2 -0
- package/dist/model/index.js.map +1 -0
- package/dist/model/model.d.ts +85 -0
- package/dist/model/model.js +8 -0
- package/dist/model/model.js.map +1 -0
- package/dist/result.d.ts +23 -0
- package/dist/result.js +3 -0
- package/dist/result.js.map +1 -0
- package/dist/run.d.ts +10 -0
- package/dist/run.js +477 -0
- package/dist/run.js.map +1 -0
- package/dist/sandbox/adapters/docker.d.ts +22 -0
- package/dist/sandbox/adapters/docker.js +443 -0
- package/dist/sandbox/adapters/docker.js.map +1 -0
- package/dist/sandbox/adapters/local.d.ts +11 -0
- package/dist/sandbox/adapters/local.js +284 -0
- package/dist/sandbox/adapters/local.js.map +1 -0
- package/dist/sandbox/conformance.d.ts +73 -0
- package/dist/sandbox/conformance.js +309 -0
- package/dist/sandbox/conformance.js.map +1 -0
- package/dist/sandbox/index.d.ts +1 -0
- package/dist/sandbox/index.js +2 -0
- package/dist/sandbox/index.js.map +1 -0
- package/dist/sandbox/sandbox.d.ts +172 -0
- package/dist/sandbox/sandbox.js +2 -0
- package/dist/sandbox/sandbox.js.map +1 -0
- package/dist/session/entry.d.ts +196 -0
- package/dist/session/entry.js +2 -0
- package/dist/session/entry.js.map +1 -0
- package/dist/session/index.d.ts +2 -0
- package/dist/session/index.js +2 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/log.d.ts +20 -0
- package/dist/session/log.js +24 -0
- package/dist/session/log.js.map +1 -0
- package/dist/session/messages.d.ts +42 -0
- package/dist/session/messages.js +99 -0
- package/dist/session/messages.js.map +1 -0
- package/dist/store/adapters/sqlite.d.ts +20 -0
- package/dist/store/adapters/sqlite.js +365 -0
- package/dist/store/adapters/sqlite.js.map +1 -0
- package/dist/store/conformance.d.ts +66 -0
- package/dist/store/conformance.js +580 -0
- package/dist/store/conformance.js.map +1 -0
- package/dist/store/index.d.ts +1 -0
- package/dist/store/index.js +2 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/store.d.ts +244 -0
- package/dist/store/store.js +2 -0
- package/dist/store/store.js.map +1 -0
- package/dist/tools/execute.d.ts +24 -0
- package/dist/tools/execute.js +105 -0
- package/dist/tools/execute.js.map +1 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +2 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/tool.d.ts +106 -0
- package/dist/tools/tool.js +34 -0
- package/dist/tools/tool.js.map +1 -0
- package/package.json +119 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adam Dray
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# aglib
|
|
2
|
+
|
|
3
|
+
A small TypeScript toolkit for building **your own agent harness**, inside an application you own.
|
|
4
|
+
|
|
5
|
+
You bring the product — users, tenancy, channels, UI, deployment, policy. aglib gives you a
|
|
6
|
+
session log you can resume, a loop that runs against it, tools under your own permission rule,
|
|
7
|
+
somewhere contained to run them, and four seams with adapters: model, store, sandbox, harness.
|
|
8
|
+
|
|
9
|
+
> **Pre-release.** The API changes without deprecation aliases, so pin an exact version: `bun add aglib@npm:@a-dray/aglib@<version>`. Both recipes run. The unscoped name is a lookalike of an existing package; the alias keeps imports as `aglib`.
|
|
10
|
+
|
|
11
|
+
## The shape
|
|
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
|
+
```
|
|
47
|
+
|
|
48
|
+
The claim carries the session, the position and the queue together, so a worker lines nothing up by
|
|
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
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
bun install --frozen-lockfile
|
|
75
|
+
echo 'OPENROUTER_API_KEY=sk-or-...' > .env
|
|
76
|
+
bun run recipe personal-agent "what did I decide about pricing?"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { runAgent, defineTool, textOf } from "aglib";
|
|
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));
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Drop `store` and the same agent runs entirely in memory. Durability is a composition choice,
|
|
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.
|
|
119
|
+
|
|
120
|
+
## Recipes
|
|
121
|
+
|
|
122
|
+
Two applications, chosen because between them they exercise every seam. They are the
|
|
123
|
+
specification: anything with no call site in one of them should not exist.
|
|
124
|
+
|
|
125
|
+
| Recipe | What it proves |
|
|
126
|
+
| --- | --- |
|
|
127
|
+
| [`recipes/personal-agent`](recipes/personal-agent/README.md) | The log, compaction, context lifetimes, tools, session search. Memory built entirely *on* the library, not in it. |
|
|
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. |
|
|
129
|
+
|
|
130
|
+
## What it is not
|
|
131
|
+
|
|
132
|
+
Not a workflow engine, channel gateway, scheduler, memory product, prompt registry, deployment
|
|
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.
|
|
138
|
+
|
|
139
|
+
It also ships no price list. Rates go stale between releases and only one provider ever reported a
|
|
140
|
+
cost, so the log carries token counts and the money is arithmetic over a table you pass in.
|
|
141
|
+
|
|
142
|
+
## Four ways in
|
|
143
|
+
|
|
144
|
+
aglib is for building the **outer** harness — the durable session, the tools, the policy, the
|
|
145
|
+
orchestration — around whatever does the reasoning inside.
|
|
146
|
+
|
|
147
|
+
| You want | You do |
|
|
148
|
+
| --- | --- |
|
|
149
|
+
| A loop you own end to end | Use `adapters/native` |
|
|
150
|
+
| Your own reasoning strategy | Write a `Harness` and drop it in |
|
|
151
|
+
| A vendor SDK, deeply integrated | Wrap it as a `Harness` — you keep its prompt, tools and settings, and choose which to override |
|
|
152
|
+
| Somebody else's whole agent, cheaply | Use `adapters/acp` — any agent in the ACP registry, still inside your log, tools and sandbox |
|
|
153
|
+
|
|
154
|
+
The last two are not equivalent, and `recipes/agent-service` publishes a table of exactly which
|
|
155
|
+
control points each one gives you.
|
|
156
|
+
|
|
157
|
+
## Development
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
bun run check # typecheck, tests, build, Node verification, recipes, docs, invariants
|
|
161
|
+
bun run recipe personal-agent "..."
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`bun run check` is the green gate. A failing check is a decision, not an obstacle: fix the code,
|
|
165
|
+
or change the check deliberately in the same commit with the reason in the message.
|
|
166
|
+
|
|
167
|
+
See [`AGENTS.md`](AGENTS.md) for the map, and [`docs/`](docs/) for the four documents that own
|
|
168
|
+
the product.
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT. See [`LICENSE`](LICENSE).
|
package/dist/agent.d.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { Content } from "./content.js";
|
|
2
|
+
import type { Harness } from "./harness/harness.js";
|
|
3
|
+
import type { Decide, Tool } from "./tools/tool.js";
|
|
4
|
+
import type { Delivery, Runnable, Store } from "./store/store.js";
|
|
5
|
+
import type { Update } from "./harness/harness.js";
|
|
6
|
+
import type { Failure } from "./result.js";
|
|
7
|
+
import type { From, Usage } from "./session/entry.js";
|
|
8
|
+
/** An inert declaration. Nothing here opens a connection or holds state. */
|
|
9
|
+
export interface Agent {
|
|
10
|
+
id: string;
|
|
11
|
+
/** Bump when instructions, tools or configuration stop being compatible with an existing session. */
|
|
12
|
+
version: string;
|
|
13
|
+
instructions: Content;
|
|
14
|
+
harness: Harness;
|
|
15
|
+
tools?: readonly Tool[];
|
|
16
|
+
/** Per-call policy over parsed arguments. Absent means every call executes. */
|
|
17
|
+
decide?: Decide;
|
|
18
|
+
/** Runs once as an activation ends, whatever ended it; its deliveries commit with the final entries. */
|
|
19
|
+
finished?(run: {
|
|
20
|
+
sessionId: string;
|
|
21
|
+
runId: string;
|
|
22
|
+
outcome: "completed" | "failed" | "cancelled";
|
|
23
|
+
output: Content;
|
|
24
|
+
}): readonly Delivery[] | Promise<readonly Delivery[]>;
|
|
25
|
+
/**
|
|
26
|
+
* Ceilings on one activation, over the facts this library holds: turns and
|
|
27
|
+
* tool calls are on the log, and a deadline is the clock.
|
|
28
|
+
*
|
|
29
|
+
* There is no ceiling on money, and the asymmetry is the point. A spend limit
|
|
30
|
+
* would have to read a rate table the library does not have and should not
|
|
31
|
+
* carry, so it would take a function from the caller and then need defending
|
|
32
|
+
* against it — a code for "you declared a ceiling nothing can hold", a rule
|
|
33
|
+
* for a generation the function could not price, a check for a price that is
|
|
34
|
+
* negative or not a number. All of that is the cost of enforcing something
|
|
35
|
+
* from a fact we do not own, and enforcing it is policy besides: the two
|
|
36
|
+
* applications that wanted one wanted it to stop at different moments.
|
|
37
|
+
*
|
|
38
|
+
* `RunResult.usage` is what a caller needs from here, and it is solid. What
|
|
39
|
+
* that costs, and what to do about it, is priced by whoever holds the rates.
|
|
40
|
+
*/
|
|
41
|
+
limits?: {
|
|
42
|
+
maxTurns?: number;
|
|
43
|
+
maxToolCalls?: number;
|
|
44
|
+
deadline?: string;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* How an activation ended, and what it consumed on the way.
|
|
49
|
+
*
|
|
50
|
+
* `usage` is on every outcome, not only a completed one: a run that burned four
|
|
51
|
+
* dollars of tokens and then failed burned them, and a caller that has to ask
|
|
52
|
+
* the log to find that out has been handed a result missing the expensive half
|
|
53
|
+
* of what happened.
|
|
54
|
+
*
|
|
55
|
+
* Summed from the `assistant` entries this activation committed, which is why a
|
|
56
|
+
* harness does not report it — the log already holds every generation, and a
|
|
57
|
+
* second total is a second answer. Money is not here: the counts are the fact,
|
|
58
|
+
* and the rates that turn them into money belong to the deployment.
|
|
59
|
+
*/
|
|
60
|
+
export type RunResult = {
|
|
61
|
+
status: "completed";
|
|
62
|
+
output: Content;
|
|
63
|
+
usage: Usage;
|
|
64
|
+
seq: number;
|
|
65
|
+
} | {
|
|
66
|
+
status: "cancelled";
|
|
67
|
+
usage: Usage;
|
|
68
|
+
seq: number;
|
|
69
|
+
} | {
|
|
70
|
+
status: "failed";
|
|
71
|
+
error: Failure;
|
|
72
|
+
usage: Usage;
|
|
73
|
+
seq: number;
|
|
74
|
+
};
|
|
75
|
+
export interface AgentRun extends AsyncIterable<Update> {
|
|
76
|
+
readonly result: Promise<RunResult>;
|
|
77
|
+
cancel(): void;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* One piece of input for an activation, and where it came from.
|
|
81
|
+
*
|
|
82
|
+
* Always the object form, because `Content` may itself be an array of parts and
|
|
83
|
+
* a bare array therefore cannot say whether it is one multi-part message or
|
|
84
|
+
* several messages. `RunAgentOptions.input` still takes plain content for the
|
|
85
|
+
* common case; only the *many* form is required to name each arrival.
|
|
86
|
+
*/
|
|
87
|
+
export interface Arrival {
|
|
88
|
+
input: Content;
|
|
89
|
+
from?: From;
|
|
90
|
+
}
|
|
91
|
+
interface RunAgentBase {
|
|
92
|
+
agent: Agent;
|
|
93
|
+
/** Opaque application index key, used only when this run creates the session. */
|
|
94
|
+
key?: string;
|
|
95
|
+
context?: {
|
|
96
|
+
run?: string;
|
|
97
|
+
turn?: string;
|
|
98
|
+
};
|
|
99
|
+
signal?: AbortSignal;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* An activation is working one of two things, and never both.
|
|
103
|
+
*
|
|
104
|
+
* A caller is **sending** — a person typed, a webhook fired, a test asked a
|
|
105
|
+
* question. Or a worker is running **what the store handed it**, which carries
|
|
106
|
+
* the session, the position to write from, and the deliveries to consume, all
|
|
107
|
+
* as one value that arrived together and cannot be recombined wrongly.
|
|
108
|
+
*
|
|
109
|
+
* That second form replaced three fields a worker had to line up by hand:
|
|
110
|
+
* `sessionId`, the position, and how many deliveries to take. Every one of them
|
|
111
|
+
* was only ever a field of the claim, so each was a value another field already
|
|
112
|
+
* determined — and the one that mattered, taking the deliveries, silently
|
|
113
|
+
* left the input queued for ever when it was forgotten. Both applications
|
|
114
|
+
* written on this wrote the same eight lines to get it right.
|
|
115
|
+
*/
|
|
116
|
+
export type RunAgentOptions = RunAgentBase & ({
|
|
117
|
+
/** One message as plain content, one named arrival, or several named arrivals. */
|
|
118
|
+
input: Content | Arrival | readonly Arrival[];
|
|
119
|
+
/**
|
|
120
|
+
* Which session it goes to, as a UUID. A new one is opened when this is
|
|
121
|
+
* omitted. An application's own naming for a conversation goes in `key`,
|
|
122
|
+
* which is opaque to the library; a store is entitled to keep a session
|
|
123
|
+
* id in a `uuid` column and refuse anything else.
|
|
124
|
+
*/
|
|
125
|
+
sessionId?: string;
|
|
126
|
+
/** Omit for an ephemeral run: the log lives in memory and nothing is persisted. */
|
|
127
|
+
store?: Store;
|
|
128
|
+
claim?: never;
|
|
129
|
+
} | {
|
|
130
|
+
/**
|
|
131
|
+
* What `store.next()` or `store.interrupted()` handed this worker.
|
|
132
|
+
*
|
|
133
|
+
* Its deliveries become this activation's input and are consumed by the
|
|
134
|
+
* write that commits them, in the same compare-and-swap that checks the
|
|
135
|
+
* position — so a worker that loses the race loses its turn and never the
|
|
136
|
+
* messages. A claim with an empty queue is a **resumption**: nothing opens
|
|
137
|
+
* it, the loop continues the committed log, and the effect that already
|
|
138
|
+
* happened is read there rather than asked for again. Only a harness
|
|
139
|
+
* declaring `recovery: "history"` is given one.
|
|
140
|
+
*/
|
|
141
|
+
claim: Runnable;
|
|
142
|
+
/**
|
|
143
|
+
* Required here, unlike a caller sending. A claim is a position in a log
|
|
144
|
+
* and a queue to consume, and both of those are the store's — without one
|
|
145
|
+
* the deliveries would be replayed into memory and left queued for ever.
|
|
146
|
+
*/
|
|
147
|
+
store: Store;
|
|
148
|
+
/**
|
|
149
|
+
* Something the worker is adding of its own, ahead of what was waiting.
|
|
150
|
+
*
|
|
151
|
+
* One real use: orientation for a harness whose protocol has no system
|
|
152
|
+
* prompt, which has nowhere else to go and must lead. Adding nothing is
|
|
153
|
+
* the ordinary case.
|
|
154
|
+
*/
|
|
155
|
+
input?: Content | Arrival | readonly Arrival[];
|
|
156
|
+
sessionId?: never;
|
|
157
|
+
});
|
|
158
|
+
export {};
|
package/dist/agent.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent.js","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"","sourcesContent":["import type { Content } from \"./content.js\";\nimport type { Harness } from \"./harness/harness.js\";\nimport type { Decide, Tool } from \"./tools/tool.js\";\nimport type { Delivery, Runnable, Store } from \"./store/store.js\";\nimport type { Update } from \"./harness/harness.js\";\nimport type { Failure } from \"./result.js\";\nimport type { From, Usage } from \"./session/entry.js\";\n\n/** An inert declaration. Nothing here opens a connection or holds state. */\nexport interface Agent {\n id: string;\n /** Bump when instructions, tools or configuration stop being compatible with an existing session. */\n version: string;\n instructions: Content;\n harness: Harness;\n tools?: readonly Tool[];\n /** Per-call policy over parsed arguments. Absent means every call executes. */\n decide?: Decide;\n /** Runs once as an activation ends, whatever ended it; its deliveries commit with the final entries. */\n finished?(run: {\n sessionId: string; runId: string;\n outcome: \"completed\" | \"failed\" | \"cancelled\";\n output: Content;\n }): readonly Delivery[] | Promise<readonly Delivery[]>;\n /**\n * Ceilings on one activation, over the facts this library holds: turns and\n * tool calls are on the log, and a deadline is the clock.\n *\n * There is no ceiling on money, and the asymmetry is the point. A spend limit\n * would have to read a rate table the library does not have and should not\n * carry, so it would take a function from the caller and then need defending\n * against it — a code for \"you declared a ceiling nothing can hold\", a rule\n * for a generation the function could not price, a check for a price that is\n * negative or not a number. All of that is the cost of enforcing something\n * from a fact we do not own, and enforcing it is policy besides: the two\n * applications that wanted one wanted it to stop at different moments.\n *\n * `RunResult.usage` is what a caller needs from here, and it is solid. What\n * that costs, and what to do about it, is priced by whoever holds the rates.\n */\n limits?: { maxTurns?: number; maxToolCalls?: number; deadline?: string };\n}\n\n/**\n * How an activation ended, and what it consumed on the way.\n *\n * `usage` is on every outcome, not only a completed one: a run that burned four\n * dollars of tokens and then failed burned them, and a caller that has to ask\n * the log to find that out has been handed a result missing the expensive half\n * of what happened.\n *\n * Summed from the `assistant` entries this activation committed, which is why a\n * harness does not report it — the log already holds every generation, and a\n * second total is a second answer. Money is not here: the counts are the fact,\n * and the rates that turn them into money belong to the deployment.\n */\nexport type RunResult =\n | { status: \"completed\"; output: Content; usage: Usage; seq: number }\n | { status: \"cancelled\"; usage: Usage; seq: number }\n | { status: \"failed\"; error: Failure; usage: Usage; seq: number };\n\nexport interface AgentRun extends AsyncIterable<Update> {\n readonly result: Promise<RunResult>;\n cancel(): void;\n}\n\n/**\n * One piece of input for an activation, and where it came from.\n *\n * Always the object form, because `Content` may itself be an array of parts and\n * a bare array therefore cannot say whether it is one multi-part message or\n * several messages. `RunAgentOptions.input` still takes plain content for the\n * common case; only the *many* form is required to name each arrival.\n */\nexport interface Arrival { input: Content; from?: From }\n\ninterface RunAgentBase {\n agent: Agent;\n /** Opaque application index key, used only when this run creates the session. */\n key?: string;\n context?: { run?: string; turn?: string };\n signal?: AbortSignal;\n}\n\n/**\n * An activation is working one of two things, and never both.\n *\n * A caller is **sending** — a person typed, a webhook fired, a test asked a\n * question. Or a worker is running **what the store handed it**, which carries\n * the session, the position to write from, and the deliveries to consume, all\n * as one value that arrived together and cannot be recombined wrongly.\n *\n * That second form replaced three fields a worker had to line up by hand:\n * `sessionId`, the position, and how many deliveries to take. Every one of them\n * was only ever a field of the claim, so each was a value another field already\n * determined — and the one that mattered, taking the deliveries, silently\n * left the input queued for ever when it was forgotten. Both applications\n * written on this wrote the same eight lines to get it right.\n */\nexport type RunAgentOptions = RunAgentBase & (\n | {\n /** One message as plain content, one named arrival, or several named arrivals. */\n input: Content | Arrival | readonly Arrival[];\n /**\n * Which session it goes to, as a UUID. A new one is opened when this is\n * omitted. An application's own naming for a conversation goes in `key`,\n * which is opaque to the library; a store is entitled to keep a session\n * id in a `uuid` column and refuse anything else.\n */\n sessionId?: string;\n /** Omit for an ephemeral run: the log lives in memory and nothing is persisted. */\n store?: Store;\n claim?: never;\n }\n | {\n /**\n * What `store.next()` or `store.interrupted()` handed this worker.\n *\n * Its deliveries become this activation's input and are consumed by the\n * write that commits them, in the same compare-and-swap that checks the\n * position — so a worker that loses the race loses its turn and never the\n * messages. A claim with an empty queue is a **resumption**: nothing opens\n * it, the loop continues the committed log, and the effect that already\n * happened is read there rather than asked for again. Only a harness\n * declaring `recovery: \"history\"` is given one.\n */\n claim: Runnable;\n /**\n * Required here, unlike a caller sending. A claim is a position in a log\n * and a queue to consume, and both of those are the store's — without one\n * the deliveries would be replayed into memory and left queued for ever.\n */\n store: Store;\n /**\n * Something the worker is adding of its own, ahead of what was waiting.\n *\n * One real use: orientation for a harness whose protocol has no system\n * prompt, which has nowhere else to go and must lead. Adding nothing is\n * the ordinary case.\n */\n input?: Content | Arrival | readonly Arrival[];\n sessionId?: never;\n }\n);\n"]}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { JsonValue } from "./json.js";
|
|
2
|
+
export type ContentSource = {
|
|
3
|
+
kind: "inline";
|
|
4
|
+
data: string;
|
|
5
|
+
} | {
|
|
6
|
+
kind: "url";
|
|
7
|
+
url: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* What a turn or a tool result is made of. Parts rather than a string because
|
|
11
|
+
* some content cannot honestly be made into one: a screenshot, a PDF, a block
|
|
12
|
+
* only the provider that produced it can interpret.
|
|
13
|
+
*/
|
|
14
|
+
export type ContentPart = {
|
|
15
|
+
type: "text";
|
|
16
|
+
text: string;
|
|
17
|
+
} | {
|
|
18
|
+
type: "image";
|
|
19
|
+
mediaType: string;
|
|
20
|
+
source: ContentSource;
|
|
21
|
+
} | {
|
|
22
|
+
type: "file";
|
|
23
|
+
mediaType: string;
|
|
24
|
+
name?: string;
|
|
25
|
+
source: ContentSource;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A block this package does not model — provider-executed search, server-side
|
|
29
|
+
* reasoning, anything with a signature. Kept verbatim so the provider that
|
|
30
|
+
* produced it can be handed it back, never interpreted, and never shown to
|
|
31
|
+
* the model as text. `provider` names who made it: one provider's block is
|
|
32
|
+
* not valid on another's wire.
|
|
33
|
+
*/
|
|
34
|
+
| {
|
|
35
|
+
type: "opaque";
|
|
36
|
+
provider: string;
|
|
37
|
+
data: JsonValue;
|
|
38
|
+
};
|
|
39
|
+
export type Content = string | readonly ContentPart[];
|
|
40
|
+
/**
|
|
41
|
+
* The model-visible text of some content: text parts only, joined by newline.
|
|
42
|
+
* The single projection from parts to a string — images, files and opaque
|
|
43
|
+
* blocks are deliberately dropped rather than stringified into a transcript.
|
|
44
|
+
*/
|
|
45
|
+
export declare function textOf(content: Content): string;
|
package/dist/content.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model-visible text of some content: text parts only, joined by newline.
|
|
3
|
+
* The single projection from parts to a string — images, files and opaque
|
|
4
|
+
* blocks are deliberately dropped rather than stringified into a transcript.
|
|
5
|
+
*/
|
|
6
|
+
export function textOf(content) {
|
|
7
|
+
if (typeof content === "string")
|
|
8
|
+
return content;
|
|
9
|
+
return content.filter((part) => part.type === "text").map((part) => part.text).join("\n");
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=content.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content.js","sourceRoot":"","sources":["../src/content.ts"],"names":[],"mappings":"AA0BA;;;;GAIG;AACH,MAAM,UAAU,MAAM,CAAC,OAAgB;IACrC,IAAI,OAAO,OAAO,KAAK,QAAQ;QAAE,OAAO,OAAO,CAAC;IAChD,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC5F,CAAC","sourcesContent":["import type { JsonValue } from \"./json.js\";\n\nexport type ContentSource =\n | { kind: \"inline\"; data: string }\n | { kind: \"url\"; url: string };\n\n/**\n * What a turn or a tool result is made of. Parts rather than a string because\n * some content cannot honestly be made into one: a screenshot, a PDF, a block\n * only the provider that produced it can interpret.\n */\nexport type ContentPart =\n | { type: \"text\"; text: string }\n | { type: \"image\"; mediaType: string; source: ContentSource }\n | { type: \"file\"; mediaType: string; name?: string; source: ContentSource }\n /**\n * A block this package does not model — provider-executed search, server-side\n * reasoning, anything with a signature. Kept verbatim so the provider that\n * produced it can be handed it back, never interpreted, and never shown to\n * the model as text. `provider` names who made it: one provider's block is\n * not valid on another's wire.\n */\n | { type: \"opaque\"; provider: string; data: JsonValue };\n\nexport type Content = string | readonly ContentPart[];\n\n/**\n * The model-visible text of some content: text parts only, joined by newline.\n * The single projection from parts to a string — images, files and opaque\n * blocks are deliberately dropped rather than stringified into a transcript.\n */\nexport function textOf(content: Content): string {\n if (typeof content === \"string\") return content;\n return content.filter((part) => part.type === \"text\").map((part) => part.text).join(\"\\n\");\n}\n"]}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { Harness } from "../../harness.js";
|
|
2
|
+
import type { Sandbox } from "../../../sandbox/sandbox.js";
|
|
3
|
+
import type { Decide } from "../../../tools/tool.js";
|
|
4
|
+
/** An agent process: argv and the environment that selects its provider and model. */
|
|
5
|
+
export interface AcpAgent {
|
|
6
|
+
command: readonly string[];
|
|
7
|
+
env?: Readonly<Record<string, string>>;
|
|
8
|
+
}
|
|
9
|
+
/** A tool server the foreign agent connects to itself. This is how it gets ours. */
|
|
10
|
+
export interface AcpMcpServer {
|
|
11
|
+
name: string;
|
|
12
|
+
command: string;
|
|
13
|
+
args?: readonly string[];
|
|
14
|
+
env?: readonly {
|
|
15
|
+
name: string;
|
|
16
|
+
value: string;
|
|
17
|
+
}[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Something the agent lets a client change about a session.
|
|
21
|
+
*
|
|
22
|
+
* The protocol used to name models specifically; it now publishes a list of
|
|
23
|
+
* options and gives each a category, so a client can tell a model selector
|
|
24
|
+
* from a reasoning level without knowing the agent. We report them rather than
|
|
25
|
+
* interpret them: the ids and the choices are the agent's, and a service that
|
|
26
|
+
* mapped them onto its own three levels would be guessing at another product's
|
|
27
|
+
* vocabulary.
|
|
28
|
+
*/
|
|
29
|
+
export interface AcpConfigOption {
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
/** "model", "thought_level", "mode", "model_config", or whatever the agent says. */
|
|
33
|
+
category: string;
|
|
34
|
+
description?: string;
|
|
35
|
+
current: string | boolean;
|
|
36
|
+
/** Absent for a boolean option. */
|
|
37
|
+
choices?: readonly {
|
|
38
|
+
id: string;
|
|
39
|
+
name: string;
|
|
40
|
+
}[];
|
|
41
|
+
}
|
|
42
|
+
export interface AcpHarnessOptions {
|
|
43
|
+
/** Names this harness in the log and in the UI. */
|
|
44
|
+
id: string;
|
|
45
|
+
agent: AcpAgent;
|
|
46
|
+
/**
|
|
47
|
+
* Where the agent runs. Not merely where its file requests are served: the
|
|
48
|
+
* process itself is started here, because a coding agent runs its own shell
|
|
49
|
+
* and only delegates the calls it chooses to.
|
|
50
|
+
*/
|
|
51
|
+
sandbox: Sandbox;
|
|
52
|
+
mcpServers?: readonly AcpMcpServer[];
|
|
53
|
+
/**
|
|
54
|
+
* What to set before prompting, by option id. Applied only where the agent
|
|
55
|
+
* published that option and, for a select, that value — otherwise the run
|
|
56
|
+
* fails naming what it does offer, rather than quietly running something else.
|
|
57
|
+
*/
|
|
58
|
+
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
|
+
/** What the agent published. The caller persists it so a session can offer the agent's own choices. */
|
|
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
|
+
/**
|
|
70
|
+
* Applied to the agent's own tools, per call, before they run.
|
|
71
|
+
*
|
|
72
|
+
* The protocol carries a title and the raw arguments for these, but no
|
|
73
|
+
* schema, so `spec.parameters` is `{}` and `spec.description` is the agent's
|
|
74
|
+
* own title for the call. That is what we actually have.
|
|
75
|
+
*/
|
|
76
|
+
decide?: Decide;
|
|
77
|
+
/** The agent's previous session id, so this activation continues its context. */
|
|
78
|
+
resume?: string;
|
|
79
|
+
/** Called with the agent's session id, so the caller can persist it for the next activation. */
|
|
80
|
+
onSession?(sessionId: string): void;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Runs a foreign coding agent over the Agent Client Protocol.
|
|
84
|
+
*
|
|
85
|
+
* The adapter is not a remote control. It does three things the raw protocol
|
|
86
|
+
* does not: it normalises whatever the agent did into this package's entries,
|
|
87
|
+
* so one durable log describes a Claude Code turn and a native-loop turn
|
|
88
|
+
* identically; it puts the agent's own tools under the application's decision
|
|
89
|
+
* function, one call at a time; and it hands the agent tool servers of ours, so
|
|
90
|
+
* agents can dispatch and message each other.
|
|
91
|
+
*/
|
|
92
|
+
export declare function createAcpHarness(options: AcpHarnessOptions): Harness;
|