@cubicecho/agent-core 2.4.0 → 2.5.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/README.md +189 -1
- package/dist/agent-loop.d.ts +202 -0
- package/dist/agent-loop.js +355 -0
- package/dist/capabilities.d.ts +36 -2
- package/dist/capabilities.js +73 -19
- package/dist/client.d.ts +13 -0
- package/dist/client.js +11 -0
- package/dist/compaction.d.ts +133 -0
- package/dist/compaction.js +173 -0
- package/dist/config.d.ts +20 -0
- package/dist/events.d.ts +5 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.js +7 -3
- package/dist/retry.d.ts +9 -0
- package/dist/retry.js +9 -0
- package/dist/run-turn.d.ts +9 -3
- package/dist/run-turn.js +11 -4
- package/dist/side-task.d.ts +35 -1
- package/dist/side-task.js +101 -32
- package/dist/snapshot.d.ts +57 -0
- package/dist/snapshot.js +123 -0
- package/dist/stream.d.ts +8 -0
- package/dist/stream.js +4 -1
- package/dist/tool-calls.d.ts +67 -0
- package/dist/tool-calls.js +346 -0
- package/dist/tool-loading.d.ts +21 -1
- package/dist/tool-loading.js +23 -6
- package/llms.txt +56 -0
- package/package.json +1 -1
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type OpenAI from "openai";
|
|
2
|
+
import type { Endpoint } from "./config.ts";
|
|
3
|
+
import { type HookContext, type HookNote, type HookRunner } from "./hooks.ts";
|
|
4
|
+
import { type SideTaskOptions } from "./side-task.ts";
|
|
5
|
+
/**
|
|
6
|
+
* Keeping a long run inside its window: stale tool results cleared, and the oldest stretch folded
|
|
7
|
+
* into a summary the model writes itself.
|
|
8
|
+
*
|
|
9
|
+
* Both rewrite the transcript's prefix, and a prefix that changes is a prompt cache that misses —
|
|
10
|
+
* on a local server that is the whole prompt processed again, every token of it. So neither is
|
|
11
|
+
* meant to run a little on every turn: run them rarely, and together, at the point
|
|
12
|
+
* `planCompaction` says the window is filling, and the cache is paid for once rather than on
|
|
13
|
+
* every step.
|
|
14
|
+
*/
|
|
15
|
+
type Message = OpenAI.ChatCompletionMessageParam;
|
|
16
|
+
/** The fraction of the window in use before a summary is worth its own round trip. */
|
|
17
|
+
export declare const COMPACT_AT = 0.75;
|
|
18
|
+
/** The fraction of the window the kept tail may fill, leaving room for the run to grow again. */
|
|
19
|
+
export declare const KEEP_RATIO = 0.35;
|
|
20
|
+
/**
|
|
21
|
+
* The summariser's instruction when the caller gives none.
|
|
22
|
+
*
|
|
23
|
+
* Asks for notes rather than a retelling, because what the summary replaces is the model's only
|
|
24
|
+
* record of what was decided, and a narrative spends its words on the order things happened in.
|
|
25
|
+
*/
|
|
26
|
+
export declare const SUMMARY_PROMPT: string;
|
|
27
|
+
/**
|
|
28
|
+
* How a summary message opens, which is also how `planCompaction` knows one from a system prompt.
|
|
29
|
+
*/
|
|
30
|
+
export declare const SUMMARY_LEAD = "Summary of the earlier part of this conversation, which is no longer shown in full:\n\n";
|
|
31
|
+
/** What `pruneToolResults` takes. */
|
|
32
|
+
export interface PruneOptions {
|
|
33
|
+
/** How many of the latest tool results are left whole, 5 by default. */
|
|
34
|
+
keepLast?: number;
|
|
35
|
+
/** A result this long or shorter is left whole wherever it is, 256 characters by default. */
|
|
36
|
+
maxChars?: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The transcript with every tool result but the latest few replaced by a one-line stub.
|
|
40
|
+
*
|
|
41
|
+
* The cheap half of compaction. A `read_file` of a 40k-character file is 10k tokens on every turn
|
|
42
|
+
* after it, and by then the model has usually taken what it wanted from it; the stub keeps the
|
|
43
|
+
* call answered — a call with no result is a malformed transcript — and says how much was there,
|
|
44
|
+
* so a model that does need it again knows to ask. Short results are kept, since a stub saves
|
|
45
|
+
* nothing on them. Returns the same array when there was nothing to clear. Rewrites the prefix;
|
|
46
|
+
* see the module comment on when to run it.
|
|
47
|
+
*
|
|
48
|
+
* @param messages The transcript. Not written to.
|
|
49
|
+
* @param options How many results to keep and how long one must be to clear.
|
|
50
|
+
*/
|
|
51
|
+
export declare function pruneToolResults(messages: Message[], { keepLast, maxChars }?: PruneOptions): Message[];
|
|
52
|
+
/** What `planCompaction` takes. */
|
|
53
|
+
export interface CompactionOptions {
|
|
54
|
+
/** The model's window, in tokens. Zero or less never compacts. */
|
|
55
|
+
limit: number;
|
|
56
|
+
/**
|
|
57
|
+
* What the transcript costs now. The last turn's reported prompt tokens are the best number;
|
|
58
|
+
* absent is the estimate of the whole transcript.
|
|
59
|
+
*/
|
|
60
|
+
used?: number;
|
|
61
|
+
/** The fraction of `limit` in use before compacting. `COMPACT_AT` by default. */
|
|
62
|
+
compactAt?: number;
|
|
63
|
+
/** The fraction of `limit` the kept tail may fill. `KEEP_RATIO` by default. */
|
|
64
|
+
keepRatio?: number;
|
|
65
|
+
/** One message's tokens. `messageTokens` by default. */
|
|
66
|
+
estimate?: (message: Message) => number;
|
|
67
|
+
}
|
|
68
|
+
/** Where to cut, as `compactTranscript` takes it. */
|
|
69
|
+
export interface CompactionPlan {
|
|
70
|
+
/** The first message folded away. Everything before it is a system prompt and stays. */
|
|
71
|
+
from: number;
|
|
72
|
+
/** The first message kept whole, always a user message. */
|
|
73
|
+
cut: number;
|
|
74
|
+
/** The messages from `from` to `cut`, the ones the summary replaces. */
|
|
75
|
+
toSummarise: Message[];
|
|
76
|
+
/** The summary an earlier compaction left, which this one continues. */
|
|
77
|
+
previous?: string;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Where to fold a transcript that has grown into its window, or `undefined` when it should not be.
|
|
81
|
+
*
|
|
82
|
+
* The kept tail is walked back from the end until it fills `keepRatio` of the window, then moved
|
|
83
|
+
* forward onto a user message: a transcript resuming mid-exchange — a tool result with no call
|
|
84
|
+
* before it, a reply with no question — is malformed and servers refuse it. The system prompts at
|
|
85
|
+
* the head are never folded, and a summary an earlier compaction left there is continued rather
|
|
86
|
+
* than summarised as if it were conversation. No plan comes back when the window is not full
|
|
87
|
+
* enough, or when the only legal cut folds too little to pay for the summary.
|
|
88
|
+
*
|
|
89
|
+
* @param messages The transcript, system prompts included if the caller keeps them in it.
|
|
90
|
+
* @param options The window, what is in use, and the ratios. See `CompactionOptions`.
|
|
91
|
+
*/
|
|
92
|
+
export declare function planCompaction(messages: Message[], { limit, used, compactAt, keepRatio, estimate, }: CompactionOptions): CompactionPlan | undefined;
|
|
93
|
+
/**
|
|
94
|
+
* What the summariser is handed for a plan: the earlier summary if there was one, then each
|
|
95
|
+
* message as its role and at most 4000 characters of its text.
|
|
96
|
+
*
|
|
97
|
+
* @param plan What `planCompaction` returned.
|
|
98
|
+
*/
|
|
99
|
+
export declare function summaryInput(plan: CompactionPlan): string;
|
|
100
|
+
/**
|
|
101
|
+
* A summariser that asks `model` with `SUMMARY_PROMPT`, for `compactTranscript`.
|
|
102
|
+
*
|
|
103
|
+
* @param config The endpoint the summary is written through.
|
|
104
|
+
* @param model The model to write it, which may be a smaller one than the run's.
|
|
105
|
+
* @param options Cancellation and notices; the ceiling is 1024 and the instruction
|
|
106
|
+
* `SUMMARY_PROMPT` unless given.
|
|
107
|
+
*/
|
|
108
|
+
export declare const summariser: (config: Endpoint, model: string, { system, maxTokens, ...options }?: SideTaskOptions & {
|
|
109
|
+
system?: string;
|
|
110
|
+
}) => (text: string) => Promise<string>;
|
|
111
|
+
/**
|
|
112
|
+
* The transcript with the plan's stretch replaced by one system message holding its summary.
|
|
113
|
+
*
|
|
114
|
+
* `beforeCompact` is told what is being folded while the summary is written, beside it rather
|
|
115
|
+
* than ahead of it — a memory server filing it is not a rescue worth making the run wait for, and
|
|
116
|
+
* `notify` never rejects. A hook cannot veto the compaction: `HookOutcome` has no way to say so,
|
|
117
|
+
* and a run over its window has no better option anyway. An empty summary folds nothing, and the
|
|
118
|
+
* transcript comes back as it was. Rewrites the prefix; see the module comment on when to run it.
|
|
119
|
+
*
|
|
120
|
+
* @param messages The transcript the plan was made for. Not written to.
|
|
121
|
+
* @param plan What `planCompaction` returned for it.
|
|
122
|
+
* @param summarise Writes the summary from `summaryInput`'s text. See `summariser`.
|
|
123
|
+
* @param options Hooks to tell. `context` is extended with `compacting` and `range`, whose
|
|
124
|
+
* indexes are the plan's.
|
|
125
|
+
*/
|
|
126
|
+
export declare function compactTranscript(messages: Message[], plan: CompactionPlan, summarise: (text: string) => Promise<string>, { hooks, }?: {
|
|
127
|
+
hooks?: {
|
|
128
|
+
run: HookRunner;
|
|
129
|
+
context: HookContext;
|
|
130
|
+
onNote?: (note: HookNote) => void;
|
|
131
|
+
};
|
|
132
|
+
}): Promise<Message[]>;
|
|
133
|
+
export {};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { notify, turnMessages } from "./hooks.js";
|
|
2
|
+
import { messageTokens } from "./retry.js";
|
|
3
|
+
import { ask } from "./side-task.js";
|
|
4
|
+
/** The fraction of the window in use before a summary is worth its own round trip. */
|
|
5
|
+
export const COMPACT_AT = 0.75;
|
|
6
|
+
/** The fraction of the window the kept tail may fill, leaving room for the run to grow again. */
|
|
7
|
+
export const KEEP_RATIO = 0.35;
|
|
8
|
+
/** How much of any one message the summariser is shown. A pasted file is not worth it whole. */
|
|
9
|
+
const SUMMARY_SLICE = 4000;
|
|
10
|
+
/**
|
|
11
|
+
* The summariser's instruction when the caller gives none.
|
|
12
|
+
*
|
|
13
|
+
* Asks for notes rather than a retelling, because what the summary replaces is the model's only
|
|
14
|
+
* record of what was decided, and a narrative spends its words on the order things happened in.
|
|
15
|
+
*/
|
|
16
|
+
export const SUMMARY_PROMPT = "You maintain the running memory of a long conversation. Rewrite the exchange below as " +
|
|
17
|
+
"notes the assistant can rely on after the original messages are gone. Keep decisions, " +
|
|
18
|
+
"facts, file paths, names, numbers, and anything still unresolved. Drop pleasantries and " +
|
|
19
|
+
"anything already superseded. Write compact prose or bullets — no preamble, no sign-off.";
|
|
20
|
+
/**
|
|
21
|
+
* How a summary message opens, which is also how `planCompaction` knows one from a system prompt.
|
|
22
|
+
*/
|
|
23
|
+
export const SUMMARY_LEAD = "Summary of the earlier part of this conversation, which is no longer shown in full:\n\n";
|
|
24
|
+
/** A message's content as plain text: parts joined, anything but text left out. */
|
|
25
|
+
const textOf = (content) => typeof content === "string"
|
|
26
|
+
? content
|
|
27
|
+
: Array.isArray(content)
|
|
28
|
+
? content.map((part) => ("text" in part ? part.text : "")).join(" ")
|
|
29
|
+
: "";
|
|
30
|
+
/** What the summariser reads for one message: its text and the calls it made. */
|
|
31
|
+
const messageText = (message) => {
|
|
32
|
+
const calls = "tool_calls" in message && message.tool_calls
|
|
33
|
+
? message.tool_calls
|
|
34
|
+
.map((call) => call.type === "function" ? `${call.function.name}(${call.function.arguments})` : "")
|
|
35
|
+
.join(" ")
|
|
36
|
+
: "";
|
|
37
|
+
return `${textOf(message.content)} ${calls}`.trim();
|
|
38
|
+
};
|
|
39
|
+
const isSummary = (message) => message.role === "system" && textOf(message.content).startsWith(SUMMARY_LEAD);
|
|
40
|
+
/**
|
|
41
|
+
* The transcript with every tool result but the latest few replaced by a one-line stub.
|
|
42
|
+
*
|
|
43
|
+
* The cheap half of compaction. A `read_file` of a 40k-character file is 10k tokens on every turn
|
|
44
|
+
* after it, and by then the model has usually taken what it wanted from it; the stub keeps the
|
|
45
|
+
* call answered — a call with no result is a malformed transcript — and says how much was there,
|
|
46
|
+
* so a model that does need it again knows to ask. Short results are kept, since a stub saves
|
|
47
|
+
* nothing on them. Returns the same array when there was nothing to clear. Rewrites the prefix;
|
|
48
|
+
* see the module comment on when to run it.
|
|
49
|
+
*
|
|
50
|
+
* @param messages The transcript. Not written to.
|
|
51
|
+
* @param options How many results to keep and how long one must be to clear.
|
|
52
|
+
*/
|
|
53
|
+
export function pruneToolResults(messages, { keepLast = 5, maxChars = 256 } = {}) {
|
|
54
|
+
let kept = 0;
|
|
55
|
+
let out;
|
|
56
|
+
for (let at = messages.length - 1; at >= 0; at--) {
|
|
57
|
+
const message = messages[at];
|
|
58
|
+
if (message.role !== "tool")
|
|
59
|
+
continue;
|
|
60
|
+
if (kept++ < keepLast)
|
|
61
|
+
continue;
|
|
62
|
+
const text = textOf(message.content);
|
|
63
|
+
if (text.length <= maxChars || text.startsWith("[result cleared"))
|
|
64
|
+
continue;
|
|
65
|
+
out ??= [...messages];
|
|
66
|
+
out[at] = {
|
|
67
|
+
...message,
|
|
68
|
+
content: `[result cleared, ${text.length.toLocaleString("en-US")} chars]`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return out ?? messages;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Where to fold a transcript that has grown into its window, or `undefined` when it should not be.
|
|
75
|
+
*
|
|
76
|
+
* The kept tail is walked back from the end until it fills `keepRatio` of the window, then moved
|
|
77
|
+
* forward onto a user message: a transcript resuming mid-exchange — a tool result with no call
|
|
78
|
+
* before it, a reply with no question — is malformed and servers refuse it. The system prompts at
|
|
79
|
+
* the head are never folded, and a summary an earlier compaction left there is continued rather
|
|
80
|
+
* than summarised as if it were conversation. No plan comes back when the window is not full
|
|
81
|
+
* enough, or when the only legal cut folds too little to pay for the summary.
|
|
82
|
+
*
|
|
83
|
+
* @param messages The transcript, system prompts included if the caller keeps them in it.
|
|
84
|
+
* @param options The window, what is in use, and the ratios. See `CompactionOptions`.
|
|
85
|
+
*/
|
|
86
|
+
export function planCompaction(messages, { limit, used, compactAt = COMPACT_AT, keepRatio = KEEP_RATIO, estimate = messageTokens, }) {
|
|
87
|
+
if (!(limit > 0))
|
|
88
|
+
return undefined;
|
|
89
|
+
const cost = used ?? messages.reduce((total, message) => total + estimate(message), 0);
|
|
90
|
+
if (cost < limit * compactAt)
|
|
91
|
+
return undefined;
|
|
92
|
+
let from = 0;
|
|
93
|
+
let previous;
|
|
94
|
+
while (from < messages.length && messages[from].role === "system") {
|
|
95
|
+
if (isSummary(messages[from]))
|
|
96
|
+
previous = textOf(messages[from].content).slice(SUMMARY_LEAD.length);
|
|
97
|
+
from++;
|
|
98
|
+
}
|
|
99
|
+
const budget = limit * keepRatio;
|
|
100
|
+
let kept = 0;
|
|
101
|
+
let cut = messages.length;
|
|
102
|
+
for (let at = messages.length - 1; at > from; at--) {
|
|
103
|
+
kept += estimate(messages[at]);
|
|
104
|
+
if (kept > budget)
|
|
105
|
+
break;
|
|
106
|
+
cut = at;
|
|
107
|
+
}
|
|
108
|
+
while (cut < messages.length && messages[cut].role !== "user")
|
|
109
|
+
cut++;
|
|
110
|
+
if (cut >= messages.length || cut - from < 2)
|
|
111
|
+
return undefined;
|
|
112
|
+
return { from, cut, toSummarise: messages.slice(from, cut), ...(previous ? { previous } : {}) };
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* What the summariser is handed for a plan: the earlier summary if there was one, then each
|
|
116
|
+
* message as its role and at most 4000 characters of its text.
|
|
117
|
+
*
|
|
118
|
+
* @param plan What `planCompaction` returned.
|
|
119
|
+
*/
|
|
120
|
+
export function summaryInput(plan) {
|
|
121
|
+
const transcript = plan.toSummarise
|
|
122
|
+
.map((message) => {
|
|
123
|
+
const text = messageText(message);
|
|
124
|
+
return text ? `${message.role}: ${text.slice(0, SUMMARY_SLICE)}` : "";
|
|
125
|
+
})
|
|
126
|
+
.filter(Boolean)
|
|
127
|
+
.join("\n\n");
|
|
128
|
+
return plan.previous
|
|
129
|
+
? `Notes so far:\n${plan.previous}\n\nContinue them with this exchange:\n\n${transcript}`
|
|
130
|
+
: transcript;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* A summariser that asks `model` with `SUMMARY_PROMPT`, for `compactTranscript`.
|
|
134
|
+
*
|
|
135
|
+
* @param config The endpoint the summary is written through.
|
|
136
|
+
* @param model The model to write it, which may be a smaller one than the run's.
|
|
137
|
+
* @param options Cancellation and notices; the ceiling is 1024 and the instruction
|
|
138
|
+
* `SUMMARY_PROMPT` unless given.
|
|
139
|
+
*/
|
|
140
|
+
export const summariser = (config, model, { system = SUMMARY_PROMPT, maxTokens = 1024, ...options } = {}) => (text) => ask(config, model, system, text, { maxTokens, ...options });
|
|
141
|
+
/**
|
|
142
|
+
* The transcript with the plan's stretch replaced by one system message holding its summary.
|
|
143
|
+
*
|
|
144
|
+
* `beforeCompact` is told what is being folded while the summary is written, beside it rather
|
|
145
|
+
* than ahead of it — a memory server filing it is not a rescue worth making the run wait for, and
|
|
146
|
+
* `notify` never rejects. A hook cannot veto the compaction: `HookOutcome` has no way to say so,
|
|
147
|
+
* and a run over its window has no better option anyway. An empty summary folds nothing, and the
|
|
148
|
+
* transcript comes back as it was. Rewrites the prefix; see the module comment on when to run it.
|
|
149
|
+
*
|
|
150
|
+
* @param messages The transcript the plan was made for. Not written to.
|
|
151
|
+
* @param plan What `planCompaction` returned for it.
|
|
152
|
+
* @param summarise Writes the summary from `summaryInput`'s text. See `summariser`.
|
|
153
|
+
* @param options Hooks to tell. `context` is extended with `compacting` and `range`, whose
|
|
154
|
+
* indexes are the plan's.
|
|
155
|
+
*/
|
|
156
|
+
export async function compactTranscript(messages, plan, summarise, { hooks, } = {}) {
|
|
157
|
+
const [summary] = await Promise.all([
|
|
158
|
+
summarise(summaryInput(plan)),
|
|
159
|
+
hooks &&
|
|
160
|
+
notify(hooks.run, "beforeCompact", {
|
|
161
|
+
...hooks.context,
|
|
162
|
+
compacting: turnMessages(hooks.context.session.id, messages, plan.from, plan.cut),
|
|
163
|
+
range: { from: plan.from, through: plan.cut },
|
|
164
|
+
}, hooks.onNote),
|
|
165
|
+
]);
|
|
166
|
+
if (!summary.trim())
|
|
167
|
+
return messages;
|
|
168
|
+
return [
|
|
169
|
+
...messages.slice(0, plan.from).filter((message) => !isSummary(message)),
|
|
170
|
+
{ role: "system", content: `${SUMMARY_LEAD}${summary.trim()}` },
|
|
171
|
+
...messages.slice(plan.cut),
|
|
172
|
+
];
|
|
173
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -30,8 +30,28 @@ export interface Endpoint {
|
|
|
30
30
|
/** What to ask the model for. */
|
|
31
31
|
export interface ModelParams {
|
|
32
32
|
model: string;
|
|
33
|
+
/** The reply's ceiling. Zero or less sends none, leaving it to the server. */
|
|
33
34
|
maxTokens: number;
|
|
34
35
|
temperature: number;
|
|
36
|
+
/**
|
|
37
|
+
* `reasoning_effort` for a model that deliberates. Absent or `"off"` sends none, which is the
|
|
38
|
+
* only value a server that has never heard of reasoning accepts — a setting that means "leave
|
|
39
|
+
* the field out" rather than a level to ask for.
|
|
40
|
+
*/
|
|
41
|
+
reasoningEffort?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Request fields this interface cannot spell, merged into the body last by `buildBody`.
|
|
44
|
+
*
|
|
45
|
+
* What a model card asks for and nothing here names: `top_k`, `min_p`, `repeat_penalty` —
|
|
46
|
+
* what actually stops a small model looping — and a server's own fields, such as llama.cpp's
|
|
47
|
+
* `id_slot`, which pins a session to one slot of a `--parallel` server so its KV cache is
|
|
48
|
+
* still warm on the next turn. A local server ignores a field it does not know; OpenAI refuses
|
|
49
|
+
* one by name, and `negotiate` drops that name for the model and sends the request again.
|
|
50
|
+
* `model`, `messages`, `stream` and `tools` are the loop's and are not overridden from here.
|
|
51
|
+
* Ollama's `options` object is not read on its `/v1` route, so sampling there has to go in as
|
|
52
|
+
* top-level fields like any other.
|
|
53
|
+
*/
|
|
54
|
+
extraBody?: Record<string, unknown>;
|
|
35
55
|
}
|
|
36
56
|
/** How tools reach the model, and how long it may keep calling them. */
|
|
37
57
|
export interface ToolPolicy {
|
package/dist/events.d.ts
CHANGED
|
@@ -94,6 +94,11 @@ export interface RunUsage {
|
|
|
94
94
|
promptTokens: number;
|
|
95
95
|
completionTokens: number;
|
|
96
96
|
totalTokens: number;
|
|
97
|
+
/**
|
|
98
|
+
* How much of `promptTokens` the endpoint served from its prompt cache. Optional so a caller
|
|
99
|
+
* emitting usage before this field existed still compiles; absent reads the same as zero.
|
|
100
|
+
*/
|
|
101
|
+
cachedTokens?: number;
|
|
97
102
|
}
|
|
98
103
|
/**
|
|
99
104
|
* One thing that happened in a run, as a watcher receives it.
|
package/dist/index.d.ts
CHANGED
|
@@ -9,18 +9,22 @@
|
|
|
9
9
|
* prompts, and whatever the run is about — because that is the caller's, and it is the part
|
|
10
10
|
* that differs between one server and the next.
|
|
11
11
|
*/
|
|
12
|
+
export { type AgentLoopHooks, type AgentLoopOptions, type AgentLoopResult, buildBody, preselect, preview, resolveApiKey, runAgentLoop, type ToolCallOutcome, type ToolCallRequest, } from "./agent-loop.ts";
|
|
12
13
|
export { type Capabilities, capabilitiesFor, type ModelCapabilities, modelCapabilitiesFor, type NegotiateOptions, negotiate, resetCapabilities, } from "./capabilities.ts";
|
|
13
14
|
export type { CatalogServer } from "./catalog.ts";
|
|
14
15
|
export { contextLimitFor, getClient, listModels, type ModelInfo, NO_KEY, resetClients, timeoutMs, } from "./client.ts";
|
|
16
|
+
export { COMPACT_AT, type CompactionOptions, type CompactionPlan, compactTranscript, KEEP_RATIO, type PruneOptions, planCompaction, pruneToolResults, SUMMARY_LEAD, SUMMARY_PROMPT, summariser, summaryInput, } from "./compaction.ts";
|
|
15
17
|
export type { AgentConfig, Endpoint, ModelParams, RetryPolicy, ToolPolicy, } from "./config.ts";
|
|
16
18
|
export { errorMessage } from "./errors.ts";
|
|
17
19
|
export { configureEvents, type EventBusOptions, emit, endRun, fold, history, type RunEvent, type RunEventInput, type RunEventKind, type RunUsage, resetEvents, watch, } from "./events.ts";
|
|
18
20
|
export { assembleContext, configureHooks, type Gathered, gather, HOOK_CONTEXT_TOKENS, HOOK_EVENTS, HOOK_PREFACE, type HookContext, type HookEvent, type HookMessage, type HookNote, type HookOptions, type HookOutcome, type HookRunner, INJECT_EVENTS, notify, resetHooks, turnIndex, turnMessages, withContext, } from "./hooks.ts";
|
|
19
21
|
export { resetAll } from "./reset.ts";
|
|
20
|
-
export { backoffMs, ContextOverflow, compact, EndpointSilent, isOverflow, isTransient, requestTokens, SMALLEST_LIKELY_WINDOW, sleep, } from "./retry.ts";
|
|
22
|
+
export { backoffMs, ContextOverflow, compact, EndpointSilent, isOverflow, isTransient, messageTokens, requestTokens, SMALLEST_LIKELY_WINDOW, sleep, } from "./retry.ts";
|
|
21
23
|
export { type RunTurnOptions, runTurn } from "./run-turn.ts";
|
|
22
24
|
export { isGrammarError, relaxTools, sanitizeTools } from "./schema-compat.ts";
|
|
23
|
-
export { ask, clean, listLines, parseJson, resetHints, type SideTaskOptions, tryAsk, } from "./side-task.ts";
|
|
25
|
+
export { type AskJsonOptions, ask, askJson, clean, listLines, parseJson, resetHints, type SideTaskOptions, tryAsk, } from "./side-task.ts";
|
|
26
|
+
export { CAPABILITY_SNAPSHOT_VERSION, type CapabilitySnapshot, type EndpointSnapshot, exportCapabilities, importCapabilities, type ModelSnapshot, } from "./snapshot.ts";
|
|
24
27
|
export { type Produced, type StreamTurnOptions, streamTurn, type Turn, type TurnUsage, } from "./stream.ts";
|
|
25
28
|
export { estimateTokens } from "./tokens.ts";
|
|
26
|
-
export {
|
|
29
|
+
export { parseToolArguments, recoverToolCalls, ToolArgumentsError, type ToolCall, } from "./tool-calls.ts";
|
|
30
|
+
export { carryOver, catalogList, catalogPrompt, expandNames, inCatalog, LOAD_TOOLS, LOAD_TOOLS_DEFINITION, loadResult, MAX_CARRIED, MAX_PER_LOAD, PRESELECT_SCHEMA, PRESELECT_SYSTEM, preselectInput, preselection, preselectSystem, requestedNames, } from "./tool-loading.ts";
|
package/dist/index.js
CHANGED
|
@@ -9,16 +9,20 @@
|
|
|
9
9
|
* prompts, and whatever the run is about — because that is the caller's, and it is the part
|
|
10
10
|
* that differs between one server and the next.
|
|
11
11
|
*/
|
|
12
|
+
export { buildBody, preselect, preview, resolveApiKey, runAgentLoop, } from "./agent-loop.js";
|
|
12
13
|
export { capabilitiesFor, modelCapabilitiesFor, negotiate, resetCapabilities, } from "./capabilities.js";
|
|
13
14
|
export { contextLimitFor, getClient, listModels, NO_KEY, resetClients, timeoutMs, } from "./client.js";
|
|
15
|
+
export { COMPACT_AT, compactTranscript, KEEP_RATIO, planCompaction, pruneToolResults, SUMMARY_LEAD, SUMMARY_PROMPT, summariser, summaryInput, } from "./compaction.js";
|
|
14
16
|
export { errorMessage } from "./errors.js";
|
|
15
17
|
export { configureEvents, emit, endRun, fold, history, resetEvents, watch, } from "./events.js";
|
|
16
18
|
export { assembleContext, configureHooks, gather, HOOK_CONTEXT_TOKENS, HOOK_EVENTS, HOOK_PREFACE, INJECT_EVENTS, notify, resetHooks, turnIndex, turnMessages, withContext, } from "./hooks.js";
|
|
17
19
|
export { resetAll } from "./reset.js";
|
|
18
|
-
export { backoffMs, ContextOverflow, compact, EndpointSilent, isOverflow, isTransient, requestTokens, SMALLEST_LIKELY_WINDOW, sleep, } from "./retry.js";
|
|
20
|
+
export { backoffMs, ContextOverflow, compact, EndpointSilent, isOverflow, isTransient, messageTokens, requestTokens, SMALLEST_LIKELY_WINDOW, sleep, } from "./retry.js";
|
|
19
21
|
export { runTurn } from "./run-turn.js";
|
|
20
22
|
export { isGrammarError, relaxTools, sanitizeTools } from "./schema-compat.js";
|
|
21
|
-
export { ask, clean, listLines, parseJson, resetHints, tryAsk, } from "./side-task.js";
|
|
23
|
+
export { ask, askJson, clean, listLines, parseJson, resetHints, tryAsk, } from "./side-task.js";
|
|
24
|
+
export { CAPABILITY_SNAPSHOT_VERSION, exportCapabilities, importCapabilities, } from "./snapshot.js";
|
|
22
25
|
export { streamTurn, } from "./stream.js";
|
|
23
26
|
export { estimateTokens } from "./tokens.js";
|
|
24
|
-
export {
|
|
27
|
+
export { parseToolArguments, recoverToolCalls, ToolArgumentsError, } from "./tool-calls.js";
|
|
28
|
+
export { carryOver, catalogList, catalogPrompt, expandNames, inCatalog, LOAD_TOOLS, LOAD_TOOLS_DEFINITION, loadResult, MAX_CARRIED, MAX_PER_LOAD, PRESELECT_SCHEMA, PRESELECT_SYSTEM, preselectInput, preselection, preselectSystem, requestedNames, } from "./tool-loading.js";
|
package/dist/retry.d.ts
CHANGED
|
@@ -47,6 +47,15 @@ export declare const compact: (tokens: number) => string;
|
|
|
47
47
|
* @param body The request as it will be sent, tools included.
|
|
48
48
|
*/
|
|
49
49
|
export declare const requestTokens: (body: OpenAI.ChatCompletionCreateParamsStreaming) => number;
|
|
50
|
+
/**
|
|
51
|
+
* One message's estimated tokens, by the same count `requestTokens` sums for a whole request.
|
|
52
|
+
*
|
|
53
|
+
* For the arithmetic that weighs part of a transcript against a window — `planCompaction`'s kept
|
|
54
|
+
* tail — where `estimateTokens` on the text alone would leave out the calls and the envelope.
|
|
55
|
+
*
|
|
56
|
+
* @param message The message as it will be sent.
|
|
57
|
+
*/
|
|
58
|
+
export declare const messageTokens: (message: OpenAI.ChatCompletionMessageParam) => number;
|
|
50
59
|
/**
|
|
51
60
|
* Whether a refusal means the request was too big, rather than merely refused.
|
|
52
61
|
*
|
package/dist/retry.js
CHANGED
|
@@ -136,6 +136,15 @@ export const requestTokens = (body) => {
|
|
|
136
136
|
chars += messageChars(message);
|
|
137
137
|
return Math.ceil(chars / CHARS_PER_TOKEN) + (body.tools?.length ? toolsCost(body.tools) : 0);
|
|
138
138
|
};
|
|
139
|
+
/**
|
|
140
|
+
* One message's estimated tokens, by the same count `requestTokens` sums for a whole request.
|
|
141
|
+
*
|
|
142
|
+
* For the arithmetic that weighs part of a transcript against a window — `planCompaction`'s kept
|
|
143
|
+
* tail — where `estimateTokens` on the text alone would leave out the calls and the envelope.
|
|
144
|
+
*
|
|
145
|
+
* @param message The message as it will be sent.
|
|
146
|
+
*/
|
|
147
|
+
export const messageTokens = (message) => Math.ceil(messageChars(message) / CHARS_PER_TOKEN);
|
|
139
148
|
/**
|
|
140
149
|
* Servers refuse an over-long request in their own words; these are the ones worth reading as
|
|
141
150
|
* that rather than as a broken request. Matched loosely — every one of them is some
|
package/dist/run-turn.d.ts
CHANGED
|
@@ -39,8 +39,9 @@ export interface RunTurnOptions extends Omit<StreamTurnOptions, "produced"> {
|
|
|
39
39
|
/**
|
|
40
40
|
* What the model will read, in tokens. Zero — the default — sends whatever it is given.
|
|
41
41
|
*
|
|
42
|
-
* With a limit, the request is sized before it is sent
|
|
43
|
-
*
|
|
42
|
+
* With a limit, the request is sized before it is sent — the prompt plus the reply ceiling
|
|
43
|
+
* the body carries, since that is what the endpoint weighs — and a `ContextOverflow` is raised
|
|
44
|
+
* here rather than by the endpoint one round trip later. It is opt-in because the number is the
|
|
44
45
|
* caller's to find: `contextLimitFor` asks the endpoint, an operator's own setting overrides
|
|
45
46
|
* it, and neither is something a turn should be doing network I/O to discover. A limit below
|
|
46
47
|
* `SMALLEST_LIKELY_WINDOW` is not believed — a model with a window that small is rare enough
|
|
@@ -57,6 +58,11 @@ export interface RunTurnOptions extends Omit<StreamTurnOptions, "produced"> {
|
|
|
57
58
|
* `request` has to know what this model refused before it can build one that avoids it.
|
|
58
59
|
*/
|
|
59
60
|
model?: string;
|
|
61
|
+
/**
|
|
62
|
+
* The body fields a refusal may take away when the endpoint has never heard of them — the keys
|
|
63
|
+
* of `extraBody`, ordinarily. See `NegotiateOptions.droppable`; it needs `model` too.
|
|
64
|
+
*/
|
|
65
|
+
droppable?: Iterable<string>;
|
|
60
66
|
}
|
|
61
67
|
/**
|
|
62
68
|
* `request` is a callback rather than a body because the body has to be rebuilt from whatever
|
|
@@ -71,4 +77,4 @@ export interface RunTurnOptions extends Omit<StreamTurnOptions, "produced"> {
|
|
|
71
77
|
* @param options Retry budget, context limit, the model to negotiate for, notices, and the
|
|
72
78
|
* stream's own callbacks.
|
|
73
79
|
*/
|
|
74
|
-
export declare function runTurn(client: OpenAI, supports: Capabilities, request: (supports: Capabilities, model: ModelCapabilities | undefined) => OpenAI.ChatCompletionCreateParamsStreaming, { maxRetries, onNotice, contextLimit, model, ...stream }?: RunTurnOptions): Promise<Turn>;
|
|
80
|
+
export declare function runTurn(client: OpenAI, supports: Capabilities, request: (supports: Capabilities, model: ModelCapabilities | undefined) => OpenAI.ChatCompletionCreateParamsStreaming, { maxRetries, onNotice, contextLimit, model, droppable, ...stream }?: RunTurnOptions): Promise<Turn>;
|
package/dist/run-turn.js
CHANGED
|
@@ -15,7 +15,7 @@ import { streamTurn } from "./stream.js";
|
|
|
15
15
|
* @param options Retry budget, context limit, the model to negotiate for, notices, and the
|
|
16
16
|
* stream's own callbacks.
|
|
17
17
|
*/
|
|
18
|
-
export async function runTurn(client, supports, request, { maxRetries = 0, onNotice, contextLimit = 0, model, ...stream } = {}) {
|
|
18
|
+
export async function runTurn(client, supports, request, { maxRetries = 0, onNotice, contextLimit = 0, model, droppable, ...stream } = {}) {
|
|
19
19
|
// Sized once rather than per build. `request` is called again for every downgrade and every
|
|
20
20
|
// retry, but a downgraded body is strictly smaller than the one before it and the transcript
|
|
21
21
|
// does not change between attempts — so the first body is the one worth measuring, and
|
|
@@ -26,11 +26,18 @@ export async function runTurn(client, supports, request, { maxRetries = 0, onNot
|
|
|
26
26
|
if (!sized && contextLimit >= SMALLEST_LIKELY_WINDOW) {
|
|
27
27
|
sized = true;
|
|
28
28
|
const needed = requestTokens(body);
|
|
29
|
+
// The endpoint refuses on the prompt plus the reply — llama.cpp sizes the slot with
|
|
30
|
+
// `n_predict` in, OpenAI with the ceiling — so a prompt that fits the window but not the
|
|
31
|
+
// window less the ceiling was let through here to be refused one round trip later, which
|
|
32
|
+
// is the trip this guard exists to save. Read off the body under whichever spelling was
|
|
33
|
+
// chosen. No ceiling reserves nothing: the server then gives the reply what is left.
|
|
34
|
+
const reserve = Math.max(0, body.max_completion_tokens ?? body.max_tokens ?? 0);
|
|
29
35
|
// Not retried, and deliberately not a capability: `isTransient` refuses it and none of the
|
|
30
36
|
// words below are ones `negotiate` reads as a refusal it can answer, so this leaves both
|
|
31
37
|
// loops on the first attempt instead of being sent again to be refused again.
|
|
32
|
-
if (needed > contextLimit) {
|
|
33
|
-
|
|
38
|
+
if (needed + reserve > contextLimit) {
|
|
39
|
+
const reserved = reserve ? ` plus ${compact(reserve)} reserved for the reply` : "";
|
|
40
|
+
throw new ContextOverflow(`the request is about ${compact(needed)} tokens${reserved}, over this model's ${compact(contextLimit)}`);
|
|
34
41
|
}
|
|
35
42
|
}
|
|
36
43
|
return body;
|
|
@@ -38,7 +45,7 @@ export async function runTurn(client, supports, request, { maxRetries = 0, onNot
|
|
|
38
45
|
for (let attempt = 0;; attempt++) {
|
|
39
46
|
const produced = { any: false };
|
|
40
47
|
try {
|
|
41
|
-
return await negotiate(supports, (capabilities, box, forModel) => streamTurn(client, measured(capabilities, forModel), { ...stream, produced: box }), { produced, onNotice, model });
|
|
48
|
+
return await negotiate(supports, (capabilities, box, forModel) => streamTurn(client, measured(capabilities, forModel), { ...stream, produced: box }), { produced, onNotice, model, droppable });
|
|
42
49
|
}
|
|
43
50
|
catch (error) {
|
|
44
51
|
// The abort is read before the classification, not after. A run stopped by its operator
|
package/dist/side-task.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { Endpoint } from "./config.ts";
|
|
2
|
+
/** An (endpoint, model) pair as `noHints` holds it: `[endpointId, model]`, stringified. */
|
|
3
|
+
export declare const hintKey: (endpoint: string, model: string) => string;
|
|
4
|
+
/** The pairs that refused the hints, the live set, for `exportCapabilities` and `importCapabilities`. */
|
|
5
|
+
export declare const refusedHints: () => Set<string>;
|
|
2
6
|
/** Test seam, alongside `resetClients` and `resetAll`: forget which models refused the hints. */
|
|
3
7
|
export declare const resetHints: () => void;
|
|
4
8
|
/** What a side task may be given. All optional — one given none of them still runs. */
|
|
@@ -32,7 +36,37 @@ export interface SideTaskOptions {
|
|
|
32
36
|
* @param user The input it applies to.
|
|
33
37
|
* @param options Reply ceiling, temperature, cancellation, notices.
|
|
34
38
|
*/
|
|
35
|
-
export declare function ask(config: Endpoint, model: string, system: string, user: string,
|
|
39
|
+
export declare function ask(config: Endpoint, model: string, system: string, user: string, options?: SideTaskOptions): Promise<string>;
|
|
40
|
+
/** What `askJson` takes besides a side task's options. */
|
|
41
|
+
export interface AskJsonOptions extends SideTaskOptions {
|
|
42
|
+
/** What the schema is called in the request, `answer` by default. Letters, digits, `_` and `-`. */
|
|
43
|
+
name?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Asks the server to hold the reply to the schema exactly, true by default. OpenAI's strict mode
|
|
46
|
+
* wants every property required and `additionalProperties: false`; a schema written otherwise
|
|
47
|
+
* wants this off there.
|
|
48
|
+
*/
|
|
49
|
+
strict?: boolean;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* A side task whose answer is JSON matching a schema, parsed. Undefined when no JSON came back.
|
|
53
|
+
* Throws like any request.
|
|
54
|
+
*
|
|
55
|
+
* Sends `response_format` with the schema, which a llama.cpp server compiles into a grammar and
|
|
56
|
+
* vLLM, LM Studio, Ollama and OpenAI each hold the reply to, so a small model that wraps JSON in
|
|
57
|
+
* prose cannot. The schema is normalised as a tool's parameters are, and relaxed where the endpoint
|
|
58
|
+
* could not build a grammar, since the same converter reads both. A model that refuses the field
|
|
59
|
+
* latches it off and is asked in words: the schema rides on the system prompt either way, and
|
|
60
|
+
* the reply goes through `parseJson`, which finds the JSON in whatever came back.
|
|
61
|
+
*
|
|
62
|
+
* @param config Where to send it and how long to wait.
|
|
63
|
+
* @param model The model to ask.
|
|
64
|
+
* @param system The instruction. The schema is appended to it.
|
|
65
|
+
* @param user The input it applies to.
|
|
66
|
+
* @param schema The JSON Schema of the answer. Its root is held to an object, as a tool's is.
|
|
67
|
+
* @param options A side task's options, plus the schema's `name` and whether it is `strict`.
|
|
68
|
+
*/
|
|
69
|
+
export declare function askJson<T>(config: Endpoint, model: string, system: string, user: string, schema: Record<string, unknown>, { name, strict, ...options }?: AskJsonOptions): Promise<T | undefined>;
|
|
36
70
|
/**
|
|
37
71
|
* A side task is never worth failing the work it supports. Callers that can carry on without
|
|
38
72
|
* an answer use this and get `undefined` instead of an exception.
|