@qvac/harness 0.7.3 → 0.8.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.
Files changed (106) hide show
  1. package/CHANGELOG.md +45 -1
  2. package/NOTICE.md +16 -0
  3. package/dist-lib/lib/capabilities/completion.d.ts +1 -0
  4. package/dist-lib/lib/capabilities/completion.js +28 -8
  5. package/dist-lib/lib/capabilities/tool-turns.d.ts +3 -0
  6. package/dist-lib/lib/capabilities/tool-turns.js +7 -0
  7. package/dist-lib/lib/context-window.js +2 -2
  8. package/dist-lib/lib/helpers/run-outcome.d.ts +4 -1
  9. package/dist-lib/lib/helpers/run-outcome.js +14 -8
  10. package/dist-lib/lib/helpers/tool-call-markup.js +4 -4
  11. package/dist-lib/lib/hooks/built-in/arg-repair.js +7 -0
  12. package/dist-lib/lib/office/engine/codes.d.ts +62 -0
  13. package/dist-lib/lib/office/engine/codes.js +61 -0
  14. package/dist-lib/lib/office/engine/docx/document.d.ts +216 -0
  15. package/dist-lib/lib/office/engine/docx/document.js +1152 -0
  16. package/dist-lib/lib/office/engine/docx/index.d.ts +7 -0
  17. package/dist-lib/lib/office/engine/docx/index.js +8 -0
  18. package/dist-lib/lib/office/engine/docx/styles.d.ts +31 -0
  19. package/dist-lib/lib/office/engine/docx/styles.js +173 -0
  20. package/dist-lib/lib/office/engine/docx/template.d.ts +18 -0
  21. package/dist-lib/lib/office/engine/docx/template.js +133 -0
  22. package/dist-lib/lib/office/engine/docx/text.d.ts +55 -0
  23. package/dist-lib/lib/office/engine/docx/text.js +538 -0
  24. package/dist-lib/lib/office/engine/drawingml/chart.d.ts +63 -0
  25. package/dist-lib/lib/office/engine/drawingml/chart.js +539 -0
  26. package/dist-lib/lib/office/engine/drawingml/text.d.ts +35 -0
  27. package/dist-lib/lib/office/engine/drawingml/text.js +352 -0
  28. package/dist-lib/lib/office/engine/errors.d.ts +6 -0
  29. package/dist-lib/lib/office/engine/errors.js +12 -0
  30. package/dist-lib/lib/office/engine/image.d.ts +8 -0
  31. package/dist-lib/lib/office/engine/image.js +48 -0
  32. package/dist-lib/lib/office/engine/index.d.ts +12 -0
  33. package/dist-lib/lib/office/engine/index.js +7 -0
  34. package/dist-lib/lib/office/engine/opc/content-types.d.ts +17 -0
  35. package/dist-lib/lib/office/engine/opc/content-types.js +74 -0
  36. package/dist-lib/lib/office/engine/opc/media.d.ts +13 -0
  37. package/dist-lib/lib/office/engine/opc/media.js +11 -0
  38. package/dist-lib/lib/office/engine/opc/package.d.ts +51 -0
  39. package/dist-lib/lib/office/engine/opc/package.js +324 -0
  40. package/dist-lib/lib/office/engine/opc/paths.d.ts +7 -0
  41. package/dist-lib/lib/office/engine/opc/paths.js +46 -0
  42. package/dist-lib/lib/office/engine/opc/properties.d.ts +7 -0
  43. package/dist-lib/lib/office/engine/opc/properties.js +75 -0
  44. package/dist-lib/lib/office/engine/opc/relationships.d.ts +23 -0
  45. package/dist-lib/lib/office/engine/opc/relationships.js +65 -0
  46. package/dist-lib/lib/office/engine/opc/zip.d.ts +15 -0
  47. package/dist-lib/lib/office/engine/opc/zip.js +264 -0
  48. package/dist-lib/lib/office/engine/pptx/fit.d.ts +12 -0
  49. package/dist-lib/lib/office/engine/pptx/fit.js +37 -0
  50. package/dist-lib/lib/office/engine/pptx/index.d.ts +8 -0
  51. package/dist-lib/lib/office/engine/pptx/index.js +8 -0
  52. package/dist-lib/lib/office/engine/pptx/inherit.d.ts +29 -0
  53. package/dist-lib/lib/office/engine/pptx/inherit.js +155 -0
  54. package/dist-lib/lib/office/engine/pptx/notes.d.ts +5 -0
  55. package/dist-lib/lib/office/engine/pptx/notes.js +85 -0
  56. package/dist-lib/lib/office/engine/pptx/presentation.d.ts +54 -0
  57. package/dist-lib/lib/office/engine/pptx/presentation.js +749 -0
  58. package/dist-lib/lib/office/engine/pptx/read.d.ts +9 -0
  59. package/dist-lib/lib/office/engine/pptx/read.js +66 -0
  60. package/dist-lib/lib/office/engine/pptx/shapes.d.ts +52 -0
  61. package/dist-lib/lib/office/engine/pptx/shapes.js +276 -0
  62. package/dist-lib/lib/office/engine/pptx/slides.d.ts +27 -0
  63. package/dist-lib/lib/office/engine/pptx/slides.js +123 -0
  64. package/dist-lib/lib/office/engine/pptx/template.d.ts +20 -0
  65. package/dist-lib/lib/office/engine/pptx/template.js +391 -0
  66. package/dist-lib/lib/office/engine/pptx/types.d.ts +120 -0
  67. package/dist-lib/lib/office/engine/pptx/types.js +1 -0
  68. package/dist-lib/lib/office/engine/units.d.ts +3 -0
  69. package/dist-lib/lib/office/engine/units.js +12 -0
  70. package/dist-lib/lib/office/engine/utf8.d.ts +2 -0
  71. package/dist-lib/lib/office/engine/utf8.js +103 -0
  72. package/dist-lib/lib/office/engine/xlsx/formula.d.ts +49 -0
  73. package/dist-lib/lib/office/engine/xlsx/formula.js +381 -0
  74. package/dist-lib/lib/office/engine/xlsx/index.d.ts +8 -0
  75. package/dist-lib/lib/office/engine/xlsx/index.js +8 -0
  76. package/dist-lib/lib/office/engine/xlsx/refs.d.ts +22 -0
  77. package/dist-lib/lib/office/engine/xlsx/refs.js +82 -0
  78. package/dist-lib/lib/office/engine/xlsx/reshape.d.ts +21 -0
  79. package/dist-lib/lib/office/engine/xlsx/reshape.js +424 -0
  80. package/dist-lib/lib/office/engine/xlsx/sheet.d.ts +108 -0
  81. package/dist-lib/lib/office/engine/xlsx/sheet.js +662 -0
  82. package/dist-lib/lib/office/engine/xlsx/strings.d.ts +15 -0
  83. package/dist-lib/lib/office/engine/xlsx/strings.js +93 -0
  84. package/dist-lib/lib/office/engine/xlsx/styles.d.ts +38 -0
  85. package/dist-lib/lib/office/engine/xlsx/styles.js +369 -0
  86. package/dist-lib/lib/office/engine/xlsx/template.d.ts +7 -0
  87. package/dist-lib/lib/office/engine/xlsx/template.js +54 -0
  88. package/dist-lib/lib/office/engine/xlsx/workbook.d.ts +161 -0
  89. package/dist-lib/lib/office/engine/xlsx/workbook.js +850 -0
  90. package/dist-lib/lib/office/engine/xlsx/write.d.ts +2 -0
  91. package/dist-lib/lib/office/engine/xlsx/write.js +42 -0
  92. package/dist-lib/lib/office/engine/xml.d.ts +45 -0
  93. package/dist-lib/lib/office/engine/xml.js +258 -0
  94. package/dist-lib/lib/tools/exec/scope.d.ts +2 -0
  95. package/dist-lib/lib/tools/exec/scope.js +33 -2
  96. package/dist-lib/lib/tools/pdf/build.d.ts +43 -0
  97. package/dist-lib/lib/tools/pdf/build.js +150 -0
  98. package/dist-lib/lib/tools/pdf/images.d.ts +7 -0
  99. package/dist-lib/lib/tools/pdf/images.js +82 -0
  100. package/dist-lib/lib/tools/pdf/request.d.ts +63 -0
  101. package/dist-lib/lib/tools/pdf/request.js +335 -0
  102. package/dist-lib/lib/tools/pdf.d.ts +6 -0
  103. package/dist-lib/lib/tools/pdf.js +302 -0
  104. package/dist-lib/lib/tools/skill.js +14 -0
  105. package/package.json +25 -1
  106. package/worker/harness-sidecar.mjs +3 -1
