@qvac/harness 0.8.6 → 0.8.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +1298 -507
  2. package/dist-lib/index.d.ts +11 -0
  3. package/dist-lib/index.js +102 -25
  4. package/dist-lib/lib/capabilities/completion.d.ts +5 -4
  5. package/dist-lib/lib/capabilities/completion.js +132 -64
  6. package/dist-lib/lib/capabilities/tool-selection.d.ts +1 -3
  7. package/dist-lib/lib/capabilities/tool-selection.js +2 -4
  8. package/dist-lib/lib/capabilities/tool-turns.d.ts +0 -1
  9. package/dist-lib/lib/capabilities/tool-turns.js +0 -6
  10. package/dist-lib/lib/connect.js +3 -0
  11. package/dist-lib/lib/contract-types.d.ts +2 -0
  12. package/dist-lib/lib/helpers/tool-call-markup.d.ts +1 -0
  13. package/dist-lib/lib/helpers/tool-call-markup.js +3 -0
  14. package/dist-lib/lib/models/context-choice.d.ts +15 -0
  15. package/dist-lib/lib/models/context-choice.js +55 -0
  16. package/dist-lib/lib/models/model-pool.d.ts +3 -1
  17. package/dist-lib/lib/models/model-pool.js +8 -11
  18. package/dist-lib/lib/models/sdk-faults.d.ts +1 -0
  19. package/dist-lib/lib/models/sdk-faults.js +6 -0
  20. package/dist-lib/lib/office/engine/pptx/picture-area.d.ts +23 -0
  21. package/dist-lib/lib/office/engine/pptx/picture-area.js +84 -0
  22. package/dist-lib/lib/office/engine/pptx/presentation.d.ts +2 -0
  23. package/dist-lib/lib/office/engine/pptx/presentation.js +55 -17
  24. package/dist-lib/lib/office/engine/pptx/types.d.ts +1 -0
  25. package/dist-lib/lib/office/engine/xlsx/workbook.d.ts +1 -0
  26. package/dist-lib/lib/office/engine/xlsx/workbook.js +34 -3
  27. package/dist-lib/lib/serve.js +6 -1
  28. package/dist-lib/lib/spawn.js +4 -10
  29. package/dist-lib/lib/tools/exec.d.ts +4 -1
  30. package/dist-lib/lib/tools/exec.js +55 -11
  31. package/dist-lib/lib/tools/file-glob.js +1 -0
  32. package/dist-lib/lib/tools/grep.js +1 -0
  33. package/dist-lib/lib/tools/office/ops.js +1 -1
  34. package/dist-lib/lib/tools/office.js +29 -6
  35. package/dist-lib/lib/tools/read-files.js +1 -0
  36. package/dist-lib/lib/tools/skill.js +6 -4
  37. package/dist-lib/lib/tools/skills/catalog.d.ts +2 -0
  38. package/dist-lib/lib/tools/skills/catalog.js +4 -0
  39. package/dist-lib/lib/tools/skills/cli-schema-check.d.ts +1 -0
  40. package/dist-lib/lib/tools/skills/cli-schema-check.js +3 -0
  41. package/dist-lib/lib/tools/skills/cli-schema-load.js +10 -0
  42. package/dist-lib/lib/tools/skills/cli-schema-types.d.ts +2 -0
  43. package/dist-lib/lib/tools/skills/cli-schema.js +6 -3
  44. package/dist-lib/lib/tools/skills/frontmatter-parser.d.ts +2 -0
  45. package/dist-lib/lib/tools/skills/frontmatter-parser.js +4 -0
  46. package/dist-lib/lib/tools/skills/manifest-parse.d.ts +1 -0
  47. package/dist-lib/lib/tools/skills/manifest-parse.js +12 -0
  48. package/dist-lib/lib/tools/skills/mention-name.d.ts +6 -0
  49. package/dist-lib/lib/tools/skills/mention-name.js +6 -0
  50. package/dist-lib/lib/tools/skills/mention.d.ts +2 -3
  51. package/dist-lib/lib/tools/skills/mention.js +5 -6
  52. package/dist-lib/lib/tools/weather-lookup.js +1 -0
  53. package/dist-lib/lib/tools/web-fetch.js +1 -0
  54. package/dist-lib/lib/tools/web-search.js +1 -0
  55. package/dist-lib/schema/harness.js +4 -2
  56. package/dist-lib/spec/hrpc/messages.js +18 -8
  57. package/dist-lib/spec/hyperschema/index.js +18 -8
  58. package/dist-lib/spec/hyperschema/schema.json +11 -1
  59. package/dist-lib/spec/hyperschema/types.d.ts +3 -1
  60. package/package.json +5 -7
  61. package/worker/harness-sidecar.mjs +2 -1
