@qvac/harness 0.7.0 → 1.0.0-rc.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 (204) hide show
  1. package/CHANGELOG.md +42 -1
  2. package/README.md +239 -2
  3. package/dist-lib/index.d.ts +58 -3
  4. package/dist-lib/index.js +736 -239
  5. package/dist-lib/lib/capabilities/completion-engine/abort-polyfill.d.ts +1 -0
  6. package/dist-lib/lib/capabilities/completion-engine/abort-polyfill.js +10 -0
  7. package/dist-lib/lib/capabilities/completion-engine/actors/answering.d.ts +3 -0
  8. package/dist-lib/lib/capabilities/completion-engine/actors/answering.js +17 -0
  9. package/dist-lib/lib/capabilities/completion-engine/actors/approval.d.ts +4 -0
  10. package/dist-lib/lib/capabilities/completion-engine/actors/approval.js +29 -0
  11. package/dist-lib/lib/capabilities/completion-engine/actors/direct-search.d.ts +4 -0
  12. package/dist-lib/lib/capabilities/completion-engine/actors/direct-search.js +73 -0
  13. package/dist-lib/lib/capabilities/completion-engine/actors/execute.d.ts +41 -0
  14. package/dist-lib/lib/capabilities/completion-engine/actors/execute.js +49 -0
  15. package/dist-lib/lib/capabilities/completion-engine/actors/executing.d.ts +3 -0
  16. package/dist-lib/lib/capabilities/completion-engine/actors/executing.js +102 -0
  17. package/dist-lib/lib/capabilities/completion-engine/actors/execution-plan.d.ts +47 -0
  18. package/dist-lib/lib/capabilities/completion-engine/actors/execution-plan.js +87 -0
  19. package/dist-lib/lib/capabilities/completion-engine/actors/finishing.d.ts +2 -0
  20. package/dist-lib/lib/capabilities/completion-engine/actors/finishing.js +32 -0
  21. package/dist-lib/lib/capabilities/completion-engine/actors/inspect-preload.d.ts +3 -0
  22. package/dist-lib/lib/capabilities/completion-engine/actors/inspect-preload.js +75 -0
  23. package/dist-lib/lib/capabilities/completion-engine/actors/inspecting.d.ts +3 -0
  24. package/dist-lib/lib/capabilities/completion-engine/actors/inspecting.js +152 -0
  25. package/dist-lib/lib/capabilities/completion-engine/actors/loading-skill.d.ts +3 -0
  26. package/dist-lib/lib/capabilities/completion-engine/actors/loading-skill.js +94 -0
  27. package/dist-lib/lib/capabilities/completion-engine/actors/preload-failure.d.ts +9 -0
  28. package/dist-lib/lib/capabilities/completion-engine/actors/preload-failure.js +11 -0
  29. package/dist-lib/lib/capabilities/completion-engine/actors/preloading.d.ts +3 -0
  30. package/dist-lib/lib/capabilities/completion-engine/actors/preloading.js +114 -0
  31. package/dist-lib/lib/capabilities/completion-engine/actors/preparing.d.ts +3 -0
  32. package/dist-lib/lib/capabilities/completion-engine/actors/preparing.js +198 -0
  33. package/dist-lib/lib/capabilities/completion-engine/actors/rejecting.d.ts +3 -0
  34. package/dist-lib/lib/capabilities/completion-engine/actors/rejecting.js +71 -0
  35. package/dist-lib/lib/capabilities/completion-engine/actors/rejection.d.ts +18 -0
  36. package/dist-lib/lib/capabilities/completion-engine/actors/rejection.js +70 -0
  37. package/dist-lib/lib/capabilities/completion-engine/actors/resolving.d.ts +3 -0
  38. package/dist-lib/lib/capabilities/completion-engine/actors/resolving.js +60 -0
  39. package/dist-lib/lib/capabilities/completion-engine/actors/stream.d.ts +21 -0
  40. package/dist-lib/lib/capabilities/completion-engine/actors/stream.js +48 -0
  41. package/dist-lib/lib/capabilities/completion-engine/actors/streaming.d.ts +3 -0
  42. package/dist-lib/lib/capabilities/completion-engine/actors/streaming.js +156 -0
  43. package/dist-lib/lib/capabilities/completion-engine/actors/wrap-up.d.ts +7 -0
  44. package/dist-lib/lib/capabilities/completion-engine/actors/wrap-up.js +61 -0
  45. package/dist-lib/lib/capabilities/completion-engine/driver.d.ts +25 -0
  46. package/dist-lib/lib/capabilities/completion-engine/driver.js +553 -0
  47. package/dist-lib/lib/capabilities/completion-engine/durable.d.ts +61 -0
  48. package/dist-lib/lib/capabilities/completion-engine/durable.js +150 -0
  49. package/dist-lib/lib/capabilities/completion-engine/operations.d.ts +6 -0
  50. package/dist-lib/lib/capabilities/completion-engine/operations.js +239 -0
  51. package/dist-lib/lib/capabilities/completion-engine/ports.d.ts +3 -0
  52. package/dist-lib/lib/capabilities/completion-engine/ports.js +267 -0
  53. package/dist-lib/lib/capabilities/completion-engine/session.d.ts +76 -0
  54. package/dist-lib/lib/capabilities/completion-engine/session.js +70 -0
  55. package/dist-lib/lib/capabilities/completion-loop/index.d.ts +80 -0
  56. package/dist-lib/lib/capabilities/completion-loop/index.js +158 -0
  57. package/dist-lib/lib/capabilities/completion-loop/metrics.d.ts +5 -0
  58. package/dist-lib/lib/capabilities/completion-loop/metrics.js +29 -0
  59. package/dist-lib/lib/capabilities/completion-loop/round.d.ts +33 -0
  60. package/dist-lib/lib/capabilities/completion-loop/round.js +353 -0
  61. package/dist-lib/lib/capabilities/completion-loop/tool-execution.d.ts +60 -0
  62. package/dist-lib/lib/capabilities/completion-loop/tool-execution.js +270 -0
  63. package/dist-lib/lib/capabilities/completion-loop/turns.d.ts +23 -0
  64. package/dist-lib/lib/capabilities/completion-loop/turns.js +112 -0
  65. package/dist-lib/lib/capabilities/completion-runtime.d.ts +46 -0
  66. package/dist-lib/lib/capabilities/completion-runtime.js +74 -0
  67. package/dist-lib/lib/capabilities/completion.d.ts +18 -335
  68. package/dist-lib/lib/capabilities/completion.js +20 -1301
  69. package/dist-lib/lib/capabilities/download.d.ts +1 -1
  70. package/dist-lib/lib/capabilities/download.js +7 -2
  71. package/dist-lib/lib/capabilities/invoke.js +5 -3
  72. package/dist-lib/lib/capabilities/tool-plan.d.ts +35 -0
  73. package/dist-lib/lib/capabilities/tool-plan.js +66 -0
  74. package/dist-lib/lib/capabilities/tool-turns.d.ts +11 -2
  75. package/dist-lib/lib/capabilities/tool-turns.js +30 -2
  76. package/dist-lib/lib/capabilities/types.d.ts +287 -0
  77. package/dist-lib/lib/capabilities/types.js +1 -0
  78. package/dist-lib/lib/completion-lifecycle.d.ts +14 -0
  79. package/dist-lib/lib/completion-lifecycle.js +126 -0
  80. package/dist-lib/lib/connect.d.ts +10 -3
  81. package/dist-lib/lib/connect.js +329 -23
  82. package/dist-lib/lib/contract-types.d.ts +19 -2
  83. package/dist-lib/lib/harness-lifecycle.d.ts +66 -0
  84. package/dist-lib/lib/harness-lifecycle.js +129 -0
  85. package/dist-lib/lib/helpers/detach-json.d.ts +1 -0
  86. package/dist-lib/lib/helpers/detach-json.js +146 -0
  87. package/dist-lib/lib/helpers/markup-scanner.d.ts +26 -0
  88. package/dist-lib/lib/helpers/markup-scanner.js +156 -0
  89. package/dist-lib/lib/helpers/markup-stream-filter.js +138 -35
  90. package/dist-lib/lib/helpers/race-abort.d.ts +9 -4
  91. package/dist-lib/lib/helpers/race-abort.js +2 -7
  92. package/dist-lib/lib/helpers/recovery-required.d.ts +1 -0
  93. package/dist-lib/lib/helpers/recovery-required.js +1 -0
  94. package/dist-lib/lib/helpers/run-outcome.d.ts +10 -2
  95. package/dist-lib/lib/helpers/run-outcome.js +8 -5
  96. package/dist-lib/lib/helpers/tool-call-markup.d.ts +2 -0
  97. package/dist-lib/lib/helpers/tool-call-markup.js +197 -82
  98. package/dist-lib/lib/hooks/built-in/arg-repair.d.ts +1 -0
  99. package/dist-lib/lib/hooks/built-in/arg-repair.js +1 -0
  100. package/dist-lib/lib/hooks/built-in/egress.d.ts +1 -0
  101. package/dist-lib/lib/hooks/built-in/egress.js +2 -1
  102. package/dist-lib/lib/hooks/built-in/sandbox.d.ts +1 -0
  103. package/dist-lib/lib/hooks/built-in/sandbox.js +6 -1
  104. package/dist-lib/lib/hooks/built-in/skill-catalog.d.ts +1 -0
  105. package/dist-lib/lib/hooks/built-in/skill-catalog.js +2 -1
  106. package/dist-lib/lib/hooks/pipeline.d.ts +4 -1
  107. package/dist-lib/lib/hooks/pipeline.js +24 -2
  108. package/dist-lib/lib/hooks/run.d.ts +2 -1
  109. package/dist-lib/lib/hooks/run.js +19 -7
  110. package/dist-lib/lib/hooks/types.d.ts +17 -6
  111. package/dist-lib/lib/machine/completion/actions/approval.d.ts +3 -0
  112. package/dist-lib/lib/machine/completion/actions/approval.js +16 -0
  113. package/dist-lib/lib/machine/completion/actions/completion.d.ts +23 -0
  114. package/dist-lib/lib/machine/completion/actions/completion.js +142 -0
  115. package/dist-lib/lib/machine/completion/actions/skills.d.ts +9 -0
  116. package/dist-lib/lib/machine/completion/actions/skills.js +83 -0
  117. package/dist-lib/lib/machine/completion/actions/tools.d.ts +16 -0
  118. package/dist-lib/lib/machine/completion/actions/tools.js +173 -0
  119. package/dist-lib/lib/machine/completion/context.d.ts +9 -0
  120. package/dist-lib/lib/machine/completion/context.js +44 -0
  121. package/dist-lib/lib/machine/completion/contract.d.ts +6 -0
  122. package/dist-lib/lib/machine/completion/contract.js +1 -0
  123. package/dist-lib/lib/machine/completion/guards/retry.d.ts +9 -0
  124. package/dist-lib/lib/machine/completion/guards/retry.js +25 -0
  125. package/dist-lib/lib/machine/completion/implementations.d.ts +81 -0
  126. package/dist-lib/lib/machine/completion/implementations.js +152 -0
  127. package/dist-lib/lib/machine/completion/machine.d.ts +375 -0
  128. package/dist-lib/lib/machine/completion/machine.js +511 -0
  129. package/dist-lib/lib/machine/completion/ports.d.ts +65 -0
  130. package/dist-lib/lib/machine/completion/ports.js +224 -0
  131. package/dist-lib/lib/machine/completion/rules.d.ts +39 -0
  132. package/dist-lib/lib/machine/completion/rules.js +115 -0
  133. package/dist-lib/lib/machine/completion/types.d.ts +493 -0
  134. package/dist-lib/lib/machine/completion/types.js +1 -0
  135. package/dist-lib/lib/machine/engine.d.ts +93 -0
  136. package/dist-lib/lib/machine/engine.js +483 -0
  137. package/dist-lib/lib/machine/errors.d.ts +44 -0
  138. package/dist-lib/lib/machine/errors.js +373 -0
  139. package/dist-lib/lib/machine/guards.d.ts +2 -0
  140. package/dist-lib/lib/machine/guards.js +6 -0
  141. package/dist-lib/lib/machine/index.d.ts +20 -0
  142. package/dist-lib/lib/machine/index.js +11 -0
  143. package/dist-lib/lib/machine/replay-output.d.ts +24 -0
  144. package/dist-lib/lib/machine/replay-output.js +50 -0
  145. package/dist-lib/lib/machine/stable-stringify.d.ts +1 -0
  146. package/dist-lib/lib/machine/stable-stringify.js +11 -0
  147. package/dist-lib/lib/models/model-handle.d.ts +5 -1
  148. package/dist-lib/lib/models/model-handle.js +8 -3
  149. package/dist-lib/lib/models/model-list.d.ts +5 -3
  150. package/dist-lib/lib/models/model-pool.d.ts +18 -7
  151. package/dist-lib/lib/models/model-pool.js +126 -38
  152. package/dist-lib/lib/run-log/history-codec.d.ts +23 -0
  153. package/dist-lib/lib/run-log/history-codec.js +973 -0
  154. package/dist-lib/lib/run-log/patch.d.ts +6 -0
  155. package/dist-lib/lib/run-log/patch.js +93 -0
  156. package/dist-lib/lib/run-log/persisted-actor.d.ts +18 -0
  157. package/dist-lib/lib/run-log/persisted-actor.js +218 -0
  158. package/dist-lib/lib/run-log/protocol-types.d.ts +153 -0
  159. package/dist-lib/lib/run-log/protocol-types.js +1 -0
  160. package/dist-lib/lib/run-log/protocol.d.ts +27 -0
  161. package/dist-lib/lib/run-log/protocol.js +1056 -0
  162. package/dist-lib/lib/run-log/replay.d.ts +25 -0
  163. package/dist-lib/lib/run-log/replay.js +129 -0
  164. package/dist-lib/lib/run-log/run-journal.d.ts +125 -0
  165. package/dist-lib/lib/run-log/run-journal.js +783 -0
  166. package/dist-lib/lib/run-log-inspect.d.ts +25 -0
  167. package/dist-lib/lib/run-log-inspect.js +627 -0
  168. package/dist-lib/lib/run-log-protocol.d.ts +2 -0
  169. package/dist-lib/lib/run-log-protocol.js +1 -0
  170. package/dist-lib/lib/run-log-redact.d.ts +27 -0
  171. package/dist-lib/lib/run-log-redact.js +1107 -0
  172. package/dist-lib/lib/serve.js +308 -12
  173. package/dist-lib/lib/spawn.js +3 -2
  174. package/dist-lib/lib/tool-call-display.d.ts +1 -21
  175. package/dist-lib/lib/tools/exec.d.ts +2 -1
  176. package/dist-lib/lib/tools/exec.js +57 -2
  177. package/dist-lib/lib/tools/generate-image.js +1 -1
  178. package/dist-lib/lib/tools/generate-music.js +1 -1
  179. package/dist-lib/lib/tools/http-request.d.ts +2 -1
  180. package/dist-lib/lib/tools/http-request.js +2 -1
  181. package/dist-lib/lib/tools/mcp-call.js +8 -1
  182. package/dist-lib/lib/tools/operations/builders.d.ts +1 -0
  183. package/dist-lib/lib/tools/operations/builders.js +1 -0
  184. package/dist-lib/lib/tools/operations/compile.js +11 -1
  185. package/dist-lib/lib/tools/operations/manifest.js +1 -0
  186. package/dist-lib/lib/tools/outbound-guard.d.ts +8 -6
  187. package/dist-lib/lib/tools/outbound-guard.js +25 -8
  188. package/dist-lib/lib/tools/skills/catalog.d.ts +1 -0
  189. package/dist-lib/lib/tools/skills/catalog.js +35 -0
  190. package/dist-lib/lib/tools/skills/cli-schema-types.d.ts +1 -0
  191. package/dist-lib/lib/tools/skills/cli-schema.js +25 -0
  192. package/dist-lib/lib/wire.d.ts +5 -0
  193. package/dist-lib/lib/wire.js +128 -5
  194. package/dist-lib/schema/harness.js +22 -6
  195. package/dist-lib/spec/hrpc/messages.js +100 -18
  196. package/dist-lib/spec/hyperschema/index.js +100 -18
  197. package/dist-lib/spec/hyperschema/schema.json +74 -1
  198. package/dist-lib/spec/hyperschema/types.d.ts +11 -3
  199. package/package.json +19 -1
  200. package/patches/@qvac%2Finference@0.20.0.patch +67 -0
  201. package/patches/bare-stow@0.1.5.patch +68 -0
  202. package/patches/hypercore-blob-server@1.15.0.patch +15 -0
  203. package/patches/localwatch@1.4.3.patch +13 -0
  204. package/patches/pyodide@314.0.2.patch +144 -0
