@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.
Files changed (108) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -0
  3. package/dist/agent.d.ts +158 -0
  4. package/dist/agent.js +2 -0
  5. package/dist/agent.js.map +1 -0
  6. package/dist/content.d.ts +45 -0
  7. package/dist/content.js +11 -0
  8. package/dist/content.js.map +1 -0
  9. package/dist/harness/adapters/acp/index.d.ts +92 -0
  10. package/dist/harness/adapters/acp/index.js +479 -0
  11. package/dist/harness/adapters/acp/index.js.map +1 -0
  12. package/dist/harness/adapters/acp/rpc.d.ts +23 -0
  13. package/dist/harness/adapters/acp/rpc.js +103 -0
  14. package/dist/harness/adapters/acp/rpc.js.map +1 -0
  15. package/dist/harness/adapters/native/compaction.d.ts +16 -0
  16. package/dist/harness/adapters/native/compaction.js +55 -0
  17. package/dist/harness/adapters/native/compaction.js.map +1 -0
  18. package/dist/harness/adapters/native/index.d.ts +2 -0
  19. package/dist/harness/adapters/native/index.js +2 -0
  20. package/dist/harness/adapters/native/index.js.map +1 -0
  21. package/dist/harness/adapters/native/loop.d.ts +25 -0
  22. package/dist/harness/adapters/native/loop.js +153 -0
  23. package/dist/harness/adapters/native/loop.js.map +1 -0
  24. package/dist/harness/harness.d.ts +129 -0
  25. package/dist/harness/harness.js +2 -0
  26. package/dist/harness/harness.js.map +1 -0
  27. package/dist/harness/index.d.ts +3 -0
  28. package/dist/harness/index.js +2 -0
  29. package/dist/harness/index.js.map +1 -0
  30. package/dist/index.d.ts +10 -0
  31. package/dist/index.js +5 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/json.d.ts +4 -0
  34. package/dist/json.js +2 -0
  35. package/dist/json.js.map +1 -0
  36. package/dist/model/adapters/anthropic/index.d.ts +34 -0
  37. package/dist/model/adapters/anthropic/index.js +271 -0
  38. package/dist/model/adapters/anthropic/index.js.map +1 -0
  39. package/dist/model/adapters/fake/index.d.ts +27 -0
  40. package/dist/model/adapters/fake/index.js +37 -0
  41. package/dist/model/adapters/fake/index.js.map +1 -0
  42. package/dist/model/adapters/openai-compatible/index.d.ts +29 -0
  43. package/dist/model/adapters/openai-compatible/index.js +226 -0
  44. package/dist/model/adapters/openai-compatible/index.js.map +1 -0
  45. package/dist/model/conformance.d.ts +210 -0
  46. package/dist/model/conformance.js +427 -0
  47. package/dist/model/conformance.js.map +1 -0
  48. package/dist/model/index.d.ts +2 -0
  49. package/dist/model/index.js +2 -0
  50. package/dist/model/index.js.map +1 -0
  51. package/dist/model/model.d.ts +85 -0
  52. package/dist/model/model.js +8 -0
  53. package/dist/model/model.js.map +1 -0
  54. package/dist/result.d.ts +23 -0
  55. package/dist/result.js +3 -0
  56. package/dist/result.js.map +1 -0
  57. package/dist/run.d.ts +10 -0
  58. package/dist/run.js +477 -0
  59. package/dist/run.js.map +1 -0
  60. package/dist/sandbox/adapters/docker.d.ts +22 -0
  61. package/dist/sandbox/adapters/docker.js +443 -0
  62. package/dist/sandbox/adapters/docker.js.map +1 -0
  63. package/dist/sandbox/adapters/local.d.ts +11 -0
  64. package/dist/sandbox/adapters/local.js +284 -0
  65. package/dist/sandbox/adapters/local.js.map +1 -0
  66. package/dist/sandbox/conformance.d.ts +73 -0
  67. package/dist/sandbox/conformance.js +309 -0
  68. package/dist/sandbox/conformance.js.map +1 -0
  69. package/dist/sandbox/index.d.ts +1 -0
  70. package/dist/sandbox/index.js +2 -0
  71. package/dist/sandbox/index.js.map +1 -0
  72. package/dist/sandbox/sandbox.d.ts +172 -0
  73. package/dist/sandbox/sandbox.js +2 -0
  74. package/dist/sandbox/sandbox.js.map +1 -0
  75. package/dist/session/entry.d.ts +196 -0
  76. package/dist/session/entry.js +2 -0
  77. package/dist/session/entry.js.map +1 -0
  78. package/dist/session/index.d.ts +2 -0
  79. package/dist/session/index.js +2 -0
  80. package/dist/session/index.js.map +1 -0
  81. package/dist/session/log.d.ts +20 -0
  82. package/dist/session/log.js +24 -0
  83. package/dist/session/log.js.map +1 -0
  84. package/dist/session/messages.d.ts +42 -0
  85. package/dist/session/messages.js +99 -0
  86. package/dist/session/messages.js.map +1 -0
  87. package/dist/store/adapters/sqlite.d.ts +20 -0
  88. package/dist/store/adapters/sqlite.js +365 -0
  89. package/dist/store/adapters/sqlite.js.map +1 -0
  90. package/dist/store/conformance.d.ts +66 -0
  91. package/dist/store/conformance.js +580 -0
  92. package/dist/store/conformance.js.map +1 -0
  93. package/dist/store/index.d.ts +1 -0
  94. package/dist/store/index.js +2 -0
  95. package/dist/store/index.js.map +1 -0
  96. package/dist/store/store.d.ts +244 -0
  97. package/dist/store/store.js +2 -0
  98. package/dist/store/store.js.map +1 -0
  99. package/dist/tools/execute.d.ts +24 -0
  100. package/dist/tools/execute.js +105 -0
  101. package/dist/tools/execute.js.map +1 -0
  102. package/dist/tools/index.d.ts +2 -0
  103. package/dist/tools/index.js +2 -0
  104. package/dist/tools/index.js.map +1 -0
  105. package/dist/tools/tool.d.ts +106 -0
  106. package/dist/tools/tool.js +34 -0
  107. package/dist/tools/tool.js.map +1 -0
  108. 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).
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=agent.js.map
@@ -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;
@@ -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;