@chatpanel/events 0.102.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.
Files changed (138) hide show
  1. package/adapters.js +1 -83
  2. package/adaptive-tool-policy.js +1 -45
  3. package/agent.js +2 -255
  4. package/attribution.js +1 -132
  5. package/backup-envelope.js +1 -221
  6. package/board-tool.js +2 -165
  7. package/budget.js +1 -116
  8. package/capability.js +1 -126
  9. package/citations.js +5 -127
  10. package/client-prefs.js +1 -148
  11. package/context-attachments.js +8 -144
  12. package/cowriter-router.js +1 -83
  13. package/cowriter-writer.js +19 -193
  14. package/cowriter.js +1 -190
  15. package/curate.js +3 -509
  16. package/distance.js +1 -124
  17. package/engine.js +1 -131
  18. package/entitlement.js +1 -332
  19. package/entity.js +1 -354
  20. package/event.js +1 -175
  21. package/extraction.js +4 -293
  22. package/failover.js +1 -93
  23. package/find-tool.js +1 -66
  24. package/flowchart.js +2 -644
  25. package/gate.js +1 -74
  26. package/harness.js +4 -101
  27. package/index.js +1 -345
  28. package/invariants.js +1 -174
  29. package/job.js +3 -147
  30. package/kernel.js +1 -255
  31. package/knowledge-derive.js +1 -267
  32. package/knowledge.js +3 -230
  33. package/library.js +11 -280
  34. package/loop.js +1 -132
  35. package/manifest.js +1 -107
  36. package/markdown-authoring.js +11 -288
  37. package/markdown-render.js +0 -0
  38. package/mcp-client.js +8 -358
  39. package/mcp-dispatch.js +1 -52
  40. package/mcp-errors.js +1 -106
  41. package/mcp-manager.js +1 -127
  42. package/media-transcript.js +8 -531
  43. package/meeting-analyzers.js +1 -83
  44. package/meeting-insights.js +7 -164
  45. package/meeting-shape.js +1 -282
  46. package/meeting-text.js +5 -94
  47. package/memory.js +2 -638
  48. package/model-candidates.js +1 -358
  49. package/model-health.js +1 -182
  50. package/model-ledger.js +1 -228
  51. package/model-picker.js +1 -188
  52. package/note-actions.js +15 -246
  53. package/note-graph.js +1 -158
  54. package/note-links.js +1 -68
  55. package/note-mentions.js +8 -114
  56. package/note-plan.js +9 -107
  57. package/note-research.js +1 -135
  58. package/observability.js +1 -119
  59. package/omni.js +1 -125
  60. package/order.js +1 -78
  61. package/package.json +1 -4
  62. package/pdf-layout.js +7 -253
  63. package/project-run.js +20 -444
  64. package/project.js +1 -170
  65. package/promotion.js +1 -171
  66. package/queue.js +1 -98
  67. package/reach.js +1 -23
  68. package/recipe-tool.js +2 -161
  69. package/recipe.js +1 -264
  70. package/record-list.js +2 -110
  71. package/recruit.js +3 -456
  72. package/redaction-tokens.js +1 -61
  73. package/ref.js +1 -55
  74. package/registry.js +1 -240
  75. package/route-graph.js +1 -115
  76. package/route-strategies.js +2 -232
  77. package/router.js +2 -880
  78. package/rrf.js +1 -89
  79. package/rules.js +1 -142
  80. package/schedule.js +1 -663
  81. package/scm-connection.js +1 -180
  82. package/scopes.js +1 -12
  83. package/scorecard.js +1 -335
  84. package/search-engines.js +1 -81
  85. package/skill-manifest.js +1 -330
  86. package/skill-scan.js +3 -209
  87. package/skill-sources.js +1 -162
  88. package/skill-vars.js +2 -252
  89. package/slash-commands.js +1 -158
  90. package/source-gate.js +1 -92
  91. package/sources-retrieval.js +5 -186
  92. package/sources.js +1 -273
  93. package/store.js +1 -171
  94. package/structured.js +4 -901
  95. package/subject-kinds.js +1 -5
  96. package/subject-name.js +1 -96
  97. package/sync-plan.js +1 -213
  98. package/synthesis.js +3 -123
  99. package/tags.js +1 -203
  100. package/team-board.js +6 -337
  101. package/team-cache.js +2 -63
  102. package/team-observe.js +1 -118
  103. package/team-org.js +1 -442
  104. package/team-plan.js +4 -131
  105. package/team-record.js +1 -225
  106. package/team-run.js +9 -652
  107. package/team-subtask.js +2 -191
  108. package/team-task.js +2 -124
  109. package/team-tool.js +2 -196
  110. package/team-trail.js +2 -73
  111. package/team-worklog.js +2 -149
  112. package/team.js +1 -302
  113. package/text-search.js +1 -155
  114. package/theme.js +17 -154
  115. package/titles.js +4 -285
  116. package/tool-discovery.js +1 -87
  117. package/tool-dispatch.js +5 -230
  118. package/tool-groups.js +1 -81
  119. package/tool-hints.js +10 -185
  120. package/tool-loop-guard.js +4 -182
  121. package/tool-need.js +2 -124
  122. package/tool-result.js +2 -438
  123. package/tool-round.js +0 -0
  124. package/tool-schema.js +1 -155
  125. package/tool-traits.js +1 -182
  126. package/toolset.js +2 -88
  127. package/trajectory.js +1 -530
  128. package/turn-loop.js +4 -440
  129. package/upcast.js +1 -37
  130. package/vault.js +2 -250
  131. package/view.js +1 -88
  132. package/voice-intents.js +2 -1488
  133. package/voice-speaker.js +1 -98
  134. package/weather-tool.js +1 -65
  135. package/weather.js +2 -211
  136. package/web-search-tool.js +14 -102
  137. package/web-search.js +1 -160
  138. package/widget.js +1 -153
package/turn-loop.js CHANGED
@@ -1,442 +1,6 @@
1
- // The turn loop a model that asks for tools gets them run, and is asked again.
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
- import { runToolRound } from './tool-round.js';
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
- /** Model requests one turn may make when tools are armed. Configurable, 60 is the ceiling either client ran with. */
35
- export const DEFAULT_MAX_ROUNDS = 60;
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
- // Schema evolution an append-only log outlives every version of the code that wrote
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};