package/CHANGELOG.md CHANGED
@@ -8,6 +8,49 @@ While the major version is `0`, a breaking change bumps the minor version.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.8.0] - 2026-10-05
12
+
13
+ ### Added
14
+
15
+ - A skill-gated `pdf` tool on `bare-pdfium` 1.6.0. With an `attachmentId` and
16
+ no `action` it reads a PDF: the page count, each page's text and the form
17
+ fields. With an `action` it saves a new file named `output` and attaches it:
18
+ `create` from Markdown (chat images placed with `![caption](attachmentId)`),
19
+ `merge`, `split`, `rotate`, `watermark`, `fill`, `encrypt`, `decrypt` and
20
+ `text`. Errors name what exists, a locked PDF takes its password on any
21
+ action, and the one-line answer comes back as `reply`. On Node it reports
22
+ that the device cannot open PDFs. The pdf skill in the next `@qvac/skills`
23
+ release uses it instead of Python. (#256)
24
+ - An office engine in `lib/office/engine/` that reads, edits and creates `.pptx`,
25
+ `.docx` and `.xlsx` files in TypeScript on Bare and Node, so office editing no
26
+ longer needs Python and can run on mobile. It keeps untouched parts byte for
27
+ byte, fails with a `DocsError` whose code names the problem, and moves Excel
28
+ references when rows and columns change. It adds `@xmldom/xmldom`, makes
29
+ `bare-zlib` a direct dependency and maps `#zlib` to `bare-zlib` or `zlib`.
30
+
31
+ ### Fixed
32
+
33
+ - Qwen3.5-family models: a tool call that ran goes back into the chat history
34
+ in the Qwen3.5 template's own format (`<tool_call>` wrapper, one line per tag,
35
+ string values as written) instead of a compact `<function=…>` line. A model
36
+ several calls into a turn copied the compact form, the tool grammar rejected
37
+ it, and the run ended in "This didn't finish". Models with another tool format
38
+ (e.g. MedPsy) still get a format that is not their own, until the SDK accepts
39
+ structured tool calls in history. (QVAC-23092, #146)
40
+ - A skill loaded with a `file` it does not have, before its instructions were
41
+ loaded, now returns those instructions with a line naming the files it has,
42
+ so the skill counts as loaded and its skill-gated tools appear. Before, a
43
+ one-file skill answered that the model already had its instructions, and its
44
+ tools stayed hidden. (#257)
45
+ - After a tool attaches a file, the loop tells the model the file is shown and
46
+ to answer, allows one more tool round, then writes the answer in a tool-less
47
+ round. Small models no longer keep calling tools on their own output until
48
+ the round cap. (#258)
49
+ - `exec`: a `$` before a digit, a space or punctuation in a shell command is
50
+ escaped instead of rejected, so text like `$1,299` runs on the first try. Real
51
+ shell expansion is still refused, now with a hint to single-quote the text.
52
+ (#208)
53
+
11
54
  ## [0.7.3] - 2026-10-01
12
55
 
13
56
  ### Added
@@ -1065,7 +1108,8 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
1065
1108
  - GPU stats are read through `bare-gpu-info`, so every dependency installs from
1066
1109
  the public npm registry. (#12)
1067
1110
 
1068
- [Unreleased]: https://github.com/tetherto/qvac-harness/compare/v0.7.3...HEAD
1111
+ [Unreleased]: https://github.com/tetherto/qvac-harness/compare/v0.8.0...HEAD
1112
+ [0.8.0]: https://github.com/tetherto/qvac-harness/compare/v0.7.3...v0.8.0
1069
1113
  [0.7.3]: https://github.com/tetherto/qvac-harness/compare/v0.7.2...v0.7.3
1070
1114
  [0.7.2]: https://github.com/tetherto/qvac-harness/compare/v0.7.1...v0.7.2
1071
1115
  [0.7.1]: https://github.com/tetherto/qvac-harness/compare/v0.7.0...v0.7.1
package/NOTICE.md CHANGED
@@ -11,3 +11,19 @@ distributed under the License is distributed on an "AS IS" BASIS,
11
11
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  See the License for the specific language governing permissions and
13
13
  limitations under the License.
14
+
15
+ ## Third-party components
16
+
17
+ ### @xmldom/xmldom
18
+
19
+ XML parsing and serialization for the office engine. Copyright 2019 - present
20
+ Christopher J. Brody and other contributors; Copyright 2012 - 2017 @jindw and
21
+ other contributors. Licensed under the MIT License.
22
+ <https://github.com/xmldom/xmldom>
23
+
24
+ ## Test data
25
+
26
+ The files under `test/fixtures/office-corpus` are taken from the Apache POI test
27
+ data. Copyright The Apache Software Foundation. Licensed under the Apache
28
+ License, Version 2.0. <https://github.com/apache/poi>. They are used by the test
29
+ suite only and are not part of the published package.
@@ -123,6 +123,7 @@ export interface RunBudget {
123
123
  export interface ToolExecuteContext {
124
124
  agentId?: string | null | undefined;
125
125
  skills?: readonly string[] | undefined;
126
+ skillsLoadedThisTurn?: readonly string[] | undefined;
126
127
  ctxSize?: number | undefined;
127
128
  image?: ImageSettings | undefined;
128
129
  settings?: Record<string, unknown> | undefined;
@@ -29,7 +29,7 @@ import { resolveImplicitSkill, resolveSkillMention, skillPreloadMessage } from '
29
29
  import { NETWORK_TOOLSETS, permittedTools } from '../tools/toolsets.js';
30
30
  import { promptPrefixHash } from './prompt-prefix.js';
31
31
  import { logToolPrompt } from './tool-prompt-log.js';
32
- import { DIRECT_CALL_ID, loadedSkillFilesFrom, loadedSkillsFrom, TOOL_CONTINUATION_NUDGE, toolResultTurn, withoutSkillLoadTurns } from './tool-turns.js';
32
+ import { DIRECT_CALL_ID, deliversFile, FILE_DELIVERED_NUDGE, loadedSkillFilesFrom, loadedSkillsFrom, TOOL_CONTINUATION_NUDGE, toolResultTurn, withoutSkillLoadTurns } from './tool-turns.js';
33
33
  export function imageSettingsFrom(agent) {
34
34
  if (!agent)
35
35
  return undefined;
@@ -153,7 +153,7 @@ function assistantToolTurn(contentText, calls) {
153
153
  if (!calls.length || contentText.includes('<function='))
154
154
  return contentText;
155
155
  const markup = toolCallMarkup(calls);
156
- return contentText ? `${contentText}\n${markup}` : markup;
156
+ return contentText ? `${contentText}\n\n${markup}` : markup;
157
157
  }
158
158
  function preloadLastUser(messages, name, body) {
159
159
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -734,17 +734,18 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
734
734
  let skillIcon = null;
735
735
  const skillEmoji = (name) => skillCatalog.find((entry) => entry.name === name)?.emoji ?? null;
736
736
  const nudgedSkills = new Set();
737
- function recordSkillLoad(skillName, output, { mainRequested, askedFile }) {
737
+ function recordSkillLoad(skillName, output, { mainRequested, askedFile, executed }) {
738
738
  if (!output || typeof output !== 'object' || !('content' in output)) {
739
739
  return;
740
740
  }
741
741
  loadedSkills.add(skillName);
742
- if ('file' in output && typeof output.file === 'string') {
742
+ const loadedFile = 'file' in output && typeof output.file === 'string' ? output.file : null;
743
+ if (loadedFile !== null) {
743
744
  const files = loadedSkillFiles.get(skillName) ?? new Set();
744
- files.add(output.file);
745
+ files.add(loadedFile);
745
746
  loadedSkillFiles.set(skillName, files);
746
747
  }
747
- if (mainRequested) {
748
+ if (mainRequested || (executed && loadedFile === null)) {
748
749
  loadedThisTurn.add(skillName);
749
750
  }
750
751
  else {
@@ -928,6 +929,7 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
928
929
  for (; round < MAX_TOOL_ROUNDS; round++) {
929
930
  if (signal?.aborted)
930
931
  throw new AbortedError();
932
+ let deliveredThisRound = false;
931
933
  if (Date.now() >= runBudget.deadlineMs) {
932
934
  outcome = 'time-capped';
933
935
  break;
@@ -1059,6 +1061,7 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
1059
1061
  skills: [...loadedSkills].filter((name) => !skillsSelected ||
1060
1062
  (agent?.enabledSkills ?? []).includes(name) ||
1061
1063
  name === mention?.name),
1064
+ skillsLoadedThisTurn: [...loadedThisTurn],
1062
1065
  settings: toolConfigs?.[call.name],
1063
1066
  toolOps: grantedOps,
1064
1067
  fsRoots,
@@ -1165,7 +1168,11 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
1165
1168
  webSearches++;
1166
1169
  }
1167
1170
  if (call.name === SKILL_TOOL_NAME) {
1168
- recordSkillLoad(skillName, output, { mainRequested, askedFile });
1171
+ recordSkillLoad(skillName, output, {
1172
+ mainRequested,
1173
+ askedFile,
1174
+ executed: execution.executed
1175
+ });
1169
1176
  }
1170
1177
  if (!activity &&
1171
1178
  !silent &&
@@ -1196,6 +1203,9 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
1196
1203
  if (isMutation && executionOutcome.kind === 'success') {
1197
1204
  runOutcomes.confirmWrite(output);
1198
1205
  }
1206
+ if (executionOutcome.kind === 'success' && deliversFile(output)) {
1207
+ deliveredThisRound = true;
1208
+ }
1199
1209
  const turn = toolResultTurn(call.name, call.id, output);
1200
1210
  sources.push({
1201
1211
  origin: call.name === SKILL_TOOL_NAME ? 'skill' : 'tool-result',
@@ -1215,7 +1225,17 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
1215
1225
  outcome = 'answered';
1216
1226
  break;
1217
1227
  }
1218
- history.push({ role: 'user', content: TOOL_CONTINUATION_NUDGE });
1228
+ history.push({
1229
+ role: 'user',
1230
+ content: deliveredThisRound ? FILE_DELIVERED_NUDGE : TOOL_CONTINUATION_NUDGE
1231
+ });
1232
+ if (runOutcomes.facts().delivered) {
1233
+ outcome = 'delivered';
1234
+ break;
1235
+ }
1236
+ if (deliveredThisRound) {
1237
+ runOutcomes.confirmDelivery();
1238
+ }
1219
1239
  if (blockedRepeats >= MAX_BLOCKED_REPEATS) {
1220
1240
  break;
1221
1241
  }
@@ -1,6 +1,9 @@
1
1
  import type { Message } from './completion.ts';
2
2
  export declare const DIRECT_CALL_ID = "call-direct";
3
3
  export declare const TOOL_CONTINUATION_NUDGE = "[Tool results above. Continue the user's request: call the next tool, or answer.]";
4
+ export declare const FILE_DELIVERED_NUDGE = "[Tool results above. The file is attached to the chat and shown to the user. Answer now, unless the request still needs a file that is not made yet.]";
5
+ export declare function isContinuationNudge(content: string): boolean;
6
+ export declare function deliversFile(output: unknown): boolean;
4
7
  export declare function toolResultTurn(name: string, id: string, output: unknown): string;
5
8
  export declare function skillLoadedTurn(name: string): string;
6
9
  export declare function attachmentTurn(name: string, result: unknown): string | null;
@@ -2,6 +2,13 @@ import { isRecord } from '../guards.js';
2
2
  import { SKILL_TOOL_NAME } from './tool-names.js';
3
3
  export const DIRECT_CALL_ID = 'call-direct';
4
4
  export const TOOL_CONTINUATION_NUDGE = "[Tool results above. Continue the user's request: call the next tool, or answer.]";
5
+ export const FILE_DELIVERED_NUDGE = '[Tool results above. The file is attached to the chat and shown to the user. Answer now, unless the request still needs a file that is not made yet.]';
6
+ export function isContinuationNudge(content) {
7
+ return content === TOOL_CONTINUATION_NUDGE || content === FILE_DELIVERED_NUDGE;
8
+ }
9
+ export function deliversFile(output) {
10
+ return isRecord(output) && Array.isArray(output.attachments) && output.attachments.length > 0;
11
+ }
5
12
  export function toolResultTurn(name, id, output) {
6
13
  return `[Tool: ${name} (${id})]\n${JSON.stringify(historyPayload(output))}`;
7
14
  }
@@ -1,7 +1,7 @@
1
- import { TOOL_CONTINUATION_NUDGE } from './capabilities/tool-turns.js';
1
+ import { isContinuationNudge } from './capabilities/tool-turns.js';
2
2
  export const TOOL_RESULT_TRUNCATION_MARKER = '\n[tool result truncated for context]\n';
3
3
  function isTurnBoundary(message) {
4
- return message.role === 'user' && message.content !== TOOL_CONTINUATION_NUDGE;
4
+ return message.role === 'user' && !isContinuationNudge(message.content);
5
5
  }
6
6
  export function contextCandidates(history) {
7
7
  const { prefix, turns } = splitTurns(history);
@@ -1,13 +1,15 @@
1
1
  import type { ToolExecutionOutcome } from '../tool-call-display.ts';
2
- export type RunOutcome = 'answered' | 'mute' | 'exhausted' | 'search-capped' | 'time-capped';
2
+ export type RunOutcome = 'answered' | 'mute' | 'exhausted' | 'search-capped' | 'time-capped' | 'delivered';
3
3
  export declare const DEGENERATE_ANSWER_FALLBACK = "I couldn't complete that step and don't have a useful result to share.";
4
4
  export declare const ROUND_CAP_ANSWER = "I ran out of steps before I could finish that. Ask me to continue and I'll pick up where I left off.";
5
5
  export declare const SEARCH_CAPPED_ANSWER = "I searched the web but couldn't find the answer in the results I got.";
6
+ export declare const DELIVERED_ANSWER = "Your file is attached to the chat.";
6
7
  export declare const WRITE_RESULT_LEAD = "Here's what came back:";
7
8
  export declare const TIME_CAPPED_ANSWER = "I hit this run's time budget before I could finish that. Ask me to continue and I'll pick up where I left off.";
8
9
  export interface RunOutcomeFacts {
9
10
  readonly latestFailure: string | null;
10
11
  readonly confirmedWrites: ReadonlyArray<string>;
12
+ readonly delivered: boolean;
11
13
  }
12
14
  export interface HistoryTurn {
13
15
  readonly role: string;
@@ -19,6 +21,7 @@ export interface RunOutcomeTracker {
19
21
  record(result: unknown, outcome: ToolExecutionOutcome): void;
20
22
  finishRound(): void;
21
23
  confirmWrite(result: unknown): void;
24
+ confirmDelivery(): void;
22
25
  facts(): RunOutcomeFacts;
23
26
  }
24
27
  export declare function createRunOutcomeTracker(): RunOutcomeTracker;
@@ -1,8 +1,9 @@
1
- import { TOOL_CONTINUATION_NUDGE } from '../capabilities/tool-turns.js';
1
+ import { isContinuationNudge } from '../capabilities/tool-turns.js';
2
2
  import { isRecord } from '../guards.js';
3
3
  export const DEGENERATE_ANSWER_FALLBACK = "I couldn't complete that step and don't have a useful result to share.";
4
4
  export const ROUND_CAP_ANSWER = "I ran out of steps before I could finish that. Ask me to continue and I'll pick up where I left off.";
5
5
  export const SEARCH_CAPPED_ANSWER = "I searched the web but couldn't find the answer in the results I got.";
6
+ export const DELIVERED_ANSWER = 'Your file is attached to the chat.';
6
7
  export const WRITE_RESULT_LEAD = "Here's what came back:";
7
8
  export const TIME_CAPPED_ANSWER = "I hit this run's time budget before I could finish that. Ask me to continue and I'll pick up where I left off.";
8
9
  const MAX_FAILURE_CHARS = 300;
@@ -18,6 +19,7 @@ export function createRunOutcomeTracker() {
18
19
  let roundFailure = null;
19
20
  let latestFailure = null;
20
21
  let hasRoundResult = false;
22
+ let delivered = false;
21
23
  return {
22
24
  confirmedWrites,
23
25
  beginRound() {
@@ -42,8 +44,11 @@ export function createRunOutcomeTracker() {
42
44
  confirmedWrites.push(write);
43
45
  }
44
46
  },
47
+ confirmDelivery() {
48
+ delivered = true;
49
+ },
45
50
  facts() {
46
- return { latestFailure, confirmedWrites };
51
+ return { latestFailure, confirmedWrites, delivered };
47
52
  }
48
53
  };
49
54
  }
@@ -64,11 +69,12 @@ export function failureFromToolExecution(result, outcome) {
64
69
  }
65
70
  export function degenerateAnswer(outcome, history, facts) {
66
71
  const rest = (facts ? facts.latestFailure : lastFailedToolAnswer(history)) ?? cappedAnswer(outcome, history);
67
- const written = writeReport(facts?.confirmedWrites ?? []);
68
- if (written) {
69
- return rest ? `${written}\n\n${rest}` : written;
70
- }
71
- return rest ?? DEGENERATE_ANSWER_FALLBACK;
72
+ const lines = [
73
+ writeReport(facts?.confirmedWrites ?? []),
74
+ facts?.delivered ? DELIVERED_ANSWER : null,
75
+ rest
76
+ ].filter((line) => line !== null);
77
+ return lines.length ? lines.join('\n\n') : DEGENERATE_ANSWER_FALLBACK;
72
78
  }
73
79
  function cappedAnswer(outcome, history) {
74
80
  if (outcome === 'search-capped') {
@@ -108,7 +114,7 @@ function searchCappedAnswer(history) {
108
114
  export function lastFailedToolAnswer(history) {
109
115
  for (let i = history.length - 1; i >= 0; i--) {
110
116
  const turn = history[i];
111
- if (turn?.role === 'user' && turn.content === TOOL_CONTINUATION_NUDGE) {
117
+ if (turn?.role === 'user' && isContinuationNudge(turn.content)) {
112
118
  continue;
113
119
  }
114
120
  if (turn?.role !== 'tool') {
@@ -120,12 +120,12 @@ export function toolCallMarkup(calls) {
120
120
  }
121
121
  function markupForCall(call) {
122
122
  const params = Object.entries(call.arguments ?? {})
123
- .map(([key, value]) => `<parameter=${key}>${encodeMarkupValue(value)}</parameter>`)
123
+ .map(([key, value]) => `<parameter=${key}>\n${markupValue(value)}\n</parameter>\n`)
124
124
  .join('');
125
- return `<function=${call.name}>${params}</function>`;
125
+ return `<tool_call>\n<function=${call.name}>\n${params}</function>\n</tool_call>`;
126
126
  }
127
- function encodeMarkupValue(value) {
128
- return JSON.stringify(value ?? null).replace(/</g, '\\u003c');
127
+ function markupValue(value) {
128
+ return typeof value === 'string' ? value : JSON.stringify(value ?? null);
129
129
  }
130
130
  const ARG_WRAPPER_KEYS = new Set(['parameters', 'arguments', 'args', 'input', 'params']);
131
131
  function isPlainObject(value) {
@@ -1,6 +1,7 @@
1
1
  import { jsonrepair } from 'jsonrepair';
2
2
  import { isRecord } from '../../guards.js';
3
3
  import { hlog } from '../../log.js';
4
+ import { escapeLiteralDollars } from '../../tools/exec/scope.js';
4
5
  const STRUCTURED_TYPES = new Set(['object', 'array']);
5
6
  export class ArgRepairHook {
6
7
  phase = 'preToolCall';
@@ -24,6 +25,12 @@ export class ArgRepairHook {
24
25
  }
25
26
  args = { ...args, [key]: parsed };
26
27
  }
28
+ if (ctx.toolName === 'exec' && args.language !== 'python' && typeof args.command === 'string') {
29
+ const command = escapeLiteralDollars(args.command);
30
+ if (command !== args.command) {
31
+ args = { ...args, command };
32
+ }
33
+ }
27
34
  if (args !== ctx.args) {
28
35
  return { args };
29
36
  }
@@ -0,0 +1,62 @@
1
+ export declare const CODES: {
2
+ readonly INVALID_INPUT: 'the input is not a Uint8Array or Buffer';
3
+ readonly NOT_OOXML: 'the file is not a zip archive with [Content_Types].xml';
4
+ readonly NOT_PPTX: 'the file is not a PowerPoint presentation';
5
+ readonly NOT_DOCX: 'the file is not a Word document';
6
+ readonly NOT_XLSX: 'the file is not an Excel workbook';
7
+ readonly UNSUPPORTED_PPTX: 'the presentation uses Strict Open XML';
8
+ readonly UNSUPPORTED_DOCX: 'the document uses Strict Open XML';
9
+ readonly UNSUPPORTED_XLSX: 'the workbook uses Strict Open XML';
10
+ readonly BROKEN_PPTX: 'the presentation points at parts that do not exist';
11
+ readonly BROKEN_DOCX: 'the document is missing a required part';
12
+ readonly BROKEN_XLSX: 'the workbook points at parts that do not exist';
13
+ readonly TOO_LARGE: 'the file exceeds the entry, size or compression-ratio limits';
14
+ readonly UNSAFE_XML: 'an XML part declares a DOCTYPE or nests too deeply';
15
+ readonly INVALID_XML: 'an XML part is not well-formed';
16
+ readonly PART_EXISTS: 'a part with that name already exists';
17
+ readonly PART_MISSING: 'the part is not in the package';
18
+ readonly UNTOUCHED_EDIT: 'a part was changed without touch() (strict mode only)';
19
+ readonly INVALID_OPTIONS: 'an option is unknown or out of range';
20
+ readonly INVALID_TEXT: 'text or paragraphs are not in an accepted shape';
21
+ readonly INVALID_STYLE: 'a style or format value is unknown or out of range';
22
+ readonly INVALID_BOX: 'a position or size is not a valid whole number of EMU';
23
+ readonly INVALID_IMAGE: 'the image is empty, truncated or has no size';
24
+ readonly UNSUPPORTED_IMAGE: 'the image is not PNG, JPEG or GIF';
25
+ readonly INVALID_TABLE: 'table rows or cells are not in an accepted shape';
26
+ readonly INVALID_CHART: 'a chart option is not valid';
27
+ readonly INVALID_CHART_DATA: 'chart categories or series are not valid';
28
+ readonly UNSUPPORTED_CHART: 'that chart type cannot be created or edited';
29
+ readonly CHART_COMBO: 'a combo chart must keep its number of series';
30
+ readonly NOT_CHART: 'the target holds no chart';
31
+ readonly SLIDE_NOT_FOUND: 'no slide at that index';
32
+ readonly SHAPE_NOT_FOUND: 'no shape with that id or role on the slide';
33
+ readonly LAYOUT_NOT_FOUND: 'no layout with that name';
34
+ readonly PARAGRAPH_NOT_FOUND: 'no paragraph at that index';
35
+ readonly NOT_TEXT: 'the shape or block holds no text';
36
+ readonly NOT_PICTURE: 'the shape is not a picture';
37
+ readonly NOT_TABLE: 'the shape or block is not a table';
38
+ readonly CELL_NOT_FOUND: 'no table cell at that row and column';
39
+ readonly CELL_MERGED: 'the cell is covered by a merged cell';
40
+ readonly SLIDE_IS_LINKED: 'another slide links to the slide';
41
+ readonly TOO_MANY_SLIDES: 'the deck has run out of slide ids';
42
+ readonly BLOCK_NOT_FOUND: 'no block at that index';
43
+ readonly NOT_PARAGRAPH: 'the block is not a paragraph';
44
+ readonly NOT_HEADING: 'the block is not a heading';
45
+ readonly STYLE_NOT_FOUND: 'no style with that name';
46
+ readonly TRACKED_CHANGES: 'the paragraph has tracked changes that an edit would accept silently';
47
+ readonly ROW_NOT_FOUND: 'no table row at that index';
48
+ readonly ROW_MERGED: 'the row is part of a vertically merged cell';
49
+ readonly LAST_ROW: 'the table would be left without rows';
50
+ readonly SHEET_NOT_FOUND: 'no sheet with that name or index';
51
+ readonly NOT_WORKSHEET: 'the sheet is a chart sheet or dialog sheet';
52
+ readonly INVALID_NAME: 'the sheet name is not allowed';
53
+ readonly INVALID_CELL: 'not a valid cell, row or column reference';
54
+ readonly INVALID_RANGE: 'not a valid range or limit';
55
+ readonly INVALID_VALUE: 'a cell value, formula or date is not valid';
56
+ readonly INVALID_FORMULA: 'text starting with = does not parse as a formula';
57
+ readonly SHARED_FORMULA: 'the cell belongs to a shared or array formula range';
58
+ readonly SHEET_HAS_DEPENDENTS: 'the sheet has pivot tables, comments or other parts that rows or columns cannot move through yet';
59
+ readonly SHEET_IS_REFERENCED: 'other formulas, names or charts refer to the sheet';
60
+ readonly LAST_SHEET: 'a workbook needs at least one visible sheet';
61
+ };
62
+ export type DocsErrorCode = keyof typeof CODES;
@@ -0,0 +1,61 @@
1
+ export const CODES = {
2
+ INVALID_INPUT: 'the input is not a Uint8Array or Buffer',
3
+ NOT_OOXML: 'the file is not a zip archive with [Content_Types].xml',
4
+ NOT_PPTX: 'the file is not a PowerPoint presentation',
5
+ NOT_DOCX: 'the file is not a Word document',
6
+ NOT_XLSX: 'the file is not an Excel workbook',
7
+ UNSUPPORTED_PPTX: 'the presentation uses Strict Open XML',
8
+ UNSUPPORTED_DOCX: 'the document uses Strict Open XML',
9
+ UNSUPPORTED_XLSX: 'the workbook uses Strict Open XML',
10
+ BROKEN_PPTX: 'the presentation points at parts that do not exist',
11
+ BROKEN_DOCX: 'the document is missing a required part',
12
+ BROKEN_XLSX: 'the workbook points at parts that do not exist',
13
+ TOO_LARGE: 'the file exceeds the entry, size or compression-ratio limits',
14
+ UNSAFE_XML: 'an XML part declares a DOCTYPE or nests too deeply',
15
+ INVALID_XML: 'an XML part is not well-formed',
16
+ PART_EXISTS: 'a part with that name already exists',
17
+ PART_MISSING: 'the part is not in the package',
18
+ UNTOUCHED_EDIT: 'a part was changed without touch() (strict mode only)',
19
+ INVALID_OPTIONS: 'an option is unknown or out of range',
20
+ INVALID_TEXT: 'text or paragraphs are not in an accepted shape',
21
+ INVALID_STYLE: 'a style or format value is unknown or out of range',
22
+ INVALID_BOX: 'a position or size is not a valid whole number of EMU',
23
+ INVALID_IMAGE: 'the image is empty, truncated or has no size',
24
+ UNSUPPORTED_IMAGE: 'the image is not PNG, JPEG or GIF',
25
+ INVALID_TABLE: 'table rows or cells are not in an accepted shape',
26
+ INVALID_CHART: 'a chart option is not valid',
27
+ INVALID_CHART_DATA: 'chart categories or series are not valid',
28
+ UNSUPPORTED_CHART: 'that chart type cannot be created or edited',
29
+ CHART_COMBO: 'a combo chart must keep its number of series',
30
+ NOT_CHART: 'the target holds no chart',
31
+ SLIDE_NOT_FOUND: 'no slide at that index',
32
+ SHAPE_NOT_FOUND: 'no shape with that id or role on the slide',
33
+ LAYOUT_NOT_FOUND: 'no layout with that name',
34
+ PARAGRAPH_NOT_FOUND: 'no paragraph at that index',
35
+ NOT_TEXT: 'the shape or block holds no text',
36
+ NOT_PICTURE: 'the shape is not a picture',
37
+ NOT_TABLE: 'the shape or block is not a table',
38
+ CELL_NOT_FOUND: 'no table cell at that row and column',
39
+ CELL_MERGED: 'the cell is covered by a merged cell',
40
+ SLIDE_IS_LINKED: 'another slide links to the slide',
41
+ TOO_MANY_SLIDES: 'the deck has run out of slide ids',
42
+ BLOCK_NOT_FOUND: 'no block at that index',
43
+ NOT_PARAGRAPH: 'the block is not a paragraph',
44
+ NOT_HEADING: 'the block is not a heading',
45
+ STYLE_NOT_FOUND: 'no style with that name',
46
+ TRACKED_CHANGES: 'the paragraph has tracked changes that an edit would accept silently',
47
+ ROW_NOT_FOUND: 'no table row at that index',
48
+ ROW_MERGED: 'the row is part of a vertically merged cell',
49
+ LAST_ROW: 'the table would be left without rows',
50
+ SHEET_NOT_FOUND: 'no sheet with that name or index',
51
+ NOT_WORKSHEET: 'the sheet is a chart sheet or dialog sheet',
52
+ INVALID_NAME: 'the sheet name is not allowed',
53
+ INVALID_CELL: 'not a valid cell, row or column reference',
54
+ INVALID_RANGE: 'not a valid range or limit',
55
+ INVALID_VALUE: 'a cell value, formula or date is not valid',
56
+ INVALID_FORMULA: 'text starting with = does not parse as a formula',
57
+ SHARED_FORMULA: 'the cell belongs to a shared or array formula range',
58
+ SHEET_HAS_DEPENDENTS: 'the sheet has pivot tables, comments or other parts that rows or columns cannot move through yet',
59
+ SHEET_IS_REFERENCED: 'other formulas, names or charts refer to the sheet',
60
+ LAST_SHEET: 'a workbook needs at least one visible sheet'
61
+ };