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