@qvac/harness 0.8.6 → 0.8.7

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 (51) hide show
  1. package/CHANGELOG.md +1259 -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 +4 -4
  5. package/dist-lib/lib/capabilities/completion.js +86 -57
  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 +2 -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 +5 -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/office/ops.js +1 -1
  32. package/dist-lib/lib/tools/office.js +29 -6
  33. package/dist-lib/lib/tools/skills/catalog.d.ts +1 -0
  34. package/dist-lib/lib/tools/skills/catalog.js +2 -0
  35. package/dist-lib/lib/tools/skills/cli-schema-check.d.ts +1 -0
  36. package/dist-lib/lib/tools/skills/cli-schema-check.js +3 -0
  37. package/dist-lib/lib/tools/skills/cli-schema-load.js +10 -0
  38. package/dist-lib/lib/tools/skills/cli-schema-types.d.ts +2 -0
  39. package/dist-lib/lib/tools/skills/cli-schema.js +6 -3
  40. package/dist-lib/lib/tools/skills/frontmatter-parser.d.ts +1 -0
  41. package/dist-lib/lib/tools/skills/manifest-parse.d.ts +1 -0
  42. package/dist-lib/lib/tools/skills/manifest-parse.js +12 -0
  43. package/dist-lib/lib/tools/skills/mention.d.ts +0 -1
  44. package/dist-lib/lib/tools/skills/mention.js +0 -3
  45. package/dist-lib/schema/harness.js +2 -1
  46. package/dist-lib/spec/hrpc/messages.js +10 -5
  47. package/dist-lib/spec/hyperschema/index.js +10 -5
  48. package/dist-lib/spec/hyperschema/schema.json +6 -1
  49. package/dist-lib/spec/hyperschema/types.d.ts +2 -1
  50. package/package.json +5 -7
  51. package/worker/harness-sidecar.mjs +2 -1
