@chatpanel/events 0.102.0 → 0.104.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +4 -5
- package/pdf-layout.js +7 -253
- package/policy.js +1 -0
- 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 -442
- 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/voice-speaker.js
CHANGED
|
@@ -1,98 +1 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
// In a voice conversation every finalized sentence is SENT, so anything the microphone hears
|
|
4
|
-
// becomes a question: a television, a colleague at the next desk, someone answering their own
|
|
5
|
-
// phone behind you. The engine transcribes them all perfectly and correctly, and the
|
|
6
|
-
// assistant answers the room.
|
|
7
|
-
//
|
|
8
|
-
// The gateway already computes a 512-d speaker fingerprint per committed segment and clusters
|
|
9
|
-
// it into a stable label for the session (`diarize-engine.js`). All that is missing is the
|
|
10
|
-
// DECISION, which is this file: the first voice in a conversation is the person who started
|
|
11
|
-
// it, and later sentences from a different voice are not their turn.
|
|
12
|
-
//
|
|
13
|
-
// THE FAILURE DIRECTION MATTERS MORE THAN THE FEATURE. An assistant that occasionally answers
|
|
14
|
-
// the television is annoying; one that ignores YOU is broken, and from the outside the two
|
|
15
|
-
// look identical — a mic that is open and going nowhere. So every uncertain case sends:
|
|
16
|
-
//
|
|
17
|
-
// · no speaker on the final (diarization off, model missing, embedding failed) → send;
|
|
18
|
-
// · no primary enrolled yet → this is the first voice, enroll it and send;
|
|
19
|
-
// · the primary has not been heard for a long time → adopt whoever is talking now and
|
|
20
|
-
// send, because the phone may have been handed over, or the first voice may have been
|
|
21
|
-
// the television while the user was drawing breath.
|
|
22
|
-
//
|
|
23
|
-
// Only one case holds: a DIFFERENT voice, while the person having the conversation is still
|
|
24
|
-
// in it. That is the case the user asked for and the only one we can be confident about.
|
|
25
|
-
//
|
|
26
|
-
// The engine's own honesty note applies here too: embeddings separate different speakers
|
|
27
|
-
// well, but similar voices can merge — so this gate can let a very similar voice through. It
|
|
28
|
-
// never claims to be security, only to keep the room out of the conversation.
|
|
29
|
-
|
|
30
|
-
/** How long the primary must be silent before another voice may take over the conversation. */
|
|
31
|
-
export const RE_ENROLL_MS = 45_000;
|
|
32
|
-
|
|
33
|
-
/** A label the gateway pins to the microphone channel; never a guess, so always the primary. */
|
|
34
|
-
export const PINNED_SELF = 'You';
|
|
35
|
-
|
|
36
|
-
const labelOf = (speaker) => {
|
|
37
|
-
if (!speaker) return '';
|
|
38
|
-
if (typeof speaker === 'string') return speaker;
|
|
39
|
-
return String(speaker.label || speaker.id || '');
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* A per-conversation speaker gate.
|
|
44
|
-
*
|
|
45
|
-
* @param {object} [opts]
|
|
46
|
-
* @param {number} [opts.reEnrollMs] silence after which another voice may take over
|
|
47
|
-
* @param {() => number} [opts.now] injected clock, so the rules are testable without waiting
|
|
48
|
-
*/
|
|
49
|
-
export function createSpeakerGate({ reEnrollMs = RE_ENROLL_MS, now = Date.now } = {}) {
|
|
50
|
-
let primary = ''; // the label of the person having this conversation
|
|
51
|
-
let lastHeard = 0; // when the primary last said something
|
|
52
|
-
let held = 0; // sentences kept out, for the UI to report honestly
|
|
53
|
-
|
|
54
|
-
return {
|
|
55
|
-
/** The enrolled voice, or '' before anyone has spoken. */
|
|
56
|
-
primary: () => primary,
|
|
57
|
-
/** How many sentences this gate has held back. */
|
|
58
|
-
heldCount: () => held,
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Should this finalized sentence become a turn?
|
|
62
|
-
*
|
|
63
|
-
* @param {{ speaker?: any }} [final]
|
|
64
|
-
* @returns {{ send: boolean, reason: 'no-speaker'|'enrolled'|'primary'|'adopted'|'other', speaker: string }}
|
|
65
|
-
*/
|
|
66
|
-
admit(final = {}) {
|
|
67
|
-
const label = labelOf(final.speaker);
|
|
68
|
-
// Diarization is optional and fails open — a sentence with no speaker is always sent.
|
|
69
|
-
if (!label) return { send: true, reason: 'no-speaker', speaker: '' };
|
|
70
|
-
|
|
71
|
-
const t = now();
|
|
72
|
-
if (!primary) {
|
|
73
|
-
primary = label;
|
|
74
|
-
lastHeard = t;
|
|
75
|
-
return { send: true, reason: 'enrolled', speaker: label };
|
|
76
|
-
}
|
|
77
|
-
if (label === primary) {
|
|
78
|
-
lastHeard = t;
|
|
79
|
-
return { send: true, reason: 'primary', speaker: label };
|
|
80
|
-
}
|
|
81
|
-
// A different voice. Only take over when the conversation has clearly moved on.
|
|
82
|
-
if (t - lastHeard >= reEnrollMs) {
|
|
83
|
-
primary = label;
|
|
84
|
-
lastHeard = t;
|
|
85
|
-
return { send: true, reason: 'adopted', speaker: label };
|
|
86
|
-
}
|
|
87
|
-
held += 1;
|
|
88
|
-
return { send: false, reason: 'other', speaker: label };
|
|
89
|
-
},
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Forget who was talking. Called when a conversation starts, and by a user who wants the
|
|
93
|
-
* gate to hear them again — a voice it merged or mislabelled must be recoverable without
|
|
94
|
-
* ending the session.
|
|
95
|
-
*/
|
|
96
|
-
reset() { primary = ''; lastHeard = 0; held = 0; },
|
|
97
|
-
};
|
|
98
|
-
}
|
|
1
|
+
const i=45e3,d="You",l=r=>r?typeof r=="string"?r:String(r.label||r.id||""):"";function p({reEnrollMs:r=45e3,now:s=Date.now}={}){let t="",n=0,o=0;return{primary:()=>t,heldCount:()=>o,admit(u={}){const e=l(u.speaker);if(!e)return{send:!0,reason:"no-speaker",speaker:""};const a=s();return t?e===t?(n=a,{send:!0,reason:"primary",speaker:e}):a-n>=r?(t=e,n=a,{send:!0,reason:"adopted",speaker:e}):(o+=1,{send:!1,reason:"other",speaker:e}):(t=e,n=a,{send:!0,reason:"enrolled",speaker:e})},reset(){t="",n=0,o=0}}}export{d as PINNED_SELF,i as RE_ENROLL_MS,p as createSpeakerGate};
|
package/weather-tool.js
CHANGED
|
@@ -1,65 +1 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
// The extension answers "how is the weather in X" with this rather than with a web search,
|
|
4
|
-
// because weather sites are rendered by scripts and a fetched page is navigation and no
|
|
5
|
-
// temperature — which is exactly what a desktop turn without this tool produced: "I couldn't
|
|
6
|
-
// retrieve reliable live weather". The engine (`weather.js`: the wttr.in query, the parse,
|
|
7
|
-
// the ambiguity check) is already shared; this is the tool every client arms in front of it.
|
|
8
|
-
//
|
|
9
|
-
// `fetchJson(url, { timeoutMs })` is injected: each host owns its network guard and applies
|
|
10
|
-
// it to a URL a MODEL supplied — attacker-influenced by construction.
|
|
11
|
-
|
|
12
|
-
import { getWeather } from './weather.js';
|
|
13
|
-
|
|
14
|
-
export const WEATHER_TOOL_NAME = 'weather';
|
|
15
|
-
|
|
16
|
-
export const WEATHER_TOOL_SYSTEM =
|
|
17
|
-
'For weather, call `weather` FIRST — it answers the whole question in one request. Only '
|
|
18
|
-
+ 'fall back to web_search if it tells you to. Report the location it says it resolved, '
|
|
19
|
-
+ 'because a bare town name can geocode to the wrong place.';
|
|
20
|
-
|
|
21
|
-
export function weatherToolProvider({ fetchJson } = {}) {
|
|
22
|
-
if (typeof fetchJson !== 'function') throw new Error('weatherToolProvider: fetchJson required');
|
|
23
|
-
return {
|
|
24
|
-
specs: [
|
|
25
|
-
{
|
|
26
|
-
name: 'weather',
|
|
27
|
-
// A read, declared: the round runner overlaps reads and serialises everything it
|
|
28
|
-
// cannot classify, and "weather" is not a verb its name heuristic knows.
|
|
29
|
-
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
30
|
-
description:
|
|
31
|
-
'Current conditions and a short forecast for one place, in a single request. Use this '
|
|
32
|
-
+ 'for any weather question instead of searching. Returns the location it actually '
|
|
33
|
-
+ 'resolved to — say which place the answer is for.',
|
|
34
|
-
parameters: {
|
|
35
|
-
type: 'object',
|
|
36
|
-
properties: {
|
|
37
|
-
location: {
|
|
38
|
-
type: 'string',
|
|
39
|
-
description: 'A place, as the user said it. Add a state or country only if THEY did '
|
|
40
|
-
+ '— "Fairview, OR" if they said so, plain "Fairview" if they did not.',
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
required: ['location'],
|
|
44
|
-
additionalProperties: false,
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
],
|
|
48
|
-
system: WEATHER_TOOL_SYSTEM,
|
|
49
|
-
async execute(name, input) {
|
|
50
|
-
if (name !== 'weather') return JSON.stringify({ error: `Unknown tool: ${name}` });
|
|
51
|
-
const location = String(input?.location || '').trim();
|
|
52
|
-
if (!location) return 'No location provided to weather.';
|
|
53
|
-
const got = await getWeather(location, { fetchJson });
|
|
54
|
-
// THE FALLBACK IS AN INSTRUCTION, not an error string. A model handed "weather failed"
|
|
55
|
-
// stops, or apologises; a model told which tool answers this next just uses it. The
|
|
56
|
-
// whole point of preferring one source is that it must degrade to the general one.
|
|
57
|
-
if (!got.ok) {
|
|
58
|
-
return `The weather service could not answer for "${location}" (${got.reason}). `
|
|
59
|
-
+ `Now call web_search for "weather in ${location}" and answer from the results — `
|
|
60
|
-
+ 'do not tell the user a tool failed.';
|
|
61
|
-
}
|
|
62
|
-
return { text: got.text, note: 'ChatPanel · wttr.in' };
|
|
63
|
-
},
|
|
64
|
-
};
|
|
65
|
-
}
|
|
1
|
+
import{getWeather as a}from"./weather.js";const l="weather",i="For weather, call `weather` FIRST — it answers the whole question in one request. Only fall back to web_search if it tells you to. Report the location it says it resolved, because a bare town name can geocode to the wrong place.";function c({fetchJson:o}={}){if(typeof o!="function")throw new Error("weatherToolProvider: fetchJson required");return{specs:[{name:"weather",annotations:{readOnlyHint:!0,idempotentHint:!0,openWorldHint:!0},description:"Current conditions and a short forecast for one place, in a single request. Use this for any weather question instead of searching. Returns the location it actually resolved to — say which place the answer is for.",parameters:{type:"object",properties:{location:{type:"string",description:'A place, as the user said it. Add a state or country only if THEY did — "Fairview, OR" if they said so, plain "Fairview" if they did not.'}},required:["location"],additionalProperties:!1}}],system:i,async execute(r,n){if(r!=="weather")return JSON.stringify({error:`Unknown tool: ${r}`});const e=String(n?.location||"").trim();if(!e)return"No location provided to weather.";const t=await a(e,{fetchJson:o});return t.ok?{text:t.text,note:"ChatPanel · wttr.in"}:`The weather service could not answer for "${e}" (${t.reason}). Now call web_search for "weather in ${e}" and answer from the results — do not tell the user a tool failed.`}}}export{l as WEATHER_TOOL_NAME,i as WEATHER_TOOL_SYSTEM,c as weatherToolProvider};
|
package/weather.js
CHANGED
|
@@ -1,211 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
// There was no weather capability at all, so every "how is the weather in X" went to
|
|
4
|
-
// web_search and the model assembled an answer from whatever three sites it happened to
|
|
5
|
-
// scrape. That is slow (several page fetches for one number), inconsistent between asks, and
|
|
6
|
-
// it failed in the way scraping fails: a repeated search got refused by the loop guard, the
|
|
7
|
-
// reworded retry returned nothing, and the model announced it had no access to weather.
|
|
8
|
-
//
|
|
9
|
-
// wttr.in answers the whole question in ONE request as JSON, which is why it is the default.
|
|
10
|
-
// It is not authoritative and it is not always up, so the tool falls back to search rather
|
|
11
|
-
// than pretending — but the common case costs one round trip and reads the same every time.
|
|
12
|
-
//
|
|
13
|
-
// THE GEOCODER IS THE KNOWN WEAKNESS, and it is handled by disclosure rather than cleverness.
|
|
14
|
-
// "Issaquah" once resolved to somewhere obscure and the answer looked plausible and was for
|
|
15
|
-
// the wrong place. wttr.in reports what it actually resolved to, so `formatWeather` always
|
|
16
|
-
// prints that line: a wrong place is then visible instead of silent. Guessing a region on the
|
|
17
|
-
// user's behalf would replace a visible error with an invisible one.
|
|
18
|
-
//
|
|
19
|
-
// Pure and platform-free, like every other module here: the fetch is INJECTED, so the
|
|
20
|
-
// extension passes its SSRF-guarded secureFetch, the gateway passes its own, and neither has
|
|
21
|
-
// a second copy of the parsing.
|
|
22
|
-
|
|
23
|
-
export const WEATHER_HOST = 'https://wttr.in';
|
|
24
|
-
|
|
25
|
-
/** Long enough for a cold CDN edge, short enough that a hung host still falls back. */
|
|
26
|
-
export const WEATHER_TIMEOUT_MS = 7000;
|
|
27
|
-
|
|
28
|
-
export class WeatherError extends Error {
|
|
29
|
-
constructor(code, message) { super(message); this.name = 'WeatherError'; this.code = code; }
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* The URL for one location.
|
|
34
|
-
*
|
|
35
|
-
* The location goes in the PATH, so it is percent-encoded whole — a bare `encodeURIComponent`
|
|
36
|
-
* of "Seattle, WA" gives `Seattle%2C%20WA`, which wttr.in reads correctly, while leaving the
|
|
37
|
-
* comma raw would let a location containing `/` or `?` reshape the request. Never interpolate
|
|
38
|
-
* an unencoded user string into a path.
|
|
39
|
-
*/
|
|
40
|
-
export function weatherUrl(location, { host = WEATHER_HOST } = {}) {
|
|
41
|
-
const q = String(location ?? '').trim();
|
|
42
|
-
if (!q) throw new WeatherError('NO_LOCATION', 'a location is required');
|
|
43
|
-
if (q.length > 120) throw new WeatherError('BAD_LOCATION', 'location is too long to be one');
|
|
44
|
-
return `${host}/${encodeURIComponent(q)}?format=j1`;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const num = (v) => {
|
|
48
|
-
const n = Number(v);
|
|
49
|
-
return Number.isFinite(n) ? n : null;
|
|
50
|
-
};
|
|
51
|
-
const str = (v) => String(v ?? '').trim();
|
|
52
|
-
// wttr.in nests half its scalars as [{ value: "…" }], which is a shape, not data.
|
|
53
|
-
const firstValue = (v) => (Array.isArray(v) ? str(v[0]?.value) : str(v));
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* wttr.in's `format=j1` body → a shape that does not mention wttr.in.
|
|
57
|
-
*
|
|
58
|
-
* Returns null for anything unusable rather than throwing: a malformed body and an outage are
|
|
59
|
-
* the same event to the caller, and both mean "fall back to search".
|
|
60
|
-
*/
|
|
61
|
-
export function parseWeather(json, { query = '' } = {}) {
|
|
62
|
-
if (!json || typeof json !== 'object') return null;
|
|
63
|
-
const cur = Array.isArray(json.current_condition) ? json.current_condition[0] : null;
|
|
64
|
-
if (!cur) return null;
|
|
65
|
-
const tempC = num(cur.temp_C);
|
|
66
|
-
const tempF = num(cur.temp_F);
|
|
67
|
-
// A reading with no temperature is not a weather reading. Guarded because an error page
|
|
68
|
-
// served as JSON still parses into an object, and half-empty output read as real data is
|
|
69
|
-
// worse than no data.
|
|
70
|
-
if (tempC === null && tempF === null) return null;
|
|
71
|
-
|
|
72
|
-
const areaRaw = Array.isArray(json.nearest_area) ? json.nearest_area[0] : null;
|
|
73
|
-
const area = areaRaw ? {
|
|
74
|
-
name: firstValue(areaRaw.areaName),
|
|
75
|
-
region: firstValue(areaRaw.region),
|
|
76
|
-
country: firstValue(areaRaw.country),
|
|
77
|
-
lat: num(areaRaw.latitude),
|
|
78
|
-
lon: num(areaRaw.longitude),
|
|
79
|
-
} : null;
|
|
80
|
-
|
|
81
|
-
const days = (Array.isArray(json.weather) ? json.weather : []).slice(0, 3).map((d) => ({
|
|
82
|
-
date: str(d.date),
|
|
83
|
-
maxC: num(d.maxtempC),
|
|
84
|
-
maxF: num(d.maxtempF),
|
|
85
|
-
minC: num(d.mintempC),
|
|
86
|
-
minF: num(d.mintempF),
|
|
87
|
-
sunrise: str(d.astronomy?.[0]?.sunrise),
|
|
88
|
-
sunset: str(d.astronomy?.[0]?.sunset),
|
|
89
|
-
// The midday slot is the day's headline condition; the array is 3-hourly.
|
|
90
|
-
condition: firstValue(d.hourly?.[4]?.weatherDesc) || firstValue(d.hourly?.[0]?.weatherDesc),
|
|
91
|
-
chanceOfRain: num(d.hourly?.[4]?.chanceofrain),
|
|
92
|
-
}));
|
|
93
|
-
|
|
94
|
-
return {
|
|
95
|
-
query: str(query),
|
|
96
|
-
area,
|
|
97
|
-
now: {
|
|
98
|
-
tempC,
|
|
99
|
-
tempF,
|
|
100
|
-
feelsLikeC: num(cur.FeelsLikeC),
|
|
101
|
-
feelsLikeF: num(cur.FeelsLikeF),
|
|
102
|
-
condition: firstValue(cur.weatherDesc),
|
|
103
|
-
humidity: num(cur.humidity),
|
|
104
|
-
windKph: num(cur.windspeedKmph),
|
|
105
|
-
windMph: num(cur.windspeedMiles),
|
|
106
|
-
windDir: str(cur.winddir16Point),
|
|
107
|
-
precipMm: num(cur.precipMM),
|
|
108
|
-
uv: num(cur.uvIndex),
|
|
109
|
-
observedAt: str(cur.localObsDateTime),
|
|
110
|
-
},
|
|
111
|
-
days,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** "Seattle, Washington, United States" — whatever of it exists, deduped. */
|
|
116
|
-
export function areaLabel(area, fallback = '') {
|
|
117
|
-
if (!area) return fallback;
|
|
118
|
-
const parts = [area.name, area.region, area.country].map(str).filter(Boolean);
|
|
119
|
-
const seen = new Set();
|
|
120
|
-
const uniq = parts.filter((p) => {
|
|
121
|
-
const k = p.toLowerCase();
|
|
122
|
-
if (seen.has(k)) return false;
|
|
123
|
-
seen.add(k);
|
|
124
|
-
return true;
|
|
125
|
-
});
|
|
126
|
-
return uniq.join(', ') || fallback;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* True when the user named a bare place with no region or country.
|
|
131
|
-
*
|
|
132
|
-
* Not used to REWRITE the query — that is how you turn a wrong answer into a confidently
|
|
133
|
-
* wrong one. It is used to add a line telling the model to say which place it got, so the
|
|
134
|
-
* user can correct it. Dozens of towns are called Fairview.
|
|
135
|
-
*/
|
|
136
|
-
export function isAmbiguousLocation(location) {
|
|
137
|
-
const q = str(location);
|
|
138
|
-
if (!q) return false;
|
|
139
|
-
return !/[,]/.test(q) && q.split(/\s+/).length <= 2;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const c2f = (c) => Math.round((c * 9) / 5 + 32);
|
|
143
|
-
const temp = (c, f) => {
|
|
144
|
-
const cc = c === null && f !== null ? Math.round(((f - 32) * 5) / 9) : c;
|
|
145
|
-
const ff = f === null && c !== null ? c2f(c) : f;
|
|
146
|
-
if (cc === null && ff === null) return '';
|
|
147
|
-
if (cc === null) return `${ff}°F`;
|
|
148
|
-
if (ff === null) return `${cc}°C`;
|
|
149
|
-
return `${ff}°F / ${cc}°C`;
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
/** What the model reads. Compact — this is one fact, not a report. */
|
|
153
|
-
export function formatWeather(w) {
|
|
154
|
-
if (!w) return '';
|
|
155
|
-
const where = areaLabel(w.area, w.query);
|
|
156
|
-
const lines = [];
|
|
157
|
-
// FIRST LINE NAMES THE PLACE IT ACTUALLY RESOLVED. The geocoder picks obscure matches for
|
|
158
|
-
// bare town names, and an answer that does not say where it is from cannot be caught.
|
|
159
|
-
lines.push(`Weather for ${where}${w.query && where.toLowerCase() !== w.query.toLowerCase() ? ` (asked: "${w.query}")` : ''}`);
|
|
160
|
-
const n = w.now;
|
|
161
|
-
const now = [`Now: ${temp(n.tempC, n.tempF)}`];
|
|
162
|
-
if (n.feelsLikeC !== null || n.feelsLikeF !== null) now.push(`feels like ${temp(n.feelsLikeC, n.feelsLikeF)}`);
|
|
163
|
-
if (n.condition) now.push(n.condition.toLowerCase());
|
|
164
|
-
lines.push(now.join(', '));
|
|
165
|
-
const detail = [];
|
|
166
|
-
if (n.humidity !== null) detail.push(`humidity ${n.humidity}%`);
|
|
167
|
-
if (n.windMph !== null || n.windKph !== null) {
|
|
168
|
-
detail.push(`wind ${n.windMph !== null ? `${n.windMph} mph` : `${n.windKph} km/h`}${n.windDir ? ` ${n.windDir}` : ''}`);
|
|
169
|
-
}
|
|
170
|
-
if (n.precipMm !== null) detail.push(`precip ${n.precipMm} mm`);
|
|
171
|
-
if (n.uv !== null) detail.push(`UV ${n.uv}`);
|
|
172
|
-
if (detail.length) lines.push(detail.join(' · '));
|
|
173
|
-
for (const d of w.days) {
|
|
174
|
-
const range = `${temp(d.maxC, d.maxF)} high / ${temp(d.minC, d.minF)} low`;
|
|
175
|
-
const rain = d.chanceOfRain !== null ? `, ${d.chanceOfRain}% rain` : '';
|
|
176
|
-
lines.push(`${d.date}: ${range}${d.condition ? `, ${d.condition.toLowerCase()}` : ''}${rain}`);
|
|
177
|
-
}
|
|
178
|
-
if (n.observedAt) lines.push(`Observed ${n.observedAt} local. Source: wttr.in`);
|
|
179
|
-
if (isAmbiguousLocation(w.query)) {
|
|
180
|
-
lines.push('The place name was ambiguous — TELL THE USER which location this is for, and '
|
|
181
|
-
+ 'offer to re-check with a state or country if it is the wrong one.');
|
|
182
|
-
}
|
|
183
|
-
return lines.join('\n');
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Ask for one location's weather.
|
|
188
|
-
*
|
|
189
|
-
* @param fetchJson async (url, { timeoutMs }) -> parsed JSON. Injected, so the caller's
|
|
190
|
-
* SSRF-guarded fetch is the only one that runs and this module stays runnable in Node,
|
|
191
|
-
* a browser and a mobile runtime.
|
|
192
|
-
* @returns { ok: true, weather, text } | { ok: false, reason }
|
|
193
|
-
*
|
|
194
|
-
* NEVER THROWS. Every failure is a reason the caller can hand to the model as "use search
|
|
195
|
-
* instead", because an exception here would surface as a tool error and stop the turn on a
|
|
196
|
-
* question the fallback can still answer.
|
|
197
|
-
*/
|
|
198
|
-
export async function getWeather(location, { fetchJson, timeoutMs = WEATHER_TIMEOUT_MS, host = WEATHER_HOST } = {}) {
|
|
199
|
-
let url;
|
|
200
|
-
try { url = weatherUrl(location, { host }); } catch (e) { return { ok: false, reason: e.message }; }
|
|
201
|
-
if (typeof fetchJson !== 'function') return { ok: false, reason: 'no fetch was provided' };
|
|
202
|
-
let json;
|
|
203
|
-
try {
|
|
204
|
-
json = await fetchJson(url, { timeoutMs });
|
|
205
|
-
} catch (e) {
|
|
206
|
-
return { ok: false, reason: e?.message || 'the weather service did not answer' };
|
|
207
|
-
}
|
|
208
|
-
const weather = parseWeather(json, { query: location });
|
|
209
|
-
if (!weather) return { ok: false, reason: 'the weather service returned nothing usable' };
|
|
210
|
-
return { ok: true, weather, text: formatWeather(weather) };
|
|
211
|
-
}
|
|
1
|
+
const f="https://wttr.in",L=7e3;class m extends Error{constructor(r,n){super(n),this.name="WeatherError",this.code=r}}function d(e,{host:r=f}={}){const n=String(e??"").trim();if(!n)throw new m("NO_LOCATION","a location is required");if(n.length>120)throw new m("BAD_LOCATION","location is too long to be one");return`${r}/${encodeURIComponent(n)}?format=j1`}const s=e=>{const r=Number(e);return Number.isFinite(r)?r:null},l=e=>String(e??"").trim(),h=e=>Array.isArray(e)?l(e[0]?.value):l(e);function w(e,{query:r=""}={}){if(!e||typeof e!="object")return null;const n=Array.isArray(e.current_condition)?e.current_condition[0]:null;if(!n)return null;const t=s(n.temp_C),a=s(n.temp_F);if(t===null&&a===null)return null;const i=Array.isArray(e.nearest_area)?e.nearest_area[0]:null,o=i?{name:h(i.areaName),region:h(i.region),country:h(i.country),lat:s(i.latitude),lon:s(i.longitude)}:null,c=(Array.isArray(e.weather)?e.weather:[]).slice(0,3).map(u=>({date:l(u.date),maxC:s(u.maxtempC),maxF:s(u.maxtempF),minC:s(u.mintempC),minF:s(u.mintempF),sunrise:l(u.astronomy?.[0]?.sunrise),sunset:l(u.astronomy?.[0]?.sunset),condition:h(u.hourly?.[4]?.weatherDesc)||h(u.hourly?.[0]?.weatherDesc),chanceOfRain:s(u.hourly?.[4]?.chanceofrain)}));return{query:l(r),area:o,now:{tempC:t,tempF:a,feelsLikeC:s(n.FeelsLikeC),feelsLikeF:s(n.FeelsLikeF),condition:h(n.weatherDesc),humidity:s(n.humidity),windKph:s(n.windspeedKmph),windMph:s(n.windspeedMiles),windDir:l(n.winddir16Point),precipMm:s(n.precipMM),uv:s(n.uvIndex),observedAt:l(n.localObsDateTime)},days:c}}function y(e,r=""){if(!e)return r;const n=[e.name,e.region,e.country].map(l).filter(Boolean),t=new Set;return n.filter(i=>{const o=i.toLowerCase();return t.has(o)?!1:(t.add(o),!0)}).join(", ")||r}function $(e){const r=l(e);return r?!/[,]/.test(r)&&r.split(/\s+/).length<=2:!1}const C=e=>Math.round(e*9/5+32),p=(e,r)=>{const n=e===null&&r!==null?Math.round((r-32)*5/9):e,t=r===null&&e!==null?C(e):r;return n===null&&t===null?"":n===null?`${t}°F`:t===null?`${n}°C`:`${t}°F / ${n}°C`};function g(e){if(!e)return"";const r=y(e.area,e.query),n=[];n.push(`Weather for ${r}${e.query&&r.toLowerCase()!==e.query.toLowerCase()?` (asked: "${e.query}")`:""}`);const t=e.now,a=[`Now: ${p(t.tempC,t.tempF)}`];(t.feelsLikeC!==null||t.feelsLikeF!==null)&&a.push(`feels like ${p(t.feelsLikeC,t.feelsLikeF)}`),t.condition&&a.push(t.condition.toLowerCase()),n.push(a.join(", "));const i=[];t.humidity!==null&&i.push(`humidity ${t.humidity}%`),(t.windMph!==null||t.windKph!==null)&&i.push(`wind ${t.windMph!==null?`${t.windMph} mph`:`${t.windKph} km/h`}${t.windDir?` ${t.windDir}`:""}`),t.precipMm!==null&&i.push(`precip ${t.precipMm} mm`),t.uv!==null&&i.push(`UV ${t.uv}`),i.length&&n.push(i.join(" · "));for(const o of e.days){const c=`${p(o.maxC,o.maxF)} high / ${p(o.minC,o.minF)} low`,u=o.chanceOfRain!==null?`, ${o.chanceOfRain}% rain`:"";n.push(`${o.date}: ${c}${o.condition?`, ${o.condition.toLowerCase()}`:""}${u}`)}return t.observedAt&&n.push(`Observed ${t.observedAt} local. Source: wttr.in`),$(e.query)&&n.push("The place name was ambiguous — TELL THE USER which location this is for, and offer to re-check with a state or country if it is the wrong one."),n.join(`
|
|
2
|
+
`)}async function x(e,{fetchJson:r,timeoutMs:n=7e3,host:t=f}={}){let a;try{a=d(e,{host:t})}catch(c){return{ok:!1,reason:c.message}}if(typeof r!="function")return{ok:!1,reason:"no fetch was provided"};let i;try{i=await r(a,{timeoutMs:n})}catch(c){return{ok:!1,reason:c?.message||"the weather service did not answer"}}const o=w(i,{query:e});return o?{ok:!0,weather:o,text:g(o)}:{ok:!1,reason:"the weather service returned nothing usable"}}export{f as WEATHER_HOST,L as WEATHER_TIMEOUT_MS,m as WeatherError,y as areaLabel,g as formatWeather,x as getWeather,$ as isAmbiguousLocation,w as parseWeather,d as weatherUrl};
|
package/web-search-tool.js
CHANGED
|
@@ -1,107 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
//
|
|
4
|
-
// Running a search is platform work: the extension fetches SERPs from a service worker
|
|
5
|
-
// with DOMParser, the desktop's main process reads anchors with a tokenizer, a gateway that
|
|
6
|
-
// grew one would use its own fetch. What is identical everywhere is what the model is told
|
|
7
|
-
// the tool does, what it is told about citing, and how a result list becomes text it can
|
|
8
|
-
// cite from — so that is what lives here, and `search` is injected.
|
|
9
|
-
//
|
|
10
|
-
// The citation rules are in the RESULT, not only in the system prompt, on purpose: the
|
|
11
|
-
// model reads them at the moment it has sources in hand, which is when it is most likely to
|
|
12
|
-
// follow them, and a turn that never searches never pays for them.
|
|
1
|
+
const a="web_search",u='You can call web_search to look up current information from the web — prices, news, recent events, documentation, or anything time-sensitive or that may have changed since your training. Call it whenever the user asks about such things instead of guessing or saying you are unsure. Cite results inline as markdown links — e.g. ([1](https://…)) — never HTML, <sup>, or bare numbers, and finish with a "Sources" list of the links you used.',c=Object.freeze({name:a,annotations:{readOnlyHint:!0,idempotentHint:!0,openWorldHint:!0},description:"Search the web and return ranked result snippets with their source URLs. Use this for current events, live prices/quotes, news, product/library docs, or any fact you are unsure about or that may have changed since training — prefer it over guessing.",parameters:{type:"object",properties:{query:{type:"string",description:"The search query — a few keywords work best."}},required:["query"],additionalProperties:!1}});function l(e){if(!e?.results?.length){const t=(e?.engines||[]).join(", ");return`No web results for "${e?.query||""}"`+(t?` (searched: ${t}).`:".")+" This may mean the engine blocked the request rather than that nothing exists — do NOT conclude the information is unavailable. Try a shorter, more general query (drop dates and qualifiers), and tell the user they can enable another search engine in ChatPanel settings if it keeps failing."}const r=Array.isArray(e.engines)?e.engines:[],o=e.results.map(t=>`[${t.rank}] [${t.title}](${t.url})`).join(`
|
|
2
|
+
`),s=e.results[0].url,n=`Web search results for "${e.query}" (engines: ${r.join(", ")}).
|
|
13
3
|
|
|
14
|
-
|
|
4
|
+
Citation rules: when a claim draws on a result below, cite it inline as a markdown link to that result's URL — e.g. ([1](${s})). Cite multiple sources as separate links: ([1](url)) ([3](url)). Do NOT output HTML, <sup>, or bare bracket numbers like [1] — every citation must be a clickable markdown link. Finish with a "Sources" section that repeats, as markdown links, each source you cited.
|
|
15
5
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
+ 'Call it whenever the user asks about such things instead of guessing or saying you are unsure. '
|
|
20
|
-
+ 'Cite results inline as markdown links — e.g. ([1](https://…)) — never HTML, <sup>, or bare '
|
|
21
|
-
+ 'numbers, and finish with a "Sources" list of the links you used.';
|
|
6
|
+
Sources:
|
|
7
|
+
${o}`,i=e.results.map(t=>`### [${t.rank}] ${t.title}
|
|
8
|
+
<${t.url}>
|
|
22
9
|
|
|
23
|
-
|
|
24
|
-
name: WEB_SEARCH_TOOL_NAME,
|
|
25
|
-
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
26
|
-
description:
|
|
27
|
-
'Search the web and return ranked result snippets with their source URLs. Use this for '
|
|
28
|
-
+ 'current events, live prices/quotes, news, product/library docs, or any fact you are unsure '
|
|
29
|
-
+ 'about or that may have changed since training — prefer it over guessing.',
|
|
30
|
-
parameters: {
|
|
31
|
-
type: 'object',
|
|
32
|
-
properties: {
|
|
33
|
-
query: { type: 'string', description: 'The search query — a few keywords work best.' },
|
|
34
|
-
},
|
|
35
|
-
required: ['query'],
|
|
36
|
-
additionalProperties: false,
|
|
37
|
-
},
|
|
38
|
-
});
|
|
10
|
+
${t.text}`).join(`
|
|
39
11
|
|
|
40
|
-
|
|
41
|
-
* Flatten a search result into one readable blob the model can cite from.
|
|
42
|
-
*
|
|
43
|
-
* `res` is `{ query, engines: [ids], results: [{ rank, title, url, text }] }`. A citation
|
|
44
|
-
* index of markdown links sits at the TOP so it survives truncation.
|
|
45
|
-
*/
|
|
46
|
-
export function searchResultsToText(res) {
|
|
47
|
-
if (!res?.results?.length) {
|
|
48
|
-
// Name the engines HERE especially. The success path already lists them; the failure
|
|
49
|
-
// path did not, so "no web results" looked like "the web has nothing" when it usually
|
|
50
|
-
// means "the one engine we were allowed to ask returned nothing" — a search engine
|
|
51
|
-
// blocking us and a query with no answer are completely different problems, and the
|
|
52
|
-
// model cannot tell them apart without this.
|
|
53
|
-
const tried = (res?.engines || []).join(', ');
|
|
54
|
-
return `No web results for "${res?.query || ''}"`
|
|
55
|
-
+ (tried ? ` (searched: ${tried}).` : '.')
|
|
56
|
-
+ ' This may mean the engine blocked the request rather than that nothing exists —'
|
|
57
|
-
+ ' do NOT conclude the information is unavailable. Try a shorter, more general query'
|
|
58
|
-
+ ' (drop dates and qualifiers), and tell the user they can enable another search'
|
|
59
|
-
+ ' engine in ChatPanel settings if it keeps failing.';
|
|
60
|
-
}
|
|
61
|
-
const engines = Array.isArray(res.engines) ? res.engines : [];
|
|
62
|
-
const sources = res.results.map((r) => `[${r.rank}] [${r.title}](${r.url})`).join('\n');
|
|
63
|
-
const example = res.results[0].url;
|
|
64
|
-
const head =
|
|
65
|
-
`Web search results for "${res.query}" (engines: ${engines.join(', ')}).\n\n`
|
|
66
|
-
+ 'Citation rules: when a claim draws on a result below, cite it inline as a markdown '
|
|
67
|
-
+ `link to that result's URL — e.g. ([1](${example})). Cite multiple sources as separate `
|
|
68
|
-
+ 'links: ([1](url)) ([3](url)). Do NOT output HTML, <sup>, or bare bracket numbers like '
|
|
69
|
-
+ '[1] — every citation must be a clickable markdown link. Finish with a "Sources" section '
|
|
70
|
-
+ 'that repeats, as markdown links, each source you cited.\n\n'
|
|
71
|
-
+ `Sources:\n${sources}`;
|
|
72
|
-
const body = res.results
|
|
73
|
-
.map((r) => `### [${r.rank}] ${r.title}\n<${r.url}>\n\n${r.text}`)
|
|
74
|
-
.join('\n\n---\n\n');
|
|
75
|
-
return `${head}\n\n---\nResult details:\n\n${body}`;
|
|
76
|
-
}
|
|
12
|
+
---
|
|
77
13
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
export function webSearchToolProvider({ search } = {}) {
|
|
85
|
-
if (typeof search !== 'function') throw new Error('webSearchToolProvider: search required');
|
|
86
|
-
return {
|
|
87
|
-
specs: [WEB_SEARCH_SPEC],
|
|
88
|
-
system: WEB_SEARCH_TOOL_SYSTEM,
|
|
89
|
-
async execute(name, input) {
|
|
90
|
-
if (name !== WEB_SEARCH_TOOL_NAME) return JSON.stringify({ error: `Unknown tool: ${name}` });
|
|
91
|
-
const q = String(input?.query || '').trim();
|
|
92
|
-
if (!q) return 'No query provided to web_search.';
|
|
93
|
-
try {
|
|
94
|
-
const res = await search(q);
|
|
95
|
-
// Return an OBJECT so the step can name the ENGINE that actually served the results.
|
|
96
|
-
// "web_search" alone doesn't tell the user whether Startpage or DuckDuckGo answered —
|
|
97
|
-
// which matters, because engines differ in coverage, and because a CLI agent may have
|
|
98
|
-
// run its OWN search instead of this one. `note` becomes the step's badge; `text` is
|
|
99
|
-
// what the model reads, unchanged.
|
|
100
|
-
const engines = (res?.engines || []).join(', ');
|
|
101
|
-
return { text: searchResultsToText(res), note: engines ? `ChatPanel · ${engines}` : 'ChatPanel' };
|
|
102
|
-
} catch (e) {
|
|
103
|
-
return `web_search failed: ${e?.message || e}`;
|
|
104
|
-
}
|
|
105
|
-
},
|
|
106
|
-
};
|
|
107
|
-
}
|
|
14
|
+
`);return`${n}
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
Result details:
|
|
18
|
+
|
|
19
|
+
${i}`}function h({search:e}={}){if(typeof e!="function")throw new Error("webSearchToolProvider: search required");return{specs:[c],system:u,async execute(r,o){if(r!==a)return JSON.stringify({error:`Unknown tool: ${r}`});const s=String(o?.query||"").trim();if(!s)return"No query provided to web_search.";try{const n=await e(s),i=(n?.engines||[]).join(", ");return{text:l(n),note:i?`ChatPanel · ${i}`:"ChatPanel"}}catch(n){return`web_search failed: ${n?.message||n}`}}}}export{c as WEB_SEARCH_SPEC,a as WEB_SEARCH_TOOL_NAME,u as WEB_SEARCH_TOOL_SYSTEM,l as searchResultsToText,h as webSearchToolProvider};
|