@chatpanel/events 0.102.0 → 0.104.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.
Files changed (139) 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 +4 -5
  62. package/pdf-layout.js +7 -253
  63. package/policy.js +1 -0
  64. package/project-run.js +20 -444
  65. package/project.js +1 -170
  66. package/promotion.js +1 -171
  67. package/queue.js +1 -98
  68. package/reach.js +1 -23
  69. package/recipe-tool.js +2 -161
  70. package/recipe.js +1 -264
  71. package/record-list.js +2 -110
  72. package/recruit.js +3 -456
  73. package/redaction-tokens.js +1 -61
  74. package/ref.js +1 -55
  75. package/registry.js +1 -240
  76. package/route-graph.js +1 -115
  77. package/route-strategies.js +2 -232
  78. package/router.js +2 -880
  79. package/rrf.js +1 -89
  80. package/rules.js +1 -142
  81. package/schedule.js +1 -663
  82. package/scm-connection.js +1 -180
  83. package/scopes.js +1 -12
  84. package/scorecard.js +1 -335
  85. package/search-engines.js +1 -81
  86. package/skill-manifest.js +1 -330
  87. package/skill-scan.js +3 -209
  88. package/skill-sources.js +1 -162
  89. package/skill-vars.js +2 -252
  90. package/slash-commands.js +1 -158
  91. package/source-gate.js +1 -92
  92. package/sources-retrieval.js +5 -186
  93. package/sources.js +1 -273
  94. package/store.js +1 -171
  95. package/structured.js +4 -901
  96. package/subject-kinds.js +1 -5
  97. package/subject-name.js +1 -96
  98. package/sync-plan.js +1 -213
  99. package/synthesis.js +3 -123
  100. package/tags.js +1 -203
  101. package/team-board.js +6 -337
  102. package/team-cache.js +2 -63
  103. package/team-observe.js +1 -118
  104. package/team-org.js +1 -442
  105. package/team-plan.js +4 -131
  106. package/team-record.js +1 -225
  107. package/team-run.js +9 -652
  108. package/team-subtask.js +2 -191
  109. package/team-task.js +2 -124
  110. package/team-tool.js +2 -196
  111. package/team-trail.js +2 -73
  112. package/team-worklog.js +2 -149
  113. package/team.js +1 -302
  114. package/text-search.js +1 -155
  115. package/theme.js +17 -154
  116. package/titles.js +4 -285
  117. package/tool-discovery.js +1 -87
  118. package/tool-dispatch.js +5 -230
  119. package/tool-groups.js +1 -81
  120. package/tool-hints.js +10 -185
  121. package/tool-loop-guard.js +4 -182
  122. package/tool-need.js +2 -124
  123. package/tool-result.js +2 -438
  124. package/tool-round.js +0 -0
  125. package/tool-schema.js +1 -155
  126. package/tool-traits.js +1 -182
  127. package/toolset.js +2 -88
  128. package/trajectory.js +1 -530
  129. package/turn-loop.js +4 -440
  130. package/upcast.js +1 -37
  131. package/vault.js +2 -250
  132. package/view.js +1 -88
  133. package/voice-intents.js +2 -1488
  134. package/voice-speaker.js +1 -98
  135. package/weather-tool.js +1 -65
  136. package/weather.js +2 -211
  137. package/web-search-tool.js +14 -102
  138. package/web-search.js +1 -160
  139. package/widget.js +1 -153
