@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-loop-guard.js
CHANGED
|
@@ -1,184 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
// Lived inside the extension's providers.js for a year, which meant the desktop could not
|
|
4
|
-
// use it and wrote a smaller one (a `seen` map and a repeat count) that answered the same
|
|
5
|
-
// model behaviour differently: the extension blocked a repeated write BEFORE running it,
|
|
6
|
-
// the desktop ran it once and replayed it; the extension noticed a whole round repeating,
|
|
7
|
-
// the desktop only a single call. Same tools, same model, two outcomes. Now one guard,
|
|
8
|
-
// with everything either client had learned:
|
|
9
|
-
//
|
|
10
|
-
// • a call repeated past `maxIdenticalCalls` is not executed — a READ is answered from
|
|
11
|
-
// its first result (a pure read asked twice has one answer, and refusing it is how a
|
|
12
|
-
// small model concludes the tool is broken and invents an answer); a WRITE is refused
|
|
13
|
-
// with a result that says why, because replaying a click would be a lie about
|
|
14
|
-
// something that changed the world;
|
|
15
|
-
// • observation tools never count — read → act → read again with the same empty input
|
|
16
|
-
// is correct, not a loop;
|
|
17
|
-
// • a discrete-input tool that SUCCEEDED (a keystroke, a click) is progress and clears
|
|
18
|
-
// its own count — pressing Enter twice is normal; failing to press it twice is not;
|
|
19
|
-
// • a ROUND that repeats the previous round byte for byte, or in which every call was
|
|
20
|
-
// blocked, counts toward `stalled` — and a stalled turn is offered no more tools, so
|
|
21
|
-
// the model has to answer with what it has;
|
|
22
|
-
// • `repeats` counts every replay and refusal across the turn, so a loop can tell when
|
|
23
|
-
// the model has been told enough times (the desktop's rule: three, then a closing
|
|
24
|
-
// request with no tools).
|
|
25
|
-
//
|
|
26
|
-
// Class R: no I/O, no clock. Names are read through `effectiveToolName` so a dispatched
|
|
27
|
-
// action is judged on what it is, not on the dispatcher's name.
|
|
1
|
+
import{effectiveToolName as f}from"./tool-traits.js";import{resultText as h}from"./adaptive-tool-policy.js";const x=3,A=2,T=3,S=new Set(["inspect_page","read_canvas","screenshot","marked_screenshot"]),k=new Set(["press_key","type_text","click_at","move_mouse","click_mark","draw_path","input_sequence","click_element","click_by_text"]);function m(e){return!S.has(e)&&!k.has(e)&&e!=="scroll"}function d(e){return e==null||typeof e!="object"?JSON.stringify(e):Array.isArray(e)?`[${e.map(d).join(",")}]`:`{${Object.keys(e).sort().map(t=>`${JSON.stringify(t)}:${d(e[t])}`).join(",")}}`}function g(e,t){return`${String(e||"")}
|
|
2
|
+
${d(t??{})}`}function w(e){return(Array.isArray(e)?e:[]).filter(t=>m(f(t?.name,t?.input))).map(t=>g(t.name,t.input)).sort().join("|")}function O(e,t,i={}){return JSON.stringify({ok:!1,blocked:!0,error:"tool_loop_blocked",tool:e||"tool",message:t,retry_hint:"Answer using the already available conversation context and tool results. Do not call more tools unless the user asks you to continue.",...i})}function N(e,t,i=null){const l=f(e,i);if(l==="scroll")try{return JSON.parse(h(t))?.atBottom===!1}catch{return!1}if(k.has(l))try{return JSON.parse(h(t))?.ok===!0}catch{return!1}return!1}function R({maxIdenticalCalls:e=x,maxStalledRounds:t=A,maxRepeats:i=T}={}){const l=new Map,p=new Map;let c=0,y=null,u=0;return{get disabled(){return!1},get stalled(){return c>=t},get repeats(){return u},get looping(){return u>=i},noteRound(o,a,n=""){const r=a>0&&o>=a,s=!!n&&n===y;y=n,s?c+=2:r?c+=1:c=0},reset(o){o&&l.delete(o)},remember(o,a,n,r,{readOnly:s=!1}={}){!o||!r||!s||p.set(o,r)},check(o,a){if(S.has(f(o,a)))return{blocked:!1};const n=g(o,a),r=(l.get(n)||0)+1;if(l.set(n,r),r>e&&p.has(n)){u+=1;const s=p.get(n),_="[This exact call was already made this turn; the result is unchanged. Answer from what you have.]",b=typeof s=="string"?`${s}
|
|
28
3
|
|
|
29
|
-
|
|
30
|
-
import { resultText } from './adaptive-tool-policy.js';
|
|
4
|
+
${_}`:s&&typeof s=="object"&&typeof s.text=="string"?{...s,text:`${s.text}
|
|
31
5
|
|
|
32
|
-
export
|
|
33
|
-
export const DEFAULT_MAX_STALLED_ROUNDS = 2;
|
|
34
|
-
export const DEFAULT_MAX_REPEATS = 3;
|
|
35
|
-
|
|
36
|
-
// Observation/read tools are MEANT to be repeated with the SAME (empty) input.
|
|
37
|
-
export const OBSERVATION_TOOLS = new Set(['inspect_page', 'read_canvas', 'screenshot', 'marked_screenshot']);
|
|
38
|
-
|
|
39
|
-
// Tools whose whole job is ONE discrete physical input. A SUCCESSFUL application counts as
|
|
40
|
-
// progress and clears the repeat count; a failing one (unknown key, nothing at point) does
|
|
41
|
-
// not, so a genuinely stuck call still trips the guard.
|
|
42
|
-
export const INPUT_PROGRESS_TOOLS = new Set([
|
|
43
|
-
'press_key', 'type_text', 'click_at', 'move_mouse', 'click_mark', 'draw_path', 'input_sequence',
|
|
44
|
-
'click_element', 'click_by_text',
|
|
45
|
-
]);
|
|
46
|
-
|
|
47
|
-
/** A tool whose repetition signals a LOOP (search/query/fetch), not one meant to repeat. */
|
|
48
|
-
export function isLoopableTool(name) {
|
|
49
|
-
return !OBSERVATION_TOOLS.has(name) && !INPUT_PROGRESS_TOOLS.has(name) && name !== 'scroll';
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function stableStringify(value) {
|
|
53
|
-
if (value == null || typeof value !== 'object') return JSON.stringify(value);
|
|
54
|
-
if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
|
|
55
|
-
return `{${Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(',')}}`;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** The identity of a call: its name and its arguments with keys in a stable order. */
|
|
59
|
-
export function stableToolCallKey(name, input) {
|
|
60
|
-
return `${String(name || '')}\n${stableStringify(input ?? {})}`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** The identity of a ROUND: its loopable calls, sorted — so a re-read or a scroll never makes two rounds look alike. */
|
|
64
|
-
export function roundSignature(calls) {
|
|
65
|
-
return (Array.isArray(calls) ? calls : [])
|
|
66
|
-
.filter((c) => isLoopableTool(effectiveToolName(c?.name, c?.input)))
|
|
67
|
-
.map((c) => stableToolCallKey(c.name, c.input))
|
|
68
|
-
.sort()
|
|
69
|
-
.join('|');
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** The result a refused repeat receives — machine-readable, with the way out spelled out. */
|
|
73
|
-
export function blockedToolResult(name, message, extra = {}) {
|
|
74
|
-
return JSON.stringify({
|
|
75
|
-
ok: false,
|
|
76
|
-
blocked: true,
|
|
77
|
-
error: 'tool_loop_blocked',
|
|
78
|
-
tool: name || 'tool',
|
|
79
|
-
message,
|
|
80
|
-
retry_hint: 'Answer using the already available conversation context and tool results. Do not call more tools unless the user asks you to continue.',
|
|
81
|
-
...extra,
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Did a repeatable tool actually do something? For `scroll`, "more page below"; for a
|
|
87
|
-
* discrete input, `ok: true`. Anything else is not progress — the step cap is the backstop.
|
|
88
|
-
*/
|
|
89
|
-
export function toolMadeProgress(name, result, input = null) {
|
|
90
|
-
// Through the dispatcher too: `page {action:'scroll'}` is a scroll. Judged by the bare
|
|
91
|
-
// name, four scrolls through `page` looked like a stuck loop and were blocked.
|
|
92
|
-
const eff = effectiveToolName(name, input);
|
|
93
|
-
if (eff === 'scroll') {
|
|
94
|
-
try { return JSON.parse(resultText(result))?.atBottom === false; } catch { return false; }
|
|
95
|
-
}
|
|
96
|
-
if (INPUT_PROGRESS_TOOLS.has(eff)) {
|
|
97
|
-
try { return JSON.parse(resultText(result))?.ok === true; } catch { return false; }
|
|
98
|
-
}
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* @param maxIdenticalCalls how many times the SAME call runs before it is replayed or refused
|
|
104
|
-
* @param maxStalledRounds how many no-progress rounds in a row before `stalled`
|
|
105
|
-
* @param maxRepeats how many replays/refusals in a turn before `looping`
|
|
106
|
-
*/
|
|
107
|
-
export function createToolLoopGuard({
|
|
108
|
-
maxIdenticalCalls = DEFAULT_MAX_IDENTICAL_CALLS,
|
|
109
|
-
maxStalledRounds = DEFAULT_MAX_STALLED_ROUNDS,
|
|
110
|
-
maxRepeats = DEFAULT_MAX_REPEATS,
|
|
111
|
-
} = {}) {
|
|
112
|
-
const counts = new Map();
|
|
113
|
-
const lastResult = new Map(); // key → what that identical call returned the first time
|
|
114
|
-
let stalledRounds = 0;
|
|
115
|
-
let lastSignature = null;
|
|
116
|
-
let repeats = 0;
|
|
117
|
-
|
|
118
|
-
return {
|
|
119
|
-
// No nuclear per-turn kill switch — one looping tool must not disable the rest. The
|
|
120
|
-
// round cap is the overall backstop.
|
|
121
|
-
get disabled() { return false; },
|
|
122
|
-
get stalled() { return stalledRounds >= maxStalledRounds; },
|
|
123
|
-
/** Replays and refusals so far this turn. */
|
|
124
|
-
get repeats() { return repeats; },
|
|
125
|
-
/** The model has been answered "you already asked that" enough times to stop asking. */
|
|
126
|
-
get looping() { return repeats >= maxRepeats; },
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* After each round, note progress. No progress = every call was blocked, OR the round's
|
|
130
|
-
* loopable call-set is byte-identical to the previous round's (a loop even before the
|
|
131
|
-
* per-tool threshold trips). An exact-repeat round is definitive — two strikes at once.
|
|
132
|
-
*/
|
|
133
|
-
noteRound(blockedCount, total, signature = '') {
|
|
134
|
-
const allBlocked = total > 0 && blockedCount >= total;
|
|
135
|
-
const repeatRound = !!signature && signature === lastSignature;
|
|
136
|
-
lastSignature = signature;
|
|
137
|
-
if (repeatRound) stalledRounds += 2;
|
|
138
|
-
else if (allBlocked) stalledRounds += 1;
|
|
139
|
-
else stalledRounds = 0;
|
|
140
|
-
},
|
|
141
|
-
|
|
142
|
-
/** Clear a call's repeat count when it actually made progress. */
|
|
143
|
-
reset(key) { if (key) counts.delete(key); },
|
|
144
|
-
|
|
145
|
-
/** Remember what a READ returned, so a repeat can be answered instead of refused. */
|
|
146
|
-
remember(key, name, input, result, { readOnly = false } = {}) {
|
|
147
|
-
if (!key || !result || !readOnly) return;
|
|
148
|
-
lastResult.set(key, result);
|
|
149
|
-
},
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Should this call run? `{ blocked, replayed, count, key, result }` — `result` is what to
|
|
153
|
-
* answer with when it should not.
|
|
154
|
-
*/
|
|
155
|
-
check(name, input) {
|
|
156
|
-
if (OBSERVATION_TOOLS.has(effectiveToolName(name, input))) return { blocked: false };
|
|
157
|
-
const key = stableToolCallKey(name, input);
|
|
158
|
-
const count = (counts.get(key) || 0) + 1;
|
|
159
|
-
counts.set(key, count);
|
|
160
|
-
if (count > maxIdenticalCalls && lastResult.has(key)) {
|
|
161
|
-
// Serve the answer it already earned — and say so, because a model that repeats
|
|
162
|
-
// itself is usually waiting for a value that will not change. Still counted, so a
|
|
163
|
-
// genuinely stuck loop stays visible in the log.
|
|
164
|
-
repeats += 1;
|
|
165
|
-
const prior = lastResult.get(key);
|
|
166
|
-
const note = '[This exact call was already made this turn; the result is unchanged. Answer from what you have.]';
|
|
167
|
-
const result = typeof prior === 'string' ? `${prior}\n\n${note}` : (prior && typeof prior === 'object' && typeof prior.text === 'string' ? { ...prior, text: `${prior.text}\n\n${note}` } : prior);
|
|
168
|
-
return { blocked: false, replayed: true, count, key, result };
|
|
169
|
-
}
|
|
170
|
-
if (count > maxIdenticalCalls) {
|
|
171
|
-
repeats += 1;
|
|
172
|
-
return {
|
|
173
|
-
blocked: true, count, key,
|
|
174
|
-
result: blockedToolResult(
|
|
175
|
-
name,
|
|
176
|
-
`Skipped a repeated identical ${name || 'tool'} call (${count}× with the same input). Vary the input or try a different action — your other tools still work.`,
|
|
177
|
-
{ repeated: true, identicalCallCount: count, maxIdenticalCalls },
|
|
178
|
-
),
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
return { blocked: false, count, key };
|
|
182
|
-
},
|
|
183
|
-
};
|
|
184
|
-
}
|
|
6
|
+
${_}`}:s;return{blocked:!1,replayed:!0,count:r,key:n,result:b}}return r>e?(u+=1,{blocked:!0,count:r,key:n,result:O(o,`Skipped a repeated identical ${o||"tool"} call (${r}× with the same input). Vary the input or try a different action — your other tools still work.`,{repeated:!0,identicalCallCount:r,maxIdenticalCalls:e})}):{blocked:!1,count:r,key:n}}}}export{x as DEFAULT_MAX_IDENTICAL_CALLS,T as DEFAULT_MAX_REPEATS,A as DEFAULT_MAX_STALLED_ROUNDS,k as INPUT_PROGRESS_TOOLS,S as OBSERVATION_TOOLS,O as blockedToolResult,R as createToolLoopGuard,m as isLoopableTool,w as roundSignature,g as stableToolCallKey,N as toolMadeProgress};
|
package/tool-need.js
CHANGED
|
@@ -1,124 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
// Every turn was armed with the same equipment regardless of what was said. "hi" arrived at
|
|
4
|
-
// the model carrying a history dispatcher, an MCP dispatcher and ~1,200 tokens of rulebook
|
|
5
|
-
// explaining how to use them, and that cost more than the prompt itself. That is not only
|
|
6
|
-
// waste, it CHANGES THE ANSWER: a turn that carries tools requires a model that can call
|
|
7
|
-
// them (see requirementsFor), so a greeting eliminated every model without the tools
|
|
8
|
-
// capability and then paid a CLI agent two seconds to spawn a process in order to wave back.
|
|
9
|
-
//
|
|
10
|
-
// Equipment is not demand. The question "what does this turn need" has to be asked of the
|
|
11
|
-
// MESSAGE, before the toolset exists — which is what the router already does for model
|
|
12
|
-
// choice, from the same signals, for free.
|
|
13
|
-
//
|
|
14
|
-
// THE ERROR BIAS IS THE OPPOSITE OF THE ROUTER'S, which is why this is not simply
|
|
15
|
-
// `signals.smalltalk`. Mis-routing a turn produces a worse answer; withholding the history
|
|
16
|
-
// tools from "what did we decide in the standup" produces "I cannot access your meetings" —
|
|
17
|
-
// wrong, and the exact thing the tool system prompt exists to prevent. So this does not try
|
|
18
|
-
// to detect which turns need tools. It recognises the narrow class of turns that provably
|
|
19
|
-
// cannot — pleasantries, and nothing else — and arms everything otherwise.
|
|
20
|
-
//
|
|
21
|
-
// Recognised by VOCABULARY rather than by absence: every word must be a conversational
|
|
22
|
-
// move. An unknown word, a typo, a name, a question — anything at all — falls through to
|
|
23
|
-
// "arm the tools", which is the safe direction. `smalltalk` still has to agree, so the two
|
|
24
|
-
// definitions of trivial cannot drift apart.
|
|
25
|
-
//
|
|
26
|
-
// Class R: no model call, no network, no I/O. Reading a string.
|
|
27
|
-
|
|
28
|
-
import { signalsFrom } from './router.js';
|
|
29
|
-
|
|
30
|
-
// Greetings, thanks, acknowledgements, farewells — and the filler that attaches to them
|
|
31
|
-
// ("hey there", "ok got it", "thanks so much"). Deliberately small: every addition widens
|
|
32
|
-
// the set of turns that get no tools, so a word earns its place by being unable to appear
|
|
33
|
-
// in a request.
|
|
34
|
-
const PLEASANTRY = new Set([
|
|
35
|
-
'hi', 'hii', 'hiya', 'hey', 'heya', 'hello', 'helo', 'yo', 'sup', 'howdy', 'greetings',
|
|
36
|
-
'good', 'morning', 'afternoon', 'evening', 'night', 'day',
|
|
37
|
-
'thanks', 'thank', 'thankyou', 'thx', 'tnx', 'ty', 'cheers', 'appreciated',
|
|
38
|
-
'ok', 'okay', 'k', 'kk', 'got', 'sounds', 'perfect', 'great', 'cool', 'nice', 'awesome',
|
|
39
|
-
'lol', 'haha', 'hah', 'hehe', 'nvm', 'yep', 'yeah', 'yes', 'no', 'nope', 'sure',
|
|
40
|
-
'bye', 'goodbye', 'later', 'ya', 'cya', 'ciao', 'welcome', 'worries', 'problem', 'np',
|
|
41
|
-
'please', 'there', 'again', 'all', 'everyone', 'team', 'friend', 'mate', 'buddy',
|
|
42
|
-
'you', 'u', 'so', 'much', 'very', 'well', 'and', 'a', 'the',
|
|
43
|
-
]);
|
|
44
|
-
|
|
45
|
-
// At most a short phrase. A long message built entirely from these words is not a greeting,
|
|
46
|
-
// it is something this rule does not understand — and not understanding means arm the tools.
|
|
47
|
-
const MAX_PLEASANTRY_WORDS = 6;
|
|
48
|
-
|
|
49
|
-
function isPleasantry(text) {
|
|
50
|
-
// Strip emoji, punctuation and digits: "hi!! 👋" is "hi". Anything left must be a word in
|
|
51
|
-
// the vocabulary.
|
|
52
|
-
const words = String(text || '')
|
|
53
|
-
.toLowerCase()
|
|
54
|
-
.replace(/[^\p{L}\s']/gu, ' ')
|
|
55
|
-
.split(/\s+/)
|
|
56
|
-
.filter(Boolean);
|
|
57
|
-
if (!words.length || words.length > MAX_PLEASANTRY_WORDS) return false;
|
|
58
|
-
return words.every((w) => PLEASANTRY.has(w));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* @param request { text } | { messages } — the turn, as the router already reads it.
|
|
63
|
-
* @param signals precomputed signalsFrom(request), when the caller already has them.
|
|
64
|
-
* @param attachments anything the user attached; presence alone means there is material.
|
|
65
|
-
* @param explicit the user or a skill ASKED for tools this turn (MCP mode 'on', the
|
|
66
|
-
* /history hint, a running skill). Never second-guessed.
|
|
67
|
-
* @returns { tools, why } — `tools: false` means build nothing at all this turn.
|
|
68
|
-
*/
|
|
69
|
-
export function toolNeedFor({ request = null, signals = null, attachments = [], explicit = false } = {}) {
|
|
70
|
-
if (explicit) return { tools: true, why: 'the turn asked for tools' };
|
|
71
|
-
// MATERIAL THE USER HANDED OVER — not the tab that happens to be open.
|
|
72
|
-
//
|
|
73
|
-
// The side panel auto-attaches the current page to every send, so "hi" on a search results
|
|
74
|
-
// page arrived carrying an attachment and armed the full toolset: three tools, ~2,300
|
|
75
|
-
// tokens and a page read, to say hello back. The open tab is METADATA about where the user
|
|
76
|
-
// is, not content they asked about; treating the two the same made the ambient page defeat
|
|
77
|
-
// this rule on every turn where it mattered.
|
|
78
|
-
//
|
|
79
|
-
// `auto` marks it. A page the user genuinely attached has no such mark and still counts —
|
|
80
|
-
// and a message that ASKS about the page ("summarise this") is not a pleasantry anyway, so
|
|
81
|
-
// it arms tools by the ordinary path rather than by what happens to be attached.
|
|
82
|
-
if ((attachments || []).some((a) => a && !a.auto)) {
|
|
83
|
-
return { tools: true, why: 'the turn carries an attachment' };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const text = String(request?.text ?? (request?.messages || []).map((m) => m?.content || '').join('\n'));
|
|
87
|
-
if (!isPleasantry(text)) return { tools: true, why: 'the request may need something fetched' };
|
|
88
|
-
|
|
89
|
-
// And the router has to agree it is trivial. Two definitions of "asks for nothing" that
|
|
90
|
-
// can disagree is the duplication this codebase keeps removing; requiring both means the
|
|
91
|
-
// stricter one always wins.
|
|
92
|
-
const sig = signals || signalsFrom(request || {});
|
|
93
|
-
if (!sig.smalltalk) return { tools: true, why: 'the request may need something fetched' };
|
|
94
|
-
|
|
95
|
-
return { tools: false, why: 'a greeting — nothing to look up' };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// ── Which GRANTS a task's wording calls for (F8 §15.2) ──────────────────────────────────
|
|
99
|
-
//
|
|
100
|
-
// A team member holding `web` that answers a "latest price" question from memory has not
|
|
101
|
-
// done the task. This is the deterministic half of the tool-choice guard: read the task's
|
|
102
|
-
// text for the vocabulary that names a source — the web, the person's own history, attached
|
|
103
|
-
// material — and return the grants that vocabulary fits. The runner nudges a member ONCE
|
|
104
|
-
// when it ends with zero calls while holding one of these. Conservative by construction:
|
|
105
|
-
// nothing here fires on a task that could plausibly be answered from what the model knows.
|
|
106
|
-
const NEEDS = Object.freeze([
|
|
107
|
-
['web', /\b(search|google|look ?up|latest|current|recent|today|this (week|month|year)|news|price|prices|pricing|cost of|quote|stock|market|website|url|online|web|docs?umentation|release notes|changelog|versions?)\b/i],
|
|
108
|
-
['history', /\b(our (meeting|call|notes?|chats?|conversation)|what (did|was) (we|i)|we (decided|agreed|discussed|said)|in (my|our) (notes?|meetings?|history|chats?)|past (chats?|meetings?|notes?)|earlier (meeting|conversation|chat|note)|transcript|standup|retro)\b/i],
|
|
109
|
-
['data', /\b(attached|attachment|this (page|document|file|pdf|spreadsheet|sheet)|the (document|file|pdf|spreadsheet) (above|provided|attached))\b/i],
|
|
110
|
-
]);
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* The grants a task's text calls for, in the order they fit: `['web']`, `['history', 'web']`,
|
|
114
|
-
* `[]`. `held` narrows to what the role actually has, so the caller gets what is BOTH needed
|
|
115
|
-
* and available — an empty list means no nudge.
|
|
116
|
-
*/
|
|
117
|
-
export function grantsNeededFor(text, { held = null } = {}) {
|
|
118
|
-
const t = String(text || '');
|
|
119
|
-
if (!t.trim()) return [];
|
|
120
|
-
const out = NEEDS.filter(([, re]) => re.test(t)).map(([g]) => g);
|
|
121
|
-
if (!held) return out;
|
|
122
|
-
const have = new Set((Array.isArray(held) ? held : []).map((g) => String(g).toLowerCase()));
|
|
123
|
-
return out.filter((g) => have.has(g) || (g === 'history' && have.has('data')));
|
|
124
|
-
}
|
|
1
|
+
import{signalsFrom as i}from"./router.js";const h=new Set(["hi","hii","hiya","hey","heya","hello","helo","yo","sup","howdy","greetings","good","morning","afternoon","evening","night","day","thanks","thank","thankyou","thx","tnx","ty","cheers","appreciated","ok","okay","k","kk","got","sounds","perfect","great","cool","nice","awesome","lol","haha","hah","hehe","nvm","yep","yeah","yes","no","nope","sure","bye","goodbye","later","ya","cya","ciao","welcome","worries","problem","np","please","there","again","all","everyone","team","friend","mate","buddy","you","u","so","much","very","well","and","a","the"]),l=6;function c(o){const e=String(o||"").toLowerCase().replace(/[^\p{L}\s']/gu," ").split(/\s+/).filter(Boolean);return!e.length||e.length>l?!1:e.every(n=>h.has(n))}function g({request:o=null,signals:e=null,attachments:n=[],explicit:r=!1}={}){if(r)return{tools:!0,why:"the turn asked for tools"};if((n||[]).some(a=>a&&!a.auto))return{tools:!0,why:"the turn carries an attachment"};const s=String(o?.text??(o?.messages||[]).map(a=>a?.content||"").join(`
|
|
2
|
+
`));return c(s)?(e||i(o||{})).smalltalk?{tools:!1,why:"a greeting — nothing to look up"}:{tools:!0,why:"the request may need something fetched"}:{tools:!0,why:"the request may need something fetched"}}const u=Object.freeze([["web",/\b(search|google|look ?up|latest|current|recent|today|this (week|month|year)|news|price|prices|pricing|cost of|quote|stock|market|website|url|online|web|docs?umentation|release notes|changelog|versions?)\b/i],["history",/\b(our (meeting|call|notes?|chats?|conversation)|what (did|was) (we|i)|we (decided|agreed|discussed|said)|in (my|our) (notes?|meetings?|history|chats?)|past (chats?|meetings?|notes?)|earlier (meeting|conversation|chat|note)|transcript|standup|retro)\b/i],["data",/\b(attached|attachment|this (page|document|file|pdf|spreadsheet|sheet)|the (document|file|pdf|spreadsheet) (above|provided|attached))\b/i]]);function y(o,{held:e=null}={}){const n=String(o||"");if(!n.trim())return[];const r=u.filter(([,t])=>t.test(n)).map(([t])=>t);if(!e)return r;const s=new Set((Array.isArray(e)?e:[]).map(t=>String(t).toLowerCase()));return r.filter(t=>s.has(t)||t==="history"&&s.has("data"))}export{y as grantsNeededFor,g as toolNeedFor};
|