@qvac/harness 0.2.10 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/harness.bundle +13 -11
  2. package/dist-lib/index.d.ts +26 -1
  3. package/dist-lib/index.js +94 -3
  4. package/dist-lib/lib/capabilities/completion.d.ts +2 -0
  5. package/dist-lib/lib/capabilities/detect-language.d.ts +11 -0
  6. package/dist-lib/lib/capabilities/detect-language.js +13 -0
  7. package/dist-lib/lib/capabilities/enhance.d.ts +10 -0
  8. package/dist-lib/lib/capabilities/enhance.js +29 -0
  9. package/dist-lib/lib/capabilities/invoke.d.ts +24 -0
  10. package/dist-lib/lib/capabilities/invoke.js +57 -0
  11. package/dist-lib/lib/connect.d.ts +4 -1
  12. package/dist-lib/lib/connect.js +50 -21
  13. package/dist-lib/lib/contract-types.d.ts +21 -0
  14. package/dist-lib/lib/helpers/clean-answer.d.ts +1 -0
  15. package/dist-lib/lib/helpers/clean-answer.js +16 -0
  16. package/dist-lib/lib/helpers/load-stall.d.ts +6 -0
  17. package/dist-lib/lib/helpers/load-stall.js +24 -0
  18. package/dist-lib/lib/hooks/built-in/sandbox.js +3 -1
  19. package/dist-lib/lib/models/model-pool.d.ts +6 -0
  20. package/dist-lib/lib/models/model-pool.js +12 -0
  21. package/dist-lib/lib/models/nmt-catalog.d.ts +34 -0
  22. package/dist-lib/lib/models/nmt-catalog.js +166 -0
  23. package/dist-lib/lib/serve.js +33 -4
  24. package/dist-lib/lib/tools/generate-image.js +2 -25
  25. package/dist-lib/lib/tools/http-request.js +1 -1
  26. package/dist-lib/lib/tools/skills/manifest-parse.d.ts +1 -0
  27. package/dist-lib/lib/tools/skills/manifest-parse.js +92 -1
  28. package/dist-lib/lib/tools/skills/manifest.d.ts +12 -0
  29. package/dist-lib/lib/tools/translate.d.ts +78 -0
  30. package/dist-lib/lib/tools/translate.js +228 -0
  31. package/dist-lib/lib/tools/web-fetch.js +12 -2
  32. package/dist-lib/schema/harness.js +53 -3
  33. package/dist-lib/spec/bind.js +3 -0
  34. package/dist-lib/spec/calls.js +1 -0
  35. package/dist-lib/spec/hrpc/hrpc.json +13 -0
  36. package/dist-lib/spec/hrpc/index.js +15 -3
  37. package/dist-lib/spec/hrpc/messages.js +206 -10
  38. package/dist-lib/spec/hyperschema/index.js +206 -10
  39. package/dist-lib/spec/hyperschema/schema.json +163 -1
  40. package/dist-lib/spec/hyperschema/types.d.ts +43 -2
  41. package/package.json +8 -2
@@ -1,6 +1,9 @@
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';
6
+ import { type EnhanceMode } from './lib/capabilities/enhance.ts';
4
7
  import { type DownloadArgs, type DownloadEventStream } from './lib/capabilities/download.ts';
5
8
  import { type EmbedContext } from './lib/capabilities/embed.ts';
6
9
  import { type KvCacheDeletionResult } from './lib/capabilities/kv-cache.ts';
@@ -22,6 +25,10 @@ import type { ResolvedSkillCatalogEntry } from './lib/tools/skills/catalog.ts';
22
25
  import { type SkillSettingEntry } from './lib/tools/skills/skill-settings.ts';
23
26
  export * from './lib/capabilities/completion.ts';
24
27
  export { attachmentTurn, loadedSkillsFrom, skillLoadedTurn } from './lib/capabilities/tool-turns.ts';
