@qvac/harness 0.2.8 → 0.2.10
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/README.md +15 -7
- package/dist/harness.bundle +13 -12
- package/dist-lib/index.d.ts +2 -1
- package/dist-lib/index.js +2 -1
- package/dist-lib/lib/capabilities/completion.d.ts +36 -2
- package/dist-lib/lib/capabilities/completion.js +150 -27
- package/dist-lib/lib/connect.js +3 -1
- package/dist-lib/lib/contract-types.d.ts +2 -0
- package/dist-lib/lib/docx/index.js +4 -27
- package/dist-lib/lib/hooks/built-in/index.d.ts +1 -0
- package/dist-lib/lib/hooks/built-in/index.js +1 -0
- package/dist-lib/lib/hooks/built-in/sandbox.d.ts +5 -0
- package/dist-lib/lib/hooks/built-in/sandbox.js +43 -0
- package/dist-lib/lib/hooks/built-in/skill-catalog.js +7 -2
- package/dist-lib/lib/hooks/registry.js +2 -1
- package/dist-lib/lib/hooks/types.d.ts +1 -0
- package/dist-lib/lib/office/index.d.ts +6 -0
- package/dist-lib/lib/office/index.js +38 -0
- package/dist-lib/lib/office/pptx.d.ts +1 -0
- package/dist-lib/lib/office/pptx.js +26 -0
- package/dist-lib/lib/office/python.d.ts +7 -0
- package/dist-lib/lib/office/python.js +32 -0
- package/dist-lib/lib/office/xlsx.d.ts +1 -0
- package/dist-lib/lib/office/xlsx.js +26 -0
- package/dist-lib/lib/serve.js +1 -0
- package/dist-lib/lib/tools/edit-image.js +1 -1
- package/dist-lib/lib/tools/generate-image.js +1 -1
- package/dist-lib/lib/tools/generate-music.js +1 -1
- package/dist-lib/lib/tools/http-request.js +3 -3
- package/dist-lib/lib/tools/mcp-call.js +3 -3
- package/dist-lib/lib/tools/skill.js +4 -2
- package/dist-lib/lib/tools/skills/auth-resolver.d.ts +1 -0
- package/dist-lib/lib/tools/skills/auth-resolver.js +59 -29
- package/dist-lib/lib/tools/toolsets.d.ts +1 -0
- package/dist-lib/lib/tools/toolsets.js +1 -0
- package/dist-lib/lib/tools/weather-lookup.js +9 -3
- package/dist-lib/lib/tools/web-fetch.js +6 -3
- package/dist-lib/lib/tools/web-search.js +5 -2
- package/dist-lib/lib/wire.js +28 -0
- package/dist-lib/schema/harness.js +7 -3
- package/dist-lib/spec/hrpc/messages.js +28 -10
- package/dist-lib/spec/hyperschema/index.js +28 -10
- package/dist-lib/spec/hyperschema/schema.json +19 -1
- package/dist-lib/spec/hyperschema/types.d.ts +4 -2
- package/package.json +10 -6
- package/.claude/skills/asana/SKILL.md +0 -86
- package/.claude/skills/gh-stack/SKILL.md +0 -181
- package/.claude/skills/gh-stack/references/commands.md +0 -177
- package/.claude/skills/gh-stack/references/stack-design.md +0 -97
- package/.claude/skills/gh-stack/references/troubleshooting.md +0 -159
- package/.claude/skills/js-house-style/SKILL.md +0 -671
- package/.claude/skills/kvcache/SKILL.md +0 -171
- package/.claude/skills/pr-create/SKILL.md +0 -180
- package/.claude/skills/pr-review/SKILL.md +0 -414
- package/CLAUDE.md +0 -39
- package/docs/decisions/2026-05-29-kvcache-tool-turns.md +0 -75
- package/docs/decisions/2026-06-02-kvcache-tool-turns-reenabled.md +0 -127
- package/docs/decisions/2026-06-23-android-cpu-llm-defaults.md +0 -126
- package/docs/decisions/2026-07-29-assistant-owned-storage-layout.md +0 -88
- package/docs/decisions/2026-08-04-sidecar-orphan-reaping.md +0 -79
- package/docs/decisions/2026-08-11-pyodide-worker-thread.md +0 -68
- package/docs/decisions/2026-08-17-harness-hook-pipeline.md +0 -84
- package/docs/decisions/2026-08-17-harness-outbound-egress-guard.md +0 -176
- package/docs/decisions/2026-08-20-chat-prompt-prefix-is-stable.md +0 -95
- package/docs/decisions/2026-08-20-kvcache-static-placement.md +0 -109
- package/docs/decisions/2026-08-21-vendored-python-wheels.md +0 -85
- package/docs/decisions/2026-08-26-exec-is-not-gated-by-skills.md +0 -64
- package/docs/decisions/2026-08-27-writable-skills-overlay.md +0 -56
- package/docs/decisions/2026-09-01-skill-operation-manifests.md +0 -53
- package/docs/decisions/2026-09-01-usage-breakdown-is-measured.md +0 -78
- package/docs/decisions/2026-09-03-usage-breakdown-reported-by-the-harness.md +0 -136
- package/docs/decisions/2026-09-10-skill-wording-is-measured-offline.md +0 -63
- package/docs/decisions/2026-09-16-a-lookup-reports-the-place-it-resolved.md +0 -70
- package/docs/decisions/INDEX.md +0 -40
- package/docs/decisions/TEMPLATE.md +0 -32
- package/docs/kvcache-reference.md +0 -655
package/dist-lib/index.d.ts
CHANGED
|
@@ -93,6 +93,7 @@ export interface CompletionContext {
|
|
|
93
93
|
toolConfigs?: Record<string, Record<string, unknown>>;
|
|
94
94
|
generationParams?: LlmGenerationParams;
|
|
95
95
|
responseFormat?: ResponseFormat;
|
|
96
|
+
captureInferenceCalls?: boolean;
|
|
96
97
|
grantedDirs?: string[];
|
|
97
98
|
grantedDrives?: GrantedDrive[];
|
|
98
99
|
driveOp?: DriveOpFn;
|
|
@@ -239,7 +240,7 @@ export declare class Harness extends ReadyResource {
|
|
|
239
240
|
agent?: Agent;
|
|
240
241
|
signal?: BareAbortSignal;
|
|
241
242
|
}): Promise<void>;
|
|
242
|
-
completion({ model, agent, messages, cacheKey, thinkingDefault, toolConfigs, generationParams, responseFormat, grantedDirs, grantedDrives, driveOp, grantedWriteDirs, approve, attach, fetchAttachment, ragSearch, progress, askUser, signal }: CompletionContext): AsyncGenerator<CompletionEvent>;
|
|
243
|
+
completion({ model, agent, messages, cacheKey, thinkingDefault, toolConfigs, generationParams, responseFormat, captureInferenceCalls, grantedDirs, grantedDrives, driveOp, grantedWriteDirs, approve, attach, fetchAttachment, ragSearch, progress, askUser, signal }: CompletionContext): AsyncGenerator<CompletionEvent>;
|
|
243
244
|
private _makeRoomForCompletion;
|
|
244
245
|
private _evictWedgedModel;
|
|
245
246
|
private _evictAfterLeases;
|
package/dist-lib/index.js
CHANGED
|
@@ -943,7 +943,7 @@ export class Harness extends ReadyResource {
|
|
|
943
943
|
releaseModel(pool, key);
|
|
944
944
|
}
|
|
945
945
|
}
|
|
946
|
-
async *completion({ model, agent, messages, cacheKey, thinkingDefault, toolConfigs, generationParams, responseFormat, grantedDirs, grantedDrives, driveOp, grantedWriteDirs, approve, attach, fetchAttachment, ragSearch, progress, askUser, signal }) {
|
|
946
|
+
async *completion({ model, agent, messages, cacheKey, thinkingDefault, toolConfigs, generationParams, responseFormat, captureInferenceCalls, grantedDirs, grantedDrives, driveOp, grantedWriteDirs, approve, attach, fetchAttachment, ragSearch, progress, askUser, signal }) {
|
|
947
947
|
await this.ready();
|
|
948
948
|
if (this.suspended)
|
|
949
949
|
throw new Error('harness is suspended');
|
|
@@ -1016,6 +1016,7 @@ export class Harness extends ReadyResource {
|
|
|
1016
1016
|
thinkingDefault,
|
|
1017
1017
|
generationParams,
|
|
1018
1018
|
responseFormat,
|
|
1019
|
+
captureInferenceCalls,
|
|
1019
1020
|
messages: turns,
|
|
1020
1021
|
tools: this._tools,
|
|
1021
1022
|
skillCatalog: await this._completionSkills(agent?.id ?? null, model),
|
|
@@ -36,6 +36,7 @@ export interface Agent {
|
|
|
36
36
|
imageHeight?: number | null;
|
|
37
37
|
approvalMode?: 'ask' | 'skip-all' | null;
|
|
38
38
|
disabledToolsets?: string[] | null;
|
|
39
|
+
sandboxMode?: boolean | null;
|
|
39
40
|
}
|
|
40
41
|
export interface ImageSettings {
|
|
41
42
|
modelChoice?: string;
|
|
@@ -221,8 +222,40 @@ export type MetricsEvent = {
|
|
|
221
222
|
ctxSize?: number | undefined;
|
|
222
223
|
backends?: RunBackendUse[] | undefined;
|
|
223
224
|
usage?: UsageBreakdown | undefined;
|
|
225
|
+
stopReason?: string | undefined;
|
|
224
226
|
};
|
|
225
|
-
export
|
|
227
|
+
export interface InferenceCall {
|
|
228
|
+
round: number;
|
|
229
|
+
droppedTurns: number;
|
|
230
|
+
modelId: string;
|
|
231
|
+
messages: Message[];
|
|
232
|
+
tools: SdkTool[] | undefined;
|
|
233
|
+
generationParams: LlmGenerationParams | undefined;
|
|
234
|
+
responseFormat: ResponseFormat | undefined;
|
|
235
|
+
toolDialect: ToolDialect | undefined;
|
|
236
|
+
cacheKey: string | undefined;
|
|
237
|
+
promptPrefix: string;
|
|
238
|
+
}
|
|
239
|
+
export type InferenceCallEvent = {
|
|
240
|
+
type: 'inference-call';
|
|
241
|
+
call: InferenceCall;
|
|
242
|
+
};
|
|
243
|
+
export type InferenceSentEvent = {
|
|
244
|
+
type: 'inference-sent';
|
|
245
|
+
round: number;
|
|
246
|
+
droppedTurns: number;
|
|
247
|
+
messages: Message[] | undefined;
|
|
248
|
+
tokens: InferenceTokens | undefined;
|
|
249
|
+
outcome: 'completed' | 'overflowed' | 'failed';
|
|
250
|
+
reason: string | undefined;
|
|
251
|
+
};
|
|
252
|
+
export interface InferenceTokens {
|
|
253
|
+
prompt?: number | undefined;
|
|
254
|
+
generated?: number | undefined;
|
|
255
|
+
cached?: number | undefined;
|
|
256
|
+
timeToFirstToken?: number | undefined;
|
|
257
|
+
}
|
|
258
|
+
export type CompletionEvent = TextEvent | ToolCallEvent | ToolResultEvent | ToolActivityEvent | ErrorEvent | AbortedEvent | MetricsEvent | InferenceCallEvent | InferenceSentEvent;
|
|
226
259
|
export interface CompletionSdkResult {
|
|
227
260
|
requestId?: string;
|
|
228
261
|
tokenStream: AsyncIterable<string>;
|
|
@@ -283,6 +316,7 @@ interface CompletionArgs {
|
|
|
283
316
|
toolConfigs?: Record<string, Record<string, unknown>>;
|
|
284
317
|
fsRoots?: readonly DriveRoot[];
|
|
285
318
|
fsWriteRoots?: readonly DriveRoot[];
|
|
319
|
+
captureInferenceCalls?: boolean;
|
|
286
320
|
}
|
|
287
321
|
export declare const CONTEXT_OVERFLOW_MESSAGE = "This message is too large for the model context window.";
|
|
288
322
|
export declare function trustedSourcesFrom(history: readonly Message[]): TrustedSource[];
|
|
@@ -290,5 +324,5 @@ export declare function toSdkTool(tool: ToolDefinition): SdkTool;
|
|
|
290
324
|
export type CompletionSkillEntry = SkillCatalogEntry & {
|
|
291
325
|
readonly status?: SkillState;
|
|
292
326
|
};
|
|
293
|
-
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 }: CompletionArgs): AsyncGenerator<CompletionEvent>;
|
|
327
|
+
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>;
|
|
294
328
|
export {};
|
|
@@ -24,7 +24,7 @@ import { collectToolGrants, enabledTools, isSelected, skillIsActive, skillsAreSe
|
|
|
24
24
|
import { guardToolCall } from '../tools/outbound-guard.js';
|
|
25
25
|
import { isAvailableState } from '../tools/skills/skill-state-constants.js';
|
|
26
26
|
import { resolveImplicitSkill, resolveSkillMention, skillPreloadMessage } from '../tools/skills/mention.js';
|
|
27
|
-
import { permittedTools } from '../tools/toolsets.js';
|
|
27
|
+
import { NETWORK_TOOLSETS, permittedTools } from '../tools/toolsets.js';
|
|
28
28
|
import { promptPrefixHash } from './prompt-prefix.js';
|
|
29
29
|
import { logToolPrompt } from './tool-prompt-log.js';
|
|
30
30
|
import { DIRECT_CALL_ID, loadedSkillFilesFrom, loadedSkillsFrom, TOOL_CONTINUATION_NUDGE, toolResultTurn, withoutSkillLoadTurns } from './tool-turns.js';
|
|
@@ -157,7 +157,8 @@ function preloadLastUser(messages, name, body) {
|
|
|
157
157
|
}
|
|
158
158
|
return messages;
|
|
159
159
|
}
|
|
160
|
-
function collectStats(state, stats) {
|
|
160
|
+
function collectStats(state, stats, stopReason) {
|
|
161
|
+
state.stopReason = stopReason;
|
|
161
162
|
if (!stats)
|
|
162
163
|
return;
|
|
163
164
|
if (state.ttft === undefined && stats.timeToFirstToken)
|
|
@@ -191,7 +192,8 @@ function metricsEvent(state, usage) {
|
|
|
191
192
|
cacheTokens: state.cacheTokens,
|
|
192
193
|
...(state.ctxSize ? { ctxSize: state.ctxSize } : {}),
|
|
193
194
|
...(state.backends.length ? { backends: state.backends } : {}),
|
|
194
|
-
...(usage ? { usage } : {})
|
|
195
|
+
...(usage ? { usage } : {}),
|
|
196
|
+
...(state.stopReason ? { stopReason: state.stopReason } : {})
|
|
195
197
|
};
|
|
196
198
|
}
|
|
197
199
|
const TOOL_DENIED_MESSAGE = 'The user declined this call. Do not retry it or call it with different arguments to get ' +
|
|
@@ -200,7 +202,7 @@ const TOOL_DENIED_MESSAGE = 'The user declined this call. Do not retry it or cal
|
|
|
200
202
|
function deniedResult() {
|
|
201
203
|
return { error: TOOL_DENIED_MESSAGE, denied: true };
|
|
202
204
|
}
|
|
203
|
-
function
|
|
205
|
+
function callKey(name, args) {
|
|
204
206
|
try {
|
|
205
207
|
return `${name}:${JSON.stringify(args)}`;
|
|
206
208
|
}
|
|
@@ -208,6 +210,34 @@ function denialKey(name, args) {
|
|
|
208
210
|
return name;
|
|
209
211
|
}
|
|
210
212
|
}
|
|
213
|
+
const RETRYABLE_STATUS = new Set([408, 429]);
|
|
214
|
+
const RETRYABLE_CODE = new Set(['timeout', 'transport', 'busy']);
|
|
215
|
+
function callSucceeded(result, outcome) {
|
|
216
|
+
if (outcome.kind !== 'success')
|
|
217
|
+
return false;
|
|
218
|
+
const exitCode = result?.exitCode;
|
|
219
|
+
return typeof exitCode !== 'number' || exitCode === 0;
|
|
220
|
+
}
|
|
221
|
+
function retryHints(result) {
|
|
222
|
+
if (!result || typeof result !== 'object' || Array.isArray(result))
|
|
223
|
+
return {};
|
|
224
|
+
const record = result;
|
|
225
|
+
return {
|
|
226
|
+
...(typeof record.status === 'number' ? { status: record.status } : {}),
|
|
227
|
+
...(typeof record.code === 'string' ? { code: record.code } : {})
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function settledFailure(result, outcome) {
|
|
231
|
+
if (outcome.kind === 'denied' || callSucceeded(result, outcome))
|
|
232
|
+
return false;
|
|
233
|
+
if (outcome.kind === 'error' && outcome.error !== undefined)
|
|
234
|
+
return false;
|
|
235
|
+
const { code, status } = retryHints(result);
|
|
236
|
+
if (code !== undefined && RETRYABLE_CODE.has(code))
|
|
237
|
+
return false;
|
|
238
|
+
return status === undefined || (status < 500 && !RETRYABLE_STATUS.has(status));
|
|
239
|
+
}
|
|
240
|
+
const MAX_BLOCKED_REPEATS = 3;
|
|
211
241
|
function terminalCall(result, outcome, options) {
|
|
212
242
|
return { result, outcome, executed: options.executed };
|
|
213
243
|
}
|
|
@@ -248,7 +278,7 @@ async function executeCall(tool, args, ctx, { approve, signal, denied, sources =
|
|
|
248
278
|
});
|
|
249
279
|
return blockedResult({ error: refusal, guidance: true }, refusal);
|
|
250
280
|
}
|
|
251
|
-
const key =
|
|
281
|
+
const key = callKey(tool.name, args);
|
|
252
282
|
if (denied?.has(key)) {
|
|
253
283
|
hlog('tool denied already, not asking again', { tool: tool.name });
|
|
254
284
|
return deniedCall();
|
|
@@ -364,7 +394,7 @@ function blockedSkillMessage(name, catalog, listShown) {
|
|
|
364
394
|
`skill is available this turn — do not guess another name, and answer from what you ` +
|
|
365
395
|
`already have.`;
|
|
366
396
|
}
|
|
367
|
-
async function resolveToolCalls(toolCalls, enabled, agentId, pipeline, skillNames, signal) {
|
|
397
|
+
async function resolveToolCalls(toolCalls, enabled, agentId, pipeline, skillNames, { sandboxMode, signal }) {
|
|
368
398
|
const resolved = [];
|
|
369
399
|
for (const call of toolCalls) {
|
|
370
400
|
if (signal?.aborted)
|
|
@@ -388,6 +418,7 @@ async function resolveToolCalls(toolCalls, enabled, agentId, pipeline, skillName
|
|
|
388
418
|
agentId,
|
|
389
419
|
skillNames,
|
|
390
420
|
parameters: tool.parameters,
|
|
421
|
+
sandboxMode,
|
|
391
422
|
signal
|
|
392
423
|
});
|
|
393
424
|
resolved.push(fold.ok
|
|
@@ -463,6 +494,21 @@ async function* runRoundTokens(sdk, result, state, signal, startInThink = false)
|
|
|
463
494
|
await events.return(null);
|
|
464
495
|
}
|
|
465
496
|
}
|
|
497
|
+
function sentMessages(stats) {
|
|
498
|
+
const sent = stats?.sentMessages;
|
|
499
|
+
return Array.isArray(sent) ? sent : null;
|
|
500
|
+
}
|
|
501
|
+
function roundTokens(stats) {
|
|
502
|
+
if (!stats)
|
|
503
|
+
return undefined;
|
|
504
|
+
const tokens = {
|
|
505
|
+
prompt: stats.promptTokens,
|
|
506
|
+
generated: stats.generatedTokens,
|
|
507
|
+
cached: stats.cacheTokens,
|
|
508
|
+
timeToFirstToken: stats.timeToFirstToken
|
|
509
|
+
};
|
|
510
|
+
return Object.values(tokens).some((value) => value !== undefined) ? tokens : undefined;
|
|
511
|
+
}
|
|
466
512
|
const TRUNCATED_FINAL_MS = 2000;
|
|
467
513
|
function truncatedFinal(final, contentText) {
|
|
468
514
|
const accepted = { contentText, toolCalls: [], raw: { fullText: contentText } };
|
|
@@ -496,17 +542,35 @@ async function* runRecoveredRoundWith(sdk, args, modelId) {
|
|
|
496
542
|
let emitted = false;
|
|
497
543
|
const state = { stopToken: null, unproven: null, rawContentText: '' };
|
|
498
544
|
const cacheable = candidate.history === args.history;
|
|
545
|
+
const params = {
|
|
546
|
+
modelId,
|
|
547
|
+
history: projectVisionHistory(candidate.history),
|
|
548
|
+
stream: true,
|
|
549
|
+
tools: args.tools,
|
|
550
|
+
generationParams: args.generationParams,
|
|
551
|
+
responseFormat: args.responseFormat,
|
|
552
|
+
toolDialect: args.toolDialect,
|
|
553
|
+
kvCache: cacheable ? args.cacheKey : undefined
|
|
554
|
+
};
|
|
555
|
+
if (args.capture) {
|
|
556
|
+
yield {
|
|
557
|
+
type: 'inference-call',
|
|
558
|
+
call: {
|
|
559
|
+
round: args.round,
|
|
560
|
+
droppedTurns: candidate.droppedTurns,
|
|
561
|
+
modelId,
|
|
562
|
+
messages: params.history,
|
|
563
|
+
tools: params.tools,
|
|
564
|
+
generationParams: params.generationParams,
|
|
565
|
+
responseFormat: params.responseFormat,
|
|
566
|
+
toolDialect: params.toolDialect,
|
|
567
|
+
cacheKey: params.kvCache,
|
|
568
|
+
promptPrefix: args.promptPrefix
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
}
|
|
499
572
|
try {
|
|
500
|
-
const result = sdk.completion(
|
|
501
|
-
modelId,
|
|
502
|
-
history: projectVisionHistory(candidate.history),
|
|
503
|
-
stream: true,
|
|
504
|
-
tools: args.tools,
|
|
505
|
-
generationParams: args.generationParams,
|
|
506
|
-
...(args.responseFormat ? { responseFormat: args.responseFormat } : {}),
|
|
507
|
-
toolDialect: args.toolDialect,
|
|
508
|
-
...(cacheable && args.cacheKey ? { kvCache: args.cacheKey } : {})
|
|
509
|
-
});
|
|
573
|
+
const result = sdk.completion(params);
|
|
510
574
|
result.final.catch(noop);
|
|
511
575
|
const startInThink = args.generationParams?.reasoning_budget === -1;
|
|
512
576
|
for await (const event of runRoundTokens(sdk, result, state, args.signal, startInThink)) {
|
|
@@ -518,6 +582,17 @@ async function* runRecoveredRoundWith(sdk, args, modelId) {
|
|
|
518
582
|
const final = state.stopToken
|
|
519
583
|
? await truncatedFinal(result.final, state.rawContentText)
|
|
520
584
|
: await raceAbort(result.final, args.signal);
|
|
585
|
+
if (args.capture) {
|
|
586
|
+
yield {
|
|
587
|
+
type: 'inference-sent',
|
|
588
|
+
round: args.round,
|
|
589
|
+
droppedTurns: candidate.droppedTurns,
|
|
590
|
+
messages: sentMessages(final.stats) ?? undefined,
|
|
591
|
+
tokens: roundTokens(final.stats),
|
|
592
|
+
outcome: 'completed',
|
|
593
|
+
reason: undefined
|
|
594
|
+
};
|
|
595
|
+
}
|
|
521
596
|
const replay = unprovenReplay(state, final);
|
|
522
597
|
hlog('final', {
|
|
523
598
|
stopReason: final.stopReason,
|
|
@@ -535,6 +610,17 @@ async function* runRecoveredRoundWith(sdk, args, modelId) {
|
|
|
535
610
|
};
|
|
536
611
|
}
|
|
537
612
|
catch (err) {
|
|
613
|
+
if (args.capture) {
|
|
614
|
+
yield {
|
|
615
|
+
type: 'inference-sent',
|
|
616
|
+
round: args.round,
|
|
617
|
+
droppedTurns: candidate.droppedTurns,
|
|
618
|
+
messages: undefined,
|
|
619
|
+
tokens: undefined,
|
|
620
|
+
outcome: isContextOverflowError(err) ? 'overflowed' : 'failed',
|
|
621
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
622
|
+
};
|
|
623
|
+
}
|
|
538
624
|
if (!isContextOverflowError(err))
|
|
539
625
|
throw err;
|
|
540
626
|
if (emitted)
|
|
@@ -551,10 +637,13 @@ function* reportUnnamedWrites(said, landed) {
|
|
|
551
637
|
if (unreported)
|
|
552
638
|
yield { type: 'content', text: unreported };
|
|
553
639
|
}
|
|
554
|
-
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 = [] }) {
|
|
640
|
+
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 }) {
|
|
555
641
|
const pipeline = new HookPipeline();
|
|
556
642
|
const agentId = agent?.id ?? null;
|
|
557
|
-
const
|
|
643
|
+
const disabledToolsets = agent?.sandboxMode
|
|
644
|
+
? [...(agent.disabledToolsets ?? []), ...NETWORK_TOOLSETS]
|
|
645
|
+
: agent?.disabledToolsets;
|
|
646
|
+
const tools = permittedTools(allTools, disabledToolsets);
|
|
558
647
|
const writableRoots = [
|
|
559
648
|
...fsWriteRoots,
|
|
560
649
|
...fsRoots.filter((root) => root.kind === 'hyper' && root.writable && !fsWriteRoots.includes(root))
|
|
@@ -709,6 +798,8 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
|
|
|
709
798
|
let roundCacheKey = stubDropped ? undefined : visionHistoryCacheKey(cacheKey, history);
|
|
710
799
|
const runUsage = () => usageBreakdown(promptParts, stats.cacheTokens, model);
|
|
711
800
|
const denied = new Set();
|
|
801
|
+
const failed = new Map();
|
|
802
|
+
let blockedRepeats = 0;
|
|
712
803
|
let webSearches = 0;
|
|
713
804
|
const sources = trustedSourcesFrom(messages);
|
|
714
805
|
if (preloadedSkill)
|
|
@@ -789,7 +880,9 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
|
|
|
789
880
|
let mute = 0;
|
|
790
881
|
let narrated = false;
|
|
791
882
|
let cappedRounds = 0;
|
|
792
|
-
|
|
883
|
+
let roundsSent = 0;
|
|
884
|
+
let round = 0;
|
|
885
|
+
for (; round < MAX_TOOL_ROUNDS; round++) {
|
|
793
886
|
if (signal?.aborted)
|
|
794
887
|
throw new AbortedError();
|
|
795
888
|
if (Date.now() >= runBudget.deadlineMs) {
|
|
@@ -820,13 +913,17 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
|
|
|
820
913
|
responseFormat,
|
|
821
914
|
toolDialect,
|
|
822
915
|
cacheKey: roundCacheKey,
|
|
823
|
-
signal
|
|
916
|
+
signal,
|
|
917
|
+
round,
|
|
918
|
+
promptPrefix,
|
|
919
|
+
capture: captureInferenceCalls
|
|
824
920
|
});
|
|
921
|
+
roundsSent = round + 1;
|
|
825
922
|
const final = recovered.final;
|
|
826
923
|
if (!recovered.cacheable)
|
|
827
924
|
roundCacheKey = undefined;
|
|
828
925
|
history = recovered.history;
|
|
829
|
-
collectStats(stats, final.stats);
|
|
926
|
+
collectStats(stats, final.stats, final.stopReason);
|
|
830
927
|
hlog('round stats', { round, wallMs: Date.now() - roundStarted, ...(final.stats ?? {}) });
|
|
831
928
|
const markup = stripToolResponseBlocks(truncateAtControlToken(final.raw.fullText) || recovered.unproven || '');
|
|
832
929
|
let toolCalls = final.toolCalls?.length
|
|
@@ -880,7 +977,7 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
|
|
|
880
977
|
}
|
|
881
978
|
mute = 0;
|
|
882
979
|
narrated = false;
|
|
883
|
-
const resolved = await resolveToolCalls(toolCalls, enabled, agent?.id ?? null, pipeline, inContextSkills.map((skill) => skill.name), signal);
|
|
980
|
+
const resolved = await resolveToolCalls(toolCalls, enabled, agent?.id ?? null, pipeline, inContextSkills.map((skill) => skill.name), { sandboxMode: agent?.sandboxMode === true, signal });
|
|
884
981
|
if (signal?.aborted)
|
|
885
982
|
throw new AbortedError();
|
|
886
983
|
history = [
|
|
@@ -891,10 +988,12 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
|
|
|
891
988
|
}
|
|
892
989
|
];
|
|
893
990
|
let cappedCalls = 0;
|
|
991
|
+
let progressed = false;
|
|
894
992
|
runOutcomes.beginRound();
|
|
895
993
|
for (const { call, tool, args, rawCall, rejection, intent } of resolved) {
|
|
896
994
|
if (signal?.aborted)
|
|
897
995
|
throw new AbortedError();
|
|
996
|
+
const priorFailure = failed.get(callKey(call.name, args));
|
|
898
997
|
const searchCapped = tool?.name === WEB_SEARCH_TOOL_NAME && webSearches >= MAX_WEB_SEARCHES_PER_TURN;
|
|
899
998
|
const callCtx = {
|
|
900
999
|
...toolContext(turnImages, ctxSize),
|
|
@@ -912,7 +1011,7 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
|
|
|
912
1011
|
const resource = tool?.resource?.(args) ?? null;
|
|
913
1012
|
const skillName = unquoteArg(args?.name ?? args?.skill_name ?? args?.skill);
|
|
914
1013
|
const skillReloaded = tool?.name === SKILL_TOOL_NAME && !args?.file && loadedThisTurn.has(skillName);
|
|
915
|
-
const silent = searchCapped || skillReloaded;
|
|
1014
|
+
const silent = searchCapped || skillReloaded || priorFailure !== undefined;
|
|
916
1015
|
const display = silent
|
|
917
1016
|
? undefined
|
|
918
1017
|
: labelOf(call.name, args, skillCall ? skillEmoji(skillName) : skillIcon);
|
|
@@ -926,7 +1025,15 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
|
|
|
926
1025
|
const unreadyReason = `skill "${skillName}" is ${unreadyState ?? 'unavailable'} and cannot be used yet. ` +
|
|
927
1026
|
`Tell the user to open it in Skills and finish its setup; do not retry.`;
|
|
928
1027
|
let execution;
|
|
929
|
-
if (
|
|
1028
|
+
if (priorFailure) {
|
|
1029
|
+
blockedRepeats++;
|
|
1030
|
+
hlog('repeat of a call that already failed; answered without running it', {
|
|
1031
|
+
tool: call.name,
|
|
1032
|
+
blockedRepeats
|
|
1033
|
+
});
|
|
1034
|
+
execution = terminalCall(priorFailure.result, priorFailure.outcome, { executed: false });
|
|
1035
|
+
}
|
|
1036
|
+
else if (skillReloaded) {
|
|
930
1037
|
execution = terminalCall({
|
|
931
1038
|
name: skillName,
|
|
932
1039
|
content: `${skillName} is already loaded. Follow the instructions you were given and do not load it again.`
|
|
@@ -974,13 +1081,20 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
|
|
|
974
1081
|
});
|
|
975
1082
|
const output = settled.result;
|
|
976
1083
|
const executionOutcome = settled.outcome;
|
|
1084
|
+
if (!priorFailure && settledFailure(output, executionOutcome)) {
|
|
1085
|
+
failed.set(callKey(call.name, args), settled);
|
|
1086
|
+
}
|
|
1087
|
+
else if (execution.executed && callSucceeded(output, executionOutcome)) {
|
|
1088
|
+
progressed = true;
|
|
1089
|
+
}
|
|
977
1090
|
runOutcomes.record(output, executionOutcome);
|
|
978
1091
|
if (searchCapped) {
|
|
979
1092
|
cappedCalls++;
|
|
980
1093
|
hlog('web_search capped', { searches: webSearches });
|
|
981
1094
|
}
|
|
982
|
-
else if (tool?.name === WEB_SEARCH_TOOL_NAME && !rejection)
|
|
1095
|
+
else if (tool?.name === WEB_SEARCH_TOOL_NAME && !rejection && !priorFailure) {
|
|
983
1096
|
webSearches++;
|
|
1097
|
+
}
|
|
984
1098
|
if (call.name === SKILL_TOOL_NAME &&
|
|
985
1099
|
output &&
|
|
986
1100
|
typeof output === 'object' &&
|
|
@@ -1032,7 +1146,13 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
|
|
|
1032
1146
|
history.push({ role: 'tool', content: turn });
|
|
1033
1147
|
}
|
|
1034
1148
|
runOutcomes.finishRound();
|
|
1149
|
+
if (progressed) {
|
|
1150
|
+
failed.clear();
|
|
1151
|
+
blockedRepeats = 0;
|
|
1152
|
+
}
|
|
1035
1153
|
history.push({ role: 'user', content: TOOL_CONTINUATION_NUDGE });
|
|
1154
|
+
if (blockedRepeats >= MAX_BLOCKED_REPEATS)
|
|
1155
|
+
break;
|
|
1036
1156
|
if (cappedCalls === resolved.length) {
|
|
1037
1157
|
if (cappedRounds++ >= MAX_CAPPED_ROUNDS) {
|
|
1038
1158
|
outcome = 'search-capped';
|
|
@@ -1060,9 +1180,12 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
|
|
|
1060
1180
|
tools: undefined,
|
|
1061
1181
|
generationParams,
|
|
1062
1182
|
cacheKey: roundCacheKey,
|
|
1063
|
-
signal
|
|
1183
|
+
signal,
|
|
1184
|
+
round: roundsSent,
|
|
1185
|
+
promptPrefix: promptPrefixHash({ system: systemContent, tools: undefined }),
|
|
1186
|
+
capture: captureInferenceCalls
|
|
1064
1187
|
});
|
|
1065
|
-
collectStats(stats, recovered.final.stats);
|
|
1188
|
+
collectStats(stats, recovered.final.stats, recovered.final.stopReason);
|
|
1066
1189
|
const answer = recovered.replay ? stripToolCallMarkup(recovered.replay) : '';
|
|
1067
1190
|
if (answer)
|
|
1068
1191
|
yield { type: 'content', text: answer };
|
package/dist-lib/lib/connect.js
CHANGED
|
@@ -112,6 +112,7 @@ export class RemoteHarness extends EventEmitter {
|
|
|
112
112
|
thinkingDefault: context.thinkingDefault,
|
|
113
113
|
generationParams: context.generationParams,
|
|
114
114
|
responseFormat: context.responseFormat,
|
|
115
|
+
captureInferenceCalls: context.captureInferenceCalls,
|
|
115
116
|
messages: context.messages.map(({ role, content, attachments }) => ({
|
|
116
117
|
role,
|
|
117
118
|
content,
|
|
@@ -655,7 +656,8 @@ function knobs(agent) {
|
|
|
655
656
|
imageWidth: agent.imageWidth ?? undefined,
|
|
656
657
|
imageHeight: agent.imageHeight ?? undefined,
|
|
657
658
|
approvalMode: agent.approvalMode ?? undefined,
|
|
658
|
-
disabledToolsets: agent.disabledToolsets ?? undefined
|
|
659
|
+
disabledToolsets: agent.disabledToolsets ?? undefined,
|
|
660
|
+
sandboxMode: agent.sandboxMode ?? undefined
|
|
659
661
|
};
|
|
660
662
|
}
|
|
661
663
|
async function bridgeAttach(event, attach) {
|
|
@@ -38,6 +38,7 @@ export interface AgentKnobs {
|
|
|
38
38
|
imageHeight?: number | null;
|
|
39
39
|
approvalMode?: 'ask' | 'skip-all' | null;
|
|
40
40
|
disabledToolsets?: string[] | null;
|
|
41
|
+
sandboxMode?: boolean | null;
|
|
41
42
|
}
|
|
42
43
|
export interface CompletionContext {
|
|
43
44
|
model: string;
|
|
@@ -48,6 +49,7 @@ export interface CompletionContext {
|
|
|
48
49
|
toolConfigs?: Record<string, Record<string, unknown>>;
|
|
49
50
|
generationParams?: LlmGenerationParams;
|
|
50
51
|
responseFormat?: ResponseFormat;
|
|
52
|
+
captureInferenceCalls?: boolean;
|
|
51
53
|
grantedDirs?: string[];
|
|
52
54
|
grantedDrives?: GrantedDrive[];
|
|
53
55
|
driveOp?: DriveOpFn;
|
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import { runPython } from '../tools/exec/adapters/python/pyodide.js';
|
|
4
|
-
import { planVendoredInstall, vendoredManifest, wheelsDir } from '../tools/exec/adapters/python/wheels.js';
|
|
1
|
+
import { extractWithPython } from '../office/python.js';
|
|
5
2
|
export { closePythonWorker as closeDocxWorker } from '../tools/exec/adapters/python/worker/client.js';
|
|
6
|
-
const MAX_OFFICE_BYTES = 20 * 1024 * 1024;
|
|
7
3
|
const PACKAGES = ['python-docx==1.2.0'];
|
|
8
4
|
const EXTRACT = `from docx import Document
|
|
9
5
|
from docx.text.paragraph import Paragraph
|
|
@@ -21,29 +17,10 @@ def text(container):
|
|
|
21
17
|
Path("extracted.txt").write_text("\\n".join(text(Document("input.docx"))), encoding="utf-8")
|
|
22
18
|
`;
|
|
23
19
|
export async function extractDocxText(bytes, fileName, options = {}) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
const dir = wheelsDir();
|
|
28
|
-
const manifest = vendoredManifest(dir);
|
|
29
|
-
const install = planVendoredInstall(PACKAGES, { dir, manifest });
|
|
30
|
-
if (!dir ||
|
|
31
|
-
!manifest ||
|
|
32
|
-
!install ||
|
|
33
|
-
!['micropip', ...install.packages].every((name) => Object.keys(manifest.files).some((file) => file.replaceAll('_', '-').startsWith(`${name}-`) && fs.existsSync(`${dir}/${file}`)))) {
|
|
34
|
-
throw new Error('bundled DOCX extraction packages are unavailable');
|
|
35
|
-
}
|
|
36
|
-
const result = await runPython(EXTRACT, {
|
|
20
|
+
return extractWithPython(bytes, fileName, {
|
|
21
|
+
format: 'DOCX',
|
|
37
22
|
packages: PACKAGES,
|
|
38
|
-
|
|
39
|
-
outputs: ['extracted.txt'],
|
|
23
|
+
source: EXTRACT,
|
|
40
24
|
signal: options.signal
|
|
41
25
|
});
|
|
42
|
-
if (result.exitCode !== 0)
|
|
43
|
-
throw new Error(`${fileName}: ${result.stderr.trim()}`);
|
|
44
|
-
const output = result.files?.get('extracted.txt');
|
|
45
|
-
if (!output) {
|
|
46
|
-
throw new Error(result.rejected?.[0]?.reason ?? 'DOCX extraction produced no text file');
|
|
47
|
-
}
|
|
48
|
-
return b4a.toString(output, 'utf8');
|
|
49
26
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { bareBin, firstCommandToken } from '../../tools/exec/scope.js';
|
|
2
|
+
const NETWORK_BINS = new Set([
|
|
3
|
+
'curl',
|
|
4
|
+
'wget',
|
|
5
|
+
'nc',
|
|
6
|
+
'ncat',
|
|
7
|
+
'netcat',
|
|
8
|
+
'ssh',
|
|
9
|
+
'scp',
|
|
10
|
+
'sftp',
|
|
11
|
+
'rsync',
|
|
12
|
+
'ftp',
|
|
13
|
+
'telnet',
|
|
14
|
+
'git',
|
|
15
|
+
'npm',
|
|
16
|
+
'npx',
|
|
17
|
+
'pnpm',
|
|
18
|
+
'yarn',
|
|
19
|
+
'bun',
|
|
20
|
+
'pip',
|
|
21
|
+
'pip3',
|
|
22
|
+
'brew',
|
|
23
|
+
'dig',
|
|
24
|
+
'nslookup',
|
|
25
|
+
'ping',
|
|
26
|
+
'openssl'
|
|
27
|
+
]);
|
|
28
|
+
export class SandboxHook {
|
|
29
|
+
phase = 'preToolCall';
|
|
30
|
+
run(ctx) {
|
|
31
|
+
if (ctx.phase !== 'preToolCall')
|
|
32
|
+
return;
|
|
33
|
+
if (!ctx.sandboxMode || ctx.toolName !== 'exec')
|
|
34
|
+
return;
|
|
35
|
+
if (Array.isArray(ctx.args.packages) && ctx.args.packages.length) {
|
|
36
|
+
return { error: 'sandbox mode: installing packages reaches the network', guidance: true };
|
|
37
|
+
}
|
|
38
|
+
const bin = bareBin(firstCommandToken(String(ctx.args.command ?? '')).replace(/^.*[/\\]/, ''));
|
|
39
|
+
if (bin && NETWORK_BINS.has(bin)) {
|
|
40
|
+
return { error: `sandbox mode: ${bin} reaches the network`, guidance: true };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -11,7 +11,12 @@ export class SkillCatalogGuardHook {
|
|
|
11
11
|
const known = new Set(entries.map((entry) => (typeof entry === 'string' ? entry : entry.name)));
|
|
12
12
|
const raw = ctx.args.name ?? ctx.args.skill_name ?? ctx.args.skill;
|
|
13
13
|
const name = typeof raw === 'string' ? raw : '';
|
|
14
|
-
if (
|
|
15
|
-
return
|
|
14
|
+
if (known.has(name))
|
|
15
|
+
return;
|
|
16
|
+
const listed = [...known].join(', ');
|
|
17
|
+
const rest = listed
|
|
18
|
+
? `The only skills are: ${listed}. Nothing else needs a skill load — answer or act with the tools you already have.`
|
|
19
|
+
: 'No skill is available this turn — answer or act with the tools you already have.';
|
|
20
|
+
return { error: `skill not found: ${name}. ${rest}`, guidance: true };
|
|
16
21
|
}
|
|
17
22
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { ArgRepairHook, EgressGuardHook, SkillCatalogGuardHook } from './built-in/index.js';
|
|
1
|
+
import { ArgRepairHook, EgressGuardHook, SandboxHook, SkillCatalogGuardHook } from './built-in/index.js';
|
|
2
2
|
export const harnessHooks = [
|
|
3
3
|
new ArgRepairHook(),
|
|
4
4
|
new EgressGuardHook(),
|
|
5
|
+
new SandboxHook(),
|
|
5
6
|
new SkillCatalogGuardHook()
|
|
6
7
|
];
|
|
@@ -14,6 +14,7 @@ export interface PreToolCallPayload {
|
|
|
14
14
|
readonly signal?: BareAbortSignal | undefined;
|
|
15
15
|
readonly skillNames?: readonly string[];
|
|
16
16
|
readonly parameters?: ToolDefinition['parameters'];
|
|
17
|
+
readonly sandboxMode?: boolean;
|
|
17
18
|
args: Record<string, unknown>;
|
|
18
19
|
}
|
|
19
20
|
export interface PostToolCallPayload {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AbortSignal } from 'bare-abort-controller';
|
|
2
|
+
export { closePythonWorker as closeOfficeWorker } from '../tools/exec/adapters/python/worker/client.ts';
|
|
3
|
+
export declare function extractOfficeText(bytes: Uint8Array, fileName: string, options?: {
|
|
4
|
+
readonly signal?: AbortSignal | undefined;
|
|
5
|
+
readonly mimeType?: string | undefined;
|
|
6
|
+
}): Promise<string>;
|