@a-dray/aglib 0.1.1 → 0.3.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/dist/agent.d.ts +16 -9
- package/dist/agent.js.map +1 -1
- package/dist/harness/adapters/native/compaction.d.ts +25 -2
- package/dist/harness/adapters/native/compaction.js +52 -9
- package/dist/harness/adapters/native/compaction.js.map +1 -1
- package/dist/harness/adapters/native/loop.d.ts +1 -8
- package/dist/harness/adapters/native/loop.js +9 -38
- package/dist/harness/adapters/native/loop.js.map +1 -1
- package/dist/harness/harness.d.ts +31 -2
- package/dist/harness/harness.js.map +1 -1
- package/dist/harness/index.d.ts +2 -1
- package/dist/harness/index.js +1 -0
- package/dist/harness/index.js.map +1 -1
- package/dist/model/adapters/openai-compatible/index.js +1 -1
- package/dist/model/adapters/openai-compatible/index.js.map +1 -1
- package/dist/render.d.ts +1 -1
- package/dist/render.js +9 -0
- package/dist/render.js.map +1 -1
- package/dist/run.js +74 -39
- package/dist/run.js.map +1 -1
- package/dist/session/entry.d.ts +8 -2
- package/dist/session/entry.js.map +1 -1
- package/dist/session/messages.d.ts +9 -0
- package/dist/session/messages.js +8 -5
- package/dist/session/messages.js.map +1 -1
- package/package.json +1 -1
package/dist/agent.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Content } from "./content.js";
|
|
2
|
-
import type { Harness } from "./harness/harness.js";
|
|
2
|
+
import type { Harness, LifecycleHook } from "./harness/harness.js";
|
|
3
3
|
import type { Decide, Tool } from "./tools/tool.js";
|
|
4
|
-
import type {
|
|
4
|
+
import type { Runnable, Store } from "./store/store.js";
|
|
5
5
|
import type { Update } from "./harness/harness.js";
|
|
6
6
|
import type { Failure } from "./result.js";
|
|
7
7
|
import type { From, Usage } from "./session/entry.js";
|
|
@@ -15,13 +15,20 @@ export interface Agent {
|
|
|
15
15
|
tools?: readonly Tool[];
|
|
16
16
|
/** Per-call policy over parsed arguments. Absent means every call executes. */
|
|
17
17
|
decide?: Decide;
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Name each arrival's sender in the turn the model reads, as `[from kind id]`.
|
|
20
|
+
*
|
|
21
|
+
* Off by default, because two separate things were one: `from` is provenance
|
|
22
|
+
* the log holds whatever this says, and this is text in a prompt. An
|
|
23
|
+
* application that renders its own attribution into the input it delivers —
|
|
24
|
+
* most do, since only they can name a sender in words their agent knows —
|
|
25
|
+
* would otherwise have the model read two names for one sender, one of them a
|
|
26
|
+
* session id that means nothing to it. Turn it on where nothing else says who
|
|
27
|
+
* wrote.
|
|
28
|
+
*/
|
|
29
|
+
attribution?: boolean;
|
|
30
|
+
/** Ordered, uniquely named lifecycle callbacks. Continuations share this run's durable log. */
|
|
31
|
+
hooks?: readonly LifecycleHook[];
|
|
25
32
|
/**
|
|
26
33
|
* Ceilings on one activation, over the facts this library holds: turns and
|
|
27
34
|
* tool calls are on the log, and a deadline is the clock.
|
package/dist/agent.js.map
CHANGED
|
@@ -1 +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 {
|
|
1
|
+
{"version":3,"file":"agent.js","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"","sourcesContent":["import type { Content } from \"./content.js\";\nimport type { Harness, LifecycleHook } from \"./harness/harness.js\";\nimport type { Decide, Tool } from \"./tools/tool.js\";\nimport type { 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 /**\n * Name each arrival's sender in the turn the model reads, as `[from kind id]`.\n *\n * Off by default, because two separate things were one: `from` is provenance\n * the log holds whatever this says, and this is text in a prompt. An\n * application that renders its own attribution into the input it delivers —\n * most do, since only they can name a sender in words their agent knows —\n * would otherwise have the model read two names for one sender, one of them a\n * session id that means nothing to it. Turn it on where nothing else says who\n * wrote.\n */\n attribution?: boolean;\n /** Ordered, uniquely named lifecycle callbacks. Continuations share this run's durable log. */\n hooks?: readonly LifecycleHook[];\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"]}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { LifecycleHook } from "../../harness.js";
|
|
2
|
+
import type { Model, Message, ModelError } from "../../../model/model.js";
|
|
2
3
|
import type { Stored } from "../../../session/entry.js";
|
|
4
|
+
import { type Result } from "../../../result.js";
|
|
3
5
|
/**
|
|
4
6
|
* Crude and deliberate: four characters per token, over the serialized request.
|
|
5
7
|
* A real count needs the provider's tokenizer, which would mean shipping one
|
|
@@ -7,10 +9,31 @@ import type { Stored } from "../../../session/entry.js";
|
|
|
7
9
|
* costs one early or late compaction, not correctness.
|
|
8
10
|
*/
|
|
9
11
|
export declare function estimateTokens(messages: readonly Message[]): number;
|
|
12
|
+
/**
|
|
13
|
+
* The latest position that can be folded without separating a tool call from
|
|
14
|
+
* its results.
|
|
15
|
+
*
|
|
16
|
+
* A position is safe when the log is **drained** there: every call an assistant
|
|
17
|
+
* turn asked for has its `tool.finished`. The gap between two batches of calls
|
|
18
|
+
* is such a point, as are a turn that asked for nothing and the end of a run.
|
|
19
|
+
* Cutting anywhere else leaves one call of a batch summarized and its sibling
|
|
20
|
+
* live, which shows the model a result for a call it can no longer see.
|
|
21
|
+
*
|
|
22
|
+
* A finished run and an empty turn alone were not enough, and the run that
|
|
23
|
+
* needed compaction was exactly the run that had neither: inside one activation
|
|
24
|
+
* every assistant turn holds calls until the one that ends it, so a single long
|
|
25
|
+
* activation never folded and grew until the provider refused it.
|
|
26
|
+
*/
|
|
10
27
|
export declare function compactionCut(entries: readonly Stored[]): number | undefined;
|
|
11
28
|
export declare function summarize(input: {
|
|
12
29
|
model: Model;
|
|
13
30
|
messages: readonly Message[];
|
|
14
31
|
prompt?: (messages: readonly Message[]) => string;
|
|
15
32
|
signal?: AbortSignal;
|
|
16
|
-
}): Promise<string
|
|
33
|
+
}): Promise<Result<string, ModelError>>;
|
|
34
|
+
/** Compact before native model calls. A failed summary ends the run with its provider error. */
|
|
35
|
+
export declare function createCompactionHook(options: {
|
|
36
|
+
model: Model;
|
|
37
|
+
maxInputTokens: number;
|
|
38
|
+
prompt?: (messages: readonly Message[]) => string;
|
|
39
|
+
}): LifecycleHook;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { foldedThrough, toMessages } from "../../../session/messages.js";
|
|
1
2
|
import { collect } from "../../../model/model.js";
|
|
3
|
+
import { ok, err } from "../../../result.js";
|
|
2
4
|
import { textOf } from "../../../content.js";
|
|
3
5
|
/**
|
|
4
6
|
* Crude and deliberate: four characters per token, over the serialized request.
|
|
@@ -9,24 +11,39 @@ import { textOf } from "../../../content.js";
|
|
|
9
11
|
export function estimateTokens(messages) {
|
|
10
12
|
return messages.reduce((total, message) => total + JSON.stringify(message).length, 0) / 4;
|
|
11
13
|
}
|
|
14
|
+
/** How much of the tail is kept verbatim. A constant until a caller disagrees. */
|
|
15
|
+
const KEEP_FRACTION = 0.4;
|
|
12
16
|
/**
|
|
13
17
|
* The latest position that can be folded without separating a tool call from
|
|
14
18
|
* its results.
|
|
15
19
|
*
|
|
16
|
-
*
|
|
17
|
-
* turn
|
|
18
|
-
*
|
|
19
|
-
* call
|
|
20
|
+
* A position is safe when the log is **drained** there: every call an assistant
|
|
21
|
+
* turn asked for has its `tool.finished`. The gap between two batches of calls
|
|
22
|
+
* is such a point, as are a turn that asked for nothing and the end of a run.
|
|
23
|
+
* Cutting anywhere else leaves one call of a batch summarized and its sibling
|
|
24
|
+
* live, which shows the model a result for a call it can no longer see.
|
|
25
|
+
*
|
|
26
|
+
* A finished run and an empty turn alone were not enough, and the run that
|
|
27
|
+
* needed compaction was exactly the run that had neither: inside one activation
|
|
28
|
+
* every assistant turn holds calls until the one that ends it, so a single long
|
|
29
|
+
* activation never folded and grew until the provider refused it.
|
|
20
30
|
*/
|
|
21
|
-
/** How much of the tail is kept verbatim. A constant until a caller disagrees. */
|
|
22
|
-
const KEEP_FRACTION = 0.4;
|
|
23
31
|
export function compactionCut(entries) {
|
|
24
32
|
const boundary = Math.floor(entries.length * (1 - KEEP_FRACTION));
|
|
33
|
+
const awaiting = new Set();
|
|
25
34
|
let cut;
|
|
26
35
|
for (const entry of entries.slice(0, boundary)) {
|
|
36
|
+
if (entry.type === "assistant")
|
|
37
|
+
for (const call of entry.calls ?? [])
|
|
38
|
+
awaiting.add(call.callId);
|
|
39
|
+
if (entry.type === "tool.finished")
|
|
40
|
+
awaiting.delete(entry.callId);
|
|
41
|
+
// A run that ended takes its unanswered calls with it. The projection closes
|
|
42
|
+
// each one beside the turn that asked for it, so both fall on the same side
|
|
43
|
+
// of any later cut.
|
|
27
44
|
if (entry.type === "run.finished")
|
|
28
|
-
|
|
29
|
-
if (
|
|
45
|
+
awaiting.clear();
|
|
46
|
+
if (!awaiting.size)
|
|
30
47
|
cut = entry.seq;
|
|
31
48
|
}
|
|
32
49
|
return cut;
|
|
@@ -50,6 +67,32 @@ export async function summarize(input) {
|
|
|
50
67
|
maxOutputTokens: 2_000,
|
|
51
68
|
...(input.signal ? { signal: input.signal } : {}),
|
|
52
69
|
}));
|
|
53
|
-
|
|
70
|
+
if (!outcome.ok)
|
|
71
|
+
return outcome;
|
|
72
|
+
const summary = textOf(outcome.value.message.content);
|
|
73
|
+
return summary ? ok(summary) : err({ code: "failed", message: "Compaction produced no summary.", retryable: false });
|
|
74
|
+
}
|
|
75
|
+
/** Compact before native model calls. A failed summary ends the run with its provider error. */
|
|
76
|
+
export function createCompactionHook(options) {
|
|
77
|
+
return {
|
|
78
|
+
name: "compaction",
|
|
79
|
+
async beforeModel(context) {
|
|
80
|
+
if (estimateTokens(context.history()) <= options.maxInputTokens)
|
|
81
|
+
return;
|
|
82
|
+
const entries = context.entries();
|
|
83
|
+
const cut = compactionCut(entries);
|
|
84
|
+
if (cut === undefined || cut <= foldedThrough(entries))
|
|
85
|
+
return;
|
|
86
|
+
const summary = await summarize({
|
|
87
|
+
model: options.model,
|
|
88
|
+
messages: toMessages({ instructions: context.instructions, entries: entries.filter(entry => entry.seq <= cut) }),
|
|
89
|
+
...(options.prompt ? { prompt: options.prompt } : {}),
|
|
90
|
+
signal: context.signal,
|
|
91
|
+
});
|
|
92
|
+
if (!summary.ok)
|
|
93
|
+
return summary.error;
|
|
94
|
+
await context.commit([{ type: "summary", runId: context.runId, content: summary.value, replaces: cut }]);
|
|
95
|
+
},
|
|
96
|
+
};
|
|
54
97
|
}
|
|
55
98
|
//# sourceMappingURL=compaction.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compaction.js","sourceRoot":"","sources":["../../../../src/harness/adapters/native/compaction.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"compaction.js","sourceRoot":"","sources":["../../../../src/harness/adapters/native/compaction.ts"],"names":[],"mappings":"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","sourcesContent":["import type { LifecycleHook } from \"../../harness.js\";\nimport { foldedThrough, toMessages } from \"../../../session/messages.js\";\nimport type { Model, Message, ModelError } from \"../../../model/model.js\";\nimport type { Stored } from \"../../../session/entry.js\";\nimport { collect } from \"../../../model/model.js\";\nimport { ok, err, type Result } from \"../../../result.js\";\nimport { textOf } from \"../../../content.js\";\n\n/**\n * Crude and deliberate: four characters per token, over the serialized request.\n * A real count needs the provider's tokenizer, which would mean shipping one\n * per provider to decide a threshold that is itself a guess. Being wrong here\n * costs one early or late compaction, not correctness.\n */\nexport function estimateTokens(messages: readonly Message[]): number {\n return messages.reduce((total, message) => total + JSON.stringify(message).length, 0) / 4;\n}\n\n/** How much of the tail is kept verbatim. A constant until a caller disagrees. */\nconst KEEP_FRACTION = 0.4;\n\n/**\n * The latest position that can be folded without separating a tool call from\n * its results.\n *\n * A position is safe when the log is **drained** there: every call an assistant\n * turn asked for has its `tool.finished`. The gap between two batches of calls\n * is such a point, as are a turn that asked for nothing and the end of a run.\n * Cutting anywhere else leaves one call of a batch summarized and its sibling\n * live, which shows the model a result for a call it can no longer see.\n *\n * A finished run and an empty turn alone were not enough, and the run that\n * needed compaction was exactly the run that had neither: inside one activation\n * every assistant turn holds calls until the one that ends it, so a single long\n * activation never folded and grew until the provider refused it.\n */\nexport function compactionCut(entries: readonly Stored[]): number | undefined {\n const boundary = Math.floor(entries.length * (1 - KEEP_FRACTION));\n const awaiting = new Set<string>();\n let cut: number | undefined;\n for (const entry of entries.slice(0, boundary)) {\n if (entry.type === \"assistant\") for (const call of entry.calls ?? []) awaiting.add(call.callId);\n if (entry.type === \"tool.finished\") awaiting.delete(entry.callId);\n // A run that ended takes its unanswered calls with it. The projection closes\n // each one beside the turn that asked for it, so both fall on the same side\n // of any later cut.\n if (entry.type === \"run.finished\") awaiting.clear();\n if (!awaiting.size) cut = entry.seq;\n }\n return cut;\n}\n\n/** What the summary must preserve. Overridable, because what matters is domain-specific. */\nfunction summaryPrompt(messages: readonly Message[]): string {\n return [\n \"You are compacting an agent conversation that continues after this summary.\",\n \"Summarize the transcript below faithfully and concisely, covering:\",\n \"- Intent: the goal and the current state of the task\",\n \"- Decisions: choices made so far and why\",\n \"- Artifacts: files, outputs and results worth remembering (exact names, paths, values)\",\n \"- Pending: unfinished work, next steps, open questions\",\n \"\",\n messages.map((message) => `${message.role}: ${textOf(message.content)}`).join(\"\\n\\n\"),\n ].join(\"\\n\");\n}\n\nexport async function summarize(input: {\n model: Model;\n messages: readonly Message[];\n prompt?: (messages: readonly Message[]) => string;\n signal?: AbortSignal;\n}): Promise<Result<string, ModelError>> {\n const outcome = await collect(input.model.generate({\n messages: [{ role: \"user\", content: (input.prompt ?? summaryPrompt)(input.messages) }],\n maxOutputTokens: 2_000,\n ...(input.signal ? { signal: input.signal } : {}),\n }));\n if (!outcome.ok) return outcome;\n const summary = textOf(outcome.value.message.content);\n return summary ? ok(summary) : err({ code: \"failed\", message: \"Compaction produced no summary.\", retryable: false });\n}\n\n\n/** Compact before native model calls. A failed summary ends the run with its provider error. */\nexport function createCompactionHook(options: {\n model: Model;\n maxInputTokens: number;\n prompt?: (messages: readonly Message[]) => string;\n}): LifecycleHook {\n return {\n name: \"compaction\",\n async beforeModel(context) {\n if (estimateTokens(context.history()) <= options.maxInputTokens) return;\n const entries = context.entries();\n const cut = compactionCut(entries);\n if (cut === undefined || cut <= foldedThrough(entries)) return;\n const summary = await summarize({\n model: options.model,\n messages: toMessages({ instructions: context.instructions, entries: entries.filter(entry => entry.seq <= cut) }),\n ...(options.prompt ? { prompt: options.prompt } : {}),\n signal: context.signal,\n });\n if (!summary.ok) return summary.error;\n await context.commit([{ type: \"summary\", runId: context.runId, content: summary.value, replaces: cut }]);\n },\n };\n}\n"]}
|
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
import type { Harness } from "../../harness.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { Model } from "../../../model/model.js";
|
|
3
3
|
export interface NativeHarnessOptions {
|
|
4
4
|
model: Model;
|
|
5
|
-
/** Fold older turns into a summary once a request passes this size. */
|
|
6
|
-
compaction?: {
|
|
7
|
-
maxInputTokens: number;
|
|
8
|
-
/** Defaults to the main model. A cheaper one is usually the right call. */
|
|
9
|
-
model?: Model;
|
|
10
|
-
prompt?: (messages: readonly Message[]) => string;
|
|
11
|
-
};
|
|
12
5
|
maxOutputTokens?: number;
|
|
13
6
|
temperature?: number;
|
|
14
7
|
/** How hard the model should think, where the provider supports it. */
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { foldedThrough, toMessages } from "../../../session/messages.js";
|
|
2
|
-
import { compactionCut, estimateTokens, summarize } from "./compaction.js";
|
|
3
1
|
/**
|
|
4
2
|
* Ask the model, run what it asks for, repeat.
|
|
5
3
|
*
|
|
@@ -14,46 +12,17 @@ export function createNativeHarness(options) {
|
|
|
14
12
|
recovery: "history",
|
|
15
13
|
async run(context) {
|
|
16
14
|
const { runId } = context;
|
|
17
|
-
let compacting = true;
|
|
18
15
|
for (;;) {
|
|
19
16
|
if (context.signal.aborted)
|
|
20
17
|
return { status: "cancelled" };
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const cut = compactionCut(entries);
|
|
26
|
-
// Only ever fold forward. A cut at or behind the last summary removes
|
|
27
|
-
// nothing, so committing one would buy a model call and another entry
|
|
28
|
-
// and leave the request exactly as large — every turn, without end.
|
|
29
|
-
if (cut !== undefined && cut > folded) {
|
|
30
|
-
const summary = await summarize({
|
|
31
|
-
model: options.compaction.model ?? options.model,
|
|
32
|
-
// Exactly the span being replaced. Summarizing the whole history
|
|
33
|
-
// put everything after the cut into the summary as well, so it
|
|
34
|
-
// stayed in the request twice: once verbatim, once paraphrased.
|
|
35
|
-
messages: toMessages({
|
|
36
|
-
instructions: context.instructions,
|
|
37
|
-
entries: entries.filter((entry) => entry.seq <= cut),
|
|
38
|
-
}),
|
|
39
|
-
...(options.compaction.prompt ? { prompt: options.compaction.prompt } : {}),
|
|
40
|
-
signal: context.signal,
|
|
41
|
-
});
|
|
42
|
-
if (!summary) {
|
|
43
|
-
// Paid for and produced nothing. Attempted once per activation
|
|
44
|
-
// rather than once per turn: retrying bought another model call
|
|
45
|
-
// and another failure every turn, and the request stayed exactly
|
|
46
|
-
// as large either way.
|
|
47
|
-
compacting = false;
|
|
48
|
-
}
|
|
49
|
-
if (summary) {
|
|
50
|
-
// The folded entries stay in the log. Compaction changes what the
|
|
51
|
-
// model is shown on the next turn, never what happened.
|
|
52
|
-
await context.commit([{ type: "summary", runId, content: summary, replaces: cut }]);
|
|
53
|
-
history = context.history();
|
|
54
|
-
}
|
|
55
|
-
}
|
|
18
|
+
for (const hook of context.hooks ?? []) {
|
|
19
|
+
const failure = await hook.beforeModel?.(context);
|
|
20
|
+
if (failure)
|
|
21
|
+
return { status: "failed", error: failure };
|
|
56
22
|
}
|
|
23
|
+
if (context.signal.aborted)
|
|
24
|
+
return { status: "cancelled" };
|
|
25
|
+
const history = context.history();
|
|
57
26
|
// One cache mark, at the end of the system prefix: instructions plus
|
|
58
27
|
// run-scoped context, which are fixed for the life of the run.
|
|
59
28
|
const cacheAfter = history.findIndex((message) => message.role !== "system");
|
|
@@ -79,6 +48,8 @@ export function createNativeHarness(options) {
|
|
|
79
48
|
}
|
|
80
49
|
step = await generation.next();
|
|
81
50
|
}
|
|
51
|
+
for (const hook of context.hooks ?? [])
|
|
52
|
+
await hook.afterModel?.(context, step.value);
|
|
82
53
|
if (!step.value.ok) {
|
|
83
54
|
return step.value.error.code === "cancelled"
|
|
84
55
|
? { status: "cancelled" }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loop.js","sourceRoot":"","sources":["../../../../src/harness/adapters/native/loop.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Harness, HarnessContext, HarnessResult } from \"../../harness.js\";\nimport type { Entry } from \"../../../session/entry.js\";\nimport type { Message, Model } from \"../../../model/model.js\";\nimport { foldedThrough, toMessages } from \"../../../session/messages.js\";\nimport { compactionCut, estimateTokens, summarize } from \"./compaction.js\";\n\nexport interface NativeHarnessOptions {\n model: Model;\n /** Fold older turns into a summary once a request passes this size. */\n compaction?: {\n maxInputTokens: number;\n /** Defaults to the main model. A cheaper one is usually the right call. */\n model?: Model;\n prompt?: (messages: readonly Message[]) => string;\n };\n maxOutputTokens?: number;\n temperature?: number;\n /** How hard the model should think, where the provider supports it. */\n effort?: \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\";\n}\n\n/**\n * Ask the model, run what it asks for, repeat.\n *\n * The loop holds no transcript. Every turn it re-reads `context.history()`,\n * which projects the committed log, and every result it produces goes back\n * through `context.commit` before the next turn is built. There is exactly one\n * representation of the conversation and it is the log.\n */\nexport function createNativeHarness(options: NativeHarnessOptions): Harness {\n return {\n id: \"native\",\n recovery: \"history\",\n\n async run(context: HarnessContext): Promise<HarnessResult> {\n const { runId } = context;\n let compacting = true;\n\n for (;;) {\n if (context.signal.aborted) return { status: \"cancelled\" };\n\n let history = context.history();\n\n if (compacting && options.compaction && estimateTokens(history) > options.compaction.maxInputTokens) {\n const entries = context.entries();\n const folded = foldedThrough(entries);\n const cut = compactionCut(entries);\n\n // Only ever fold forward. A cut at or behind the last summary removes\n // nothing, so committing one would buy a model call and another entry\n // and leave the request exactly as large — every turn, without end.\n if (cut !== undefined && cut > folded) {\n const summary = await summarize({\n model: options.compaction.model ?? options.model,\n // Exactly the span being replaced. Summarizing the whole history\n // put everything after the cut into the summary as well, so it\n // stayed in the request twice: once verbatim, once paraphrased.\n messages: toMessages({\n instructions: context.instructions,\n entries: entries.filter((entry) => entry.seq <= cut),\n }),\n ...(options.compaction.prompt ? { prompt: options.compaction.prompt } : {}),\n signal: context.signal,\n });\n if (!summary) {\n // Paid for and produced nothing. Attempted once per activation\n // rather than once per turn: retrying bought another model call\n // and another failure every turn, and the request stayed exactly\n // as large either way.\n compacting = false;\n }\n if (summary) {\n // The folded entries stay in the log. Compaction changes what the\n // model is shown on the next turn, never what happened.\n await context.commit([{ type: \"summary\", runId, content: summary, replaces: cut }]);\n history = context.history();\n }\n }\n }\n\n // One cache mark, at the end of the system prefix: instructions plus\n // run-scoped context, which are fixed for the life of the run.\n const cacheAfter = history.findIndex((message) => message.role !== \"system\");\n\n const startedAt = new Date().toISOString();\n const generation = options.model.generate({\n messages: history,\n ...(context.tools ? { tools: context.tools.list() } : {}),\n ...(options.maxOutputTokens !== undefined ? { maxOutputTokens: options.maxOutputTokens } : {}),\n ...(options.temperature !== undefined ? { temperature: options.temperature } : {}),\n ...(options.effort ? { effort: options.effort } : {}),\n ...(cacheAfter > 0 ? { cacheAfter } : {}),\n signal: context.signal,\n });\n\n let step = await generation.next();\n while (!step.done) {\n const delta = step.value;\n if (delta.type === \"text.delta\") context.emit({ type: \"text.delta\", text: delta.text });\n if (delta.type === \"reasoning.delta\") context.emit({ type: \"reasoning.delta\", text: delta.text });\n if (delta.type === \"tool-call.delta\") {\n context.emit({ type: \"tool-call.delta\", callId: delta.callId, arguments: delta.arguments });\n }\n step = await generation.next();\n }\n if (!step.value.ok) {\n return step.value.error.code === \"cancelled\"\n ? { status: \"cancelled\" }\n : { status: \"failed\", error: step.value.error };\n }\n\n const endedAt = new Date().toISOString();\n const response = step.value.value;\n const calls = response.message.calls ?? [];\n\n await context.commit([{\n type: \"assistant\", runId, content: response.message.content,\n ...(calls.length ? { calls } : {}),\n usage: response.usage,\n generation: { ...(response.model ? { model: response.model } : {}), startedAt, endedAt },\n }]);\n\n if (!calls.length) {\n // The reason the model stopped decides the outcome, and it decides it\n // before the absence of tool calls does. A turn cut off by the output\n // ceiling or declined by a safety classifier is not an answer, and\n // reporting either as `completed` hands the caller a truncated or\n // refused response dressed as a finished one.\n if (response.finishReason === \"length\") {\n return { status: \"failed\", error: {\n code: \"output-truncated\",\n message: \"The response stopped at the output limit before it was finished.\",\n retryable: false,\n } };\n }\n if (response.finishReason === \"refusal\") {\n return { status: \"failed\", error: {\n code: \"refused\",\n message: \"The provider declined to answer.\",\n retryable: false,\n } };\n }\n return { status: \"completed\", output: response.message.content };\n }\n\n if (response.finishReason === \"length\") {\n // A truncated response can carry half-parsed arguments that happen to\n // be valid JSON. None of the batch runs; the model is told why and\n // can reissue.\n await context.commit(calls.map((call): Entry => ({\n type: \"tool.finished\", runId, callId: call.callId,\n result: { content: \"Not executed: the response was truncated. Reissue the call if it is still needed.\", isError: true },\n })));\n continue;\n }\n\n if (!context.tools) {\n await context.commit(calls.map((call): Entry => ({\n type: \"tool.finished\", runId, callId: call.callId,\n result: { content: `No tool named '${call.name}' is available.`, isError: true },\n })));\n continue;\n }\n\n // Recorded before anything runs. On restart an unmatched `tool.started`\n // is how we know the process died mid-effect — and why nothing is\n // re-executed automatically.\n await context.commit(calls.map((call): Entry => ({ type: \"tool.started\", runId, callId: call.callId })));\n\n const outcome = await context.tools.execute({ calls, signal: context.signal });\n await context.commit(outcome.results.map(({ callId, result }): Entry => ({\n type: \"tool.finished\", runId, callId, result,\n })));\n\n // Nothing is half-done here: every call asked for has been answered and\n // committed. Anything waiting is folded in before the next request is\n // built, so a message delivered to a busy session is read without the\n // activation having to be ended and its work thrown away.\n await context.drain?.();\n }\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"loop.js","sourceRoot":"","sources":["../../../../src/harness/adapters/native/loop.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Harness, HarnessContext, HarnessResult } from \"../../harness.js\";\nimport type { Entry } from \"../../../session/entry.js\";\nimport type { Model } from \"../../../model/model.js\";\n\nexport interface NativeHarnessOptions {\n model: Model;\n maxOutputTokens?: number;\n temperature?: number;\n /** How hard the model should think, where the provider supports it. */\n effort?: \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\";\n}\n\n/**\n * Ask the model, run what it asks for, repeat.\n *\n * The loop holds no transcript. Every turn it re-reads `context.history()`,\n * which projects the committed log, and every result it produces goes back\n * through `context.commit` before the next turn is built. There is exactly one\n * representation of the conversation and it is the log.\n */\nexport function createNativeHarness(options: NativeHarnessOptions): Harness {\n return {\n id: \"native\",\n recovery: \"history\",\n\n async run(context: HarnessContext): Promise<HarnessResult> {\n const { runId } = context;\n\n for (;;) {\n if (context.signal.aborted) return { status: \"cancelled\" };\n\n for (const hook of context.hooks ?? []) {\n const failure = await hook.beforeModel?.(context);\n if (failure) return { status: \"failed\", error: failure };\n }\n if (context.signal.aborted) return { status: \"cancelled\" };\n const history = context.history();\n\n // One cache mark, at the end of the system prefix: instructions plus\n // run-scoped context, which are fixed for the life of the run.\n const cacheAfter = history.findIndex((message) => message.role !== \"system\");\n\n const startedAt = new Date().toISOString();\n const generation = options.model.generate({\n messages: history,\n ...(context.tools ? { tools: context.tools.list() } : {}),\n ...(options.maxOutputTokens !== undefined ? { maxOutputTokens: options.maxOutputTokens } : {}),\n ...(options.temperature !== undefined ? { temperature: options.temperature } : {}),\n ...(options.effort ? { effort: options.effort } : {}),\n ...(cacheAfter > 0 ? { cacheAfter } : {}),\n signal: context.signal,\n });\n\n let step = await generation.next();\n while (!step.done) {\n const delta = step.value;\n if (delta.type === \"text.delta\") context.emit({ type: \"text.delta\", text: delta.text });\n if (delta.type === \"reasoning.delta\") context.emit({ type: \"reasoning.delta\", text: delta.text });\n if (delta.type === \"tool-call.delta\") {\n context.emit({ type: \"tool-call.delta\", callId: delta.callId, arguments: delta.arguments });\n }\n step = await generation.next();\n }\n for (const hook of context.hooks ?? []) await hook.afterModel?.(context, step.value);\n if (!step.value.ok) {\n return step.value.error.code === \"cancelled\"\n ? { status: \"cancelled\" }\n : { status: \"failed\", error: step.value.error };\n }\n\n const endedAt = new Date().toISOString();\n const response = step.value.value;\n const calls = response.message.calls ?? [];\n\n await context.commit([{\n type: \"assistant\", runId, content: response.message.content,\n ...(calls.length ? { calls } : {}),\n usage: response.usage,\n generation: { ...(response.model ? { model: response.model } : {}), startedAt, endedAt },\n }]);\n\n if (!calls.length) {\n // The reason the model stopped decides the outcome, and it decides it\n // before the absence of tool calls does. A turn cut off by the output\n // ceiling or declined by a safety classifier is not an answer, and\n // reporting either as `completed` hands the caller a truncated or\n // refused response dressed as a finished one.\n if (response.finishReason === \"length\") {\n return { status: \"failed\", error: {\n code: \"output-truncated\",\n message: \"The response stopped at the output limit before it was finished.\",\n retryable: false,\n } };\n }\n if (response.finishReason === \"refusal\") {\n return { status: \"failed\", error: {\n code: \"refused\",\n message: \"The provider declined to answer.\",\n retryable: false,\n } };\n }\n return { status: \"completed\", output: response.message.content };\n }\n\n if (response.finishReason === \"length\") {\n // A truncated response can carry half-parsed arguments that happen to\n // be valid JSON. None of the batch runs; the model is told why and\n // can reissue.\n await context.commit(calls.map((call): Entry => ({\n type: \"tool.finished\", runId, callId: call.callId,\n result: { content: \"Not executed: the response was truncated. Reissue the call if it is still needed.\", isError: true },\n })));\n continue;\n }\n\n if (!context.tools) {\n await context.commit(calls.map((call): Entry => ({\n type: \"tool.finished\", runId, callId: call.callId,\n result: { content: `No tool named '${call.name}' is available.`, isError: true },\n })));\n continue;\n }\n\n // Recorded before anything runs. On restart an unmatched `tool.started`\n // is how we know the process died mid-effect — and why nothing is\n // re-executed automatically.\n await context.commit(calls.map((call): Entry => ({ type: \"tool.started\", runId, callId: call.callId })));\n\n const outcome = await context.tools.execute({ calls, signal: context.signal });\n await context.commit(outcome.results.map(({ callId, result }): Entry => ({\n type: \"tool.finished\", runId, callId, result,\n })));\n\n // Nothing is half-done here: every call asked for has been answered and\n // committed. Anything waiting is folded in before the next request is\n // built, so a message delivered to a busy session is read without the\n // activation having to be ended and its work thrown away.\n await context.drain?.();\n }\n },\n };\n}\n"]}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Content } from "../content.js";
|
|
2
|
-
import type { Entry, Stored } from "../session/entry.js";
|
|
3
|
-
import type { Message } from "../model/model.js";
|
|
2
|
+
import type { Delivery, Entry, Stored, Usage } from "../session/entry.js";
|
|
3
|
+
import type { Message, ModelResponse, ModelError } from "../model/model.js";
|
|
4
|
+
import type { Result } from "../result.js";
|
|
4
5
|
import type { ToolExecutor } from "../tools/tool.js";
|
|
5
6
|
import type { Failure } from "../result.js";
|
|
6
7
|
import type { JsonValue } from "../json.js";
|
|
@@ -13,6 +14,10 @@ import type { JsonValue } from "../json.js";
|
|
|
13
14
|
* to the same question into the world.
|
|
14
15
|
*/
|
|
15
16
|
export type Update = {
|
|
17
|
+
type: "hook.error";
|
|
18
|
+
hook: string;
|
|
19
|
+
message: string;
|
|
20
|
+
} | {
|
|
16
21
|
type: "text.delta";
|
|
17
22
|
text: string;
|
|
18
23
|
} | {
|
|
@@ -33,6 +38,8 @@ export type Update = {
|
|
|
33
38
|
entry: Stored;
|
|
34
39
|
};
|
|
35
40
|
export interface HarnessContext {
|
|
41
|
+
/** The same callbacks used by the run; a harness invokes model boundaries it owns. */
|
|
42
|
+
hooks?: readonly LifecycleHook[];
|
|
36
43
|
sessionId: string;
|
|
37
44
|
runId: string;
|
|
38
45
|
instructions: Content;
|
|
@@ -111,6 +118,28 @@ export type HarnessResult = {
|
|
|
111
118
|
status: "failed";
|
|
112
119
|
error: Failure;
|
|
113
120
|
};
|
|
121
|
+
/** One application extension, configured alongside other lifecycle work. */
|
|
122
|
+
export interface LifecycleHook {
|
|
123
|
+
name: string;
|
|
124
|
+
beforeRun?(context: HarnessContext): void | Promise<void>;
|
|
125
|
+
beforeModel?(context: HarnessContext): void | Failure | Promise<void | Failure>;
|
|
126
|
+
afterModel?(context: HarnessContext, result: Result<ModelResponse, ModelError>): void | Promise<void>;
|
|
127
|
+
/** Only a completed, non-cancelled run may continue, once per hook name and run id. */
|
|
128
|
+
beforeStop?(context: HarnessContext, result: HarnessResult): void | {
|
|
129
|
+
input: Content;
|
|
130
|
+
} | {
|
|
131
|
+
deliveries: readonly Delivery[];
|
|
132
|
+
} | Promise<void | {
|
|
133
|
+
input: Content;
|
|
134
|
+
} | {
|
|
135
|
+
deliveries: readonly Delivery[];
|
|
136
|
+
}>;
|
|
137
|
+
/** After the terminal write; exceptions are observable but cannot change the outcome or skip other hooks. */
|
|
138
|
+
afterRun?(context: HarnessContext, result: HarnessResult & {
|
|
139
|
+
seq: number;
|
|
140
|
+
usage: Usage;
|
|
141
|
+
}): void | Promise<void>;
|
|
142
|
+
}
|
|
114
143
|
/**
|
|
115
144
|
* Executes one activation of an agent.
|
|
116
145
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"harness.js","sourceRoot":"","sources":["../../src/harness/harness.ts"],"names":[],"mappings":"","sourcesContent":["import type { Content } from \"../content.js\";\nimport type { Entry, Stored } from \"../session/entry.js\";\nimport type { Message } from \"../model/model.js\";\nimport type { ToolExecutor } from \"../tools/tool.js\";\nimport type { Failure } from \"../result.js\";\nimport type { JsonValue } from \"../json.js\";\n\n/**\n * Ephemeral output for a live viewer. Only committed entries are recovery state.\n *\n * An `entry` update carries the whole `Stored` shape — sequence and commit time\n * both. Anything building a live projection off this stream needs the timestamp\n * the log already has, and inventing one at receipt would put a second answer\n * to the same question into the world.\n */\nexport type Update =\n | { type: \"text.delta\"; text: string }\n | { type: \"reasoning.delta\"; text: string }\n | { type: \"tool.progress\"; callId: string; data: JsonValue }\n /** Arguments arriving a fragment at a time, so a viewer can show a call forming. */\n | { type: \"tool-call.delta\"; callId: string; arguments: string }\n | { type: \"entry\"; entry: Stored };\n\nexport interface HarnessContext {\n sessionId: string;\n runId: string;\n instructions: Content;\n /**\n * The log projected for this turn. A function, not a snapshot: it is rebuilt\n * from committed entries every time it is read, so a harness cannot hold a\n * transcript that drifts from the log.\n */\n history(): readonly Message[];\n /**\n * The committed log itself. A harness needs it to reason in sequence numbers\n * — where a compaction may cut, which call has no result yet — which the\n * projection deliberately does not carry.\n */\n entries(): readonly Stored[];\n /**\n * Application context placed around the cache boundary. `run` is stable for\n * the whole run and sits inside the cached prefix; `turn` is for this request\n * only and sits after it. Getting that split right is the loop's job because\n * only the loop knows where the prefix ends.\n */\n context?: { run?: string; turn?: string };\n tools?: ToolExecutor;\n /**\n * Commit to the log. Returns when durable — a harness never holds\n * uncommitted state.\n *\n * A refused write **rejects**, and a harness must not catch it: a lost\n * compare-and-swap means another writer is at this position, and the run is\n * over. Anything the agent was sending with that write goes with it.\n */\n commit(entries: readonly Entry[]): Promise<void>;\n /**\n * Fold any waiting input into this activation, and say how much arrived.\n *\n * Called at a boundary the harness picks — ours takes it after each batch of\n * tool results, which is the last point at which nothing is half-done. This\n * is the whole of `priority: \"turn\"`: a message reaching a busy session\n * without the turn's work being thrown away to get its attention.\n *\n * A harness with no such point does not offer this, and a `\"turn\"` delivery\n * to it waits for the next activation instead. It is never promoted to\n * ending the one that is running — the sender asked for the place that costs\n * nothing, and the answer to \"I cannot\" is later, not more destructive.\n *\n * Absent when there is no store, because then nothing can be waiting.\n *\n * Two things it will not do. It folds nothing into an activation that has\n * been cancelled — a message taken off the queue by a run that is ending is a\n * message nobody answers and nothing can find again. And a store it cannot\n * read answers zero, the same as an empty queue: the failure is not hidden\n * for long, because the next commit meets it.\n */\n drain?(): Promise<number>;\n emit(update: Update): void;\n signal: AbortSignal;\n}\n\n/**\n * How one activation ended, and nothing about what it consumed.\n *\n * A harness used to report usage too, and that was a second answer to a\n * question the log already answers: every generation's counts are on the\n * `assistant` entry that carried it. `runAgent` sums those, so accounting is\n * the same fact for a harness that owns its own loop as for ours, and a\n * cancelled or failed activation still says what it spent.\n */\nexport type HarnessResult =\n | { status: \"completed\"; output: Content }\n | { status: \"cancelled\" }\n | { status: \"failed\"; error: Failure };\n\n/**\n * Executes one activation of an agent.\n *\n * One fact, not eight. Everything the old capability matrix carried was either\n * optional output, a different subsystem, or something no consumer asked about.\n *\n * `toolUse` went the same way, one release later. It claimed to say whose tools\n * ran — \"application\" through the executor, \"harness\" its own — and three\n * things were wrong with it. Nothing branched on it; no document owned the\n * claim; and the word did not survive contact with the adapters. A harness can\n * run a *vendor's* tools through our executor, validated and authorized, and a\n * harness can run tools *we wrote* inside its own process. Whose tools, whose\n * code and whose authority are three questions, and one enum answered none of\n * them reliably.\n *\n * The question it was reaching for — did anyone authorize this call — is a fact\n * about a call, not about a harness, and belongs on the tool entry if and when\n * something needs to read it. `recipes/vendored-agent` and\n * `docs/ARCHITECTURE.md` carry the per-adapter comparison in prose, which is\n * where a fact about adapters belongs and where it can be accurate.\n */\nexport interface Harness {\n readonly id: string;\n /** \"history\": committed entries are enough to restart. \"none\": an interrupted run is over. */\n readonly recovery: \"history\" | \"none\";\n run(context: HarnessContext): Promise<HarnessResult>;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"harness.js","sourceRoot":"","sources":["../../src/harness/harness.ts"],"names":[],"mappings":"","sourcesContent":["import type { Content } from \"../content.js\";\nimport type { Delivery, Entry, Stored, Usage } from \"../session/entry.js\";\nimport type { Message, ModelResponse, ModelError } from \"../model/model.js\";\nimport type { Result } from \"../result.js\";\nimport type { ToolExecutor } from \"../tools/tool.js\";\nimport type { Failure } from \"../result.js\";\nimport type { JsonValue } from \"../json.js\";\n\n/**\n * Ephemeral output for a live viewer. Only committed entries are recovery state.\n *\n * An `entry` update carries the whole `Stored` shape — sequence and commit time\n * both. Anything building a live projection off this stream needs the timestamp\n * the log already has, and inventing one at receipt would put a second answer\n * to the same question into the world.\n */\nexport type Update =\n | { type: \"hook.error\"; hook: string; message: string }\n | { type: \"text.delta\"; text: string }\n | { type: \"reasoning.delta\"; text: string }\n | { type: \"tool.progress\"; callId: string; data: JsonValue }\n /** Arguments arriving a fragment at a time, so a viewer can show a call forming. */\n | { type: \"tool-call.delta\"; callId: string; arguments: string }\n | { type: \"entry\"; entry: Stored };\n\nexport interface HarnessContext {\n /** The same callbacks used by the run; a harness invokes model boundaries it owns. */\n hooks?: readonly LifecycleHook[];\n sessionId: string;\n runId: string;\n instructions: Content;\n /**\n * The log projected for this turn. A function, not a snapshot: it is rebuilt\n * from committed entries every time it is read, so a harness cannot hold a\n * transcript that drifts from the log.\n */\n history(): readonly Message[];\n /**\n * The committed log itself. A harness needs it to reason in sequence numbers\n * — where a compaction may cut, which call has no result yet — which the\n * projection deliberately does not carry.\n */\n entries(): readonly Stored[];\n /**\n * Application context placed around the cache boundary. `run` is stable for\n * the whole run and sits inside the cached prefix; `turn` is for this request\n * only and sits after it. Getting that split right is the loop's job because\n * only the loop knows where the prefix ends.\n */\n context?: { run?: string; turn?: string };\n tools?: ToolExecutor;\n /**\n * Commit to the log. Returns when durable — a harness never holds\n * uncommitted state.\n *\n * A refused write **rejects**, and a harness must not catch it: a lost\n * compare-and-swap means another writer is at this position, and the run is\n * over. Anything the agent was sending with that write goes with it.\n */\n commit(entries: readonly Entry[]): Promise<void>;\n /**\n * Fold any waiting input into this activation, and say how much arrived.\n *\n * Called at a boundary the harness picks — ours takes it after each batch of\n * tool results, which is the last point at which nothing is half-done. This\n * is the whole of `priority: \"turn\"`: a message reaching a busy session\n * without the turn's work being thrown away to get its attention.\n *\n * A harness with no such point does not offer this, and a `\"turn\"` delivery\n * to it waits for the next activation instead. It is never promoted to\n * ending the one that is running — the sender asked for the place that costs\n * nothing, and the answer to \"I cannot\" is later, not more destructive.\n *\n * Absent when there is no store, because then nothing can be waiting.\n *\n * Two things it will not do. It folds nothing into an activation that has\n * been cancelled — a message taken off the queue by a run that is ending is a\n * message nobody answers and nothing can find again. And a store it cannot\n * read answers zero, the same as an empty queue: the failure is not hidden\n * for long, because the next commit meets it.\n */\n drain?(): Promise<number>;\n emit(update: Update): void;\n signal: AbortSignal;\n}\n\n/**\n * How one activation ended, and nothing about what it consumed.\n *\n * A harness used to report usage too, and that was a second answer to a\n * question the log already answers: every generation's counts are on the\n * `assistant` entry that carried it. `runAgent` sums those, so accounting is\n * the same fact for a harness that owns its own loop as for ours, and a\n * cancelled or failed activation still says what it spent.\n */\nexport type HarnessResult =\n | { status: \"completed\"; output: Content }\n | { status: \"cancelled\" }\n | { status: \"failed\"; error: Failure };\n\n/** One application extension, configured alongside other lifecycle work. */\nexport interface LifecycleHook {\n name: string;\n beforeRun?(context: HarnessContext): void | Promise<void>;\n beforeModel?(context: HarnessContext): void | Failure | Promise<void | Failure>;\n afterModel?(context: HarnessContext, result: Result<ModelResponse, ModelError>): void | Promise<void>;\n /** Only a completed, non-cancelled run may continue, once per hook name and run id. */\n beforeStop?(context: HarnessContext, result: HarnessResult):\n void | { input: Content } | { deliveries: readonly Delivery[] } |\n Promise<void | { input: Content } | { deliveries: readonly Delivery[] }>;\n /** After the terminal write; exceptions are observable but cannot change the outcome or skip other hooks. */\n afterRun?(context: HarnessContext, result: HarnessResult & { seq: number; usage: Usage }): void | Promise<void>;\n}\n\n/**\n * Executes one activation of an agent.\n *\n * One fact, not eight. Everything the old capability matrix carried was either\n * optional output, a different subsystem, or something no consumer asked about.\n *\n * `toolUse` went the same way, one release later. It claimed to say whose tools\n * ran — \"application\" through the executor, \"harness\" its own — and three\n * things were wrong with it. Nothing branched on it; no document owned the\n * claim; and the word did not survive contact with the adapters. A harness can\n * run a *vendor's* tools through our executor, validated and authorized, and a\n * harness can run tools *we wrote* inside its own process. Whose tools, whose\n * code and whose authority are three questions, and one enum answered none of\n * them reliably.\n *\n * The question it was reaching for — did anyone authorize this call — is a fact\n * about a call, not about a harness, and belongs on the tool entry if and when\n * something needs to read it. `recipes/vendored-agent` and\n * `docs/ARCHITECTURE.md` carry the per-adapter comparison in prose, which is\n * where a fact about adapters belongs and where it can be accurate.\n */\nexport interface Harness {\n readonly id: string;\n /** \"history\": committed entries are enough to restart. \"none\": an interrupted run is over. */\n readonly recovery: \"history\" | \"none\";\n run(context: HarnessContext): Promise<HarnessResult>;\n}\n"]}
|
package/dist/harness/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { createNativeHarness } from "./adapters/native/loop.js";
|
|
2
|
+
export { createCompactionHook } from "./adapters/native/compaction.js";
|
|
2
3
|
export type { NativeHarnessOptions } from "./adapters/native/loop.js";
|
|
3
|
-
export type { Harness, HarnessContext, HarnessResult, Update } from "./harness.js";
|
|
4
|
+
export type { Harness, HarnessContext, HarnessResult, LifecycleHook, Update } from "./harness.js";
|
package/dist/harness/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/harness/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC","sourcesContent":["export { createNativeHarness } from \"./adapters/native/loop.js\";\nexport type { NativeHarnessOptions } from \"./adapters/native/loop.js\";\nexport type { Harness, HarnessContext, HarnessResult, Update } from \"./harness.js\";\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/harness/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC","sourcesContent":["export { createNativeHarness } from \"./adapters/native/loop.js\";\nexport { createCompactionHook } from \"./adapters/native/compaction.js\";\nexport type { NativeHarnessOptions } from \"./adapters/native/loop.js\";\nexport type { Harness, HarnessContext, HarnessResult, LifecycleHook, Update } from \"./harness.js\";\n"]}
|
|
@@ -168,7 +168,7 @@ function encodeContent(content) {
|
|
|
168
168
|
}
|
|
169
169
|
const encodeTool = (tool) => ({
|
|
170
170
|
type: "function",
|
|
171
|
-
function: { name: tool.name, description: tool.description, parameters: tool.parameters },
|
|
171
|
+
function: { name: tool.name, description: tool.description, parameters: tool.parameters, strict: false },
|
|
172
172
|
});
|
|
173
173
|
const decodeFinish = (reason) => reason === "tool_calls" ? "tool-calls"
|
|
174
174
|
: reason === "length" ? "length"
|