@qvac/harness 0.2.9 → 0.2.12
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 +11 -62
- package/dist/prebuilds-manifest.json +0 -8
- package/dist-lib/index.d.ts +16 -1
- package/dist-lib/index.js +31 -1
- package/dist-lib/lib/capabilities/completion.d.ts +1 -0
- package/dist-lib/lib/capabilities/completion.js +66 -9
- package/dist-lib/lib/capabilities/detect-language.d.ts +11 -0
- package/dist-lib/lib/capabilities/detect-language.js +13 -0
- package/dist-lib/lib/capabilities/invoke.d.ts +15 -0
- package/dist-lib/lib/capabilities/invoke.js +33 -0
- package/dist-lib/lib/connect.d.ts +4 -1
- package/dist-lib/lib/connect.js +51 -22
- package/dist-lib/lib/contract-types.d.ts +21 -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 +45 -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 +32 -4
- 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 +4 -4
- 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/skills/manifest-parse.d.ts +1 -0
- package/dist-lib/lib/tools/skills/manifest-parse.js +92 -1
- package/dist-lib/lib/tools/skills/manifest.d.ts +12 -0
- 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 +18 -5
- package/dist-lib/lib/tools/web-search.js +5 -2
- package/dist-lib/schema/harness.js +54 -4
- package/dist-lib/spec/bind.js +3 -0
- package/dist-lib/spec/calls.js +1 -0
- package/dist-lib/spec/hrpc/hrpc.json +13 -0
- package/dist-lib/spec/hrpc/index.js +15 -3
- package/dist-lib/spec/hrpc/messages.js +207 -13
- package/dist-lib/spec/hyperschema/index.js +207 -13
- package/dist-lib/spec/hyperschema/schema.json +163 -1
- package/dist-lib/spec/hyperschema/types.d.ts +43 -2
- package/package.json +7 -2
|
@@ -272,14 +272,6 @@
|
|
|
272
272
|
"win32-arm64",
|
|
273
273
|
"win32-x64"
|
|
274
274
|
],
|
|
275
|
-
"bare-tty/node_modules/bare-signals": [
|
|
276
|
-
"darwin-arm64",
|
|
277
|
-
"darwin-x64",
|
|
278
|
-
"linux-arm64",
|
|
279
|
-
"linux-x64",
|
|
280
|
-
"win32-arm64",
|
|
281
|
-
"win32-x64"
|
|
282
|
-
],
|
|
283
275
|
"bare-tty": [
|
|
284
276
|
"darwin-arm64",
|
|
285
277
|
"darwin-x64",
|
package/dist-lib/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { AbortSignal as BareAbortSignal } from 'bare-abort-controller';
|
|
2
2
|
import ReadyResource from 'ready-resource';
|
|
3
3
|
import { type Agent, type ApproveFn, type AskUserFn, type AttachFn, type CompletionEvent, type FetchAttachmentFn, type Message, type ProgressFn, type RagSearchFn, type ResponseFormat, type ToolDefinition, type ToolExecuteContext } from './lib/capabilities/completion.ts';
|
|
4
|
+
import { type DetectLanguageContext, type DetectedLanguage } from './lib/capabilities/detect-language.ts';
|
|
5
|
+
import { type Capability } from './lib/capabilities/invoke.ts';
|
|
4
6
|
import { type DownloadArgs, type DownloadEventStream } from './lib/capabilities/download.ts';
|
|
5
7
|
import { type EmbedContext } from './lib/capabilities/embed.ts';
|
|
6
8
|
import { type KvCacheDeletionResult } from './lib/capabilities/kv-cache.ts';
|
|
@@ -22,6 +24,8 @@ import type { ResolvedSkillCatalogEntry } from './lib/tools/skills/catalog.ts';
|
|
|
22
24
|
import { type SkillSettingEntry } from './lib/tools/skills/skill-settings.ts';
|
|
23
25
|
export * from './lib/capabilities/completion.ts';
|
|
24
26
|
export { attachmentTurn, loadedSkillsFrom, skillLoadedTurn } from './lib/capabilities/tool-turns.ts';
|
|
27
|
+
export { CAPABILITIES, isCapability, type Capability } from './lib/capabilities/invoke.ts';
|
|
28
|
+
export type { DetectLanguageContext, DetectedLanguage } from './lib/capabilities/detect-language.ts';
|
|
25
29
|
export * from './lib/capabilities/download.ts';
|
|
26
30
|
export type { EmbedContext } from './lib/capabilities/embed.ts';
|
|
27
31
|
export type { SpeakContext, SpeakResult, SpeakStreamContext, TranscribeContext } from './lib/capabilities/voice.ts';
|
|
@@ -37,7 +41,7 @@ export { requiredSettings, settingEnvName, type SkillSettingEntry, type SkillSet
|
|
|
37
41
|
export type { ToolCatalogEntry } from './lib/tools/catalog.ts';
|
|
38
42
|
export { TOOLSETS, toolsetEnabled, type Toolset } from './lib/tools/toolsets.ts';
|
|
39
43
|
export { markCredentialRejected, markCredentialValid, recordCredentialHealth, resetAllCredentialHealth, resetCredentialHealth } from './lib/tools/skills/credential-health.ts';
|
|
40
|
-
export type { InstallStep, SkillRequires, SkillSetup, SkillSetupField, SkillSetupRoute } from './lib/tools/skills/manifest.ts';
|
|
44
|
+
export type { InstallStep, SkillOAuthConfig, SkillRequires, SkillSetup, SkillSetupField, SkillSetupRoute } from './lib/tools/skills/manifest.ts';
|
|
41
45
|
export { readSetup as normalizeSkillSetup } from './lib/tools/skills/manifest-parse.ts';
|
|
42
46
|
export type { CredentialRecord, CredentialStoreValue } from './lib/tools/skills/auth-resolver.ts';
|
|
43
47
|
export { isAvailableState, SKILL_STATE, type SkillState } from './lib/tools/skills/skill-state.ts';
|
|
@@ -106,6 +110,15 @@ export interface CompletionContext {
|
|
|
106
110
|
askUser?: AskUserFn;
|
|
107
111
|
signal?: BareAbortSignal;
|
|
108
112
|
}
|
|
113
|
+
export interface InvokeContext {
|
|
114
|
+
capability: Capability;
|
|
115
|
+
args: Record<string, unknown>;
|
|
116
|
+
agent?: Agent;
|
|
117
|
+
attach?: AttachFn;
|
|
118
|
+
fetchAttachment?: FetchAttachmentFn;
|
|
119
|
+
progress?: ProgressFn;
|
|
120
|
+
signal?: BareAbortSignal;
|
|
121
|
+
}
|
|
109
122
|
export declare class Harness extends ReadyResource {
|
|
110
123
|
private _tools;
|
|
111
124
|
private _sdk;
|
|
@@ -224,6 +237,7 @@ export declare class Harness extends ReadyResource {
|
|
|
224
237
|
private _defaultVoiceSrc;
|
|
225
238
|
private _voiceJob;
|
|
226
239
|
transcribe({ model, audio, signal }: TranscribeContext): Promise<string>;
|
|
240
|
+
detectLanguage(context: DetectLanguageContext): DetectedLanguage[];
|
|
227
241
|
speak({ model, text, signal }: SpeakContext): Promise<SpeakResult>;
|
|
228
242
|
speakStream({ model, text, signal }: SpeakStreamContext): AsyncIterable<SpeakResult>;
|
|
229
243
|
list(options?: ListModelsOptions): Promise<ModelInfo[]>;
|
|
@@ -240,6 +254,7 @@ export declare class Harness extends ReadyResource {
|
|
|
240
254
|
agent?: Agent;
|
|
241
255
|
signal?: BareAbortSignal;
|
|
242
256
|
}): Promise<void>;
|
|
257
|
+
invoke({ capability, args, agent, attach, fetchAttachment, progress, signal }: InvokeContext): AsyncGenerator<CompletionEvent>;
|
|
243
258
|
completion({ model, agent, messages, cacheKey, thinkingDefault, toolConfigs, generationParams, responseFormat, captureInferenceCalls, grantedDirs, grantedDrives, driveOp, grantedWriteDirs, approve, attach, fetchAttachment, ragSearch, progress, askUser, signal }: CompletionContext): AsyncGenerator<CompletionEvent>;
|
|
244
259
|
private _makeRoomForCompletion;
|
|
245
260
|
private _evictWedgedModel;
|
package/dist-lib/index.js
CHANGED
|
@@ -5,7 +5,9 @@ import ReadyResource from 'ready-resource';
|
|
|
5
5
|
import { probeGpuStats } from './lib/hardware/gpu-probe.js';
|
|
6
6
|
import { probeCapabilityProfile } from './lib/hardware/capability-probe.js';
|
|
7
7
|
import { closePythonWorker } from './lib/tools/exec/adapters/python/worker/client.js';
|
|
8
|
-
import { completion } from './lib/capabilities/completion.js';
|
|
8
|
+
import { completion, imageSettingsFrom } from './lib/capabilities/completion.js';
|
|
9
|
+
import { detectLanguage } from './lib/capabilities/detect-language.js';
|
|
10
|
+
import { invoke } from './lib/capabilities/invoke.js';
|
|
9
11
|
import { download } from './lib/capabilities/download.js';
|
|
10
12
|
import { createEmbedLane } from './lib/capabilities/embed.js';
|
|
11
13
|
import { deleteAllKvCaches, deleteKvCacheKey } from './lib/capabilities/kv-cache.js';
|
|
@@ -45,6 +47,7 @@ import { isDeviceSharedCredentialKey, scopeKey } from './lib/tools/skills/scope-
|
|
|
45
47
|
import { settingsScopeKey } from './lib/tools/skills/skill-settings.js';
|
|
46
48
|
export * from './lib/capabilities/completion.js';
|
|
47
49
|
export { attachmentTurn, loadedSkillsFrom, skillLoadedTurn } from './lib/capabilities/tool-turns.js';
|
|
50
|
+
export { CAPABILITIES, isCapability } from './lib/capabilities/invoke.js';
|
|
48
51
|
export * from './lib/capabilities/download.js';
|
|
49
52
|
export * from './lib/models/model-catalog.js';
|
|
50
53
|
export * from './lib/models/model-list.js';
|
|
@@ -809,6 +812,9 @@ export class Harness extends ReadyResource {
|
|
|
809
812
|
return transcribeOnce(sdk, id, pcm ? buildWavBuffer(pcm) : audio);
|
|
810
813
|
});
|
|
811
814
|
}
|
|
815
|
+
detectLanguage(context) {
|
|
816
|
+
return detectLanguage(context);
|
|
817
|
+
}
|
|
812
818
|
async speak({ model, text, signal }) {
|
|
813
819
|
const samples = await this._voiceJob(this._ttsModels, 'tts', model, signal, (sdk, id) => speakOnce(sdk, id, text));
|
|
814
820
|
return { audio: packSamples(samples), sampleRate: SUPERTONIC_SAMPLE_RATE };
|
|
@@ -943,6 +949,30 @@ export class Harness extends ReadyResource {
|
|
|
943
949
|
releaseModel(pool, key);
|
|
944
950
|
}
|
|
945
951
|
}
|
|
952
|
+
async *invoke({ capability, args, agent, attach, fetchAttachment, progress, signal }) {
|
|
953
|
+
if (!this.opened)
|
|
954
|
+
await this.ready();
|
|
955
|
+
if (this.suspended)
|
|
956
|
+
return yield { type: 'aborted' };
|
|
957
|
+
hlog('invoke start', { capability });
|
|
958
|
+
try {
|
|
959
|
+
yield* invoke({
|
|
960
|
+
capability,
|
|
961
|
+
args,
|
|
962
|
+
tools: this._tools,
|
|
963
|
+
ctx: {
|
|
964
|
+
image: imageSettingsFrom(agent),
|
|
965
|
+
attach,
|
|
966
|
+
fetchAttachment,
|
|
967
|
+
progress,
|
|
968
|
+
signal
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
finally {
|
|
973
|
+
hlog('invoke end', { capability });
|
|
974
|
+
}
|
|
975
|
+
}
|
|
946
976
|
async *completion({ model, agent, messages, cacheKey, thinkingDefault, toolConfigs, generationParams, responseFormat, captureInferenceCalls, grantedDirs, grantedDrives, driveOp, grantedWriteDirs, approve, attach, fetchAttachment, ragSearch, progress, askUser, signal }) {
|
|
947
977
|
await this.ready();
|
|
948
978
|
if (this.suspended)
|
|
@@ -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';
|
|
@@ -202,7 +202,7 @@ const TOOL_DENIED_MESSAGE = 'The user declined this call. Do not retry it or cal
|
|
|
202
202
|
function deniedResult() {
|
|
203
203
|
return { error: TOOL_DENIED_MESSAGE, denied: true };
|
|
204
204
|
}
|
|
205
|
-
function
|
|
205
|
+
function callKey(name, args) {
|
|
206
206
|
try {
|
|
207
207
|
return `${name}:${JSON.stringify(args)}`;
|
|
208
208
|
}
|
|
@@ -210,6 +210,34 @@ function denialKey(name, args) {
|
|
|
210
210
|
return name;
|
|
211
211
|
}
|
|
212
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;
|
|
213
241
|
function terminalCall(result, outcome, options) {
|
|
214
242
|
return { result, outcome, executed: options.executed };
|
|
215
243
|
}
|
|
@@ -250,7 +278,7 @@ async function executeCall(tool, args, ctx, { approve, signal, denied, sources =
|
|
|
250
278
|
});
|
|
251
279
|
return blockedResult({ error: refusal, guidance: true }, refusal);
|
|
252
280
|
}
|
|
253
|
-
const key =
|
|
281
|
+
const key = callKey(tool.name, args);
|
|
254
282
|
if (denied?.has(key)) {
|
|
255
283
|
hlog('tool denied already, not asking again', { tool: tool.name });
|
|
256
284
|
return deniedCall();
|
|
@@ -366,7 +394,7 @@ function blockedSkillMessage(name, catalog, listShown) {
|
|
|
366
394
|
`skill is available this turn — do not guess another name, and answer from what you ` +
|
|
367
395
|
`already have.`;
|
|
368
396
|
}
|
|
369
|
-
async function resolveToolCalls(toolCalls, enabled, agentId, pipeline, skillNames, signal) {
|
|
397
|
+
async function resolveToolCalls(toolCalls, enabled, agentId, pipeline, skillNames, { sandboxMode, signal }) {
|
|
370
398
|
const resolved = [];
|
|
371
399
|
for (const call of toolCalls) {
|
|
372
400
|
if (signal?.aborted)
|
|
@@ -390,6 +418,7 @@ async function resolveToolCalls(toolCalls, enabled, agentId, pipeline, skillName
|
|
|
390
418
|
agentId,
|
|
391
419
|
skillNames,
|
|
392
420
|
parameters: tool.parameters,
|
|
421
|
+
sandboxMode,
|
|
393
422
|
signal
|
|
394
423
|
});
|
|
395
424
|
resolved.push(fold.ok
|
|
@@ -611,7 +640,10 @@ function* reportUnnamedWrites(said, landed) {
|
|
|
611
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 }) {
|
|
612
641
|
const pipeline = new HookPipeline();
|
|
613
642
|
const agentId = agent?.id ?? null;
|
|
614
|
-
const
|
|
643
|
+
const disabledToolsets = agent?.sandboxMode
|
|
644
|
+
? [...(agent.disabledToolsets ?? []), ...NETWORK_TOOLSETS]
|
|
645
|
+
: agent?.disabledToolsets;
|
|
646
|
+
const tools = permittedTools(allTools, disabledToolsets);
|
|
615
647
|
const writableRoots = [
|
|
616
648
|
...fsWriteRoots,
|
|
617
649
|
...fsRoots.filter((root) => root.kind === 'hyper' && root.writable && !fsWriteRoots.includes(root))
|
|
@@ -766,6 +798,8 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
|
|
|
766
798
|
let roundCacheKey = stubDropped ? undefined : visionHistoryCacheKey(cacheKey, history);
|
|
767
799
|
const runUsage = () => usageBreakdown(promptParts, stats.cacheTokens, model);
|
|
768
800
|
const denied = new Set();
|
|
801
|
+
const failed = new Map();
|
|
802
|
+
let blockedRepeats = 0;
|
|
769
803
|
let webSearches = 0;
|
|
770
804
|
const sources = trustedSourcesFrom(messages);
|
|
771
805
|
if (preloadedSkill)
|
|
@@ -943,7 +977,7 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
|
|
|
943
977
|
}
|
|
944
978
|
mute = 0;
|
|
945
979
|
narrated = false;
|
|
946
|
-
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 });
|
|
947
981
|
if (signal?.aborted)
|
|
948
982
|
throw new AbortedError();
|
|
949
983
|
history = [
|
|
@@ -954,10 +988,12 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
|
|
|
954
988
|
}
|
|
955
989
|
];
|
|
956
990
|
let cappedCalls = 0;
|
|
991
|
+
let progressed = false;
|
|
957
992
|
runOutcomes.beginRound();
|
|
958
993
|
for (const { call, tool, args, rawCall, rejection, intent } of resolved) {
|
|
959
994
|
if (signal?.aborted)
|
|
960
995
|
throw new AbortedError();
|
|
996
|
+
const priorFailure = failed.get(callKey(call.name, args));
|
|
961
997
|
const searchCapped = tool?.name === WEB_SEARCH_TOOL_NAME && webSearches >= MAX_WEB_SEARCHES_PER_TURN;
|
|
962
998
|
const callCtx = {
|
|
963
999
|
...toolContext(turnImages, ctxSize),
|
|
@@ -975,7 +1011,7 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
|
|
|
975
1011
|
const resource = tool?.resource?.(args) ?? null;
|
|
976
1012
|
const skillName = unquoteArg(args?.name ?? args?.skill_name ?? args?.skill);
|
|
977
1013
|
const skillReloaded = tool?.name === SKILL_TOOL_NAME && !args?.file && loadedThisTurn.has(skillName);
|
|
978
|
-
const silent = searchCapped || skillReloaded;
|
|
1014
|
+
const silent = searchCapped || skillReloaded || priorFailure !== undefined;
|
|
979
1015
|
const display = silent
|
|
980
1016
|
? undefined
|
|
981
1017
|
: labelOf(call.name, args, skillCall ? skillEmoji(skillName) : skillIcon);
|
|
@@ -989,7 +1025,15 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
|
|
|
989
1025
|
const unreadyReason = `skill "${skillName}" is ${unreadyState ?? 'unavailable'} and cannot be used yet. ` +
|
|
990
1026
|
`Tell the user to open it in Skills and finish its setup; do not retry.`;
|
|
991
1027
|
let execution;
|
|
992
|
-
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) {
|
|
993
1037
|
execution = terminalCall({
|
|
994
1038
|
name: skillName,
|
|
995
1039
|
content: `${skillName} is already loaded. Follow the instructions you were given and do not load it again.`
|
|
@@ -1037,13 +1081,20 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
|
|
|
1037
1081
|
});
|
|
1038
1082
|
const output = settled.result;
|
|
1039
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
|
+
}
|
|
1040
1090
|
runOutcomes.record(output, executionOutcome);
|
|
1041
1091
|
if (searchCapped) {
|
|
1042
1092
|
cappedCalls++;
|
|
1043
1093
|
hlog('web_search capped', { searches: webSearches });
|
|
1044
1094
|
}
|
|
1045
|
-
else if (tool?.name === WEB_SEARCH_TOOL_NAME && !rejection)
|
|
1095
|
+
else if (tool?.name === WEB_SEARCH_TOOL_NAME && !rejection && !priorFailure) {
|
|
1046
1096
|
webSearches++;
|
|
1097
|
+
}
|
|
1047
1098
|
if (call.name === SKILL_TOOL_NAME &&
|
|
1048
1099
|
output &&
|
|
1049
1100
|
typeof output === 'object' &&
|
|
@@ -1095,7 +1146,13 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
|
|
|
1095
1146
|
history.push({ role: 'tool', content: turn });
|
|
1096
1147
|
}
|
|
1097
1148
|
runOutcomes.finishRound();
|
|
1149
|
+
if (progressed) {
|
|
1150
|
+
failed.clear();
|
|
1151
|
+
blockedRepeats = 0;
|
|
1152
|
+
}
|
|
1098
1153
|
history.push({ role: 'user', content: TOOL_CONTINUATION_NUDGE });
|
|
1154
|
+
if (blockedRepeats >= MAX_BLOCKED_REPEATS)
|
|
1155
|
+
break;
|
|
1099
1156
|
if (cappedCalls === resolved.length) {
|
|
1100
1157
|
if (cappedRounds++ >= MAX_CAPPED_ROUNDS) {
|
|
1101
1158
|
outcome = 'search-capped';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface DetectedLanguage {
|
|
2
|
+
code: string;
|
|
3
|
+
language: string;
|
|
4
|
+
probability?: number;
|
|
5
|
+
}
|
|
6
|
+
export interface DetectLanguageContext {
|
|
7
|
+
text: string;
|
|
8
|
+
topK?: number;
|
|
9
|
+
}
|
|
10
|
+
export declare const UNDETERMINED = "und";
|
|
11
|
+
export declare function detectLanguage({ text, topK }: DetectLanguageContext): DetectedLanguage[];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { detectMultiple, detectOne } from '@qvac/langdetect-text';
|
|
2
|
+
export const UNDETERMINED = 'und';
|
|
3
|
+
export function detectLanguage({ text, topK }) {
|
|
4
|
+
if (!topK || topK === 1) {
|
|
5
|
+
const { code, language } = detectOne(text);
|
|
6
|
+
return [{ code, language }];
|
|
7
|
+
}
|
|
8
|
+
return detectMultiple(text, topK).map(({ code, language, probability }) => ({
|
|
9
|
+
code,
|
|
10
|
+
language,
|
|
11
|
+
probability
|
|
12
|
+
}));
|
|
13
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CompletionEvent, ToolDefinition, ToolExecuteContext } from './completion.ts';
|
|
2
|
+
declare const CAPABILITY_TOOLS: {
|
|
3
|
+
readonly image: 'generate_image';
|
|
4
|
+
};
|
|
5
|
+
export type Capability = keyof typeof CAPABILITY_TOOLS;
|
|
6
|
+
export declare const CAPABILITIES: Capability[];
|
|
7
|
+
export declare function isCapability(name: string): name is Capability;
|
|
8
|
+
export interface InvokeOptions {
|
|
9
|
+
capability: Capability;
|
|
10
|
+
args: Record<string, unknown>;
|
|
11
|
+
tools: readonly ToolDefinition[];
|
|
12
|
+
ctx: ToolExecuteContext;
|
|
13
|
+
}
|
|
14
|
+
export declare function invoke({ capability, args, tools, ctx }: InvokeOptions): AsyncGenerator<CompletionEvent>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { AbortedError } from '../helpers/race-abort.js';
|
|
2
|
+
const CAPABILITY_TOOLS = {
|
|
3
|
+
image: 'generate_image'
|
|
4
|
+
};
|
|
5
|
+
export const CAPABILITIES = Object.keys(CAPABILITY_TOOLS);
|
|
6
|
+
export function isCapability(name) {
|
|
7
|
+
return name in CAPABILITY_TOOLS;
|
|
8
|
+
}
|
|
9
|
+
export async function* invoke({ capability, args, tools, ctx }) {
|
|
10
|
+
const name = CAPABILITY_TOOLS[capability];
|
|
11
|
+
const tool = tools.find((candidate) => candidate.name === name);
|
|
12
|
+
if (!tool) {
|
|
13
|
+
yield { type: 'error', message: `${capability} is not available on this device` };
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const startedAt = Date.now();
|
|
17
|
+
yield { type: 'tool-call', name, args, effect: tool.effect(args, ctx) };
|
|
18
|
+
try {
|
|
19
|
+
const result = await tool.execute(args, ctx);
|
|
20
|
+
if (ctx.signal?.aborted)
|
|
21
|
+
return yield { type: 'aborted' };
|
|
22
|
+
yield { type: 'tool-result', name, result };
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
if (err instanceof AbortedError || ctx.signal?.aborted) {
|
|
26
|
+
yield { type: 'aborted' };
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
yield { type: 'error', message: err instanceof Error ? err.message : String(err) };
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
yield { type: 'metrics', totalTokens: 0, durationMs: Date.now() - startedAt };
|
|
33
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import EventEmitter from 'events';
|
|
2
2
|
import { type Transport } from './rpc-session.ts';
|
|
3
3
|
import { type KvCacheDeletionResult } from './capabilities/kv-cache.ts';
|
|
4
|
-
import type { CapabilityInfo, CompletionContext, DownloadContext, EmbedContext, HarnessContract, HarnessDiagnostics, HarnessEvent, HarnessInfo, ListModelsOptions, ModelInfo, ResolvedSkillCatalogEntry, SpeakContext, SpeakResult, SpeakStreamContext, ToolCatalogEntry, TranscribeContext, WarmModelContext } from './contract-types.ts';
|
|
4
|
+
import type { CapabilityInfo, CompletionContext, DetectedLanguage, DetectLanguageContext, DownloadContext, EmbedContext, HarnessContract, HarnessDiagnostics, HarnessEvent, HarnessInfo, InvokeContext, ListModelsOptions, ModelInfo, ResolvedSkillCatalogEntry, SpeakContext, SpeakResult, SpeakStreamContext, ToolCatalogEntry, TranscribeContext, WarmModelContext } from './contract-types.ts';
|
|
5
5
|
import { type EmbedDevice } from './models/embed-config.ts';
|
|
6
6
|
import type { DeleteModelSummary } from './models/model-delete.ts';
|
|
7
7
|
import type { ManagedSkill, SkillFileContent } from './tools/skills/overlay-crud.ts';
|
|
@@ -20,9 +20,12 @@ export declare class RemoteHarness extends EventEmitter implements HarnessContra
|
|
|
20
20
|
private _ensure;
|
|
21
21
|
private _open;
|
|
22
22
|
completion(context: CompletionContext): AsyncIterable<HarnessEvent>;
|
|
23
|
+
invoke(context: InvokeContext): AsyncIterable<HarnessEvent>;
|
|
24
|
+
private _run;
|
|
23
25
|
download(context: DownloadContext): AsyncIterable<HarnessEvent>;
|
|
24
26
|
embed(context: EmbedContext): Promise<number[][]>;
|
|
25
27
|
transcribe(context: TranscribeContext): Promise<string>;
|
|
28
|
+
detectLanguage(context: DetectLanguageContext): Promise<DetectedLanguage[]>;
|
|
26
29
|
speak(context: SpeakContext): Promise<SpeakResult>;
|
|
27
30
|
speakStream(context: SpeakStreamContext): AsyncIterable<SpeakResult>;
|
|
28
31
|
getEmbeddingDevice(): Promise<EmbedDevice>;
|
package/dist-lib/lib/connect.js
CHANGED
|
@@ -91,6 +91,44 @@ export class RemoteHarness extends EventEmitter {
|
|
|
91
91
|
return session;
|
|
92
92
|
}
|
|
93
93
|
async *completion(context) {
|
|
94
|
+
yield* this._run({
|
|
95
|
+
type: 'start',
|
|
96
|
+
model: context.model,
|
|
97
|
+
agent: knobs(context.agent),
|
|
98
|
+
cacheKey: context.cacheKey,
|
|
99
|
+
thinkingDefault: context.thinkingDefault,
|
|
100
|
+
generationParams: context.generationParams,
|
|
101
|
+
responseFormat: context.responseFormat,
|
|
102
|
+
captureInferenceCalls: context.captureInferenceCalls,
|
|
103
|
+
messages: context.messages.map(({ role, content, attachments }) => ({
|
|
104
|
+
role,
|
|
105
|
+
content,
|
|
106
|
+
attachments: attachments?.map(({ path: file }) => ({ path: file }))
|
|
107
|
+
})),
|
|
108
|
+
toolConfigs: context.toolConfigs,
|
|
109
|
+
grantedDirs: context.grantedDirs,
|
|
110
|
+
grantedDrives: context.grantedDrives,
|
|
111
|
+
grantedWriteDirs: context.grantedWriteDirs
|
|
112
|
+
}, context);
|
|
113
|
+
}
|
|
114
|
+
async *invoke(context) {
|
|
115
|
+
const agent = context.agent ?? {};
|
|
116
|
+
yield* this._run({
|
|
117
|
+
type: 'invoke',
|
|
118
|
+
capability: context.capability,
|
|
119
|
+
arguments: context.args,
|
|
120
|
+
agent: knobs(agent)
|
|
121
|
+
}, {
|
|
122
|
+
model: '',
|
|
123
|
+
agent,
|
|
124
|
+
messages: [],
|
|
125
|
+
attach: context.attach,
|
|
126
|
+
fetchAttachment: context.fetchAttachment,
|
|
127
|
+
progress: context.progress,
|
|
128
|
+
signal: context.signal
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
async *_run(open, context) {
|
|
94
132
|
const { client } = await this._ensure();
|
|
95
133
|
let duplex = null;
|
|
96
134
|
let emittedAborted = false;
|
|
@@ -104,32 +142,14 @@ export class RemoteHarness extends EventEmitter {
|
|
|
104
142
|
const onAbort = () => duplex?.destroy();
|
|
105
143
|
try {
|
|
106
144
|
duplex = client.completion();
|
|
107
|
-
duplex.write(
|
|
108
|
-
type: 'start',
|
|
109
|
-
model: context.model,
|
|
110
|
-
agent: knobs(context.agent),
|
|
111
|
-
cacheKey: context.cacheKey,
|
|
112
|
-
thinkingDefault: context.thinkingDefault,
|
|
113
|
-
generationParams: context.generationParams,
|
|
114
|
-
responseFormat: context.responseFormat,
|
|
115
|
-
captureInferenceCalls: context.captureInferenceCalls,
|
|
116
|
-
messages: context.messages.map(({ role, content, attachments }) => ({
|
|
117
|
-
role,
|
|
118
|
-
content,
|
|
119
|
-
attachments: attachments?.map(({ path: file }) => ({ path: file }))
|
|
120
|
-
})),
|
|
121
|
-
toolConfigs: context.toolConfigs,
|
|
122
|
-
grantedDirs: context.grantedDirs,
|
|
123
|
-
grantedDrives: context.grantedDrives,
|
|
124
|
-
grantedWriteDirs: context.grantedWriteDirs
|
|
125
|
-
});
|
|
145
|
+
duplex.write(open);
|
|
126
146
|
if (context.signal?.aborted) {
|
|
127
147
|
duplex.destroy();
|
|
128
148
|
yield { type: 'aborted' };
|
|
129
149
|
return;
|
|
130
150
|
}
|
|
131
151
|
context.signal?.addEventListener?.('abort', onAbort, { once: true });
|
|
132
|
-
hlog('client wire open', { model: context.model });
|
|
152
|
+
hlog('client wire open', { model: context.model, capability: open.capability });
|
|
133
153
|
for await (const event of frames(duplex)) {
|
|
134
154
|
received++;
|
|
135
155
|
if (event.type !== 'content' && event.type !== 'thinking') {
|
|
@@ -288,7 +308,7 @@ export class RemoteHarness extends EventEmitter {
|
|
|
288
308
|
throw died(`harness died mid-run: ${err.message}`);
|
|
289
309
|
}
|
|
290
310
|
finally {
|
|
291
|
-
hlog('client
|
|
311
|
+
hlog('client run end', { received, aborted: Boolean(context.signal?.aborted) });
|
|
292
312
|
context.signal?.removeEventListener?.('abort', onAbort);
|
|
293
313
|
duplex?.destroy();
|
|
294
314
|
closed = true;
|
|
@@ -350,6 +370,14 @@ export class RemoteHarness extends EventEmitter {
|
|
|
350
370
|
const response = await abortable(client.transcribe({ model: context.model, audio: context.audio }), context.signal);
|
|
351
371
|
return response.text;
|
|
352
372
|
}
|
|
373
|
+
async detectLanguage(context) {
|
|
374
|
+
const { client } = await this._ensure();
|
|
375
|
+
const response = await client.detectLanguage({
|
|
376
|
+
text: context.text,
|
|
377
|
+
topK: context.topK
|
|
378
|
+
});
|
|
379
|
+
return response.languages.map(({ code, language, probability }) => probability ? { code, language, probability } : { code, language });
|
|
380
|
+
}
|
|
353
381
|
async speak(context) {
|
|
354
382
|
const { client } = await this._ensure();
|
|
355
383
|
const response = await abortable(client.speak({ model: context.model, text: context.text }), context.signal);
|
|
@@ -656,7 +684,8 @@ function knobs(agent) {
|
|
|
656
684
|
imageWidth: agent.imageWidth ?? undefined,
|
|
657
685
|
imageHeight: agent.imageHeight ?? undefined,
|
|
658
686
|
approvalMode: agent.approvalMode ?? undefined,
|
|
659
|
-
disabledToolsets: agent.disabledToolsets ?? undefined
|
|
687
|
+
disabledToolsets: agent.disabledToolsets ?? undefined,
|
|
688
|
+
sandboxMode: agent.sandboxMode ?? undefined
|
|
660
689
|
};
|
|
661
690
|
}
|
|
662
691
|
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;
|
|
@@ -67,6 +68,24 @@ export interface CompletionContext {
|
|
|
67
68
|
removeEventListener?: (type: 'abort', fn: () => void) => void;
|
|
68
69
|
};
|
|
69
70
|
}
|
|
71
|
+
export interface InvokeContext {
|
|
72
|
+
capability: string;
|
|
73
|
+
args: Record<string, unknown>;
|
|
74
|
+
agent?: AgentKnobs;
|
|
75
|
+
attach?: AttachFn;
|
|
76
|
+
fetchAttachment?: FetchAttachmentFn;
|
|
77
|
+
progress?: ProgressFn;
|
|
78
|
+
signal?: CompletionContext['signal'];
|
|
79
|
+
}
|
|
80
|
+
export interface DetectLanguageContext {
|
|
81
|
+
text: string;
|
|
82
|
+
topK?: number;
|
|
83
|
+
}
|
|
84
|
+
export interface DetectedLanguage {
|
|
85
|
+
code: string;
|
|
86
|
+
language: string;
|
|
87
|
+
probability?: number;
|
|
88
|
+
}
|
|
70
89
|
export interface HarnessEvent {
|
|
71
90
|
type: string;
|
|
72
91
|
text?: string;
|
|
@@ -145,10 +164,12 @@ export interface CapabilityInfo {
|
|
|
145
164
|
}
|
|
146
165
|
export interface HarnessContract {
|
|
147
166
|
completion(context: CompletionContext): AsyncIterable<HarnessEvent>;
|
|
167
|
+
invoke?(context: InvokeContext): AsyncIterable<HarnessEvent>;
|
|
148
168
|
download?(context: DownloadContext): AsyncIterable<HarnessEvent>;
|
|
149
169
|
describe?(): Promise<HarnessInfo | null> | HarnessInfo;
|
|
150
170
|
embed?(context: EmbedContext): Promise<number[][]>;
|
|
151
171
|
transcribe?(context: TranscribeContext): Promise<string>;
|
|
172
|
+
detectLanguage?(context: DetectLanguageContext): DetectedLanguage[] | Promise<DetectedLanguage[]>;
|
|
152
173
|
speak?(context: SpeakContext): Promise<SpeakResult>;
|
|
153
174
|
speakStream?(context: SpeakStreamContext): AsyncIterable<SpeakResult>;
|
|
154
175
|
getEmbeddingDevice?(): EmbedDevice | Promise<EmbedDevice>;
|
|
@@ -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
|
}
|