@@ -8,14 +8,14 @@ import { AbortedError, raceAbort } from '../helpers/race-abort.js';
8
8
  import { raceTimeout } from '../helpers/race-timeout.js';
9
9
  import { createRunOutcomeTracker, degenerateAnswer, unnamedWrites, writeReport } from '../helpers/run-outcome.js';
10
10
  import { splitThinking } from '../helpers/thinking.js';
11
- import { clipRawCall, normalizeToolArgs, recoverMarkupToolCalls, recoverPrintedSkillLoad, recoverSkillNamedCall, stripToolCallMarkup, stripToolResponseBlocks, toolCallMarkup } from '../helpers/tool-call-markup.js';
11
+ import { clipRawCall, hasMarkupCall, normalizeToolArgs, recoverMarkupToolCalls, recoverPrintedSkillLoad, recoverSkillNamedCall, stripToolCallMarkup, stripToolResponseBlocks, toolCallMarkup } from '../helpers/tool-call-markup.js';
12
12
  import { INTENT_PARAM, INTENT_PARAM_DESCRIPTION, takeToolIntent } from '../helpers/tool-intent.js';
13
13
  import { HookPipeline } from '../hooks/pipeline.js';
14
14
  import { fallbackToolLabel } from '../helpers/tool-label.js';
15
15
  import { hlog } from '../log.js';
16
16
  import { resolveLlmGenerationParams } from '../models/llm-presets.js';
17
17
  import { toolDialectFor } from '../models/model-catalog.js';
18
- import { isContextOverflowError, isSdkModelWedgedError } from '../models/sdk-faults.js';
18
+ import { isContextOverflowError, isGrammarOverrunError, isSdkModelWedgedError } from '../models/sdk-faults.js';
19
19
  import { annotateLastUserTime, skillsPromptSection, systemPromptParts } from '../prompt.js';
20
20
  import { isMetadataMethod } from '../tools/mcp/protocol.js';
21
21
  import { unquoteArg } from '../tools/model-args.js';
@@ -25,11 +25,11 @@ import { promptUsageParts, usageBreakdown } from './usage-breakdown.js';
25
25
  import { collectToolGrants, enabledTools, isSelected, skillIsActive, skillsAreSelected } from './tool-selection.js';
26
26
  import { guardToolCall } from '../tools/outbound-guard.js';
27
27
  import { isAvailableState } from '../tools/skills/skill-state-constants.js';