@@ -1,1303 +1,22 @@
1
- import process from 'process';
2
- import { contextCandidates } from '../context-window.js';
3
- import { errorCode, errorMessage, isRecord } from '../guards.js';
4
- import { bufferBurst } from '../helpers/buffer-burst.js';
5
- import { endsTurn, stopAtControlToken, stripControlTokens, truncateAtControlToken } from '../helpers/control-tokens.js';
6
- import { createMarkupStreamFilter } from '../helpers/markup-stream-filter.js';
7
- import { AbortedError, raceAbort } from '../helpers/race-abort.js';
8
- import { raceTimeout } from '../helpers/race-timeout.js';
9
- import { createRunOutcomeTracker, degenerateAnswer, unnamedWrites, writeReport } from '../helpers/run-outcome.js';
10
- import { splitThinking } from '../helpers/thinking.js';
11
- import { clipRawCall, normalizeToolArgs, recoverMarkupToolCalls, recoverPrintedSkillLoad, recoverSkillNamedCall, stripToolCallMarkup, stripToolResponseBlocks, toolCallMarkup } from '../helpers/tool-call-markup.js';
12
- import { INTENT_PARAM, INTENT_PARAM_DESCRIPTION, takeToolIntent } from '../helpers/tool-intent.js';
13
- import { HookPipeline } from '../hooks/pipeline.js';
14
- import { fallbackToolLabel } from '../helpers/tool-label.js';
15
- import { hlog } from '../log.js';
16
- import { resolveLlmGenerationParams } from '../models/llm-presets.js';
17
- import { customToolDialect } from '../models/model-catalog.js';
18
- import { isContextOverflowError, isSdkModelWedgedError } from '../models/sdk-faults.js';
19
- import { annotateLastUserTime, skillsPromptSection, systemPromptParts } from '../prompt.js';
20
- import { isMetadataMethod } from '../tools/mcp/protocol.js';
21
- import { unquoteArg } from '../tools/model-args.js';
22
- import { SKILL_TOOL_NAME, WEB_SEARCH_TOOL_NAME } from './tool-names.js';
23
- import { mainFileRequested, skillFileArg, skillFileStem } from '../tools/skill.js';
24
- import { promptUsageParts, usageBreakdown } from './usage-breakdown.js';
25
- import { collectToolGrants, enabledTools, isSelected, skillIsActive, skillsAreSelected } from './tool-selection.js';
26
- import { guardToolCall } from '../tools/outbound-guard.js';
27
- import { isAvailableState } from '../tools/skills/skill-state-constants.js';
28
- import { resolveImplicitSkill, resolveSkillMention, skillPreloadMessage } from '../tools/skills/mention.js';
29
- import { NETWORK_TOOLSETS, permittedTools } from '../tools/toolsets.js';
30
- import { promptPrefixHash } from './prompt-prefix.js';
31
- import { logToolPrompt } from './tool-prompt-log.js';
32
- import { DIRECT_CALL_ID, loadedSkillFilesFrom, loadedSkillsFrom, TOOL_CONTINUATION_NUDGE, toolResultTurn, withoutSkillLoadTurns } from './tool-turns.js';
33
- export function imageSettingsFrom(agent) {
34
- if (!agent)
35
- return undefined;
36
- const settings = {};
37
- if (agent.imageModelChoice) {
38
- settings.modelChoice = agent.imageModelChoice;
39
- }
40
- if (agent.imageSteps) {
41
- settings.steps = agent.imageSteps;
42
- }
43
- if (agent.imageWidth) {
44
- settings.width = agent.imageWidth;
45
- }
46
- if (agent.imageHeight) {
47
- settings.height = agent.imageHeight;
48
- }
49
- return Object.keys(settings).length ? settings : undefined;
50
- }
51
- const BURST_WINDOW_MS = 100;
52
- const MAX_TOOL_ROUNDS = 10;
53
- const RUN_WALL_CLOCK_BUDGET_MS = 30 * 60_000;
54
- function runWallClockBudgetMs() {
55
- const ms = Number(process.env.QVAC_RUN_BUDGET_MS);
56
- return ms > 0 ? ms : RUN_WALL_CLOCK_BUDGET_MS;
57
- }
58
- export const CONTEXT_OVERFLOW_MESSAGE = 'This message is too large for the model context window.';
59
- const MAX_MUTE_ROUNDS = 1;
60
- const NARRATED_INTENT = /\b(I['’]ll|I will|Now I|Let me(?! know)|Next,? I)\b[^.!?]*[.!?]?\s*$/i;
61
- const MAX_WEB_SEARCHES_PER_TURN = 3;
62
- const MAX_CAPPED_ROUNDS = 1;
63
- const WEB_SEARCH_LIMIT_MESSAGE = 'Web search limit reached for this question. Answer now from the results already gathered. ' +
64
- 'If something was not found, say so plainly and mention the most relevant sources. ' +
65
- 'Do not call web_search again.';
66
- export function trustedSourcesFrom(history) {
67
- const sources = [];
68
- for (const entry of history) {
69
- const text = String(entry.content ?? '');
70
- if (entry.role === 'user') {
71
- sources.push({ origin: 'user', text });
72
- }
73
- else if (entry.role === 'tool') {
74
- sources.push({ origin: originOfToolTurn(text), text });
75
- }
76
- }
77
- return sources;
78
- }
79
- function originOfToolTurn(text) {
80
- return text.startsWith(`[Tool: ${SKILL_TOOL_NAME} (`) ? 'skill' : 'tool-result';
81
- }
82
- const noop = () => { };
83
- function intentParameters(tool, params) {
84
- if (tool.autoApprove === true || INTENT_PARAM in params.properties) {
85
- return params;
86
- }
87
- return {
88
- ...params,
89
- properties: {
90
- [INTENT_PARAM]: { type: 'string', description: INTENT_PARAM_DESCRIPTION },
91
- ...params.properties
92
- },
93
- required: [INTENT_PARAM, ...(params.required ?? [])]
94
- };
95
- }
96
- export function toSdkTool(tool) {
97
- return {
98
- type: 'function',
99
- name: tool.name,
100
- description: tool.prompt?.description ?? tool.description,
101
- parameters: intentParameters(tool, tool.prompt?.parameters ?? tool.parameters)
102
- };
103
- }
104
- function sanitizeTurn(turn) {
105
- const stripped = stripControlTokens(turn.content);
106
- const content = turn.role === 'assistant' ? stripToolCallMarkup(stripped) : stripped;
107
- return content === turn.content ? turn : { ...turn, content };
108
- }
109
- function lastUserContent(messages) {
110
- for (let i = messages.length - 1; i >= 0; i--) {
111
- const turn = messages[i];
112
- if (turn?.role === 'user') {
113
- return turn.content;
114
- }
115
- }
116
- return '';
117
- }
118
- function lastUserImages(messages) {
119
- for (let i = messages.length - 1; i >= 0; i--) {
120
- const turn = messages[i];
121
- if (turn?.role === 'user') {
122
- return turn.attachments ?? [];
123
- }
124
- }
125
- return [];
126
- }
127
- function explicitWebSearchQuery(text, tools, { skillPreloaded }) {
128
- if (!tools.some((tool) => tool.name === WEB_SEARCH_TOOL_NAME))
129
- return null;
130
- if (explicitlyDisablesWebSearch(text))
131
- return null;
132
- const named = /\b(?:search|browse|look\s+up|find)\s+(?:the\s+)?(?:web|internet|online)\b/i.test(text);
133
- const freshness = /\b(?:latest|current|recent)\s+(?:news|events)\b/i.test(text) ||
134
- /\bwhat(?:'s|\s+is)\s+(?:the\s+)?(?:latest|current|recent)\b/i.test(text);
135
- const explicit = named || (freshness && !skillPreloaded);
136
- return explicit && text.trim() ? text.trim() : null;
137
- }
138
- function explicitlyDisablesWebSearch(text) {
139
- return (/\b(?:do\s+not|don't|dont|never)\s+(?:search|browse|look\s+up|find|use)\s+(?:the\s+)?(?:web|internet|online)\b/i.test(text) ||
140
- /\b(?:avoid|without)\s+(?:searching|browsing|using)\s+(?:the\s+)?(?:web|internet|online)\b/i.test(text));
141
- }
142
- function directToolHistory(history, args, output) {
143
- return [
144
- ...history,
145
- {
146
- role: 'assistant',
147
- content: toolCallMarkup([{ name: WEB_SEARCH_TOOL_NAME, arguments: args }])
148
- },
149
- { role: 'tool', content: toolResultTurn(WEB_SEARCH_TOOL_NAME, DIRECT_CALL_ID, output) }
150
- ];
151
- }
152
- function assistantToolTurn(contentText, calls) {
153
- if (!calls.length || contentText.includes('<function='))
154
- return contentText;
155
- const markup = toolCallMarkup(calls);
156
- return contentText ? `${contentText}\n${markup}` : markup;
157
- }
158
- function preloadLastUser(messages, name, body) {
159
- for (let i = messages.length - 1; i >= 0; i--) {
160
- const turn = messages[i];
161
- if (turn?.role !== 'user') {
162
- continue;
163
- }
164
- const next = [...messages];
165
- next[i] = { ...turn, content: skillPreloadMessage(turn.content, name, body) };
166
- return next;
167
- }
168
- return messages;
169
- }
170
- function collectStats(state, stats, stopReason) {
171
- state.stopReason = stopReason;
172
- if (!stats) {
173
- return;
174
- }
175
- if (state.ttft === undefined && stats.timeToFirstToken) {
176
- state.ttft = stats.timeToFirstToken;
177
- }
178
- if (state.promptTokensPerSecond === undefined && stats.promptTokensPerSecond) {
179
- state.promptTokensPerSecond = stats.promptTokensPerSecond;
180
- }
181
- if (stats.generatedTokens) {
182
- state.tokens += stats.generatedTokens;
183
- if (stats.tokensPerSecond) {
184
- state.durationMs += (stats.generatedTokens / stats.tokensPerSecond) * 1000;
185
- state.timedTokens += stats.generatedTokens;
186
- }
187
- }
188
- if (stats.promptTokens) {
189
- state.promptTokens = stats.promptTokens;
190
- }
191
- if (stats.cacheTokens) {
192
- state.cacheTokens = stats.cacheTokens;
193
- }
194
- }
195
- function metricsEvent(state, usage) {
196
- if (!state.tokens)
197
- return null;
198
- return {
199
- type: 'metrics',
200
- totalTokens: state.tokens,
201
- durationMs: Math.round(state.durationMs),
202
- timeToFirstToken: state.ttft,
203
- promptTokensPerSecond: state.promptTokensPerSecond,
204
- tokensPerSecond: state.durationMs > 0 ? (state.timedTokens / state.durationMs) * 1000 : undefined,
205
- promptTokens: state.promptTokens,
206
- cacheTokens: state.cacheTokens,
207
- ctxSize: state.ctxSize || undefined,
208
- backends: state.backends.length ? state.backends : undefined,
209
- usage: usage ?? undefined,
210
- stopReason: state.stopReason || undefined
211
- };
212
- }
213
- const TOOL_DENIED_MESSAGE = 'The user declined this call. Do not retry it or call it with different arguments to get ' +
214
- 'around the refusal. Tell the user plainly what you could not do, and never present a ' +
215
- 'result you did not obtain.';
216
- function deniedResult() {
217
- return { error: TOOL_DENIED_MESSAGE, denied: true };
218
- }
219
- function callKey(name, args) {
220
- try {
221
- return `${name}:${JSON.stringify(args)}`;
222
- }
223
- catch {
224
- return name;
225
- }
226
- }
227
- const RETRYABLE_STATUS = new Set([408, 429]);
228
- const RETRYABLE_CODE = new Set(['timeout', 'transport', 'busy']);
229
- function callSucceeded(result, outcome) {
230
- if (outcome.kind !== 'success')
231
- return false;
232
- const exitCode = isRecord(result) ? result.exitCode : undefined;
233
- return typeof exitCode !== 'number' || exitCode === 0;
234
- }
235
- function retryHints(result) {
236
- if (!result || typeof result !== 'object' || Array.isArray(result))
237
- return {};
238
- const status = 'status' in result ? result.status : undefined;
239
- const code = 'code' in result ? result.code : undefined;
240
- return {
241
- status: typeof status === 'number' ? status : undefined,
242
- code: typeof code === 'string' ? code : undefined
243
- };
244
- }
245
- function settledFailure(result, outcome) {
246
- if (outcome.kind === 'denied' || callSucceeded(result, outcome))
247
- return false;
248
- if (outcome.kind === 'error' && outcome.error !== undefined)
249
- return false;
250
- const { code, status } = retryHints(result);
251
- if (code !== undefined && RETRYABLE_CODE.has(code)) {
252
- return false;
253
- }
254
- return status === undefined || (status < 500 && !RETRYABLE_STATUS.has(status));
255
- }
256
- const MAX_BLOCKED_REPEATS = 3;
257
- function terminalCall(result, outcome, options) {
258
- return { result, outcome, executed: options.executed };
259
- }
260
- function executedCall(result, outcome) {
261
- return terminalCall(result, outcome, { executed: true });
262
- }
263
- function guidanceResult(message) {
264
- return terminalCall({ error: message, guidance: true }, { kind: 'guidance', message }, {
265
- executed: false
266
- });
267
- }
268
- function blockedResult(result, reason) {
269
- return terminalCall(result, { kind: 'blocked', reason }, { executed: false });
270
- }
271
- function deniedCall() {
272
- return terminalCall(deniedResult(), { kind: 'denied', reason: TOOL_DENIED_MESSAGE }, {
273
- executed: false
274
- });
275
- }
276
- async function executeCall(tool, args, { ctx, approve, signal, denied, sources = [], effect: classified, skipApprovals, onToolActivity, intent, display }) {
277
- const validationError = tool.validate?.(args, ctx);
278
- if (validationError) {
279
- hlog('tool rejected: arguments failed validation', { tool: tool.name });
280
- return guidanceResult(validationError);
281
- }
282
- const refusal = guardToolCall(tool.name, args, {
283
- sources,
284
- externalWrite: tool.externalWrite === true
285
- });
286
- if (refusal) {
287
- hlog('tool refused: unverifiable value in payload', { tool: tool.name });
288
- onToolActivity?.({
289
- type: 'tool-activity',
290
- name: tool.name,
291
- args,
292
- resource: tool.resource?.(args, ctx) ?? null,
293
- effect: tool.effect(args, ctx),
294
- decision: 'blocked'
295
- });
296
- return blockedResult({ error: refusal, guidance: true }, refusal);
297
- }
298
- const key = callKey(tool.name, args);
299
- if (denied?.has(key)) {
300
- hlog('tool denied already, not asking again', { tool: tool.name });
301
- return deniedCall();
302
- }
303
- const effect = classified ?? tool.effect(args, ctx);
304
- const resource = tool.resource?.(args, ctx) ?? null;
305
- const autoRun = effect === 'read' || tool.autoApprove === true || skipApprovals === true;
306
- hlog('tool decision', {
307
- tool: tool.name,
308
- effect,
309
- auto: autoRun,
310
- asks: Boolean(approve)
311
- });
312
- const approved = autoRun ||
313
- (approve
314
- ? await raceAbort(approve({
315
- name: tool.name,
316
- arguments: args,
317
- resource,
318
- effect,
319
- intent: intent ?? null,
320
- display: display ?? null
321
- }), signal)
322
- : false);
323
- hlog(`tool ${approved ? 'approved' : 'denied'}`, { tool: tool.name });
324
- const decision = toolDecision({
325
- auto: tool.autoApprove === true || effect === 'read',
326
- skipAll: skipApprovals === true,
327
- approved
328
- });
329
- onToolActivity?.({
330
- type: 'tool-activity',
331
- name: tool.name,
332
- args,
333
- resource,
334
- effect,
335
- decision
336
- });
337
- if (!approved) {
338
- denied?.add(key);
339
- return deniedCall();
340
- }
341
- let output;
342
- let outcome = { kind: 'success' };
343
- try {
344
- output = await tool.execute(args, { ...ctx, signal, approval: decision });
345
- hlog('tool done', { tool: tool.name });
346
- }
347
- catch (err) {
348
- const message = errorMessage(err);
349
- hlog('tool failed', { tool: tool.name, err: message });
350
- output = { error: message, code: errorCode(err) };
351
- outcome = { kind: 'error', message, error: err };
352
- }
353
- return executedCall(output, outcome);
354
- }
355
- function runToolContext(scope) {
356
- const { agent, attach, fetchAttachment, ragSearch, progress, askUser, signal, runBudget } = scope;
357
- const image = imageSettingsFrom(agent);
358
- return (turnImages = [], ctxSize) => ({
359
- agentId: agent?.id ?? null,
360
- ctxSize,
361
- image,
362
- turnImages: turnImages.length ? turnImages : undefined,
363
- attach,
364
- fetchAttachment,
365
- ragSearch,
366
- progress,
367
- askUser,
368
- signal,
369
- runBudget
370
- });
371
- }
372
- function labelOf(name, args, emoji) {
373
- const display = { label: fallbackToolLabel(name, args) };
374
- return emoji ? { ...display, iconEmoji: emoji } : display;
375
- }
376
- async function settleToolCall(pipeline, execution, call) {
377
- const folded = await pipeline.runPostToolFold({
378
- toolName: call.name,
379
- agentId: call.agentId,
380
- args: call.args,
381
- effect: call.effect ?? 'write',
382
- resource: call.resource,
383
- toolset: call.tool?.toolset,
384
- result: execution.result,
385
- outcome: execution.outcome,
386
- executed: execution.executed
387
- });
388
- return {
389
- result: folded.result,
390
- outcome: folded.outcome ?? execution.outcome
1
+ import { trackCompletion } from '../completion-lifecycle.js';
2
+ import { driveCompletion } from './completion-engine/driver.js';
3
+ import { createRunLogRedactor } from '../run-log-redact.js';
4
+ export * from './types.js';
5
+ export { CONTEXT_OVERFLOW_MESSAGE, imageSettingsFrom, loadedSkillsFrom, skillInContext, skillLoadedTurn, toSdkTool, trustedSourcesFrom, unreadySkillState } from './completion-loop/index.js';
6
+ export function completion(sdk, args, options = {}) {
7
+ const redactor = createRunLogRedactor();
8
+ redactor.registerTools(args.tools ?? []);
9
+ const runtime = typeof sdk === 'function' ? sdk : async () => ({ sdk });
10
+ const loadRuntime = async (request) => {
11
+ const loaded = await runtime(request);
12
+ redactor.registerTools(loaded.args?.tools ?? args.tools ?? []);
13
+ return loaded;
391
14
  };
392
- }
393
- function skillInContext(entry) {
394
- return entry.status === undefined || isAvailableState(entry.status);
395
- }
396
- function unreadySkillState(name, catalog) {
397
- const status = catalog.find((entry) => entry.name === name)?.status;
398
- return status !== undefined && !isAvailableState(status) ? status : null;
399
- }
400
- function blockedSkillMessage(name, catalog, listShown) {
401
- if (catalog.some((entry) => entry.name === name)) {
402
- return (`skill "${name}" is not enabled for this agent. Do not rebuild what it does with another ` +
403
- `tool — answer from what you already have, or tell the user to enable it in Skills.`);
404
- }
405
- return listShown
406
- ? `there is no skill named "${name}". Load one from the skills list you were shown, or ` +
407
- `answer from what you already have — do not guess another name.`
408
- : `there is no skill named "${name}". You were not shown a skills list, which means no ` +
409
- `skill is available this turn — do not guess another name, and answer from what you ` +
410
- `already have.`;
411
- }
412
- async function resolveToolCalls(toolCalls, { enabled, agentId, pipeline, skillNames, sandboxMode, signal }) {
413
- const resolved = [];
414
- for (const call of toolCalls) {
415
- if (signal?.aborted)
416
- throw new AbortedError();
417
- const tool = enabled.find((t) => t.name === call.name);
418
- const rawCall = call.raw === undefined ? undefined : clipRawCall(call.raw);
419
- if (!tool) {
420
- const { args } = takeToolIntent(call.arguments);
421
- resolved.push({ call, args, rawCall });
422
- continue;
423
- }
424
- const taken = takeToolIntent(call.arguments);
425
- const args = normalizeToolArgs(taken.args, Object.keys(tool.parameters.properties));
426
- const intent = taken.intent || undefined;
427
- const fold = await pipeline.runPreToolFold({
428
- toolName: tool.name,
429
- args,
430
- agentId,
431
- skillNames,
432
- parameters: tool.parameters,
433
- sandboxMode,
434
- signal
435
- });
436
- resolved.push(fold.ok
437
- ? { call, tool, args: fold.args, rawCall, intent }
438
- : { call, tool, args, rejection: fold.rejection, rawCall, intent });
439
- }
440
- return resolved;
441
- }
442
- function unprovenReplay(state, final) {
443
- if (!state.unproven)
444
- return null;
445
- if (state.stopToken) {
446
- return endsTurn(state.stopToken) ? state.unproven : null;
447
- }
448
- if (final.stopReason === 'length' || final.stopReason === 'cancelled') {
449
- return null;
450
- }
451
- return state.unproven;
452
- }
453
- async function* runRoundTokens(result, state, { sdk, signal, startInThink = false }) {
454
- const cancelThisRound = () => {
455
- if (result.requestId) {
456
- sdk.cancel?.({ requestId: result.requestId });
457
- }
458
- };
459
- signal?.addEventListener('abort', cancelThisRound, { once: true });
460
- const tokens = stopAtControlToken(result.tokenStream, (token) => {
461
- state.stopToken = token;
462
- });
463
- let tokenEvents = 0;
464
- const events = splitThinking(bufferBurst(tokens, BURST_WINDOW_MS), { startInThink });
465
- const thinkingFilter = createMarkupStreamFilter();
466
- const contentFilter = createMarkupStreamFilter();
467
- try {
468
- while (true) {
469
- const next = await events.next();
470
- if (next.done) {
471
- state.unproven = next.value;
472
- break;
473
- }
474
- if (signal?.aborted) {
475
- throw new AbortedError();
476
- }
477
- tokenEvents++;
478
- if (next.value.type === 'content') {
479
- state.rawContentText += next.value.text;
480
- }
481
- const filter = next.value.type === 'thinking' ? thinkingFilter : contentFilter;
482
- const text = filter.push(next.value.text);
483
- if (text) {
484
- yield { type: next.value.type, text };
485
- }
486
- }
487
- const thinkingTail = thinkingFilter.flush();
488
- if (thinkingTail) {
489
- yield { type: 'thinking', text: thinkingTail };
490
- }
491
- const contentTail = contentFilter.flush();
492
- if (contentTail) {
493
- yield { type: 'content', text: contentTail };
494
- }
495
- hlog('stream ended', { events: tokenEvents, stopToken: state.stopToken });
496
- if (state.stopToken) {
497
- cancelThisRound();
498
- }
499
- }
500
- finally {
501
- signal?.removeEventListener('abort', cancelThisRound);
502
- await events.return(null);
503
- }
504
- }
505
- function sentMessages(stats) {
506
- const sent = stats?.sentMessages;
507
- return Array.isArray(sent) ? sent : null;
508
- }
509
- function roundTokens(stats) {
510
- if (!stats)
511
- return undefined;
512
- const tokens = {
513
- prompt: stats.promptTokens,
514
- generated: stats.generatedTokens,
515
- cached: stats.cacheTokens,
516
- timeToFirstToken: stats.timeToFirstToken
517
- };
518
- return Object.values(tokens).some((value) => value !== undefined) ? tokens : undefined;
519
- }
520
- const TRUNCATED_FINAL_MS = 2000;
521
- function truncatedFinal(final, contentText) {
522
- const accepted = { contentText, toolCalls: [], raw: { fullText: contentText } };
523
- const guarded = final.catch((err) => {
524
- if (isSdkModelWedgedError(err))
525
- throw err;
526
- return accepted;
527
- });
528
- guarded.catch(noop);
529
- return raceTimeout(guarded, { ms: TRUNCATED_FINAL_MS, fallback: accepted });
530
- }
531
- async function* runRecoveredRound(sdk, args) {
532
- const lease = args.acquireRound ? await args.acquireRound() : null;
533
- try {
534
- return yield* runRecoveredRoundWith(sdk, args, lease?.modelId ?? args.modelId);
535
- }
536
- finally {
537
- lease?.release();
538
- }
539
- }
540
- async function* runRecoveredRoundWith(sdk, args, modelId) {
541
- let lastOverflow = null;
542
- for (const candidate of contextCandidates(args.history)) {
543
- if (args.signal?.aborted)
544
- throw new AbortedError();
545
- let contentText = '';
546
- let emitted = false;
547
- const state = { stopToken: null, unproven: null, rawContentText: '' };
548
- const cacheable = candidate.history === args.history;
549
- const params = {
550
- modelId,
551
- history: candidate.history,
552
- stream: true,
553
- tools: args.tools,
554
- generationParams: args.generationParams,
555
- responseFormat: args.responseFormat,
556
- toolDialect: args.toolDialect,
557
- kvCache: cacheable ? args.cacheKey : undefined
558
- };
559
- if (args.capture) {
560
- yield {
561
- type: 'inference-call',
562
- call: {
563
- round: args.round,
564
- droppedTurns: candidate.droppedTurns,
565
- modelId,
566
- messages: params.history,
567
- tools: params.tools,
568
- generationParams: params.generationParams,
569
- responseFormat: params.responseFormat,
570
- toolDialect: params.toolDialect,
571
- cacheKey: params.kvCache,
572
- promptPrefix: args.promptPrefix
573
- }
574
- };
575
- }
576
- try {
577
- const result = sdk.completion(params);
578
- result.final.catch(noop);
579
- const startInThink = args.generationParams?.reasoning_budget === -1;
580
- for await (const event of runRoundTokens(result, state, {
581
- sdk,
582
- signal: args.signal,
583
- startInThink
584
- })) {
585
- contentText += event.type === 'content' ? event.text : '';
586
- emitted = true;
587
- yield event;
588
- }
589
- const final = state.stopToken
590
- ? await truncatedFinal(result.final, state.rawContentText)
591
- : await raceAbort(result.final, args.signal);
592
- if (args.capture) {
593
- yield {
594
- type: 'inference-sent',
595
- round: args.round,
596
- droppedTurns: candidate.droppedTurns,
597
- messages: sentMessages(final.stats) ?? undefined,
598
- tokens: roundTokens(final.stats),
599
- outcome: 'completed',
600
- reason: undefined
601
- };
602
- }
603
- const replay = unprovenReplay(state, final);
604
- hlog('final', {
605
- stopReason: final.stopReason,
606
- toolCalls: final.toolCalls?.length ?? 0,
607
- stopToken: state.stopToken,
608
- replay: replay !== null
609
- });
610
- return {
611
- contentText,
612
- final,
613
- history: candidate.history,
614
- cacheable,
615
- unproven: state.unproven,
616
- replay
617
- };
618
- }
619
- catch (err) {
620
- if (args.capture) {
621
- yield {
622
- type: 'inference-sent',
623
- round: args.round,
624
- droppedTurns: candidate.droppedTurns,
625
- messages: undefined,
626
- tokens: undefined,
627
- outcome: isContextOverflowError(err) ? 'overflowed' : 'failed',
628
- reason: errorMessage(err)
629
- };
630
- }
631
- if (!isContextOverflowError(err)) {
632
- throw err;
633
- }
634
- if (emitted) {
635
- throw new Error(CONTEXT_OVERFLOW_MESSAGE, { cause: err });
636
- }
637
- lastOverflow = err;
638
- }
639
- }
640
- if (lastOverflow) {
641
- throw new Error(CONTEXT_OVERFLOW_MESSAGE, { cause: lastOverflow });
642
- }
643
- throw new Error('context recovery produced no completion candidate');
644
- }
645
- function* reportUnnamedWrites(said, landed) {
646
- const unreported = writeReport(unnamedWrites(said, landed));
647
- if (unreported) {
648
- yield { type: 'content', text: unreported };
649
- }
650
- }
651
- export async function* completion(sdk, { modelId, acquireRound, model, agent, ctxSize, cacheKey, thinkingDefault, generationParams: generationParamsOverride, responseFormat, messages: storedMessages, tools: allTools = [], skillCatalog = [], approve, attach, fetchAttachment, ragSearch, progress, askUser, signal, toolConfigs, fsRoots = [], fsWriteRoots = [], captureInferenceCalls = false }) {
652
- const pipeline = new HookPipeline();
653
- const agentId = agent?.id ?? null;
654
- const disabledToolsets = agent?.sandboxMode
655
- ? [...(agent.disabledToolsets ?? []), ...NETWORK_TOOLSETS]
656
- : agent?.disabledToolsets;
657
- const tools = permittedTools(allTools, disabledToolsets);
658
- const writableRoots = [
659
- ...fsWriteRoots,
660
- ...fsRoots.filter((root) => root.kind === 'hyper' && root.writable && !fsWriteRoots.includes(root))
661
- ];
662
- const messages = storedMessages.map(sanitizeTurn);
663
- const stats = { tokens: 0, timedTokens: 0, durationMs: 0, ctxSize, backends: [] };
664
- let runOutcome = 'abandoned';
665
- let runError;
666
- const runOutcomes = createRunOutcomeTracker();
667
- const landed = runOutcomes.confirmedWrites;
668
- async function* runTurn() {
669
- if (signal?.aborted)
670
- throw new AbortedError();
671
- await pipeline.onRunStart({ agentId, messages });
672
- const runBudget = {
673
- deadlineMs: Date.now() + runWallClockBudgetMs(),
674
- outputBytesUsed: 0
675
- };
676
- const toolContext = runToolContext({
677
- agent,
678
- attach,
679
- fetchAttachment,
680
- ragSearch,
681
- progress,
682
- askUser,
683
- signal,
684
- runBudget
685
- });
686
- const turnImages = lastUserImages(messages);
687
- const toolsSelected = isSelected(agent?.toolsScope, agent?.enabledTools);
688
- const skillsSelected = skillsAreSelected(agent?.skillsScope, Boolean(agent));
689
- const inContextSkills = skillCatalog.filter(skillInContext);
690
- const mention = resolveSkillMention(lastUserContent(messages), inContextSkills);
691
- const loadedSkills = loadedSkillsFrom(messages);
692
- const loadedSkillFiles = loadedSkillFilesFrom(messages);
693
- const activation = {
694
- skillsSelected,
695
- enabledSkills: agent?.enabledSkills ?? [],
696
- mention: mention?.name ?? null,
697
- loadedSkills
698
- };
699
- const activeSkills = inContextSkills.filter((skill) => skillIsActive(skill.name, activation));
700
- const { grantedNames, grantedOwners, grantedOps } = collectToolGrants(activeSkills);
701
- function selectEnabled() {
702
- if (responseFormat)
703
- return [];
704
- const loaded = activeSkills.filter((skill) => loadedSkills.has(skill.name));
705
- return enabledTools(tools, {
706
- toolsSelected,
707
- allowlist: agent?.enabledTools ?? [],
708
- grantedNames,
709
- grantedOwners,
710
- loadedGrantedNames: collectToolGrants(loaded).grantedNames,
711
- hasRoots: fsRoots.length > 0,
712
- hasWriteRoots: writableRoots.length > 0,
713
- hasActiveSkills: activeSkills.length > 0
714
- });
715
- }
716
- const reasoningEnabled = reasoningFlag(agent?.reasoning);
717
- const deviceReasoning = reasoningFlag(thinkingDefault);
718
- const resolvedGenerationParams = resolveLlmGenerationParams(model, reasoningEnabled, deviceReasoning);
719
- const generationParams = generationParamsOverride
720
- ? { ...resolvedGenerationParams, ...generationParamsOverride }
721
- : resolvedGenerationParams;
722
- const loadedThisTurn = new Set();
723
- const loadedFilesThisTurn = new Set();
724
- const fileKey = (skill, file) => `${skill}\0${skillFileStem(file)}`;
725
- let skillIcon = null;
726
- const skillEmoji = (name) => skillCatalog.find((entry) => entry.name === name)?.emoji ?? null;
727
- const nudgedSkills = new Set();
728
- function recordSkillLoad(skillName, output, { mainRequested, askedFile }) {
729
- if (!output || typeof output !== 'object' || !('content' in output)) {
730
- return;
731
- }
732
- loadedSkills.add(skillName);
733
- if ('file' in output && typeof output.file === 'string') {
734
- const files = loadedSkillFiles.get(skillName) ?? new Set();
735
- files.add(output.file);
736
- loadedSkillFiles.set(skillName, files);
737
- }
738
- if (mainRequested) {
739
- loadedThisTurn.add(skillName);
740
- }
741
- else {
742
- loadedFilesThisTurn.add(fileKey(skillName, unquoteArg(askedFile)));
743
- }
744
- skillIcon = skillEmoji(skillName) ?? skillIcon;
745
- }
746
- const toolDialect = model ? customToolDialect(model) : undefined;
747
- let turns = messages;
748
- let preloadedSkill = null;
749
- const preload = mention ?? resolveImplicitSkill(lastUserContent(messages), activeSkills);
750
- if (preload) {
751
- const loader = tools.find((tool) => tool.name === SKILL_TOOL_NAME);
752
- const loaded = loader ? await loader.execute({ name: preload.name }, toolContext()) : null;
753
- if (loaded && typeof loaded === 'object' && 'content' in loaded) {
754
- preloadedSkill = String(loaded.content);
755
- const inlined = preloadLastUser(messages, preload.name, preloadedSkill);
756
- turns = withoutSkillLoadTurns(inlined, preload.name);
757
- if (!loadedSkills.has(preload.name)) {
758
- const preloadArgs = { name: preload.name };
759
- const preloadEffect = 'read';
760
- const preloadResource = loader?.resource?.(preloadArgs) ?? null;
761
- yield {
762
- type: 'tool-call',
763
- name: SKILL_TOOL_NAME,
764
- args: preloadArgs,
765
- effect: preloadEffect,
766
- display: labelOf(SKILL_TOOL_NAME, preloadArgs, skillEmoji(preload.name))
767
- };
768
- yield {
769
- type: 'tool-activity',
770
- name: SKILL_TOOL_NAME,
771
- args: { name: preload.name },
772
- resource: null,
773
- effect: 'read',
774
- decision: 'auto'
775
- };
776
- const settled = await settleToolCall(pipeline, executedCall(loaded, { kind: 'success' }), {
777
- agentId,
778
- tool: loader,
779
- name: SKILL_TOOL_NAME,
780
- args: preloadArgs,
781
- effect: preloadEffect,
782
- resource: preloadResource
783
- });
784
- yield {
785
- type: 'tool-result',
786
- name: SKILL_TOOL_NAME,
787
- result: settled.result
788
- };
789
- }
790
- }
791
- }
792
- if (preload && preloadedSkill) {
793
- loadedSkills.add(preload.name);
794
- loadedThisTurn.add(preload.name);
795
- skillIcon = skillEmoji(preload.name) ?? skillIcon;
796
- }
797
- let enabled = selectEnabled();
798
- let sdkTools = enabled.length ? enabled.map((tool) => toSdkTool(tool)) : undefined;
799
- const system = systemPromptParts({
800
- bio: agent?.bio,
801
- toolNames: enabled.map((tool) => tool.name),
802
- skillsSection: skillsPromptSection(activeSkills)
803
- });
804
- const systemContent = system.text;
805
- let promptPrefix = promptPrefixHash({ system: systemContent, tools: sdkTools });
806
- let promptParts = promptUsageParts({ system, toolSchemas: sdkTools });
807
- function logSelection() {
808
- logToolPrompt({
809
- promptPrefix,
810
- modelId,
811
- cacheKey,
812
- agentId: agent?.id,
813
- messageCount: messages.length,
814
- responseFormat: responseFormat?.type,
815
- tools: sdkTools,
816
- definitions: enabled,
817
- activeSkills: activeSkills.map((skill) => skill.name),
818
- skillsSelected,
819
- enabledSkills: agent?.enabledSkills ?? [],
820
- mentionedSkill: mention?.name ?? null,
821
- loadedSkills,
822
- toolOps: grantedOps
823
- });
824
- }
825
- logSelection();
826
- let history = annotateLastUserTime(systemContent ? [{ role: 'system', content: systemContent }, ...turns] : turns);
827
- let roundCacheKey = preloadedSkill === null ? cacheKey : undefined;
828
- const runUsage = () => usageBreakdown(promptParts, stats.cacheTokens, model);
829
- const denied = new Set();
830
- const failed = new Map();
831
- let blockedRepeats = 0;
832
- let webSearches = 0;
833
- const sources = trustedSourcesFrom(messages);
834
- if (preloadedSkill) {
835
- sources.push({ origin: 'skill', text: preloadedSkill });
836
- }
837
- const query = explicitWebSearchQuery(lastUserContent(messages), enabled, {
838
- skillPreloaded: preloadedSkill !== null
839
- });
840
- const webSearch = enabled.find((tool) => tool.name === WEB_SEARCH_TOOL_NAME);
841
- if (query && webSearch) {
842
- runOutcomes.beginRound();
843
- const fold = await pipeline.runPreToolFold({
844
- toolName: webSearch.name,
845
- args: { query },
846
- agentId: agent?.id ?? null,
847
- signal
848
- });
849
- const args = fold.ok ? fold.args : { query };
850
- const searchEffect = webSearch.effect(args);
851
- const searchResource = webSearch.resource?.(args) ?? null;
852
- const searchCallDisplay = labelOf(webSearch.name, args, null);
853
- if (signal?.aborted) {
854
- throw new AbortedError();
855
- }
856
- yield {
857
- type: 'tool-call',
858
- name: webSearch.name,
859
- args,
860
- effect: searchEffect,
861
- display: searchCallDisplay
862
- };
863
- let activity = null;
864
- const execution = fold.ok
865
- ? await executeCall(webSearch, args, {
866
- ctx: toolContext(turnImages, ctxSize),
867
- approve,
868
- signal,
869
- denied,
870
- skipApprovals: agent?.approvalMode === 'skip-all',
871
- display: searchCallDisplay,
872
- onToolActivity: (a) => (activity = a)
873
- })
874
- : terminalCall(fold.rejection, { kind: 'guidance', message: fold.rejection.error }, {
875
- executed: false
876
- });
877
- const settled = await settleToolCall(pipeline, execution, {
878
- agentId,
879
- tool: webSearch,
880
- name: webSearch.name,
881
- args,
882
- effect: searchEffect,
883
- resource: searchResource
884
- });
885
- const output = settled.result;
886
- const executionOutcome = settled.outcome;
887
- runOutcomes.record(output, executionOutcome);
888
- runOutcomes.finishRound();
889
- if (fold.ok) {
890
- webSearches++;
891
- }
892
- if (!activity && !fold.ok) {
893
- activity = {
894
- type: 'tool-activity',
895
- name: webSearch.name,
896
- args,
897
- resource: searchResource,
898
- effect: searchEffect,
899
- decision: 'blocked'
900
- };
901
- }
902
- if (activity) {
903
- yield activity;
904
- }
905
- yield {
906
- type: 'tool-result',
907
- name: webSearch.name,
908
- result: output
909
- };
910
- history = directToolHistory(history, args, output);
911
- }
912
- let outcome = 'exhausted';
913
- let said = '';
914
- let mute = 0;
915
- let narrated = false;
916
- let cappedRounds = 0;
917
- let roundsSent = 0;
918
- let round = 0;
919
- for (; round < MAX_TOOL_ROUNDS; round++) {
920
- if (signal?.aborted)
921
- throw new AbortedError();
922
- if (Date.now() >= runBudget.deadlineMs) {
923
- outcome = 'time-capped';
924
- break;
925
- }
926
- const reselected = selectEnabled();
927
- if (reselected.map((tool) => tool.name).join() !== enabled.map((tool) => tool.name).join()) {
928
- enabled = reselected;
929
- sdkTools = enabled.length ? enabled.map((tool) => toSdkTool(tool)) : undefined;
930
- promptPrefix = promptPrefixHash({ system: systemContent, tools: sdkTools });
931
- promptParts = promptUsageParts({ system, toolSchemas: sdkTools });
932
- logSelection();
933
- }
934
- hlog('round start', {
935
- round,
936
- turns: history.length,
937
- tools: sdkTools?.length ?? 0,
938
- promptPrefix
939
- });
940
- const roundStarted = Date.now();
941
- const recovered = yield* runRecoveredRound(sdk, {
942
- modelId,
943
- acquireRound,
944
- history,
945
- tools: sdkTools,
946
- generationParams,
947
- responseFormat,
948
- toolDialect,
949
- cacheKey: roundCacheKey,
950
- signal,
951
- round,
952
- promptPrefix,
953
- capture: captureInferenceCalls
954
- });
955
- roundsSent = round + 1;
956
- const final = recovered.final;
957
- if (!recovered.cacheable) {
958
- roundCacheKey = undefined;
959
- }
960
- history = recovered.history;
961
- collectStats(stats, final.stats, final.stopReason);
962
- hlog('round stats', { round, wallMs: Date.now() - roundStarted, ...final.stats });
963
- const markup = stripToolResponseBlocks(truncateAtControlToken(final.raw.fullText) || recovered.unproven || '');
964
- let toolCalls = final.toolCalls?.length
965
- ? final.toolCalls
966
- : recoverMarkupToolCalls(markup, enabled);
967
- if (!toolCalls.length && enabled.some((tool) => tool.name === SKILL_TOOL_NAME)) {
968
- const rescued = recoverPrintedSkillLoad(markup, SKILL_TOOL_NAME);
969
- const rescuedArgs = rescued[0]?.arguments;
970
- const alreadyLoaded = rescuedArgs !== undefined &&
971
- (loadedSkillFiles.get(unquoteArg(rescuedArgs.name))?.has(unquoteArg(rescuedArgs.file)) ??
972
- false);
973
- if (!alreadyLoaded) {
974
- toolCalls = rescued;
975
- }
976
- if (toolCalls.length) {
977
- hlog('recovered printed skill load', { round });
978
- }
979
- }
980
- if (!toolCalls.length && enabled.some((tool) => tool.name === SKILL_TOOL_NAME)) {
981
- toolCalls = recoverSkillNamedCall(markup, inContextSkills
982
- .filter((skill) => !nudgedSkills.has(skill.name))
983
- .map((skill) => skill.name), SKILL_TOOL_NAME);
984
- for (const call of toolCalls) {
985
- nudgedSkills.add(unquoteArg(call.arguments.name));
986
- }
987
- if (toolCalls.length) {
988
- hlog('recovered skill-named call', { round });
989
- }
990
- }
991
- if (!toolCalls.length) {
992
- const replay = recovered.replay ? stripToolCallMarkup(recovered.replay) : '';
993
- if (replay) {
994
- yield { type: 'content', text: replay };
995
- }
996
- const spoken = recovered.contentText.trim();
997
- if (enabled.length && !narrated && !replay && NARRATED_INTENT.test(spoken)) {
998
- hlog('round narrated a tool call instead of making it', { round });
999
- narrated = true;
1000
- history = [
1001
- ...history,
1002
- { role: 'assistant', content: spoken },
1003
- { role: 'user', content: TOOL_CONTINUATION_NUDGE }
1004
- ];
1005
- continue;
1006
- }
1007
- if (replay || spoken) {
1008
- said = `${recovered.contentText}\n${replay}`;
1009
- outcome = 'answered';
1010
- break;
1011
- }
1012
- hlog('round produced no answer', { round, mute });
1013
- if (mute++ < MAX_MUTE_ROUNDS) {
1014
- continue;
1015
- }
1016
- outcome = 'mute';
1017
- break;
1018
- }
1019
- mute = 0;
1020
- narrated = false;
1021
- const resolved = await resolveToolCalls(toolCalls, {
1022
- enabled,
1023
- agentId: agent?.id ?? null,
1024
- pipeline,
1025
- skillNames: inContextSkills.map((skill) => skill.name),
1026
- sandboxMode: agent?.sandboxMode === true,
1027
- signal
1028
- });
1029
- if (signal?.aborted) {
1030
- throw new AbortedError();
1031
- }
1032
- history = [
1033
- ...history,
1034
- {
1035
- role: 'assistant',
1036
- content: assistantToolTurn(recovered.contentText, resolved.map(({ call, args }) => ({ name: call.name, arguments: args })))
1037
- }
1038
- ];
1039
- let cappedCalls = 0;
1040
- let progressed = false;
1041
- runOutcomes.beginRound();
1042
- for (const { call, tool, args, rawCall, rejection, intent } of resolved) {
1043
- if (signal?.aborted)
1044
- throw new AbortedError();
1045
- const priorFailure = failed.get(callKey(call.name, args));
1046
- const searchCapped = tool?.name === WEB_SEARCH_TOOL_NAME && webSearches >= MAX_WEB_SEARCHES_PER_TURN;
1047
- const callCtx = {
1048
- ...toolContext(turnImages, ctxSize),
1049
- skills: [...loadedSkills].filter((name) => !skillsSelected ||
1050
- (agent?.enabledSkills ?? []).includes(name) ||
1051
- name === mention?.name),
1052
- settings: toolConfigs?.[call.name],
1053
- toolOps: grantedOps,
1054
- fsRoots,
1055
- fsWriteRoots: writableRoots,
1056
- reportBackend: (use) => stats.backends.push(use),
1057
- sandboxMode: agent?.sandboxMode === true,
1058
- toolNames: enabled.map((tool) => tool.name)
1059
- };
1060
- const skillCall = call.name === SKILL_TOOL_NAME;
1061
- const effect = tool?.effect(args, callCtx) ?? (skillCall ? 'read' : undefined);
1062
- const resource = tool?.resource?.(args) ?? null;
1063
- const skillName = unquoteArg(args?.name ?? args?.skill_name ?? args?.skill);
1064
- const askedFile = skillFileArg(args);
1065
- const mainRequested = mainFileRequested(skillName, askedFile);
1066
- const skillReloaded = tool?.name === SKILL_TOOL_NAME &&
1067
- (mainRequested
1068
- ? loadedThisTurn.has(skillName)
1069
- : loadedFilesThisTurn.has(fileKey(skillName, unquoteArg(askedFile))));
1070
- const silent = searchCapped || skillReloaded || priorFailure !== undefined;
1071
- const display = silent
1072
- ? undefined
1073
- : labelOf(call.name, args, skillCall ? skillEmoji(skillName) : skillIcon);
1074
- if (!silent) {
1075
- yield { type: 'tool-call', name: call.name, args, rawCall, effect, display };
1076
- }
1077
- const unreadyState = skillCall ? unreadySkillState(skillName, skillCatalog) : null;
1078
- const skillBlocked = skillCall && !unreadyState && (!tool || !skillIsActive(skillName, activation));
1079
- let activity = null;
1080
- const blockedReason = blockedSkillMessage(skillName, skillCatalog, activeSkills.length > 0);
1081
- const unreadyReason = `skill "${skillName}" is ${unreadyState ?? 'unavailable'} and cannot be used yet. ` +
1082
- `Tell the user to open it in Skills and finish its setup; do not retry.`;
1083
- let execution;
1084
- if (priorFailure) {
1085
- blockedRepeats++;
1086
- hlog('repeat of a call that already failed; answered without running it', {
1087
- tool: call.name,
1088
- blockedRepeats
1089
- });
1090
- execution = terminalCall(priorFailure.result, priorFailure.outcome, { executed: false });
1091
- }
1092
- else if (skillReloaded) {
1093
- const loadedWhat = mainRequested ? skillName : `${skillName} ${unquoteArg(askedFile)}`;
1094
- execution = terminalCall({
1095
- name: skillName,
1096
- content: `${loadedWhat} is already loaded. Follow the instructions you were given and do not load it again.`
1097
- }, { kind: 'guidance', message: `${skillName} is already loaded.` }, { executed: false });
1098
- }
1099
- else if (skillBlocked) {
1100
- execution = blockedResult({ error: blockedReason }, blockedReason);
1101
- }
1102
- else if (unreadyState) {
1103
- execution = blockedResult({ error: unreadyReason }, unreadyReason);
1104
- }
1105
- else if (searchCapped) {
1106
- execution = terminalCall({ content: WEB_SEARCH_LIMIT_MESSAGE }, { kind: 'success' }, { executed: false });
1107
- }
1108
- else if (rejection) {
1109
- execution = terminalCall(rejection, { kind: 'guidance', message: rejection.error }, {
1110
- executed: false
1111
- });
1112
- }
1113
- else if (tool) {
1114
- execution = await executeCall(tool, args, {
1115
- ctx: callCtx,
1116
- approve,
1117
- signal,
1118
- denied,
1119
- sources,
1120
- effect,
1121
- skipApprovals: agent?.approvalMode === 'skip-all',
1122
- intent,
1123
- display,
1124
- onToolActivity: (event) => {
1125
- activity = event;
1126
- }
1127
- });
1128
- }
1129
- else {
1130
- execution = blockedResult({ error: `unknown tool: ${call.name}` }, `unknown tool: ${call.name}`);
1131
- }
1132
- const settled = await settleToolCall(pipeline, execution, {
1133
- agentId,
1134
- tool,
1135
- name: call.name,
1136
- args,
1137
- effect,
1138
- resource
1139
- });
1140
- const output = settled.result;
1141
- const executionOutcome = settled.outcome;
1142
- if (!priorFailure && settledFailure(output, executionOutcome)) {
1143
- failed.set(callKey(call.name, args), settled);
1144
- }
1145
- else if (execution.executed && callSucceeded(output, executionOutcome)) {
1146
- progressed = true;
1147
- }
1148
- runOutcomes.record(output, executionOutcome);
1149
- if (searchCapped) {
1150
- cappedCalls++;
1151
- hlog('web_search capped', { searches: webSearches });
1152
- }
1153
- else if (tool?.name === WEB_SEARCH_TOOL_NAME && !rejection && !priorFailure) {
1154
- webSearches++;
1155
- }
1156
- if (call.name === SKILL_TOOL_NAME) {
1157
- recordSkillLoad(skillName, output, { mainRequested, askedFile });
1158
- }
1159
- if (!activity &&
1160
- !silent &&
1161
- (skillBlocked || Boolean(unreadyState) || Boolean(rejection) || !tool)) {
1162
- activity = {
1163
- type: 'tool-activity',
1164
- name: call.name,
1165
- args,
1166
- resource,
1167
- effect: effect ?? 'write',
1168
- decision: 'blocked'
1169
- };
1170
- }
1171
- if (activity) {
1172
- yield activity;
1173
- }
1174
- if (!silent) {
1175
- yield {
1176
- type: 'tool-result',
1177
- name: call.name,
1178
- result: output
1179
- };
1180
- }
1181
- const isCompiledOperation = tool?.compiled === true;
1182
- const isMutation = effect === 'write' &&
1183
- ((tool?.toolset === 'mcp-call' && !isMetadataMethod(args.method)) ||
1184
- (isCompiledOperation && tool?.toolset === 'http-request'));
1185
- if (isMutation && executionOutcome.kind === 'success') {
1186
- runOutcomes.confirmWrite(output);
1187
- }
1188
- const turn = toolResultTurn(call.name, call.id, output);
1189
- sources.push({
1190
- origin: call.name === SKILL_TOOL_NAME ? 'skill' : 'tool-result',
1191
- text: turn
1192
- });
1193
- history.push({ role: 'tool', content: turn });
1194
- }
1195
- runOutcomes.finishRound();
1196
- if (progressed) {
1197
- failed.clear();
1198
- blockedRepeats = 0;
1199
- }
1200
- history.push({ role: 'user', content: TOOL_CONTINUATION_NUDGE });
1201
- if (blockedRepeats >= MAX_BLOCKED_REPEATS) {
1202
- break;
1203
- }
1204
- if (cappedCalls === resolved.length) {
1205
- if (cappedRounds++ >= MAX_CAPPED_ROUNDS) {
1206
- outcome = 'search-capped';
1207
- break;
1208
- }
1209
- }
1210
- else {
1211
- cappedRounds = 0;
1212
- }
1213
- }
1214
- if (outcome === 'answered') {
1215
- yield* reportUnnamedWrites(said, landed);
1216
- const answered = metricsEvent(stats, runUsage());
1217
- runOutcome = 'done';
1218
- if (answered) {
1219
- yield answered;
1220
- }
1221
- return;
1222
- }
1223
- if (signal?.aborted) {
1224
- throw new AbortedError();
1225
- }
1226
- const recovered = yield* runRecoveredRound(sdk, {
1227
- modelId,
1228
- acquireRound,
1229
- history,
1230
- tools: undefined,
1231
- generationParams,
1232
- cacheKey: roundCacheKey,
1233
- signal,
1234
- round: roundsSent,
1235
- promptPrefix: promptPrefixHash({ system: systemContent, tools: undefined }),
1236
- capture: captureInferenceCalls
1237
- });
1238
- collectStats(stats, recovered.final.stats, recovered.final.stopReason);
1239
- const answer = recovered.replay ? stripToolCallMarkup(recovered.replay) : '';
1240
- if (answer) {
1241
- yield { type: 'content', text: answer };
1242
- }
1243
- said = `${recovered.contentText}\n${answer}`;
1244
- if (!said.trim()) {
1245
- hlog('degenerate wrap-up', { outcome });
1246
- yield {
1247
- type: 'content',
1248
- text: degenerateAnswer(outcome, history, runOutcomes.facts())
1249
- };
1250
- }
1251
- else {
1252
- yield* reportUnnamedWrites(said, landed);
1253
- }
1254
- const metrics = metricsEvent(stats, runUsage());
1255
- runOutcome = 'done';
1256
- if (metrics) {
1257
- yield metrics;
1258
- }
1259
- }
1260
- try {
1261
- yield* runTurn();
1262
- }
1263
- catch (err) {
1264
- const written = writeReport(landed);
1265
- if (written) {
1266
- yield { type: 'content', text: written };
1267
- }
1268
- if (err instanceof AbortedError) {
1269
- runOutcome = 'aborted';
1270
- yield { type: 'aborted' };
1271
- return;
1272
- }
1273
- runOutcome = 'error';
1274
- runError = err;
1275
- throw err;
1276
- }
1277
- finally {
1278
- await pipeline.onRunEnd({
1279
- agentId: agent?.id ?? null,
1280
- stats: {
1281
- totalTokens: stats.tokens,
1282
- durationMs: Math.round(stats.durationMs),
1283
- timeToFirstToken: stats.ttft
1284
- },
1285
- outcome: runOutcome,
1286
- error: runError
1287
- });
1288
- }
1289
- }
1290
- function toolDecision({ auto, skipAll, approved }) {
1291
- if (auto)
1292
- return 'auto';
1293
- if (skipAll)
1294
- return 'skip-all';
1295
- return approved ? 'allowed' : 'denied';
1296
- }
1297
- function reasoningFlag(setting) {
1298
- if (setting === 'enabled')
1299
- return true;
1300
- if (setting === 'disabled')
1301
- return false;
1302
- return undefined;
15
+ return trackCompletion((signal) => driveCompletion({ ...args, signal }, loadRuntime, {
16
+ ...options,
17
+ redact: redactor,
18
+ redactShared: redactor.redactShared,
19
+ redactOperationResult: redactor.redactOperationResult,
20
+ redactRecord: redactor.redactRecord
21
+ }), args.signal, () => { }).iterator;
1303
22
  }