@qvac/harness 0.2.8 → 0.2.10

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 (76) hide show
  1. package/README.md +15 -7
  2. package/dist/harness.bundle +13 -12
  3. package/dist-lib/index.d.ts +2 -1
  4. package/dist-lib/index.js +2 -1
  5. package/dist-lib/lib/capabilities/completion.d.ts +36 -2
  6. package/dist-lib/lib/capabilities/completion.js +150 -27
  7. package/dist-lib/lib/connect.js +3 -1
  8. package/dist-lib/lib/contract-types.d.ts +2 -0
  9. package/dist-lib/lib/docx/index.js +4 -27
  10. package/dist-lib/lib/hooks/built-in/index.d.ts +1 -0
  11. package/dist-lib/lib/hooks/built-in/index.js +1 -0
  12. package/dist-lib/lib/hooks/built-in/sandbox.d.ts +5 -0
  13. package/dist-lib/lib/hooks/built-in/sandbox.js +43 -0
  14. package/dist-lib/lib/hooks/built-in/skill-catalog.js +7 -2
  15. package/dist-lib/lib/hooks/registry.js +2 -1
  16. package/dist-lib/lib/hooks/types.d.ts +1 -0
  17. package/dist-lib/lib/office/index.d.ts +6 -0
  18. package/dist-lib/lib/office/index.js +38 -0
  19. package/dist-lib/lib/office/pptx.d.ts +1 -0
  20. package/dist-lib/lib/office/pptx.js +26 -0
  21. package/dist-lib/lib/office/python.d.ts +7 -0
  22. package/dist-lib/lib/office/python.js +32 -0
  23. package/dist-lib/lib/office/xlsx.d.ts +1 -0
  24. package/dist-lib/lib/office/xlsx.js +26 -0
  25. package/dist-lib/lib/serve.js +1 -0
  26. package/dist-lib/lib/tools/edit-image.js +1 -1
  27. package/dist-lib/lib/tools/generate-image.js +1 -1
  28. package/dist-lib/lib/tools/generate-music.js +1 -1
  29. package/dist-lib/lib/tools/http-request.js +3 -3
  30. package/dist-lib/lib/tools/mcp-call.js +3 -3
  31. package/dist-lib/lib/tools/skill.js +4 -2
  32. package/dist-lib/lib/tools/skills/auth-resolver.d.ts +1 -0
  33. package/dist-lib/lib/tools/skills/auth-resolver.js +59 -29
  34. package/dist-lib/lib/tools/toolsets.d.ts +1 -0
  35. package/dist-lib/lib/tools/toolsets.js +1 -0
  36. package/dist-lib/lib/tools/weather-lookup.js +9 -3
  37. package/dist-lib/lib/tools/web-fetch.js +6 -3
  38. package/dist-lib/lib/tools/web-search.js +5 -2
  39. package/dist-lib/lib/wire.js +28 -0
  40. package/dist-lib/schema/harness.js +7 -3
  41. package/dist-lib/spec/hrpc/messages.js +28 -10
  42. package/dist-lib/spec/hyperschema/index.js +28 -10
  43. package/dist-lib/spec/hyperschema/schema.json +19 -1
  44. package/dist-lib/spec/hyperschema/types.d.ts +4 -2
  45. package/package.json +10 -6
  46. package/.claude/skills/asana/SKILL.md +0 -86
  47. package/.claude/skills/gh-stack/SKILL.md +0 -181
  48. package/.claude/skills/gh-stack/references/commands.md +0 -177
  49. package/.claude/skills/gh-stack/references/stack-design.md +0 -97
  50. package/.claude/skills/gh-stack/references/troubleshooting.md +0 -159
  51. package/.claude/skills/js-house-style/SKILL.md +0 -671
  52. package/.claude/skills/kvcache/SKILL.md +0 -171
  53. package/.claude/skills/pr-create/SKILL.md +0 -180
  54. package/.claude/skills/pr-review/SKILL.md +0 -414
  55. package/CLAUDE.md +0 -39
  56. package/docs/decisions/2026-05-29-kvcache-tool-turns.md +0 -75
  57. package/docs/decisions/2026-06-02-kvcache-tool-turns-reenabled.md +0 -127
  58. package/docs/decisions/2026-06-23-android-cpu-llm-defaults.md +0 -126
  59. package/docs/decisions/2026-07-29-assistant-owned-storage-layout.md +0 -88
  60. package/docs/decisions/2026-08-04-sidecar-orphan-reaping.md +0 -79
  61. package/docs/decisions/2026-08-11-pyodide-worker-thread.md +0 -68
  62. package/docs/decisions/2026-08-17-harness-hook-pipeline.md +0 -84
  63. package/docs/decisions/2026-08-17-harness-outbound-egress-guard.md +0 -176
  64. package/docs/decisions/2026-08-20-chat-prompt-prefix-is-stable.md +0 -95
  65. package/docs/decisions/2026-08-20-kvcache-static-placement.md +0 -109
  66. package/docs/decisions/2026-08-21-vendored-python-wheels.md +0 -85
  67. package/docs/decisions/2026-08-26-exec-is-not-gated-by-skills.md +0 -64
  68. package/docs/decisions/2026-08-27-writable-skills-overlay.md +0 -56
  69. package/docs/decisions/2026-09-01-skill-operation-manifests.md +0 -53
  70. package/docs/decisions/2026-09-01-usage-breakdown-is-measured.md +0 -78
  71. package/docs/decisions/2026-09-03-usage-breakdown-reported-by-the-harness.md +0 -136
  72. package/docs/decisions/2026-09-10-skill-wording-is-measured-offline.md +0 -63
  73. package/docs/decisions/2026-09-16-a-lookup-reports-the-place-it-resolved.md +0 -70
  74. package/docs/decisions/INDEX.md +0 -40
  75. package/docs/decisions/TEMPLATE.md +0 -32
  76. package/docs/kvcache-reference.md +0 -655
