@qvac/harness 0.2.10 → 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/dist/harness.bundle +10 -9
- package/dist-lib/index.d.ts +16 -1
- package/dist-lib/index.js +31 -1
- 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 +49 -21
- package/dist-lib/lib/contract-types.d.ts +20 -0
- package/dist-lib/lib/hooks/built-in/sandbox.js +3 -1
- package/dist-lib/lib/serve.js +32 -4
- package/dist-lib/lib/tools/http-request.js +1 -1
- 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/web-fetch.js +12 -2
- package/dist-lib/schema/harness.js +52 -3
- 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 +201 -10
- package/dist-lib/spec/hyperschema/index.js +201 -10
- package/dist-lib/spec/hyperschema/schema.json +158 -1
- package/dist-lib/spec/hyperschema/types.d.ts +42 -2
- package/package.json +3 -2
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)
|
|
@@ -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);
|
|
@@ -68,6 +68,24 @@ export interface CompletionContext {
|
|
|
68
68
|
removeEventListener?: (type: 'abort', fn: () => void) => void;
|
|
69
69
|
};
|
|
70
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
|
+
}
|
|
71
89
|
export interface HarnessEvent {
|
|
72
90
|
type: string;
|
|
73
91
|
text?: string;
|
|
@@ -146,10 +164,12 @@ export interface CapabilityInfo {
|
|
|
146
164
|
}
|
|
147
165
|
export interface HarnessContract {
|
|
148
166
|
completion(context: CompletionContext): AsyncIterable<HarnessEvent>;
|
|
167
|
+
invoke?(context: InvokeContext): AsyncIterable<HarnessEvent>;
|
|
149
168
|
download?(context: DownloadContext): AsyncIterable<HarnessEvent>;
|
|
150
169
|
describe?(): Promise<HarnessInfo | null> | HarnessInfo;
|
|
151
170
|
embed?(context: EmbedContext): Promise<number[][]>;
|
|
152
171
|
transcribe?(context: TranscribeContext): Promise<string>;
|
|
172
|
+
detectLanguage?(context: DetectLanguageContext): DetectedLanguage[] | Promise<DetectedLanguage[]>;
|
|
153
173
|
speak?(context: SpeakContext): Promise<SpeakResult>;
|
|
154
174
|
speakStream?(context: SpeakStreamContext): AsyncIterable<SpeakResult>;
|
|
155
175
|
getEmbeddingDevice?(): EmbedDevice | Promise<EmbedDevice>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { planVendoredInstall } from '../../tools/exec/adapters/python/wheels.js';
|
|
1
2
|
import { bareBin, firstCommandToken } from '../../tools/exec/scope.js';
|
|
2
3
|
const NETWORK_BINS = new Set([
|
|
3
4
|
'curl',
|
|
@@ -32,7 +33,8 @@ export class SandboxHook {
|
|
|
32
33
|
return;
|
|
33
34
|
if (!ctx.sandboxMode || ctx.toolName !== 'exec')
|
|
34
35
|
return;
|
|
35
|
-
|
|
36
|
+
const packages = ctx.args.packages;
|
|
37
|
+
if (Array.isArray(packages) && packages.length && !planVendoredInstall(packages.map(String))) {
|
|
36
38
|
return { error: 'sandbox mode: installing packages reaches the network', guidance: true };
|
|
37
39
|
}
|
|
38
40
|
const bin = bareBin(firstCommandToken(String(ctx.args.command ?? '')).replace(/^.*[/\\]/, ''));
|
package/dist-lib/lib/serve.js
CHANGED
|
@@ -4,6 +4,7 @@ import { settleOnClose } from './rpc-session.js';
|
|
|
4
4
|
import { pipeline, Readable } from 'streamx';
|
|
5
5
|
import { bindApi } from '../spec/bind.js';
|
|
6
6
|
import HRPC from '../spec/hrpc/index.js';
|
|
7
|
+
import { isCapability } from './capabilities/invoke.js';
|
|
7
8
|
import { unsupportedKvCacheDeletion } from './capabilities/kv-cache.js';
|
|
8
9
|
import { errorMessage } from './error-fields.js';
|
|
9
10
|
import { hlog } from './log.js';
|
|
@@ -11,7 +12,7 @@ import { pushQueue } from './queue.js';
|
|
|
11
12
|
import { TextCoalescer } from './helpers/text-coalescer.js';
|
|
12
13
|
import { prepareOpenclCache } from './models/opencl-cache.js';
|
|
13
14
|
import { stageFile, sweepStaging } from './staging.js';
|
|
14
|
-
import { readSetup } from './tools/skills/manifest-parse.js';
|
|
15
|
+
import { readSetup, toWireSetup } from './tools/skills/manifest-parse.js';
|
|
15
16
|
import { packVectors, toDownloadWire, toWire } from './wire.js';
|
|
16
17
|
const MODEL_STATUSES = new Set([
|
|
17
18
|
'loading',
|
|
@@ -96,6 +97,15 @@ export function serveHarness(stream, harness) {
|
|
|
96
97
|
text: await harness.transcribe({ model: model ?? '', audio: audio ?? Buffer.alloc(0) })
|
|
97
98
|
};
|
|
98
99
|
},
|
|
100
|
+
detectLanguage: async ({ text, topK }) => {
|
|
101
|
+
if (!harness.detectLanguage)
|
|
102
|
+
throw new Error('harness does not support detectLanguage');
|
|
103
|
+
const languages = await harness.detectLanguage({
|
|
104
|
+
text: text ?? '',
|
|
105
|
+
topK: topK ?? undefined
|
|
106
|
+
});
|
|
107
|
+
return { languages };
|
|
108
|
+
},
|
|
99
109
|
speak: async ({ model, text }) => {
|
|
100
110
|
if (!harness.speak)
|
|
101
111
|
throw new Error('harness does not support speak');
|
|
@@ -159,7 +169,7 @@ export function serveHarness(stream, harness) {
|
|
|
159
169
|
});
|
|
160
170
|
}
|
|
161
171
|
function toWireSkill(skill) {
|
|
162
|
-
const setupV2 = readSetup(skill.setup);
|
|
172
|
+
const setupV2 = toWireSetup(readSetup(skill.setup));
|
|
163
173
|
const binaries = (skill.requires?.bins ?? []).map((bin) => ({
|
|
164
174
|
bin,
|
|
165
175
|
minVersion: skill.requires?.binMinVersions?.[bin]?.min,
|
|
@@ -275,6 +285,24 @@ function speakStream(harness) {
|
|
|
275
285
|
}
|
|
276
286
|
};
|
|
277
287
|
}
|
|
288
|
+
function runFrame(harness, frame, context) {
|
|
289
|
+
if (frame.type !== 'invoke')
|
|
290
|
+
return harness.completion(context);
|
|
291
|
+
if (!harness.invoke)
|
|
292
|
+
throw new Error('harness does not support invoke');
|
|
293
|
+
const capability = frame.capability ?? '';
|
|
294
|
+
if (!isCapability(capability))
|
|
295
|
+
throw new Error(`unknown capability: ${capability}`);
|
|
296
|
+
return harness.invoke({
|
|
297
|
+
capability,
|
|
298
|
+
args: frame.arguments ?? {},
|
|
299
|
+
agent: frame.agent ?? undefined,
|
|
300
|
+
attach: context.attach,
|
|
301
|
+
fetchAttachment: context.fetchAttachment,
|
|
302
|
+
progress: context.progress,
|
|
303
|
+
signal: context.signal
|
|
304
|
+
});
|
|
305
|
+
}
|
|
278
306
|
function completion(harness) {
|
|
279
307
|
return async function* (duplex) {
|
|
280
308
|
const events = pushQueue({ highWaterMark: WIRE_HIGH_WATER_MARK });
|
|
@@ -404,7 +432,7 @@ function completion(harness) {
|
|
|
404
432
|
waiter.resolve((frame.payload ?? {}));
|
|
405
433
|
return;
|
|
406
434
|
}
|
|
407
|
-
if (frame.type !== 'start'
|
|
435
|
+
if (started || (frame.type !== 'start' && frame.type !== 'invoke'))
|
|
408
436
|
return;
|
|
409
437
|
started = true;
|
|
410
438
|
const context = {
|
|
@@ -523,7 +551,7 @@ function completion(harness) {
|
|
|
523
551
|
return events.push({ type, text: merged });
|
|
524
552
|
}, { cap: TEXT_COALESCE_CAP, holdMs: TEXT_COALESCE_MS });
|
|
525
553
|
try {
|
|
526
|
-
for await (const event of harness
|
|
554
|
+
for await (const event of runFrame(harness, frame, context)) {
|
|
527
555
|
produced++;
|
|
528
556
|
const wire = toWire(event);
|
|
529
557
|
if ((wire.type === 'content' || wire.type === 'thinking') && wire.text) {
|
|
@@ -14,7 +14,7 @@ export function createHttpRequestTool({ fetch: fetchImpl, credentialStore, crede
|
|
|
14
14
|
return {
|
|
15
15
|
name: 'http_request',
|
|
16
16
|
toolset: 'http-request',
|
|
17
|
-
description: 'Make an outbound HTTP or HTTPS request and return the response status and body (truncated). Authorization/Cookie headers are stripped before sending. Stored skill credentials are attached automatically for their allow-listed hosts; `auth` is only needed to force a specific credential key.',
|
|
17
|
+
description: 'Make an outbound HTTP or HTTPS request and return the response status and body (truncated) as text. Authorization/Cookie headers are stripped before sending. Stored skill credentials are attached automatically for their allow-listed hosts; `auth` is only needed to force a specific credential key. For APIs and text responses only: a URL that serves a file — PDF, .docx, .xlsx, an image, any binary — comes back as truncated garbage here. Use web_fetch for those; it downloads and attaches the file.',
|
|
18
18
|
parameters: {
|
|
19
19
|
type: 'object',
|
|
20
20
|
properties: {
|
|
@@ -10,3 +10,4 @@ export interface ParsedManifest {
|
|
|
10
10
|
export declare function parseManifestBlock(block: string, key?: string): ParsedManifest;
|
|
11
11
|
export declare function readInstall(v: unknown): InstallStep[] | undefined;
|
|
12
12
|
export declare function readSetup(v: unknown): SkillSetup | undefined;
|
|
13
|
+
export declare function toWireSetup(setup: SkillSetup | undefined): unknown;
|
|
@@ -165,6 +165,9 @@ function readRoutes(v) {
|
|
|
165
165
|
const steps = strArr(raw.steps);
|
|
166
166
|
const configSteps = strArr(raw.configSteps);
|
|
167
167
|
const fields = readFields(raw.fields);
|
|
168
|
+
const oauth = readOAuth(raw.oauth);
|
|
169
|
+
if (raw.kind === 'oauth' && !oauth)
|
|
170
|
+
continue;
|
|
168
171
|
routes.push({
|
|
169
172
|
kind: raw.kind,
|
|
170
173
|
label: raw.label,
|
|
@@ -175,11 +178,99 @@ function readRoutes(v) {
|
|
|
175
178
|
...(steps ? { steps } : {}),
|
|
176
179
|
...(configSteps ? { configSteps } : {}),
|
|
177
180
|
...(fields ? { fields } : {}),
|
|
178
|
-
...(typeof raw.satisfied === 'boolean' ? { satisfied: raw.satisfied } : {})
|
|
181
|
+
...(typeof raw.satisfied === 'boolean' ? { satisfied: raw.satisfied } : {}),
|
|
182
|
+
...(oauth ? { oauth } : {})
|
|
179
183
|
});
|
|
180
184
|
}
|
|
181
185
|
return routes.length > 0 ? routes : undefined;
|
|
182
186
|
}
|
|
187
|
+
const OAUTH_TOKEN_AUTH = new Set(['pkce-only', 'secret-in-body', 'secret-in-basic']);
|
|
188
|
+
const OAUTH_RESERVED_PARAMS = new Set([
|
|
189
|
+
'client_id',
|
|
190
|
+
'client_secret',
|
|
191
|
+
'redirect_uri',
|
|
192
|
+
'response_type',
|
|
193
|
+
'state',
|
|
194
|
+
'scope',
|
|
195
|
+
'code_challenge',
|
|
196
|
+
'code_challenge_method'
|
|
197
|
+
]);
|
|
198
|
+
function isOAuthTokenAuth(v) {
|
|
199
|
+
return typeof v === 'string' && OAUTH_TOKEN_AUTH.has(v);
|
|
200
|
+
}
|
|
201
|
+
function httpsUrl(v) {
|
|
202
|
+
if (typeof v !== 'string' || v.length === 0)
|
|
203
|
+
return undefined;
|
|
204
|
+
try {
|
|
205
|
+
return new URL(v).protocol === 'https:' ? v : undefined;
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function isPort(v) {
|
|
212
|
+
return typeof v === 'number' && Number.isInteger(v) && v > 0 && v < 65536;
|
|
213
|
+
}
|
|
214
|
+
function readAuthParams(v) {
|
|
215
|
+
const raw = Array.isArray(v)
|
|
216
|
+
? Object.fromEntries(v.flatMap((pair) => isObj(pair) && typeof pair.key === 'string' && typeof pair.value === 'string'
|
|
217
|
+
? [[pair.key, pair.value]]
|
|
218
|
+
: []))
|
|
219
|
+
: v;
|
|
220
|
+
const record = readStringRecord(raw);
|
|
221
|
+
if (!record)
|
|
222
|
+
return undefined;
|
|
223
|
+
const out = Object.fromEntries(Object.entries(record).filter(([key]) => !OAUTH_RESERVED_PARAMS.has(key)));
|
|
224
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
225
|
+
}
|
|
226
|
+
function readOAuth(v) {
|
|
227
|
+
if (!isObj(v))
|
|
228
|
+
return undefined;
|
|
229
|
+
const authUrl = httpsUrl(v.authUrl);
|
|
230
|
+
const tokenUrl = httpsUrl(v.tokenUrl);
|
|
231
|
+
if (typeof v.stateKey !== 'string' ||
|
|
232
|
+
v.stateKey.length === 0 ||
|
|
233
|
+
!authUrl ||
|
|
234
|
+
!tokenUrl ||
|
|
235
|
+
!isOAuthTokenAuth(v.tokenAuth)) {
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
const scopes = v.scopes === undefined || v.scopes === null ? [] : strArr(v.scopes);
|
|
239
|
+
if (!scopes)
|
|
240
|
+
return undefined;
|
|
241
|
+
const extraAuthParams = readAuthParams(v.extraAuthParams);
|
|
242
|
+
const registrationEndpoint = httpsUrl(v.registrationEndpoint);
|
|
243
|
+
return {
|
|
244
|
+
stateKey: v.stateKey,
|
|
245
|
+
authUrl,
|
|
246
|
+
tokenUrl,
|
|
247
|
+
scopes,
|
|
248
|
+
tokenAuth: v.tokenAuth,
|
|
249
|
+
...(extraAuthParams ? { extraAuthParams } : {}),
|
|
250
|
+
...(registrationEndpoint ? { registrationEndpoint } : {}),
|
|
251
|
+
...(isPort(v.port) ? { port: v.port } : {}),
|
|
252
|
+
...(typeof v.sharedIosSession === 'boolean' ? { sharedIosSession: v.sharedIosSession } : {})
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
export function toWireSetup(setup) {
|
|
256
|
+
if (!setup?.routes)
|
|
257
|
+
return setup;
|
|
258
|
+
return {
|
|
259
|
+
...setup,
|
|
260
|
+
routes: setup.routes.map((route) => route.oauth?.extraAuthParams
|
|
261
|
+
? {
|
|
262
|
+
...route,
|
|
263
|
+
oauth: {
|
|
264
|
+
...route.oauth,
|
|
265
|
+
extraAuthParams: Object.entries(route.oauth.extraAuthParams).map(([key, value]) => ({
|
|
266
|
+
key,
|
|
267
|
+
value
|
|
268
|
+
}))
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
: route)
|
|
272
|
+
};
|
|
273
|
+
}
|
|
183
274
|
function readFields(v) {
|
|
184
275
|
if (!Array.isArray(v))
|
|
185
276
|
return undefined;
|
|
@@ -29,6 +29,17 @@ export interface SkillSetupField {
|
|
|
29
29
|
readonly secret?: boolean;
|
|
30
30
|
readonly required?: boolean;
|
|
31
31
|
}
|
|
32
|
+
export interface SkillOAuthConfig {
|
|
33
|
+
readonly stateKey: string;
|
|
34
|
+
readonly authUrl: string;
|
|
35
|
+
readonly tokenUrl: string;
|
|
36
|
+
readonly scopes: readonly string[];
|
|
37
|
+
readonly tokenAuth: 'pkce-only' | 'secret-in-body' | 'secret-in-basic';
|
|
38
|
+
readonly extraAuthParams?: Readonly<Record<string, string>>;
|
|
39
|
+
readonly registrationEndpoint?: string;
|
|
40
|
+
readonly port?: number;
|
|
41
|
+
readonly sharedIosSession?: boolean;
|
|
42
|
+
}
|
|
32
43
|
export interface SkillSetupRoute {
|
|
33
44
|
readonly kind: 'oauth' | 'token' | 'install' | 'picker' | 'instructions';
|
|
34
45
|
readonly label: string;
|
|
@@ -40,6 +51,7 @@ export interface SkillSetupRoute {
|
|
|
40
51
|
readonly configSteps?: readonly string[];
|
|
41
52
|
readonly fields?: readonly SkillSetupField[];
|
|
42
53
|
readonly satisfied?: boolean;
|
|
54
|
+
readonly oauth?: SkillOAuthConfig;
|
|
43
55
|
}
|
|
44
56
|
export interface SkillSetup {
|
|
45
57
|
readonly summary?: string;
|
|
@@ -28,7 +28,11 @@ export function createWebFetchTool({ fetch: fetchImpl, resolveHost = lookupHost
|
|
|
28
28
|
toolset: 'web-search',
|
|
29
29
|
description: 'Fetch a public web page and return its readable text. ' +
|
|
30
30
|
'Use this to read the full content behind a web_search result URL, or a page the user ' +
|
|
31
|
-
'shares, when a search snippet is not enough to answer.'
|
|
31
|
+
'shares, when a search snippet is not enough to answer. ' +
|
|
32
|
+
'When the URL serves a file rather than a page — PDF, .docx, .xlsx, .pptx, an image, any ' +
|
|
33
|
+
'binary — this is also the tool that downloads it: the bytes become a chat attachment and ' +
|
|
34
|
+
'the result carries its attachmentId. It is the only way to get a remote file into the ' +
|
|
35
|
+
'chat; exec has no network and http_request truncates binary bodies.',
|
|
32
36
|
parameters: {
|
|
33
37
|
type: 'object',
|
|
34
38
|
properties: {
|
|
@@ -178,8 +182,14 @@ async function attachDocument(response, url, mimeType, attach) {
|
|
|
178
182
|
raw: !mime.startsWith('image/')
|
|
179
183
|
});
|
|
180
184
|
return {
|
|
185
|
+
attachmentId: record.id,
|
|
186
|
+
fileName: record.fileName,
|
|
187
|
+
byteLength: record.byteLength,
|
|
181
188
|
content: `Attached the file "${record.fileName}" (${Math.round(record.byteLength / 1024)}KB, ${mime}) from ${url.toString()} — ` +
|
|
182
|
-
'the user can open or save it. Its contents were NOT read
|
|
189
|
+
'the user can open or save it. Its contents were NOT read, so do not describe what is inside ' +
|
|
190
|
+
'yet. To read it, stage it with exec as ' +
|
|
191
|
+
`inputs [{ "attachmentId": "${record.id}", "path": "${record.fileName}" }] — never ask the ` +
|
|
192
|
+
'user to attach a file this call already attached.'
|
|
183
193
|
};
|
|
184
194
|
}
|
|
185
195
|
const MIME_EXTENSIONS = {
|