28
- import { resolveImplicitSkill, resolveSkillMention, skillPreloadMessage } from '../tools/skills/mention.js';
28
+ import { resolveImplicitSkill, resolveSkillMention } from '../tools/skills/mention.js';
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, deliversFile, FILE_DELIVERED_NUDGE, 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 } from './tool-turns.js';
33
33
  export function imageSettingsFrom(agent) {
34
34
  if (!agent)
35
35
  return undefined;
@@ -60,6 +60,9 @@ const MAX_MUTE_ROUNDS = 1;
60
60
  const NARRATED_INTENT = /\b(I['’]ll|I will|Now I|Let me(?! know)|Next,? I)\b[^.!?]*[.!?]?\s*$/i;
61
61
  const MAX_WEB_SEARCHES_PER_TURN = 3;
62
62
  const MAX_CAPPED_ROUNDS = 1;
63
+ const MAX_CALLS_PER_ROUND = 16;
64
+ const repeatedReadMessage = (name) => `The result of this exact ${name} call is already above. Answer from it now; ` +
65
+ `do not call ${name} again with the same arguments.`;
63
66
  const WEB_SEARCH_LIMIT_MESSAGE = 'Web search limit reached for this question. Answer now from the results already gathered. ' +
64
67
  'If something was not found, say so plainly and mention the most relevant sources. ' +
65
68
  'Do not call web_search again.';
@@ -101,9 +104,10 @@ export function toSdkTool(tool) {
101
104
  parameters: intentParameters(tool, tool.prompt?.parameters ?? tool.parameters)
102
105
  };
103
106
  }
104
- function sanitizeTurn(turn) {
107
+ function sanitizeTurn(turn, index, turns) {
105
108
  const stripped = stripControlTokens(turn.content);
106
- const content = turn.role === 'assistant' ? stripToolCallMarkup(stripped) : stripped;
109
+ const leaked = turn.role === 'assistant' && turns[index + 1]?.role !== 'tool';
110
+ const content = leaked ? stripToolCallMarkup(stripped) : stripped;
107
111
  return content === turn.content ? turn : { ...turn, content };
108
112
  }
109
113
  function lastUserContent(messages) {
@@ -139,14 +143,12 @@ function explicitlyDisablesWebSearch(text) {
139
143
  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
144
  /\b(?:avoid|without)\s+(?:searching|browsing|using)\s+(?:the\s+)?(?:web|internet|online)\b/i.test(text));
141
145
  }
142
- function directToolHistory(history, args, output) {
146
+ function directToolHistory(history, call, output) {
143
147
  return [
144
148
  ...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) }
149
+ { role: 'assistant', content: toolCallMarkup([call]) },
150
+ { role: 'tool', content: toolResultTurn(call.name, DIRECT_CALL_ID, output) },
151
+ { role: 'user', content: TOOL_CONTINUATION_NUDGE }
150
152
  ];
151
153
  }
152
154
  function assistantToolTurn(contentText, calls) {
@@ -155,18 +157,6 @@ function assistantToolTurn(contentText, calls) {
155
157
  const markup = toolCallMarkup(calls);
156
158
  return contentText ? `${contentText}\n\n${markup}` : markup;
157
159
  }
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
160
  function collectStats(state, stats, stopReason) {
171
161
  state.stopReason = stopReason;
172
162
  if (!stats) {
@@ -218,12 +208,19 @@ function deniedResult() {
218
208
  }
219
209
  function callKey(name, args) {
220
210
  try {
221
- return `${name}:${JSON.stringify(args)}`;
211
+ return `${name}:${JSON.stringify(args, sortedKeys)}`;
222
212
  }
223
213
  catch {
224
214
  return name;
225
215
  }
226
216
  }
217
+ function sortedKeys(_key, value) {
218
+ if (!isRecord(value) || Array.isArray(value))
219
+ return value;
220
+ return Object.fromEntries(Object.keys(value)
221
+ .sort()
222
+ .map((key) => [key, value[key]]));
223
+ }
227
224
  const RETRYABLE_STATUS = new Set([408, 429]);
228
225
  const RETRYABLE_CODE = new Set(['timeout', 'transport', 'busy']);
229
226
  function callSucceeded(result, outcome) {
@@ -362,10 +359,11 @@ async function executeCall(tool, args, { ctx, approve, signal, denied, sources =
362
359
  return executedCall(output, outcome);
363
360
  }
364
361
  function runToolContext(scope) {
365
- const { agent, attach, fetchAttachment, ragSearch, progress, askUser, signal, runBudget } = scope;
362
+ const { agent, session, attach, fetchAttachment, ragSearch, progress, askUser, signal, runBudget } = scope;
366
363
  const image = imageSettingsFrom(agent);
367
364
  return (turnImages = [], ctxSize) => ({
368
365
  agentId: agent?.id ?? null,
366
+ session,
369
367
  ctxSize,
370
368
  image,
371
369
  turnImages: turnImages.length ? turnImages : undefined,
@@ -466,6 +464,20 @@ async function* contentDeltas(events) {
466
464
  }
467
465
  }
468
466
  }
467
+ async function* endAtGrammarOverrun(tokens, state) {
468
+ try {
469
+ for await (const token of tokens) {
470
+ state.rawText += token;
471
+ yield token;
472
+ }
473
+ }
474
+ catch (err) {
475
+ if (!isGrammarOverrunError(err)) {
476
+ throw err;
477
+ }
478
+ state.overrun = err;
479
+ }
480
+ }
469
481
  async function* runRoundTokens(result, state, { sdk, signal, startInThink = false }) {
470
482
  const cancelThisRound = () => {
471
483
  if (result.requestId) {
@@ -473,7 +485,8 @@ async function* runRoundTokens(result, state, { sdk, signal, startInThink = fals
473
485
  }
474
486
  };
475
487
  signal?.addEventListener('abort', cancelThisRound, { once: true });
476
- const tokens = stopAtControlToken(contentDeltas(result.events), (token) => {
488
+ const deltas = endAtGrammarOverrun(contentDeltas(result.events), state);
489
+ const tokens = stopAtControlToken(deltas, (token) => {
477
490
  state.stopToken = token;
478
491
  });
479
492
  let tokenEvents = 0;
@@ -508,6 +521,9 @@ async function* runRoundTokens(result, state, { sdk, signal, startInThink = fals
508
521
  if (contentTail) {
509
522
  yield { type: 'content', text: contentTail };
510
523
  }
524
+ if (state.overrun) {
525
+ throw state.overrun;
526
+ }
511
527
  hlog('stream ended', { events: tokenEvents, stopToken: state.stopToken });
512
528
  if (state.stopToken) {
513
529
  cancelThisRound();
@@ -533,9 +549,12 @@ function roundTokens(stats) {
533
549
  };
534
550
  return Object.values(tokens).some((value) => value !== undefined) ? tokens : undefined;
535
551
  }
552
+ function acceptedFinal(contentText, fullText = contentText) {
553
+ return { contentText, toolCalls: [], raw: { fullText } };
554
+ }
536
555
  const TRUNCATED_FINAL_MS = 2000;
537
556
  function truncatedFinal(final, contentText) {
538
- const accepted = { contentText, toolCalls: [], raw: { fullText: contentText } };
557
+ const accepted = acceptedFinal(contentText);
539
558
  const guarded = final.catch((err) => {
540
559
  if (isSdkModelWedgedError(err))
541
560
  throw err;
@@ -560,7 +579,13 @@ async function* runRecoveredRoundWith(sdk, args, modelId) {
560
579
  throw new AbortedError();
561
580
  let contentText = '';
562
581
  let emitted = false;
563
- const state = { stopToken: null, unproven: null, rawContentText: '' };
582
+ const state = {
583
+ stopToken: null,
584
+ unproven: null,
585
+ rawContentText: '',
586
+ rawText: '',
587
+ overrun: null
588
+ };
564
589
  const cacheable = candidate.history === args.history;
565
590
  const params = {
566
591
  modelId,
@@ -633,6 +658,14 @@ async function* runRecoveredRoundWith(sdk, args, modelId) {
633
658
  };
634
659
  }
635
660
  catch (err) {
661
+ const recovered = isGrammarOverrunError(err) && hasMarkupCall(state.rawText);
662
+ let outcome = 'failed';
663
+ if (recovered) {
664
+ outcome = 'completed';
665
+ }
666
+ else if (isContextOverflowError(err)) {
667
+ outcome = 'overflowed';
668
+ }
636
669
  if (args.capture) {
637
670
  yield {
638
671
  type: 'inference-sent',
@@ -640,10 +673,21 @@ async function* runRecoveredRoundWith(sdk, args, modelId) {
640
673
  droppedTurns: candidate.droppedTurns,
641
674
  messages: undefined,
642
675
  tokens: undefined,
643
- outcome: isContextOverflowError(err) ? 'overflowed' : 'failed',
676
+ outcome,
644
677
  reason: errorMessage(err)
645
678
  };
646
679
  }
680
+ if (recovered) {
681
+ hlog('grammar overrun recovered', { round: args.round });
682
+ return {
683
+ contentText,
684
+ final: acceptedFinal(state.rawContentText, state.rawText),
685
+ history: candidate.history,
686
+ cacheable,
687
+ unproven: state.unproven,
688
+ replay: null
689
+ };
690
+ }
647
691
  if (!isContextOverflowError(err)) {
648
692
  throw err;
649
693
  }
@@ -664,7 +708,7 @@ function* reportUnnamedWrites(said, landed) {
664
708
  yield { type: 'content', text: unreported };
665
709
  }
666
710
  }
667
- 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 }) {
711
+ export async function* completion(sdk, { modelId, acquireRound, model, agent, ctxSize, cacheKey, session, thinkingDefault, generationParams: generationParamsOverride, responseFormat, messages: storedMessages, typedText, tools: allTools = [], skillCatalog = [], approve, attach, fetchAttachment, ragSearch, progress, askUser, signal, toolConfigs, fsRoots = [], fsWriteRoots = [], captureInferenceCalls = false }) {
668
712
  const pipeline = new HookPipeline();
669
713
  const agentId = agent?.id ?? null;
670
714
  const disabledToolsets = agent?.sandboxMode
@@ -676,12 +720,12 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
676
720
  ...fsRoots.filter((root) => root.kind === 'hyper' && root.writable && !fsWriteRoots.includes(root))
677
721
  ];
678
722
  const messages = storedMessages.map(sanitizeTurn);
723
+ const userText = typedText === undefined ? lastUserContent(messages) : stripControlTokens(typedText);
679
724
  const stats = { tokens: 0, timedTokens: 0, durationMs: 0, ctxSize, backends: [] };
680
725
  let runOutcome = 'abandoned';
681
726
  let runError;
682
727
  const runOutcomes = createRunOutcomeTracker();
683
728
  const landed = runOutcomes.confirmedWrites;
684
- let preloadCacheKey;
685
729
  async function* runTurn() {
686
730
  if (signal?.aborted)
687
731
  throw new AbortedError();
@@ -692,6 +736,7 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
692
736
  };
693
737
  const toolContext = runToolContext({
694
738
  agent,
739
+ session: session ?? sessionOf(cacheKey),
695
740
  attach,
696
741
  fetchAttachment,
697
742
  ragSearch,
@@ -704,16 +749,14 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
704
749
  const toolsSelected = isSelected(agent?.toolsScope, agent?.enabledTools);
705
750
  const skillsSelected = skillsAreSelected(agent?.skillsScope, Boolean(agent));
706
751
  const inContextSkills = skillCatalog.filter(skillInContext);
707
- const mention = resolveSkillMention(lastUserContent(messages), inContextSkills);
708
752
  const loadedSkills = loadedSkillsFrom(messages);
709
753
  const loadedSkillFiles = loadedSkillFilesFrom(messages);
710
754
  const activation = {
711
755
  skillsSelected,
712
- enabledSkills: agent?.enabledSkills ?? [],
713
- mention: mention?.name ?? null,
714
- loadedSkills
756
+ enabledSkills: agent?.enabledSkills ?? []
715
757
  };
716
758
  const activeSkills = inContextSkills.filter((skill) => skillIsActive(skill.name, activation));
759
+ const mention = resolveSkillMention(userText, activeSkills);
717
760
  const { grantedNames, grantedOwners, grantedOps } = collectToolGrants(activeSkills);
718
761
  function selectEnabled() {
719
762
  if (responseFormat)
@@ -762,16 +805,15 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
762
805
  skillIcon = skillEmoji(skillName) ?? skillIcon;
763
806
  }
764
807
  const toolDialect = model ? toolDialectFor(model) : undefined;
765
- let turns = messages;
766
808
  let preloadedSkill = null;
767
- const preload = mention ?? resolveImplicitSkill(lastUserContent(messages), activeSkills);
809
+ let preloadedResult = null;
810
+ const preload = mention ?? resolveImplicitSkill(userText, activeSkills);
768
811
  if (preload) {
769
812
  const loader = tools.find((tool) => tool.name === SKILL_TOOL_NAME);
770
813
  const loaded = loader ? await loader.execute({ name: preload.name }, toolContext()) : null;
771
814
  if (loaded && typeof loaded === 'object' && 'content' in loaded) {
772
815
  preloadedSkill = String(loaded.content);
773
- const inlined = preloadLastUser(messages, preload.name, preloadedSkill);
774
- turns = withoutSkillLoadTurns(inlined, preload.name);
816
+ preloadedResult = loaded;
775
817
  if (!loadedSkills.has(preload.name)) {
776
818
  const preloadArgs = { name: preload.name };
777
819
  const preloadEffect = 'read';
@@ -799,6 +841,7 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
799
841
  effect: preloadEffect,
800
842
  resource: preloadResource
801
843
  });
844
+ preloadedResult = settled.result;
802
845
  yield {
803
846
  type: 'tool-result',
804
847
  name: SKILL_TOOL_NAME,
@@ -841,19 +884,19 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
841
884
  });
842
885
  }
843
886
  logSelection();
844
- let history = annotateLastUserTime(systemContent ? [{ role: 'system', content: systemContent }, ...turns] : turns);
845
- preloadCacheKey = childCacheKey(cacheKey, preloadedSkill, messages.length);
846
- let roundCacheKey = preloadedSkill === null ? cacheKey : preloadCacheKey;
887
+ let history = annotateLastUserTime(systemContent ? [{ role: 'system', content: systemContent }, ...messages] : messages);
888
+ if (preload && preloadedSkill !== null) {
889
+ history = directToolHistory(history, { name: SKILL_TOOL_NAME, arguments: { name: preload.name } }, preloadedResult);
890
+ }
891
+ let roundCacheKey = cacheKey;
847
892
  const runUsage = () => usageBreakdown(promptParts, stats.cacheTokens, model);
848
893
  const denied = new Set();
849
894
  const failed = new Map();
895
+ const answered = new Set();
850
896
  let blockedRepeats = 0;
851
897
  let webSearches = 0;
852
- const sources = trustedSourcesFrom(messages);
853
- if (preloadedSkill) {
854
- sources.push({ origin: 'skill', text: preloadedSkill });
855
- }
856
- const query = explicitWebSearchQuery(lastUserContent(messages), enabled, {
898
+ const sources = trustedSourcesFrom(history);
899
+ const query = explicitWebSearchQuery(userText, enabled, {
857
900
  skillPreloaded: preloadedSkill !== null
858
901
  });
859
902
  const webSearch = enabled.find((tool) => tool.name === WEB_SEARCH_TOOL_NAME);
@@ -908,6 +951,9 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
908
951
  if (fold.ok) {
909
952
  webSearches++;
910
953
  }
954
+ if (webSearch.stableRead && execution.executed && callSucceeded(output, executionOutcome)) {
955
+ answered.add(callKey(webSearch.name, args));
956
+ }
911
957
  if (!activity && !fold.ok) {
912
958
  activity = {
913
959
  type: 'tool-activity',
@@ -926,7 +972,7 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
926
972
  name: webSearch.name,
927
973
  result: output
928
974
  };
929
- history = directToolHistory(history, args, output);
975
+ history = directToolHistory(history, { name: webSearch.name, arguments: args }, output);
930
976
  }
931
977
  let outcome = 'exhausted';
932
978
  let said = '';
@@ -1030,6 +1076,11 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
1030
1076
  break;
1031
1077
  }
1032
1078
  hlog('round produced no answer', { round, mute });
1079
+ history = [
1080
+ ...history,
1081
+ { role: 'assistant', content: spoken },
1082
+ { role: 'user', content: TOOL_CONTINUATION_NUDGE }
1083
+ ];
1033
1084
  if (mute++ < MAX_MUTE_ROUNDS) {
1034
1085
  continue;
1035
1086
  }
@@ -1038,6 +1089,10 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
1038
1089
  }
1039
1090
  mute = 0;
1040
1091
  narrated = false;
1092
+ if (toolCalls.length > MAX_CALLS_PER_ROUND) {
1093
+ hlog('round over the call cap; the rest are dropped', { round, calls: toolCalls.length });
1094
+ toolCalls = toolCalls.slice(0, MAX_CALLS_PER_ROUND);
1095
+ }
1041
1096
  const resolved = await resolveToolCalls(toolCalls, {
1042
1097
  enabled,
1043
1098
  agentId: agent?.id ?? null,
@@ -1063,13 +1118,12 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
1063
1118
  for (const { call, tool, args, rawCall, rejection, intent } of resolved) {
1064
1119
  if (signal?.aborted)
1065
1120
  throw new AbortedError();
1066
- const priorFailure = failed.get(callKey(call.name, args));
1121
+ const key = callKey(call.name, args);
1122
+ const priorFailure = failed.get(key);
1067
1123
  const searchCapped = tool?.name === WEB_SEARCH_TOOL_NAME && webSearches >= MAX_WEB_SEARCHES_PER_TURN;
1068
1124
  const callCtx = {
1069
1125
  ...toolContext(turnImages, ctxSize),
1070
- skills: [...loadedSkills].filter((name) => !skillsSelected ||
1071
- (agent?.enabledSkills ?? []).includes(name) ||
1072
- name === mention?.name),
1126
+ skills: [...loadedSkills].filter((name) => skillIsActive(name, activation)),
1073
1127
  skillsLoadedThisTurn: [...loadedThisTurn],
1074
1128
  settings: toolConfigs?.[call.name],
1075
1129
  toolOps: grantedOps,
@@ -1081,6 +1135,8 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
1081
1135
  };
1082
1136
  const skillCall = call.name === SKILL_TOOL_NAME;
1083
1137
  const effect = tool?.effect(args, callCtx) ?? (skillCall ? 'read' : undefined);
1138
+ const stableRead = tool?.stableRead === true && effect === 'read';
1139
+ const priorSuccess = !priorFailure && stableRead && answered.has(key);
1084
1140
  const resource = tool?.resource?.(args) ?? null;
1085
1141
  const skillName = unquoteArg(args?.name ?? args?.skill_name ?? args?.skill);
1086
1142
  const askedFile = skillFileArg(args);
@@ -1089,7 +1145,7 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
1089
1145
  (mainRequested
1090
1146
  ? loadedThisTurn.has(skillName)
1091
1147
  : loadedFilesThisTurn.has(fileKey(skillName, unquoteArg(askedFile))));
1092
- const silent = searchCapped || skillReloaded || priorFailure !== undefined;
1148
+ const silent = searchCapped || skillReloaded || priorFailure !== undefined || priorSuccess;
1093
1149
  const display = silent
1094
1150
  ? undefined
1095
1151
  : labelOf(call.name, args, skillCall ? skillEmoji(skillName) : skillIcon);
@@ -1111,6 +1167,14 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
1111
1167
  });
1112
1168
  execution = terminalCall(priorFailure.result, priorFailure.outcome, { executed: false });
1113
1169
  }
1170
+ else if (priorSuccess) {
1171
+ blockedRepeats++;
1172
+ hlog('repeat of a read that already answered; not run again', {
1173
+ tool: call.name,
1174
+ blockedRepeats
1175
+ });
1176
+ execution = terminalCall({ content: repeatedReadMessage(call.name) }, { kind: 'success' }, { executed: false });
1177
+ }
1114
1178
  else if (skillReloaded) {
1115
1179
  const loadedWhat = mainRequested ? skillName : `${skillName} ${unquoteArg(askedFile)}`;
1116
1180
  execution = terminalCall({
@@ -1162,18 +1226,27 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
1162
1226
  const output = settled.result;
1163
1227
  const executionOutcome = settled.outcome;
1164
1228
  if (!priorFailure && settledFailure(output, executionOutcome)) {
1165
- failed.set(callKey(call.name, args), settled);
1229
+ failed.set(key, settled);
1166
1230
  }
1167
1231
  else if (execution.executed && callSucceeded(output, executionOutcome)) {
1168
1232
  progressed = true;
1169
1233
  answer ??= answerOf(output);
1170
1234
  }
1235
+ if (execution.executed && !stableRead && !skillCall) {
1236
+ answered.clear();
1237
+ }
1238
+ else if (stableRead && execution.executed && callSucceeded(output, executionOutcome)) {
1239
+ answered.add(key);
1240
+ }
1171
1241
  runOutcomes.record(output, executionOutcome);
1172
1242
  if (searchCapped) {
1173
1243
  cappedCalls++;
1174
1244
  hlog('web_search capped', { searches: webSearches });
1175
1245
  }
1176
- else if (tool?.name === WEB_SEARCH_TOOL_NAME && !rejection && !priorFailure) {
1246
+ else if (tool?.name === WEB_SEARCH_TOOL_NAME &&
1247
+ !rejection &&
1248
+ !priorFailure &&
1249
+ !priorSuccess) {
1177
1250
  webSearches++;
1178
1251
  }
1179
1252
  if (call.name === SKILL_TOOL_NAME) {
@@ -1332,16 +1405,11 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
1332
1405
  outcome: runOutcome,
1333
1406
  error: runError
1334
1407
  });
1335
- if (preloadCacheKey) {
1336
- await sdk.deleteCache?.({ kvCacheKey: preloadCacheKey }).catch(noop);
1337
- }
1338
1408
  }
1339
1409
  }
1340
- function childCacheKey(cacheKey, preloadedSkill, turnCount) {
1341
- if (preloadedSkill === null || cacheKey === undefined) {
1342
- return undefined;
1343
- }
1344
- return `${cacheKey}/p${turnCount}`;
1410
+ function sessionOf(cacheKey) {
1411
+ const base = cacheKey?.split('/')[0];
1412
+ return base ? base : undefined;
1345
1413
  }
1346
1414
  function toolDecision({ auto, skipAll, approved }) {
1347
1415
  if (auto)
@@ -18,11 +18,9 @@ export declare function collectToolGrants<T extends {
18
18
  export interface SkillActivation {
19
19
  skillsSelected: boolean;
20
20
  enabledSkills: ReadonlyArray<string>;
21
- mention: string | null;
22
- loadedSkills: ReadonlySet<string>;
23
21
  }
24
22
  export declare function skillsAreSelected(scope: 'all' | 'selected' | null | undefined, hasAgent: boolean): boolean;
25
- export declare function skillIsActive(name: string, { skillsSelected, enabledSkills, mention, loadedSkills }: SkillActivation): boolean;
23
+ export declare function skillIsActive(name: string, { skillsSelected, enabledSkills }: SkillActivation): boolean;
26
24
  export interface SelectableTool {
27
25
  name: string;
28
26
  skillGated?: boolean | undefined;
@@ -25,10 +25,8 @@ export function collectToolGrants(skills) {
25
25
  export function skillsAreSelected(scope, hasAgent) {
26
26
  return hasAgent && scope !== 'all';
27
27
  }
28
- export function skillIsActive(name, { skillsSelected, enabledSkills, mention, loadedSkills }) {
29
- if (!skillsSelected)
30
- return true;
31
- return enabledSkills.includes(name) || name === mention || loadedSkills.has(name);
28
+ export function skillIsActive(name, { skillsSelected, enabledSkills }) {
29
+ return !skillsSelected || enabledSkills.includes(name);
32
30
  }
33
31
  export function isSelected(scope, list) {
34
32
  return (scope ?? (list?.length ? 'selected' : 'all')) === 'selected';
@@ -9,4 +9,3 @@ export declare function skillLoadedTurn(name: string): string;
9
9
  export declare function attachmentTurn(name: string, result: unknown): string | null;
10
10
  export declare function loadedSkillsFrom(messages: readonly Message[]): Set<string>;
11
11
  export declare function loadedSkillFilesFrom(messages: readonly Message[]): Map<string, Set<string>>;
12
- export declare function withoutSkillLoadTurns(messages: readonly Message[], name: string): Message[];
@@ -71,12 +71,6 @@ export function loadedSkillFilesFrom(messages) {
71
71
  }
72
72
  return loaded;
73
73
  }
74
- export function withoutSkillLoadTurns(messages, name) {
75
- return messages.filter((turn) => {
76
- const payload = skillLoadPayload(turn);
77
- return !payload || payload.name !== name || payload.file !== undefined;
78
- });
79
- }
80
74
  function* skillLoadPayloads(messages) {
81
75
  for (const turn of messages) {
82
76
  const payload = skillLoadPayload(turn);
@@ -102,6 +102,7 @@ export class RemoteHarness extends EventEmitter {
102
102
  model: context.model,
103
103
  agent: knobs(context.agent),
104
104
  cacheKey: context.cacheKey,
105
+ session: context.session,
105
106
  thinkingDefault: context.thinkingDefault,
106
107
  generationParams: context.generationParams,
107
108
  responseFormat: context.responseFormat,
@@ -111,6 +112,7 @@ export class RemoteHarness extends EventEmitter {
111
112
  content,
112
113
  attachments: attachments?.map(({ path: file }) => ({ path: file }))
113
114
  })),
115
+ payload: { typedText: context.typedText },
114
116
  toolConfigs: context.toolConfigs,
115
117
  grantedDirs: context.grantedDirs,
116
118
  grantedDrives: context.grantedDrives,
@@ -509,6 +511,7 @@ export class RemoteHarness extends EventEmitter {
509
511
  }));
510
512
  return {
511
513
  name: skill.name,
514
+ displayName: skill.displayName || undefined,
512
515
  description: skill.description,
513
516
  tools: skill.tools ?? [],
514
517
  credentials: skill.credentials ?? [],
@@ -46,7 +46,9 @@ export interface CompletionContext {
46
46
  model: string;
47
47
  agent: AgentKnobs;
48
48
  messages: ChatMessage[];
49
+ typedText?: string;
49
50
  cacheKey?: string;
51
+ session?: string;
50
52
  thinkingDefault?: string;
51
53
  toolConfigs?: Record<string, Record<string, unknown>>;
52
54
  generationParams?: LlmGenerationParams;
@@ -9,6 +9,7 @@ export declare function clipRawCall(raw: string): string;
9
9
  export interface KnownTool {
10
10
  readonly name: string;
11
11
  }
12
+ export declare function hasMarkupCall(text: string): boolean;
12
13
  export declare function recoverMarkupToolCalls(text: string, knownTools: ReadonlyArray<KnownTool>): RecoveredToolCall[];
13
14
  export declare function stripToolCallMarkup(text: string): string;
14
15
  export declare function stripToolResponseBlocks(text: string): string;
@@ -46,6 +46,9 @@ export const MAX_RAW_CALL_CHARS = 4096;
46
46
  export function clipRawCall(raw) {
47
47
  return raw.length > MAX_RAW_CALL_CHARS ? raw.slice(0, MAX_RAW_CALL_CHARS) : raw;
48
48
  }
49
+ export function hasMarkupCall(text) {
50
+ return !markupBlocks(text, FUNCTION_OPEN, FUNCTION_CLOSE).next().done;
51
+ }
49
52
  export function recoverMarkupToolCalls(text, knownTools) {
50
53
  const known = new Set(knownTools.map((tool) => tool.name));
51
54
  const calls = [];
@@ -0,0 +1,15 @@
1
+ import type { CapabilityProfile } from './model-catalog-constants.ts';
2
+ export interface ContextAsk {
3
+ trained(): Promise<number | undefined>;
4
+ fits(ctxSize: number): Promise<boolean>;
5
+ }
6
+ export declare class ContextChoice {
7
+ private readonly _profile;
8
+ private readonly _pending;
9
+ private readonly _chosen;
10
+ constructor(profile: () => Promise<CapabilityProfile | undefined>);
11
+ static ceiling(profile: CapabilityProfile | undefined): number | undefined;
12
+ chosen(key: string): number | undefined;
13
+ choose(key: string, tier: number, ask: ContextAsk): Promise<number>;
14
+ private _climb;
15
+ }
@@ -0,0 +1,55 @@
1
+ const RUNGS = [262_144, 131_072, 65_536];
2
+ const CEILINGS = {
3
+ mid: 65_536,
4
+ high: 131_072,
5
+ xhigh: 262_144
6
+ };
7
+ export class ContextChoice {
8
+ _profile;
9
+ _pending = new Map();
10
+ _chosen = new Map();
11
+ constructor(profile) {
12
+ this._profile = profile;
13
+ }
14
+ static ceiling(profile) {
15
+ return profile && CEILINGS[profile];
16
+ }
17
+ chosen(key) {
18
+ return this._chosen.get(key);
19
+ }
20
+ choose(key, tier, ask) {
21
+ const pending = this._pending.get(key);
22
+ if (pending) {
23
+ return pending;
24
+ }
25
+ const choice = this._climb(tier, ask).then((ctxSize) => {
26
+ if (ctxSize === undefined) {
27
+ this._pending.delete(key);
28
+ return tier;
29
+ }
30
+ this._chosen.set(key, ctxSize);
31
+ return ctxSize;
32
+ });
33
+ this._pending.set(key, choice);
34
+ return choice;
35
+ }
36
+ async _climb(tier, ask) {
37
+ const ceiling = ContextChoice.ceiling(await this._profile());
38
+ if (!ceiling || ceiling <= tier) {
39
+ return tier;
40
+ }
41
+ const trained = await ask.trained();
42
+ if (!trained) {
43
+ return undefined;
44
+ }
45
+ for (const rung of RUNGS) {
46
+ if (rung > ceiling || rung > trained || rung <= tier) {
47
+ continue;
48
+ }
49
+ if (await ask.fits(rung)) {
50
+ return rung;
51
+ }
52
+ }
53
+ return tier;
54
+ }
55
+ }