@@ -0,0 +1,38 @@
1
+ import path from 'path';
2
+ import { extractDocxText } from '../docx/index.js';
3
+ import { extractWithPython } from './python.js';
4
+ import { XLSX_EXTRACT } from './xlsx.js';
5
+ import { PPTX_EXTRACT } from './pptx.js';
6
+ export { closePythonWorker as closeOfficeWorker } from '../tools/exec/adapters/python/worker/client.js';
7
+ const OFFICE_MIME_EXTENSIONS = {
8
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
9
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
10
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx'
11
+ };
12
+ export async function extractOfficeText(bytes, fileName, options = {}) {
13
+ const mimeType = options.mimeType?.split(';')[0]?.trim().toLowerCase();
14
+ const suffix = path.extname(fileName).toLowerCase();
15
+ const extension = ['.docx', '.xlsx', '.pptx'].includes(suffix)
16
+ ? suffix
17
+ : OFFICE_MIME_EXTENSIONS[mimeType ?? ''];
18
+ if (extension === '.docx') {
19
+ return extractDocxText(bytes, fileName, options);
20
+ }
21
+ if (extension === '.xlsx') {
22
+ return extractWithPython(bytes, fileName, {
23
+ format: 'XLSX',
24
+ packages: ['openpyxl==3.1.5'],
25
+ source: XLSX_EXTRACT,
26
+ signal: options.signal
27
+ });
28
+ }
29
+ if (extension === '.pptx') {
30
+ return extractWithPython(bytes, fileName, {
31
+ format: 'PPTX',
32
+ packages: ['python-pptx==1.0.2'],
33
+ source: PPTX_EXTRACT,
34
+ signal: options.signal
35
+ });
36
+ }
37
+ throw new Error(`Unsupported Office document: ${fileName}`);
38
+ }
@@ -0,0 +1 @@
1
+ export declare const PPTX_EXTRACT: string;
@@ -0,0 +1,26 @@
1
+ export const PPTX_EXTRACT = `from pptx import Presentation
2
+ from pptx.enum.shapes import MSO_SHAPE_TYPE
3
+ from pathlib import Path
4
+
5
+ def text(shapes):
6
+ for shape in shapes:
7
+ if shape.shape_type == MSO_SHAPE_TYPE.GROUP:
8
+ yield from text(shape.shapes)
9
+ elif shape.has_text_frame:
10
+ if shape.text.strip():
11
+ yield shape.text
12
+ elif shape.has_table:
13
+ for row in shape.table.rows:
14
+ cells = ["" if cell.is_spanned else cell.text.replace("\\n", " ") for cell in row.cells]
15
+ if any(cell.strip() for cell in cells):
16
+ yield " | ".join(cells)
17
+
18
+ with Path("extracted.txt").open("w", encoding="utf-8") as output:
19
+ for number, slide in enumerate(Presentation("input.pptx").slides, 1):
20
+ heading = False
21
+ for block in text(slide.shapes):
22
+ if not heading:
23
+ output.write("Slide " + str(number) + "\\n")
24
+ heading = True
25
+ output.write(block + "\\n")
26
+ `;
@@ -0,0 +1,7 @@
1
+ import type { AbortSignal } from 'bare-abort-controller';
2
+ export declare function extractWithPython(bytes: Uint8Array, fileName: string, { format, packages, source, signal }: {
3
+ readonly format: string;
4
+ readonly packages: readonly string[];
5
+ readonly source: string;
6
+ readonly signal?: AbortSignal | undefined;
7
+ }): Promise<string>;
@@ -0,0 +1,32 @@
1
+ import b4a from 'b4a';
2
+ import fs from 'fs';
3
+ import { runPython } from '../tools/exec/adapters/python/pyodide.js';
4
+ import { planVendoredInstall, vendoredManifest, wheelsDir } from '../tools/exec/adapters/python/wheels.js';
5
+ const MAX_OFFICE_BYTES = 20 * 1024 * 1024;
6
+ export async function extractWithPython(bytes, fileName, { format, packages, source, signal }) {
7
+ if (bytes.byteLength > MAX_OFFICE_BYTES) {
8
+ throw new Error(`document is too large to extract: ${bytes.byteLength} bytes, limit ${MAX_OFFICE_BYTES}`);
9
+ }
10
+ const dir = wheelsDir();
11
+ const manifest = vendoredManifest(dir);
12
+ const install = planVendoredInstall(packages, { dir, manifest });
13
+ if (!dir ||
14
+ !manifest ||
15
+ !install ||
16
+ !['micropip', ...install.packages].every((name) => Object.keys(manifest.files).some((file) => file.replaceAll('_', '-').startsWith(`${name}-`) && fs.existsSync(`${dir}/${file}`)))) {
17
+ throw new Error(`bundled ${format} extraction packages are unavailable`);
18
+ }
19
+ const result = await runPython(source, {
20
+ packages,
21
+ inputs: [{ path: `input.${format.toLowerCase()}`, bytes }],
22
+ outputs: ['extracted.txt'],
23
+ signal
24
+ });
25
+ if (result.exitCode !== 0)
26
+ throw new Error(`${fileName}: ${result.stderr.trim()}`);
27
+ const output = result.files?.get('extracted.txt');
28
+ if (!output) {
29
+ throw new Error(result.rejected?.[0]?.reason ?? `${format} extraction produced no text file`);
30
+ }
31
+ return b4a.toString(output, 'utf8');
32
+ }
@@ -0,0 +1 @@
1
+ export declare const XLSX_EXTRACT: string;
@@ -0,0 +1,26 @@
1
+ export const XLSX_EXTRACT = `from openpyxl import load_workbook
2
+ from pathlib import Path
3
+
4
+ formulas = load_workbook("input.xlsx", read_only=True, data_only=False, keep_links=False)
5
+ values = load_workbook("input.xlsx", read_only=True, data_only=True, keep_links=False)
6
+ try:
7
+ with Path("extracted.txt").open("w", encoding="utf-8") as output:
8
+ for sheet in formulas.worksheets:
9
+ heading = False
10
+ cached = values[sheet.title]
11
+ sheet.reset_dimensions()
12
+ cached.reset_dimensions()
13
+ for cells, results in zip(sheet.iter_rows(), cached.iter_rows()):
14
+ row = [result.value if cell.data_type == "f" and result.value is not None else cell.value
15
+ for cell, result in zip(cells, results)]
16
+ if not any(value is not None and str(value).strip() for value in row):
17
+ continue
18
+ if not heading:
19
+ output.write("Sheet: " + sheet.title + "\\n")
20
+ heading = True
21
+ output.write(" | ".join("" if value is None else str(value).replace("\\n", " ")
22
+ for value in row) + "\\n")
23
+ finally:
24
+ formulas.close()
25
+ values.close()
26
+ `;
@@ -414,6 +414,7 @@ function completion(harness) {
414
414
  thinkingDefault: frame.thinkingDefault ?? undefined,
415
415
  generationParams: frame.generationParams ?? undefined,
416
416
  responseFormat: frame.responseFormat ?? undefined,
417
+ captureInferenceCalls: frame.captureInferenceCalls ?? undefined,
417
418
  messages: (frame.messages ?? []).map(({ role, content, attachments }) => ({
418
419
  role,
419
420
  content,
@@ -170,7 +170,7 @@ export function createEditImageTool(runtime, { loadMedia = defaultLoadMedia, res
170
170
  if (sources.length > 1 && !inContext)
171
171
  return { error: MULTI_SOURCE_UNSUPPORTED };
172
172
  if (!runtime.acquire())
173
- return { error: BUSY_ERROR };
173
+ return { error: BUSY_ERROR, code: 'busy' };
174
174
  const report = ctx.progress;
175
175
  const onTick = report && generationPercentTick((percentage) => report({ percentage }), sources.length);
176
176
  try {
@@ -323,7 +323,7 @@ export function createGenerateImageTool(runtime, clamps) {
323
323
  if (!ctx?.attach)
324
324
  return { error: 'this device cannot persist generated images' };
325
325
  if (!runtime.acquire())
326
- return { error: BUSY_ERROR };
326
+ return { error: BUSY_ERROR, code: 'busy' };
327
327
  const report = ctx.progress;
328
328
  const onTick = report && generationPercentTick((percentage) => report({ percentage }));
329
329
  try {
@@ -443,7 +443,7 @@ export function createGenerateMusicTool(runtime, clamps) {
443
443
  if (!ctx?.attach)
444
444
  return { error: 'this device cannot persist generated audio' };
445
445
  if (!runtime.acquire())
446
- return { error: BUSY_ERROR };
446
+ return { error: BUSY_ERROR, code: 'busy' };
447
447
  const report = ctx.progress;
448
448
  const onTick = report && musicPercentTick((percentage) => report({ percentage }));
449
449
  const capMax = stepsCap ?? MAX_STEPS;
@@ -163,10 +163,10 @@ export function createHttpRequestTool({ fetch: fetchImpl, credentialStore, crede
163
163
  }
164
164
  catch (err) {
165
165
  const error = err instanceof Error ? err : new Error(String(err));
166
+ const timedOut = error.name === 'AbortError' || error instanceof AbortedError;
166
167
  return {
167
- error: error.name === 'AbortError' || error instanceof AbortedError
168
- ? 'request timed out'
169
- : `request failed: ${error.message}`
168
+ error: timedOut ? 'request timed out' : `request failed: ${error.message}`,
169
+ code: timedOut ? 'timeout' : 'transport'
170
170
  };
171
171
  }
172
172
  finally {
@@ -141,10 +141,10 @@ export function createMcpCallTool({ fetch: fetchImpl, credentialStore, credentia
141
141
  }
142
142
  catch (err) {
143
143
  const error = err instanceof Error ? err : new Error(String(err));
144
+ const timedOut = error.name === 'AbortError' || error instanceof AbortedError;
144
145
  return {
145
- error: error.name === 'AbortError' || error instanceof AbortedError
146
- ? 'request timed out'
147
- : `request failed: ${error.message}`
146
+ error: timedOut ? 'request timed out' : `request failed: ${error.message}`,
147
+ code: timedOut ? 'timeout' : 'transport'
148
148
  };
149
149
  }
150
150
  finally {
@@ -131,8 +131,10 @@ export function createSkillTool({ skillsDir, overlayDir = resolveSkillsOverlayDi
131
131
  if (file === MAIN_FILE)
132
132
  return { error: `skill not found: ${name}` };
133
133
  const available = await listSkillFiles([overlayDrive, baseDrive], name);
134
- const listing = available.length ? ` — files this skill has: ${available.join(', ')}` : '';
135
- return { error: `file not found in skill ${name}: ${file}${listing}` };
134
+ const rest = available.length
135
+ ? ` — files this skill has: ${available.join(', ')}`
136
+ : '. This skill has no other files; you already have its full instructions — act now.';
137
+ return { error: `file not found in skill ${name}: ${file}${rest}` };
136
138
  }
137
139
  }
138
140
  };
@@ -19,6 +19,7 @@ export declare function isCredentialUrlAllowed(target: string, patterns: readonl
19
19
  export type CredentialAllowListFn = (ctx?: ToolExecuteContext) => CredentialAllowList;
20
20
  export type AuthOutcome = {
21
21
  readonly token: string;
22
+ readonly key: string;
22
23
  } | {
23
24
  readonly error: string;
24
25
  };
@@ -60,49 +60,79 @@ export function parseAuth(v) {
60
60
  }
61
61
  return { ok: true, value: { type: 'bearer', tokenCredentialKey: key } };
62
62
  }
63
- export async function resolveAuth(auth, url, credentialStore, credentialAllowList, ctx) {
64
- const key = auth.tokenCredentialKey;
65
- if (!credentialStore)
66
- return { error: 'credentials not configured' };
67
- const entry = credentialAllowList?.[key];
68
- if (!entry) {
63
+ function grantedKeys(url, credentialAllowList) {
64
+ return Object.entries(credentialAllowList)
65
+ .filter(([, entry]) => urlAllowed(url, Array.isArray(entry) ? entry : [entry]))
66
+ .map(([key]) => key);
67
+ }
68
+ async function connectedCredentials(keys, credentialStore, ctx) {
69
+ const connected = [];
70
+ for (const key of keys) {
71
+ const record = credentialRecord(await credentialStore(key, ctx));
72
+ if (record)
73
+ connected.push({ token: record.token, key });
74
+ }
75
+ return connected;
76
+ }
77
+ function ambiguous(keys) {
78
+ return {
79
+ error: `multiple credentials match this URL; set auth.tokenCredentialKey to one of ${keys.join(', ')}`
80
+ };
81
+ }
82
+ async function resolveGuessedKey(key, url, credentialStore, credentialAllowList, ctx) {
83
+ const granted = grantedKeys(url, credentialAllowList);
84
+ if (granted.length === 0) {
69
85
  return {
70
- error: `credential ${key} is not available here — load the skill that declares it first`
86
+ error: `unknown credential key ${key}; no loaded skill grants a credential for ${url.host} — omit auth, or load the skill that declares it`
71
87
  };
72
88
  }
89
+ const [first, ...rest] = await connectedCredentials(granted, credentialStore, ctx);
90
+ if (!first) {
91
+ return {
92
+ error: `unknown credential key ${key}; no connected credential for ${url.host} (${granted.join(', ')}). Tell the user to connect it in Skills; do not retry`
93
+ };
94
+ }
95
+ if (rest.length > 0)
96
+ return ambiguous([first, ...rest].map((c) => c.key));
97
+ return first;
98
+ }
99
+ async function resolveDeclaredKey(key, url, credentialStore, credentialAllowList, ctx) {
100
+ const entry = credentialAllowList?.[key];
101
+ if (!entry)
102
+ return null;
73
103
  const patterns = Array.isArray(entry) ? entry : [entry];
74
104
  if (!urlAllowed(url, patterns)) {
75
105
  return { error: `credential ${key} is not allowed for URL ${url.href}` };
76
106
  }
77
107
  const record = credentialRecord(await credentialStore(key, ctx));
78
- if (!record)
79
- return { error: `unknown credential: ${key}` };
80
- return { token: record.token };
108
+ if (!record) {
109
+ return {
110
+ error: `credential ${key} is not connected for ${url.host}. Tell the user to connect it in Skills; do not retry`
111
+ };
112
+ }
113
+ return { token: record.token, key };
114
+ }
115
+ export async function resolveAuth(auth, url, credentialStore, credentialAllowList, ctx) {
116
+ const key = auth.tokenCredentialKey;
117
+ if (!credentialStore)
118
+ return { error: 'credentials not configured' };
119
+ return ((await resolveDeclaredKey(key, url, credentialStore, credentialAllowList, ctx)) ??
120
+ resolveGuessedKey(key, url, credentialStore, credentialAllowList ?? {}, ctx));
81
121
  }
82
122
  export async function resolveAutoAuth(url, credentialStore, credentialAllowList, ctx) {
83
123
  if (!credentialStore || !credentialAllowList)
84
124
  return null;
85
- let selected = null;
86
- for (const [key, entry] of Object.entries(credentialAllowList)) {
87
- const patterns = Array.isArray(entry) ? entry : [entry];
88
- if (!urlAllowed(url, patterns))
89
- continue;
90
- const record = credentialRecord(await credentialStore(key, ctx));
91
- if (!record)
92
- continue;
93
- if (selected) {
94
- return { error: 'multiple credentials match this URL; set auth.tokenCredentialKey' };
95
- }
96
- selected = { token: record.token, key };
97
- }
98
- return selected;
125
+ const connected = await connectedCredentials(grantedKeys(url, credentialAllowList), credentialStore, ctx);
126
+ if (connected.length > 1)
127
+ return ambiguous(connected.map((c) => c.key));
128
+ return connected[0] ?? null;
99
129
  }
100
130
  export async function resolveBearerHeader(auth, url, credentialStore, credentialAllowList, ctx, signal) {
101
131
  if (auth) {
102
132
  const resolved = await raceAbort(resolveAuth(auth, url, credentialStore, credentialAllowList, ctx), signal);
103
133
  if ('error' in resolved)
104
134
  return { error: resolved.error };
105
- return { header: `Bearer ${resolved.token}`, credentialKey: auth.tokenCredentialKey };
135
+ return { header: `Bearer ${resolved.token}`, credentialKey: resolved.key };
106
136
  }
107
137
  const resolved = await raceAbort(resolveAutoAuth(url, credentialStore, credentialAllowList, ctx), signal);
108
138
  if (resolved && 'error' in resolved)
@@ -112,10 +142,10 @@ export async function resolveBearerHeader(auth, url, credentialStore, credential
112
142
  : { header: null };
113
143
  }
114
144
  export async function resolveRedirectBearerHeader(auth, url, credentialStore, credentialAllowList, ctx, signal) {
115
- if (!auth)
145
+ if (!auth || !credentialStore)
116
146
  return { header: null };
117
- const resolved = await raceAbort(resolveAuth(auth, url, credentialStore, credentialAllowList, ctx), signal);
118
- return 'error' in resolved
147
+ const resolved = await raceAbort(resolveDeclaredKey(auth.tokenCredentialKey, url, credentialStore, credentialAllowList, ctx), signal);
148
+ return !resolved || 'error' in resolved
119
149
  ? { header: null }
120
- : { header: `Bearer ${resolved.token}`, credentialKey: auth.tokenCredentialKey };
150
+ : { header: `Bearer ${resolved.token}`, credentialKey: resolved.key };
121
151
  }
@@ -4,5 +4,6 @@ export declare function asToolset(value?: string | null): Toolset | null;
4
4
  export interface ToolsetScoped {
5
5
  toolset?: string | null;
6
6
  }
7
+ export declare const NETWORK_TOOLSETS: ReadonlyArray<Toolset>;
7
8
  export declare function permittedTools<T extends ToolsetScoped>(tools: ReadonlyArray<T>, disabled?: ReadonlyArray<string> | null): T[];
8
9
  export declare function toolsetEnabled(toolset: Toolset, disabled?: ReadonlyArray<string> | null): boolean;
@@ -9,6 +9,7 @@ export const TOOLSETS = [
9
9
  export function asToolset(value) {
10
10
  return TOOLSETS.find((toolset) => toolset === value) ?? null;
11
11
  }
12
+ export const NETWORK_TOOLSETS = ['web-search', 'http-request', 'mcp-call'];
12
13
  export function permittedTools(tools, disabled) {
13
14
  if (!disabled?.length)
14
15
  return [...tools];
@@ -63,7 +63,7 @@ export function createWeatherLookupTool({ fetch: fetchImpl, resolveHost = lookup
63
63
  if (/location not found/i.test(body))
64
64
  return { content: notFound(location) };
65
65
  if (response.status !== 200) {
66
- return { error: unavailable(`it answered HTTP ${response.status}`) };
66
+ return { error: unavailable(`it answered HTTP ${response.status}`), code: 'transport' };
67
67
  }
68
68
  let json;
69
69
  try {
@@ -91,9 +91,15 @@ export function createWeatherLookupTool({ fetch: fetchImpl, resolveHost = lookup
91
91
  if (ctx?.signal?.aborted)
92
92
  return { error: 'weather_lookup cancelled' };
93
93
  if (controller.signal.aborted) {
94
- return { error: `weather_lookup timed out after ${LOOKUP_TIMEOUT_MS} ms` };
94
+ return {
95
+ error: `weather_lookup timed out after ${LOOKUP_TIMEOUT_MS} ms`,
96
+ code: 'timeout'
97
+ };
95
98
  }
96
- return { error: unavailable(err instanceof Error ? err.message : String(err)) };
99
+ return {
100
+ error: unavailable(err instanceof Error ? err.message : String(err)),
101
+ code: 'transport'
102
+ };
97
103
  }
98
104
  finally {
99
105
  clearTimeout(timeout);
@@ -79,7 +79,10 @@ export function createWebFetchTool({ fetch: fetchImpl, resolveHost = lookupHost
79
79
  return { error: got.error };
80
80
  const { response, finalUrl: currentUrl } = got;
81
81
  if (response.status < 200 || response.status >= 300) {
82
- return { error: `HTTP ${response.status} fetching ${currentUrl.toString()}` };
82
+ return {
83
+ error: `HTTP ${response.status} fetching ${currentUrl.toString()}`,
84
+ status: response.status
85
+ };
83
86
  }
84
87
  const rawBody = await response.text();
85
88
  const body = rawBody.length > MAX_HTML_CHARS ? rawBody.slice(0, MAX_HTML_CHARS) : rawBody;
@@ -140,9 +143,9 @@ export function createWebFetchTool({ fetch: fetchImpl, resolveHost = lookupHost
140
143
  return { error: 'web_fetch cancelled' };
141
144
  }
142
145
  if (controller.signal.aborted) {
143
- return { error: `${url.hostname} timed out after ${PAGE_TIMEOUT_MS}ms` };
146
+ return { error: `${url.hostname} timed out after ${PAGE_TIMEOUT_MS}ms`, code: 'timeout' };
144
147
  }
145
- return { error: err instanceof Error ? err.message : String(err) };
148
+ return { error: err instanceof Error ? err.message : String(err), code: 'transport' };
146
149
  }
147
150
  finally {
148
151
  clearTimeout(timeout);
@@ -43,12 +43,15 @@ export function createWebSearchTool({ fetch } = {}) {
43
43
  }
44
44
  const failed = engineSummary.filter((e) => e.status !== 'ok').length;
45
45
  if (engineSummary.length > 0 && failed === engineSummary.length) {
46
- return { error: `web_search unavailable: all ${failed} engines failed (timeout/error)` };
46
+ return {
47
+ error: `web_search unavailable: all ${failed} engines failed (timeout/error)`,
48
+ code: 'transport'
49
+ };
47
50
  }
48
51
  return { content: `No web results found for "${query}".` };
49
52
  }
50
53
  catch (err) {
51
- return { error: err instanceof Error ? err.message : String(err) };
54
+ return { error: err instanceof Error ? err.message : String(err), code: 'transport' };
52
55
  }
53
56
  }
54
57
  };
@@ -32,6 +32,20 @@ export function toWire(event) {
32
32
  const { type, ...metrics } = event;
33
33
  return { type, metrics };
34
34
  }
35
+ case 'inference-call':
36
+ return { type: 'inference-call', payload: event.call };
37
+ case 'inference-sent':
38
+ return {
39
+ type: 'inference-sent',
40
+ payload: {
41
+ round: event.round,
42
+ droppedTurns: event.droppedTurns,
43
+ messages: event.messages,
44
+ tokens: event.tokens,
45
+ outcome: event.outcome,
46
+ reason: event.reason
47
+ }
48
+ };
35
49
  case 'error':
36
50
  return { type: 'error', message: event.message };
37
51
  case 'aborted':
@@ -73,6 +87,20 @@ export function fromWire(event) {
73
87
  };
74
88
  case 'metrics':
75
89
  return { type: 'metrics', ...event.metrics };
90
+ case 'inference-call':
91
+ return { type: 'inference-call', call: event.payload };
92
+ case 'inference-sent': {
93
+ const sent = (event.payload ?? {});
94
+ return {
95
+ type: 'inference-sent',
96
+ messages: sent.messages,
97
+ round: Number(sent.round ?? 0),
98
+ droppedTurns: Number(sent.droppedTurns ?? 0),
99
+ tokens: sent.tokens,
100
+ outcome: sent.outcome || 'completed',
101
+ reason: sent.reason || undefined
102
+ };
103
+ }
76
104
  case 'error':
77
105
  return { type: 'error', message: event.message ?? 'harness error' };
78
106
  case 'aborted':
@@ -23,7 +23,8 @@ export function registerHarnessTypes(ns) {
23
23
  { name: 'imageWidth', type: 'uint' },
24
24
  { name: 'imageHeight', type: 'uint' },
25
25
  { name: 'approvalMode', type: '@harness/approval-mode' },
26
- { name: 'disabledToolsets', type: 'string', array: true }
26
+ { name: 'disabledToolsets', type: 'string', array: true },
27
+ { name: 'sandboxMode', type: 'bool' }
27
28
  ]
28
29
  });
29
30
  ns.register({
@@ -107,7 +108,8 @@ export function registerHarnessTypes(ns) {
107
108
  { name: 'grantedDirs', type: 'string', array: true },
108
109
  { name: 'grantedDrives', type: '@harness/granted-drive', array: true },
109
110
  { name: 'payload', type: 'json' },
110
- { name: 'grantedWriteDirs', type: 'string', array: true }
111
+ { name: 'grantedWriteDirs', type: 'string', array: true },
112
+ { name: 'captureInferenceCalls', type: 'bool' }
111
113
  ]
112
114
  });
113
115
  ns.register({
@@ -128,7 +130,9 @@ export function registerHarnessTypes(ns) {
128
130
  'question-ask',
129
131
  'done',
130
132
  'tool-activity',
131
- 'drive-ask'
133
+ 'drive-ask',
134
+ 'inference-call',
135
+ 'inference-sent'
132
136
  ],
133
137
  strings: true
134
138
  });
@@ -1,12 +1,12 @@
1
1
  // This file is autogenerated by the hyperschema compiler
2
- // Schema Version: 8
2
+ // Schema Version: 10
3
3
  /* eslint-disable camelcase */
4
4
  /* eslint-disable quotes */
5
5
  /* eslint-disable space-before-function-paren */
6
6
 
7
7
  import { c } from 'hyperschema/runtime'
8
8
 
9
- const VERSION = 8
9
+ const VERSION = 10
10
10
 
11
11
  // eslint-disable-next-line no-unused-vars
12
12
  let version = VERSION
@@ -143,7 +143,8 @@ const encoding4 = {
143
143
  (m.imageWidth ? 8192 : 0) |
144
144
  (m.imageHeight ? 16384 : 0) |
145
145
  ((version >= 5 && m.approvalMode) ? 32768 : 0) |
146
- ((version >= 5 && m.disabledToolsets) ? 65536 : 0)
146
+ ((version >= 5 && m.disabledToolsets) ? 65536 : 0) |
147
+ ((version >= 10 && m.sandboxMode) ? 131072 : 0)
147
148
 
148
149
  c.uint.preencode(state, flags)
149
150
 
@@ -183,7 +184,8 @@ const encoding4 = {
183
184
  (m.imageWidth ? 8192 : 0) |
184
185
  (m.imageHeight ? 16384 : 0) |
185
186
  ((version >= 5 && m.approvalMode) ? 32768 : 0) |
186
- ((version >= 5 && m.disabledToolsets) ? 65536 : 0)
187
+ ((version >= 5 && m.disabledToolsets) ? 65536 : 0) |
188
+ ((version >= 10 && m.sandboxMode) ? 131072 : 0)
187
189
 
188
190
  c.uint.encode(state, flags)
189
191
 
@@ -225,7 +227,8 @@ const encoding4 = {
225
227
  imageWidth: (flags & 8192) !== 0 ? c.uint.decode(state) : 0,
226
228
  imageHeight: (flags & 16384) !== 0 ? c.uint.decode(state) : 0,
227
229
  approvalMode: (version >= 5 && (flags & 32768) !== 0) ? encoding59.decode(state) : null,
228
- disabledToolsets: (version >= 5 && (flags & 65536) !== 0) ? encoding4_16.decode(state) : null
230
+ disabledToolsets: (version >= 5 && (flags & 65536) !== 0) ? encoding4_16.decode(state) : null,
231
+ sandboxMode: (version >= 10 && (flags & 131072) !== 0)
229
232
  }
230
233
  }
231
234
  }
@@ -478,7 +481,8 @@ const encoding11 = {
478
481
  ((version >= 5 && m.grantedDirs) ? 2097152 : 0) |
479
482
  ((version >= 5 && m.grantedDrives) ? 4194304 : 0) |
480
483
  ((version >= 5 && m.payload) ? 8388608 : 0) |
481
- ((version >= 5 && m.grantedWriteDirs) ? 16777216 : 0)
484
+ ((version >= 5 && m.grantedWriteDirs) ? 16777216 : 0) |
485
+ ((version >= 9 && m.captureInferenceCalls) ? 33554432 : 0)
482
486
 
483
487
  encoding10.preencode(state, m.type)
484
488
  c.uint.preencode(state, flags)
@@ -533,7 +537,8 @@ const encoding11 = {
533
537
  ((version >= 5 && m.grantedDirs) ? 2097152 : 0) |
534
538
  ((version >= 5 && m.grantedDrives) ? 4194304 : 0) |
535
539
  ((version >= 5 && m.payload) ? 8388608 : 0) |
536
- ((version >= 5 && m.grantedWriteDirs) ? 16777216 : 0)
540
+ ((version >= 5 && m.grantedWriteDirs) ? 16777216 : 0) |
541
+ ((version >= 9 && m.captureInferenceCalls) ? 33554432 : 0)
537
542
 
538
543
  encoding10.encode(state, m.type)
539
544
  c.uint.encode(state, flags)
@@ -592,7 +597,8 @@ const encoding11 = {
592
597
  grantedDirs: (version >= 5 && (flags & 2097152) !== 0) ? encoding11_22.decode(state) : null,
593
598
  grantedDrives: (version >= 5 && (flags & 4194304) !== 0) ? encoding11_23.decode(state) : null,
594
599
  payload: (version >= 5 && (flags & 8388608) !== 0) ? c.json.decode(state) : null,
595
- grantedWriteDirs: (version >= 5 && (flags & 16777216) !== 0) ? encoding11_25.decode(state) : null
600
+ grantedWriteDirs: (version >= 5 && (flags & 16777216) !== 0) ? encoding11_25.decode(state) : null,
601
+ captureInferenceCalls: (version >= 9 && (flags & 33554432) !== 0)
596
602
  }
597
603
  }
598
604
  }
@@ -613,13 +619,15 @@ const encoding12_enum = {
613
619
  "question-ask": 'question-ask',
614
620
  done: 'done',
615
621
  "tool-activity": 'tool-activity',
616
- "drive-ask": 'drive-ask'
622
+ "drive-ask": 'drive-ask',
623
+ "inference-call": 'inference-call',
624
+ "inference-sent": 'inference-sent'
617
625
  }
618
626
 
619
627
  // @harness/completion-event-type enum
620
628
  const encoding12 = {
621
629
  preencode (state, m) {
622
- state.end++ // max enum is 16 so always one byte
630
+ state.end++ // max enum is 18 so always one byte
623
631
  },
624
632
  encode (state, m) {
625
633
  switch (m) {
@@ -671,6 +679,12 @@ const encoding12 = {
671
679
  case 'drive-ask':
672
680
  c.uint.encode(state, 16)
673
681
  break
682
+ case 'inference-call':
683
+ c.uint.encode(state, 17)
684
+ break
685
+ case 'inference-sent':
686
+ c.uint.encode(state, 18)
687
+ break
674
688
  default:
675
689
  throw new Error('Unknown enum')
676
690
  }
@@ -709,6 +723,10 @@ const encoding12 = {
709
723
  return 'tool-activity'
710
724
  case 16:
711
725
  return 'drive-ask'
726
+ case 17:
727
+ return 'inference-call'
728
+ case 18:
729
+ return 'inference-sent'
712
730
  default: return null
713
731
  }
714
732
  }