@cubicecho/agent-core 2.3.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 +205 -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/hooks.d.ts +33 -3
- package/dist/hooks.js +45 -5
- package/dist/index.d.ts +8 -4
- package/dist/index.js +8 -4
- package/dist/reset.d.ts +6 -5
- package/dist/reset.js +8 -5
- 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 +60 -1
- 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/hooks.d.ts
CHANGED
|
@@ -122,12 +122,40 @@ export interface Gathered {
|
|
|
122
122
|
notes: HookNote[];
|
|
123
123
|
}
|
|
124
124
|
/**
|
|
125
|
-
* The most context all of a request's hooks add between them, in estimated tokens.
|
|
125
|
+
* The most context all of a request's hooks add between them by default, in estimated tokens.
|
|
126
126
|
*
|
|
127
127
|
* Enough for a handful of recalled memories, and small against any window worth running an agent
|
|
128
128
|
* in. The point is that a generous hook cannot crowd out the conversation it was meant to inform.
|
|
129
|
+
* `configureHooks` moves it for a process, and `gather` and `assembleContext` for one request.
|
|
129
130
|
*/
|
|
130
131
|
export declare const HOOK_CONTEXT_TOKENS = 2000;
|
|
132
|
+
/** What hooks are held to across a process. Every field optional; see `configureHooks`. */
|
|
133
|
+
export interface HookOptions {
|
|
134
|
+
/**
|
|
135
|
+
* The budget every injecting hook shares, when a call does not give its own. Each hook is still
|
|
136
|
+
* held to its own `maxTokens` inside it.
|
|
137
|
+
*/
|
|
138
|
+
contextTokens?: number;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Changes what hooks are held to, for a process whose windows are not the size these defaults
|
|
142
|
+
* were chosen for.
|
|
143
|
+
*
|
|
144
|
+
* Module-level for the same reason `configureEvents` is: a budget is a deployment's setting, said
|
|
145
|
+
* once at startup. A caller that sizes it per model or per agent — a 128k window can afford more
|
|
146
|
+
* recall than an 8k one — passes `maxTokens` to `gather` instead, which wins over this.
|
|
147
|
+
*
|
|
148
|
+
* @param options The limits to change. A field left out — or given anything that is not a number
|
|
149
|
+
* above zero — keeps what it has, so a half-built config narrows nothing. `Infinity` is a number
|
|
150
|
+
* above zero, and lifts the shared budget entirely.
|
|
151
|
+
* @returns Everything in force afterwards, including what this call did not change.
|
|
152
|
+
*/
|
|
153
|
+
export declare function configureHooks(options?: HookOptions): Required<HookOptions>;
|
|
154
|
+
/**
|
|
155
|
+
* Test seam: puts `configureHooks` back to the defaults, so one test's budget is not the next's.
|
|
156
|
+
* `resetAll` calls it.
|
|
157
|
+
*/
|
|
158
|
+
export declare const resetHooks: () => void;
|
|
131
159
|
/**
|
|
132
160
|
* Said once, above the blocks, so the model reads them as background rather than instructions.
|
|
133
161
|
* Names no host; `withContext` takes another for one that wants to.
|
|
@@ -148,7 +176,8 @@ export declare const HOOK_PREFACE: string;
|
|
|
148
176
|
*
|
|
149
177
|
* @param outcomes What the runners returned. An injecting outcome on an event that cannot inject
|
|
150
178
|
* adds nothing; a failed one is noted wherever it falls, including past the budget.
|
|
151
|
-
* @param maxTokens The budget every block shares.
|
|
179
|
+
* @param maxTokens The budget every block shares. Absent, or not a number above zero, is what
|
|
180
|
+
* `configureHooks` last set — `HOOK_CONTEXT_TOKENS` unless something moved it.
|
|
152
181
|
*/
|
|
153
182
|
export declare function assembleContext(outcomes: readonly HookOutcome[], maxTokens?: number): Gathered;
|
|
154
183
|
/**
|
|
@@ -217,7 +246,8 @@ export declare const turnIndex: (messages: readonly {
|
|
|
217
246
|
* @param context What the hooks are told.
|
|
218
247
|
* @param options `signal` is handed to the runner, and should be the turn's own: a user who
|
|
219
248
|
* stopped the turn stopped its recall. `onNote` hears each note as the whole is assembled.
|
|
220
|
-
* `maxTokens` is the shared budget, `
|
|
249
|
+
* `maxTokens` is the shared budget for this request, read as `assembleContext` reads it: absent
|
|
250
|
+
* or unusable is the process's, from `configureHooks`.
|
|
221
251
|
*/
|
|
222
252
|
export declare function gather(run: HookRunner, events: readonly HookEvent[], context: HookContext, { signal, onNote, maxTokens, }?: {
|
|
223
253
|
signal?: AbortSignal;
|
package/dist/hooks.js
CHANGED
|
@@ -16,12 +16,50 @@ export const HOOK_EVENTS = [
|
|
|
16
16
|
*/
|
|
17
17
|
export const INJECT_EVENTS = new Set(["sessionStart", "beforeTurn"]);
|
|
18
18
|
/**
|
|
19
|
-
* The most context all of a request's hooks add between them, in estimated tokens.
|
|
19
|
+
* The most context all of a request's hooks add between them by default, in estimated tokens.
|
|
20
20
|
*
|
|
21
21
|
* Enough for a handful of recalled memories, and small against any window worth running an agent
|
|
22
22
|
* in. The point is that a generous hook cannot crowd out the conversation it was meant to inform.
|
|
23
|
+
* `configureHooks` moves it for a process, and `gather` and `assembleContext` for one request.
|
|
23
24
|
*/
|
|
24
25
|
export const HOOK_CONTEXT_TOKENS = 2000;
|
|
26
|
+
/** The numbers this module was written with. */
|
|
27
|
+
const HOOK_DEFAULTS = { contextTokens: HOOK_CONTEXT_TOKENS };
|
|
28
|
+
/** What is in force now. Read where it is used, so a change applies from the next request. */
|
|
29
|
+
let hookLimits = { ...HOOK_DEFAULTS };
|
|
30
|
+
/**
|
|
31
|
+
* Changes what hooks are held to, for a process whose windows are not the size these defaults
|
|
32
|
+
* were chosen for.
|
|
33
|
+
*
|
|
34
|
+
* Module-level for the same reason `configureEvents` is: a budget is a deployment's setting, said
|
|
35
|
+
* once at startup. A caller that sizes it per model or per agent — a 128k window can afford more
|
|
36
|
+
* recall than an 8k one — passes `maxTokens` to `gather` instead, which wins over this.
|
|
37
|
+
*
|
|
38
|
+
* @param options The limits to change. A field left out — or given anything that is not a number
|
|
39
|
+
* above zero — keeps what it has, so a half-built config narrows nothing. `Infinity` is a number
|
|
40
|
+
* above zero, and lifts the shared budget entirely.
|
|
41
|
+
* @returns Everything in force afterwards, including what this call did not change.
|
|
42
|
+
*/
|
|
43
|
+
export function configureHooks(options = {}) {
|
|
44
|
+
for (const [name, value] of Object.entries(options)) {
|
|
45
|
+
if (typeof value === "number" && value > 0)
|
|
46
|
+
hookLimits[name] = value;
|
|
47
|
+
}
|
|
48
|
+
return { ...hookLimits };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Test seam: puts `configureHooks` back to the defaults, so one test's budget is not the next's.
|
|
52
|
+
* `resetAll` calls it.
|
|
53
|
+
*/
|
|
54
|
+
export const resetHooks = () => {
|
|
55
|
+
hookLimits = { ...HOOK_DEFAULTS };
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* The budget a call is held to: its own when it gave a usable one, the process's otherwise. The
|
|
59
|
+
* same rule `configureHooks` applies, so a `0` threaded through for "no opinion" does not quietly
|
|
60
|
+
* turn every hook's context off.
|
|
61
|
+
*/
|
|
62
|
+
const budget = (given) => typeof given === "number" && given > 0 ? given : hookLimits.contextTokens;
|
|
25
63
|
/**
|
|
26
64
|
* Said once, above the blocks, so the model reads them as background rather than instructions.
|
|
27
65
|
* Names no host; `withContext` takes another for one that wants to.
|
|
@@ -44,12 +82,13 @@ const attribute = (text) => text.replaceAll("&", "&").replaceAll('"', ""
|
|
|
44
82
|
*
|
|
45
83
|
* @param outcomes What the runners returned. An injecting outcome on an event that cannot inject
|
|
46
84
|
* adds nothing; a failed one is noted wherever it falls, including past the budget.
|
|
47
|
-
* @param maxTokens The budget every block shares.
|
|
85
|
+
* @param maxTokens The budget every block shares. Absent, or not a number above zero, is what
|
|
86
|
+
* `configureHooks` last set — `HOOK_CONTEXT_TOKENS` unless something moved it.
|
|
48
87
|
*/
|
|
49
|
-
export function assembleContext(outcomes, maxTokens
|
|
88
|
+
export function assembleContext(outcomes, maxTokens) {
|
|
50
89
|
const blocks = [];
|
|
51
90
|
const notes = [];
|
|
52
|
-
let remaining = maxTokens;
|
|
91
|
+
let remaining = budget(maxTokens);
|
|
53
92
|
for (const outcome of outcomes) {
|
|
54
93
|
const base = { event: outcome.event, source: outcome.label, hookId: outcome.hookId };
|
|
55
94
|
if (!outcome.ok) {
|
|
@@ -182,7 +221,8 @@ const runSafely = (run, event, context, signal) => Promise.resolve()
|
|
|
182
221
|
* @param context What the hooks are told.
|
|
183
222
|
* @param options `signal` is handed to the runner, and should be the turn's own: a user who
|
|
184
223
|
* stopped the turn stopped its recall. `onNote` hears each note as the whole is assembled.
|
|
185
|
-
* `maxTokens` is the shared budget, `
|
|
224
|
+
* `maxTokens` is the shared budget for this request, read as `assembleContext` reads it: absent
|
|
225
|
+
* or unusable is the process's, from `configureHooks`.
|
|
186
226
|
*/
|
|
187
227
|
export async function gather(run, events, context, { signal, onNote, maxTokens, } = {}) {
|
|
188
228
|
const outcomes = await Promise.all(events.map((event) => runSafely(run, event, context, signal)));
|
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
|
-
export { assembleContext, type Gathered, gather, HOOK_CONTEXT_TOKENS, HOOK_EVENTS, HOOK_PREFACE, type HookContext, type HookEvent, type HookMessage, type HookNote, type HookOutcome, type HookRunner, INJECT_EVENTS, notify, turnIndex, turnMessages, withContext, } from "./hooks.ts";
|
|
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
|
-
export { assembleContext, gather, HOOK_CONTEXT_TOKENS, HOOK_EVENTS, HOOK_PREFACE, INJECT_EVENTS, notify, turnIndex, turnMessages, withContext, } from "./hooks.js";
|
|
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/reset.d.ts
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Forgets everything this package remembers between calls.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Five modules here keep state for the life of the process, each for a good reason and each
|
|
5
5
|
* with its own seam: the pooled clients and their model listings, the endpoints that turned
|
|
6
6
|
* out not to take `stream_options` or a grammar, the models that refused the no-thinking
|
|
7
|
-
* hints, and the
|
|
8
|
-
* exported, because a test that means to clear
|
|
7
|
+
* hints, the event bus, and the hooks' configured budget. `resetClients`, `resetCapabilities`,
|
|
8
|
+
* `resetHints`, `resetEvents` and `resetHooks` stay exported, because a test that means to clear
|
|
9
|
+
* one thing should say so.
|
|
9
10
|
*
|
|
10
|
-
* This is for the other case, which is every teardown. What
|
|
11
|
+
* This is for the other case, which is every teardown. What they hold is *latched
|
|
11
12
|
* refusals* — a fact one test taught the process about an endpoint, still true as far as the
|
|
12
13
|
* next test can tell. Miss one and the suite becomes order-dependent in the way that passes
|
|
13
14
|
* locally and fails in CI on a different shard: the test that latched it still passes, and the
|
|
14
15
|
* one that reads the latch fails only when it happens to run second. `tests/side-task-hints.test.ts`
|
|
15
16
|
* was written that way and only passed because every case had been handed a hostname of its own.
|
|
16
17
|
*
|
|
17
|
-
* It is also the seam that does not need finding again. A
|
|
18
|
+
* It is also the seam that does not need finding again. A sixth module with a cache is a sixth
|
|
18
19
|
* line here, rather than an edit to the teardown of three consumers who will not all notice.
|
|
19
20
|
*/
|
|
20
21
|
export declare function resetAll(): void;
|
package/dist/reset.js
CHANGED
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
import { resetCapabilities } from "./capabilities.js";
|
|
2
2
|
import { resetClients } from "./client.js";
|
|
3
3
|
import { resetEvents } from "./events.js";
|
|
4
|
+
import { resetHooks } from "./hooks.js";
|
|
4
5
|
import { resetHints } from "./side-task.js";
|
|
5
6
|
/**
|
|
6
7
|
* Forgets everything this package remembers between calls.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
+
* Five modules here keep state for the life of the process, each for a good reason and each
|
|
9
10
|
* with its own seam: the pooled clients and their model listings, the endpoints that turned
|
|
10
11
|
* out not to take `stream_options` or a grammar, the models that refused the no-thinking
|
|
11
|
-
* hints, and the
|
|
12
|
-
* exported, because a test that means to clear
|
|
12
|
+
* hints, the event bus, and the hooks' configured budget. `resetClients`, `resetCapabilities`,
|
|
13
|
+
* `resetHints`, `resetEvents` and `resetHooks` stay exported, because a test that means to clear
|
|
14
|
+
* one thing should say so.
|
|
13
15
|
*
|
|
14
|
-
* This is for the other case, which is every teardown. What
|
|
16
|
+
* This is for the other case, which is every teardown. What they hold is *latched
|
|
15
17
|
* refusals* — a fact one test taught the process about an endpoint, still true as far as the
|
|
16
18
|
* next test can tell. Miss one and the suite becomes order-dependent in the way that passes
|
|
17
19
|
* locally and fails in CI on a different shard: the test that latched it still passes, and the
|
|
18
20
|
* one that reads the latch fails only when it happens to run second. `tests/side-task-hints.test.ts`
|
|
19
21
|
* was written that way and only passed because every case had been handed a hostname of its own.
|
|
20
22
|
*
|
|
21
|
-
* It is also the seam that does not need finding again. A
|
|
23
|
+
* It is also the seam that does not need finding again. A sixth module with a cache is a sixth
|
|
22
24
|
* line here, rather than an edit to the teardown of three consumers who will not all notice.
|
|
23
25
|
*/
|
|
24
26
|
export function resetAll() {
|
|
@@ -26,4 +28,5 @@ export function resetAll() {
|
|
|
26
28
|
resetCapabilities();
|
|
27
29
|
resetHints();
|
|
28
30
|
resetEvents();
|
|
31
|
+
resetHooks();
|
|
29
32
|
}
|
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
|