28
+ export { CAPABILITIES, isCapability, type Capability } from './lib/capabilities/invoke.ts';
29
+ export type { EnhanceMode } from './lib/capabilities/enhance.ts';
30
+ export { EURONANO_LANGUAGES, NMT_FROM_ENGLISH, NMT_TO_ENGLISH, SLM_LANGUAGE_CODES, SLM_LANGUAGE_NAMES, translationPlanFor, type TranslationPlan } from './lib/models/nmt-catalog.ts';
31
+ export type { DetectLanguageContext, DetectedLanguage } from './lib/capabilities/detect-language.ts';
25
32
  export * from './lib/capabilities/download.ts';
26
33
  export type { EmbedContext } from './lib/capabilities/embed.ts';
27
34
  export type { SpeakContext, SpeakResult, SpeakStreamContext, TranscribeContext } from './lib/capabilities/voice.ts';
@@ -37,7 +44,7 @@ export { requiredSettings, settingEnvName, type SkillSettingEntry, type SkillSet
37
44
  export type { ToolCatalogEntry } from './lib/tools/catalog.ts';
38
45
  export { TOOLSETS, toolsetEnabled, type Toolset } from './lib/tools/toolsets.ts';
39
46
  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';
47
+ export type { InstallStep, SkillOAuthConfig, SkillRequires, SkillSetup, SkillSetupField, SkillSetupRoute } from './lib/tools/skills/manifest.ts';
41
48
  export { readSetup as normalizeSkillSetup } from './lib/tools/skills/manifest-parse.ts';
42
49
  export type { CredentialRecord, CredentialStoreValue } from './lib/tools/skills/auth-resolver.ts';
43
50
  export { isAvailableState, SKILL_STATE, type SkillState } from './lib/tools/skills/skill-state.ts';
@@ -106,6 +113,16 @@ export interface CompletionContext {
106
113
  askUser?: AskUserFn;
107
114
  signal?: BareAbortSignal;
108
115
  }
116
+ export interface InvokeContext {
117
+ capability: Capability;
118
+ args: Record<string, unknown>;
119
+ enhance?: EnhanceMode;
120
+ agent?: Agent;
121
+ attach?: AttachFn;
122
+ fetchAttachment?: FetchAttachmentFn;
123
+ progress?: ProgressFn;
124
+ signal?: BareAbortSignal;
125
+ }
109
126
  export declare class Harness extends ReadyResource {
110
127
  private _tools;
111
128
  private _sdk;
@@ -144,7 +161,10 @@ export declare class Harness extends ReadyResource {
144
161
  private _diffusionSdk;
145
162
  private _imageRuntime;
146
163
  private _musicSdk;
164
+ private _translationSdk;
147
165
  private _musicRuntime;
166
+ private _translationRuntime;
167
+ private _capabilityTools;
148
168
  private _capability;
149
169
  private _capabilityInflight;
150
170
  private readonly _modelFaults;
@@ -197,6 +217,8 @@ export declare class Harness extends ReadyResource {
197
217
  private _loadedModels;
198
218
  private _loadPluginSdk;
199
219
  private _loadDiffusionSdk;
220
+ private _slmTranslate;
221
+ private _loadTranslationSdk;
200
222
  private _loadMusicSdk;
201
223
  private _deviceConfig;
202
224
  private _createPool;
@@ -224,6 +246,7 @@ export declare class Harness extends ReadyResource {
224
246
  private _defaultVoiceSrc;
225
247
  private _voiceJob;
226
248
  transcribe({ model, audio, signal }: TranscribeContext): Promise<string>;
249
+ detectLanguage(context: DetectLanguageContext): DetectedLanguage[];
227
250
  speak({ model, text, signal }: SpeakContext): Promise<SpeakResult>;
228
251
  speakStream({ model, text, signal }: SpeakStreamContext): AsyncIterable<SpeakResult>;
229
252
  list(options?: ListModelsOptions): Promise<ModelInfo[]>;
@@ -240,7 +263,9 @@ export declare class Harness extends ReadyResource {
240
263
  agent?: Agent;
241
264
  signal?: BareAbortSignal;
242
265
  }): Promise<void>;
266
+ invoke({ capability, args, enhance, agent, attach, fetchAttachment, progress, signal }: InvokeContext): AsyncGenerator<CompletionEvent>;
243
267
  completion({ model, agent, messages, cacheKey, thinkingDefault, toolConfigs, generationParams, responseFormat, captureInferenceCalls, grantedDirs, grantedDrives, driveOp, grantedWriteDirs, approve, attach, fetchAttachment, ragSearch, progress, askUser, signal }: CompletionContext): AsyncGenerator<CompletionEvent>;
268
+ private _residentEnhancer;
244
269
  private _makeRoomForCompletion;
245
270
  private _evictWedgedModel;
246
271
  private _evictAfterLeases;
package/dist-lib/index.js CHANGED
@@ -5,7 +5,13 @@ 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';
11
+ import { expandImagePrompt } from './lib/capabilities/enhance.js';
12
+ import { cleanAnswer } from './lib/helpers/clean-answer.js';
13
+ import { isTranslationModel } from './lib/models/nmt-catalog.js';
14
+ import { createTranslateTool, createTranslationRuntime } from './lib/tools/translate.js';
9
15
  import { download } from './lib/capabilities/download.js';
10
16
  import { createEmbedLane } from './lib/capabilities/embed.js';
11
17
  import { deleteAllKvCaches, deleteKvCacheKey } from './lib/capabilities/kv-cache.js';
@@ -17,7 +23,7 @@ import { AbortedError, raceAbort } from './lib/helpers/race-abort.js';
17
23
  import { audioGenDownloadMetadata, customProjectionMap, defaultProjectionMap, defaultTtsFor, defaultVoiceModel, diffusionDownloadMetadata, infraModelsFor, modelNameToCapabilityProfile, musicModelSet, setCustomModels } from './lib/models/model-catalog.js';
18
24
  import { createModelStatusCache, invalidateModelStatusCache, listModels } from './lib/models/model-list.js';
19
25
  import { deleteModelFiles, summarizeDelete } from './lib/models/model-delete.js';
20
- import { createEmbedPool, createLoadTracker, createModelPool, createVoicePool, awaitModelCapacity, evictIdle, evictModelWithin, isResident, leaseCount, loadedModels, modelKey, releaseModel, resolveLoadOptions, progressState } from './lib/models/model-pool.js';
26
+ import { createEmbedPool, createLoadTracker, createModelPool, createVoicePool, awaitModelCapacity, evictIdle, evictModelWithin, isResident, leaseResidentModel, leaseCount, loadedModels, modelKey, releaseModel, resolveLoadOptions, progressState } from './lib/models/model-pool.js';
21
27
  import { resolveMainGpu } from './lib/models/gpu-selection.js';
22
28
  import { openclCacheDir } from './lib/models/opencl-cache.js';
23
29
  import { assertEmbedDevice, createEmbedConfig } from './lib/models/embed-config.js';
@@ -45,6 +51,8 @@ import { isDeviceSharedCredentialKey, scopeKey } from './lib/tools/skills/scope-
45
51
  import { settingsScopeKey } from './lib/tools/skills/skill-settings.js';
46
52
  export * from './lib/capabilities/completion.js';
47
53
  export { attachmentTurn, loadedSkillsFrom, skillLoadedTurn } from './lib/capabilities/tool-turns.js';
54
+ export { CAPABILITIES, isCapability } from './lib/capabilities/invoke.js';
55
+ export { EURONANO_LANGUAGES, NMT_FROM_ENGLISH, NMT_TO_ENGLISH, SLM_LANGUAGE_CODES, SLM_LANGUAGE_NAMES, translationPlanFor } from './lib/models/nmt-catalog.js';
48
56
  export * from './lib/capabilities/download.js';
49
57
  export * from './lib/models/model-catalog.js';
50
58
  export * from './lib/models/model-list.js';
@@ -116,7 +124,10 @@ export class Harness extends ReadyResource {
116
124
  _diffusionSdk;
117
125
  _imageRuntime;
118
126
  _musicSdk;
127
+ _translationSdk;
119
128
  _musicRuntime;
129
+ _translationRuntime;
130
+ _capabilityTools;
120
131
  _capability = null;
121
132
  _capabilityInflight = null;
122
133
  _modelFaults = new Map();
@@ -201,6 +212,16 @@ export class Harness extends ReadyResource {
201
212
  }
202
213
  })
203
214
  : null;
215
+ this._translationRuntime = createTranslationRuntime({
216
+ loads: this._loads,
217
+ loadSdk: () => this._loadTranslationSdk(),
218
+ onProgress: (model, progress) => this.emit('model', progressState(model, progress))
219
+ });
220
+ this._capabilityTools = [
221
+ createTranslateTool(this._translationRuntime, {
222
+ slm: (...args) => this._slmTranslate(...args)
223
+ })
224
+ ];
204
225
  this._tools = [
205
226
  ...tools,
206
227
  ...(this._imageRuntime
@@ -220,6 +241,7 @@ export class Harness extends ReadyResource {
220
241
  this._platform = platform;
221
242
  this._diffusionSdk = null;
222
243
  this._musicSdk = null;
244
+ this._translationSdk = null;
223
245
  }
224
246
  async _open() {
225
247
  installResolutionDetector(import.meta);
@@ -513,6 +535,27 @@ export class Harness extends ReadyResource {
513
535
  this._diffusionSdk = sdk;
514
536
  }, () => import('@qvac/sdk/sdcpp-generation/plugin').then((m) => m.diffusionPlugin));
515
537
  }
538
+ async _slmTranslate(model, { text, from, to }, signal) {
539
+ const loadOpts = resolveLoadOptions({}, this._deviceConfig());
540
+ const key = modelKey(model, loadOpts);
541
+ const pool = this._models;
542
+ if (!isResident(pool, key))
543
+ await this._makeRoomForCompletion(signal);
544
+ try {
545
+ const { modelId } = await raceAbort(pool.open(key, model, loadOpts).load(), signal);
546
+ const sdk = await this._sdk.load();
547
+ const answer = await raceAbort(sdk.translate({ modelId, text, from, to, stream: false, modelType: 'llm' }).text, signal);
548
+ return cleanAnswer(answer, text);
549
+ }
550
+ finally {
551
+ releaseModel(pool, key);
552
+ }
553
+ }
554
+ _loadTranslationSdk() {
555
+ return this._loadPluginSdk(() => this._translationSdk, (sdk) => {
556
+ this._translationSdk = sdk;
557
+ }, () => import('@qvac/sdk/nmtcpp-translation/plugin').then((m) => m.nmtPlugin));
558
+ }
516
559
  _loadMusicSdk() {
517
560
  return this._loadPluginSdk(() => this._musicSdk, (sdk) => {
518
561
  this._musicSdk = sdk;
@@ -645,7 +688,8 @@ export class Harness extends ReadyResource {
645
688
  this._ttsModels.destroy(),
646
689
  this._transcriptionModels.destroy(),
647
690
  this._imageRuntime?.unload(),
648
- this._musicRuntime?.unload()
691
+ this._musicRuntime?.unload(),
692
+ this._translationRuntime.unload()
649
693
  ]);
650
694
  for (const outcome of released) {
651
695
  if (outcome.status !== 'rejected')
@@ -809,6 +853,9 @@ export class Harness extends ReadyResource {
809
853
  return transcribeOnce(sdk, id, pcm ? buildWavBuffer(pcm) : audio);
810
854
  });
811
855
  }
856
+ detectLanguage(context) {
857
+ return detectLanguage(context);
858
+ }
812
859
  async speak({ model, text, signal }) {
813
860
  const samples = await this._voiceJob(this._ttsModels, 'tts', model, signal, (sdk, id) => speakOnce(sdk, id, text));
814
861
  return { audio: packSamples(samples), sampleRate: SUPERTONIC_SAMPLE_RATE };
@@ -943,6 +990,32 @@ export class Harness extends ReadyResource {
943
990
  releaseModel(pool, key);
944
991
  }
945
992
  }
993
+ async *invoke({ capability, args, enhance, agent, attach, fetchAttachment, progress, signal }) {
994
+ if (!this.opened)
995
+ await this.ready();
996
+ if (this.suspended)
997
+ return yield { type: 'aborted' };
998
+ hlog('invoke start', { capability, enhance });
999
+ try {
1000
+ yield* invoke({
1001
+ capability,
1002
+ args,
1003
+ enhance,
1004
+ enhancer: this._residentEnhancer(),
1005
+ tools: [...this._tools, ...this._capabilityTools],
1006
+ ctx: {
1007
+ image: imageSettingsFrom(agent),
1008
+ attach,
1009
+ fetchAttachment,
1010
+ progress,
1011
+ signal
1012
+ }
1013
+ });
1014
+ }
1015
+ finally {
1016
+ hlog('invoke end', { capability });
1017
+ }
1018
+ }
946
1019
  async *completion({ model, agent, messages, cacheKey, thinkingDefault, toolConfigs, generationParams, responseFormat, captureInferenceCalls, grantedDirs, grantedDrives, driveOp, grantedWriteDirs, approve, attach, fetchAttachment, ragSearch, progress, askUser, signal }) {
947
1020
  await this.ready();
948
1021
  if (this.suspended)
@@ -1059,6 +1132,24 @@ export class Harness extends ReadyResource {
1059
1132
  void this._musicRuntime?.unloadIdle().catch(noop);
1060
1133
  }
1061
1134
  }
1135
+ _residentEnhancer() {
1136
+ return async (prompt, signal) => {
1137
+ const lease = await leaseResidentModel(this._models, (model) => !isTranslationModel(model));
1138
+ if (!lease)
1139
+ return null;
1140
+ try {
1141
+ const sdk = await this._sdk.load();
1142
+ return await expandImagePrompt(sdk, lease.modelId, lease.model, prompt, signal);
1143
+ }
1144
+ catch (err) {
1145
+ hlog('enhance failed', { err: err instanceof Error ? err.message : String(err) });
1146
+ return null;
1147
+ }
1148
+ finally {
1149
+ lease.release();
1150
+ }
1151
+ };
1152
+ }
1062
1153
  async _makeRoomForCompletion(signal, deadlineMs = MODEL_ROOM_WAIT_MS) {
1063
1154
  const roomed = await awaitModelCapacity(this._models, this._completionModelCap, deadlineMs, 250, signal);
1064
1155
  if (!roomed) {
@@ -222,6 +222,8 @@ export type MetricsEvent = {
222
222
  ctxSize?: number | undefined;
223
223
  backends?: RunBackendUse[] | undefined;
224
224
  usage?: UsageBreakdown | undefined;
225
+ route?: 'direct' | 'enhanced' | undefined;
226
+ routeModel?: string | undefined;
225
227
  stopReason?: string | undefined;
226
228
  };
227
229
  export interface InferenceCall {
@@ -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,10 @@
1
+ import type { AbortSignal as BareAbortSignal } from 'bare-abort-controller';
2
+ import type { CompletionSdk } from './completion.ts';
3
+ export type EnhanceMode = 'never' | 'auto';
4
+ export interface EnhanceResult {
5
+ text: string;
6
+ model: string;
7
+ totalTokens?: number;
8
+ }
9
+ export type Enhancer = (prompt: string, signal?: BareAbortSignal) => Promise<EnhanceResult | null>;
10
+ export declare function expandImagePrompt(sdk: CompletionSdk, modelId: string, model: string, prompt: string, signal?: BareAbortSignal): Promise<EnhanceResult | null>;
@@ -0,0 +1,29 @@
1
+ import { cleanAnswer } from '../helpers/clean-answer.js';
2
+ import { raceAbort } from '../helpers/race-abort.js';
3
+ const SYSTEM = "Rewrite the user's description as a single vivid prompt for an image generator. " +
4
+ 'Answer with the prompt alone: no preamble, no quotes, no explanation.';
5
+ const MAX_PROMPT_CHARS = 400;
6
+ export async function expandImagePrompt(sdk, modelId, model, prompt, signal) {
7
+ const run = sdk.completion({
8
+ modelId,
9
+ history: [
10
+ { role: 'system', content: SYSTEM },
11
+ { role: 'user', content: prompt }
12
+ ],
13
+ stream: true,
14
+ generationParams: { temp: 0.7, reasoning_budget: 0 }
15
+ });
16
+ let final;
17
+ try {
18
+ final = await raceAbort(run.final, signal);
19
+ }
20
+ catch (err) {
21
+ if (run.requestId)
22
+ void sdk.cancel?.({ requestId: run.requestId }).catch(() => { });
23
+ throw err;
24
+ }
25
+ const text = cleanAnswer(final.contentText).slice(0, MAX_PROMPT_CHARS);
26
+ if (!text)
27
+ return null;
28
+ return { text, model, totalTokens: final.stats?.generatedTokens };
29
+ }
@@ -0,0 +1,24 @@
1
+ import type { CompletionEvent, ToolDefinition, ToolExecuteContext } from './completion.ts';
2
+ import type { EnhanceMode, Enhancer } from './enhance.ts';
3
+ declare const CAPABILITY_TOOLS: {
4
+ readonly image: {
5
+ readonly tool: 'generate_image';
6
+ readonly enhances: 'prompt';
7
+ };
8
+ readonly translate: {
9
+ readonly tool: 'translate';
10
+ };
11
+ };
12
+ export type Capability = keyof typeof CAPABILITY_TOOLS;
13
+ export declare const CAPABILITIES: Capability[];
14
+ export declare function isCapability(name: string): name is Capability;
15
+ export interface InvokeOptions {
16
+ capability: Capability;
17
+ args: Record<string, unknown>;
18
+ tools: readonly ToolDefinition[];
19
+ ctx: ToolExecuteContext;
20
+ enhance?: EnhanceMode;
21
+ enhancer?: Enhancer;
22
+ }
23
+ export declare function invoke({ capability, args, tools, ctx, enhance, enhancer }: InvokeOptions): AsyncGenerator<CompletionEvent>;
24
+ export {};
@@ -0,0 +1,57 @@
1
+ import { AbortedError } from '../helpers/race-abort.js';
2
+ const CAPABILITY_TOOLS = {
3
+ image: { tool: 'generate_image', enhances: 'prompt' },
4
+ translate: { tool: 'translate' }
5
+ };
6
+ export const CAPABILITIES = Object.keys(CAPABILITY_TOOLS);
7
+ export function isCapability(name) {
8
+ return name in CAPABILITY_TOOLS;
9
+ }
10
+ export async function* invoke({ capability, args, tools, ctx, enhance = 'never', enhancer }) {
11
+ const entry = CAPABILITY_TOOLS[capability];
12
+ const tool = tools.find((candidate) => candidate.name === entry.tool);
13
+ if (!tool) {
14
+ yield { type: 'error', message: `${capability} is not available on this device` };
15
+ return;
16
+ }
17
+ const startedAt = Date.now();
18
+ const field = 'enhances' in entry ? entry.enhances : null;
19
+ let routeModel;
20
+ let totalTokens = 0;
21
+ let called = args;
22
+ if (enhance === 'auto' && field && enhancer) {
23
+ const source = called[field];
24
+ if (typeof source === 'string' && source) {
25
+ const enhanced = await enhancer(source, ctx.signal);
26
+ if (enhanced) {
27
+ called = { ...called, [field]: enhanced.text };
28
+ routeModel = enhanced.model;
29
+ totalTokens = enhanced.totalTokens ?? 0;
30
+ }
31
+ }
32
+ }
33
+ if (ctx.signal?.aborted)
34
+ return yield { type: 'aborted' };
35
+ yield { type: 'tool-call', name: entry.tool, args: called, effect: tool.effect(called, ctx) };
36
+ try {
37
+ const result = await tool.execute(called, ctx);
38
+ if (ctx.signal?.aborted)
39
+ return yield { type: 'aborted' };
40
+ yield { type: 'tool-result', name: entry.tool, result };
41
+ }
42
+ catch (err) {
43
+ if (err instanceof AbortedError || ctx.signal?.aborted) {
44
+ yield { type: 'aborted' };
45
+ return;
46
+ }
47
+ yield { type: 'error', message: err instanceof Error ? err.message : String(err) };
48
+ return;
49
+ }
50
+ yield {
51
+ type: 'metrics',
52
+ totalTokens,
53
+ durationMs: Date.now() - startedAt,
54
+ route: routeModel ? 'enhanced' : 'direct',
55
+ routeModel
56
+ };
57
+ }
@@ -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>;
@@ -91,6 +91,45 @@ 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
+ enhance: context.enhance,
121
+ agent: knobs(agent)
122
+ }, {
123
+ model: '',
124
+ agent,
125
+ messages: [],
126
+ attach: context.attach,
127
+ fetchAttachment: context.fetchAttachment,
128
+ progress: context.progress,
129
+ signal: context.signal
130
+ });
131
+ }
132
+ async *_run(open, context) {
94
133
  const { client } = await this._ensure();
95
134
  let duplex = null;
96
135
  let emittedAborted = false;
@@ -104,32 +143,14 @@ export class RemoteHarness extends EventEmitter {
104
143
  const onAbort = () => duplex?.destroy();
105
144
  try {
106
145
  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
- });
146
+ duplex.write(open);
126
147
  if (context.signal?.aborted) {
127
148
  duplex.destroy();
128
149
  yield { type: 'aborted' };
129
150
  return;
130
151
  }
131
152
  context.signal?.addEventListener?.('abort', onAbort, { once: true });
132
- hlog('client wire open', { model: context.model });
153
+ hlog('client wire open', { model: context.model, capability: open.capability });
133
154
  for await (const event of frames(duplex)) {
134
155
  received++;
135
156
  if (event.type !== 'content' && event.type !== 'thinking') {
@@ -288,7 +309,7 @@ export class RemoteHarness extends EventEmitter {
288
309
  throw died(`harness died mid-run: ${err.message}`);
289
310
  }
290
311
  finally {
291
- hlog('client completion end', { received, aborted: Boolean(context.signal?.aborted) });
312
+ hlog('client run end', { received, aborted: Boolean(context.signal?.aborted) });
292
313
  context.signal?.removeEventListener?.('abort', onAbort);
293
314
  duplex?.destroy();
294
315
  closed = true;
@@ -350,6 +371,14 @@ export class RemoteHarness extends EventEmitter {
350
371
  const response = await abortable(client.transcribe({ model: context.model, audio: context.audio }), context.signal);
351
372
  return response.text;
352
373
  }
374
+ async detectLanguage(context) {
375
+ const { client } = await this._ensure();
376
+ const response = await client.detectLanguage({
377
+ text: context.text,
378
+ topK: context.topK
379
+ });
380
+ return response.languages.map(({ code, language, probability }) => probability ? { code, language, probability } : { code, language });
381
+ }
353
382
  async speak(context) {
354
383
  const { client } = await this._ensure();
355
384
  const response = await abortable(client.speak({ model: context.model, text: context.text }), context.signal);
@@ -68,6 +68,25 @@ 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
+ enhance?: 'never' | 'auto';
75
+ agent?: AgentKnobs;
76
+ attach?: AttachFn;
77
+ fetchAttachment?: FetchAttachmentFn;
78
+ progress?: ProgressFn;
79
+ signal?: CompletionContext['signal'];
80
+ }
81
+ export interface DetectLanguageContext {
82
+ text: string;
83
+ topK?: number;
84
+ }
85
+ export interface DetectedLanguage {
86
+ code: string;
87
+ language: string;
88
+ probability?: number;
89
+ }
71
90
  export interface HarnessEvent {
72
91
  type: string;
73
92
  text?: string;
@@ -146,10 +165,12 @@ export interface CapabilityInfo {
146
165
  }
147
166
  export interface HarnessContract {
148
167
  completion(context: CompletionContext): AsyncIterable<HarnessEvent>;
168
+ invoke?(context: InvokeContext): AsyncIterable<HarnessEvent>;
149
169
  download?(context: DownloadContext): AsyncIterable<HarnessEvent>;
150
170
  describe?(): Promise<HarnessInfo | null> | HarnessInfo;
151
171
  embed?(context: EmbedContext): Promise<number[][]>;
152
172
  transcribe?(context: TranscribeContext): Promise<string>;
173
+ detectLanguage?(context: DetectLanguageContext): DetectedLanguage[] | Promise<DetectedLanguage[]>;
153
174
  speak?(context: SpeakContext): Promise<SpeakResult>;
154
175
  speakStream?(context: SpeakStreamContext): AsyncIterable<SpeakResult>;
155
176
  getEmbeddingDevice?(): EmbedDevice | Promise<EmbedDevice>;
@@ -0,0 +1 @@
1
+ export declare function cleanAnswer(raw: string, source?: string): string;
@@ -0,0 +1,16 @@
1
+ import { unwrap } from 'asenion';
2
+ import { firstMarker, markers, truncateAt } from 'asenion/guards';
3
+ const LEAKS = [
4
+ ...markers.controlTokens,
5
+ ...markers.turnEnd,
6
+ ...markers.thinking,
7
+ ...markers.toolMarkup,
8
+ ...markers.scaffold
9
+ ];
10
+ const THINKING_CLOSE = markers.thinking.filter((marker) => marker.startsWith('</'));
11
+ export function cleanAnswer(raw, source = '') {
12
+ const close = firstMarker(raw, THINKING_CLOSE);
13
+ const body = close ? raw.slice(close.index + close.marker.length) : raw;
14
+ const text = truncateAt(body, LEAKS).trim();
15
+ return unwrap(source) === source ? unwrap(text) : text;
16
+ }
@@ -0,0 +1,6 @@
1
+ export interface LoadStall {
2
+ bump: () => void;
3
+ clear: () => void;
4
+ promise: Promise<never>;
5
+ }
6
+ export declare function createLoadStall(ms: number, message: string): LoadStall;
@@ -0,0 +1,24 @@
1
+ export function createLoadStall(ms, message) {
2
+ let timer = null;
3
+ let rejectStall = null;
4
+ const promise = new Promise((_, reject) => {
5
+ rejectStall = reject;
6
+ });
7
+ const arm = () => {
8
+ if (timer !== null)
9
+ clearTimeout(timer);
10
+ timer = setTimeout(() => {
11
+ rejectStall?.(new Error(message));
12
+ }, ms);
13
+ };
14
+ arm();
15
+ return {
16
+ bump: arm,
17
+ clear: () => {
18
+ if (timer !== null)
19
+ clearTimeout(timer);
20
+ timer = null;
21
+ },
22
+ promise
23
+ };
24
+ }
@@ -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
- if (Array.isArray(ctx.args.packages) && ctx.args.packages.length) {
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(/^.*[/\\]/, ''));