@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
@@ -0,0 +1,85 @@
1
+ import type { Content, ContentPart } from "../content.js";
2
+ import type { ToolCall, Usage } from "../session/entry.js";
3
+ import type { Message } from "../session/messages.js";
4
+ import type { ToolSpec } from "../tools/tool.js";
5
+ import type { Failure, Result } from "../result.js";
6
+ /**
7
+ * One provider call.
8
+ *
9
+ * There is no model *name* here, and that is the point: a model is a value.
10
+ * `createAnthropicModel({ apiKey, model })` is the provider, the credential and
11
+ * the model together, and choosing a different one is choosing a different
12
+ * `Model` — which is already how the loop passes a cheaper one to compaction.
13
+ *
14
+ * A name would have to be resolved, and resolving is what goes wrong. The one
15
+ * application that had a name field to use encoded provider and model into it
16
+ * as `provider/model`, then had to split it back out — ambiguously, because a
17
+ * provider's own ids contain slashes (`z-ai/glm-5.2`), so the split was
18
+ * "up to the first one" with a comment apologising for it. One field, two facts.
19
+ * Names leave this library as observations — `ModelResponse.model` says what
20
+ * actually served — and never enter it as selections.
21
+ */
22
+ export interface ModelRequest {
23
+ messages: readonly Message[];
24
+ tools?: readonly ToolSpec[];
25
+ maxOutputTokens?: number;
26
+ /**
27
+ * Only for providers that still accept sampling controls. The current
28
+ * Anthropic models reject it outright, so that adapter does not send it —
29
+ * declaring the option here and dropping it there is the honest shape, since
30
+ * the alternative is a request the provider 400s.
31
+ */
32
+ temperature?: number;
33
+ /**
34
+ * How much the model should think before answering, where it can. Named
35
+ * levels rather than a token budget, because a budget is the one thing no
36
+ * two providers agree on — and the providers that had one have since removed
37
+ * it. Each adapter maps a level to its own control; one that has none
38
+ * ignores it rather than failing.
39
+ */
40
+ effort?: "low" | "medium" | "high" | "xhigh" | "max";
41
+ /** End the cacheable prefix after this many messages. A decision about one request only. */
42
+ cacheAfter?: number;
43
+ signal?: AbortSignal;
44
+ }
45
+ export type ModelDelta = {
46
+ type: "text.delta";
47
+ text: string;
48
+ } | {
49
+ type: "reasoning.delta";
50
+ text: string;
51
+ } | {
52
+ type: "tool-call.delta";
53
+ callId: string;
54
+ arguments: string;
55
+ };
56
+ export interface ModelResponse {
57
+ message: {
58
+ content: Content;
59
+ calls?: readonly ToolCall[];
60
+ };
61
+ finishReason: "stop" | "tool-calls" | "length" | "refusal";
62
+ usage: Usage;
63
+ model?: string;
64
+ }
65
+ export interface ModelError extends Failure {
66
+ code: "auth" | "rate-limit" | "context-length" | "cancelled" | "provider" | "failed";
67
+ }
68
+ /**
69
+ * One method. Streaming is not a second contract: a caller that wants the whole
70
+ * response drains the generator and takes its return value, so no adapter has
71
+ * to implement the same normalization twice.
72
+ */
73
+ export interface Model {
74
+ readonly id: string;
75
+ generate(request: ModelRequest): AsyncGenerator<ModelDelta, Result<ModelResponse, ModelError>>;
76
+ }
77
+ /** Drains a generation and returns only its outcome. */
78
+ export declare function collect(generation: AsyncGenerator<ModelDelta, Result<ModelResponse, ModelError>>): Promise<Result<ModelResponse, ModelError>>;
79
+ /**
80
+ * The vocabulary an adapter needs, re-exported from the port it implements.
81
+ * An adapter imports these from here rather than reaching into the modules that
82
+ * own them, so the port is the whole of its surface — every one of them appears
83
+ * in a type above, which is what makes this the place to get them.
84
+ */
85
+ export type { ContentPart, Message, ToolCall, ToolSpec, Usage };
@@ -0,0 +1,8 @@
1
+ /** Drains a generation and returns only its outcome. */
2
+ export async function collect(generation) {
3
+ let step = await generation.next();
4
+ while (!step.done)
5
+ step = await generation.next();
6
+ return step.value;
7
+ }
8
+ //# sourceMappingURL=model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model.js","sourceRoot":"","sources":["../../src/model/model.ts"],"names":[],"mappings":"AAyEA,wDAAwD;AACxD,MAAM,CAAC,KAAK,UAAU,OAAO,CAC3B,UAAyE;IAEzE,IAAI,IAAI,GAAG,MAAM,UAAU,CAAC,IAAI,EAAE,CAAC;IACnC,OAAO,CAAC,IAAI,CAAC,IAAI;QAAE,IAAI,GAAG,MAAM,UAAU,CAAC,IAAI,EAAE,CAAC;IAClD,OAAO,IAAI,CAAC,KAAK,CAAC;AACpB,CAAC","sourcesContent":["import type { Content, ContentPart } from \"../content.js\";\nimport type { ToolCall, Usage } from \"../session/entry.js\";\nimport type { Message } from \"../session/messages.js\";\nimport type { ToolSpec } from \"../tools/tool.js\";\nimport type { Failure, Result } from \"../result.js\";\nimport type { JsonValue } from \"../json.js\";\n\n/**\n * One provider call.\n *\n * There is no model *name* here, and that is the point: a model is a value.\n * `createAnthropicModel({ apiKey, model })` is the provider, the credential and\n * the model together, and choosing a different one is choosing a different\n * `Model` — which is already how the loop passes a cheaper one to compaction.\n *\n * A name would have to be resolved, and resolving is what goes wrong. The one\n * application that had a name field to use encoded provider and model into it\n * as `provider/model`, then had to split it back out — ambiguously, because a\n * provider's own ids contain slashes (`z-ai/glm-5.2`), so the split was\n * \"up to the first one\" with a comment apologising for it. One field, two facts.\n * Names leave this library as observations — `ModelResponse.model` says what\n * actually served — and never enter it as selections.\n */\nexport interface ModelRequest {\n messages: readonly Message[];\n tools?: readonly ToolSpec[];\n maxOutputTokens?: number;\n /**\n * Only for providers that still accept sampling controls. The current\n * Anthropic models reject it outright, so that adapter does not send it —\n * declaring the option here and dropping it there is the honest shape, since\n * the alternative is a request the provider 400s.\n */\n temperature?: number;\n /**\n * How much the model should think before answering, where it can. Named\n * levels rather than a token budget, because a budget is the one thing no\n * two providers agree on — and the providers that had one have since removed\n * it. Each adapter maps a level to its own control; one that has none\n * ignores it rather than failing.\n */\n effort?: \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\";\n /** End the cacheable prefix after this many messages. A decision about one request only. */\n cacheAfter?: number;\n signal?: AbortSignal;\n}\n\nexport type ModelDelta =\n | { type: \"text.delta\"; text: string }\n | { type: \"reasoning.delta\"; text: string }\n | { type: \"tool-call.delta\"; callId: string; arguments: string };\n\nexport interface ModelResponse {\n message: { content: Content; calls?: readonly ToolCall[] };\n finishReason: \"stop\" | \"tool-calls\" | \"length\" | \"refusal\";\n usage: Usage;\n model?: string;\n}\n\nexport interface ModelError extends Failure {\n code: \"auth\" | \"rate-limit\" | \"context-length\" | \"cancelled\" | \"provider\" | \"failed\";\n}\n\n/**\n * One method. Streaming is not a second contract: a caller that wants the whole\n * response drains the generator and takes its return value, so no adapter has\n * to implement the same normalization twice.\n */\nexport interface Model {\n readonly id: string;\n generate(request: ModelRequest): AsyncGenerator<ModelDelta, Result<ModelResponse, ModelError>>;\n}\n\n/** Drains a generation and returns only its outcome. */\nexport async function collect(\n generation: AsyncGenerator<ModelDelta, Result<ModelResponse, ModelError>>,\n): Promise<Result<ModelResponse, ModelError>> {\n let step = await generation.next();\n while (!step.done) step = await generation.next();\n return step.value;\n}\n\n/**\n * The vocabulary an adapter needs, re-exported from the port it implements.\n * An adapter imports these from here rather than reaching into the modules that\n * own them, so the port is the whole of its surface — every one of them appears\n * in a type above, which is what makes this the place to get them.\n */\nexport type { ContentPart, Message, ToolCall, ToolSpec, Usage };\n"]}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Expected failures are data. Throws are for programmer errors and violated
3
+ * invariants — never for an outcome a caller is meant to handle.
4
+ */
5
+ export type Result<T, E> = {
6
+ ok: true;
7
+ value: T;
8
+ } | {
9
+ ok: false;
10
+ error: E;
11
+ };
12
+ /**
13
+ * Every recoverable failure in the package. `code` is a stable identifier a
14
+ * caller may branch on; each layer narrows it to its own closed union. Nobody
15
+ * ever reads `message` to decide behaviour.
16
+ */
17
+ export interface Failure {
18
+ code: string;
19
+ message: string;
20
+ retryable: boolean;
21
+ }
22
+ export declare const ok: <T>(value: T) => Result<T, never>;
23
+ export declare const err: <E>(error: E) => Result<never, E>;
package/dist/result.js ADDED
@@ -0,0 +1,3 @@
1
+ export const ok = (value) => ({ ok: true, value });
2
+ export const err = (error) => ({ ok: false, error });
3
+ //# sourceMappingURL=result.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"result.js","sourceRoot":"","sources":["../src/result.ts"],"names":[],"mappings":"AAmBA,MAAM,CAAC,MAAM,EAAE,GAAG,CAAI,KAAQ,EAAoB,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;AAC3E,MAAM,CAAC,MAAM,GAAG,GAAG,CAAI,KAAQ,EAAoB,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC","sourcesContent":["/**\n * Expected failures are data. Throws are for programmer errors and violated\n * invariants — never for an outcome a caller is meant to handle.\n */\nexport type Result<T, E> =\n | { ok: true; value: T }\n | { ok: false; error: E };\n\n/**\n * Every recoverable failure in the package. `code` is a stable identifier a\n * caller may branch on; each layer narrows it to its own closed union. Nobody\n * ever reads `message` to decide behaviour.\n */\nexport interface Failure {\n code: string;\n message: string;\n retryable: boolean;\n}\n\nexport const ok = <T>(value: T): Result<T, never> => ({ ok: true, value });\nexport const err = <E>(error: E): Result<never, E> => ({ ok: false, error });\n"]}
package/dist/run.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { AgentRun, RunAgentOptions } from "./agent.js";
2
+ /**
3
+ * Start an agent on a session.
4
+ *
5
+ * Without a store the log lives in memory and the session is ephemeral; with
6
+ * one, every commit is durable before the next turn is built. The agent
7
+ * definition is identical either way — durability is a composition choice, not
8
+ * a different program.
9
+ */
10
+ export declare function runAgent(options: RunAgentOptions): AgentRun;
package/dist/run.js ADDED
@@ -0,0 +1,477 @@
1
+ import { err } from "./result.js";
2
+ import { createLog } from "./session/log.js";
3
+ import { toMessages } from "./session/messages.js";
4
+ import { createExecutor } from "./tools/execute.js";
5
+ /**
6
+ * Start an agent on a session.
7
+ *
8
+ * Without a store the log lives in memory and the session is ephemeral; with
9
+ * one, every commit is durable before the next turn is built. The agent
10
+ * definition is identical either way — durability is a composition choice, not
11
+ * a different program.
12
+ */
13
+ export function runAgent(options) {
14
+ // A claim names its own session; a caller sending into a new one names none.
15
+ const claim = options.claim;
16
+ const sessionId = claim?.sessionId ?? options.sessionId ?? crypto.randomUUID();
17
+ const controller = new AbortController();
18
+ // Composed rather than forwarded. `addEventListener("abort", …)` never fires
19
+ // on a signal that has already aborted, so a caller who gave up before
20
+ // calling got a full run anyway; and a listener added per run to a signal
21
+ // that outlives it is a leak the run cannot clean up.
22
+ const signal = options.signal
23
+ ? AbortSignal.any([options.signal, controller.signal])
24
+ : controller.signal;
25
+ const updates = createChannel();
26
+ const result = execute();
27
+ return {
28
+ result,
29
+ cancel: () => controller.abort(),
30
+ [Symbol.asyncIterator]: () => updates.iterator(),
31
+ };
32
+ async function execute() {
33
+ // What this activation consumed. Summed from the `assistant` entries it
34
+ // commits rather than reported by the harness, so one answer serves a loop
35
+ // we own and a loop we do not — and so a run that failed still says what it
36
+ // burned on the way there.
37
+ const usage = {};
38
+ try {
39
+ const { agent, store } = options;
40
+ const restored = store ? await open(store, sessionId, agent, options.key) : undefined;
41
+ if (restored && !restored.ok) {
42
+ return {
43
+ status: "failed", error: restored.error,
44
+ seq: positionOf(restored.error, 0), usage,
45
+ };
46
+ }
47
+ // The store's own position, not the last entry's. They agree only while
48
+ // nothing has ever been folded away, and the store is the one that knows.
49
+ const log = createLog(restored?.value.entries ?? [], restored?.value.seq ?? 0);
50
+ const stopping = (error) => ({ status: "failed", error, seq: log.seq, usage });
51
+ // A claim was read at a position. If the session has moved since, this
52
+ // worker lost, and the deliveries it was carrying are still where it
53
+ // found them.
54
+ if (claim && claim.seq !== log.seq) {
55
+ return stopping({ code: "conflict", message: `Session is at ${log.seq}, not ${claim.seq}`, retryable: true });
56
+ }
57
+ // A ceiling on money needs something that can count it. Refused here
58
+ // rather than silently ignored, because a limit that is declared and not
59
+ // enforced is the most expensive kind of nothing.
60
+ // The activation still open on this log, if one is. It decides three
61
+ // things below: whether there is anything to resume, which run a
62
+ // resumption continues, and which run a fresh activation supersedes.
63
+ const interrupted = openRunId(log.entries);
64
+ // What this run is adding, then what was waiting. A claim's deliveries
65
+ // are named so the log records who each came from; anything the worker
66
+ // supplies itself leads, because the only thing that ever does is
67
+ // orientation for a harness with nowhere else to put it.
68
+ const arrivals = [
69
+ ...arrivalsOf(options.input),
70
+ ...(claim?.pending ?? []).map((delivery) => ({
71
+ input: delivery.input,
72
+ ...(delivery.from ? { from: delivery.from } : {}),
73
+ })),
74
+ ];
75
+ // A resumption continues the run it was handed, under that run's id. A
76
+ // fresh id would leave the log holding a `run.started` nothing closes and
77
+ // a `run.finished` nothing opened — a pair that only the store's
78
+ // type-based rule saves, and that anything grouping entries by run reads
79
+ // as broken. A resumption without an interrupted run is refused below.
80
+ const runId = arrivals.length || !interrupted ? crypto.randomUUID() : interrupted;
81
+ // Deliveries ride the next commit, which is the one that records the work
82
+ // that produced them. They used to be held back to the run's final entry
83
+ // so that a failed run handed nothing to anyone — a stronger promise than
84
+ // any caller needs, and it was being paid for with a lie: a `send` tool
85
+ // returned "Delivered" and interrupted the recipient many turns before
86
+ // the delivery existed, and never at all if a later turn failed.
87
+ const deliveries = [];
88
+ // The ceiling has one owner, and it is here rather than in a harness.
89
+ // Counting committed entries is the only way to bound a harness that owns
90
+ // its own loop — which is most of them — so a limit declared on the agent
91
+ // holds for every one instead of only for ours.
92
+ const limits = agent.limits;
93
+ let stopped;
94
+ let turns = 0;
95
+ let toolCalls = 0;
96
+ function exceeded() {
97
+ if (!limits)
98
+ return undefined;
99
+ if (limits.maxTurns !== undefined && turns >= limits.maxTurns) {
100
+ return { code: "turn-limit", message: `Stopped after ${turns} turns.`, retryable: false };
101
+ }
102
+ if (limits.maxToolCalls !== undefined && toolCalls >= limits.maxToolCalls) {
103
+ return { code: "tool-call-limit", message: `Stopped after ${toolCalls} tool calls.`, retryable: false };
104
+ }
105
+ // Checked where work is recorded rather than on a timer: a deadline
106
+ // stops the next piece of work, it does not interrupt one mid-flight.
107
+ if (limits.deadline !== undefined && Date.now() >= Date.parse(limits.deadline)) {
108
+ return { code: "deadline", message: `Stopped at the deadline of ${limits.deadline}.`, retryable: false };
109
+ }
110
+ return undefined;
111
+ }
112
+ // How many deliveries the next write takes off this session's own queue:
113
+ // what the claim was carrying, and afterwards whatever `drain` folded in.
114
+ // Cleared by the write that lands, because the input is gone once the
115
+ // entries carrying it are durable, and not before.
116
+ let take = claim?.pending.length ?? 0;
117
+ async function commit(entries, enqueue) {
118
+ const outgoing = [...deliveries, ...(enqueue ?? [])];
119
+ deliveries.length = 0;
120
+ if (!entries.length && !outgoing.length)
121
+ return;
122
+ // Counted before the write, not after: the tokens were consumed whether
123
+ // or not this entry wins the compare-and-swap, and a run that reports
124
+ // nothing after paying for a generation is the opposite of what
125
+ // `RunResult.usage` promises.
126
+ for (const entry of entries) {
127
+ if (entry.type === "assistant" && entry.usage)
128
+ accumulate(usage, entry.usage);
129
+ }
130
+ if (store) {
131
+ const written = await store.append({
132
+ sessionId, expectedSeq: log.seq, entries,
133
+ ...(outgoing.length ? { enqueue: outgoing } : {}),
134
+ ...(take ? { takePending: take } : {}),
135
+ });
136
+ if (!written.ok)
137
+ throw new CommitFailed(written.error, positionOf(written.error, log.seq));
138
+ take = 0;
139
+ }
140
+ for (const stored of log.append(entries))
141
+ updates.push({ type: "entry", entry: stored });
142
+ if (!stopped) {
143
+ for (const entry of entries) {
144
+ if (entry.type === "assistant")
145
+ turns += 1;
146
+ // `tool.finished`, not `tool.started`: the start is committed before
147
+ // the batch executes, so counting it there cancelled the very batch
148
+ // that reached the ceiling — `maxToolCalls: 1` ran no tools at all,
149
+ // and said it had stopped after one.
150
+ if (entry.type === "tool.finished")
151
+ toolCalls += 1;
152
+ }
153
+ stopped = exceeded();
154
+ if (stopped)
155
+ controller.abort();
156
+ }
157
+ }
158
+ /**
159
+ * What the agent wants delivered as this run ends.
160
+ *
161
+ * A callback that throws must not cost the log its terminal entry: the
162
+ * run would stay open, and an open run nothing closes is claimed by
163
+ * `interrupted` every window for ever — the trap the no-recovery path above
164
+ * exists to close. So the throw is held, the run is ended without the
165
+ * deliveries, and it is re-thrown once the log is safe.
166
+ */
167
+ let reportFailure;
168
+ async function reporting(run) {
169
+ if (!agent.finished)
170
+ return [];
171
+ try {
172
+ return await agent.finished({ sessionId, ...run });
173
+ }
174
+ catch (error) {
175
+ reportFailure = error;
176
+ return [];
177
+ }
178
+ }
179
+ // Input that arrived while this activation was running, taken from the
180
+ // front of the queue and committed as part of it. `takePending` removes
181
+ // exactly what was committed, in the same write, so losing the position
182
+ // loses the turn and never the messages.
183
+ async function drain() {
184
+ if (!store)
185
+ return 0;
186
+ // Nothing is folded into an activation that is over. Draining here
187
+ // committed the waiting deliveries and took them off the queue, and the
188
+ // loop then ended — leaving a peer's message answered by nobody and
189
+ // invisible to `next` (nothing pending) and to `interrupted` (no open run),
190
+ // which is the trap this whole path exists to close.
191
+ if (signal.aborted)
192
+ return 0;
193
+ const current = await store.read({ sessionId, afterSeq: log.seq });
194
+ if (!current.ok || !current.value.pending.length)
195
+ return 0;
196
+ const waiting = current.value.pending;
197
+ take = waiting.length;
198
+ await commit(waiting.map((delivery) => ({
199
+ type: "run.started", runId, input: delivery.input,
200
+ ...(delivery.from ? { from: delivery.from } : {}),
201
+ })));
202
+ return waiting.length;
203
+ }
204
+ const executor = agent.tools?.length
205
+ ? createExecutor({
206
+ tools: agent.tools,
207
+ ...(agent.decide ? { decide: agent.decide } : {}),
208
+ sessionId, runId,
209
+ enqueue: (delivery) => deliveries.push(delivery),
210
+ report: (callId, data) => updates.push({ type: "tool.progress", callId, data }),
211
+ })
212
+ : undefined;
213
+ if (!arrivals.length) {
214
+ // An open run is what a resumption continues. Without one there is
215
+ // nothing half-done here: a closed log would otherwise take a second
216
+ // `run.finished` with no beginning to match it.
217
+ if (!interrupted) {
218
+ return stopping({
219
+ code: "nothing-to-resume",
220
+ message: "An activation with no input continues an interrupted run, and this session has none open.",
221
+ retryable: false,
222
+ });
223
+ }
224
+ if (agent.harness.recovery !== "history") {
225
+ // Asked to continue a log this harness cannot read. The interrupted
226
+ // run is ended rather than left open, because nothing else will
227
+ // continue it either — the harness is a property of the session — and
228
+ // a run that stays open is handed back by `interrupted` every window,
229
+ // for ever. A worker that provisions anything before it runs pays for
230
+ // that each time round.
231
+ const failure = {
232
+ code: "no-recovery",
233
+ message: `The ${agent.harness.id} harness cannot restart from history, so this activation cannot be continued.`,
234
+ retryable: false,
235
+ };
236
+ // Whoever was waiting on it is told, in the write that ends it. A
237
+ // parent expecting a child that can now never report is the same dead
238
+ // end one layer up.
239
+ const told = await reporting({ runId: interrupted, outcome: "failed", output: "" });
240
+ await commit([{ type: "run.finished", runId: interrupted, outcome: "failed", error: failure }], told);
241
+ return stopping(failure);
242
+ }
243
+ }
244
+ // An interrupted predecessor is ended before a new activation begins over
245
+ // it, so one session never holds two open runs. Only on a claim: the
246
+ // store established there that nothing else is running, where a bare
247
+ // `sessionId` establishes nothing and the run may still be live.
248
+ if (arrivals.length && interrupted && claim) {
249
+ await commit([{
250
+ type: "run.finished", runId: interrupted, outcome: "cancelled",
251
+ error: {
252
+ code: "superseded",
253
+ message: "This activation was interrupted, and a later one began over it.",
254
+ retryable: false,
255
+ },
256
+ }]);
257
+ }
258
+ // Nothing opens a resumption. A `run.started` recording input nobody sent
259
+ // would be a second beginning for one conversation; the loop reads the
260
+ // history it was handed instead, sees the call that already ran, and
261
+ // carries on rather than asking for it again.
262
+ await commit(arrivals.map((arrival) => ({
263
+ type: "run.started", runId, input: arrival.input,
264
+ ...(arrival.from ? { from: arrival.from } : {}),
265
+ })));
266
+ // Every ceiling is checked where work is recorded, and a resumption
267
+ // records nothing before its first turn — so a deadline that had already
268
+ // passed was never seen. Checked once here too, before the harness is
269
+ // handed anything.
270
+ stopped ??= exceeded();
271
+ const outcome = stopped
272
+ ? { status: "failed", error: stopped }
273
+ : await agent.harness.run({
274
+ sessionId, runId,
275
+ instructions: agent.instructions,
276
+ history: () => toMessages({
277
+ instructions: agent.instructions,
278
+ entries: log.entries,
279
+ ...(options.context ? { context: options.context } : {}),
280
+ }),
281
+ entries: () => log.entries,
282
+ ...(options.context ? { context: options.context } : {}),
283
+ ...(executor ? { tools: executor } : {}),
284
+ commit: (entries) => commit(entries),
285
+ ...(store ? { drain } : {}),
286
+ emit: (update) => updates.push(update),
287
+ signal,
288
+ });
289
+ // A harness stopped by the ceiling reports a cancellation, because that is
290
+ // all it saw. The run knows why it was cancelled and says so instead.
291
+ const settled = stopped && outcome.status === "cancelled"
292
+ ? { status: "failed", error: stopped }
293
+ : outcome;
294
+ const output = settled.status === "completed" ? settled.output : "";
295
+ const finished = await reporting({ runId, outcome: settled.status, output });
296
+ await commit([{
297
+ type: "run.finished", runId,
298
+ outcome: settled.status === "completed" ? "completed" : settled.status === "cancelled" ? "cancelled" : "failed",
299
+ ...(settled.status === "failed" ? { error: settled.error } : {}),
300
+ }], finished);
301
+ if (settled.status === "completed") {
302
+ return { status: "completed", output: settled.output, seq: log.seq, usage };
303
+ }
304
+ if (settled.status === "cancelled")
305
+ return { status: "cancelled", seq: log.seq, usage };
306
+ return stopping(settled.error);
307
+ }
308
+ catch (error) {
309
+ if (error instanceof CommitFailed) {
310
+ return { status: "failed", error: error.failure, seq: error.seq, usage };
311
+ }
312
+ throw error;
313
+ }
314
+ finally {
315
+ updates.close();
316
+ }
317
+ }
318
+ }
319
+ class CommitFailed extends Error {
320
+ failure;
321
+ seq;
322
+ constructor(failure, seq) {
323
+ super(failure.message);
324
+ this.failure = failure;
325
+ this.seq = seq;
326
+ }
327
+ }
328
+ /**
329
+ * Where the session actually is after a refused write. A conflict knows; every
330
+ * other failure leaves the caller with the position it already had. Reporting
331
+ * zero told a caller at forty that it was at the beginning.
332
+ *
333
+ * The `typeof` is not defensive noise: a store may report a conflict, and only
334
+ * a `StoreConflict` carries the position. Casting on the code alone produced a
335
+ * `RunResult.seq` of `undefined` under a type that says `number`.
336
+ */
337
+ function positionOf(error, fallback) {
338
+ const actual = error.actualSeq;
339
+ return error.code === "conflict" && typeof actual === "number" ? actual : fallback;
340
+ }
341
+ const isArrival = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && "input" in value;
342
+ /**
343
+ * One arrival, or several.
344
+ *
345
+ * The elements decide it, because the outer shape cannot: an arrival names its
346
+ * `input`, a content part names its `type`, and nothing is both. Reading every
347
+ * array as a batch is what turned one image-and-text message into one entry per
348
+ * part, each holding a part where whole content belongs.
349
+ */
350
+ function arrivalsOf(input) {
351
+ if (input === undefined)
352
+ return [];
353
+ // Nothing sent is nothing sent. Falling through committed a `run.started`
354
+ // whose input was an empty array of content parts.
355
+ if (Array.isArray(input) && input.length === 0)
356
+ return [];
357
+ if (Array.isArray(input) && input.length > 0 && input.every(isArrival))
358
+ return input;
359
+ if (isArrival(input))
360
+ return [input];
361
+ return [{ input: input }];
362
+ }
363
+ /**
364
+ * The activation still open on this log, if one is.
365
+ *
366
+ * `run.started` opens and `run.finished` closes, which is the same rule the
367
+ * store maintains its claim from — read here from the entries rather than asked
368
+ * of the store, because the log is the only thing that knows.
369
+ */
370
+ function openRunId(entries) {
371
+ let open;
372
+ for (const entry of entries) {
373
+ if (entry.type === "run.started")
374
+ open = entry.runId;
375
+ if (entry.type === "run.finished")
376
+ open = undefined;
377
+ }
378
+ return open;
379
+ }
380
+ /** Token counts add; unknown stays unknown rather than becoming a zero. */
381
+ function accumulate(total, turn) {
382
+ for (const key of ["inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens"]) {
383
+ if (turn[key] !== undefined)
384
+ total[key] = (total[key] ?? 0) + turn[key];
385
+ }
386
+ }
387
+ async function open(store, sessionId, agent, key) {
388
+ const existing = await store.read({ sessionId });
389
+ if (existing.ok) {
390
+ // `version` is documented as the boundary past which an agent is no longer
391
+ // compatible with an existing session. Nothing was comparing it, so the
392
+ // boundary held for exactly as long as nobody crossed it.
393
+ const stored = existing.value.agent;
394
+ if (stored.id !== agent.id || stored.version !== agent.version) {
395
+ return err({
396
+ code: "conflict",
397
+ message: `Session ${sessionId} belongs to ${stored.id}@${stored.version}, not ${agent.id}@${agent.version}`,
398
+ retryable: false,
399
+ });
400
+ }
401
+ return existing;
402
+ }
403
+ if (existing.error.code !== "not-found")
404
+ return existing;
405
+ const created = await store.create({
406
+ sessionId, agent: { id: agent.id, version: agent.version }, ...(key ? { key } : {}),
407
+ });
408
+ if (!created.ok)
409
+ return created;
410
+ return store.read({ sessionId });
411
+ }
412
+ /**
413
+ * A bounded fan-out queue for ephemeral updates.
414
+ *
415
+ * One buffer of recent updates, which every listener starts from and which
416
+ * never grows past its limit. That single rule gives three things:
417
+ *
418
+ * **A run nobody drains costs a fixed amount.** A headless worker used to
419
+ * accumulate every delta of an entire activation, because the queue only ever
420
+ * grew. Dropping the oldest is safe precisely because updates are not recovery
421
+ * state and the committed log is.
422
+ *
423
+ * **Arriving late loses nothing that matters.** The first entry commits before
424
+ * `runAgent` returns, so a caller that iterates immediately would otherwise
425
+ * never see its own run start. A UI reconnecting mid-run gets recent history
426
+ * for the same reason, without asking the store for it.
427
+ *
428
+ * **Everyone sees everything.** Each listener drains its own copy, so an
429
+ * application can watch a run for telemetry and for a UI at once instead of the
430
+ * two stealing events from each other.
431
+ */
432
+ function createChannel(limit = 1_024) {
433
+ const listeners = new Set();
434
+ const replay = [];
435
+ let closed = false;
436
+ const bounded = (buffer, value) => {
437
+ if (buffer.length >= limit)
438
+ buffer.shift();
439
+ buffer.push(value);
440
+ };
441
+ return {
442
+ push(value) {
443
+ bounded(replay, value);
444
+ for (const listener of listeners) {
445
+ bounded(listener.buffer, value);
446
+ listener.wake?.();
447
+ }
448
+ },
449
+ close() {
450
+ closed = true;
451
+ for (const listener of listeners)
452
+ listener.wake?.();
453
+ },
454
+ iterator() {
455
+ const listener = { buffer: [...replay] };
456
+ listeners.add(listener);
457
+ const done = () => {
458
+ listeners.delete(listener);
459
+ return { value: undefined, done: true };
460
+ };
461
+ return {
462
+ async next() {
463
+ for (;;) {
464
+ const value = listener.buffer.shift();
465
+ if (value !== undefined)
466
+ return { value, done: false };
467
+ if (closed)
468
+ return done();
469
+ await new Promise((resolve) => { listener.wake = resolve; });
470
+ }
471
+ },
472
+ async return() { return done(); },
473
+ };
474
+ },
475
+ };
476
+ }
477
+ //# sourceMappingURL=run.js.map