package/capability.js CHANGED
@@ -1,126 +1 @@
1
- // The capability signature one call shape a rule, a schedule, the user or a model all
2
- // invoke identically, through one policy path.
3
- //
4
- // `actor` is the field that makes capabilities turn-independent; it is the whole of the
5
- // "capabilities are not turn-shaped" principle expressed as data rather than as a
6
- // subsystem.
7
- //
8
- // `requirements` is what the router dispatches on: not "which model" but "what must be
9
- // true". {maxLatencyMs:100, deterministic:true, egress:'none'} selects class R or M on a
10
- // host that can realize it — or REFUSES. Silently exceeding a declared budget is the
11
- // failure mode this exists to prevent.
12
-
13
- import { CLASSES, EFFECTS, EGRESS, ACTOR_KINDS, SCOPE_KINDS, EventError } from './event.js';
14
- import { DATA_SCOPES } from './scopes.js';
15
- import { validateView } from './view.js';
16
-
17
- export { DATA_SCOPES } from './scopes.js';
18
-
19
- const str = (v) => typeof v === 'string' && v.length > 0;
20
- const strs = (v, allowed = null) => Array.isArray(v) && v.every((x) => str(x) && (!allowed || allowed.includes(x)));
21
-
22
- /**
23
- * Validate a capability DECLARATION — the static surface a reviewer, a user or an admin
24
- * approves BEFORE the capability runs. Everything here is readable without executing
25
- * anything, which is what makes load-time approval possible.
26
- */
27
- export function validateCapability(c) {
28
- if (!c || typeof c !== 'object') throw new EventError('SHAPE', 'capability must be an object');
29
- if (!str(c.id)) throw new EventError('SHAPE', 'capability.id required');
30
- if (!str(c.version)) throw new EventError('SHAPE', 'capability.version required');
31
- if (!CLASSES.includes(c.class)) throw new EventError('SHAPE', `capability.class must be one of ${CLASSES}`);
32
- if (!strs(c.requires)) throw new EventError('SHAPE', 'capability.requires must be string[]');
33
- if (!strs(c.provides)) throw new EventError('SHAPE', 'capability.provides must be string[]');
34
- if (!strs(c.reads, DATA_SCOPES)) throw new EventError('SHAPE', `capability.reads must be within ${DATA_SCOPES}`);
35
- if (!strs(c.writes, DATA_SCOPES)) throw new EventError('SHAPE', `capability.writes must be within ${DATA_SCOPES}`);
36
- if (!EGRESS.includes(c.egress)) throw new EventError('SHAPE', `capability.egress must be one of ${EGRESS}`);
37
- if (!EFFECTS.includes(c.effects)) throw new EventError('SHAPE', `capability.effects must be one of ${EFFECTS}`);
38
- if (typeof c.invoke !== 'function') throw new EventError('SHAPE', 'capability.invoke required');
39
- if (typeof c.disclose !== 'function') throw new EventError('SHAPE', 'capability.disclose required');
40
- if (!c.output || typeof c.output.render !== 'function') {
41
- throw new EventError('SHAPE', 'capability.output.render required — canonical value and rendering are separate');
42
- }
43
- // A capability MAY ship its own UI. Optional, and validated against this capability so a
44
- // view can never name a capability its owner isn't already allowed to call.
45
- if (c.view != null) validateView(c.view, c);
46
- // A class-R capability that declares egress is a contradiction: R is a determinism
47
- // guarantee, and a network round-trip is not deterministic.
48
- if (c.class === 'R' && c.egress !== 'none') {
49
- throw new EventError('CONTRADICTION', 'class R must declare egress:none');
50
- }
51
- return c;
52
- }
53
-
54
- /**
55
- * Validate an INVOCATION. Enforces the one rule that is easiest to forget and worst to
56
- * miss: a capability that is not `pure` cannot be invoked without an idempotency key,
57
- * because a retried delegated call would otherwise perform the side effect twice.
58
- */
59
- export function validateInvocation(inv, capability) {
60
- if (!inv || typeof inv !== 'object') throw new EventError('SHAPE', 'invocation must be an object');
61
- if (!str(inv.capability)) throw new EventError('SHAPE', 'invocation.capability required');
62
- if (!inv.actor || !ACTOR_KINDS.includes(inv.actor.kind) || !str(inv.actor.id)) {
63
- throw new EventError('SHAPE', `invocation.actor.kind must be one of ${ACTOR_KINDS}`);
64
- }
65
- if (!inv.scope || !SCOPE_KINDS.includes(inv.scope.kind) || !str(inv.scope.id)) {
66
- throw new EventError('SHAPE', `invocation.scope.kind must be one of ${SCOPE_KINDS}`);
67
- }
68
- if (!Array.isArray(inv.causes)) throw new EventError('SHAPE', 'invocation.causes must be string[]');
69
- const effects = capability ? capability.effects : inv.effects;
70
- if (effects && effects !== 'pure' && !str(inv.idempotencyKey)) {
71
- throw new EventError('IDEMPOTENCY', `invocation of a '${effects}' capability requires an idempotencyKey`);
72
- }
73
- return inv;
74
- }
75
-
76
- /**
77
- * Can this capability satisfy these requirements on this host?
78
- * Returns { ok, reasons[] } — REFUSING is a valid, expected outcome.
79
- *
80
- * `host` supplies what it can actually realize: { realizes: {R:{maxMs},M:{maxMs},...} }.
81
- * Class is intrinsic (a guarantee); latency is host-bound. Never fuse the two.
82
- */
83
- export function canSatisfy(capability, requirements = {}, host = null) {
84
- const reasons = [];
85
- const { maxLatencyMs, deterministic, egress, maxCostUsd } = requirements;
86
-
87
- if (deterministic === true && !['R', 'M'].includes(capability.class)) {
88
- reasons.push(`class ${capability.class} is not deterministic`);
89
- }
90
- if (egress === 'none' && capability.egress !== 'none') {
91
- reasons.push(`capability egresses '${capability.egress}', requirement is 'none'`);
92
- }
93
- if (egress === 'redacted' && capability.egress === 'delegated') {
94
- reasons.push('delegated egress is not controlled, requirement is redacted');
95
- }
96
- if (maxLatencyMs != null && host) {
97
- const realized = host.realizes && host.realizes[capability.class];
98
- if (!realized) reasons.push(`host cannot realize class ${capability.class}`);
99
- else if (realized.maxMs > maxLatencyMs) {
100
- reasons.push(`host realizes class ${capability.class} at ~${realized.maxMs}ms, requirement is ${maxLatencyMs}ms`);
101
- }
102
- }
103
- if (maxCostUsd != null && capability.class === 'C' && maxCostUsd <= 0) {
104
- reasons.push('cloud class requires a positive cost ceiling');
105
- }
106
- return { ok: reasons.length === 0, reasons };
107
- }
108
-
109
- /**
110
- * The model-facing projection — an ALLOWLIST built from exactly three fields.
111
- *
112
- * Never an omit-list. An omit-list leaks the next field someone adds; this cannot,
113
- * because `invoke`, `effects`, `cost`, `writes` and `egress` are never copied.
114
- */
115
- export function toModelSchema(capability) {
116
- return {
117
- name: capability.id,
118
- description: capability.disclose().gist,
119
- parameters: capability.input || { type: 'object', properties: {} },
120
- };
121
- }
122
-
123
- /** The same allowlist over a toolset — the only supported way to build a model request. */
124
- export function toModelSchemas(capabilities) {
125
- return capabilities.map(toModelSchema);
126
- }
1
+ import{CLASSES as d,EFFECTS as p,EGRESS as w,ACTOR_KINDS as m,SCOPE_KINDS as S,EventError as s}from"./event.js";import{DATA_SCOPES as n}from"./scopes.js";import{validateView as E}from"./view.js";import{DATA_SCOPES as v}from"./scopes.js";const o=e=>typeof e=="string"&&e.length>0,a=(e,r=null)=>Array.isArray(e)&&e.every(t=>o(t)&&(!r||r.includes(t)));function g(e){if(!e||typeof e!="object")throw new s("SHAPE","capability must be an object");if(!o(e.id))throw new s("SHAPE","capability.id required");if(!o(e.version))throw new s("SHAPE","capability.version required");if(!d.includes(e.class))throw new s("SHAPE",`capability.class must be one of ${d}`);if(!a(e.requires))throw new s("SHAPE","capability.requires must be string[]");if(!a(e.provides))throw new s("SHAPE","capability.provides must be string[]");if(!a(e.reads,n))throw new s("SHAPE",`capability.reads must be within ${n}`);if(!a(e.writes,n))throw new s("SHAPE",`capability.writes must be within ${n}`);if(!w.includes(e.egress))throw new s("SHAPE",`capability.egress must be one of ${w}`);if(!p.includes(e.effects))throw new s("SHAPE",`capability.effects must be one of ${p}`);if(typeof e.invoke!="function")throw new s("SHAPE","capability.invoke required");if(typeof e.disclose!="function")throw new s("SHAPE","capability.disclose required");if(!e.output||typeof e.output.render!="function")throw new s("SHAPE","capability.output.render required — canonical value and rendering are separate");if(e.view!=null&&E(e.view,e),e.class==="R"&&e.egress!=="none")throw new s("CONTRADICTION","class R must declare egress:none");return e}function H(e,r){if(!e||typeof e!="object")throw new s("SHAPE","invocation must be an object");if(!o(e.capability))throw new s("SHAPE","invocation.capability required");if(!e.actor||!m.includes(e.actor.kind)||!o(e.actor.id))throw new s("SHAPE",`invocation.actor.kind must be one of ${m}`);if(!e.scope||!S.includes(e.scope.kind)||!o(e.scope.id))throw new s("SHAPE",`invocation.scope.kind must be one of ${S}`);if(!Array.isArray(e.causes))throw new s("SHAPE","invocation.causes must be string[]");const t=r?r.effects:e.effects;if(t&&t!=="pure"&&!o(e.idempotencyKey))throw new s("IDEMPOTENCY",`invocation of a '${t}' capability requires an idempotencyKey`);return e}function $(e,r={},t=null){const i=[],{maxLatencyMs:c,deterministic:h,egress:f,maxCostUsd:l}=r;if(h===!0&&!["R","M"].includes(e.class)&&i.push(`class ${e.class} is not deterministic`),f==="none"&&e.egress!=="none"&&i.push(`capability egresses '${e.egress}', requirement is 'none'`),f==="redacted"&&e.egress==="delegated"&&i.push("delegated egress is not controlled, requirement is redacted"),c!=null&&t){const u=t.realizes&&t.realizes[e.class];u?u.maxMs>c&&i.push(`host realizes class ${e.class} at ~${u.maxMs}ms, requirement is ${c}ms`):i.push(`host cannot realize class ${e.class}`)}return l!=null&&e.class==="C"&&l<=0&&i.push("cloud class requires a positive cost ceiling"),{ok:i.length===0,reasons:i}}function A(e){return{name:e.id,description:e.disclose().gist,parameters:e.input||{type:"object",properties:{}}}}function q(e){return e.map(A)}export{v as DATA_SCOPES,$ as canSatisfy,A as toModelSchema,q as toModelSchemas,g as validateCapability,H as validateInvocation};
package/citations.js CHANGED
@@ -1,128 +1,6 @@
1
- // Turn the citations a model wrote into the citations a reader can click.
2
- //
3
- // Models are told to cite as markdown links and finish with a Sources section. Large ones
4
- // mostly comply; small ones write bare `[1]` and stop — and a user then sees numbers that
5
- // reference nothing, with the URLs sitting unused in a tool result they never see. That is
6
- // exactly what happened on a real answer about SpaceX: five sources fetched, five bracket
7
- // numbers rendered, no links anywhere.
8
- //
9
- // The instinct is to write a firmer instruction. But the mapping from [1] to a URL is
10
- // already known EXACTLY — the tool result numbered them — so this is a substitution, not a
11
- // judgement, and a deterministic pass cannot fail to follow it. Prompting is the wrong tool
12
- // for something a rule can guarantee.
13
- //
14
- // Shared rather than client-side: any client that shows model output with sources needs
15
- // this, and the numbering convention belongs with the tool contract that produced it.
1
+ const g=/^\[(\d+)\]\s*\[([^\]]*)\]\(([^)\s]+)\)/gm;function k(n){const o=new Map;for(const e of String(n||"").matchAll(g)){const s=Number(e[1]);o.has(s)||o.set(s,{rank:s,title:e[2].trim(),url:e[3]})}return[...o.values()].sort((e,s)=>e.rank-s.rank)}const y=/\[(\d+(?:\s*,\s*\d+)*)\](?!\()/g;function d(n,o,{heading:e="Sources"}={}){const s=String(n??""),p=new Map((o||[]).filter(t=>t?.url).map(t=>[Number(t.rank),t]));if(!s.trim()||!p.size)return s;const r=new Set,c=s.split(/(```[\s\S]*?```|`[^`\n]*`)/g).map((t,a)=>a%2===1?t:t.replace(y,(h,m)=>{const f=m.split(",").map(u=>Number(u.trim()));return f.every(u=>p.has(u))?(f.forEach(u=>r.add(u)),f.map(u=>`([${u}](${p.get(u).url}))`).join(" ")):h})).join("");if(!r.size||new RegExp(`(^|\\n)\\s*(?:#{1,6}\\s*|\\*\\*)?${e}\\b`,"i").test(c))return c;const i=[...r].sort((t,a)=>t-a).map(t=>{const a=p.get(t);return`${t}. [${a.title||a.url}](${a.url})`}).join(`
2
+ `);return`${c.trimEnd()}
16
3
 
17
- /** `[1] [Title](https://url)` — the citation index every search result opens with. */
18
- const INDEX_RE = /^\[(\d+)\]\s*\[([^\]]*)\]\(([^)\s]+)\)/gm;
19
-
20
- /**
21
- * Recover the numbered sources from a tool result.
22
- *
23
- * Parsed from the SAME text the model was shown, so the numbers can never disagree with
24
- * what it read — deriving them from anywhere else would reintroduce the mismatch this
25
- * exists to remove.
26
- */
27
- export function sourcesFromToolText(text) {
28
- const out = new Map();
29
- for (const m of String(text || '').matchAll(INDEX_RE)) {
30
- const rank = Number(m[1]);
31
- if (!out.has(rank)) out.set(rank, { rank, title: m[2].trim(), url: m[3] });
32
- }
33
- return [...out.values()].sort((a, b) => a.rank - b.rank);
34
- }
35
-
36
- // A bare citation: [1] or [1, 3] or [1,3] — but NOT a markdown link `[x](url)`, NOT a
37
- // footnote definition at line start, and not `[]`.
38
- const BARE_RE = /\[(\d+(?:\s*,\s*\d+)*)\](?!\()/g;
39
-
40
- /**
41
- * Rewrite bare `[n]` citations as markdown links, and append a Sources section listing
42
- * exactly what was cited.
43
- *
44
- * Only ever ADDS links; text the model already wrote as a link is untouched, and a number
45
- * with no matching source is left exactly as it is — inventing a link for `[7]` when seven
46
- * sources were never returned would be fabricating a citation, which is worse than an
47
- * unlinked number.
48
- */
49
- export function linkifyCitations(answer, sources, { heading = 'Sources' } = {}) {
50
- const text = String(answer ?? '');
51
- const byRank = new Map((sources || []).filter((s) => s?.url).map((s) => [Number(s.rank), s]));
52
- if (!text.trim() || !byRank.size) return text;
53
-
54
- const cited = new Set();
55
- // Skip fenced code: a `[1]` inside a code block is code, not a citation.
56
- const parts = text.split(/(```[\s\S]*?```|`[^`\n]*`)/g);
57
- const linked = parts.map((part, i) => {
58
- if (i % 2 === 1) return part; // the captured code spans
59
- return part.replace(BARE_RE, (whole, group) => {
60
- const ranks = group.split(',').map((n) => Number(n.trim()));
61
- if (!ranks.every((n) => byRank.has(n))) return whole; // unknown number → leave alone
62
- ranks.forEach((n) => cited.add(n));
63
- return ranks.map((n) => `([${n}](${byRank.get(n).url}))`).join(' ');
64
- });
65
- }).join('');
66
-
67
- if (!cited.size) return linked;
68
-
69
- // A Sources section the model already wrote is left alone — appending a second one is a
70
- // worse outcome than a slightly differently-formatted first.
71
- // Match the heading however it was written — `## Sources`, `**Sources**`, or bare. The
72
- // first version only matched the bare form, so a model that bolded it got a second one.
73
- if (new RegExp(`(^|\\n)\\s*(?:#{1,6}\\s*|\\*\\*)?${heading}\\b`, 'i').test(linked)) return linked;
74
-
75
- const list = [...cited].sort((a, b) => a - b)
76
- .map((n) => { const s = byRank.get(n); return `${n}. [${s.title || s.url}](${s.url})`; })
77
- .join('\n');
78
- return `${linked.trimEnd()}\n\n**${heading}**\n${list}\n`;
79
- }
80
-
81
- // ---------------------------------------------------------------------------------------
82
- // The collector — what a turn was GIVEN, gathered as its tools return it, and put back
83
- // under the answer as links. Was the extension's `citationCollector`; the desktop had none,
84
- // so its answers cited `[1]` with nothing to click.
85
- // ---------------------------------------------------------------------------------------
86
-
87
- /** Tools whose job is to RETURN material rather than to act on something. */
88
- export const RETRIEVAL_TOOLS = Object.freeze(new Set(['find', 'source', 'history_search', 'web_search', 'search', 'fetch', 'read_page']));
89
-
90
- const retrievalName = (name) => String(name || '').replace(/^mcp[_-]/, '').split('__')[0];
91
-
92
- /**
93
- * Wrap a toolset so every result is read for numbered sources.
94
- *
95
- * @param tools `{ execute, … }`; returned as `.tools`, wrapped
96
- * @param onRetrieved `({ tool, count, chars, sources }) => void` — RETRIEVAL IS INPUT. Called
97
- * per call that returned material, so the turn shows what it was given and
98
- * by which tool. A retrieval tool that returned no LINKS still returned
99
- * material (notes, past chats), and is reported with `count: 0` — silence
100
- * there would under-report exactly the private sources this makes visible.
101
- * @returns `{ tools, list(), apply(answer) }`
102
- */
103
- export function createCitationCollector(tools, { onRetrieved = null } = {}) {
104
- const sources = new Map();
105
- const collect = (name, out) => {
106
- const body = typeof out === 'string' ? out : (out?.text || '');
107
- if (!body) return;
108
- const found = sourcesFromToolText(body);
109
- for (const s of found) if (!sources.has(s.rank)) sources.set(s.rank, s);
110
- if (!onRetrieved) return;
111
- try {
112
- if (found.length) onRetrieved({ tool: name, count: found.length, chars: body.length, sources: found.slice(0, 20).map((x) => ({ rank: x.rank, title: x.title || '', url: x.url || '' })) });
113
- else if (RETRIEVAL_TOOLS.has(retrievalName(name))) onRetrieved({ tool: name, count: 0, chars: body.length, sources: [] });
114
- } catch { /* reporting never breaks a turn */ }
115
- };
116
- const wrapped = tools && typeof tools.execute === 'function'
117
- ? { ...tools, execute: async (name, input, meta) => { const out = await tools.execute(name, input, meta); collect(name, out); return out; } }
118
- : tools;
119
- return {
120
- tools: wrapped,
121
- list: () => [...sources.values()],
122
- /** The answer with its `[n]` citations linked and a Sources section — or the answer untouched when nothing was retrieved. */
123
- apply(answer) {
124
- if (!sources.size || !answer || typeof answer !== 'string') return answer;
125
- try { return linkifyCitations(answer, [...sources.values()]); } catch { return answer; }
126
- },
127
- };
128
- }
4
+ **${e}**
5
+ ${i}
6
+ `}const x=Object.freeze(new Set(["find","source","history_search","web_search","search","fetch","read_page"])),E=n=>String(n||"").replace(/^mcp[_-]/,"").split("__")[0];function _(n,{onRetrieved:o=null}={}){const e=new Map,s=(r,l)=>{const c=typeof l=="string"?l:l?.text||"";if(!c)return;const i=k(c);for(const t of i)e.has(t.rank)||e.set(t.rank,t);if(o)try{i.length?o({tool:r,count:i.length,chars:c.length,sources:i.slice(0,20).map(t=>({rank:t.rank,title:t.title||"",url:t.url||""}))}):x.has(E(r))&&o({tool:r,count:0,chars:c.length,sources:[]})}catch{}};return{tools:n&&typeof n.execute=="function"?{...n,execute:async(r,l,c)=>{const i=await n.execute(r,l,c);return s(r,i),i}}:n,list:()=>[...e.values()],apply(r){if(!e.size||!r||typeof r!="string")return r;try{return d(r,[...e.values()])}catch{return r}}}}export{x as RETRIEVAL_TOOLS,_ as createCitationCollector,d as linkifyCitations,k as sourcesFromToolText};
package/client-prefs.js CHANGED
@@ -1,148 +1 @@
1
- // CLIENT PREFERENCES THAT EVERY CLIENT SHARES which settings travel between the extension
2
- // and the desktop, how they are cut out of the extension's settings tree and put back, and
3
- // how two copies that were both edited are reconciled.
4
- //
5
- // The extension keeps its settings in chrome.storage, which nothing on the machine can read.
6
- // The desktop and the extension DO share one address: the gateway. So the shareable settings
7
- // live there as a document of SECTIONS, each stamped with when it was last written, and both
8
- // clients push the sections they change and take the sections the other wrote more recently.
9
- // Per-section last-writer-wins, not per-key: a section is one thing a person edits on one
10
- // screen (the MCP server list, the search engines), and merging two edits of one list by key
11
- // would invent a list neither of them made.
12
- //
13
- // WHAT DOES NOT TRAVEL, ON PURPOSE. Endpoints and their keys (the gateway's destinations are
14
- // the desktop's copy of that idea, and a key should not be copied by a sync), tokens, the
15
- // active agent (per surface), theme and layout (per window), anything about a browser tab.
16
- //
17
- // Pure: `now` is injected, and nothing here knows a storage API or a network.
18
-
19
- /** The sections, and where each lives in the extension's settings tree. */
20
- export const PREF_SECTIONS = Object.freeze([
21
- { id: 'mcpServers', label: 'MCP tool servers', path: ['mcpServers'], kind: 'array' },
22
- { id: 'skills', label: 'Skills', path: ['skills'], kind: 'array' },
23
- { id: 'skillDirs', label: 'Skill folders', path: ['ui', 'skillDirs'], kind: 'array' },
24
- { id: 'recipes', label: 'Recipes', path: ['recipes'], kind: 'array' },
25
- { id: 'teams', label: 'Teams', path: ['teams'], kind: 'array' },
26
- // The agent pool (agent.js). NOT `settings.agents` — that key is the extension's harness
27
- // list (its bridge "agents"), which the naming rule keeps; the pool lives beside it.
28
- { id: 'agents', label: 'Agents', path: ['agentPool'], kind: 'array' },
29
- // SCM connections (scm-connection.js): kind, host, secret REF — never the token, which
30
- // stays in the machine's keychain / the bridge's secret store and does not travel.
31
- { id: 'connections', label: 'Connections', path: ['connections'], kind: 'array' },
32
- // The project pages (F8 §12): the goal, its stakeholder, budget, repos, gate. Jobs and runs
33
- // fold on the gateway's project record, not here.
34
- { id: 'projects', label: 'Projects', path: ['projects'], kind: 'array' },
35
- { id: 'webSearch', label: 'Web search', path: ['ui', 'webSearch'], kind: 'object' },
36
- { id: 'tools', label: 'Tools', path: null, kind: 'object', keys: ['mcpToolsMode', 'maxToolsPerTurn', 'maxToolRoundsPerTurn', 'historyTools', 'historyContextMode', 'dataDispatch', 'toolResultMaxChars'] },
37
- { id: 'suggestions', label: 'Smart suggestions', path: ['ui', 'suggestions'], kind: 'object' },
38
- { id: 'topics', label: 'Topic extraction', path: ['ui', 'topicExtraction'], kind: 'object' },
39
- { id: 'voice', label: 'Voice', path: ['ui', 'voice'], kind: 'object' },
40
- { id: 'watch', label: 'Watch', path: ['ui', 'watch'], kind: 'object' },
41
- { id: 'meetings', label: 'Meetings', path: null, kind: 'object', keys: ['meetingWindowMin', 'liveNotesIntervalMin', 'alertSound'] },
42
- { id: 'redaction', label: 'Redaction (client-side)', path: ['ui', 'piiRedaction'], kind: 'object' },
43
- // Internal sites (source-gate.js): which hosts are internal and how far their content may
44
- // travel. A rule the desktop enforces too, or a page internal in the panel is sent
45
- // anywhere from the desk.
46
- { id: 'internalSites', label: 'Internal sites', path: ['privacy'], kind: 'object' },
47
- ]);
48
-
49
- export const PREF_SECTION_IDS = Object.freeze(PREF_SECTIONS.map((s) => s.id));
50
-
51
- const clone = (v) => (v === undefined ? undefined : JSON.parse(JSON.stringify(v)));
52
-
53
- function getPath(obj, path) {
54
- let cur = obj;
55
- for (const k of path) { if (!cur || typeof cur !== 'object') return undefined; cur = cur[k]; }
56
- return cur;
57
- }
58
-
59
- function setPath(obj, path, value) {
60
- let cur = obj;
61
- for (let i = 0; i < path.length - 1; i += 1) {
62
- const k = path[i];
63
- if (!cur[k] || typeof cur[k] !== 'object') cur[k] = {};
64
- cur = cur[k];
65
- }
66
- cur[path[path.length - 1]] = value;
67
- }
68
-
69
- /** One section's value, read out of the extension's settings tree. `undefined` when absent. */
70
- export function sectionValue(settings, id) {
71
- const spec = PREF_SECTIONS.find((s) => s.id === id);
72
- if (!spec || !settings) return undefined;
73
- if (spec.path) return clone(getPath(settings, spec.path));
74
- const ui = settings.ui || {};
75
- const out = {};
76
- let any = false;
77
- for (const k of spec.keys) if (ui[k] !== undefined) { out[k] = clone(ui[k]); any = true; }
78
- return any ? out : undefined;
79
- }
80
-
81
- /** Every section present in the settings tree: `{ id: value }`. */
82
- export function pickSections(settings) {
83
- const out = {};
84
- for (const s of PREF_SECTIONS) {
85
- const v = sectionValue(settings, s.id);
86
- if (v !== undefined) out[s.id] = v;
87
- }
88
- return out;
89
- }
90
-
91
- /** The settings tree with these section values put back. Returns a new object. */
92
- export function applySections(settings, sections) {
93
- const next = clone(settings || {}) || {};
94
- for (const [id, value] of Object.entries(sections || {})) {
95
- const spec = PREF_SECTIONS.find((s) => s.id === id);
96
- if (!spec || value === undefined) continue;
97
- if (spec.path) { setPath(next, spec.path, clone(value)); continue; }
98
- if (!next.ui || typeof next.ui !== 'object') next.ui = {};
99
- for (const k of spec.keys) if (value && value[k] !== undefined) next.ui[k] = clone(value[k]);
100
- }
101
- return next;
102
- }
103
-
104
- /** A stable fingerprint of a value — key order does not count as a change. */
105
- export function sectionHash(value) {
106
- const sort = (v) => {
107
- if (Array.isArray(v)) return v.map(sort);
108
- if (v && typeof v === 'object') return Object.fromEntries(Object.keys(v).sort().map((k) => [k, sort(v[k])]));
109
- return v;
110
- };
111
- try { return JSON.stringify(sort(value)); } catch { return String(value); }
112
- }
113
-
114
- /**
115
- * Reconcile two stamped copies: `{ id: { value, updatedAt } }` each.
116
- *
117
- * The newer stamp wins per section; equal stamps keep `local` (nothing to do). Returns the
118
- * merged document plus which sections each side contributed, so a client knows what to
119
- * write locally (`fromRemote`) and what to push (`fromLocal`).
120
- */
121
- export function mergeStamped(local = {}, remote = {}) {
122
- const merged = {};
123
- const fromRemote = [];
124
- const fromLocal = [];
125
- const ids = new Set([...Object.keys(local || {}), ...Object.keys(remote || {})]);
126
- for (const id of ids) {
127
- const l = local?.[id]; const r = remote?.[id];
128
- const lt = Number(l?.updatedAt) || 0; const rt = Number(r?.updatedAt) || 0;
129
- if (r && (!l || rt > lt)) { merged[id] = { value: clone(r.value), updatedAt: rt }; if (l ? sectionHash(l.value) !== sectionHash(r.value) : true) fromRemote.push(id); continue; }
130
- if (l) { merged[id] = { value: clone(l.value), updatedAt: lt }; if (!r || lt > rt) fromLocal.push(id); }
131
- }
132
- return { merged, fromRemote, fromLocal };
133
- }
134
-
135
- /**
136
- * What a client should PUSH: sections whose value differs from what it last pushed. Each is
137
- * stamped `now`, so the other side sees them as newer than its own copy.
138
- */
139
- export function changedSections(sections, lastPushed = {}, { now = Date.now() } = {}) {
140
- const out = {};
141
- for (const [id, value] of Object.entries(sections || {})) {
142
- if (!PREF_SECTION_IDS.includes(id)) continue;
143
- const prev = lastPushed?.[id];
144
- if (prev && prev.hash === sectionHash(value)) continue;
145
- out[id] = { value: clone(value), updatedAt: now };
146
- }
147
- return out;
148
- }
1
+ const l=Object.freeze([{id:"mcpServers",label:"MCP tool servers",path:["mcpServers"],kind:"array"},{id:"skills",label:"Skills",path:["skills"],kind:"array"},{id:"skillDirs",label:"Skill folders",path:["ui","skillDirs"],kind:"array"},{id:"recipes",label:"Recipes",path:["recipes"],kind:"array"},{id:"teams",label:"Teams",path:["teams"],kind:"array"},{id:"agents",label:"Agents",path:["agentPool"],kind:"array"},{id:"connections",label:"Connections",path:["connections"],kind:"array"},{id:"projects",label:"Projects",path:["projects"],kind:"array"},{id:"webSearch",label:"Web search",path:["ui","webSearch"],kind:"object"},{id:"tools",label:"Tools",path:null,kind:"object",keys:["mcpToolsMode","maxToolsPerTurn","maxToolRoundsPerTurn","historyTools","historyContextMode","dataDispatch","toolResultMaxChars"]},{id:"suggestions",label:"Smart suggestions",path:["ui","suggestions"],kind:"object"},{id:"topics",label:"Topic extraction",path:["ui","topicExtraction"],kind:"object"},{id:"voice",label:"Voice",path:["ui","voice"],kind:"object"},{id:"watch",label:"Watch",path:["ui","watch"],kind:"object"},{id:"meetings",label:"Meetings",path:null,kind:"object",keys:["meetingWindowMin","liveNotesIntervalMin","alertSound"]},{id:"redaction",label:"Redaction (client-side)",path:["ui","piiRedaction"],kind:"object"},{id:"internalSites",label:"Internal sites",path:["privacy"],kind:"object"}]),b=Object.freeze(l.map(n=>n.id)),c=n=>n===void 0?void 0:JSON.parse(JSON.stringify(n));function h(n,i){let e=n;for(const t of i){if(!e||typeof e!="object")return;e=e[t]}return e}function k(n,i,e){let t=n;for(let r=0;r<i.length-1;r+=1){const s=i[r];(!t[s]||typeof t[s]!="object")&&(t[s]={}),t=t[s]}t[i[i.length-1]]=e}function y(n,i){const e=l.find(o=>o.id===i);if(!e||!n)return;if(e.path)return c(h(n,e.path));const t=n.ui||{},r={};let s=!1;for(const o of e.keys)t[o]!==void 0&&(r[o]=c(t[o]),s=!0);return s?r:void 0}function j(n){const i={};for(const e of l){const t=y(n,e.id);t!==void 0&&(i[e.id]=t)}return i}function m(n,i){const e=c(n||{})||{};for(const[t,r]of Object.entries(i||{})){const s=l.find(o=>o.id===t);if(!(!s||r===void 0)){if(s.path){k(e,s.path,c(r));continue}(!e.ui||typeof e.ui!="object")&&(e.ui={});for(const o of s.keys)r&&r[o]!==void 0&&(e.ui[o]=c(r[o]))}}return e}function p(n){const i=e=>Array.isArray(e)?e.map(i):e&&typeof e=="object"?Object.fromEntries(Object.keys(e).sort().map(t=>[t,i(e[t])])):e;try{return JSON.stringify(i(n))}catch{return String(n)}}function S(n={},i={}){const e={},t=[],r=[],s=new Set([...Object.keys(n||{}),...Object.keys(i||{})]);for(const o of s){const a=n?.[o],d=i?.[o],u=Number(a?.updatedAt)||0,f=Number(d?.updatedAt)||0;if(d&&(!a||f>u)){e[o]={value:c(d.value),updatedAt:f},(!a||p(a.value)!==p(d.value))&&t.push(o);continue}a&&(e[o]={value:c(a.value),updatedAt:u},(!d||u>f)&&r.push(o))}return{merged:e,fromRemote:t,fromLocal:r}}function g(n,i={},{now:e=Date.now()}={}){const t={};for(const[r,s]of Object.entries(n||{})){if(!b.includes(r))continue;const o=i?.[r];o&&o.hash===p(s)||(t[r]={value:c(s),updatedAt:e})}return t}export{l as PREF_SECTIONS,b as PREF_SECTION_IDS,m as applySections,g as changedSections,S as mergeStamped,j as pickSections,p as sectionHash,y as sectionValue};
@@ -1,148 +1,12 @@
1
- // ATTACHED CONTEXT, AS THE MODEL SEES IT the same on every client.
2
- //
3
- // A message carries `attachments`: pages, selections, fetched links, files, images, and the
4
- // user's own records. This is how they reach a model: text sources folded into the message
5
- // as <context> blocks, images as image blocks in the provider's wire shape, and — when the
6
- // turn has tools and the sources are big — a MANIFEST plus a `source` tool instead of the
7
- // full text, so "hi" on a long page does not pay for the page.
8
- //
9
- // Moved out of the extension's providers.js so the desktop's turn assembles attachments
10
- // exactly the way the panel does. Pure: nothing here fetches or renders.
1
+ import{makeSourceStore as d,manifestText as l,readSource as f,approxTokens as p}from"./sources-retrieval.js";function h(n){let t=n.content||"";const i=(n.attachments||[]).filter(e=>e.kind!=="image");if(i.length){const e=i.map(r=>{const c=`[${r.kind||"context"}] ${r.title||r.url||""}`.trim();return`<context source="${(r.url||r.title||"").replace(/"/g,"")}">
2
+ # ${c}
3
+ ${r.text||""}
4
+ </context>`}).join(`
11
5
 
12
- import { makeSourceStore, manifestText, readSource, approxTokens } from './sources-retrieval.js';
6
+ `);t=t?`${t}
13
7
 
14
- // Flatten a stored message (with attachments) into the text the model sees.
15
- // Image attachments are excluded here — they go to the model as image blocks
16
- // (see toMultimodalMessages), not as text.
17
- export function renderContent(m) {
18
- let text = m.content || '';
19
- const ctx = (m.attachments || []).filter((a) => a.kind !== 'image');
20
- if (ctx.length) {
21
- const blocks = ctx
22
- .map((a) => {
23
- const head = `[${a.kind || 'context'}] ${a.title || a.url || ''}`.trim();
24
- return `<context source="${(a.url || a.title || '').replace(/"/g, '')}">\n# ${head}\n${a.text || ''}\n</context>`;
25
- })
26
- .join('\n\n');
27
- text = text ? `${text}\n\n${blocks}` : blocks;
28
- }
29
- return text;
30
- }
8
+ ${e}`:e}return t}function y(n){return n.filter(t=>t.role==="user"||t.role==="assistant").map(t=>({role:t.role,content:h(t)}))}function g(n){return(n.attachments||[]).filter(t=>t.kind==="image"&&t.dataUrl)}function k(n,t){return n.filter(i=>i.role==="user"||i.role==="assistant").map(i=>{const e=h(i),r=i.role==="user"?g(i):[];if(r.length===0)return{role:i.role,content:e};if(t==="anthropic"){const a=[];for(const s of r){const o=/^data:([^;]+);base64,(.+)$/s.exec(s.dataUrl);o&&a.push({type:"image",source:{type:"base64",media_type:o[1],data:o[2]}})}return e&&a.push({type:"text",text:e}),{role:"user",content:a.length?a:e}}const c=[];e&&c.push({type:"text",text:e});for(const a of r)c.push({type:"image_url",image_url:{url:a.dataUrl}});return{role:"user",content:c.length?c:e}})}function $(n,t,{minTokens:i=700}={}){if(!t?.specs?.length)return null;const e=[];for(const s of n||[])for(const o of s?.attachments||[])o?.kind==="image"||!o?.text||e.push(o);if(!e.length)return null;const r=d(e.map(s=>({kind:s.kind||"context",title:s.title,url:s.url,text:s.text})));if(r.tokens<i)return null;const c=new Map(e.map((s,o)=>[s,r.entries[o]?.id]));return{messages:(n||[]).map(s=>s?.attachments?.some(o=>c.get(o))?{...s,attachments:s.attachments.map(o=>{const u=c.get(o);return u?{...o,text:`(not included — read with \`source\`: id ${u}, ~${p(o.text)} tokens)`}:o})}:s),store:r}}const m={name:"source",description:"Read an attached source (a page, tab, selection or file the user attached). Their content is NOT in the conversation — read what you need from here.",parameters:{type:"object",properties:{id:{type:"string",description:"The source id from the manifest, e.g. page-1."},query:{type:"string",description:"What you are looking for. A large source returns the matching sections rather than its first page."}},required:["id"]}};function S(n,t){const i={...m},e=[n?.system,`${l(t)}
31
9
 
32
- export function toChatMessages(messages) {
33
- return messages
34
- .filter((m) => m.role === 'user' || m.role === 'assistant')
35
- .map((m) => ({ role: m.role, content: renderContent(m) }));
36
- }
10
+ Their content is NOT in this conversation. Call \`source\` with an id — and a query when the source is large — to read what you need.`].filter(Boolean).join(`
37
11
 
38
- // Image attachments on a message: { dataUrl: 'data:<media>;base64,<...>' }.
39
- export function imageAttachmentsOf(m) {
40
- return (m.attachments || []).filter((a) => a.kind === 'image' && a.dataUrl);
41
- }
42
-
43
- // Like toChatMessages, but emits multimodal content (text + image blocks) for
44
- // user messages that carry images, in the given provider's wire format. Falls
45
- // back to plain string content when there are no images. `provider` is
46
- // 'openai' | 'anthropic'.
47
- export function toMultimodalMessages(messages, provider) {
48
- return messages
49
- .filter((m) => m.role === 'user' || m.role === 'assistant')
50
- .map((m) => {
51
- const text = renderContent(m);
52
- const imgs = m.role === 'user' ? imageAttachmentsOf(m) : [];
53
- if (imgs.length === 0) return { role: m.role, content: text };
54
- if (provider === 'anthropic') {
55
- const content = [];
56
- for (const a of imgs) {
57
- const match = /^data:([^;]+);base64,(.+)$/s.exec(a.dataUrl);
58
- if (match) content.push({ type: 'image', source: { type: 'base64', media_type: match[1], data: match[2] } });
59
- }
60
- if (text) content.push({ type: 'text', text });
61
- return { role: 'user', content: content.length ? content : text };
62
- }
63
- // openai (and OpenAI-compatible vision endpoints)
64
- const content = [];
65
- if (text) content.push({ type: 'text', text });
66
- for (const a of imgs) content.push({ type: 'image_url', image_url: { url: a.dataUrl } });
67
- return { role: 'user', content: content.length ? content : text };
68
- });
69
- }
70
-
71
- /**
72
- * Hand the model a MANIFEST of what is attached, and a tool to read it.
73
- *
74
- * Attachments used to be flattened into the first message — every attached tab, in full,
75
- * before the model had said anything. "hi" on a long page paid for the whole page, and five
76
- * attached tabs put five documents in the prompt to answer a question about one paragraph of
77
- * one of them.
78
- *
79
- * Two conditions, both necessary:
80
- * - THE TURN MUST CARRY TOOLS. Deferring content a model cannot then fetch does not save
81
- * tokens, it deletes the context — the worst possible outcome, and silently.
82
- * - IT MUST BE WORTH A ROUND TRIP. Below the threshold the extra call costs more than the
83
- * text it avoids, so small attachments still travel inline.
84
- *
85
- * Returns the rewritten messages plus a store, or null when nothing was deferred.
86
- */
87
- export function deferAttachedSources(messages, tools, { minTokens = 700 } = {}) {
88
- if (!tools?.specs?.length) return null;
89
- const carried = [];
90
- for (const m of messages || []) {
91
- for (const a of m?.attachments || []) {
92
- if (a?.kind === 'image' || !a?.text) continue;
93
- carried.push(a);
94
- }
95
- }
96
- if (!carried.length) return null;
97
- const store = makeSourceStore(carried.map((a) => ({
98
- kind: a.kind || 'context', title: a.title, url: a.url, text: a.text,
99
- })));
100
- if (store.tokens < minTokens) return null;
101
- // Same index, same id: the manifest and the store must agree or the model asks for
102
- // something real and is told it does not exist.
103
- const idFor = new Map(carried.map((a, i) => [a, store.entries[i]?.id]));
104
- const out = (messages || []).map((m) => {
105
- if (!m?.attachments?.some((a) => idFor.get(a))) return m;
106
- return {
107
- ...m,
108
- attachments: m.attachments.map((a) => {
109
- const id = idFor.get(a);
110
- // The stub keeps the title and url — knowing WHAT is attached is what lets the model
111
- // decide whether to read it, and that part is cheap.
112
- return id ? { ...a, text: `(not included — read with \`source\`: id ${id}, ~${approxTokens(a.text)} tokens)` } : a;
113
- }),
114
- };
115
- });
116
- return { messages: out, store };
117
- }
118
-
119
- export const SOURCE_TOOL_SPEC = {
120
- name: 'source',
121
- description: 'Read an attached source (a page, tab, selection or file the user attached). Their content is NOT in the conversation — read what you need from here.',
122
- parameters: {
123
- type: 'object',
124
- properties: {
125
- id: { type: 'string', description: 'The source id from the manifest, e.g. page-1.' },
126
- query: { type: 'string', description: 'What you are looking for. A large source returns the matching sections rather than its first page.' },
127
- },
128
- required: ['id'],
129
- },
130
- };
131
-
132
- /** Add `source` to an existing toolset without disturbing what is already there. */
133
- export function withSourceTool(tools, store) {
134
- const spec = { ...SOURCE_TOOL_SPEC };
135
- const system = [
136
- tools?.system,
137
- `${manifestText(store)}\n\nTheir content is NOT in this conversation. Call \`source\` with an id — and a query when the source is large — to read what you need.`,
138
- ].filter(Boolean).join('\n\n');
139
- return {
140
- ...tools,
141
- specs: [...(tools?.specs || []), spec],
142
- system,
143
- systemParts: { ...(tools?.systemParts || {}), source: approxTokens(manifestText(store)) },
144
- execute: async (name, input, meta) => (name === 'source'
145
- ? JSON.stringify(readSource(store, typeof input === 'string' ? JSON.parse(input || '{}') : (input || {})))
146
- : tools.execute(name, input, meta)),
147
- };
148
- }
12
+ `);return{...n,specs:[...n?.specs||[],i],system:e,systemParts:{...n?.systemParts||{},source:p(l(t))},execute:async(r,c,a)=>r==="source"?JSON.stringify(f(t,typeof c=="string"?JSON.parse(c||"{}"):c||{})):n.execute(r,c,a)}}export{m as SOURCE_TOOL_SPEC,$ as deferAttachedSources,g as imageAttachmentsOf,h as renderContent,y as toChatMessages,k as toMultimodalMessages,S as withSourceTool};