@a-dray/aglib 0.2.0 → 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 +4 -9
- package/dist/agent.js.map +1 -1
- package/dist/harness/adapters/native/compaction.d.ts +10 -2
- package/dist/harness/adapters/native/compaction.js +29 -1
- package/dist/harness/adapters/native/compaction.js.map +1 -1
- package/dist/harness/adapters/native/loop.d.ts +1 -25
- 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 +73 -39
- package/dist/run.js.map +1 -1
- package/dist/session/entry.d.ts +5 -0
- package/dist/session/entry.js.map +1 -1
- package/dist/session/messages.js +3 -0
- 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";
|
|
@@ -27,13 +27,8 @@ export interface Agent {
|
|
|
27
27
|
* wrote.
|
|
28
28
|
*/
|
|
29
29
|
attribution?: boolean;
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
sessionId: string;
|
|
33
|
-
runId: string;
|
|
34
|
-
outcome: "completed" | "failed" | "cancelled";
|
|
35
|
-
output: Content;
|
|
36
|
-
}): readonly Delivery[] | Promise<readonly Delivery[]>;
|
|
30
|
+
/** Ordered, uniquely named lifecycle callbacks. Continuations share this run's durable log. */
|
|
31
|
+
hooks?: readonly LifecycleHook[];
|
|
37
32
|
/**
|
|
38
33
|
* Ceilings on one activation, over the facts this library holds: turns and
|
|
39
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
|
|
@@ -28,4 +30,10 @@ export declare function summarize(input: {
|
|
|
28
30
|
messages: readonly Message[];
|
|
29
31
|
prompt?: (messages: readonly Message[]) => string;
|
|
30
32
|
signal?: AbortSignal;
|
|
31
|
-
}): 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.
|
|
@@ -65,6 +67,32 @@ export async function summarize(input) {
|
|
|
65
67
|
maxOutputTokens: 2_000,
|
|
66
68
|
...(input.signal ? { signal: input.signal } : {}),
|
|
67
69
|
}));
|
|
68
|
-
|
|
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
|
+
};
|
|
69
97
|
}
|
|
70
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,31 +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
|
-
/**
|
|
8
|
-
* The estimated request size a fold happens above.
|
|
9
|
-
*
|
|
10
|
-
* A budget, not a provider fact: it belongs below the input window of the
|
|
11
|
-
* model that answers, with room for the turn that follows the fold. Folding
|
|
12
|
-
* early spends a model call and hands the run a paraphrase of work the log
|
|
13
|
-
* still holds in full; folding late risks the request a provider refuses.
|
|
14
|
-
*/
|
|
15
|
-
maxInputTokens: number;
|
|
16
|
-
/**
|
|
17
|
-
* Who writes the summary. Defaults to the run's own model, so an
|
|
18
|
-
* application that says nothing meets neither a second provider nor a
|
|
19
|
-
* second credential.
|
|
20
|
-
*
|
|
21
|
-
* A cheaper one is right where the span being folded is long and reading it
|
|
22
|
-
* back is all the work — this is not the reasoning the main model is being
|
|
23
|
-
* paid for. It still has to take the whole span in one request, and to be
|
|
24
|
-
* good enough that what it drops is what did not matter.
|
|
25
|
-
*/
|
|
26
|
-
model?: Model;
|
|
27
|
-
prompt?: (messages: readonly Message[]) => string;
|
|
28
|
-
};
|
|
29
5
|
maxOutputTokens?: number;
|
|
30
6
|
temperature?: number;
|
|
31
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 /**\n * The estimated request size a fold happens above.\n *\n * A budget, not a provider fact: it belongs below the input window of the\n * model that answers, with room for the turn that follows the fold. Folding\n * early spends a model call and hands the run a paraphrase of work the log\n * still holds in full; folding late risks the request a provider refuses.\n */\n maxInputTokens: number;\n /**\n * Who writes the summary. Defaults to the run's own model, so an\n * application that says nothing meets neither a second provider nor a\n * second credential.\n *\n * A cheaper one is right where the span being folded is long and reading it\n * back is all the work — this is not the reasoning the main model is being\n * paid for. It still has to take the whole span in one request, and to be\n * good enough that what it drops is what did not matter.\n */\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":"AAYA;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAA6B;IAC/D,OAAO;QACL,EAAE,EAAE,QAAQ;QACZ,QAAQ,EAAE,SAAS;QAEnB,KAAK,CAAC,GAAG,CAAC,OAAuB;YAC/B,MAAM,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC;YAE1B,SAAS,CAAC;gBACR,IAAI,OAAO,CAAC,MAAM,CAAC,OAAO;oBAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;gBAE3D,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,IAAI,EAAE,EAAE,CAAC;oBACvC,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,CAAC;oBAClD,IAAI,OAAO;wBAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;gBAC3D,CAAC;gBACD,IAAI,OAAO,CAAC,MAAM,CAAC,OAAO;oBAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;gBAC3D,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;gBAElC,qEAAqE;gBACrE,+DAA+D;gBAC/D,MAAM,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC;gBAE7E,MAAM,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;gBAC3C,MAAM,UAAU,GAAG,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC;oBACxC,QAAQ,EAAE,OAAO;oBACjB,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;oBACzD,GAAG,CAAC,OAAO,CAAC,eAAe,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;oBAC9F,GAAG,CAAC,OAAO,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;oBAClF,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;oBACrD,GAAG,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;oBACzC,MAAM,EAAE,OAAO,CAAC,MAAM;iBACvB,CAAC,CAAC;gBAEH,IAAI,IAAI,GAAG,MAAM,UAAU,CAAC,IAAI,EAAE,CAAC;gBACnC,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;oBAClB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;oBACzB,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY;wBAAE,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;oBACxF,IAAI,KAAK,CAAC,IAAI,KAAK,iBAAiB;wBAAE,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,iBAAiB,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;oBAClG,IAAI,KAAK,CAAC,IAAI,KAAK,iBAAiB,EAAE,CAAC;wBACrC,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,iBAAiB,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC;oBAC9F,CAAC;oBACD,IAAI,GAAG,MAAM,UAAU,CAAC,IAAI,EAAE,CAAC;gBACjC,CAAC;gBACD,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,KAAK,IAAI,EAAE;oBAAE,MAAM,IAAI,CAAC,UAAU,EAAE,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;gBACrF,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC;oBACnB,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,WAAW;wBAC1C,CAAC,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE;wBACzB,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;gBACpD,CAAC;gBAED,MAAM,OAAO,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;gBACzC,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC;gBAClC,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC;gBAE3C,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;wBACpB,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,OAAO;wBAC3D,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;wBAClC,KAAK,EAAE,QAAQ,CAAC,KAAK;wBACrB,UAAU,EAAE,EAAE,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE;qBACzF,CAAC,CAAC,CAAC;gBAEJ,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;oBAClB,sEAAsE;oBACtE,sEAAsE;oBACtE,mEAAmE;oBACnE,kEAAkE;oBAClE,8CAA8C;oBAC9C,IAAI,QAAQ,CAAC,YAAY,KAAK,QAAQ,EAAE,CAAC;wBACvC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE;gCAChC,IAAI,EAAE,kBAAkB;gCACxB,OAAO,EAAE,kEAAkE;gCAC3E,SAAS,EAAE,KAAK;6BACjB,EAAE,CAAC;oBACN,CAAC;oBACD,IAAI,QAAQ,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;wBACxC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE;gCAChC,IAAI,EAAE,SAAS;gCACf,OAAO,EAAE,kCAAkC;gCAC3C,SAAS,EAAE,KAAK;6BACjB,EAAE,CAAC;oBACN,CAAC;oBACD,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;gBACnE,CAAC;gBAED,IAAI,QAAQ,CAAC,YAAY,KAAK,QAAQ,EAAE,CAAC;oBACvC,sEAAsE;oBACtE,mEAAmE;oBACnE,eAAe;oBACf,MAAM,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAS,EAAE,CAAC,CAAC;wBAC/C,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM;wBACjD,MAAM,EAAE,EAAE,OAAO,EAAE,mFAAmF,EAAE,OAAO,EAAE,IAAI,EAAE;qBACxH,CAAC,CAAC,CAAC,CAAC;oBACL,SAAS;gBACX,CAAC;gBAED,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;oBACnB,MAAM,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAS,EAAE,CAAC,CAAC;wBAC/C,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM;wBACjD,MAAM,EAAE,EAAE,OAAO,EAAE,kBAAkB,IAAI,CAAC,IAAI,iBAAiB,EAAE,OAAO,EAAE,IAAI,EAAE;qBACjF,CAAC,CAAC,CAAC,CAAC;oBACL,SAAS;gBACX,CAAC;gBAED,wEAAwE;gBACxE,kEAAkE;gBAClE,6BAA6B;gBAC7B,MAAM,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAS,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC;gBAEzG,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;gBAC/E,MAAM,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,EAAS,EAAE,CAAC,CAAC;oBACvE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM;iBAC7C,CAAC,CAAC,CAAC,CAAC;gBAEL,wEAAwE;gBACxE,sEAAsE;gBACtE,sEAAsE;gBACtE,0DAA0D;gBAC1D,MAAM,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC;YAC1B,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC","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"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/model/adapters/openai-compatible/index.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n ContentPart, Message, Model, ModelDelta, ModelError, ModelRequest, ModelResponse, ToolCall, ToolSpec, Usage,\n} from \"../../model.js\";\nimport { err, ok, type Result } from \"../../../result.js\";\nimport { textOf } from \"../../../content.js\";\n\nexport interface OpenAiCompatibleOptions {\n apiKey: string;\n baseUrl: string;\n model: string;\n headers?: Readonly<Record<string, string>>;\n /**\n * How this endpoint spells reasoning effort. OpenAI takes `reasoning_effort`;\n * OpenRouter takes `reasoning: { effort }` and forwards it to whoever is\n * serving the model. Sending both is not compatibility — OpenAI rejects the\n * request outright — so the dialect is declared rather than guessed.\n */\n effortParameter?: \"reasoning_effort\" | \"reasoning\" | \"none\";\n /** Injectable for tests; defaults to global fetch. */\n fetch?: typeof fetch;\n}\n\n/**\n * The OpenAI chat-completions wire, which almost every provider now speaks —\n * OpenRouter, Together, Groq, vLLM, Ollama. One adapter, one base URL.\n */\nexport function createOpenAiCompatibleModel(options: OpenAiCompatibleOptions): Model {\n const call = options.fetch ?? fetch;\n return {\n id: `openai-compatible:${options.model}`,\n\n async *generate(request: ModelRequest): AsyncGenerator<ModelDelta, Result<ModelResponse, ModelError>> {\n let response: Response;\n try {\n response = await call(`${options.baseUrl}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n authorization: `Bearer ${options.apiKey}`,\n ...options.headers,\n },\n body: JSON.stringify({\n model: options.model,\n messages: request.messages.map(encodeMessage),\n ...(request.tools?.length ? { tools: request.tools.map(encodeTool) } : {}),\n ...(request.maxOutputTokens !== undefined ? { max_tokens: request.maxOutputTokens } : {}),\n ...(request.temperature !== undefined ? { temperature: request.temperature } : {}),\n ...encodeEffort(request.effort, options.effortParameter ?? \"reasoning_effort\"),\n stream: true,\n stream_options: { include_usage: true },\n }),\n ...(request.signal ? { signal: request.signal } : {}),\n });\n } catch (error) {\n return err(transportError(error, request.signal));\n }\n\n if (!response.ok) return err(await httpError(response));\n if (!response.body) return err({ code: \"provider\", message: \"No response body\", retryable: true });\n\n const text: string[] = [];\n const calls = new Map<number, { callId: string; name: string; arguments: string }>();\n let finish: ModelResponse[\"finishReason\"] = \"stop\";\n let usage: Usage = {};\n let model: string | undefined;\n\n // A stream that dies mid-body — cancelled, dropped, truncated — must come\n // back as a typed failure like any other. Without this the generator throws,\n // `run.result` rejects instead of resolving `cancelled`, no `run.finished`\n // is ever committed, and the activation stays open until its claim expires.\n try {\n for await (const event of sseLines(response.body)) {\n if (event === \"[DONE]\") break;\n let frame: ChatFrame;\n try { frame = JSON.parse(event) as ChatFrame; } catch { continue; }\n model ??= frame.model;\n if (frame.usage) usage = decodeUsage(frame.usage);\n\n const choice = frame.choices?.[0];\n if (!choice) continue;\n if (choice.finish_reason) finish = decodeFinish(choice.finish_reason);\n\n const content = choice.delta?.content;\n if (content) { text.push(content); yield { type: \"text.delta\", text: content }; }\n\n const reasoning = choice.delta?.reasoning;\n if (reasoning) yield { type: \"reasoning.delta\", text: reasoning };\n\n for (const fragment of choice.delta?.tool_calls ?? []) {\n // Providers stream tool arguments in fragments, keyed by position;\n // only the first fragment carries the id and the name.\n const existing = calls.get(fragment.index) ?? {\n callId: fragment.id ?? `call_${fragment.index}`,\n name: fragment.function?.name ?? \"\",\n arguments: \"\",\n };\n if (fragment.id) existing.callId = fragment.id;\n if (fragment.function?.name) existing.name = fragment.function.name;\n if (fragment.function?.arguments) {\n existing.arguments += fragment.function.arguments;\n yield { type: \"tool-call.delta\", callId: existing.callId, arguments: fragment.function.arguments };\n }\n calls.set(fragment.index, existing);\n }\n }\n } catch (error) {\n return err(transportError(error, request.signal));\n }\n\n const collected: ToolCall[] = [...calls.values()]\n .map((call) => ({ callId: call.callId, name: call.name, arguments: call.arguments || \"{}\" }));\n\n return ok({\n message: { content: text.join(\"\"), ...(collected.length ? { calls: collected } : {}) },\n finishReason: collected.length && finish === \"stop\" ? \"tool-calls\" : finish,\n usage,\n ...(model ? { model } : {}),\n });\n },\n };\n}\n\n/** OpenRouter is this wire with a fixed base URL and attribution headers. */\nexport function createOpenRouterModel(input: {\n apiKey: string; model: string; appUrl?: string; appName?: string; fetch?: typeof fetch;\n}): Model {\n return createOpenAiCompatibleModel({\n apiKey: input.apiKey,\n baseUrl: \"https://openrouter.ai/api/v1\",\n model: input.model,\n effortParameter: \"reasoning\",\n headers: {\n ...(input.appUrl ? { \"http-referer\": input.appUrl } : {}),\n ...(input.appName ? { \"x-title\": input.appName } : {}),\n },\n ...(input.fetch ? { fetch: input.fetch } : {}),\n });\n}\n\nfunction encodeEffort(\n effort: ModelRequest[\"effort\"],\n parameter: \"reasoning_effort\" | \"reasoning\" | \"none\",\n): Record<string, unknown> {\n if (!effort || parameter === \"none\") return {};\n return parameter === \"reasoning\" ? { reasoning: { effort } } : { reasoning_effort: effort };\n}\n\nfunction encodeMessage(message: Message): Record<string, unknown> {\n if (message.role === \"tool\") {\n return { role: \"tool\", tool_call_id: message.callId, content: textOf(message.content) };\n }\n if (message.role === \"assistant\") {\n return {\n role: \"assistant\",\n content: textOf(message.content) || null,\n ...(message.calls?.length\n ? {\n tool_calls: message.calls.map((call) => ({\n id: call.callId, type: \"function\",\n function: { name: call.name, arguments: call.arguments },\n })),\n }\n : {}),\n };\n }\n return { role: message.role, content: encodeContent(message.content) };\n}\n\nfunction encodeContent(content: Message[\"content\"]): unknown {\n if (typeof content === \"string\") return content;\n const parts: Record<string, unknown>[] = [];\n for (const part of content as readonly ContentPart[]) {\n if (part.type === \"text\") { parts.push({ type: \"text\", text: part.text }); continue; }\n if (part.type === \"image\") {\n const url = part.source.kind === \"url\" ? part.source.url : `data:${part.mediaType};base64,${part.source.data}`;\n parts.push({ type: \"image_url\", image_url: { url } });\n }\n // Files and opaque blocks have no place on this wire. Dropping them beats\n // stringifying them into something the model would read as prose.\n }\n const only = parts.length === 1 ? parts[0] : undefined;\n return only?.type === \"text\" ? only.text : parts;\n}\n\nconst encodeTool = (tool: ToolSpec) => ({\n type: \"function\",\n function: { name: tool.name, description: tool.description, parameters: tool.parameters },\n});\n\nconst decodeFinish = (reason: string): ModelResponse[\"finishReason\"] =>\n reason === \"tool_calls\" ? \"tool-calls\"\n : reason === \"length\" ? \"length\"\n : reason === \"content_filter\" ? \"refusal\"\n : \"stop\";\n\nconst decodeUsage = (usage: ChatUsage): Usage => ({\n // Carried, not computed. A router picks an upstream provider per request and\n // adds its own margin, so what it charged is a thing only it can say — and\n // the frame carrying it is the one this adapter already reads.\n ...(typeof usage.cost === \"number\" ? { costUsd: usage.cost } : {}),\n // `prompt_tokens` is the whole prompt and `cached_tokens` is counted inside\n // it — \"cached tokens present in the prompt\". `Usage` keeps the three input\n // counts disjoint, so the cached part comes out here rather than every\n // caller having to know which wire produced the number it is holding.\n ...(usage.prompt_tokens !== undefined\n ? { inputTokens: Math.max(usage.prompt_tokens - (usage.prompt_tokens_details?.cached_tokens ?? 0), 0) } : {}),\n ...(usage.completion_tokens !== undefined ? { outputTokens: usage.completion_tokens } : {}),\n ...(usage.prompt_tokens_details?.cached_tokens !== undefined\n ? { cacheReadTokens: usage.prompt_tokens_details.cached_tokens } : {}),\n});\n\nasync function httpError(response: Response): Promise<ModelError> {\n const body = await response.text().catch(() => \"\");\n const code: ModelError[\"code\"] =\n response.status === 401 || response.status === 403 ? \"auth\"\n : response.status === 429 ? \"rate-limit\"\n : response.status === 400 && /context|token/i.test(body) ? \"context-length\"\n : response.status >= 500 ? \"provider\"\n : \"failed\";\n return {\n code,\n message: `${response.status} ${response.statusText}${body ? `: ${body.slice(0, 400)}` : \"\"}`,\n retryable: code === \"rate-limit\" || code === \"provider\",\n };\n}\n\nfunction transportError(error: unknown, signal?: AbortSignal): ModelError {\n if (signal?.aborted) return { code: \"cancelled\", message: \"Generation cancelled\", retryable: false };\n return { code: \"provider\", message: error instanceof Error ? error.message : String(error), retryable: true };\n}\n\n/** Server-sent events, reassembled across chunk boundaries. */\nasync function* sseLines(body: ReadableStream<Uint8Array>): AsyncGenerator<string> {\n const decoder = new TextDecoder();\n let buffer = \"\";\n for await (const chunk of body as unknown as AsyncIterable<Uint8Array>) {\n buffer += decoder.decode(chunk, { stream: true });\n let newline = buffer.indexOf(\"\\n\");\n while (newline >= 0) {\n const line = buffer.slice(0, newline).trim();\n buffer = buffer.slice(newline + 1);\n if (line.startsWith(\"data:\")) yield line.slice(5).trim();\n newline = buffer.indexOf(\"\\n\");\n }\n }\n}\n\ninterface ChatUsage {\n prompt_tokens?: number;\n completion_tokens?: number;\n prompt_tokens_details?: { cached_tokens?: number };\n /**\n * What this generation was charged. OpenRouter sets it on every response and\n * calls it credits, whose base currency is US dollars; plain OpenAI and the\n * self-hosted endpoints on this wire do not send it at all, so it is absent\n * rather than zero there.\n */\n cost?: number;\n}\ninterface ChatFrame {\n model?: string;\n usage?: ChatUsage;\n choices?: readonly {\n finish_reason?: string | null;\n delta?: {\n content?: string;\n reasoning?: string;\n tool_calls?: readonly { index: number; id?: string; function?: { name?: string; arguments?: string } }[];\n };\n }[];\n}\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/model/adapters/openai-compatible/index.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n ContentPart, Message, Model, ModelDelta, ModelError, ModelRequest, ModelResponse, ToolCall, ToolSpec, Usage,\n} from \"../../model.js\";\nimport { err, ok, type Result } from \"../../../result.js\";\nimport { textOf } from \"../../../content.js\";\n\nexport interface OpenAiCompatibleOptions {\n apiKey: string;\n baseUrl: string;\n model: string;\n headers?: Readonly<Record<string, string>>;\n /**\n * How this endpoint spells reasoning effort. OpenAI takes `reasoning_effort`;\n * OpenRouter takes `reasoning: { effort }` and forwards it to whoever is\n * serving the model. Sending both is not compatibility — OpenAI rejects the\n * request outright — so the dialect is declared rather than guessed.\n */\n effortParameter?: \"reasoning_effort\" | \"reasoning\" | \"none\";\n /** Injectable for tests; defaults to global fetch. */\n fetch?: typeof fetch;\n}\n\n/**\n * The OpenAI chat-completions wire, which almost every provider now speaks —\n * OpenRouter, Together, Groq, vLLM, Ollama. One adapter, one base URL.\n */\nexport function createOpenAiCompatibleModel(options: OpenAiCompatibleOptions): Model {\n const call = options.fetch ?? fetch;\n return {\n id: `openai-compatible:${options.model}`,\n\n async *generate(request: ModelRequest): AsyncGenerator<ModelDelta, Result<ModelResponse, ModelError>> {\n let response: Response;\n try {\n response = await call(`${options.baseUrl}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n authorization: `Bearer ${options.apiKey}`,\n ...options.headers,\n },\n body: JSON.stringify({\n model: options.model,\n messages: request.messages.map(encodeMessage),\n ...(request.tools?.length ? { tools: request.tools.map(encodeTool) } : {}),\n ...(request.maxOutputTokens !== undefined ? { max_tokens: request.maxOutputTokens } : {}),\n ...(request.temperature !== undefined ? { temperature: request.temperature } : {}),\n ...encodeEffort(request.effort, options.effortParameter ?? \"reasoning_effort\"),\n stream: true,\n stream_options: { include_usage: true },\n }),\n ...(request.signal ? { signal: request.signal } : {}),\n });\n } catch (error) {\n return err(transportError(error, request.signal));\n }\n\n if (!response.ok) return err(await httpError(response));\n if (!response.body) return err({ code: \"provider\", message: \"No response body\", retryable: true });\n\n const text: string[] = [];\n const calls = new Map<number, { callId: string; name: string; arguments: string }>();\n let finish: ModelResponse[\"finishReason\"] = \"stop\";\n let usage: Usage = {};\n let model: string | undefined;\n\n // A stream that dies mid-body — cancelled, dropped, truncated — must come\n // back as a typed failure like any other. Without this the generator throws,\n // `run.result` rejects instead of resolving `cancelled`, no `run.finished`\n // is ever committed, and the activation stays open until its claim expires.\n try {\n for await (const event of sseLines(response.body)) {\n if (event === \"[DONE]\") break;\n let frame: ChatFrame;\n try { frame = JSON.parse(event) as ChatFrame; } catch { continue; }\n model ??= frame.model;\n if (frame.usage) usage = decodeUsage(frame.usage);\n\n const choice = frame.choices?.[0];\n if (!choice) continue;\n if (choice.finish_reason) finish = decodeFinish(choice.finish_reason);\n\n const content = choice.delta?.content;\n if (content) { text.push(content); yield { type: \"text.delta\", text: content }; }\n\n const reasoning = choice.delta?.reasoning;\n if (reasoning) yield { type: \"reasoning.delta\", text: reasoning };\n\n for (const fragment of choice.delta?.tool_calls ?? []) {\n // Providers stream tool arguments in fragments, keyed by position;\n // only the first fragment carries the id and the name.\n const existing = calls.get(fragment.index) ?? {\n callId: fragment.id ?? `call_${fragment.index}`,\n name: fragment.function?.name ?? \"\",\n arguments: \"\",\n };\n if (fragment.id) existing.callId = fragment.id;\n if (fragment.function?.name) existing.name = fragment.function.name;\n if (fragment.function?.arguments) {\n existing.arguments += fragment.function.arguments;\n yield { type: \"tool-call.delta\", callId: existing.callId, arguments: fragment.function.arguments };\n }\n calls.set(fragment.index, existing);\n }\n }\n } catch (error) {\n return err(transportError(error, request.signal));\n }\n\n const collected: ToolCall[] = [...calls.values()]\n .map((call) => ({ callId: call.callId, name: call.name, arguments: call.arguments || \"{}\" }));\n\n return ok({\n message: { content: text.join(\"\"), ...(collected.length ? { calls: collected } : {}) },\n finishReason: collected.length && finish === \"stop\" ? \"tool-calls\" : finish,\n usage,\n ...(model ? { model } : {}),\n });\n },\n };\n}\n\n/** OpenRouter is this wire with a fixed base URL and attribution headers. */\nexport function createOpenRouterModel(input: {\n apiKey: string; model: string; appUrl?: string; appName?: string; fetch?: typeof fetch;\n}): Model {\n return createOpenAiCompatibleModel({\n apiKey: input.apiKey,\n baseUrl: \"https://openrouter.ai/api/v1\",\n model: input.model,\n effortParameter: \"reasoning\",\n headers: {\n ...(input.appUrl ? { \"http-referer\": input.appUrl } : {}),\n ...(input.appName ? { \"x-title\": input.appName } : {}),\n },\n ...(input.fetch ? { fetch: input.fetch } : {}),\n });\n}\n\nfunction encodeEffort(\n effort: ModelRequest[\"effort\"],\n parameter: \"reasoning_effort\" | \"reasoning\" | \"none\",\n): Record<string, unknown> {\n if (!effort || parameter === \"none\") return {};\n return parameter === \"reasoning\" ? { reasoning: { effort } } : { reasoning_effort: effort };\n}\n\nfunction encodeMessage(message: Message): Record<string, unknown> {\n if (message.role === \"tool\") {\n return { role: \"tool\", tool_call_id: message.callId, content: textOf(message.content) };\n }\n if (message.role === \"assistant\") {\n return {\n role: \"assistant\",\n content: textOf(message.content) || null,\n ...(message.calls?.length\n ? {\n tool_calls: message.calls.map((call) => ({\n id: call.callId, type: \"function\",\n function: { name: call.name, arguments: call.arguments },\n })),\n }\n : {}),\n };\n }\n return { role: message.role, content: encodeContent(message.content) };\n}\n\nfunction encodeContent(content: Message[\"content\"]): unknown {\n if (typeof content === \"string\") return content;\n const parts: Record<string, unknown>[] = [];\n for (const part of content as readonly ContentPart[]) {\n if (part.type === \"text\") { parts.push({ type: \"text\", text: part.text }); continue; }\n if (part.type === \"image\") {\n const url = part.source.kind === \"url\" ? part.source.url : `data:${part.mediaType};base64,${part.source.data}`;\n parts.push({ type: \"image_url\", image_url: { url } });\n }\n // Files and opaque blocks have no place on this wire. Dropping them beats\n // stringifying them into something the model would read as prose.\n }\n const only = parts.length === 1 ? parts[0] : undefined;\n return only?.type === \"text\" ? only.text : parts;\n}\n\nconst encodeTool = (tool: ToolSpec) => ({\n type: \"function\",\n function: { name: tool.name, description: tool.description, parameters: tool.parameters, strict: false },\n});\n\nconst decodeFinish = (reason: string): ModelResponse[\"finishReason\"] =>\n reason === \"tool_calls\" ? \"tool-calls\"\n : reason === \"length\" ? \"length\"\n : reason === \"content_filter\" ? \"refusal\"\n : \"stop\";\n\nconst decodeUsage = (usage: ChatUsage): Usage => ({\n // Carried, not computed. A router picks an upstream provider per request and\n // adds its own margin, so what it charged is a thing only it can say — and\n // the frame carrying it is the one this adapter already reads.\n ...(typeof usage.cost === \"number\" ? { costUsd: usage.cost } : {}),\n // `prompt_tokens` is the whole prompt and `cached_tokens` is counted inside\n // it — \"cached tokens present in the prompt\". `Usage` keeps the three input\n // counts disjoint, so the cached part comes out here rather than every\n // caller having to know which wire produced the number it is holding.\n ...(usage.prompt_tokens !== undefined\n ? { inputTokens: Math.max(usage.prompt_tokens - (usage.prompt_tokens_details?.cached_tokens ?? 0), 0) } : {}),\n ...(usage.completion_tokens !== undefined ? { outputTokens: usage.completion_tokens } : {}),\n ...(usage.prompt_tokens_details?.cached_tokens !== undefined\n ? { cacheReadTokens: usage.prompt_tokens_details.cached_tokens } : {}),\n});\n\nasync function httpError(response: Response): Promise<ModelError> {\n const body = await response.text().catch(() => \"\");\n const code: ModelError[\"code\"] =\n response.status === 401 || response.status === 403 ? \"auth\"\n : response.status === 429 ? \"rate-limit\"\n : response.status === 400 && /context|token/i.test(body) ? \"context-length\"\n : response.status >= 500 ? \"provider\"\n : \"failed\";\n return {\n code,\n message: `${response.status} ${response.statusText}${body ? `: ${body.slice(0, 400)}` : \"\"}`,\n retryable: code === \"rate-limit\" || code === \"provider\",\n };\n}\n\nfunction transportError(error: unknown, signal?: AbortSignal): ModelError {\n if (signal?.aborted) return { code: \"cancelled\", message: \"Generation cancelled\", retryable: false };\n return { code: \"provider\", message: error instanceof Error ? error.message : String(error), retryable: true };\n}\n\n/** Server-sent events, reassembled across chunk boundaries. */\nasync function* sseLines(body: ReadableStream<Uint8Array>): AsyncGenerator<string> {\n const decoder = new TextDecoder();\n let buffer = \"\";\n for await (const chunk of body as unknown as AsyncIterable<Uint8Array>) {\n buffer += decoder.decode(chunk, { stream: true });\n let newline = buffer.indexOf(\"\\n\");\n while (newline >= 0) {\n const line = buffer.slice(0, newline).trim();\n buffer = buffer.slice(newline + 1);\n if (line.startsWith(\"data:\")) yield line.slice(5).trim();\n newline = buffer.indexOf(\"\\n\");\n }\n }\n}\n\ninterface ChatUsage {\n prompt_tokens?: number;\n completion_tokens?: number;\n prompt_tokens_details?: { cached_tokens?: number };\n /**\n * What this generation was charged. OpenRouter sets it on every response and\n * calls it credits, whose base currency is US dollars; plain OpenAI and the\n * self-hosted endpoints on this wire do not send it at all, so it is absent\n * rather than zero there.\n */\n cost?: number;\n}\ninterface ChatFrame {\n model?: string;\n usage?: ChatUsage;\n choices?: readonly {\n finish_reason?: string | null;\n delta?: {\n content?: string;\n reasoning?: string;\n tool_calls?: readonly { index: number; id?: string; function?: { name?: string; arguments?: string } }[];\n };\n }[];\n}\n"]}
|
package/dist/render.d.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* that cannot fail, so a terminal channel is *nothing but* this projection.
|
|
19
19
|
*
|
|
20
20
|
* **The answer and the stream are different things, and only one is a message.**
|
|
21
|
-
* `RunResult.output` is what `
|
|
21
|
+
* `RunResult.output` is what `beforeStop` hands to `Delivery.input`: the same
|
|
22
22
|
* `Content`, whether the recipient is a person or another session. The stream
|
|
23
23
|
* is not a message and never becomes one — a `cancelled` or `failed` result has
|
|
24
24
|
* no `output` field at all, so a run can stream a paragraph and produce nothing
|
package/dist/render.js
CHANGED
|
@@ -189,6 +189,11 @@ function createRenderer(sink) {
|
|
|
189
189
|
/** A delta: written, and recorded as the part of the coming entry already shown. */
|
|
190
190
|
const say = (text) => { streamed += text; emit(text); };
|
|
191
191
|
const entry = (stored) => {
|
|
192
|
+
if (stored.type === "hook.input") {
|
|
193
|
+
if (debugging)
|
|
194
|
+
line(dim(`◦ ${stored.hook} · ${oneLine(readable(stored.input), 72)}`));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
192
197
|
if (stored.type === "run.started") {
|
|
193
198
|
// Input with no `from` is what the caller just passed in. Echoing it says
|
|
194
199
|
// everything twice. Input *with* one came from somewhere else — a
|
|
@@ -277,6 +282,10 @@ function createRenderer(sink) {
|
|
|
277
282
|
};
|
|
278
283
|
return {
|
|
279
284
|
update(next) {
|
|
285
|
+
if (next.type === "hook.error") {
|
|
286
|
+
line(`Hook ${next.hook}: ${next.message}`);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
280
289
|
if (next.type === "text.delta") {
|
|
281
290
|
say(next.text);
|
|
282
291
|
return;
|