@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/tool-discovery.js
CHANGED
|
@@ -1,87 +1 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
// Narrowing keeps a turn's tool list short by ranking the connected tools against the
|
|
4
|
-
// message and showing the top few. It keeps the EXECUTE map whole on purpose — a tool the
|
|
5
|
-
// model knows the name of still runs — but nothing told the model the names it was not
|
|
6
|
-
// shown. So a tool that ranked low was, for that turn, gone: the model could not ask for
|
|
7
|
-
// it because it did not know to.
|
|
8
|
-
//
|
|
9
|
-
// This is the missing half. A `find` action searches the FULL set — every tool the group
|
|
10
|
-
// owns, narrowed or not — and answers with names and one-liners, cheap enough to sit on
|
|
11
|
-
// every dispatcher and cheap enough to call on a hunch. The model discovers, then calls;
|
|
12
|
-
// the menu stays short; the capability decision the cap used to make silently is now a
|
|
13
|
-
// call away. `describe` already gives the full schema for one action, so the two together
|
|
14
|
-
// are the pay-as-you-go ladder: names → one line → full schema, each on demand.
|
|
15
|
-
//
|
|
16
|
-
// Ranking is injected. The lexical, IDF-weighted ranker the extension and gateway share
|
|
17
|
-
// lives in @chatpanel/pii; this package must stay dependency-free, so it takes `rank` and
|
|
18
|
-
// falls back to a plain token overlap when a host has none.
|
|
19
|
-
|
|
20
|
-
export const FIND_ACTION = 'find';
|
|
21
|
-
|
|
22
|
-
const STOP = new Set(['the', 'and', 'for', 'with', 'that', 'this', 'use', 'can', 'you', 'your', 'from', 'what', 'how', 'are', 'get', 'find', 'tool', 'tools']);
|
|
23
|
-
|
|
24
|
-
const tokens = (s) => String(s || '').toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 2 && !STOP.has(w));
|
|
25
|
-
|
|
26
|
-
/** Token overlap — a fallback, not a ranker anyone should prefer. */
|
|
27
|
-
export function overlapRank(specs, query) {
|
|
28
|
-
const q = new Set(tokens(query));
|
|
29
|
-
if (!q.size) return [...specs];
|
|
30
|
-
return specs
|
|
31
|
-
.map((s, i) => {
|
|
32
|
-
const hay = `${s.name} ${s.description || ''}`.toLowerCase();
|
|
33
|
-
let n = 0;
|
|
34
|
-
for (const w of q) if (hay.includes(w)) n += 1;
|
|
35
|
-
return { s, i, n };
|
|
36
|
-
})
|
|
37
|
-
.sort((a, b) => (b.n - a.n) || (a.i - b.i))
|
|
38
|
-
.map((x) => x.s);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** First sentence, whitespace collapsed, capped — the "one line" of the ladder. */
|
|
42
|
-
export function oneLiner(description, max = 110) {
|
|
43
|
-
const s = String(description || '').replace(/\s+/g, ' ').trim();
|
|
44
|
-
const cut = s.search(/[.!?]\s|\n/);
|
|
45
|
-
const first = cut > 20 ? s.slice(0, cut + 1) : s;
|
|
46
|
-
return first.length > max ? `${first.slice(0, max - 1).trimEnd()}…` : first;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const requiredOf = (spec) => {
|
|
50
|
-
const p = spec?.parameters || spec?.inputSchema;
|
|
51
|
-
return Array.isArray(p?.required) ? p.required.map(String) : [];
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* @param specs the FULL set — not the narrowed menu
|
|
56
|
-
* @param rank `(specs, query) => specs` most-relevant first
|
|
57
|
-
* @returns `[{ name, summary, required }]`
|
|
58
|
-
*/
|
|
59
|
-
export function findTools(specs, query, { limit = 8, rank = overlapRank } = {}) {
|
|
60
|
-
const list = (specs || []).filter((s) => s && s.name);
|
|
61
|
-
const q = String(query || '').trim();
|
|
62
|
-
const ranked = q ? rank(list, q) : list;
|
|
63
|
-
const cap = Math.max(1, Math.min(50, Number(limit) || 8));
|
|
64
|
-
return ranked.slice(0, cap).map((s) => ({ name: s.name, summary: oneLiner(s.description), required: requiredOf(s) }));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** The JSON text a `find` action returns, with the next step spelled out. */
|
|
68
|
-
export function findToolsResult(specs, query, { limit, rank, describeAction = 'describe', menu = [] } = {}) {
|
|
69
|
-
const found = findTools(specs, query, { limit, rank });
|
|
70
|
-
const total = (specs || []).length;
|
|
71
|
-
const hidden = new Set(menu.map((m) => (typeof m === 'string' ? m : m?.name)));
|
|
72
|
-
return JSON.stringify({
|
|
73
|
-
query: String(query || ''),
|
|
74
|
-
matches: found.map((f) => ({ ...f, listed: hidden.size ? hidden.has(f.name) : undefined })),
|
|
75
|
-
total,
|
|
76
|
-
hint: found.length
|
|
77
|
-
? `Call one as {"action":"<name>","args":{…}}. Unsure of its arguments? {"action":"${describeAction}","args":{"tool":"<name>"}}.`
|
|
78
|
-
: `No tool matched "${query}" among ${total}. Try other words, or describe the task differently.`,
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** The parameter schema fragment a dispatcher advertises for `find`. */
|
|
83
|
-
export function findActionArgs() {
|
|
84
|
-
return {
|
|
85
|
-
query: { type: 'string', description: `With action="${FIND_ACTION}": words describing the task; returns matching tool names.` },
|
|
86
|
-
};
|
|
87
|
-
}
|
|
1
|
+
const d="find",l=new Set(["the","and","for","with","that","this","use","can","you","your","from","what","how","are","get","find","tool","tools"]),m=e=>String(e||"").toLowerCase().split(/[^a-z0-9]+/).filter(t=>t.length>2&&!l.has(t));function f(e,t){const o=new Set(m(t));return o.size?e.map((n,r)=>{const a=`${n.name} ${n.description||""}`.toLowerCase();let s=0;for(const c of o)a.includes(c)&&(s+=1);return{s:n,i:r,n:s}}).sort((n,r)=>r.n-n.n||n.i-r.i).map(n=>n.s):[...e]}function h(e,t=110){const o=String(e||"").replace(/\s+/g," ").trim(),n=o.search(/[.!?]\s|\n/),r=n>20?o.slice(0,n+1):o;return r.length>t?`${r.slice(0,t-1).trimEnd()}…`:r}const g=e=>{const t=e?.parameters||e?.inputSchema;return Array.isArray(t?.required)?t.required.map(String):[]};function p(e,t,{limit:o=8,rank:n=f}={}){const r=(e||[]).filter(i=>i&&i.name),a=String(t||"").trim(),s=a?n(r,a):r,c=Math.max(1,Math.min(50,Number(o)||8));return s.slice(0,c).map(i=>({name:i.name,summary:h(i.description),required:g(i)}))}function y(e,t,{limit:o,rank:n,describeAction:r="describe",menu:a=[]}={}){const s=p(e,t,{limit:o,rank:n}),c=(e||[]).length,i=new Set(a.map(u=>typeof u=="string"?u:u?.name));return JSON.stringify({query:String(t||""),matches:s.map(u=>({...u,listed:i.size?i.has(u.name):void 0})),total:c,hint:s.length?`Call one as {"action":"<name>","args":{…}}. Unsure of its arguments? {"action":"${r}","args":{"tool":"<name>"}}.`:`No tool matched "${t}" among ${c}. Try other words, or describe the task differently.`})}function S(){return{query:{type:"string",description:`With action="${d}": words describing the task; returns matching tool names.`}}}export{d as FIND_ACTION,S as findActionArgs,p as findTools,y as findToolsResult,h as oneLiner,f as overlapRank};
|
package/tool-dispatch.js
CHANGED
|
@@ -1,231 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
// Paid whether or not the turn touched any of them, and on a small local model that can eat
|
|
5
|
-
// half the context before the user has typed anything. So a group is registered as one
|
|
6
|
-
// compact tool carrying an action enum and a one-line gist each; the full schema for any
|
|
7
|
-
// action is REACHABLE via `{action:'describe', tool:'<name>'}`, and arguments are validated
|
|
8
|
-
// at execution with a structured error the model can act on.
|
|
9
|
-
//
|
|
10
|
-
// WHY A DISPATCHER RATHER THAN AN INDEX. Over MCP a model may only call tools that are
|
|
11
|
-
// REGISTERED; returning a schema from an index tool would not make the described tool
|
|
12
|
-
// callable. A dispatcher is one registered tool that can reach all of them, so the same
|
|
13
|
-
// mechanism works for a relayed CLI agent and for an in-client loop.
|
|
14
|
-
//
|
|
15
|
-
// The page dispatcher proved the shape and earned three bugs doing it (the stripped `args`
|
|
16
|
-
// envelope, the blinded loop guard, the unreadable activity rows). Every later group — the
|
|
17
|
-
// user's own data, MCP servers, and now the desktop's turn — goes through this instead of
|
|
18
|
-
// re-earning them. What a group supplies is only what is genuinely its own: a name, a
|
|
19
|
-
// sentence about when to reach for it, and whether its tools are remote.
|
|
1
|
+
import{FIND_ACTION as g,findToolsResult as A,findActionArgs as N}from"./tool-discovery.js";import{traitsIndex as q}from"./tool-traits.js";const m="describe";function O(e){const t=String(e.description||"").replace(/\s+/g," ").trim(),r=t.search(/(?<=[.!?])\s/),n=r>0?t.slice(0,r):t;return n.length>90?`${n.slice(0,87).trimEnd()}...`:n}function b(e){const t=e?.parameters?.required;return Array.isArray(t)?t:[]}function k(e){return e.map(t=>{const r=b(t);return`- ${t.name}${r.length?`(${r.join(", ")})`:"()"}: ${O(t)}`}).join(`
|
|
2
|
+
`)}function j({name:e,description:t,specs:r,hidden:n=0}){const c=n>0?`
|
|
3
|
+
${n} more action${n===1?"":"s"} not listed — {"action":"${g}","args":{"query":"<task words>"}} finds them by name.`:"";return{name:e,description:`${t}
|
|
20
4
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
export const DESCRIBE_ACTION = 'describe';
|
|
25
|
-
|
|
26
|
-
/** First sentence of a description — enough to choose an action, not to call it blind. */
|
|
27
|
-
function gistOf(spec) {
|
|
28
|
-
const text = String(spec.description || '').replace(/\s+/g, ' ').trim();
|
|
29
|
-
const stop = text.search(/(?<=[.!?])\s/);
|
|
30
|
-
const first = stop > 0 ? text.slice(0, stop) : text;
|
|
31
|
-
return first.length > 90 ? `${first.slice(0, 87).trimEnd()}...` : first;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function requiredOf(spec) {
|
|
35
|
-
const req = spec?.parameters?.required;
|
|
36
|
-
return Array.isArray(req) ? req : [];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** The action menu — one line per action, enough to choose but not to call blind. */
|
|
40
|
-
export function actionMenu(specs) {
|
|
41
|
-
return specs.map((s) => {
|
|
42
|
-
const req = requiredOf(s);
|
|
43
|
-
return `- ${s.name}${req.length ? `(${req.join(', ')})` : '()'}: ${gistOf(s)}`;
|
|
44
|
-
}).join('\n');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Build a dispatcher spec for ANY group of tools.
|
|
49
|
-
*
|
|
50
|
-
* @param hidden how many more actions the group can reach than the menu lists (a relevance
|
|
51
|
-
* cap trimmed it). When > 0 the spec says so and names the way back: `find`
|
|
52
|
-
* searches every tool the group owns, listed or not. Without that line a tool
|
|
53
|
-
* the cap dropped was, for that turn, gone.
|
|
54
|
-
*/
|
|
55
|
-
export function buildGroupDispatchSpec({ name, description, specs, hidden = 0 }) {
|
|
56
|
-
const more = hidden > 0
|
|
57
|
-
? `\n${hidden} more action${hidden === 1 ? '' : 's'} not listed — {"action":"${FIND_ACTION}","args":{"query":"<task words>"}} finds them by name.`
|
|
58
|
-
: '';
|
|
59
|
-
return {
|
|
60
|
-
name,
|
|
61
|
-
description: `${description}\n\nActions:\n${actionMenu(specs)}${more}`,
|
|
62
|
-
parameters: {
|
|
63
|
-
type: 'object',
|
|
64
|
-
properties: {
|
|
65
|
-
action: {
|
|
66
|
-
type: 'string',
|
|
67
|
-
enum: [DESCRIBE_ACTION, ...(hidden > 0 ? [FIND_ACTION] : []), ...specs.map((s) => s.name)],
|
|
68
|
-
description: 'Which action to run.',
|
|
69
|
-
},
|
|
70
|
-
// A DECLARED envelope, not `additionalProperties`. Providers and MCP validators
|
|
71
|
-
// routinely strip properties that are not in `properties`, so undeclared top-level
|
|
72
|
-
// arguments silently vanish before they reach the executor — which is exactly how
|
|
73
|
-
// `structured_insert` lost its `elements` array. Anything declared survives.
|
|
74
|
-
args: {
|
|
75
|
-
type: 'object',
|
|
76
|
-
description: 'The chosen action\'s own arguments, verbatim. Use {} when it takes none.',
|
|
77
|
-
additionalProperties: true,
|
|
78
|
-
},
|
|
79
|
-
tool: { type: 'string', description: `With action="${DESCRIBE_ACTION}": the action to describe.` },
|
|
80
|
-
...(hidden > 0 ? findActionArgs() : {}),
|
|
81
|
-
},
|
|
82
|
-
required: ['action'],
|
|
83
|
-
additionalProperties: true, // tolerated, but never relied upon — see `args`
|
|
84
|
-
},
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Validate arguments against the REAL spec. Returns null when fine, else a structured error
|
|
90
|
-
* naming exactly what is missing — a bounded repair path instead of a dead turn.
|
|
91
|
-
*/
|
|
92
|
-
export function validateAction(spec, args) {
|
|
93
|
-
const missing = requiredOf(spec).filter((k) => args[k] === undefined || args[k] === null);
|
|
94
|
-
if (!missing.length) return null;
|
|
95
|
-
return {
|
|
96
|
-
error: `Missing required argument(s) for "${spec.name}": ${missing.join(', ')}.`,
|
|
97
|
-
required: requiredOf(spec),
|
|
98
|
-
hint: `Put them inside \`args\`: {"action":"${spec.name}","args":{...}}. `
|
|
99
|
-
+ `Call {"action":"${DESCRIBE_ACTION}","args":{"tool":"${spec.name}"}} for the full schema.`,
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Route one dispatch call to the real per-action executor.
|
|
105
|
-
*
|
|
106
|
-
* `runAction(name, args, meta)` is the EXISTING guarded executor, so every confirmation gate,
|
|
107
|
-
* budget and site grant keeps firing on the real action name — the dispatcher must never
|
|
108
|
-
* become a way around them.
|
|
109
|
-
*
|
|
110
|
-
* @param specs the MENU — what the dispatcher lists
|
|
111
|
-
* @param all everything the group can reach; defaults to the menu. When larger, `find`
|
|
112
|
-
* searches it and any action in it runs, listed or not.
|
|
113
|
-
* @param rank `(specs, query) => specs` for `find`; the shared IDF ranker when given
|
|
114
|
-
*/
|
|
115
|
-
export function makeGroupDispatchExecutor({ name: dispatchName, specs, all = specs, runAction, rank }) {
|
|
116
|
-
const byName = new Map(all.map((s) => [s.name, s]));
|
|
117
|
-
for (const s of specs) byName.set(s.name, s); // the menu's copy wins a duplicate name
|
|
118
|
-
const menuNames = specs.map((s) => s.name);
|
|
119
|
-
return async (name, input, meta) => {
|
|
120
|
-
if (name !== dispatchName) return runAction(name, input, meta); // direct calls still work
|
|
121
|
-
// Accept BOTH shapes. `args` is the declared envelope and the one the description
|
|
122
|
-
// teaches; top-level arguments are merged too, so a model that ignores the envelope — or
|
|
123
|
-
// a provider that happens to pass extras through — still works rather than failing in a
|
|
124
|
-
// way that looks like the tool is broken.
|
|
125
|
-
const raw = input || {};
|
|
126
|
-
const { action: rawAction, args: envelope, tool: rawTool, ...rest } = raw;
|
|
127
|
-
const args = { ...rest, ...(envelope && typeof envelope === 'object' ? envelope : {}) };
|
|
128
|
-
const action = String(rawAction || '');
|
|
129
|
-
|
|
130
|
-
if (action === FIND_ACTION) {
|
|
131
|
-
return findToolsResult(all, String(args.query ?? rawTool ?? ''), { rank, describeAction: DESCRIBE_ACTION, menu: menuNames });
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (action === DESCRIBE_ACTION) {
|
|
135
|
-
const spec = byName.get(String(args.tool || rawTool || ''));
|
|
136
|
-
return JSON.stringify(
|
|
137
|
-
spec
|
|
138
|
-
? {
|
|
139
|
-
name: spec.name,
|
|
140
|
-
// The full contract when the menu carried a compressed one (tool-schema.js).
|
|
141
|
-
description: spec.full?.description || spec.description,
|
|
142
|
-
parameters: spec.full?.parameters || spec.parameters,
|
|
143
|
-
...(spec.annotations ? { annotations: spec.annotations } : {}),
|
|
144
|
-
callAs: { action: spec.name, args: '<the properties above, verbatim>' },
|
|
145
|
-
}
|
|
146
|
-
: { error: `Unknown action "${args.tool || rawTool}".`, actions: [...byName.keys()] },
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const spec = byName.get(action);
|
|
151
|
-
if (!spec) {
|
|
152
|
-
return JSON.stringify({
|
|
153
|
-
error: `Unknown action "${action}".`,
|
|
154
|
-
actions: menuNames,
|
|
155
|
-
...(all.length > specs.length ? { hint: `${all.length - specs.length} more are reachable: {"action":"${FIND_ACTION}","args":{"query":"…"}} finds them.` } : {}),
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
const bad = validateAction(spec, args);
|
|
159
|
-
if (bad) return JSON.stringify(bad);
|
|
160
|
-
return runAction(action, args, meta);
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Attach a group's detailed guidance to `describe` instead of the prompt. The model reads it
|
|
166
|
-
* at the moment it is about to act on it — which is when it is most likely to follow it —
|
|
167
|
-
* and a turn that never reaches for the group never pays for it.
|
|
168
|
-
*/
|
|
169
|
-
export function withGuidance(execute, guidance) {
|
|
170
|
-
if (!guidance) return execute;
|
|
171
|
-
return async (name, input, meta) => {
|
|
172
|
-
const out = await execute(name, input, meta);
|
|
173
|
-
if (String(input?.action || '') !== DESCRIBE_ACTION) return out;
|
|
174
|
-
try {
|
|
175
|
-
const parsed = JSON.parse(out);
|
|
176
|
-
if (!parsed || !parsed.name) return out;
|
|
177
|
-
return JSON.stringify({ ...parsed, guidance });
|
|
178
|
-
} catch {
|
|
179
|
-
return out;
|
|
180
|
-
}
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Turn a toolset into ONE provider — the reusable half of progressive disclosure.
|
|
186
|
-
*
|
|
187
|
-
* @param inner a toolset ({ specs, execute, system }) — the real tools, kept whole.
|
|
188
|
-
* @param resident the ONE line that stays in the prompt. Everything else the group wants to
|
|
189
|
-
* say travels with `describe`.
|
|
190
|
-
* @param remote true when these tools call a third party. This is load-bearing for
|
|
191
|
-
* PRIVACY, not bookkeeping: the harness uses it to keep PII off remote tools
|
|
192
|
-
* under "redact remote". A dispatcher that lost the flag would quietly turn
|
|
193
|
-
* redacted tools into unredacted ones.
|
|
194
|
-
* @param all every spec the group can reach when the menu (`inner.specs`) is a
|
|
195
|
-
* relevance-capped subset. `find` searches it; any action in it runs.
|
|
196
|
-
* @param rank the ranker `find` uses — the shared IDF one, so discovery agrees with the
|
|
197
|
-
* narrowing that hid the tool in the first place.
|
|
198
|
-
*/
|
|
199
|
-
export function makeDispatchProvider({ name, description, resident, inner, remote = false, all = null, rank = undefined }) {
|
|
200
|
-
if (!inner || !inner.specs?.length) return null;
|
|
201
|
-
const specs = inner.specs;
|
|
202
|
-
const reach = all && all.length > specs.length ? all : specs;
|
|
203
|
-
return {
|
|
204
|
-
specs: [buildGroupDispatchSpec({ name, specs, description, hidden: reach.length - specs.length })],
|
|
205
|
-
system: resident,
|
|
206
|
-
remote,
|
|
207
|
-
// What each REAL tool does to the world (annotations, else its name) — read by the round
|
|
208
|
-
// runner through the dispatcher, which otherwise hides every inner spec.
|
|
209
|
-
traits: traitsIndex(reach),
|
|
210
|
-
// …and WHICH tools are behind this name, so a recipe step can name the real tool and be
|
|
211
|
-
// routed through the dispatcher (buildToolset builds `hiddenVia` from it).
|
|
212
|
-
reach,
|
|
213
|
-
execute: withGuidance(
|
|
214
|
-
makeGroupDispatchExecutor({
|
|
215
|
-
name,
|
|
216
|
-
specs,
|
|
217
|
-
all: reach,
|
|
218
|
-
rank,
|
|
219
|
-
// Routes on the REAL tool name so every guard, budget and gate downstream keeps
|
|
220
|
-
// firing on the name it was written against.
|
|
221
|
-
runAction: (toolName, args, meta) => inner.execute(toolName, args, meta),
|
|
222
|
-
}),
|
|
223
|
-
inner.system,
|
|
224
|
-
),
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/** Rough token estimate — used by budget tests, not at runtime. */
|
|
229
|
-
export function estimateTokens(value) {
|
|
230
|
-
return Math.round(JSON.stringify(value).length / 4);
|
|
231
|
-
}
|
|
5
|
+
Actions:
|
|
6
|
+
${k(r)}${c}`,parameters:{type:"object",properties:{action:{type:"string",enum:[m,...n>0?[g]:[],...r.map(i=>i.name)],description:"Which action to run."},args:{type:"object",description:"The chosen action's own arguments, verbatim. Use {} when it takes none.",additionalProperties:!0},tool:{type:"string",description:`With action="${m}": the action to describe.`},...n>0?N():{}},required:["action"],additionalProperties:!0}}}function v(e,t){const r=b(e).filter(n=>t[n]===void 0||t[n]===null);return r.length?{error:`Missing required argument(s) for "${e.name}": ${r.join(", ")}.`,required:b(e),hint:`Put them inside \`args\`: {"action":"${e.name}","args":{...}}. Call {"action":"${m}","args":{"tool":"${e.name}"}} for the full schema.`}:null}function J({name:e,specs:t,all:r=t,runAction:n,rank:c}){const i=new Map(r.map(o=>[o.name,o]));for(const o of t)i.set(o.name,o);const a=t.map(o=>o.name);return async(o,u,l)=>{if(o!==e)return n(o,u,l);const h=u||{},{action:d,args:y,tool:$,...w}=h,f={...w,...y&&typeof y=="object"?y:{}},p=String(d||"");if(p===g)return A(r,String(f.query??$??""),{rank:c,describeAction:m,menu:a});if(p===m){const s=i.get(String(f.tool||$||""));return JSON.stringify(s?{name:s.name,description:s.full?.description||s.description,parameters:s.full?.parameters||s.parameters,...s.annotations?{annotations:s.annotations}:{},callAs:{action:s.name,args:"<the properties above, verbatim>"}}:{error:`Unknown action "${f.tool||$}".`,actions:[...i.keys()]})}const S=i.get(p);if(!S)return JSON.stringify({error:`Unknown action "${p}".`,actions:a,...r.length>t.length?{hint:`${r.length-t.length} more are reachable: {"action":"${g}","args":{"query":"…"}} finds them.`}:{}});const x=v(S,f);return x?JSON.stringify(x):n(p,f,l)}}function T(e,t){return t?async(r,n,c)=>{const i=await e(r,n,c);if(String(n?.action||"")!==m)return i;try{const a=JSON.parse(i);return!a||!a.name?i:JSON.stringify({...a,guidance:t})}catch{return i}}:e}function C({name:e,description:t,resident:r,inner:n,remote:c=!1,all:i=null,rank:a=void 0}){if(!n||!n.specs?.length)return null;const o=n.specs,u=i&&i.length>o.length?i:o;return{specs:[j({name:e,specs:o,description:t,hidden:u.length-o.length})],system:r,remote:c,traits:q(u),reach:u,execute:T(J({name:e,specs:o,all:u,rank:a,runAction:(l,h,d)=>n.execute(l,h,d)}),n.system)}}function E(e){return Math.round(JSON.stringify(e).length/4)}export{m as DESCRIBE_ACTION,k as actionMenu,j as buildGroupDispatchSpec,E as estimateTokens,C as makeDispatchProvider,J as makeGroupDispatchExecutor,v as validateAction,T as withGuidance};
|
package/tool-groups.js
CHANGED
|
@@ -1,81 +1 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
// The extension assembles a turn's toolset from three hardcoded blocks: the user's own
|
|
4
|
-
// data, their MCP servers, and the page. Each block is the same shape — decide whether it
|
|
5
|
-
// applies, build a provider, collapse it behind a dispatcher — written out three times, so
|
|
6
|
-
// a fourth means editing a shared function and no other client can contribute one at all.
|
|
7
|
-
//
|
|
8
|
-
// The failure this prevents is not hypothetical. A duplicated search-engine list in two
|
|
9
|
-
// files produced a retired engine that kept reappearing in settings; two implementations of
|
|
10
|
-
// one decision disagree eventually. A registry makes that unrepresentable: there is one
|
|
11
|
-
// list, and it is the registrations.
|
|
12
|
-
//
|
|
13
|
-
// A group DECIDES and BUILDS; it does not know about the others. Ordering is explicit
|
|
14
|
-
// priority rather than call order, because "which tools does the model see first" should be
|
|
15
|
-
// a stated decision and not an accident of where someone added a line.
|
|
16
|
-
|
|
17
|
-
export class ToolGroupError extends Error {
|
|
18
|
-
constructor(code, message) { super(message); this.name = 'ToolGroupError'; this.code = code; }
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* @param applies (ctx) => boolean — cheap, synchronous, no side effects. Kept separate from
|
|
23
|
-
* `build` so "should this be offered" can be answered without paying to construct
|
|
24
|
-
* it: MCP construction connects to servers, and asking that question should not.
|
|
25
|
-
* @param build async (ctx) => provider | null. Returning null is normal (nothing
|
|
26
|
-
* configured), not an error.
|
|
27
|
-
*/
|
|
28
|
-
export function defineToolGroup({ id, label, applies, build, priority = 0 }) {
|
|
29
|
-
if (!id) throw new ToolGroupError('BAD_GROUP', 'group.id required');
|
|
30
|
-
if (typeof build !== 'function') throw new ToolGroupError('BAD_GROUP', `group '${id}': build required`);
|
|
31
|
-
return Object.freeze({
|
|
32
|
-
id,
|
|
33
|
-
label: label || id,
|
|
34
|
-
priority,
|
|
35
|
-
applies: typeof applies === 'function' ? applies : () => true,
|
|
36
|
-
build,
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function createToolGroupRegistry() {
|
|
41
|
-
const groups = [];
|
|
42
|
-
return {
|
|
43
|
-
add(group) {
|
|
44
|
-
groups.push(group);
|
|
45
|
-
return () => { const i = groups.indexOf(group); if (i >= 0) groups.splice(i, 1); };
|
|
46
|
-
},
|
|
47
|
-
|
|
48
|
-
list: () => [...groups].sort((a, b) => b.priority - a.priority),
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Build every group that applies, in priority order.
|
|
52
|
-
*
|
|
53
|
-
* Groups are built CONCURRENTLY because one of them connects to remote servers and
|
|
54
|
-
* serialising would add its latency to every turn — but the RESULT is re-sorted by
|
|
55
|
-
* priority, so the order the model sees never depends on which finished first.
|
|
56
|
-
*
|
|
57
|
-
* A group that throws is dropped with a report, not propagated: a broken MCP server
|
|
58
|
-
* must not cost the user their history tools. Same isolation rule as the source
|
|
59
|
-
* registry and the adapter registry, for the same reason.
|
|
60
|
-
*/
|
|
61
|
-
async build(ctx, { onError = () => {}, admit = null } = {}) {
|
|
62
|
-
const eligible = this.list().filter((g) => {
|
|
63
|
-
// Admission is checked BEFORE `applies`, and both before `build`. A group the user
|
|
64
|
-
// switched off must not do its work and be discarded afterwards — for MCP that work
|
|
65
|
-
// is connecting to servers, which is what once made a first turn wait 45 seconds.
|
|
66
|
-
if (admit && !admit(g)) return false;
|
|
67
|
-
try { return !!g.applies(ctx); } catch (e) { onError(g.id, e); return false; }
|
|
68
|
-
});
|
|
69
|
-
const built = await Promise.all(eligible.map(async (g) => {
|
|
70
|
-
try { return { id: g.id, priority: g.priority, provider: await g.build(ctx) }; } catch (e) {
|
|
71
|
-
onError(g.id, e);
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
}));
|
|
75
|
-
return built
|
|
76
|
-
.filter((b) => b && b.provider)
|
|
77
|
-
.sort((a, b) => b.priority - a.priority)
|
|
78
|
-
.map((b) => ({ id: b.id, provider: b.provider }));
|
|
79
|
-
},
|
|
80
|
-
};
|
|
81
|
-
}
|
|
1
|
+
class p extends Error{constructor(t,e){super(e),this.name="ToolGroupError",this.code=t}}function s({id:i,label:t,applies:e,build:o,priority:u=0}){if(!i)throw new p("BAD_GROUP","group.id required");if(typeof o!="function")throw new p("BAD_GROUP",`group '${i}': build required`);return Object.freeze({id:i,label:t||i,priority:u,applies:typeof e=="function"?e:()=>!0,build:o})}function c(){const i=[];return{add(t){return i.push(t),()=>{const e=i.indexOf(t);e>=0&&i.splice(e,1)}},list:()=>[...i].sort((t,e)=>e.priority-t.priority),async build(t,{onError:e=()=>{},admit:o=null}={}){const u=this.list().filter(r=>{if(o&&!o(r))return!1;try{return!!r.applies(t)}catch(n){return e(r.id,n),!1}});return(await Promise.all(u.map(async r=>{try{return{id:r.id,priority:r.priority,provider:await r.build(t)}}catch(n){return e(r.id,n),null}}))).filter(r=>r&&r.provider).sort((r,n)=>n.priority-r.priority).map(r=>({id:r.id,provider:r.provider}))}}}export{p as ToolGroupError,c as createToolGroupRegistry,s as defineToolGroup};
|
package/tool-hints.js
CHANGED
|
@@ -1,185 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return 'Cite these sources inline with <sup>[1]</sup> and add a bottom "Sources" list with labels and links: reuse any "Open in ChatPanel" link, derive a canonical URL from a returned ID (e.g. Wikipedia page ID → https://en.wikipedia.org/?curid=<id>), else give the ID/label. Never guess a URL you cannot derive.';
|
|
12
|
-
}
|
|
13
|
-
return [
|
|
14
|
-
'Source citation policy:',
|
|
15
|
-
'When your answer uses any attached, retrieved, searched, or tool-provided source, including MCP tools and history/search tools, cite the relevant claim inline with superscript markers like <sup>[1]</sup>.',
|
|
16
|
-
'Finish with a "Sources" section listing each cited source once. Match the numbering used in the answer.',
|
|
17
|
-
'For each source, include the best available title/name and URL/link. A ChatPanel history/note/meeting/chat result already carries an "Open in ChatPanel" chrome-extension:// link — reuse that exact link so the user can jump straight to the item.',
|
|
18
|
-
'If a tool returns a stable identifier for a known public source instead of a URL (e.g. a Wikipedia page ID, DOI, arXiv/PubMed ID, or a slug), build the canonical URL from it — for a Wikipedia page ID use https://en.wikipedia.org/?curid=<id> — and cite that. Deterministically constructing a URL from an ID the tool returned is derivation, not fabrication.',
|
|
19
|
-
'Only when no URL is present AND none can be derived, fall back to the source ID, page ID, tool name, search result label, or file/meeting/chat label so the user can still find it.',
|
|
20
|
-
'Do not invent sources, page IDs, or titles, and never guess a URL you cannot derive from a returned identifier.',
|
|
21
|
-
'If you did not use sources beyond general reasoning, omit the Sources section.',
|
|
22
|
-
].join('\n');
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function toolStatus(result) {
|
|
26
|
-
const o = resultObject(result);
|
|
27
|
-
if (!o) {
|
|
28
|
-
// A plain-text result (a search's prose, a relayed agent's line): an error when it says
|
|
29
|
-
// so the way the shared tools do — `error: …`, `web_search failed: …` — else fine.
|
|
30
|
-
const s = typeof result === 'string' ? result : (result && typeof result === 'object' && typeof result.text === 'string' ? result.text : '');
|
|
31
|
-
if (!s.trim()) return '';
|
|
32
|
-
return /^(error:|\w+ failed\b)/i.test(s) ? `error: ${s.slice(0, 80)}` : 'ok';
|
|
33
|
-
}
|
|
34
|
-
if (o.error) {
|
|
35
|
-
const detail = errorDetail(o);
|
|
36
|
-
if (o.blocked) return `blocked: ${detail}`.slice(0, 90);
|
|
37
|
-
return `error: ${detail}`.slice(0, 90);
|
|
38
|
-
}
|
|
39
|
-
if (typeof o.mode === 'string') return o.mode;
|
|
40
|
-
if (o.ok === false) return `fail${o.error ? ': ' + String(o.error).slice(0, 70) : ''}`;
|
|
41
|
-
if (o.note) return String(o.note).slice(0, 80);
|
|
42
|
-
return 'ok';
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// The GENERIC MCP guidance + citation policy — identical for every server, so emit
|
|
46
|
-
// it ONCE per turn (see buildToolset), not once per connected server. Previously
|
|
47
|
-
// this rode inside every mcpInventorySystem() block, so N servers repeated it N
|
|
48
|
-
// times and bloated the prompt by thousands of tokens.
|
|
49
|
-
export function mcpSharedSystem() {
|
|
50
|
-
return [
|
|
51
|
-
'One or more MCP servers are connected in ChatPanel for this conversation (each listed below with its callable tools).',
|
|
52
|
-
'Use their MCP tools directly when the user asks for matching data or actions. Do not ask the user to configure or discover these tools if they are listed.',
|
|
53
|
-
'Do not call MCP tools when the attached page or provided context is enough to answer; summarize or analyze that context directly.',
|
|
54
|
-
'Prefer relevant MCP tools over web search for their domain. If an MCP tool fails, state the exact tool error first, then say whether you are falling back to another source.',
|
|
55
|
-
"Match the user's request domain to the tool's domain: Hacker News requests should use Hacker News tools, Confluence requests should use Confluence tools, and Jira requests should use Jira tools.",
|
|
56
|
-
'Form arguments precisely: pass ONLY the specific entity, identifier, or minimal distinctive keywords the tool needs — not the full sentence, greetings, pleasantries, or unrelated personal details from the conversation. For lookup-by-title/ID tools pass the exact canonical name or ID (e.g. title "Seattle", not "which state is Seattle in"); for search tools pass the fewest keywords that uniquely identify the target. Fill every required argument and follow each argument\'s schema description.',
|
|
57
|
-
"If you don't know the exact title or ID a lookup tool requires, first call the matching search/list tool to resolve it, then call the lookup — never guess identifiers.",
|
|
58
|
-
'Do not retry the exact same failed tool call. Re-check the listed inputs, choose a better matching tool, or answer with the tool error.',
|
|
59
|
-
'When MCP or search results inform the answer, include inline citations and a bottom Sources section; do not wait for the user to ask for links.',
|
|
60
|
-
sourceCitationSystem(),
|
|
61
|
-
].join('\n');
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Per-server block: ONLY the server-specific inventory (name + callable tool names
|
|
65
|
-
// + a short guide). The generic rules/citation live once in mcpSharedSystem().
|
|
66
|
-
export function mcpInventorySystem(serverName, specs = []) {
|
|
67
|
-
if (!specs.length) return '';
|
|
68
|
-
const title = String(serverName || 'MCP').trim() || 'MCP';
|
|
69
|
-
const names = specs.map((s) => s.name).filter(Boolean);
|
|
70
|
-
const shownNames = names.slice(0, 80).join(', ');
|
|
71
|
-
const moreNames = names.length > 80 ? `, and ${names.length - 80} more` : '';
|
|
72
|
-
const useful = specs.slice(0, 18).map(toolLine).filter(Boolean).join('\n');
|
|
73
|
-
return [
|
|
74
|
-
`MCP server "${title}" — callable tools: ${shownNames}${moreNames}.`,
|
|
75
|
-
useful ? `Tool guide:\n${useful}` : '',
|
|
76
|
-
].filter(Boolean).join('\n');
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function resultObject(result) {
|
|
80
|
-
if (typeof result === 'string') return parseJson(result);
|
|
81
|
-
if (!result || typeof result !== 'object') return null;
|
|
82
|
-
if (typeof result.text === 'string') return parseJson(result.text) || result;
|
|
83
|
-
return result;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function parseJson(s) {
|
|
87
|
-
try {
|
|
88
|
-
return JSON.parse(s);
|
|
89
|
-
} catch {
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function errorDetail(o) {
|
|
95
|
-
const detail =
|
|
96
|
-
o.message ||
|
|
97
|
-
o.detail ||
|
|
98
|
-
o.error_description ||
|
|
99
|
-
o.refusal_reason ||
|
|
100
|
-
o.retry_hint ||
|
|
101
|
-
o.error;
|
|
102
|
-
if (typeof detail === 'string') return detail;
|
|
103
|
-
try {
|
|
104
|
-
return JSON.stringify(detail);
|
|
105
|
-
} catch {
|
|
106
|
-
return String(detail);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function toolLine(spec) {
|
|
111
|
-
const desc = cleanDescription(spec.description || spec.name || '');
|
|
112
|
-
const inputs = inputNames(spec.parameters);
|
|
113
|
-
const inputText = inputs.length ? ` inputs: ${inputs.join(', ')}` : '';
|
|
114
|
-
return `- ${spec.name}${inputText}: ${desc}`.slice(0, 260);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function cleanDescription(desc) {
|
|
118
|
-
return String(desc)
|
|
119
|
-
.replace(/^\[[^\]]+\]\s*/, '')
|
|
120
|
-
.replace(/\s+/g, ' ')
|
|
121
|
-
.trim()
|
|
122
|
-
.slice(0, 180);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function inputNames(schema = {}) {
|
|
126
|
-
const props = schema?.properties || {};
|
|
127
|
-
const required = new Set(schema?.required || []);
|
|
128
|
-
return Object.keys(props)
|
|
129
|
-
.slice(0, 6)
|
|
130
|
-
.map((name) => required.has(name) ? `${name}*` : name);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* How to build something the user can KEEP.
|
|
135
|
-
*
|
|
136
|
-
* Nobody should have to know an API to ask for a timer. The user says "make me a pomodoro
|
|
137
|
-
* timer"; the model is the one that should know ChatPanel renders a single HTML file in a
|
|
138
|
-
* sandbox and offers to keep it, and that a widget which remembers anything must save it
|
|
139
|
-
* through `chatpanel.setState` — because nothing else in the sandbox persists.
|
|
140
|
-
*
|
|
141
|
-
* Kept short on purpose: it rides on every turn that could produce HTML, so it earns its
|
|
142
|
-
* tokens by being the difference between a widget that forgets on close and one that doesn't.
|
|
143
|
-
*/
|
|
144
|
-
export function widgetAuthoringSystem() {
|
|
145
|
-
return [
|
|
146
|
-
'Building small apps (widgets):',
|
|
147
|
-
'When the user asks for a small self-contained tool — a timer, calculator, converter, sticky note, tracker, checklist, dice, scoreboard — answer with ONE ```html block containing a complete, self-contained file (inline CSS/JS, no external requests). ChatPanel runs it in a sandbox and offers a "+ Keep" button that saves it as a permanent widget in the user\'s panel.',
|
|
148
|
-
'If the widget should remember anything between visits — elapsed time, notes, a tally, settings — persist it with the ChatPanel widget API, which is injected automatically:',
|
|
149
|
-
' await chatpanel.getState() // returns what was saved, or null the first time',
|
|
150
|
-
' await chatpanel.setState(value) // save any JSON value',
|
|
151
|
-
'Load state on start and save it whenever it changes. It works in the chat preview too, not only once kept. localStorage is backed by the same store, but indexedDB and cookies THROW in the sandbox — never use them.',
|
|
152
|
-
'The sandbox has NO network access and no access to the user\'s data, so build the widget to work entirely offline unless the user has granted it a capability.',
|
|
153
|
-
'Keep it compact and readable at ~320px wide (the panel is narrow), and give it sensible defaults so it is useful the moment it appears.',
|
|
154
|
-
'Give the file a <title> — it becomes the widget\'s name, and its icon is chosen from that name (a "Pomodoro Timer" gets a timer, "Standup Notes" a notepad).',
|
|
155
|
-
].join('\n');
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* How to keep a secret, told ONLY when the turn is about one.
|
|
160
|
-
*
|
|
161
|
-
* The widget guidance above rides on every turn that could produce HTML and is already at
|
|
162
|
-
* its budget, and this is three times the size of the line it would replace. It is also
|
|
163
|
-
* irrelevant to a pomodoro timer. So it is gated on the user actually asking for something
|
|
164
|
-
* that holds credentials — which is cheap, deterministic, and wrong only in the direction
|
|
165
|
-
* that costs nothing (a missed hint means a widget that stores a password in plain state,
|
|
166
|
-
* and the vault is still there for the next attempt).
|
|
167
|
-
*/
|
|
168
|
-
const SECRETY = /\b(password|passphrase|secret|vault|credential|api[- ]?key|token|pin code|private note|login|2fa|seed phrase)/i;
|
|
169
|
-
|
|
170
|
-
export function wantsVaultGuidance(userText) {
|
|
171
|
-
return SECRETY.test(String(userText || ''));
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export function vaultWidgetSystem() {
|
|
175
|
-
return [
|
|
176
|
-
'Secrets in a widget (passwords, keys, private notes) must NOT go in state — state is plain JSON on disk. Use the encrypted vault, which is keyed by a passphrase ChatPanel never stores:',
|
|
177
|
-
' <meta name="chatpanel-requests" content="vault.status, vault.unlock, vault.list, vault.add, vault.reveal">',
|
|
178
|
-
' await chatpanel.invoke("vault.status") // { exists, locked, entries }',
|
|
179
|
-
' await chatpanel.invoke("vault.unlock") // ChatPanel asks for the passphrase itself',
|
|
180
|
-
' await chatpanel.invoke("vault.add", { title, note, secret })',
|
|
181
|
-
' await chatpanel.invoke("vault.list", { query }) // titles only, never secrets',
|
|
182
|
-
' await chatpanel.invoke("vault.reveal", { id }) // ChatPanel asks the user each time',
|
|
183
|
-
'Never build your own passphrase box and never keep the secret in a variable longer than you need it. The user approves these once, when they keep the widget, so any call can throw — catch it and say so in the UI. Build the locked state first: a vault UI that assumes it is unlocked is wrong most of the time.',
|
|
184
|
-
].join('\n');
|
|
185
|
-
}
|
|
1
|
+
function g(...e){return e.map(t=>String(t||"").trim()).filter(Boolean).join(`
|
|
2
|
+
|
|
3
|
+
`)}function c({compact:e=!1}={}){return e?'Cite these sources inline with <sup>[1]</sup> and add a bottom "Sources" list with labels and links: reuse any "Open in ChatPanel" link, derive a canonical URL from a returned ID (e.g. Wikipedia page ID → https://en.wikipedia.org/?curid=<id>), else give the ID/label. Never guess a URL you cannot derive.':["Source citation policy:","When your answer uses any attached, retrieved, searched, or tool-provided source, including MCP tools and history/search tools, cite the relevant claim inline with superscript markers like <sup>[1]</sup>.",'Finish with a "Sources" section listing each cited source once. Match the numbering used in the answer.','For each source, include the best available title/name and URL/link. A ChatPanel history/note/meeting/chat result already carries an "Open in ChatPanel" chrome-extension:// link — reuse that exact link so the user can jump straight to the item.',"If a tool returns a stable identifier for a known public source instead of a URL (e.g. a Wikipedia page ID, DOI, arXiv/PubMed ID, or a slug), build the canonical URL from it — for a Wikipedia page ID use https://en.wikipedia.org/?curid=<id> — and cite that. Deterministically constructing a URL from an ID the tool returned is derivation, not fabrication.","Only when no URL is present AND none can be derived, fall back to the source ID, page ID, tool name, search result label, or file/meeting/chat label so the user can still find it.","Do not invent sources, page IDs, or titles, and never guess a URL you cannot derive from a returned identifier.","If you did not use sources beyond general reasoning, omit the Sources section."].join(`
|
|
4
|
+
`)}function w(e){const t=u(e);if(!t){const n=typeof e=="string"?e:e&&typeof e=="object"&&typeof e.text=="string"?e.text:"";return n.trim()?/^(error:|\w+ failed\b)/i.test(n)?`error: ${n.slice(0,80)}`:"ok":""}if(t.error){const n=h(t);return t.blocked?`blocked: ${n}`.slice(0,90):`error: ${n}`.slice(0,90)}return typeof t.mode=="string"?t.mode:t.ok===!1?`fail${t.error?": "+String(t.error).slice(0,70):""}`:t.note?String(t.note).slice(0,80):"ok"}function y(){return["One or more MCP servers are connected in ChatPanel for this conversation (each listed below with its callable tools).","Use their MCP tools directly when the user asks for matching data or actions. Do not ask the user to configure or discover these tools if they are listed.","Do not call MCP tools when the attached page or provided context is enough to answer; summarize or analyze that context directly.","Prefer relevant MCP tools over web search for their domain. If an MCP tool fails, state the exact tool error first, then say whether you are falling back to another source.","Match the user's request domain to the tool's domain: Hacker News requests should use Hacker News tools, Confluence requests should use Confluence tools, and Jira requests should use Jira tools.",`Form arguments precisely: pass ONLY the specific entity, identifier, or minimal distinctive keywords the tool needs — not the full sentence, greetings, pleasantries, or unrelated personal details from the conversation. For lookup-by-title/ID tools pass the exact canonical name or ID (e.g. title "Seattle", not "which state is Seattle in"); for search tools pass the fewest keywords that uniquely identify the target. Fill every required argument and follow each argument's schema description.`,"If you don't know the exact title or ID a lookup tool requires, first call the matching search/list tool to resolve it, then call the lookup — never guess identifiers.","Do not retry the exact same failed tool call. Re-check the listed inputs, choose a better matching tool, or answer with the tool error.","When MCP or search results inform the answer, include inline citations and a bottom Sources section; do not wait for the user to ask for links.",c()].join(`
|
|
5
|
+
`)}function v(e,t=[]){if(!t.length)return"";const n=String(e||"MCP").trim()||"MCP",a=t.map(l=>l.name).filter(Boolean),r=a.slice(0,80).join(", "),s=a.length>80?`, and ${a.length-80} more`:"",i=t.slice(0,18).map(d).filter(Boolean).join(`
|
|
6
|
+
`);return[`MCP server "${n}" — callable tools: ${r}${s}.`,i?`Tool guide:
|
|
7
|
+
${i}`:""].filter(Boolean).join(`
|
|
8
|
+
`)}function u(e){return typeof e=="string"?o(e):!e||typeof e!="object"?null:typeof e.text=="string"&&o(e.text)||e}function o(e){try{return JSON.parse(e)}catch{return null}}function h(e){const t=e.message||e.detail||e.error_description||e.refusal_reason||e.retry_hint||e.error;if(typeof t=="string")return t;try{return JSON.stringify(t)}catch{return String(t)}}function d(e){const t=p(e.description||e.name||""),n=f(e.parameters),a=n.length?` inputs: ${n.join(", ")}`:"";return`- ${e.name}${a}: ${t}`.slice(0,260)}function p(e){return String(e).replace(/^\[[^\]]+\]\s*/,"").replace(/\s+/g," ").trim().slice(0,180)}function f(e={}){const t=e?.properties||{},n=new Set(e?.required||[]);return Object.keys(t).slice(0,6).map(a=>n.has(a)?`${a}*`:a)}function k(){return["Building small apps (widgets):",'When the user asks for a small self-contained tool — a timer, calculator, converter, sticky note, tracker, checklist, dice, scoreboard — answer with ONE ```html block containing a complete, self-contained file (inline CSS/JS, no external requests). ChatPanel runs it in a sandbox and offers a "+ Keep" button that saves it as a permanent widget in the user\'s panel.',"If the widget should remember anything between visits — elapsed time, notes, a tally, settings — persist it with the ChatPanel widget API, which is injected automatically:"," await chatpanel.getState() // returns what was saved, or null the first time"," await chatpanel.setState(value) // save any JSON value","Load state on start and save it whenever it changes. It works in the chat preview too, not only once kept. localStorage is backed by the same store, but indexedDB and cookies THROW in the sandbox — never use them.","The sandbox has NO network access and no access to the user's data, so build the widget to work entirely offline unless the user has granted it a capability.","Keep it compact and readable at ~320px wide (the panel is narrow), and give it sensible defaults so it is useful the moment it appears.",`Give the file a <title> — it becomes the widget's name, and its icon is chosen from that name (a "Pomodoro Timer" gets a timer, "Standup Notes" a notepad).`].join(`
|
|
9
|
+
`)}const m=/\b(password|passphrase|secret|vault|credential|api[- ]?key|token|pin code|private note|login|2fa|seed phrase)/i;function b(e){return m.test(String(e||""))}function S(){return["Secrets in a widget (passwords, keys, private notes) must NOT go in state — state is plain JSON on disk. Use the encrypted vault, which is keyed by a passphrase ChatPanel never stores:",' <meta name="chatpanel-requests" content="vault.status, vault.unlock, vault.list, vault.add, vault.reveal">',' await chatpanel.invoke("vault.status") // { exists, locked, entries }',' await chatpanel.invoke("vault.unlock") // ChatPanel asks for the passphrase itself',' await chatpanel.invoke("vault.add", { title, note, secret })',' await chatpanel.invoke("vault.list", { query }) // titles only, never secrets',' await chatpanel.invoke("vault.reveal", { id }) // ChatPanel asks the user each time',"Never build your own passphrase box and never keep the secret in a variable longer than you need it. The user approves these once, when they keep the widget, so any call can throw — catch it and say so in the UI. Build the locked state first: a vault UI that assumes it is unlocked is wrong most of the time."].join(`
|
|
10
|
+
`)}export{g as combineSystemPrompt,v as mcpInventorySystem,y as mcpSharedSystem,c as sourceCitationSystem,w as toolStatus,S as vaultWidgetSystem,b as wantsVaultGuidance,k as widgetAuthoringSystem};
|