@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.
- package/CHANGELOG.md +1298 -507
- package/dist-lib/index.d.ts +11 -0
- package/dist-lib/index.js +102 -25
- package/dist-lib/lib/capabilities/completion.d.ts +5 -4
- package/dist-lib/lib/capabilities/completion.js +132 -64
- package/dist-lib/lib/capabilities/tool-selection.d.ts +1 -3
- package/dist-lib/lib/capabilities/tool-selection.js +2 -4
- package/dist-lib/lib/capabilities/tool-turns.d.ts +0 -1
- package/dist-lib/lib/capabilities/tool-turns.js +0 -6
- package/dist-lib/lib/connect.js +3 -0
- package/dist-lib/lib/contract-types.d.ts +2 -0
- package/dist-lib/lib/helpers/tool-call-markup.d.ts +1 -0
- package/dist-lib/lib/helpers/tool-call-markup.js +3 -0
- package/dist-lib/lib/models/context-choice.d.ts +15 -0
- package/dist-lib/lib/models/context-choice.js +55 -0
- package/dist-lib/lib/models/model-pool.d.ts +3 -1
- package/dist-lib/lib/models/model-pool.js +8 -11
- package/dist-lib/lib/models/sdk-faults.d.ts +1 -0
- package/dist-lib/lib/models/sdk-faults.js +6 -0
- package/dist-lib/lib/office/engine/pptx/picture-area.d.ts +23 -0
- package/dist-lib/lib/office/engine/pptx/picture-area.js +84 -0
- package/dist-lib/lib/office/engine/pptx/presentation.d.ts +2 -0
- package/dist-lib/lib/office/engine/pptx/presentation.js +55 -17
- package/dist-lib/lib/office/engine/pptx/types.d.ts +1 -0
- package/dist-lib/lib/office/engine/xlsx/workbook.d.ts +1 -0
- package/dist-lib/lib/office/engine/xlsx/workbook.js +34 -3
- package/dist-lib/lib/serve.js +6 -1
- package/dist-lib/lib/spawn.js +4 -10
- package/dist-lib/lib/tools/exec.d.ts +4 -1
- package/dist-lib/lib/tools/exec.js +55 -11
- package/dist-lib/lib/tools/file-glob.js +1 -0
- package/dist-lib/lib/tools/grep.js +1 -0
- package/dist-lib/lib/tools/office/ops.js +1 -1
- package/dist-lib/lib/tools/office.js +29 -6
- package/dist-lib/lib/tools/read-files.js +1 -0
- package/dist-lib/lib/tools/skill.js +6 -4
- package/dist-lib/lib/tools/skills/catalog.d.ts +2 -0
- package/dist-lib/lib/tools/skills/catalog.js +4 -0
- package/dist-lib/lib/tools/skills/cli-schema-check.d.ts +1 -0
- package/dist-lib/lib/tools/skills/cli-schema-check.js +3 -0
- package/dist-lib/lib/tools/skills/cli-schema-load.js +10 -0
- package/dist-lib/lib/tools/skills/cli-schema-types.d.ts +2 -0
- package/dist-lib/lib/tools/skills/cli-schema.js +6 -3
- package/dist-lib/lib/tools/skills/frontmatter-parser.d.ts +2 -0
- package/dist-lib/lib/tools/skills/frontmatter-parser.js +4 -0
- package/dist-lib/lib/tools/skills/manifest-parse.d.ts +1 -0
- package/dist-lib/lib/tools/skills/manifest-parse.js +12 -0
- package/dist-lib/lib/tools/skills/mention-name.d.ts +6 -0
- package/dist-lib/lib/tools/skills/mention-name.js +6 -0
- package/dist-lib/lib/tools/skills/mention.d.ts +2 -3
- package/dist-lib/lib/tools/skills/mention.js +5 -6
- package/dist-lib/lib/tools/weather-lookup.js +1 -0
- package/dist-lib/lib/tools/web-fetch.js +1 -0
- package/dist-lib/lib/tools/web-search.js +1 -0
- package/dist-lib/schema/harness.js +4 -2
- package/dist-lib/spec/hrpc/messages.js +18 -8
- package/dist-lib/spec/hyperschema/index.js +18 -8
- package/dist-lib/spec/hyperschema/schema.json +11 -1
- package/dist-lib/spec/hyperschema/types.d.ts +3 -1
- package/package.json +5 -7
- 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
|
|
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
|
|
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
|
|
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,
|
|
146
|
+
function directToolHistory(history, call, output) {
|
|
143
147
|
return [
|
|
144
148
|
...history,
|
|
145
|
-
{
|
|
146
|
-
|
|
147
|
-
|
|
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
|
|
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 =
|
|
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 = {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 }, ...
|
|
845
|
-
|
|
846
|
-
|
|
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(
|
|
853
|
-
|
|
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
|
|
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) =>
|
|
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(
|
|
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 &&
|
|
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
|
|
1341
|
-
|
|
1342
|
-
|
|
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
|
|
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
|
|
29
|
-
|
|
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);
|
package/dist-lib/lib/connect.js
CHANGED
|
@@ -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
|
+
}
|