@@ -99,7 +99,9 @@ export interface CompletionContext {
99
99
  model: string;
100
100
  agent?: Agent;
101
101
  messages: Message[];
102
+ typedText?: string;
102
103
  cacheKey?: string;
104
+ session?: string;
103
105
  thinkingDefault?: string;
104
106
  toolConfigs?: Record<string, Record<string, unknown>>;
105
107
  generationParams?: LlmGenerationParams;
@@ -189,6 +191,7 @@ export declare class Harness extends ReadyResource {
189
191
  private _sizeLoads;
190
192
  private _capBytes;
191
193
  private readonly _reserved;
194
+ private readonly _contexts;
192
195
  private readonly _admission;
193
196
  private readonly _byHand;
194
197
  constructor({ tools, sdkLoader, skillsDir, modelsDir, projectionModel, imageModel, imageClamps, musicModel, musicClamps, llmDevice, llmGpuLayers, contextSize, kvCacheK, kvCacheV, embedDevice, embedGpuLayers, embedBatchSize, transcriptionDevice, completionModelCap, credentialStore, platform, gpuStats }?: HarnessOptions);
@@ -290,14 +293,22 @@ export declare class Harness extends ReadyResource {
290
293
  private _curatedFit;
291
294
  private _admitByHand;
292
295
  private _replacedBy;
296
+ private _replacedEntries;
297
+ private _alone;
293
298
  private _sizeByHand;
294
299
  private _loadByHand;
295
300
  private _admit;
296
301
  private _makeRoomForGenerator;
297
302
  private _yieldGenerators;
298
303
  private _beforeLoad;
304
+ private _reserveByHand;
299
305
  private _reserve;
300
306
  private _sizeLoad;
307
+ private _chooseContext;
308
+ private _capRoom;
309
+ private _contextProfile;
310
+ private _trainedContext;
311
+ private _ctxSizeOf;
301
312
  private _makeRoom;
302
313
  private _capVictims;
303
314
  private _victimRank;
package/dist-lib/index.js CHANGED
@@ -28,6 +28,8 @@ import { deleteModelFiles, summarizeDelete } from './lib/models/model-delete.js'
28
28
  import { createEmbedPool, createLoadTracker, createModelPool, createVoicePool, awaitModelCapacity, evictIdle, evictIdleWhere, evictIdleWithin, evictModel, evictModelWithin, isResident, leaseResidentModel, leaseCount, loadedModels, loadingModels, settledEntries, settledModels, modelKey, modelOf, releaseModel, resolveLoadOptions, progressState } from './lib/models/model-pool.js';
29
29
  import { memoryCapBytes, nextVictim, VictimRank } from './lib/models/memory-cap.js';
30
30
  import { defaultContextSize } from './lib/models/context.js';
31
+ import { ContextChoice } from './lib/models/context-choice.js';
32
+ import { parseGgufHeader, readGgufHeaderBytes } from './lib/models/gguf-header.js';
31
33
  import { resolveMainGpu } from './lib/models/gpu-selection.js';
32
34
  import { createIdleReaper } from './lib/models/idle-unload.js';
33
35
  import { openclCacheDir } from './lib/models/opencl-cache.js';
@@ -163,8 +165,9 @@ export class Harness extends ReadyResource {
163
165
  _sizeLoads = false;
164
166
  _capBytes = null;
165
167
  _reserved = new Map();
168
+ _contexts = new ContextChoice(() => this._contextProfile());
166
169
  _admission = new ScopeLock();
167
- _byHand = new Set();
170
+ _byHand = new Map();
168
171
  constructor({ tools = [], sdkLoader, skillsDir, modelsDir, projectionModel, imageModel, imageClamps, musicModel, musicClamps, llmDevice, llmGpuLayers, contextSize, kvCacheK, kvCacheV, embedDevice, embedGpuLayers, embedBatchSize, transcriptionDevice, completionModelCap, credentialStore, platform, gpuStats } = {}) {
169
172
  super();
170
173
  this._sdk = sdkLoader ?? createSdkLoader();
@@ -682,7 +685,7 @@ export class Harness extends ReadyResource {
682
685
  const key = modelKey(model, loadOpts);
683
686
  const pool = this._models;
684
687
  if (!isResident(pool, key)) {
685
- await this._makeRoomForCompletion(model, { ctxSize: loadOpts.ctxSize, signal });
688
+ await this._makeRoomForCompletion(model, { ctxSize: this._ctxSizeOf(key, loadOpts), signal });
686
689
  }
687
690
  try {
688
691
  const { modelId } = await raceAbort(pool.open(key, model, loadOpts).load(), signal);
@@ -721,6 +724,7 @@ export class Harness extends ReadyResource {
721
724
  deviceConfig: this._deviceConfig(),
722
725
  loads: this._loads,
723
726
  beforeLoad: (load) => this._beforeLoad(load),
727
+ chooseContext: (load, tier) => this._chooseContext(load, tier),
724
728
  hold: this._hold
725
729
  });
726
730
  }
@@ -1346,7 +1350,9 @@ export class Harness extends ReadyResource {
1346
1350
  }
1347
1351
  async _admitByHand(model, target) {
1348
1352
  if (this._capBytes === null) {
1349
- return target.pool !== this._models || this._fits(model, target.ctxSize);
1353
+ return (target.pool !== this._models ||
1354
+ this._alone(model, target) ||
1355
+ this._fits(model, target.ctxSize));
1350
1356
  }
1351
1357
  return this._admit(async () => {
1352
1358
  if (isResident(target.pool, target.key) || this._byHand.has(target.key)) {
@@ -1354,18 +1360,14 @@ export class Harness extends ReadyResource {
1354
1360
  }
1355
1361
  const sized = await this._sizeByHand(model, target);
1356
1362
  const capBytes = this._capBytes;
1357
- const settled = this._pools().flatMap((pool) => settledEntries(pool));
1358
- const replacedBy = this._replacedBy(model);
1359
- const replaced = target.pool === this._models
1360
- ? settledEntries(this._models).filter(({ key, refs }) => refs === 0 && replacedBy(key))
1361
- : [];
1363
+ const replaced = this._replacedEntries(model, target);
1362
1364
  const used = this._usedBytes() - replaced.reduce((sum, { bytes }) => sum + (bytes ?? 0), 0);
1363
- const alone = this._reserved.size === 0 && settled.length === replaced.length;
1365
+ const alone = this._alone(model, target);
1364
1366
  const fits = capBytes === null || alone || sized.bytes === undefined || used + sized.bytes <= capBytes;
1365
- if (sized.tooLarge || !fits) {
1367
+ if ((sized.tooLarge && !alone) || !fits) {
1366
1368
  return false;
1367
1369
  }
1368
- this._byHand.add(target.key);
1370
+ this._byHand.set(target.key, sized.ctxSize);
1369
1371
  if (sized.bytes !== undefined) {
1370
1372
  this._reserved.set(target.key, sized.bytes);
1371
1373
  }
@@ -1375,19 +1377,35 @@ export class Harness extends ReadyResource {
1375
1377
  _replacedBy(model) {
1376
1378
  return (key) => modelOf(key) === model && !this._replying.includes(key);
1377
1379
  }
1378
- _sizeByHand(model, target) {
1380
+ _replacedEntries(model, target) {
1381
+ const replacedBy = this._replacedBy(model);
1382
+ return target.pool === this._models
1383
+ ? settledEntries(this._models).filter(({ key, refs }) => refs === 0 && replacedBy(key))
1384
+ : [];
1385
+ }
1386
+ _alone(model, target) {
1387
+ const settled = this._pools().flatMap((pool) => settledEntries(pool));
1388
+ const loading = this._pools().some((pool) => loadingModels(pool).length > 0);
1389
+ return (!loading &&
1390
+ this._reserved.size === 0 &&
1391
+ settled.length === this._replacedEntries(model, target).length);
1392
+ }
1393
+ async _sizeByHand(model, target) {
1379
1394
  const addon = modelAddonOf(model);
1380
1395
  const modelSrc = curatedModelAsset(model, addon) ?? customModelSrc(model, addon) ?? model;
1381
- const contextTokens = target.ctxSize || defaultContextSize(modelSrc, customContextLength(model));
1382
- return this._sizeLoad({
1396
+ const ctxSize = target.pool === this._models
1397
+ ? target.ctxSize || defaultContextSize(modelSrc, customContextLength(model))
1398
+ : undefined;
1399
+ const sized = await this._sizeLoad({
1383
1400
  key: target.key,
1384
1401
  model,
1385
1402
  modelSrc,
1386
1403
  modelType: target.modelType,
1387
- modelConfig: target.pool === this._models
1388
- ? { ctx_size: contextTokens, projectionModelSrc: this._projectionFor(model) }
1389
- : undefined
1404
+ modelConfig: ctxSize === undefined
1405
+ ? undefined
1406
+ : { ctx_size: ctxSize, projectionModelSrc: this._projectionFor(model) }
1390
1407
  });
1408
+ return { ...sized, ctxSize };
1391
1409
  }
1392
1410
  async _loadByHand(target, signal) {
1393
1411
  const loading = target.open().load();
@@ -1429,11 +1447,22 @@ export class Harness extends ReadyResource {
1429
1447
  async _beforeLoad(load) {
1430
1448
  if (!this._sizeLoads)
1431
1449
  return undefined;
1432
- const bytes = this._byHand.delete(load.key)
1433
- ? this._reserved.get(load.key)
1450
+ const bytes = this._byHand.has(load.key)
1451
+ ? await this._admit(() => this._reserveByHand(load))
1434
1452
  : await this._admit(() => this._reserve(load));
1435
1453
  return { bytes, release: () => this._reserved.delete(load.key) };
1436
1454
  }
1455
+ async _reserveByHand(load) {
1456
+ const admittedAt = this._byHand.get(load.key);
1457
+ this._byHand.delete(load.key);
1458
+ if (load.modelConfig?.['ctx_size'] !== admittedAt) {
1459
+ const { bytes } = await this._sizeLoad(load);
1460
+ if (bytes !== undefined) {
1461
+ this._reserved.set(load.key, bytes);
1462
+ }
1463
+ }
1464
+ return this._reserved.get(load.key);
1465
+ }
1437
1466
  async _reserve(load) {
1438
1467
  const sized = await this._sizeLoad(load);
1439
1468
  if (this._capBytes !== null) {
@@ -1461,17 +1490,60 @@ export class Harness extends ReadyResource {
1461
1490
  const bytes = fit?.estimate?.upperBoundBytes ?? fit?.floorBytes ?? downloaded;
1462
1491
  return {
1463
1492
  bytes: bytes === undefined ? undefined : bytes + projection,
1464
- tooLarge: fit?.verdict === 'likely-too-large'
1493
+ tooLarge: fit?.verdict === 'likely-too-large',
1494
+ confirmed: fit?.verdict === 'likely-fits' &&
1495
+ (fit.evidence === 'calibration' || fit.evidence === 'native-fit')
1465
1496
  };
1466
1497
  }
1467
1498
  catch (err) {
1468
1499
  hlog('fit estimate failed', { model, err: errorMessage(err) });
1469
1500
  return {
1470
1501
  bytes: downloaded === undefined ? undefined : downloaded + projection,
1471
- tooLarge: false
1502
+ tooLarge: false,
1503
+ confirmed: false
1472
1504
  };
1473
1505
  }
1474
1506
  }
1507
+ async _chooseContext(load, tier) {
1508
+ if (isTranslationModel(load.model)) {
1509
+ return tier;
1510
+ }
1511
+ const ctxSize = await this._contexts.choose(load.key, tier, {
1512
+ trained: () => this._trainedContext(load),
1513
+ fits: async (rung) => {
1514
+ const sized = await this._sizeLoad({
1515
+ ...load,
1516
+ modelConfig: { ...load.modelConfig, ctx_size: rung }
1517
+ });
1518
+ const room = this._capRoom(load.key);
1519
+ return sized.confirmed && (room === null || (sized.bytes ?? Infinity) <= room);
1520
+ }
1521
+ });
1522
+ hlog('context chosen', { model: load.model, tier, ctxSize });
1523
+ return ctxSize;
1524
+ }
1525
+ _capRoom(key) {
1526
+ const cap = this._capBytes;
1527
+ return cap === null ? null : cap - (this._usedBytes() - (this._reserved.get(key) ?? 0));
1528
+ }
1529
+ async _contextProfile() {
1530
+ const { capabilityProfile, memoryBudget } = await this.capability();
1531
+ return memoryBudget ? capabilityProfile : undefined;
1532
+ }
1533
+ async _trainedContext({ model, modelSrc }) {
1534
+ const custom = customContextLength(model);
1535
+ if (custom || typeof modelSrc === 'string') {
1536
+ return custom;
1537
+ }
1538
+ const sdk = await this._sdk.load();
1539
+ const info = await sdk.getModelInfo({ name: modelSrc.name }).catch(() => null);
1540
+ const file = info?.cacheFiles[0]?.path;
1541
+ const bytes = file ? await readGgufHeaderBytes(file) : null;
1542
+ return (bytes && parseGgufHeader(bytes)?.contextLength) || undefined;
1543
+ }
1544
+ _ctxSizeOf(key, loadOpts) {
1545
+ return loadOpts.ctxSize || this._contexts.chosen(key);
1546
+ }
1475
1547
  async _makeRoom(need, room) {
1476
1548
  const capBytes = this._capBytes;
1477
1549
  if (capBytes === null) {
@@ -1563,7 +1635,7 @@ export class Harness extends ReadyResource {
1563
1635
  pool: this._models,
1564
1636
  key,
1565
1637
  modelType: 'llamacpp-completion',
1566
- ctxSize: loadOpts.ctxSize,
1638
+ ctxSize: this._ctxSizeOf(key, loadOpts),
1567
1639
  open: () => this._models.open(key, model, loadOpts)
1568
1640
  };
1569
1641
  }
@@ -1625,7 +1697,7 @@ export class Harness extends ReadyResource {
1625
1697
  completion(context) {
1626
1698
  return this._busyStream(() => this._completion(context));
1627
1699
  }
1628
- async *_completion({ model, agent, messages, cacheKey, thinkingDefault, toolConfigs, generationParams, responseFormat, captureInferenceCalls, grantedDirs, grantedDrives, driveOp, grantedWriteDirs, approve, attach, fetchAttachment, ragSearch, progress, askUser, signal }) {
1700
+ async *_completion({ model, agent, messages, typedText, cacheKey, session, thinkingDefault, toolConfigs, generationParams, responseFormat, captureInferenceCalls, grantedDirs, grantedDrives, driveOp, grantedWriteDirs, approve, attach, fetchAttachment, ragSearch, progress, askUser, signal }) {
1629
1701
  if (!this.opened)
1630
1702
  await this.ready();
1631
1703
  if (this.suspended)
@@ -1637,12 +1709,15 @@ export class Harness extends ReadyResource {
1637
1709
  const resident = isResident(pool, key);
1638
1710
  hlog('completion start', { model, resident, projection: Boolean(projectionModel) });
1639
1711
  if (!resident) {
1640
- await this._makeRoomForCompletion(model, { ctxSize: loadOpts.ctxSize, signal });
1712
+ await this._makeRoomForCompletion(model, { ctxSize: this._ctxSizeOf(key, loadOpts), signal });
1641
1713
  await this._yieldGenerators();
1642
1714
  }
1643
1715
  const acquireRound = async () => {
1644
1716
  if (!isResident(pool, key)) {
1645
- await this._makeRoomForCompletion(model, { ctxSize: loadOpts.ctxSize, signal });
1717
+ await this._makeRoomForCompletion(model, {
1718
+ ctxSize: this._ctxSizeOf(key, loadOpts),
1719
+ signal
1720
+ });
1646
1721
  await this._yieldGenerators();
1647
1722
  }
1648
1723
  try {
@@ -1690,11 +1765,13 @@ export class Harness extends ReadyResource {
1690
1765
  agent,
1691
1766
  ctxSize,
1692
1767
  cacheKey,
1768
+ session,
1693
1769
  thinkingDefault,
1694
1770
  generationParams,
1695
1771
  responseFormat,
1696
1772
  captureInferenceCalls,
1697
1773
  messages: turns,
1774
+ typedText,
1698
1775
  tools: this._tools,
1699
1776
  skillCatalog: await this._completionSkills(agent?.id ?? null, model),
1700
1777
  toolConfigs,
@@ -125,6 +125,7 @@ export interface ToolExecuteContext {
125
125
  skills?: readonly string[] | undefined;
126
126
  skillsLoadedThisTurn?: readonly string[] | undefined;
127
127
  ctxSize?: number | undefined;
128
+ session?: string | undefined;
128
129
  image?: ImageSettings | undefined;
129
130
  settings?: Record<string, unknown> | undefined;
130
131
  turnImages?: {
@@ -296,9 +297,6 @@ export interface CompletionSdk {
296
297
  cancel?(params: {
297
298
  requestId: string;
298
299
  }): Promise<void>;
299
- deleteCache?(params: {
300
- kvCacheKey: string;
301
- }): Promise<unknown>;
302
300
  }
303
301
  export interface RoundModelLease {
304
302
  modelId: string;
@@ -311,10 +309,12 @@ interface CompletionArgs {
311
309
  agent?: Agent;
312
310
  ctxSize?: number;
313
311
  cacheKey?: string;
312
+ session?: string;
314
313
  thinkingDefault?: string;
315
314
  generationParams?: LlmGenerationParams;
316
315
  responseFormat?: ResponseFormat;
317
316
  messages: Message[];
317
+ typedText?: string;
318
318
  tools?: ToolDefinition[];
319
319
  skillCatalog?: readonly CompletionSkillEntry[];
320
320
  approve?: ApproveFn;
@@ -335,5 +335,5 @@ export declare function toSdkTool(tool: ToolDefinition): SdkTool;
335
335
  export type CompletionSkillEntry = SkillCatalogEntry & {
336
336
  readonly status?: SkillState;
337
337
  };
338
- export declare function completion(sdk: CompletionSdk, { 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 }: CompletionArgs): AsyncGenerator<CompletionEvent>;
338
+ export declare function completion(sdk: CompletionSdk, { 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 }: CompletionArgs): AsyncGenerator<CompletionEvent>;
339
339
  export {};
@@ -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;
@@ -139,14 +139,11 @@ function explicitlyDisablesWebSearch(text) {
139
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
140
  /\b(?:avoid|without)\s+(?:searching|browsing|using)\s+(?:the\s+)?(?:web|internet|online)\b/i.test(text));
141
141
  }
142
- function directToolHistory(history, args, output) {
142
+ function directToolHistory(history, call, output) {
143
143
  return [
144
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) }
145
+ { role: 'assistant', content: toolCallMarkup([call]) },
146
+ { role: 'tool', content: toolResultTurn(call.name, DIRECT_CALL_ID, output) }
150
147
  ];
151
148
  }
152
149
  function assistantToolTurn(contentText, calls) {
@@ -155,18 +152,6 @@ function assistantToolTurn(contentText, calls) {
155
152
  const markup = toolCallMarkup(calls);
156
153
  return contentText ? `${contentText}\n\n${markup}` : markup;
157
154
  }
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
155
  function collectStats(state, stats, stopReason) {
171
156
  state.stopReason = stopReason;
172
157
  if (!stats) {
@@ -362,10 +347,11 @@ async function executeCall(tool, args, { ctx, approve, signal, denied, sources =
362
347
  return executedCall(output, outcome);
363
348
  }
364
349
  function runToolContext(scope) {
365
- const { agent, attach, fetchAttachment, ragSearch, progress, askUser, signal, runBudget } = scope;
350
+ const { agent, session, attach, fetchAttachment, ragSearch, progress, askUser, signal, runBudget } = scope;
366
351
  const image = imageSettingsFrom(agent);
367
352
  return (turnImages = [], ctxSize) => ({
368
353
  agentId: agent?.id ?? null,
354
+ session,
369
355
  ctxSize,
370
356
  image,
371
357
  turnImages: turnImages.length ? turnImages : undefined,
@@ -466,6 +452,20 @@ async function* contentDeltas(events) {
466
452
  }
467
453
  }
468
454
  }
455
+ async function* endAtGrammarOverrun(tokens, state) {
456
+ try {
457
+ for await (const token of tokens) {
458
+ state.rawText += token;
459
+ yield token;
460
+ }
461
+ }
462
+ catch (err) {
463
+ if (!isGrammarOverrunError(err)) {
464
+ throw err;
465
+ }
466
+ state.overrun = err;
467
+ }
468
+ }
469
469
  async function* runRoundTokens(result, state, { sdk, signal, startInThink = false }) {
470
470
  const cancelThisRound = () => {
471
471
  if (result.requestId) {
@@ -473,7 +473,8 @@ async function* runRoundTokens(result, state, { sdk, signal, startInThink = fals
473
473
  }
474
474
  };
475
475
  signal?.addEventListener('abort', cancelThisRound, { once: true });
476
- const tokens = stopAtControlToken(contentDeltas(result.events), (token) => {
476
+ const deltas = endAtGrammarOverrun(contentDeltas(result.events), state);
477
+ const tokens = stopAtControlToken(deltas, (token) => {
477
478
  state.stopToken = token;
478
479
  });
479
480
  let tokenEvents = 0;
@@ -508,6 +509,9 @@ async function* runRoundTokens(result, state, { sdk, signal, startInThink = fals
508
509
  if (contentTail) {
509
510
  yield { type: 'content', text: contentTail };
510
511
  }
512
+ if (state.overrun) {
513
+ throw state.overrun;
514
+ }
511
515
  hlog('stream ended', { events: tokenEvents, stopToken: state.stopToken });
512
516
  if (state.stopToken) {
513
517
  cancelThisRound();
@@ -533,9 +537,12 @@ function roundTokens(stats) {
533
537
  };
534
538
  return Object.values(tokens).some((value) => value !== undefined) ? tokens : undefined;
535
539
  }
540
+ function acceptedFinal(contentText, fullText = contentText) {
541
+ return { contentText, toolCalls: [], raw: { fullText } };
542
+ }
536
543
  const TRUNCATED_FINAL_MS = 2000;
537
544
  function truncatedFinal(final, contentText) {
538
- const accepted = { contentText, toolCalls: [], raw: { fullText: contentText } };
545
+ const accepted = acceptedFinal(contentText);
539
546
  const guarded = final.catch((err) => {
540
547
  if (isSdkModelWedgedError(err))
541
548
  throw err;
@@ -560,7 +567,13 @@ async function* runRecoveredRoundWith(sdk, args, modelId) {
560
567
  throw new AbortedError();
561
568
  let contentText = '';
562
569
  let emitted = false;
563
- const state = { stopToken: null, unproven: null, rawContentText: '' };
570
+ const state = {
571
+ stopToken: null,
572
+ unproven: null,
573
+ rawContentText: '',
574
+ rawText: '',
575
+ overrun: null
576
+ };
564
577
  const cacheable = candidate.history === args.history;
565
578
  const params = {
566
579
  modelId,
@@ -633,6 +646,14 @@ async function* runRecoveredRoundWith(sdk, args, modelId) {
633
646
  };
634
647
  }
635
648
  catch (err) {
649
+ const recovered = isGrammarOverrunError(err) && hasMarkupCall(state.rawText);
650
+ let outcome = 'failed';
651
+ if (recovered) {
652
+ outcome = 'completed';
653
+ }
654
+ else if (isContextOverflowError(err)) {
655
+ outcome = 'overflowed';
656
+ }
636
657
  if (args.capture) {
637
658
  yield {
638
659
  type: 'inference-sent',
@@ -640,10 +661,21 @@ async function* runRecoveredRoundWith(sdk, args, modelId) {
640
661
  droppedTurns: candidate.droppedTurns,
641
662
  messages: undefined,
642
663
  tokens: undefined,
643
- outcome: isContextOverflowError(err) ? 'overflowed' : 'failed',
664
+ outcome,
644
665
  reason: errorMessage(err)
645
666
  };
646
667
  }
668
+ if (recovered) {
669
+ hlog('grammar overrun recovered', { round: args.round });
670
+ return {
671
+ contentText,
672
+ final: acceptedFinal(state.rawContentText, state.rawText),
673
+ history: candidate.history,
674
+ cacheable,
675
+ unproven: state.unproven,
676
+ replay: null
677
+ };
678
+ }
647
679
  if (!isContextOverflowError(err)) {
648
680
  throw err;
649
681
  }
@@ -664,7 +696,7 @@ function* reportUnnamedWrites(said, landed) {
664
696
  yield { type: 'content', text: unreported };
665
697
  }
666
698
  }
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 }) {
699
+ 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
700
  const pipeline = new HookPipeline();
669
701
  const agentId = agent?.id ?? null;
670
702
  const disabledToolsets = agent?.sandboxMode
@@ -676,12 +708,12 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
676
708
  ...fsRoots.filter((root) => root.kind === 'hyper' && root.writable && !fsWriteRoots.includes(root))
677
709
  ];
678
710
  const messages = storedMessages.map(sanitizeTurn);
711
+ const userText = typedText === undefined ? lastUserContent(messages) : stripControlTokens(typedText);
679
712
  const stats = { tokens: 0, timedTokens: 0, durationMs: 0, ctxSize, backends: [] };
680
713
  let runOutcome = 'abandoned';
681
714
  let runError;
682
715
  const runOutcomes = createRunOutcomeTracker();
683
716
  const landed = runOutcomes.confirmedWrites;
684
- let preloadCacheKey;
685
717
  async function* runTurn() {
686
718
  if (signal?.aborted)
687
719
  throw new AbortedError();
@@ -692,6 +724,7 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
692
724
  };
693
725
  const toolContext = runToolContext({
694
726
  agent,
727
+ session: session ?? sessionOf(cacheKey),
695
728
  attach,
696
729
  fetchAttachment,
697
730
  ragSearch,
@@ -704,16 +737,14 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
704
737
  const toolsSelected = isSelected(agent?.toolsScope, agent?.enabledTools);
705
738
  const skillsSelected = skillsAreSelected(agent?.skillsScope, Boolean(agent));
706
739
  const inContextSkills = skillCatalog.filter(skillInContext);
707
- const mention = resolveSkillMention(lastUserContent(messages), inContextSkills);
708
740
  const loadedSkills = loadedSkillsFrom(messages);
709
741
  const loadedSkillFiles = loadedSkillFilesFrom(messages);
710
742
  const activation = {
711
743
  skillsSelected,
712
- enabledSkills: agent?.enabledSkills ?? [],
713
- mention: mention?.name ?? null,
714
- loadedSkills
744
+ enabledSkills: agent?.enabledSkills ?? []
715
745
  };
716
746
  const activeSkills = inContextSkills.filter((skill) => skillIsActive(skill.name, activation));
747
+ const mention = resolveSkillMention(userText, activeSkills);
717
748
  const { grantedNames, grantedOwners, grantedOps } = collectToolGrants(activeSkills);
718
749
  function selectEnabled() {
719
750
  if (responseFormat)
@@ -762,16 +793,15 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
762
793
  skillIcon = skillEmoji(skillName) ?? skillIcon;
763
794
  }
764
795
  const toolDialect = model ? toolDialectFor(model) : undefined;
765
- let turns = messages;
766
796
  let preloadedSkill = null;
767
- const preload = mention ?? resolveImplicitSkill(lastUserContent(messages), activeSkills);
797
+ let preloadedResult = null;
798
+ const preload = mention ?? resolveImplicitSkill(userText, activeSkills);
768
799
  if (preload) {
769
800
  const loader = tools.find((tool) => tool.name === SKILL_TOOL_NAME);
770
801
  const loaded = loader ? await loader.execute({ name: preload.name }, toolContext()) : null;
771
802
  if (loaded && typeof loaded === 'object' && 'content' in loaded) {
772
803
  preloadedSkill = String(loaded.content);
773
- const inlined = preloadLastUser(messages, preload.name, preloadedSkill);
774
- turns = withoutSkillLoadTurns(inlined, preload.name);
804
+ preloadedResult = loaded;
775
805
  if (!loadedSkills.has(preload.name)) {
776
806
  const preloadArgs = { name: preload.name };
777
807
  const preloadEffect = 'read';
@@ -799,6 +829,7 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
799
829
  effect: preloadEffect,
800
830
  resource: preloadResource
801
831
  });
832
+ preloadedResult = settled.result;
802
833
  yield {
803
834
  type: 'tool-result',
804
835
  name: SKILL_TOOL_NAME,
@@ -841,19 +872,19 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
841
872
  });
842
873
  }
843
874
  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;
875
+ let history = annotateLastUserTime(systemContent ? [{ role: 'system', content: systemContent }, ...messages] : messages);
876
+ if (preload && preloadedSkill !== null) {
877
+ history = directToolHistory(history, { name: SKILL_TOOL_NAME, arguments: { name: preload.name } }, preloadedResult);
878
+ history.push({ role: 'user', content: TOOL_CONTINUATION_NUDGE });
879
+ }
880
+ let roundCacheKey = cacheKey;
847
881
  const runUsage = () => usageBreakdown(promptParts, stats.cacheTokens, model);
848
882
  const denied = new Set();
849
883
  const failed = new Map();
850
884
  let blockedRepeats = 0;
851
885
  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, {
886
+ const sources = trustedSourcesFrom(history);
887
+ const query = explicitWebSearchQuery(userText, enabled, {
857
888
  skillPreloaded: preloadedSkill !== null
858
889
  });
859
890
  const webSearch = enabled.find((tool) => tool.name === WEB_SEARCH_TOOL_NAME);
@@ -926,7 +957,7 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
926
957
  name: webSearch.name,
927
958
  result: output
928
959
  };
929
- history = directToolHistory(history, args, output);
960
+ history = directToolHistory(history, { name: webSearch.name, arguments: args }, output);
930
961
  }
931
962
  let outcome = 'exhausted';
932
963
  let said = '';
@@ -1030,6 +1061,11 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
1030
1061
  break;
1031
1062
  }
1032
1063
  hlog('round produced no answer', { round, mute });
1064
+ history = [
1065
+ ...history,
1066
+ { role: 'assistant', content: spoken },
1067
+ { role: 'user', content: TOOL_CONTINUATION_NUDGE }
1068
+ ];
1033
1069
  if (mute++ < MAX_MUTE_ROUNDS) {
1034
1070
  continue;
1035
1071
  }
@@ -1067,9 +1103,7 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
1067
1103
  const searchCapped = tool?.name === WEB_SEARCH_TOOL_NAME && webSearches >= MAX_WEB_SEARCHES_PER_TURN;
1068
1104
  const callCtx = {
1069
1105
  ...toolContext(turnImages, ctxSize),
1070
- skills: [...loadedSkills].filter((name) => !skillsSelected ||
1071
- (agent?.enabledSkills ?? []).includes(name) ||
1072
- name === mention?.name),
1106
+ skills: [...loadedSkills].filter((name) => skillIsActive(name, activation)),
1073
1107
  skillsLoadedThisTurn: [...loadedThisTurn],
1074
1108
  settings: toolConfigs?.[call.name],
1075
1109
  toolOps: grantedOps,
@@ -1332,16 +1366,11 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
1332
1366
  outcome: runOutcome,
1333
1367
  error: runError
1334
1368
  });
1335
- if (preloadCacheKey) {
1336
- await sdk.deleteCache?.({ kvCacheKey: preloadCacheKey }).catch(noop);
1337
- }
1338
1369
  }
1339
1370
  }
1340
- function childCacheKey(cacheKey, preloadedSkill, turnCount) {
1341
- if (preloadedSkill === null || cacheKey === undefined) {
1342
- return undefined;
1343
- }
1344
- return `${cacheKey}/p${turnCount}`;
1371
+ function sessionOf(cacheKey) {
1372
+ const base = cacheKey?.split('/')[0];
1373
+ return base ? base : undefined;
1345
1374
  }
1346
1375
  function toolDecision({ auto, skipAll, approved }) {
1347
1376
  if (auto)