@chatpanel/events 0.101.0 → 0.103.1
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/adapters.js +1 -83
- package/adaptive-tool-policy.js +1 -45
- package/agent.js +2 -255
- package/attribution.js +1 -132
- package/backup-envelope.js +1 -221
- package/board-tool.js +2 -165
- package/budget.js +1 -116
- package/capability.js +1 -126
- package/citations.js +5 -127
- package/client-prefs.js +1 -148
- package/context-attachments.js +8 -144
- package/cowriter-router.js +1 -83
- package/cowriter-writer.js +19 -193
- package/cowriter.js +1 -190
- package/curate.js +3 -509
- package/distance.js +1 -124
- package/engine.js +1 -131
- package/entitlement.js +1 -332
- package/entity.js +1 -354
- package/event.js +1 -175
- package/extraction.js +4 -293
- package/failover.js +1 -93
- package/find-tool.js +1 -66
- package/flowchart.js +2 -644
- package/gate.js +1 -74
- package/harness.js +4 -101
- package/index.js +1 -345
- package/invariants.js +1 -174
- package/job.js +3 -147
- package/kernel.js +1 -255
- package/knowledge-derive.js +1 -267
- package/knowledge.js +3 -230
- package/library.js +11 -280
- package/loop.js +1 -132
- package/manifest.js +1 -107
- package/markdown-authoring.js +11 -288
- package/markdown-render.js +0 -0
- package/mcp-client.js +8 -358
- package/mcp-dispatch.js +1 -52
- package/mcp-errors.js +1 -106
- package/mcp-manager.js +1 -127
- package/media-transcript.js +8 -531
- package/meeting-analyzers.js +1 -83
- package/meeting-insights.js +7 -164
- package/meeting-shape.js +1 -282
- package/meeting-text.js +5 -94
- package/memory.js +2 -638
- package/model-candidates.js +1 -358
- package/model-health.js +1 -182
- package/model-ledger.js +1 -228
- package/model-picker.js +1 -188
- package/note-actions.js +15 -246
- package/note-graph.js +1 -158
- package/note-links.js +1 -68
- package/note-mentions.js +8 -114
- package/note-plan.js +9 -107
- package/note-research.js +1 -135
- package/observability.js +1 -119
- package/omni.js +1 -125
- package/order.js +1 -78
- package/package.json +1 -4
- package/pdf-layout.js +7 -253
- package/project-run.js +20 -444
- package/project.js +1 -170
- package/promotion.js +1 -171
- package/queue.js +1 -98
- package/reach.js +1 -23
- package/recipe-tool.js +2 -161
- package/recipe.js +1 -264
- package/record-list.js +2 -110
- package/recruit.js +3 -456
- package/redaction-tokens.js +1 -61
- package/ref.js +1 -55
- package/registry.js +1 -240
- package/route-graph.js +1 -115
- package/route-strategies.js +2 -232
- package/router.js +2 -880
- package/rrf.js +1 -89
- package/rules.js +1 -142
- package/schedule.js +1 -663
- package/scm-connection.js +1 -180
- package/scopes.js +1 -12
- package/scorecard.js +1 -335
- package/search-engines.js +1 -81
- package/skill-manifest.js +1 -330
- package/skill-scan.js +3 -209
- package/skill-sources.js +1 -162
- package/skill-vars.js +2 -252
- package/slash-commands.js +1 -158
- package/source-gate.js +1 -92
- package/sources-retrieval.js +5 -186
- package/sources.js +1 -273
- package/store.js +1 -171
- package/structured.js +4 -901
- package/subject-kinds.js +1 -5
- package/subject-name.js +1 -96
- package/sync-plan.js +1 -213
- package/synthesis.js +3 -123
- package/tags.js +1 -203
- package/team-board.js +6 -337
- package/team-cache.js +2 -63
- package/team-observe.js +1 -118
- package/team-org.js +1 -348
- package/team-plan.js +4 -131
- package/team-record.js +1 -225
- package/team-run.js +9 -652
- package/team-subtask.js +2 -191
- package/team-task.js +2 -124
- package/team-tool.js +2 -196
- package/team-trail.js +2 -73
- package/team-worklog.js +2 -149
- package/team.js +1 -302
- package/text-search.js +1 -155
- package/theme.js +17 -154
- package/titles.js +4 -285
- package/tool-discovery.js +1 -87
- package/tool-dispatch.js +5 -230
- package/tool-groups.js +1 -81
- package/tool-hints.js +10 -185
- package/tool-loop-guard.js +4 -182
- package/tool-need.js +2 -124
- package/tool-result.js +2 -438
- package/tool-round.js +0 -0
- package/tool-schema.js +1 -155
- package/tool-traits.js +1 -182
- package/toolset.js +2 -88
- package/trajectory.js +1 -530
- package/turn-loop.js +4 -440
- package/upcast.js +1 -37
- package/vault.js +2 -250
- package/view.js +1 -88
- package/voice-intents.js +2 -1488
- package/voice-speaker.js +1 -98
- package/weather-tool.js +1 -65
- package/weather.js +2 -211
- package/web-search-tool.js +14 -102
- package/web-search.js +1 -160
- package/widget.js +1 -153
package/turn-loop.js
CHANGED
|
@@ -1,442 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
// One request either ends with an answer or with tool calls. On the second, the calls are
|
|
4
|
-
// run here and their results go back as the next request, until the model answers in words,
|
|
5
|
-
// the round cap is reached, or the guard decides the model is going in circles. That loop
|
|
6
|
-
// was written three times — once per provider in the extension, once in the desktop — and
|
|
7
|
-
// each copy knew something the others did not: the extension withheld tools on the last
|
|
8
|
-
// round and noticed a round repeating itself; the desktop kept a transcript, survived an
|
|
9
|
-
// abort with the words so far, and nudged a relayed CLI agent that ignores "no tools" until
|
|
10
|
-
// it answered. A fix to one never reached the other two. Now there is one loop and three
|
|
11
|
-
// bindings, each about thirty lines.
|
|
12
|
-
//
|
|
13
|
-
// What is injected, because it is the host's:
|
|
14
|
-
// • `stream(req)` — ONE model request: the provider call, its SSE decoding, its auth.
|
|
15
|
-
// Returns `{ ok, text, toolCalls, usage, aborted, error, finish,
|
|
16
|
-
// blocks?, noVision? }`. A thrown error propagates untouched — the
|
|
17
|
-
// extension's failover reads it.
|
|
18
|
-
// • `tools.execute` — what a call does. The toolset also carries `specs`, `traits`,
|
|
19
|
-
// `remoteTools`, `serialTools`.
|
|
20
|
-
// • `transcript` — how the asked/answered pair is written in this provider's wire
|
|
21
|
-
// shape. OpenAI (also what the gateway relays) and Anthropic ship
|
|
22
|
-
// here; a host with a third shape brings its own.
|
|
23
|
-
// • the callbacks — deltas, activity, steps, wire messages.
|
|
24
|
-
//
|
|
25
|
-
// What is NOT injected, because it is the point: the guard, the round, the cap, the
|
|
26
|
-
// exhaustion, the accounting. Class R with an async seam: no I/O of its own, no clock.
|
|
1
|
+
import{runToolRound as st}from"./tool-round.js";import{toolTraits as at,effectiveToolName as H,parallelEligible as it}from"./tool-traits.js";import{createToolLoopGuard as C,roundSignature as ut,toolMadeProgress as ct,blockedToolResult as lt}from"./tool-loop-guard.js";import{createAdaptiveToolPolicy as Y,resultText as J}from"./adaptive-tool-policy.js";import{toolStatus as pt}from"./tool-hints.js";const F=60,dt=6,K=4e3,X=`
|
|
27
2
|
|
|
28
|
-
|
|
29
|
-
import { toolTraits, effectiveToolName, parallelEligible } from './tool-traits.js';
|
|
30
|
-
import { createToolLoopGuard, roundSignature, toolMadeProgress, blockedToolResult } from './tool-loop-guard.js';
|
|
31
|
-
import { createAdaptiveToolPolicy, resultText } from './adaptive-tool-policy.js';
|
|
32
|
-
import { toolStatus } from './tool-hints.js';
|
|
3
|
+
`,z=Object.freeze(["The tool budget for this turn is spent. Do not call tools again; write your answer now with what you have, including your findings.","No more tool calls will be answered. Reply with your answer as plain text, now — a partial answer beats none.","FINAL: any further tool call ends this turn with no answer. Write what you have found, as text, in this message."]),mt="You have repeated the same tool call several times. Do not call tools again; answer now with what you have.",Rt='_(Reached the action limit for one turn — say "continue" to keep going.)_';function At({tools:t,agent:o,settings:e,fallback:n=F}={}){if(!t)return 1;const s=Math.max(1,Number(n)||F),i=Number(o?.maxRequestsPerTurn)||0;if(i>0)return Math.min(s,i);const m=Number(e?.ui?.maxToolRoundsPerTurn??e?.maxToolRoundsPerTurn)||0;return m>0?Math.min(s,m):s}function G(t){if(!t)return{};if(typeof t=="object")return t;try{return JSON.parse(t)}catch{return{}}}const Q=t=>typeof t.arguments=="string"?t.arguments:JSON.stringify(t.arguments??t.input??{});function ft(t){const o=String(J(t)||"");return o.length>K?`${o.slice(0,K)}…`:o}function ht(t,o){const e=H(t,o),n=o&&typeof o=="object"&&o.args&&typeof o.args=="object"?o.args:o||{},s=["query","id","tool","location","ref"].find(i=>typeof n[i]=="string"&&n[i]);return s?`${e} "${String(n[s]).slice(0,80)}"`:e}function Nt(t,o){const e=Array.isArray(t)?[...t]:[],n=String(o||"").trim();if(!n)return e;const s=e.findIndex(i=>i?.role==="system"&&typeof i.content=="string");return s>=0?(e[s]={...e[s],content:`${e[s].content}
|
|
33
4
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
/** How many stray tool calls a closing request (no tools offered) is answered before the turn ends anyway. */
|
|
37
|
-
export const DEFAULT_MAX_FINISH_TRIES = 6;
|
|
38
|
-
/** What a step shows of a result — the model receives the whole thing. */
|
|
39
|
-
export const STEP_RESULT_MAX_CHARS = 4000;
|
|
40
|
-
/** Rounds of one turn are separated in the text the user reads. */
|
|
41
|
-
export const ROUND_SEPARATOR = '\n\n';
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* A relayed CLI agent keeps its OWN session and its own tools, so "no tools offered" does
|
|
45
|
-
* not stop it asking — it answered the closing request with another call and an empty text,
|
|
46
|
-
* and a member's whole answer was its opening sentence. Each such call is answered with the
|
|
47
|
-
* next of these until words come back.
|
|
48
|
-
*/
|
|
49
|
-
export const FINISH_NUDGES = Object.freeze([
|
|
50
|
-
'The tool budget for this turn is spent. Do not call tools again; write your answer now with what you have, including your findings.',
|
|
51
|
-
'No more tool calls will be answered. Reply with your answer as plain text, now — a partial answer beats none.',
|
|
52
|
-
'FINAL: any further tool call ends this turn with no answer. Write what you have found, as text, in this message.',
|
|
53
|
-
]);
|
|
54
|
-
export const LOOPING_NUDGE = 'You have repeated the same tool call several times. Do not call tools again; answer now with what you have.';
|
|
55
|
-
/** Appended by a client that renders the exhausted flag as words — kept here so both say the same thing. */
|
|
56
|
-
export const EXHAUSTED_NOTE = '_(Reached the action limit for one turn — say "continue" to keep going.)_';
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* How many rounds this turn may take. The agent's own setting wins, then the user's
|
|
60
|
-
* preference, then the default; a turn without tools is one request.
|
|
61
|
-
*/
|
|
62
|
-
export function roundCap({ tools, agent, settings, fallback = DEFAULT_MAX_ROUNDS } = {}) {
|
|
63
|
-
if (!tools) return 1;
|
|
64
|
-
const ceiling = Math.max(1, Number(fallback) || DEFAULT_MAX_ROUNDS);
|
|
65
|
-
const own = Number(agent?.maxRequestsPerTurn) || 0;
|
|
66
|
-
if (own > 0) return Math.min(ceiling, own);
|
|
67
|
-
const pref = Number(settings?.ui?.maxToolRoundsPerTurn ?? settings?.maxToolRoundsPerTurn) || 0;
|
|
68
|
-
if (pref > 0) return Math.min(ceiling, pref);
|
|
69
|
-
return ceiling;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function safeJson(s) {
|
|
73
|
-
if (!s) return {};
|
|
74
|
-
if (typeof s === 'object') return s;
|
|
75
|
-
try { return JSON.parse(s); } catch { return {}; }
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const argString = (c) => (typeof c.arguments === 'string' ? c.arguments : JSON.stringify(c.arguments ?? c.input ?? {}));
|
|
79
|
-
|
|
80
|
-
/** What the model reads back from a tool: the `string | { text }` executor contract. */
|
|
81
|
-
export { resultText };
|
|
82
|
-
|
|
83
|
-
/** A short, display-safe slice of a result for a step — the model still gets the full result. */
|
|
84
|
-
export function stepResultText(result) {
|
|
85
|
-
const s = String(resultText(result) || '');
|
|
86
|
-
return s.length > STEP_RESULT_MAX_CHARS ? `${s.slice(0, STEP_RESULT_MAX_CHARS)}…` : s;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** One line for the activity trail — the real action and the argument that identifies it. */
|
|
90
|
-
export function describeCall(name, input) {
|
|
91
|
-
const eff = effectiveToolName(name, input);
|
|
92
|
-
const args = input && typeof input === 'object' && input.args && typeof input.args === 'object' ? input.args : (input || {});
|
|
93
|
-
const key = ['query', 'id', 'tool', 'location', 'ref'].find((k) => typeof args[k] === 'string' && args[k]);
|
|
94
|
-
return key ? `${eff} "${String(args[key]).slice(0, 80)}"` : eff;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** Put the toolset's guidance in front of the model, merged into an existing system turn. */
|
|
98
|
-
export function withToolSystem(messages, system) {
|
|
99
|
-
const list = Array.isArray(messages) ? [...messages] : [];
|
|
100
|
-
const text = String(system || '').trim();
|
|
101
|
-
if (!text) return list;
|
|
102
|
-
const i = list.findIndex((m) => m?.role === 'system' && typeof m.content === 'string');
|
|
103
|
-
if (i >= 0) { list[i] = { ...list[i], content: `${list[i].content}\n\n${text}` }; return list; }
|
|
104
|
-
return [{ role: 'system', content: text }, ...list];
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// ---------------------------------------------------------------------------------------
|
|
108
|
-
// Usage — adds up across rounds, in both key styles, so a team budget reading
|
|
109
|
-
// `prompt_tokens` and a ledger reading `inputTokens` see the same turn.
|
|
110
|
-
// ---------------------------------------------------------------------------------------
|
|
111
|
-
|
|
112
|
-
const n = (v) => Number(v) || 0;
|
|
113
|
-
|
|
114
|
-
export function normalizeUsage(u) {
|
|
115
|
-
if (!u || typeof u !== 'object') return null;
|
|
116
|
-
const inputTokens = n(u.inputTokens ?? u.input_tokens ?? u.prompt_tokens);
|
|
117
|
-
const outputTokens = n(u.outputTokens ?? u.output_tokens ?? u.completion_tokens);
|
|
118
|
-
const cacheReadTokens = n(u.cacheReadTokens ?? u.cache_read_input_tokens ?? u.prompt_tokens_details?.cached_tokens);
|
|
119
|
-
const cacheWriteTokens = n(u.cacheWriteTokens ?? u.cache_creation_input_tokens);
|
|
120
|
-
const out = {
|
|
121
|
-
inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens,
|
|
122
|
-
prompt_tokens: inputTokens, completion_tokens: outputTokens, total_tokens: n(u.total_tokens) || inputTokens + outputTokens,
|
|
123
|
-
calls: n(u.calls) || 1,
|
|
124
|
-
reported: u.reported !== false && (inputTokens > 0 || outputTokens > 0),
|
|
125
|
-
};
|
|
126
|
-
const usd = n(u.usd ?? u.cost);
|
|
127
|
-
if (usd) out.usd = usd;
|
|
128
|
-
return out;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** Two usage records as one. Either may be in a provider's raw shape. */
|
|
132
|
-
export function addUsage(a, b) {
|
|
133
|
-
const A = normalizeUsage(a); const B = normalizeUsage(b);
|
|
134
|
-
if (!B) return A;
|
|
135
|
-
if (!A) return B;
|
|
136
|
-
const out = {
|
|
137
|
-
inputTokens: A.inputTokens + B.inputTokens, outputTokens: A.outputTokens + B.outputTokens,
|
|
138
|
-
cacheReadTokens: A.cacheReadTokens + B.cacheReadTokens, cacheWriteTokens: A.cacheWriteTokens + B.cacheWriteTokens,
|
|
139
|
-
calls: A.calls + B.calls, reported: A.reported || B.reported,
|
|
140
|
-
};
|
|
141
|
-
out.prompt_tokens = out.inputTokens; out.completion_tokens = out.outputTokens; out.total_tokens = A.total_tokens + B.total_tokens;
|
|
142
|
-
const usd = n(A.usd) + n(B.usd);
|
|
143
|
-
if (usd) out.usd = usd;
|
|
144
|
-
return out;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** ~4 chars/token — ONLY when a provider reported nothing. No tokenizer: real usage is accurate and free. */
|
|
148
|
-
export function estimateTokens(text) {
|
|
149
|
-
return Math.max(0, Math.round(String(text || '').length / 4));
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function estimatedUsage(messages, text) {
|
|
153
|
-
const inText = (messages || []).map((m) => (typeof m?.content === 'string' ? m.content : JSON.stringify(m?.content || ''))).join('\n');
|
|
154
|
-
return { inputTokens: estimateTokens(inText), outputTokens: estimateTokens(text), cacheReadTokens: 0, cacheWriteTokens: 0, estimated: true };
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// ---------------------------------------------------------------------------------------
|
|
158
|
-
// Transcripts — the asked/answered pair in a provider's wire shape.
|
|
159
|
-
// ---------------------------------------------------------------------------------------
|
|
160
|
-
|
|
161
|
-
/** The OpenAI chat shape: what the gateway relays and every provider behind it understands. */
|
|
162
|
-
export const openAiTranscript = Object.freeze({
|
|
163
|
-
asked(res, calls) {
|
|
164
|
-
return {
|
|
165
|
-
role: 'assistant',
|
|
166
|
-
content: String(res?.text || '') || null,
|
|
167
|
-
tool_calls: calls.map((c) => ({ id: c.id, type: 'function', function: { name: c.name, arguments: argString(c) } })),
|
|
168
|
-
};
|
|
169
|
-
},
|
|
170
|
-
answered(pairs, { noVision = false } = {}) {
|
|
171
|
-
const out = pairs.map(({ call, result }) => ({ role: 'tool', tool_call_id: call.id, content: resultText(result) }));
|
|
172
|
-
// A tool message cannot carry an image. A screenshot goes back as a user message AFTER
|
|
173
|
-
// the round's tool messages (a user turn between two tool turns is rejected by strict
|
|
174
|
-
// providers), or as a note once the model has said it has no vision.
|
|
175
|
-
for (const { call, result } of pairs) {
|
|
176
|
-
const image = result && typeof result === 'object' ? result.image : null;
|
|
177
|
-
if (!image) continue;
|
|
178
|
-
out.push(noVision
|
|
179
|
-
? { role: 'user', content: `(Screenshot from ${call.name} omitted — this model has no vision. Rely on read_canvas / inspect_page / tool results.)` }
|
|
180
|
-
: { role: 'user', content: [{ type: 'text', text: `(Screenshot from ${call.name})` }, { type: 'image_url', image_url: { url: image } }] });
|
|
181
|
-
}
|
|
182
|
-
return out;
|
|
183
|
-
},
|
|
184
|
-
nudge(calls, text) {
|
|
185
|
-
return [
|
|
186
|
-
{ role: 'assistant', content: null, tool_calls: calls.map((c) => ({ id: c.id, type: 'function', function: { name: c.name, arguments: argString(c) } })) },
|
|
187
|
-
...calls.map((c) => ({ role: 'tool', tool_call_id: c.id, content: text })),
|
|
188
|
-
];
|
|
189
|
-
},
|
|
190
|
-
system: (text) => ({ role: 'system', content: text }),
|
|
191
|
-
said: (text) => ({ role: 'assistant', content: text }),
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
/** The Anthropic Messages shape: content blocks, every result of a round in ONE user turn. */
|
|
195
|
-
export const anthropicTranscript = Object.freeze({
|
|
196
|
-
asked(res, calls) {
|
|
197
|
-
// Echo the assistant's own blocks when the adapter kept them (text and tool_use in the
|
|
198
|
-
// order they came), dropping empty text — the API rejects zero-length text content.
|
|
199
|
-
const blocks = Array.isArray(res?.blocks) && res.blocks.length
|
|
200
|
-
? res.blocks.filter((b) => b && (b.type === 'tool_use' || (b.type === 'text' && b.text))).map((b) => (b.type === 'tool_use' ? { type: 'tool_use', id: b.id, name: b.name, input: b.input ?? safeJson(b.json) } : { type: 'text', text: b.text }))
|
|
201
|
-
: [...(res?.text ? [{ type: 'text', text: String(res.text) }] : []), ...calls.map((c) => ({ type: 'tool_use', id: c.id, name: c.name, input: c.input }))];
|
|
202
|
-
return { role: 'assistant', content: blocks };
|
|
203
|
-
},
|
|
204
|
-
answered(pairs) {
|
|
205
|
-
return [{
|
|
206
|
-
role: 'user',
|
|
207
|
-
content: pairs.map(({ call, result }) => {
|
|
208
|
-
const text = resultText(result);
|
|
209
|
-
const image = result && typeof result === 'object' ? result.image : null;
|
|
210
|
-
if (!image) return { type: 'tool_result', tool_use_id: call.id, content: text };
|
|
211
|
-
const im = /^data:([^;]+);base64,(.+)$/s.exec(image);
|
|
212
|
-
const content = [];
|
|
213
|
-
if (im) content.push({ type: 'image', source: { type: 'base64', media_type: im[1], data: im[2] } });
|
|
214
|
-
content.push({ type: 'text', text });
|
|
215
|
-
return { type: 'tool_result', tool_use_id: call.id, content };
|
|
216
|
-
}),
|
|
217
|
-
}];
|
|
218
|
-
},
|
|
219
|
-
nudge(calls, text) {
|
|
220
|
-
return [
|
|
221
|
-
{ role: 'assistant', content: calls.map((c) => ({ type: 'tool_use', id: c.id, name: c.name, input: c.input })) },
|
|
222
|
-
{ role: 'user', content: calls.map((c) => ({ type: 'tool_result', tool_use_id: c.id, content: text })) },
|
|
223
|
-
];
|
|
224
|
-
},
|
|
225
|
-
// No system role in the message list — the instruction rides as the user's words.
|
|
226
|
-
system: (text) => ({ role: 'user', content: text }),
|
|
227
|
-
said: (text) => ({ role: 'assistant', content: text }),
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
// ---------------------------------------------------------------------------------------
|
|
231
|
-
// Calls — one guarded call, one guarded round. The same code answers a call that arrives
|
|
232
|
-
// mid-stream from a CLI agent (the bridge relays one at a time) and a round of calls from
|
|
233
|
-
// an API model.
|
|
234
|
-
// ---------------------------------------------------------------------------------------
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* @param tools the toolset
|
|
238
|
-
* @param guard a tool-loop guard (one per turn)
|
|
239
|
-
* @param policy an adaptive tool policy (one per turn)
|
|
240
|
-
* @param modelLabel `() => string` — WHICH model made this call, read per call: a turn can
|
|
241
|
-
* change model mid-flight (failover), and attributing every action to
|
|
242
|
-
* whichever model finished misreports the work
|
|
243
|
-
* @param maxCalls after this many calls in the turn, each further one is answered with a
|
|
244
|
-
* "budget spent" nudge instead of running — the cap for a host that has
|
|
245
|
-
* no rounds to count (a relayed CLI agent). 0 = no cap.
|
|
246
|
-
* @param onStep `(step)` — `{ phase, callId, name, action, input, text, status, result, image, model }`
|
|
247
|
-
*/
|
|
248
|
-
export function createCallRunner({ tools, guard = createToolLoopGuard(), policy = createAdaptiveToolPolicy(), modelLabel = () => null, maxCalls = 0, onStep = null, steps = [] } = {}) {
|
|
249
|
-
let made = 0;
|
|
250
|
-
const traitsOf = (c) => {
|
|
251
|
-
const eff = effectiveToolName(c.name, c.input);
|
|
252
|
-
return tools?.traits?.get(eff) || tools?.traits?.get(c.name) || toolTraits({ name: eff });
|
|
253
|
-
};
|
|
254
|
-
const stepOf = (c, phase, result) => {
|
|
255
|
-
const step = { phase, callId: c.id, name: c.name, action: effectiveToolName(c.name, c.input), input: c.input, text: describeCall(c.name, c.input), model: modelLabel() };
|
|
256
|
-
if (phase === 'done') {
|
|
257
|
-
const image = result && typeof result === 'object' ? result.image : undefined;
|
|
258
|
-
Object.assign(step, { status: toolStatus(result), result: stepResultText(result), ...(image ? { image } : {}) });
|
|
259
|
-
}
|
|
260
|
-
return step;
|
|
261
|
-
};
|
|
262
|
-
const start = (c) => { try { onStep?.(stepOf(c, 'start')); } catch { /* reporting never breaks a turn */ } };
|
|
263
|
-
const done = (c, result) => {
|
|
264
|
-
const step = stepOf(c, 'done', result);
|
|
265
|
-
steps.push(step);
|
|
266
|
-
try { onStep?.(step); } catch { /* reporting never breaks a turn */ }
|
|
267
|
-
};
|
|
268
|
-
const settle = (c, g, result) => {
|
|
269
|
-
policy.recordResult(c.name, result);
|
|
270
|
-
if (!g.blocked && !g.replayed && toolMadeProgress(c.name, result, c.input)) guard.reset(g.key);
|
|
271
|
-
// Only a read is remembered for replay — the traits decide, not a list of names.
|
|
272
|
-
if (!g.replayed) guard.remember(g.key, c.name, c.input, result, { readOnly: !!traitsOf(c)?.readOnly });
|
|
273
|
-
};
|
|
274
|
-
const spent = (c) => blockedToolResult(c.name, FINISH_NUDGES[Math.min(Math.max(0, made - maxCalls - 1), FINISH_NUDGES.length - 1)], { budget: 'spent', calls: made, maxCalls });
|
|
275
|
-
const execute = async (c, g, meta) => {
|
|
276
|
-
made += 1;
|
|
277
|
-
if (maxCalls > 0 && made > maxCalls) return spent(c);
|
|
278
|
-
if (g.blocked || g.replayed) return g.result;
|
|
279
|
-
if (typeof tools?.execute !== 'function') return JSON.stringify({ error: 'no tools armed' });
|
|
280
|
-
return tools.execute(c.name, c.input, { callId: c.id, ...(meta || {}) });
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
return {
|
|
284
|
-
guard, policy, steps, traitsOf,
|
|
285
|
-
get calls() { return made; },
|
|
286
|
-
get exhausted() { return maxCalls > 0 && made >= maxCalls; },
|
|
287
|
-
|
|
288
|
-
/** One call, arriving on its own (a relayed agent). Every exit produces a result. */
|
|
289
|
-
async one(call, meta = null) {
|
|
290
|
-
const c = { id: call.id, name: call.name, input: call.input ?? safeJson(call.arguments) };
|
|
291
|
-
start(c);
|
|
292
|
-
const g = guard.check(c.name, c.input);
|
|
293
|
-
let result;
|
|
294
|
-
try { result = await execute(c, g, meta); } catch (e) { result = JSON.stringify({ error: String(e?.message || e) }); }
|
|
295
|
-
settle(c, g, result);
|
|
296
|
-
done(c, result);
|
|
297
|
-
return result;
|
|
298
|
-
},
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* One ROUND — reads overlapped, writes in the model's order, identical calls coalesced
|
|
302
|
-
* (tool-round.js). The guard is consulted up front in the model's order (its counts are
|
|
303
|
-
* order-dependent); the policy and the guard's memory are updated from each result.
|
|
304
|
-
*/
|
|
305
|
-
async round(wanted) {
|
|
306
|
-
const calls = wanted.map((c) => ({ id: c.id, name: c.name, input: c.input ?? safeJson(c.arguments) }));
|
|
307
|
-
const guards = calls.map((c) => guard.check(c.name, c.input));
|
|
308
|
-
const { results } = await runToolRound(calls, {
|
|
309
|
-
execute: (c, i) => execute(c, guards[i]),
|
|
310
|
-
traitsOf,
|
|
311
|
-
concurrent: (c, t) => parallelEligible(tools, c, t),
|
|
312
|
-
onStart: (c) => start(c),
|
|
313
|
-
onDone: (c, i, result) => done(c, result),
|
|
314
|
-
});
|
|
315
|
-
results.forEach((result, i) => settle(calls[i], guards[i], result));
|
|
316
|
-
const blocked = guards.filter((g) => g.blocked).length;
|
|
317
|
-
guard.noteRound(blocked, calls.length, roundSignature(calls));
|
|
318
|
-
return { calls, results, blocked };
|
|
319
|
-
},
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// ---------------------------------------------------------------------------------------
|
|
324
|
-
// The loop.
|
|
325
|
-
// ---------------------------------------------------------------------------------------
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Run a turn to completion.
|
|
329
|
-
*
|
|
330
|
-
* @param stream `(req) => { ok, text, toolCalls?, usage?, error?, aborted?, finish?, blocks?, noVision? }`
|
|
331
|
-
* — ONE request. `req` is `{ model, messages, tools, signal, redaction, run,
|
|
332
|
-
* onDelta(delta), onActivity }`; `req.tools` is the CANONICAL spec list
|
|
333
|
-
* (`{ name, description, parameters }`) or null when none are offered — the
|
|
334
|
-
* adapter shapes it for its provider.
|
|
335
|
-
* @param tools `{ specs, execute, traits?, remoteTools?, serialTools?, system? }`; absent = one plain request
|
|
336
|
-
* @param messages the wire messages as the host assembled them (system turns included)
|
|
337
|
-
* @param transcript how asked/answered are written — `openAiTranscript` (default) or `anthropicTranscript`
|
|
338
|
-
* @param maxRounds model requests with tools; see `roundCap`
|
|
339
|
-
* @param maxFinishTries stray calls answered on the closing request before giving up
|
|
340
|
-
* @param onDelta `(delta, text)` — `text` is everything said so far ACROSS rounds
|
|
341
|
-
* @param onEvent the extension's activity stream: `{type:'tool'|'finish'|'usage', …}`
|
|
342
|
-
* @param onStep the desktop's activity trail: one step per call, start and done
|
|
343
|
-
* @param onMessage `(msg)` each wire message the moment it exists — a record that grows as
|
|
344
|
-
* the turn goes, so a process that dies mid-turn leaves the work so far
|
|
345
|
-
* @param usageLabel `{ provider, model }` stamped on the usage event
|
|
346
|
-
* @returns `{ ok, text, usage, rounds, steps, transcript, exhausted, aborted, error, finish }`
|
|
347
|
-
*/
|
|
348
|
-
export async function runTurnLoop({
|
|
349
|
-
model, messages, tools, signal, redaction, run, stream,
|
|
350
|
-
transcript = openAiTranscript,
|
|
351
|
-
maxRounds = DEFAULT_MAX_ROUNDS, maxFinishTries = DEFAULT_MAX_FINISH_TRIES,
|
|
352
|
-
guard = createToolLoopGuard(), policy = createAdaptiveToolPolicy(),
|
|
353
|
-
modelLabel = () => model || null, usageLabel = null,
|
|
354
|
-
onDelta = null, onEvent = null, onStep = null, onMessage = null, onActivity = null,
|
|
355
|
-
} = {}) {
|
|
356
|
-
if (typeof stream !== 'function') throw new Error('runTurnLoop: stream required');
|
|
357
|
-
const armed = !!(tools && Array.isArray(tools.specs) && tools.specs.length);
|
|
358
|
-
const specs = armed ? tools.specs : null;
|
|
359
|
-
const cap = armed ? Math.max(1, Number(maxRounds) || DEFAULT_MAX_ROUNDS) : 1;
|
|
360
|
-
const steps = [];
|
|
361
|
-
const runner = createCallRunner({
|
|
362
|
-
tools, guard, policy, modelLabel, steps,
|
|
363
|
-
onStep: (step) => {
|
|
364
|
-
try { onStep?.(step); } catch { /* never break a turn */ }
|
|
365
|
-
try { onEvent?.({ type: 'tool', ...step }); } catch { /* never break a turn */ }
|
|
366
|
-
},
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
let convo = [...(messages || [])];
|
|
370
|
-
let said = ''; // everything the model has said so far, across rounds
|
|
371
|
-
let usage = null;
|
|
372
|
-
let rounds = 0;
|
|
373
|
-
let noVision = false;
|
|
374
|
-
let finishTries = 0;
|
|
375
|
-
let exhausted = false;
|
|
376
|
-
const push = (msgs) => { for (const m of msgs) { convo = [...convo, m]; try { onMessage?.(m); } catch { /* never break a turn */ } } };
|
|
377
|
-
const finish = (reason) => { try { onEvent?.({ type: 'finish', reason }); } catch { /* ignore */ } };
|
|
378
|
-
const usageEvent = (text) => {
|
|
379
|
-
if (!onEvent) return;
|
|
380
|
-
const u = usage && usage.reported ? { ...usage, estimated: false } : estimatedUsage(convo, text);
|
|
381
|
-
try { onEvent({ type: 'usage', provider: usageLabel?.provider || 'unknown', model: usageLabel?.model || model || null, inputTokens: u.inputTokens, outputTokens: u.outputTokens, cacheReadTokens: u.cacheReadTokens, cacheWriteTokens: u.cacheWriteTokens, estimated: !!u.estimated }); } catch { /* ignore */ }
|
|
382
|
-
};
|
|
383
|
-
const result = (over) => ({ ok: true, text: said, usage, rounds, steps, transcript: convo, exhausted, aborted: false, ...over });
|
|
384
|
-
const closeWith = (text, over = {}) => {
|
|
385
|
-
const t = String(text || '');
|
|
386
|
-
if (t.trim()) push([transcript.said(t)]);
|
|
387
|
-
return result(over);
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
// eslint-disable-next-line no-constant-condition
|
|
391
|
-
while (true) {
|
|
392
|
-
rounds += 1;
|
|
393
|
-
// The closing request: the cap is reached, the guard says the model is circling, or a
|
|
394
|
-
// stray call already came back to a request that offered nothing. No tools, so the turn
|
|
395
|
-
// ends with words — and if the agent asks anyway, it is answered until it stops.
|
|
396
|
-
const closing = !armed || rounds >= cap || guard.stalled || guard.looping || finishTries > 0;
|
|
397
|
-
const offered = closing ? null : specs.filter((s) => !policy.isSuppressed(s?.name));
|
|
398
|
-
let roundText = '';
|
|
399
|
-
const res = await stream({
|
|
400
|
-
model, messages: convo, tools: offered && offered.length ? offered : null, signal, onActivity,
|
|
401
|
-
// Only when set: a host reads these as "present", not as a value.
|
|
402
|
-
...(redaction !== undefined ? { redaction } : {}), ...(run ? { run } : {}),
|
|
403
|
-
onDelta: (delta) => {
|
|
404
|
-
if (!delta) return;
|
|
405
|
-
if (!roundText && said) { said += ROUND_SEPARATOR; try { onDelta?.(ROUND_SEPARATOR, said); } catch { /* ignore */ } }
|
|
406
|
-
roundText += delta; said += delta;
|
|
407
|
-
try { onDelta?.(delta, said); } catch { /* ignore */ }
|
|
408
|
-
},
|
|
409
|
-
});
|
|
410
|
-
if (res?.usage) usage = addUsage(usage, res.usage);
|
|
411
|
-
if (res?.noVision) noVision = true;
|
|
412
|
-
// Reconcile: an adapter that returned text without streaming it still gets it into `said`.
|
|
413
|
-
const text = String(res?.text || '');
|
|
414
|
-
if (text && !roundText) { if (said) said += ROUND_SEPARATOR; said += text; roundText = text; }
|
|
415
|
-
// `status` rides along when the adapter had one — a failover classifier reads it.
|
|
416
|
-
if (!res?.ok) { finish('error'); return result({ ok: false, error: res?.error || 'the model did not answer', ...(res?.status ? { status: res.status } : {}), aborted: !!res?.aborted, transcript: text.trim() ? [...convo, transcript.said(said)] : convo }); }
|
|
417
|
-
if (res.aborted || signal?.aborted) { finish('aborted'); return closeWith(said, { aborted: true }); }
|
|
418
|
-
|
|
419
|
-
const wanted = (Array.isArray(res.toolCalls) ? res.toolCalls : []).filter((c) => c && c.name).map((c) => ({ id: c.id, name: c.name, input: c.input ?? safeJson(c.arguments), arguments: c.arguments }));
|
|
420
|
-
if (!wanted.length) {
|
|
421
|
-
finish(res.finish || 'stop');
|
|
422
|
-
usageEvent(said);
|
|
423
|
-
return closeWith(said);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
if (!offered || !offered.length) {
|
|
427
|
-
// Asked with nothing offered. A relayed agent does this; answer, don't drop.
|
|
428
|
-
finishTries += 1;
|
|
429
|
-
if (finishTries > maxFinishTries) { finish('tool-step-limit'); usageEvent(said); return closeWith(said, { exhausted: true }); }
|
|
430
|
-
exhausted = exhausted || rounds >= cap;
|
|
431
|
-
push(transcript.nudge(wanted, FINISH_NUDGES[Math.min(finishTries - 1, FINISH_NUDGES.length - 1)]));
|
|
432
|
-
continue;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
push([transcript.asked(res, wanted)]);
|
|
436
|
-
const { calls, results } = await runner.round(wanted);
|
|
437
|
-
push(transcript.answered(calls.map((call, i) => ({ call, result: results[i] })), { noVision }));
|
|
438
|
-
if (signal?.aborted) { finish('aborted'); return closeWith(said, { aborted: true }); }
|
|
439
|
-
if (guard.looping) push([transcript.system(LOOPING_NUDGE)]);
|
|
440
|
-
if (rounds + 1 >= cap) exhausted = true;
|
|
441
|
-
}
|
|
442
|
-
}
|
|
5
|
+
${n}`},e):[{role:"system",content:n},...e]}const _=t=>Number(t)||0;function Z(t){if(!t||typeof t!="object")return null;const o=_(t.inputTokens??t.input_tokens??t.prompt_tokens),e=_(t.outputTokens??t.output_tokens??t.completion_tokens),n=_(t.cacheReadTokens??t.cache_read_input_tokens??t.prompt_tokens_details?.cached_tokens),s=_(t.cacheWriteTokens??t.cache_creation_input_tokens),i={inputTokens:o,outputTokens:e,cacheReadTokens:n,cacheWriteTokens:s,prompt_tokens:o,completion_tokens:e,total_tokens:_(t.total_tokens)||o+e,calls:_(t.calls)||1,reported:t.reported!==!1&&(o>0||e>0)},m=_(t.usd??t.cost);return m&&(i.usd=m),i}function yt(t,o){const e=Z(t),n=Z(o);if(!n)return e;if(!e)return n;const s={inputTokens:e.inputTokens+n.inputTokens,outputTokens:e.outputTokens+n.outputTokens,cacheReadTokens:e.cacheReadTokens+n.cacheReadTokens,cacheWriteTokens:e.cacheWriteTokens+n.cacheWriteTokens,calls:e.calls+n.calls,reported:e.reported||n.reported};s.prompt_tokens=s.inputTokens,s.completion_tokens=s.outputTokens,s.total_tokens=e.total_tokens+n.total_tokens;const i=_(e.usd)+_(n.usd);return i&&(s.usd=i),s}function tt(t){return Math.max(0,Math.round(String(t||"").length/4))}function kt(t,o){const e=(t||[]).map(n=>typeof n?.content=="string"?n.content:JSON.stringify(n?.content||"")).join(`
|
|
6
|
+
`);return{inputTokens:tt(e),outputTokens:tt(o),cacheReadTokens:0,cacheWriteTokens:0,estimated:!0}}const gt=Object.freeze({asked(t,o){return{role:"assistant",content:String(t?.text||"")||null,tool_calls:o.map(e=>({id:e.id,type:"function",function:{name:e.name,arguments:Q(e)}}))}},answered(t,{noVision:o=!1}={}){const e=t.map(({call:n,result:s})=>({role:"tool",tool_call_id:n.id,content:J(s)}));for(const{call:n,result:s}of t){const i=s&&typeof s=="object"?s.image:null;i&&e.push(o?{role:"user",content:`(Screenshot from ${n.name} omitted — this model has no vision. Rely on read_canvas / inspect_page / tool results.)`}:{role:"user",content:[{type:"text",text:`(Screenshot from ${n.name})`},{type:"image_url",image_url:{url:i}}]})}return e},nudge(t,o){return[{role:"assistant",content:null,tool_calls:t.map(e=>({id:e.id,type:"function",function:{name:e.name,arguments:Q(e)}}))},...t.map(e=>({role:"tool",tool_call_id:e.id,content:o}))]},system:t=>({role:"system",content:t}),said:t=>({role:"assistant",content:t})}),Ot=Object.freeze({asked(t,o){return{role:"assistant",content:Array.isArray(t?.blocks)&&t.blocks.length?t.blocks.filter(n=>n&&(n.type==="tool_use"||n.type==="text"&&n.text)).map(n=>n.type==="tool_use"?{type:"tool_use",id:n.id,name:n.name,input:n.input??G(n.json)}:{type:"text",text:n.text}):[...t?.text?[{type:"text",text:String(t.text)}]:[],...o.map(n=>({type:"tool_use",id:n.id,name:n.name,input:n.input}))]}},answered(t){return[{role:"user",content:t.map(({call:o,result:e})=>{const n=J(e),s=e&&typeof e=="object"?e.image:null;if(!s)return{type:"tool_result",tool_use_id:o.id,content:n};const i=/^data:([^;]+);base64,(.+)$/s.exec(s),m=[];return i&&m.push({type:"image",source:{type:"base64",media_type:i[1],data:i[2]}}),m.push({type:"text",text:n}),{type:"tool_result",tool_use_id:o.id,content:m}})}]},nudge(t,o){return[{role:"assistant",content:t.map(e=>({type:"tool_use",id:e.id,name:e.name,input:e.input}))},{role:"user",content:t.map(e=>({type:"tool_result",tool_use_id:e.id,content:o}))}]},system:t=>({role:"user",content:t}),said:t=>({role:"assistant",content:t})});function Tt({tools:t,guard:o=C(),policy:e=Y(),modelLabel:n=()=>null,maxCalls:s=0,onStep:i=null,steps:m=[]}={}){let y=0;const R=r=>{const a=H(r.name,r.input);return t?.traits?.get(a)||t?.traits?.get(r.name)||at({name:a})},E=(r,a,u)=>{const T={phase:a,callId:r.id,name:r.name,action:H(r.name,r.input),input:r.input,text:ht(r.name,r.input),model:n()};if(a==="done"){const f=u&&typeof u=="object"?u.image:void 0;Object.assign(T,{status:pt(u),result:ft(u),...f?{image:f}:{}})}return T},x=r=>{try{i?.(E(r,"start"))}catch{}},A=(r,a)=>{const u=E(r,"done",a);m.push(u);try{i?.(u)}catch{}},U=(r,a,u)=>{e.recordResult(r.name,u),!a.blocked&&!a.replayed&&ct(r.name,u,r.input)&&o.reset(a.key),a.replayed||o.remember(a.key,r.name,r.input,u,{readOnly:!!R(r)?.readOnly})},v=r=>lt(r.name,z[Math.min(Math.max(0,y-s-1),z.length-1)],{budget:"spent",calls:y,maxCalls:s}),N=async(r,a,u)=>(y+=1,s>0&&y>s?v(r):a.blocked||a.replayed?a.result:typeof t?.execute!="function"?JSON.stringify({error:"no tools armed"}):t.execute(r.name,r.input,{callId:r.id,...u||{}}));return{guard:o,policy:e,steps:m,traitsOf:R,get calls(){return y},get exhausted(){return s>0&&y>=s},async one(r,a=null){const u={id:r.id,name:r.name,input:r.input??G(r.arguments)};x(u);const T=o.check(u.name,u.input);let f;try{f=await N(u,T,a)}catch(c){f=JSON.stringify({error:String(c?.message||c)})}return U(u,T,f),A(u,f),f},async round(r){const a=r.map(c=>({id:c.id,name:c.name,input:c.input??G(c.arguments)})),u=a.map(c=>o.check(c.name,c.input)),{results:T}=await st(a,{execute:(c,g)=>N(c,u[g]),traitsOf:R,concurrent:(c,g)=>it(t,c,g),onStart:c=>x(c),onDone:(c,g,D)=>A(c,D)});T.forEach((c,g)=>U(a[g],u[g],c));const f=u.filter(c=>c.blocked).length;return o.noteRound(f,a.length,ut(a)),{calls:a,results:T,blocked:f}}}}async function jt({model:t,messages:o,tools:e,signal:n,redaction:s,run:i,stream:m,transcript:y=gt,maxRounds:R=F,maxFinishTries:E=dt,guard:x=C(),policy:A=Y(),modelLabel:U=()=>t||null,usageLabel:v=null,onDelta:N=null,onEvent:r=null,onStep:a=null,onMessage:u=null,onActivity:T=null}={}){if(typeof m!="function")throw new Error("runTurnLoop: stream required");const f=!!(e&&Array.isArray(e.specs)&&e.specs.length),c=f?e.specs:null,g=f?Math.max(1,Number(R)||F):1,D=[],et=Tt({tools:e,guard:x,policy:A,modelLabel:U,steps:D,onStep:k=>{try{a?.(k)}catch{}try{r?.({type:"tool",...k})}catch{}}});let b=[...o||[]],p="",S=null,O=0,q=!1,I=0,W=!1;const j=k=>{for(const d of k){b=[...b,d];try{u?.(d)}catch{}}},M=k=>{try{r?.({type:"finish",reason:k})}catch{}},V=k=>{if(!r)return;const d=S&&S.reported?{...S,estimated:!1}:kt(b,k);try{r({type:"usage",provider:v?.provider||"unknown",model:v?.model||t||null,inputTokens:d.inputTokens,outputTokens:d.outputTokens,cacheReadTokens:d.cacheReadTokens,cacheWriteTokens:d.cacheWriteTokens,estimated:!!d.estimated})}catch{}},B=k=>({ok:!0,text:p,usage:S,rounds:O,steps:D,transcript:b,exhausted:W,aborted:!1,...k}),P=(k,d={})=>{const w=String(k||"");return w.trim()&&j([y.said(w)]),B(d)};for(;;){O+=1;const d=!f||O>=g||x.stalled||x.looping||I>0?null:c.filter(l=>!A.isSuppressed(l?.name));let w="";const h=await m({model:t,messages:b,tools:d&&d.length?d:null,signal:n,onActivity:T,...s!==void 0?{redaction:s}:{},...i?{run:i}:{},onDelta:l=>{if(l){if(!w&&p){p+=X;try{N?.(X,p)}catch{}}w+=l,p+=l;try{N?.(l,p)}catch{}}}});h?.usage&&(S=yt(S,h.usage)),h?.noVision&&(q=!0);const $=String(h?.text||"");if($&&!w&&(p&&(p+=X),p+=$,w=$),!h?.ok)return M("error"),B({ok:!1,error:h?.error||"the model did not answer",...h?.status?{status:h.status}:{},aborted:!!h?.aborted,transcript:$.trim()?[...b,y.said(p)]:b});if(h.aborted||n?.aborted)return M("aborted"),P(p,{aborted:!0});const L=(Array.isArray(h.toolCalls)?h.toolCalls:[]).filter(l=>l&&l.name).map(l=>({id:l.id,name:l.name,input:l.input??G(l.arguments),arguments:l.arguments}));if(!L.length)return M(h.finish||"stop"),V(p),P(p);if(!d||!d.length){if(I+=1,I>E)return M("tool-step-limit"),V(p),P(p,{exhausted:!0});W=W||O>=g,j(y.nudge(L,z[Math.min(I-1,z.length-1)]));continue}j([y.asked(h,L)]);const{calls:nt,results:ot}=await et.round(L);if(j(y.answered(nt.map((l,rt)=>({call:l,result:ot[rt]})),{noVision:q})),n?.aborted)return M("aborted"),P(p,{aborted:!0});x.looping&&j([y.system(mt)]),O+1>=g&&(W=!0)}}export{dt as DEFAULT_MAX_FINISH_TRIES,F as DEFAULT_MAX_ROUNDS,Rt as EXHAUSTED_NOTE,z as FINISH_NUDGES,mt as LOOPING_NUDGE,X as ROUND_SEPARATOR,K as STEP_RESULT_MAX_CHARS,yt as addUsage,Ot as anthropicTranscript,Tt as createCallRunner,ht as describeCall,tt as estimateTokens,Z as normalizeUsage,gt as openAiTranscript,J as resultText,At as roundCap,jt as runTurnLoop,ft as stepResultText,Nt as withToolSystem};
|
package/upcast.js
CHANGED
|
@@ -1,37 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// it, so readers upcast and writers never mutate a stored event.
|
|
3
|
-
//
|
|
4
|
-
// The machinery exists from v1 with an empty chain, deliberately: the cost of adding it
|
|
5
|
-
// later is rewriting every reader, and the cost of having it now is this file.
|
|
6
|
-
//
|
|
7
|
-
// The log format is a Tesla-rule contract — additive only, guarded by a drift check,
|
|
8
|
-
// exactly like the bridge wire protocol.
|
|
9
|
-
|
|
10
|
-
import { CURRENT_VERSION, EventError, validateEvent } from './event.js';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* v(n) -> v(n+1) pure functions. Empty at v1.
|
|
14
|
-
* Each MUST be total: it may not throw for any event of its input version.
|
|
15
|
-
*/
|
|
16
|
-
export const UPCASTERS = Object.freeze({
|
|
17
|
-
// 1: (e) => ({ ...e, v: 2, payload: { ...e.payload, newField: defaultValue } }),
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
/** Carry a stored event forward to CURRENT_VERSION. Pure. */
|
|
21
|
-
export function upcast(stored) {
|
|
22
|
-
if (!stored || typeof stored !== 'object') throw new EventError('SHAPE', 'event must be an object');
|
|
23
|
-
let e = stored;
|
|
24
|
-
let guard = 0;
|
|
25
|
-
while (e.v < CURRENT_VERSION) {
|
|
26
|
-
const step = UPCASTERS[e.v];
|
|
27
|
-
if (!step) throw new EventError('UPCAST', `no upcaster from v${e.v}`, e.v);
|
|
28
|
-
e = step(e);
|
|
29
|
-
if (++guard > 64) throw new EventError('UPCAST', 'upcaster chain did not terminate');
|
|
30
|
-
}
|
|
31
|
-
if (e.v > CURRENT_VERSION) {
|
|
32
|
-
throw new EventError('UPCAST', `event is v${e.v}; this reader only knows v${CURRENT_VERSION}`, e.v);
|
|
33
|
-
}
|
|
34
|
-
return validateEvent(e);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function upcastAll(stored) { return stored.map(upcast); }
|
|
1
|
+
import{CURRENT_VERSION as r,EventError as n,validateEvent as i}from"./event.js";const a=Object.freeze({});function c(t){if(!t||typeof t!="object")throw new n("SHAPE","event must be an object");let e=t,v=0;for(;e.v<r;){const o=a[e.v];if(!o)throw new n("UPCAST",`no upcaster from v${e.v}`,e.v);if(e=o(e),++v>64)throw new n("UPCAST","upcaster chain did not terminate")}if(e.v>r)throw new n("UPCAST",`event is v${e.v}; this reader only knows v${r}`,e.v);return i(e)}function f(t){return t.map(c)}export{a as UPCASTERS,c as upcast,f as upcastAll};
|