@qvac/harness 0.2.6 → 0.2.8
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/.claude/skills/gh-stack/SKILL.md +24 -24
- package/.claude/skills/gh-stack/references/commands.md +2 -2
- package/.claude/skills/js-house-style/SKILL.md +89 -68
- package/.claude/skills/kvcache/SKILL.md +7 -7
- package/.claude/skills/pr-create/SKILL.md +58 -15
- package/.claude/skills/pr-review/SKILL.md +307 -41
- package/CLAUDE.md +3 -0
- package/README.md +26 -4
- package/dist/harness.bundle +12 -10
- package/dist-lib/index.js +58 -58
- package/dist-lib/lib/audio-transcode-bare.js +2 -2
- package/dist-lib/lib/capabilities/completion.js +30 -30
- package/dist-lib/lib/capabilities/download.js +2 -2
- package/dist-lib/lib/capabilities/speech-text.js +1 -1
- package/dist-lib/lib/capabilities/tool-prompt-log.js +1 -1
- package/dist-lib/lib/capabilities/tool-selection.js +2 -2
- package/dist-lib/lib/capabilities/tool-turns.js +1 -1
- package/dist-lib/lib/capabilities/usage-breakdown.js +1 -1
- package/dist-lib/lib/capabilities/voice.js +3 -3
- package/dist-lib/lib/connect.js +11 -11
- package/dist-lib/lib/context-window.js +1 -1
- package/dist-lib/lib/docx/index.d.ts +5 -0
- package/dist-lib/lib/docx/index.js +49 -0
- package/dist-lib/lib/hardware/capability-probe.js +3 -3
- package/dist-lib/lib/hardware/gpu-probe-addon.js +2 -2
- package/dist-lib/lib/hardware/gpu-probe-entry.js +1 -1
- package/dist-lib/lib/helpers/markup-stream-filter.js +1 -1
- package/dist-lib/lib/helpers/run-outcome.js +1 -1
- package/dist-lib/lib/helpers/spawn-path.d.ts +1 -1
- package/dist-lib/lib/helpers/thinking.js +2 -2
- package/dist-lib/lib/helpers/tool-intent.js +1 -1
- package/dist-lib/lib/helpers/tool-label.js +3 -3
- package/dist-lib/lib/hooks/built-in/arg-repair.d.ts +1 -1
- package/dist-lib/lib/hooks/built-in/arg-repair.js +1 -1
- package/dist-lib/lib/hooks/built-in/egress.d.ts +1 -1
- package/dist-lib/lib/hooks/built-in/egress.js +2 -2
- package/dist-lib/lib/hooks/built-in/index.js +3 -3
- package/dist-lib/lib/hooks/built-in/skill-catalog.d.ts +1 -1
- package/dist-lib/lib/hooks/pipeline.js +4 -4
- package/dist-lib/lib/hooks/registry.js +1 -1
- package/dist-lib/lib/hooks/run.js +1 -1
- package/dist-lib/lib/models/model-catalog.js +4 -4
- package/dist-lib/lib/models/model-delete.js +1 -1
- package/dist-lib/lib/models/model-handle.js +1 -1
- package/dist-lib/lib/models/model-list.js +1 -1
- package/dist-lib/lib/models/model-pool.js +8 -8
- package/dist-lib/lib/models/model-source.js +1 -1
- package/dist-lib/lib/models/music-catalog.d.ts +1 -1
- package/dist-lib/lib/no-redirect-fetch-bare.js +3 -3
- package/dist-lib/lib/no-redirect-fetch-impl.js +1 -1
- package/dist-lib/lib/no-redirect-fetch-node.js +2 -2
- package/dist-lib/lib/resolution-detector.js +1 -1
- package/dist-lib/lib/sdk-loader.js +2 -2
- package/dist-lib/lib/sdk-storage.js +1 -1
- package/dist-lib/lib/serve.js +10 -10
- package/dist-lib/lib/spawn.js +3 -3
- package/dist-lib/lib/staging.js +1 -1
- package/dist-lib/lib/tools/catalog.js +1 -1
- package/dist-lib/lib/tools/create-documents.js +2 -2
- package/dist-lib/lib/tools/edit-documents.js +2 -2
- package/dist-lib/lib/tools/edit-image.js +2 -2
- package/dist-lib/lib/tools/exec/adapters/python/attachments.js +2 -2
- package/dist-lib/lib/tools/exec/adapters/python/create-effect.js +1 -1
- package/dist-lib/lib/tools/exec/adapters/python/loader.js +1 -1
- package/dist-lib/lib/tools/exec/adapters/python/memfs.js +2 -2
- package/dist-lib/lib/tools/exec/adapters/python/pyodide.js +8 -6
- package/dist-lib/lib/tools/exec/adapters/python/python-adapter.js +3 -3
- package/dist-lib/lib/tools/exec/adapters/python/run-session.js +5 -5
- package/dist-lib/lib/tools/exec/adapters/python/wheels.js +3 -2
- package/dist-lib/lib/tools/exec/adapters/python/worker/client.js +4 -4
- package/dist-lib/lib/tools/exec/adapters/python/worker/entry.js +4 -4
- package/dist-lib/lib/tools/exec/adapters/shell/shell-adapter.js +1 -1
- package/dist-lib/lib/tools/exec/obsidian/cli-process.js +2 -2
- package/dist-lib/lib/tools/exec/obsidian/cli.js +4 -4
- package/dist-lib/lib/tools/exec/obsidian/command.js +1 -1
- package/dist-lib/lib/tools/exec/obsidian/route.js +7 -7
- package/dist-lib/lib/tools/exec/obsidian/vault-create.js +2 -2
- package/dist-lib/lib/tools/exec/obsidian/vault-fs.js +6 -6
- package/dist-lib/lib/tools/exec/obsidian/vault-paths.js +2 -2
- package/dist-lib/lib/tools/exec/obsidian/vault-read.js +1 -1
- package/dist-lib/lib/tools/exec/obsidian/vault-write.js +2 -2
- package/dist-lib/lib/tools/exec/runner.js +1 -1
- package/dist-lib/lib/tools/exec.js +13 -13
- package/dist-lib/lib/tools/file-glob.js +3 -3
- package/dist-lib/lib/tools/fs/local.js +1 -1
- package/dist-lib/lib/tools/fs/remote.js +1 -1
- package/dist-lib/lib/tools/fs/walk.js +2 -2
- package/dist-lib/lib/tools/generate-image.js +4 -4
- package/dist-lib/lib/tools/generate-music.js +6 -6
- package/dist-lib/lib/tools/grep.js +6 -6
- package/dist-lib/lib/tools/http-request/truncate-response.js +1 -1
- package/dist-lib/lib/tools/http-request.js +9 -9
- package/dist-lib/lib/tools/mcp/utils.js +1 -1
- package/dist-lib/lib/tools/mcp-call.js +8 -8
- package/dist-lib/lib/tools/operations/builders.d.ts +1 -1
- package/dist-lib/lib/tools/operations/builders.js +1 -1
- package/dist-lib/lib/tools/operations/compile.js +1 -1
- package/dist-lib/lib/tools/operations/manifest.d.ts +16 -16
- package/dist-lib/lib/tools/operations/manifest.js +1 -1
- package/dist-lib/lib/tools/outbound-guard.js +1 -1
- package/dist-lib/lib/tools/rag-search.js +1 -1
- package/dist-lib/lib/tools/read-files.js +3 -3
- package/dist-lib/lib/tools/skill.js +8 -8
- package/dist-lib/lib/tools/skills/auth-resolver.js +2 -2
- package/dist-lib/lib/tools/skills/bin-check.js +1 -1
- package/dist-lib/lib/tools/skills/bundled-dir.js +2 -2
- package/dist-lib/lib/tools/skills/catalog.js +10 -10
- package/dist-lib/lib/tools/skills/cli-schema-check.js +1 -1
- package/dist-lib/lib/tools/skills/cli-schema-load.js +1 -1
- package/dist-lib/lib/tools/skills/cli-schema.js +4 -4
- package/dist-lib/lib/tools/skills/credential-check.js +3 -3
- package/dist-lib/lib/tools/skills/credential-grants.js +1 -1
- package/dist-lib/lib/tools/skills/credential-health.js +2 -2
- package/dist-lib/lib/tools/skills/frontmatter-parser.js +1 -1
- package/dist-lib/lib/tools/skills/index.js +5 -5
- package/dist-lib/lib/tools/skills/mention.js +1 -1
- package/dist-lib/lib/tools/skills/overlay-crud.js +1 -1
- package/dist-lib/lib/tools/skills/setup-routes.js +1 -1
- package/dist-lib/lib/tools/skills/skill-settings.js +1 -1
- package/dist-lib/lib/tools/skills/skill-state-constants.d.ts +8 -8
- package/dist-lib/lib/tools/skills/skill-state.js +9 -9
- package/dist-lib/lib/tools/toolsets.d.ts +1 -1
- package/dist-lib/lib/tools/url-guard.js +2 -2
- package/dist-lib/lib/tools/weather-lookup/place.d.ts +11 -0
- package/dist-lib/lib/tools/weather-lookup/place.js +152 -0
- package/dist-lib/lib/tools/weather-lookup/report.d.ts +37 -0
- package/dist-lib/lib/tools/weather-lookup/report.js +56 -0
- package/dist-lib/lib/tools/weather-lookup.d.ts +8 -0
- package/dist-lib/lib/tools/weather-lookup.js +110 -0
- package/dist-lib/lib/tools/web-fetch.js +10 -10
- package/dist-lib/lib/tools/web-search.js +2 -2
- package/dist-lib/lib/transport.js +1 -1
- package/dist-lib/lib/validating-lookup.js +1 -1
- package/dist-lib/lib/web-fetch/embedded-state.js +1 -1
- package/dist-lib/lib/web-fetch/extract.js +1 -1
- package/dist-lib/lib/web-fetch/images.js +1 -1
- package/dist-lib/lib/web-fetch/video.js +1 -1
- package/dist-lib/lib/web-search/engine-registry.js +7 -7
- package/dist-lib/lib/web-search/engines/aol.js +3 -3
- package/dist-lib/lib/web-search/engines/bing.js +4 -4
- package/dist-lib/lib/web-search/engines/brave.js +2 -2
- package/dist-lib/lib/web-search/engines/duckduckgo.js +3 -3
- package/dist-lib/lib/web-search/engines/google.js +4 -4
- package/dist-lib/lib/web-search/engines/wikipedia.js +2 -2
- package/dist-lib/lib/web-search/engines/yahoo.js +3 -3
- package/dist-lib/lib/web-search/fetch-html.js +1 -1
- package/dist-lib/lib/web-search/merge-results.js +1 -1
- package/dist-lib/lib/web-search/orchestrator.js +3 -3
- package/dist-lib/schema/baseline.js +1 -1
- package/dist-lib/schema/build.js +3 -3
- package/dist-lib/schema/release-tags.d.ts +2 -1
- package/dist-lib/schema/release-tags.js +9 -7
- package/docs/decisions/2026-05-29-kvcache-tool-turns.md +75 -0
- package/docs/decisions/2026-06-02-kvcache-tool-turns-reenabled.md +127 -0
- package/docs/decisions/2026-06-23-android-cpu-llm-defaults.md +126 -0
- package/docs/decisions/2026-07-29-assistant-owned-storage-layout.md +88 -0
- package/docs/decisions/2026-08-04-sidecar-orphan-reaping.md +79 -0
- package/docs/decisions/2026-08-11-pyodide-worker-thread.md +68 -0
- package/docs/decisions/2026-08-17-harness-hook-pipeline.md +84 -0
- package/docs/decisions/2026-08-17-harness-outbound-egress-guard.md +176 -0
- package/docs/decisions/2026-08-20-chat-prompt-prefix-is-stable.md +95 -0
- package/docs/decisions/2026-08-20-kvcache-static-placement.md +109 -0
- package/docs/decisions/2026-08-21-vendored-python-wheels.md +85 -0
- package/docs/decisions/2026-08-26-exec-is-not-gated-by-skills.md +64 -0
- package/docs/decisions/2026-08-27-writable-skills-overlay.md +56 -0
- package/docs/decisions/2026-09-01-skill-operation-manifests.md +53 -0
- package/docs/decisions/2026-09-01-usage-breakdown-is-measured.md +78 -0
- package/docs/decisions/2026-09-03-usage-breakdown-reported-by-the-harness.md +136 -0
- package/docs/decisions/2026-09-10-skill-wording-is-measured-offline.md +63 -0
- package/docs/decisions/2026-09-16-a-lookup-reports-the-place-it-resolved.md +70 -0
- package/docs/decisions/INDEX.md +40 -0
- package/docs/decisions/TEMPLATE.md +32 -0
- package/package.json +21 -4
- package/worker/harness-sidecar.mjs +2 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type ResolvedPlace } from './place.ts';
|
|
2
|
+
interface Translated {
|
|
3
|
+
readonly value: string;
|
|
4
|
+
}
|
|
5
|
+
interface CurrentCondition {
|
|
6
|
+
readonly temp_C?: string;
|
|
7
|
+
readonly temp_F?: string;
|
|
8
|
+
readonly FeelsLikeC?: string;
|
|
9
|
+
readonly humidity?: string;
|
|
10
|
+
readonly windspeedKmph?: string;
|
|
11
|
+
readonly weatherDesc?: ReadonlyArray<Translated>;
|
|
12
|
+
}
|
|
13
|
+
interface HourlyEntry {
|
|
14
|
+
readonly time?: string;
|
|
15
|
+
readonly weatherDesc?: ReadonlyArray<Translated>;
|
|
16
|
+
}
|
|
17
|
+
interface DailyForecast {
|
|
18
|
+
readonly date?: string;
|
|
19
|
+
readonly maxtempC?: string;
|
|
20
|
+
readonly mintempC?: string;
|
|
21
|
+
readonly maxtempF?: string;
|
|
22
|
+
readonly mintempF?: string;
|
|
23
|
+
readonly hourly?: ReadonlyArray<HourlyEntry>;
|
|
24
|
+
}
|
|
25
|
+
interface NearestArea {
|
|
26
|
+
readonly areaName?: ReadonlyArray<Translated>;
|
|
27
|
+
readonly region?: ReadonlyArray<Translated>;
|
|
28
|
+
readonly country?: ReadonlyArray<Translated>;
|
|
29
|
+
}
|
|
30
|
+
export interface WeatherJson {
|
|
31
|
+
readonly nearest_area?: ReadonlyArray<NearestArea>;
|
|
32
|
+
readonly current_condition?: ReadonlyArray<CurrentCondition>;
|
|
33
|
+
readonly weather?: ReadonlyArray<DailyForecast>;
|
|
34
|
+
}
|
|
35
|
+
export declare function placeOf(json: WeatherJson): ResolvedPlace | null;
|
|
36
|
+
export declare function renderReport(json: WeatherJson, place: ResolvedPlace, asked: string, verified: boolean): string;
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describePlace } from './place.js';
|
|
2
|
+
const MAX_DAYS = 3;
|
|
3
|
+
const DAY_LABELS = ['today', 'tomorrow'];
|
|
4
|
+
const RANGE = '\u2013';
|
|
5
|
+
const NOON = 4;
|
|
6
|
+
function firstValue(values) {
|
|
7
|
+
return values?.[0]?.value?.trim() ?? '';
|
|
8
|
+
}
|
|
9
|
+
export function placeOf(json) {
|
|
10
|
+
const area = json.nearest_area?.[0];
|
|
11
|
+
if (area === undefined)
|
|
12
|
+
return null;
|
|
13
|
+
const place = {
|
|
14
|
+
area: firstValue(area.areaName),
|
|
15
|
+
region: firstValue(area.region),
|
|
16
|
+
country: firstValue(area.country)
|
|
17
|
+
};
|
|
18
|
+
return place.area || place.region || place.country ? place : null;
|
|
19
|
+
}
|
|
20
|
+
export function renderReport(json, place, asked, verified) {
|
|
21
|
+
const lines = [headline(place, asked, verified)];
|
|
22
|
+
const now = json.current_condition?.[0];
|
|
23
|
+
if (now !== undefined)
|
|
24
|
+
lines.push(currentLine(now));
|
|
25
|
+
const days = json.weather?.slice(0, MAX_DAYS) ?? [];
|
|
26
|
+
for (const [index, day] of days.entries())
|
|
27
|
+
lines.push(dailyLine(day, index));
|
|
28
|
+
return lines.join('\n');
|
|
29
|
+
}
|
|
30
|
+
function headline(place, asked, verified) {
|
|
31
|
+
const resolved = describePlace(place);
|
|
32
|
+
if (verified)
|
|
33
|
+
return resolved;
|
|
34
|
+
return `${resolved} (not "${asked}" — this is where it resolved)`;
|
|
35
|
+
}
|
|
36
|
+
function currentLine(now) {
|
|
37
|
+
const parts = [`Now: ${now.temp_C ?? '?'}°C / ${now.temp_F ?? '?'}°F`];
|
|
38
|
+
const desc = firstValue(now.weatherDesc);
|
|
39
|
+
if (desc)
|
|
40
|
+
parts.push(desc);
|
|
41
|
+
if (now.FeelsLikeC !== undefined)
|
|
42
|
+
parts.push(`feels like ${now.FeelsLikeC}°C`);
|
|
43
|
+
if (now.humidity !== undefined)
|
|
44
|
+
parts.push(`humidity ${now.humidity}%`);
|
|
45
|
+
if (now.windspeedKmph !== undefined)
|
|
46
|
+
parts.push(`wind ${now.windspeedKmph} km/h`);
|
|
47
|
+
return parts.join(', ');
|
|
48
|
+
}
|
|
49
|
+
function dailyLine(day, index) {
|
|
50
|
+
const label = DAY_LABELS[index];
|
|
51
|
+
const date = `${day.date ?? '?'}${label === undefined ? '' : ` (${label})`}`;
|
|
52
|
+
const celsius = `${day.mintempC ?? '?'}${RANGE}${day.maxtempC ?? '?'}°C`;
|
|
53
|
+
const fahrenheit = `${day.mintempF ?? '?'}${RANGE}${day.maxtempF ?? '?'}°F`;
|
|
54
|
+
const desc = firstValue(day.hourly?.[NOON]?.weatherDesc);
|
|
55
|
+
return `${date}: ${celsius} / ${fahrenheit}${desc ? `, ${desc}` : ''}`;
|
|
56
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../capabilities/completion.ts';
|
|
2
|
+
import type { FetchFn } from './http-request.ts';
|
|
3
|
+
import { type HostResolver } from './url-guard.ts';
|
|
4
|
+
export interface WeatherLookupToolOptions {
|
|
5
|
+
fetch?: FetchFn;
|
|
6
|
+
resolveHost?: HostResolver;
|
|
7
|
+
}
|
|
8
|
+
export declare function createWeatherLookupTool({ fetch: fetchImpl, resolveHost }?: WeatherLookupToolOptions): ToolDefinition;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import defaultFetch from '#no-redirect-fetch';
|
|
2
|
+
import { AbortController } from 'bare-abort-controller';
|
|
3
|
+
import { lookupHost } from '../dns-lookup.js';
|
|
4
|
+
import { isRedirectStatus } from './http-request/utils.js';
|
|
5
|
+
import { resolveOutboundUrl } from './url-guard.js';
|
|
6
|
+
import { checkQualifier, cityMatches, describePlace, qualifierOf } from './weather-lookup/place.js';
|
|
7
|
+
import { placeOf, renderReport } from './weather-lookup/report.js';
|
|
8
|
+
const WEATHER_HOST = 'wttr.in';
|
|
9
|
+
const LOOKUP_TIMEOUT_MS = 15_000;
|
|
10
|
+
const MAX_BODY_BYTES = 400_000;
|
|
11
|
+
export function createWeatherLookupTool({ fetch: fetchImpl, resolveHost = lookupHost } = {}) {
|
|
12
|
+
return {
|
|
13
|
+
name: 'weather_lookup',
|
|
14
|
+
toolset: 'http-request',
|
|
15
|
+
skillGated: true,
|
|
16
|
+
autoApprove: true,
|
|
17
|
+
description: 'Look up the current weather and a three-day forecast for a place, via wttr.in. ' +
|
|
18
|
+
'Returns the place the service resolved on its first line, then the conditions now and ' +
|
|
19
|
+
'one line per day. Use it for any question about weather, temperature or forecast.',
|
|
20
|
+
parameters: {
|
|
21
|
+
type: 'object',
|
|
22
|
+
properties: {
|
|
23
|
+
location: {
|
|
24
|
+
type: 'string',
|
|
25
|
+
description: 'The place exactly as the user wrote it, including any country or state: ' +
|
|
26
|
+
'"Nassau, Bahamas", "Paris, Texas", "London"'
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
required: ['location']
|
|
30
|
+
},
|
|
31
|
+
effect: () => 'read',
|
|
32
|
+
resource: () => ({ kind: 'host', label: WEATHER_HOST }),
|
|
33
|
+
async execute(args, ctx) {
|
|
34
|
+
const raw = args.location;
|
|
35
|
+
if (typeof raw !== 'string' || raw.trim().length === 0) {
|
|
36
|
+
return { error: 'location must be a non-empty string' };
|
|
37
|
+
}
|
|
38
|
+
const location = raw.trim();
|
|
39
|
+
if (ctx?.signal?.aborted)
|
|
40
|
+
return { error: 'weather_lookup cancelled' };
|
|
41
|
+
const url = new URL(`https://${WEATHER_HOST}/${encodeURIComponent(location)}?format=j1`);
|
|
42
|
+
const fetch = fetchImpl ?? defaultFetch;
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const timeout = setTimeout(() => controller.abort(undefined), LOOKUP_TIMEOUT_MS);
|
|
45
|
+
const onCallerAbort = () => controller.abort(undefined);
|
|
46
|
+
ctx?.signal?.addEventListener('abort', onCallerAbort);
|
|
47
|
+
try {
|
|
48
|
+
const check = await resolveOutboundUrl(url, resolveHost, ctx?.signal);
|
|
49
|
+
if (check.blocked)
|
|
50
|
+
return { error: check.blocked };
|
|
51
|
+
const response = await fetch(url, {
|
|
52
|
+
method: 'GET',
|
|
53
|
+
headers: { Accept: 'application/json' },
|
|
54
|
+
signal: controller.signal,
|
|
55
|
+
redirect: 'manual',
|
|
56
|
+
validatedAddresses: check.addresses,
|
|
57
|
+
maxBytes: MAX_BODY_BYTES
|
|
58
|
+
});
|
|
59
|
+
if (isRedirectStatus(response.status)) {
|
|
60
|
+
return { error: `${WEATHER_HOST} redirected the lookup for "${location}"` };
|
|
61
|
+
}
|
|
62
|
+
const body = await response.text();
|
|
63
|
+
if (/location not found/i.test(body))
|
|
64
|
+
return { content: notFound(location) };
|
|
65
|
+
if (response.status !== 200) {
|
|
66
|
+
return { error: unavailable(`it answered HTTP ${response.status}`) };
|
|
67
|
+
}
|
|
68
|
+
let json;
|
|
69
|
+
try {
|
|
70
|
+
json = JSON.parse(body);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return { error: unavailable('it did not return a forecast') };
|
|
74
|
+
}
|
|
75
|
+
const place = placeOf(json);
|
|
76
|
+
if (place === null)
|
|
77
|
+
return { content: notFound(location) };
|
|
78
|
+
const qualifier = qualifierOf(location);
|
|
79
|
+
const verdict = qualifier === null ? null : checkQualifier(qualifier, place);
|
|
80
|
+
const named = cityMatches(location, place);
|
|
81
|
+
if (verdict === 'contradiction' && !named) {
|
|
82
|
+
return {
|
|
83
|
+
content: `No such place: "${location}". ${WEATHER_HOST} resolved it to ` +
|
|
84
|
+
`${describePlace(place)}, which is somewhere else.`
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const verified = qualifier === null ? named : verdict === 'match';
|
|
88
|
+
return { content: renderReport(json, place, location, verified) };
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
if (ctx?.signal?.aborted)
|
|
92
|
+
return { error: 'weather_lookup cancelled' };
|
|
93
|
+
if (controller.signal.aborted) {
|
|
94
|
+
return { error: `weather_lookup timed out after ${LOOKUP_TIMEOUT_MS} ms` };
|
|
95
|
+
}
|
|
96
|
+
return { error: unavailable(err instanceof Error ? err.message : String(err)) };
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
clearTimeout(timeout);
|
|
100
|
+
ctx?.signal?.removeEventListener('abort', onCallerAbort);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function unavailable(reason) {
|
|
106
|
+
return `The weather service could not be reached: ${reason}.`;
|
|
107
|
+
}
|
|
108
|
+
function notFound(location) {
|
|
109
|
+
return `No such place: "${location}". ${WEATHER_HOST} does not know it.`;
|
|
110
|
+
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import defaultFetch from '#no-redirect-fetch';
|
|
2
2
|
import { AbortController } from 'bare-abort-controller';
|
|
3
|
-
import { lookupHost } from
|
|
4
|
-
import { AbortedError } from
|
|
5
|
-
import { extractEmbeddedState } from
|
|
6
|
-
import { extractPageText, looksLikeHtml } from
|
|
7
|
-
import { imageFileName, pageImageUrls } from
|
|
8
|
-
import { pageVideoRef } from
|
|
9
|
-
import { MAX_HTML_CHARS } from
|
|
10
|
-
import { BROWSER_UA } from
|
|
11
|
-
import { isRedirectStatus } from
|
|
12
|
-
import { resolveOutboundUrl } from
|
|
3
|
+
import { lookupHost } from '../dns-lookup.js';
|
|
4
|
+
import { AbortedError } from '../helpers/race-abort.js';
|
|
5
|
+
import { extractEmbeddedState } from '../web-fetch/embedded-state.js';
|
|
6
|
+
import { extractPageText, looksLikeHtml } from '../web-fetch/extract.js';
|
|
7
|
+
import { imageFileName, pageImageUrls } from '../web-fetch/images.js';
|
|
8
|
+
import { pageVideoRef } from '../web-fetch/video.js';
|
|
9
|
+
import { MAX_HTML_CHARS } from '../web-search/fetch-html.js';
|
|
10
|
+
import { BROWSER_UA } from '../web-search/user-agents.js';
|
|
11
|
+
import { isRedirectStatus } from './http-request/utils.js';
|
|
12
|
+
import { resolveOutboundUrl } from './url-guard.js';
|
|
13
13
|
const SMALL_CONTEXT_TOKENS = 12_288;
|
|
14
14
|
const SMALL_CONTEXT_CHARS = 6_000;
|
|
15
15
|
const LARGE_CONTEXT_CHARS = 16_000;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import defaultFetch from '#fetch';
|
|
2
|
-
import { formatResultsForPrompt } from
|
|
3
|
-
import { searchWeb } from
|
|
2
|
+
import { formatResultsForPrompt } from '../web-search/format-results.js';
|
|
3
|
+
import { searchWeb } from '../web-search/orchestrator.js';
|
|
4
4
|
const nodeFetch = async (url, init) => {
|
|
5
5
|
const res = await defaultFetch(url, init);
|
|
6
6
|
return { ok: res.ok, status: res.status, text: () => res.text() };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import c from 'compact-encoding';
|
|
2
2
|
import { Duplex } from 'streamx';
|
|
3
|
-
export { settleOnClose } from
|
|
3
|
+
export { settleOnClose } from './rpc-session.js';
|
|
4
4
|
export function clientSessions(pipe) {
|
|
5
5
|
let current = session(pipe);
|
|
6
6
|
pipe.on('data', (chunk) => current.push(chunk));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { decodeHtmlEntities } from
|
|
1
|
+
import { decodeHtmlEntities } from '../web-search/html-helpers.js';
|
|
2
2
|
const LDJSON_SCRIPT = /<script[^>]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
|
|
3
3
|
const JSON_SCRIPT = /<script[^>]*type=["']application\/json["'][^>]*>([\s\S]*?)<\/script>/gi;
|
|
4
4
|
const INLINE_SCRIPT = /<script\b(?![^>]*\bsrc=)(?![^>]*type=["']application\/(?:ld\+)?json["'])[^>]*>([\s\S]*?)<\/script>/gi;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { decodeHtmlEntities } from
|
|
1
|
+
import { decodeHtmlEntities } from '../web-search/html-helpers.js';
|
|
2
2
|
const META_TAG = /<meta\s[^>]*>/gi;
|
|
3
3
|
const IMAGE_ATTR = /(?:property|name)=["'](?:og:image(?::secure_url|:url)?|twitter:image)["']/i;
|
|
4
4
|
const META_CONTENT = /content=["']([^"']+)["']/i;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { decodeHtmlEntities } from
|
|
1
|
+
import { decodeHtmlEntities } from '../web-search/html-helpers.js';
|
|
2
2
|
const META_TAG = /<meta\s[^>]*>/gi;
|
|
3
3
|
const VIDEO_ATTR = /(?:property|name)=["']og:video(?::secure_url|:url)?["']/i;
|
|
4
4
|
const META_CONTENT = /content=["']([^"']+)["']/i;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { searchAol } from
|
|
2
|
-
import { searchBing } from
|
|
3
|
-
import { searchBrave } from
|
|
4
|
-
import { searchDuckDuckGo } from
|
|
5
|
-
import { searchGoogle } from
|
|
6
|
-
import { searchWikipedia } from
|
|
7
|
-
import { searchYahoo } from
|
|
1
|
+
import { searchAol } from './engines/aol.js';
|
|
2
|
+
import { searchBing } from './engines/bing.js';
|
|
3
|
+
import { searchBrave } from './engines/brave.js';
|
|
4
|
+
import { searchDuckDuckGo } from './engines/duckduckgo.js';
|
|
5
|
+
import { searchGoogle } from './engines/google.js';
|
|
6
|
+
import { searchWikipedia } from './engines/wikipedia.js';
|
|
7
|
+
import { searchYahoo } from './engines/yahoo.js';
|
|
8
8
|
export const DEFAULT_MAX_RESULTS = 8;
|
|
9
9
|
export const ENGINES = [
|
|
10
10
|
{ name: 'duckduckgo', fn: searchDuckDuckGo },
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { fetchHtml } from
|
|
2
|
-
import { clean } from
|
|
3
|
-
import { urlMatchesHost } from
|
|
1
|
+
import { fetchHtml } from '../fetch-html.js';
|
|
2
|
+
import { clean } from '../html-helpers.js';
|
|
3
|
+
import { urlMatchesHost } from '../url-helpers.js';
|
|
4
4
|
function decodeRuUrl(encoded) {
|
|
5
5
|
try {
|
|
6
6
|
const href = decodeURIComponent(encoded);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { fetchHtml } from
|
|
2
|
-
import { clean } from
|
|
3
|
-
import { urlMatchesHost } from
|
|
4
|
-
import { SAFARI_UA } from
|
|
1
|
+
import { fetchHtml } from '../fetch-html.js';
|
|
2
|
+
import { clean } from '../html-helpers.js';
|
|
3
|
+
import { urlMatchesHost } from '../url-helpers.js';
|
|
4
|
+
import { SAFARI_UA } from '../user-agents.js';
|
|
5
5
|
function isOrganicBingUrl(url) {
|
|
6
6
|
return url.startsWith('http') && !urlMatchesHost(url, 'bing.com') && !url.includes('msockid=');
|
|
7
7
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { fetchHtml } from
|
|
2
|
-
import { clean } from
|
|
1
|
+
import { fetchHtml } from '../fetch-html.js';
|
|
2
|
+
import { clean } from '../html-helpers.js';
|
|
3
3
|
function parseSnippetBlock(inner) {
|
|
4
4
|
const urlMatch = inner.match(/<a[^>]*href="(https?:\/\/(?!imgs\.search\.brave)[^"]*)"[^>]*>/i);
|
|
5
5
|
if (!urlMatch) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { fetchHtml } from
|
|
2
|
-
import { clean } from
|
|
3
|
-
import { urlMatchesHost } from
|
|
1
|
+
import { fetchHtml } from '../fetch-html.js';
|
|
2
|
+
import { clean } from '../html-helpers.js';
|
|
3
|
+
import { urlMatchesHost } from '../url-helpers.js';
|
|
4
4
|
function extractDdgUrl(raw) {
|
|
5
5
|
if (raw.includes('uddg=')) {
|
|
6
6
|
const m = raw.match(/uddg=([^&]+)/);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { fetchHtml } from
|
|
2
|
-
import { clean } from
|
|
3
|
-
import { urlMatchesHost } from
|
|
4
|
-
import { GOOGLE_GSA_UA } from
|
|
1
|
+
import { fetchHtml } from '../fetch-html.js';
|
|
2
|
+
import { clean } from '../html-helpers.js';
|
|
3
|
+
import { urlMatchesHost } from '../url-helpers.js';
|
|
4
|
+
import { GOOGLE_GSA_UA } from '../user-agents.js';
|
|
5
5
|
function resolveGoogleUrl(rawUrl) {
|
|
6
6
|
if (rawUrl.startsWith('/url?q=')) {
|
|
7
7
|
return decodeURIComponent(rawUrl.slice(7).split('&')[0] || '');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { fetchJson } from
|
|
2
|
-
import { clean } from
|
|
1
|
+
import { fetchJson } from '../fetch-html.js';
|
|
2
|
+
import { clean } from '../html-helpers.js';
|
|
3
3
|
function extractSearchItems(data) {
|
|
4
4
|
if (typeof data !== 'object' || data === null) {
|
|
5
5
|
return [];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { fetchHtml } from
|
|
2
|
-
import { clean } from
|
|
3
|
-
import { urlMatchesHost } from
|
|
1
|
+
import { fetchHtml } from '../fetch-html.js';
|
|
2
|
+
import { clean } from '../html-helpers.js';
|
|
3
|
+
import { urlMatchesHost } from '../url-helpers.js';
|
|
4
4
|
function decodeRuUrl(encoded) {
|
|
5
5
|
try {
|
|
6
6
|
const href = decodeURIComponent(encoded);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AbortController } from 'bare-abort-controller';
|
|
2
|
-
import { BROWSER_UA } from
|
|
2
|
+
import { BROWSER_UA } from './user-agents.js';
|
|
3
3
|
export const ENGINE_TIMEOUT_MS = 8000;
|
|
4
4
|
export const MAX_HTML_CHARS = 2_000_000;
|
|
5
5
|
export class TimeoutError extends Error {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { DEFAULT_MAX_RESULTS, ENGINES } from
|
|
2
|
-
import { TimeoutError } from
|
|
3
|
-
import { mergeResults } from
|
|
1
|
+
import { DEFAULT_MAX_RESULTS, ENGINES } from './engine-registry.js';
|
|
2
|
+
import { TimeoutError } from './fetch-html.js';
|
|
3
|
+
import { mergeResults } from './merge-results.js';
|
|
4
4
|
export function classifyEngineError(reason) {
|
|
5
5
|
return reason instanceof TimeoutError ? 'timeout' : 'error';
|
|
6
6
|
}
|
|
@@ -2,7 +2,7 @@ import fs from 'bare-fs';
|
|
|
2
2
|
import path from 'bare-path';
|
|
3
3
|
import process from 'bare-process';
|
|
4
4
|
import { spawnSync } from 'bare-subprocess';
|
|
5
|
-
import { previousReleaseTags } from
|
|
5
|
+
import { previousReleaseTags } from './release-tags.js';
|
|
6
6
|
export function rewindSpec(root, dirs, { carries, keep }) {
|
|
7
7
|
if (process.env.SCHEMA_NO_BASELINE)
|
|
8
8
|
return [];
|
package/dist-lib/schema/build.js
CHANGED
|
@@ -3,9 +3,9 @@ import { fileURLToPath } from 'bare-url';
|
|
|
3
3
|
import HRPCBuilder from 'hrpc';
|
|
4
4
|
import Hyperschema from 'hyperschema';
|
|
5
5
|
import HyperschemaTS from 'hyperschema-ts';
|
|
6
|
-
import { resetHrpcIds, rewindSpec, unstageSpec } from
|
|
7
|
-
import { writeBind, writeClient } from
|
|
8
|
-
import { registerHarnessApi, registerHarnessTypes } from
|
|
6
|
+
import { resetHrpcIds, rewindSpec, unstageSpec } from './baseline.js';
|
|
7
|
+
import { writeBind, writeClient } from './generate.js';
|
|
8
|
+
import { registerHarnessApi, registerHarnessTypes } from './harness.js';
|
|
9
9
|
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
10
10
|
const ROOT = path.join(dir, '..');
|
|
11
11
|
const SPEC_DIR = path.join(dir, '..', 'spec');
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { Version } from 'bare-semver';
|
|
2
|
+
export declare function currentReleaseVersion(root: string): Version;
|
|
2
3
|
export declare function previousReleaseTags(root: string, { count, carries }: {
|
|
3
4
|
count?: number;
|
|
4
5
|
carries: string;
|
|
@@ -2,24 +2,26 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { spawnSync } from 'child_process';
|
|
4
4
|
import { Version } from 'bare-semver';
|
|
5
|
-
export function
|
|
5
|
+
export function currentReleaseVersion(root) {
|
|
6
6
|
const file = path.join(root, 'package.json');
|
|
7
7
|
const { version } = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
try {
|
|
9
|
+
return Version.parse(version);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
throw new Error(`no release version in package version ${version}`);
|
|
13
|
+
}
|
|
12
14
|
}
|
|
13
15
|
const TAG_PREFIX = 'v';
|
|
14
16
|
export function previousReleaseTags(root, { count = 3, carries }) {
|
|
15
|
-
const current =
|
|
17
|
+
const current = currentReleaseVersion(root);
|
|
16
18
|
const listed = spawnSync('git', ['tag', '-l', `${TAG_PREFIX}*`], { cwd: root });
|
|
17
19
|
if (listed.status !== 0)
|
|
18
20
|
throw new Error(`git tag -l failed: ${listed.stderr ?? ''}`);
|
|
19
21
|
const newestPerLine = new Map();
|
|
20
22
|
for (const line of (listed.stdout?.toString() ?? '').split('\n')) {
|
|
21
23
|
const parsed = parseTag(line.trim());
|
|
22
|
-
if (!parsed || parsed.
|
|
24
|
+
if (!parsed || Version.compare(parsed.version, current) > 0)
|
|
23
25
|
continue;
|
|
24
26
|
const winner = newestPerLine.get(parsed.line);
|
|
25
27
|
if (!winner || Version.compare(parsed.version, winner.version) > 0) {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: ADR-2026-05-29-kvcache-tool-turns
|
|
3
|
+
status: superseded
|
|
4
|
+
introduced_by: PR-228
|
|
5
|
+
supersedes: []
|
|
6
|
+
superseded_by: [ADR-2026-06-02-kvcache-tool-turns-reenabled]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# KV-cache session is disabled for tool-using completions
|
|
10
|
+
|
|
11
|
+
> **Superseded** by
|
|
12
|
+
> [ADR-2026-06-02-kvcache-tool-turns-reenabled](./2026-06-02-kvcache-tool-turns-reenabled.md):
|
|
13
|
+
> the cache is re-enabled for tool turns now that models load with
|
|
14
|
+
> `toolsMode: "dynamic"` (the SDK's DYNAMIC path is coherent, including across
|
|
15
|
+
> reload). Kept for history.
|
|
16
|
+
|
|
17
|
+
## Decision
|
|
18
|
+
|
|
19
|
+
The `@qvac/sdk` session prompt cache (`completion({ kvCache })`) is passed
|
|
20
|
+
**only for tool-free completions** — `kvCache` is set only when
|
|
21
|
+
`tools.length === 0`. Agentic / tool-using turns always run without the session
|
|
22
|
+
cache.
|
|
23
|
+
|
|
24
|
+
## Context
|
|
25
|
+
|
|
26
|
+
The SDK session cache has two paths (see
|
|
27
|
+
`@qvac/sdk → server/bare/plugins/llamacpp-completion/ops/completion-stream.ts` in the SDK):
|
|
28
|
+
|
|
29
|
+
- **STATIC** (no `tools`): on a cache hit it sends `history.slice(savedCount)`
|
|
30
|
+
and self-heals a stale boundary by re-sending the full history. Robust against
|
|
31
|
+
our reconstructed history.
|
|
32
|
+
- **DYNAMIC** (`tools` present): on a cache hit it sends only a role-based
|
|
33
|
+
_fragment_ of the history and trusts the SDK's own tool-chain flow to have
|
|
34
|
+
anchored the rest of the cache.
|
|
35
|
+
|
|
36
|
+
Our agentic loop builds (and on reload reconstructs from a persisted transcript)
|
|
37
|
+
the full history its own way. That does not satisfy the DYNAMIC path's
|
|
38
|
+
assumptions, so on a tool-cache hit the model receives an incoherent prompt and
|
|
39
|
+
emits `<tool_call>` markup as plain text instead of a parseable tool call —
|
|
40
|
+
silently breaking tool calling (no parse, no approval, no execution). Tools are
|
|
41
|
+
text-parsed (`toolCalling: "textParse"`), so a corrupted prompt degrades to a
|
|
42
|
+
plain assistant message rather than erroring.
|
|
43
|
+
|
|
44
|
+
## Alternatives Considered
|
|
45
|
+
|
|
46
|
+
- **Always-on per-chat `kvCache`** (the state that introduced the regression):
|
|
47
|
+
breaks tool calling as above.
|
|
48
|
+
- **Auto-cache (`kvCache: true`)**: tool-call turns are never cached, and a
|
|
49
|
+
verbatim `cacheableAssistantContent` echo is required for a hit —
|
|
50
|
+
incompatible with our thinking-stripped reconstruction.
|
|
51
|
+
- **Conform to the SDK's tool-chain contract** (collapse consecutive messages +
|
|
52
|
+
`deleteCache` on divergence, like the legacy desktop worker): viable but a
|
|
53
|
+
separate project, tracked in `.ai/cursor/plans/kvcache-tool-turn-reuse.md`.
|
|
54
|
+
|
|
55
|
+
## Consequences
|
|
56
|
+
|
|
57
|
+
- Tool turns re-prefill the full history each call (no session-cache reuse).
|
|
58
|
+
Tool calling stays correct; this is the prior known-good behavior.
|
|
59
|
+
- Tool-free turns (plain chat / title / summary) still benefit from the STATIC
|
|
60
|
+
cache path.
|
|
61
|
+
- Regression guards are GPU-free and mock-based:
|
|
62
|
+
`qvac-app → core/src/services/message/agentic-completion.test.ts` (argument-contract:
|
|
63
|
+
never send `kvCache` with `tools`) and the spec-fake
|
|
64
|
+
`qvac-app → core/src/services/message/__fixtures__/fake-completion-sdk.ts` (reproduces the
|
|
65
|
+
cache-hit tool-parse failure).
|
|
66
|
+
- Revisit if the SDK adds a "send full history, you slice it" mode for tools, or
|
|
67
|
+
when the conformance project lands.
|
|
68
|
+
|
|
69
|
+
## When Agents Should Read This
|
|
70
|
+
|
|
71
|
+
Read this before changing how `kvCache` is passed to `sdk.completion`, the
|
|
72
|
+
agentic loop's history construction, or transcript reconstruction —
|
|
73
|
+
`qvac-app → core/src/services/message/agentic-completion.ts`,
|
|
74
|
+
`qvac-app → core/src/services/message/agentic-loop.ts`,
|
|
75
|
+
`qvac-app → core/src/services/common/transcript.ts`.
|