@qvac/harness 0.8.7 → 0.8.9
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/CHANGELOG.md +58 -1
- package/dist-lib/lib/capabilities/completion.d.ts +1 -0
- package/dist-lib/lib/capabilities/completion.js +48 -9
- package/dist-lib/lib/connect.js +1 -0
- package/dist-lib/lib/serve.js +1 -0
- package/dist-lib/lib/tools/file-glob.js +1 -0
- package/dist-lib/lib/tools/grep.js +1 -0
- package/dist-lib/lib/tools/read-files.js +1 -0
- package/dist-lib/lib/tools/skill.js +6 -4
- package/dist-lib/lib/tools/skills/catalog.d.ts +1 -0
- package/dist-lib/lib/tools/skills/catalog.js +2 -0
- package/dist-lib/lib/tools/skills/frontmatter-parser.d.ts +1 -0
- package/dist-lib/lib/tools/skills/frontmatter-parser.js +4 -0
- package/dist-lib/lib/tools/skills/mention-name.d.ts +6 -0
- package/dist-lib/lib/tools/skills/mention-name.js +6 -0
- package/dist-lib/lib/tools/skills/mention.d.ts +2 -2
- package/dist-lib/lib/tools/skills/mention.js +11 -4
- package/dist-lib/lib/tools/weather-lookup.js +1 -0
- package/dist-lib/lib/tools/web-fetch.js +1 -0
- package/dist-lib/lib/tools/web-search.js +1 -0
- package/dist-lib/schema/harness.js +2 -1
- package/dist-lib/spec/hrpc/messages.js +10 -5
- package/dist-lib/spec/hyperschema/index.js +10 -5
- package/dist-lib/spec/hyperschema/schema.json +6 -1
- package/dist-lib/spec/hyperschema/types.d.ts +2 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,61 @@ While the major version is `0`, a breaking change bumps the minor version.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.8.9] - 2026-10-15
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- A skill with a `display_name` answers only to that handle; a skill without
|
|
16
|
+
one still answers to its folder name. `@documents` resolves the word skill
|
|
17
|
+
and `@word` no longer does, which replaces the 0.8.8 note that both resolve.
|
|
18
|
+
Implicit preload skips a leading `@name`, so a handle that resolves to no
|
|
19
|
+
skill loads nothing, while a plain "make an excel sheet" still preloads
|
|
20
|
+
Spreadsheets. A paired device on an older app build still sends `@word` and
|
|
21
|
+
`@excel`; once the skills package declares `display_name`, a host on this
|
|
22
|
+
version answers those requests without the skill. (#505)
|
|
23
|
+
- `@qvac/skills` 0.1.15: the Word and Excel skills declare `display_name`, so
|
|
24
|
+
their handles are `@documents` and `@spreadsheets`. The browser skill hands a
|
|
25
|
+
CAPTCHA, interstitial or login page to the user instead of clicking through
|
|
26
|
+
or waiting, and keeps its Chrome cookies and logins across chats. (#513)
|
|
27
|
+
|
|
28
|
+
## [0.8.8] - 2026-10-15
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- A skill's manifest may declare `display_name`, the `@` handle users see and
|
|
33
|
+
type, as one word (letters, digits, `-` or `_`); a `display_name` that is
|
|
34
|
+
not one word is ignored. The Word and Excel skills are shown as Documents
|
|
35
|
+
and Spreadsheets, and their folder names are trademarks, so a host needs a
|
|
36
|
+
handle other than the name. Once the word skill's manifest declares
|
|
37
|
+
`display_name: Documents`, `@documents` resolves it, and `@word` still does,
|
|
38
|
+
so an app that sends the folder name keeps working. The catalog entry and
|
|
39
|
+
the `skills` wire carry `displayName`, a customized copy of a skill keeps
|
|
40
|
+
the shipped one, and `skillMentionHandle` gives a host the same rule the
|
|
41
|
+
harness matches on. The folder name stays the skill's key everywhere else.
|
|
42
|
+
(#469)
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- A replayed tool call keeps its markup when history is rendered from scratch.
|
|
47
|
+
Every assistant turn lost its tool-call markup on the way in, so a retry,
|
|
48
|
+
compaction, a dropped chat cache, an overflow candidate or another device
|
|
49
|
+
showed the model an empty assistant turn followed by a result answering
|
|
50
|
+
nothing. Only an assistant turn that no tool result follows is stripped now,
|
|
51
|
+
which still keeps markup leaked into a final answer out of the prompt. (#402)
|
|
52
|
+
- A web search the harness runs for "search the web" now ends on the same
|
|
53
|
+
continuation nudge as a search the model makes. Without it, the round after
|
|
54
|
+
the search often failed on Qwen3.5-9B with thinking on instead of answering.
|
|
55
|
+
(#486)
|
|
56
|
+
- An identical call to a read tool that already succeeded this turn is
|
|
57
|
+
answered with a note pointing at the earlier result instead of running
|
|
58
|
+
again, and counts as a strike. A small model could repeat the same call
|
|
59
|
+
hundreds of times in one round until the context filled. Any other tool
|
|
60
|
+
that runs clears the memo, and a round is cut to its first 16 calls. (#377)
|
|
61
|
+
- Asking the `skill` tool for a file a loaded skill does not have returns
|
|
62
|
+
guidance listing its real files instead of an error, so the app no longer
|
|
63
|
+
shows a failed tool call for it. A file asked of an unknown skill returns
|
|
64
|
+
`skill not found`. (#383)
|
|
65
|
+
|
|
11
66
|
## [0.8.7] - 2026-10-12
|
|
12
67
|
|
|
13
68
|
### Added
|
|
@@ -1953,7 +2008,9 @@ Install 0.2.12 or later to get them.
|
|
|
1953
2008
|
- Every dependency installs from the public npm registry: GPU stats are read
|
|
1954
2009
|
through `bare-gpu-info`. (#12)
|
|
1955
2010
|
|
|
1956
|
-
[Unreleased]: https://github.com/tetherto/qvac-harness/compare/v0.8.
|
|
2011
|
+
[Unreleased]: https://github.com/tetherto/qvac-harness/compare/v0.8.9...HEAD
|
|
2012
|
+
[0.8.9]: https://github.com/tetherto/qvac-harness/compare/v0.8.8...v0.8.9
|
|
2013
|
+
[0.8.8]: https://github.com/tetherto/qvac-harness/compare/v0.8.7...v0.8.8
|
|
1957
2014
|
[0.8.7]: https://github.com/tetherto/qvac-harness/compare/v0.8.6...v0.8.7
|
|
1958
2015
|
[0.8.6]: https://github.com/tetherto/qvac-harness/compare/v0.8.5...v0.8.6
|
|
1959
2016
|
[0.8.5]: https://github.com/tetherto/qvac-harness/compare/v0.8.0...v0.8.5
|
|
@@ -185,6 +185,7 @@ export interface ToolDefinition {
|
|
|
185
185
|
toolset: Toolset | null;
|
|
186
186
|
validate?: (args: Record<string, unknown>, ctx?: ToolExecuteContext) => string | null;
|
|
187
187
|
effect: (args: Record<string, unknown>, ctx?: ToolExecuteContext) => ToolEffect;
|
|
188
|
+
stableRead?: true;
|
|
188
189
|
resource?: (args: Record<string, unknown>, ctx?: ToolExecuteContext) => ResourceDescriptor | null;
|
|
189
190
|
compiled?: true;
|
|
190
191
|
execute: (args: Record<string, unknown>, ctx?: ToolExecuteContext) => Promise<unknown>;
|
|
@@ -60,6 +60,9 @@ const MAX_MUTE_ROUNDS = 1;
|
|
|
60
60
|
const NARRATED_INTENT = /\b(I['’]ll|I will|Now I|Let me(?! know)|Next,? I)\b[^.!?]*[.!?]?\s*$/i;
|
|
61
61
|
const MAX_WEB_SEARCHES_PER_TURN = 3;
|
|
62
62
|
const MAX_CAPPED_ROUNDS = 1;
|
|
63
|
+
const MAX_CALLS_PER_ROUND = 16;
|
|
64
|
+
const repeatedReadMessage = (name) => `The result of this exact ${name} call is already above. Answer from it now; ` +
|
|
65
|
+
`do not call ${name} again with the same arguments.`;
|
|
63
66
|
const WEB_SEARCH_LIMIT_MESSAGE = 'Web search limit reached for this question. Answer now from the results already gathered. ' +
|
|
64
67
|
'If something was not found, say so plainly and mention the most relevant sources. ' +
|
|
65
68
|
'Do not call web_search again.';
|
|
@@ -101,9 +104,10 @@ export function toSdkTool(tool) {
|
|
|
101
104
|
parameters: intentParameters(tool, tool.prompt?.parameters ?? tool.parameters)
|
|
102
105
|
};
|
|
103
106
|
}
|
|
104
|
-
function sanitizeTurn(turn) {
|
|
107
|
+
function sanitizeTurn(turn, index, turns) {
|
|
105
108
|
const stripped = stripControlTokens(turn.content);
|
|
106
|
-
const
|
|
109
|
+
const leaked = turn.role === 'assistant' && turns[index + 1]?.role !== 'tool';
|
|
110
|
+
const content = leaked ? stripToolCallMarkup(stripped) : stripped;
|
|
107
111
|
return content === turn.content ? turn : { ...turn, content };
|
|
108
112
|
}
|
|
109
113
|
function lastUserContent(messages) {
|
|
@@ -143,7 +147,8 @@ function directToolHistory(history, call, output) {
|
|
|
143
147
|
return [
|
|
144
148
|
...history,
|
|
145
149
|
{ role: 'assistant', content: toolCallMarkup([call]) },
|
|
146
|
-
{ role: 'tool', content: toolResultTurn(call.name, DIRECT_CALL_ID, output) }
|
|
150
|
+
{ role: 'tool', content: toolResultTurn(call.name, DIRECT_CALL_ID, output) },
|
|
151
|
+
{ role: 'user', content: TOOL_CONTINUATION_NUDGE }
|
|
147
152
|
];
|
|
148
153
|
}
|
|
149
154
|
function assistantToolTurn(contentText, calls) {
|
|
@@ -203,12 +208,19 @@ function deniedResult() {
|
|
|
203
208
|
}
|
|
204
209
|
function callKey(name, args) {
|
|
205
210
|
try {
|
|
206
|
-
return `${name}:${JSON.stringify(args)}`;
|
|
211
|
+
return `${name}:${JSON.stringify(args, sortedKeys)}`;
|
|
207
212
|
}
|
|
208
213
|
catch {
|
|
209
214
|
return name;
|
|
210
215
|
}
|
|
211
216
|
}
|
|
217
|
+
function sortedKeys(_key, value) {
|
|
218
|
+
if (!isRecord(value) || Array.isArray(value))
|
|
219
|
+
return value;
|
|
220
|
+
return Object.fromEntries(Object.keys(value)
|
|
221
|
+
.sort()
|
|
222
|
+
.map((key) => [key, value[key]]));
|
|
223
|
+
}
|
|
212
224
|
const RETRYABLE_STATUS = new Set([408, 429]);
|
|
213
225
|
const RETRYABLE_CODE = new Set(['timeout', 'transport', 'busy']);
|
|
214
226
|
function callSucceeded(result, outcome) {
|
|
@@ -875,12 +887,12 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
|
|
|
875
887
|
let history = annotateLastUserTime(systemContent ? [{ role: 'system', content: systemContent }, ...messages] : messages);
|
|
876
888
|
if (preload && preloadedSkill !== null) {
|
|
877
889
|
history = directToolHistory(history, { name: SKILL_TOOL_NAME, arguments: { name: preload.name } }, preloadedResult);
|
|
878
|
-
history.push({ role: 'user', content: TOOL_CONTINUATION_NUDGE });
|
|
879
890
|
}
|
|
880
891
|
let roundCacheKey = cacheKey;
|
|
881
892
|
const runUsage = () => usageBreakdown(promptParts, stats.cacheTokens, model);
|
|
882
893
|
const denied = new Set();
|
|
883
894
|
const failed = new Map();
|
|
895
|
+
const answered = new Set();
|
|
884
896
|
let blockedRepeats = 0;
|
|
885
897
|
let webSearches = 0;
|
|
886
898
|
const sources = trustedSourcesFrom(history);
|
|
@@ -939,6 +951,9 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
|
|
|
939
951
|
if (fold.ok) {
|
|
940
952
|
webSearches++;
|
|
941
953
|
}
|
|
954
|
+
if (webSearch.stableRead && execution.executed && callSucceeded(output, executionOutcome)) {
|
|
955
|
+
answered.add(callKey(webSearch.name, args));
|
|
956
|
+
}
|
|
942
957
|
if (!activity && !fold.ok) {
|
|
943
958
|
activity = {
|
|
944
959
|
type: 'tool-activity',
|
|
@@ -1074,6 +1089,10 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
|
|
|
1074
1089
|
}
|
|
1075
1090
|
mute = 0;
|
|
1076
1091
|
narrated = false;
|
|
1092
|
+
if (toolCalls.length > MAX_CALLS_PER_ROUND) {
|
|
1093
|
+
hlog('round over the call cap; the rest are dropped', { round, calls: toolCalls.length });
|
|
1094
|
+
toolCalls = toolCalls.slice(0, MAX_CALLS_PER_ROUND);
|
|
1095
|
+
}
|
|
1077
1096
|
const resolved = await resolveToolCalls(toolCalls, {
|
|
1078
1097
|
enabled,
|
|
1079
1098
|
agentId: agent?.id ?? null,
|
|
@@ -1099,7 +1118,8 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
|
|
|
1099
1118
|
for (const { call, tool, args, rawCall, rejection, intent } of resolved) {
|
|
1100
1119
|
if (signal?.aborted)
|
|
1101
1120
|
throw new AbortedError();
|
|
1102
|
-
const
|
|
1121
|
+
const key = callKey(call.name, args);
|
|
1122
|
+
const priorFailure = failed.get(key);
|
|
1103
1123
|
const searchCapped = tool?.name === WEB_SEARCH_TOOL_NAME && webSearches >= MAX_WEB_SEARCHES_PER_TURN;
|
|
1104
1124
|
const callCtx = {
|
|
1105
1125
|
...toolContext(turnImages, ctxSize),
|
|
@@ -1115,6 +1135,8 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
|
|
|
1115
1135
|
};
|
|
1116
1136
|
const skillCall = call.name === SKILL_TOOL_NAME;
|
|
1117
1137
|
const effect = tool?.effect(args, callCtx) ?? (skillCall ? 'read' : undefined);
|
|
1138
|
+
const stableRead = tool?.stableRead === true && effect === 'read';
|
|
1139
|
+
const priorSuccess = !priorFailure && stableRead && answered.has(key);
|
|
1118
1140
|
const resource = tool?.resource?.(args) ?? null;
|
|
1119
1141
|
const skillName = unquoteArg(args?.name ?? args?.skill_name ?? args?.skill);
|
|
1120
1142
|
const askedFile = skillFileArg(args);
|
|
@@ -1123,7 +1145,7 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
|
|
|
1123
1145
|
(mainRequested
|
|
1124
1146
|
? loadedThisTurn.has(skillName)
|
|
1125
1147
|
: loadedFilesThisTurn.has(fileKey(skillName, unquoteArg(askedFile))));
|
|
1126
|
-
const silent = searchCapped || skillReloaded || priorFailure !== undefined;
|
|
1148
|
+
const silent = searchCapped || skillReloaded || priorFailure !== undefined || priorSuccess;
|
|
1127
1149
|
const display = silent
|
|
1128
1150
|
? undefined
|
|
1129
1151
|
: labelOf(call.name, args, skillCall ? skillEmoji(skillName) : skillIcon);
|
|
@@ -1145,6 +1167,14 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
|
|
|
1145
1167
|
});
|
|
1146
1168
|
execution = terminalCall(priorFailure.result, priorFailure.outcome, { executed: false });
|
|
1147
1169
|
}
|
|
1170
|
+
else if (priorSuccess) {
|
|
1171
|
+
blockedRepeats++;
|
|
1172
|
+
hlog('repeat of a read that already answered; not run again', {
|
|
1173
|
+
tool: call.name,
|
|
1174
|
+
blockedRepeats
|
|
1175
|
+
});
|
|
1176
|
+
execution = terminalCall({ content: repeatedReadMessage(call.name) }, { kind: 'success' }, { executed: false });
|
|
1177
|
+
}
|
|
1148
1178
|
else if (skillReloaded) {
|
|
1149
1179
|
const loadedWhat = mainRequested ? skillName : `${skillName} ${unquoteArg(askedFile)}`;
|
|
1150
1180
|
execution = terminalCall({
|
|
@@ -1196,18 +1226,27 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
|
|
|
1196
1226
|
const output = settled.result;
|
|
1197
1227
|
const executionOutcome = settled.outcome;
|
|
1198
1228
|
if (!priorFailure && settledFailure(output, executionOutcome)) {
|
|
1199
|
-
failed.set(
|
|
1229
|
+
failed.set(key, settled);
|
|
1200
1230
|
}
|
|
1201
1231
|
else if (execution.executed && callSucceeded(output, executionOutcome)) {
|
|
1202
1232
|
progressed = true;
|
|
1203
1233
|
answer ??= answerOf(output);
|
|
1204
1234
|
}
|
|
1235
|
+
if (execution.executed && !stableRead && !skillCall) {
|
|
1236
|
+
answered.clear();
|
|
1237
|
+
}
|
|
1238
|
+
else if (stableRead && execution.executed && callSucceeded(output, executionOutcome)) {
|
|
1239
|
+
answered.add(key);
|
|
1240
|
+
}
|
|
1205
1241
|
runOutcomes.record(output, executionOutcome);
|
|
1206
1242
|
if (searchCapped) {
|
|
1207
1243
|
cappedCalls++;
|
|
1208
1244
|
hlog('web_search capped', { searches: webSearches });
|
|
1209
1245
|
}
|
|
1210
|
-
else if (tool?.name === WEB_SEARCH_TOOL_NAME &&
|
|
1246
|
+
else if (tool?.name === WEB_SEARCH_TOOL_NAME &&
|
|
1247
|
+
!rejection &&
|
|
1248
|
+
!priorFailure &&
|
|
1249
|
+
!priorSuccess) {
|
|
1211
1250
|
webSearches++;
|
|
1212
1251
|
}
|
|
1213
1252
|
if (call.name === SKILL_TOOL_NAME) {
|
package/dist-lib/lib/connect.js
CHANGED
package/dist-lib/lib/serve.js
CHANGED
|
@@ -185,12 +185,14 @@ export function createSkillTool({ skillsDir, overlayDir = resolveSkillsOverlayDi
|
|
|
185
185
|
try {
|
|
186
186
|
return await mainInstead(file, available);
|
|
187
187
|
}
|
|
188
|
-
catch {
|
|
188
|
+
catch {
|
|
189
|
+
return { error: `skill not found: ${name}` };
|
|
190
|
+
}
|
|
189
191
|
}
|
|
190
192
|
const rest = available.length
|
|
191
|
-
? `
|
|
192
|
-
: '
|
|
193
|
-
return {
|
|
193
|
+
? `Its other files: ${available.join(', ')}.`
|
|
194
|
+
: 'It has no other files; you already have its full instructions, so act now.';
|
|
195
|
+
return { name, content: `There is no file ${file} in this skill. ${rest}` };
|
|
194
196
|
}
|
|
195
197
|
}
|
|
196
198
|
};
|
|
@@ -9,6 +9,7 @@ import { type OperationSpec } from '../operations/manifest.ts';
|
|
|
9
9
|
import type { CapabilityProfile } from '../../models/model-catalog.ts';
|
|
10
10
|
export interface SkillCatalogEntry {
|
|
11
11
|
name: string;
|
|
12
|
+
displayName?: string;
|
|
12
13
|
description: string;
|
|
13
14
|
aliases?: string[];
|
|
14
15
|
preloadOnName?: boolean;
|
|
@@ -79,6 +79,7 @@ function mergeOverlay(base, overlay) {
|
|
|
79
79
|
const { origin: _origin, mcpReads: _mcpReads, operations: _operations, ...rest } = overlay;
|
|
80
80
|
return {
|
|
81
81
|
...rest,
|
|
82
|
+
displayName: base.displayName,
|
|
82
83
|
credentials: base.credentials,
|
|
83
84
|
allowList: base.allowList,
|
|
84
85
|
platform: base.platform,
|
|
@@ -156,6 +157,7 @@ async function loadEntry(skillsDir, dir, origin) {
|
|
|
156
157
|
const platform = platformsOf(meta.platform, origin);
|
|
157
158
|
return {
|
|
158
159
|
name: dir,
|
|
160
|
+
displayName: origin === 'overlay' ? undefined : meta.displayName || undefined,
|
|
159
161
|
description: meta.description ?? '',
|
|
160
162
|
aliases: meta.aliases,
|
|
161
163
|
preloadOnName: meta.preloadOnName === false ? false : undefined,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { parseManifestBlock } from './manifest-parse.js';
|
|
2
|
+
import { isMentionHandle } from './mention-name.js';
|
|
2
3
|
const FRONT_MATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
3
4
|
const FIELD_RE = /^([A-Za-z_][\w-]*):(.*)$/;
|
|
4
5
|
export function parseSkillFrontMatter(raw) {
|
|
@@ -54,6 +55,9 @@ function parseFields(block) {
|
|
|
54
55
|
else if (key === 'name' || key === 'description') {
|
|
55
56
|
meta[key] = stripQuotes(value);
|
|
56
57
|
}
|
|
58
|
+
else if (key === 'display_name' && isMentionHandle(stripQuotes(value))) {
|
|
59
|
+
meta.displayName = stripQuotes(value);
|
|
60
|
+
}
|
|
57
61
|
else if (key === 'preload_on_name') {
|
|
58
62
|
meta.preloadOnName = stripQuotes(value).toLowerCase() !== 'false';
|
|
59
63
|
}
|
|
@@ -1 +1,7 @@
|
|
|
1
1
|
export declare function skillMentionName(text: string): string | null;
|
|
2
|
+
export declare function isMentionHandle(value: string): boolean;
|
|
3
|
+
export interface MentionableSkill {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
readonly displayName?: string | undefined;
|
|
6
|
+
}
|
|
7
|
+
export declare function skillMentionHandle(skill: MentionableSkill): string;
|
|
@@ -2,3 +2,9 @@ const MENTION_PATTERN = /^@([a-z0-9][a-z0-9_-]*)(?=\s|$)/;
|
|
|
2
2
|
export function skillMentionName(text) {
|
|
3
3
|
return MENTION_PATTERN.exec(text.trimStart().toLowerCase())?.[1] ?? null;
|
|
4
4
|
}
|
|
5
|
+
export function isMentionHandle(value) {
|
|
6
|
+
return skillMentionName(`@${value}`) === value.toLowerCase();
|
|
7
|
+
}
|
|
8
|
+
export function skillMentionHandle(skill) {
|
|
9
|
+
return (skill.displayName ?? skill.name).toLowerCase();
|
|
10
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SkillCatalogEntry } from './catalog.ts';
|
|
2
|
-
import { skillMentionName } from './mention-name.ts';
|
|
3
|
-
export { skillMentionName };
|
|
2
|
+
import { skillMentionHandle, skillMentionName } from './mention-name.ts';
|
|
3
|
+
export { skillMentionHandle, skillMentionName };
|
|
4
4
|
export declare function resolveSkillMention(text: string, catalog: readonly SkillCatalogEntry[]): SkillCatalogEntry | null;
|
|
5
5
|
export declare function resolveImplicitSkill(text: string, activeSkills: readonly SkillCatalogEntry[]): SkillCatalogEntry | null;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { has } from 'asenion';
|
|
2
|
-
import { skillMentionName } from './mention-name.js';
|
|
3
|
-
export { skillMentionName };
|
|
2
|
+
import { skillMentionHandle, skillMentionName } from './mention-name.js';
|
|
3
|
+
export { skillMentionHandle, skillMentionName };
|
|
4
4
|
export function resolveSkillMention(text, catalog) {
|
|
5
5
|
const name = skillMentionName(text);
|
|
6
6
|
if (!name) {
|
|
7
7
|
return null;
|
|
8
8
|
}
|
|
9
|
-
return catalog.find((skill) => skill.
|
|
9
|
+
return (catalog.find((skill) => skill.displayName?.toLowerCase() === name) ??
|
|
10
|
+
catalog.find((skill) => !skill.displayName && skill.name.toLowerCase() === name) ??
|
|
11
|
+
null);
|
|
10
12
|
}
|
|
11
13
|
const MIN_IMPLICIT_NAME_LENGTH = 4;
|
|
12
14
|
function matchesName(text, candidate) {
|
|
@@ -16,7 +18,12 @@ function matchesName(text, candidate) {
|
|
|
16
18
|
}
|
|
17
19
|
return true;
|
|
18
20
|
}
|
|
21
|
+
function withoutMention(text) {
|
|
22
|
+
const name = skillMentionName(text);
|
|
23
|
+
return name ? text.trimStart().slice(name.length + 1) : text;
|
|
24
|
+
}
|
|
19
25
|
export function resolveImplicitSkill(text, activeSkills) {
|
|
26
|
+
const prose = withoutMention(text);
|
|
20
27
|
let best = null;
|
|
21
28
|
let bestLength = 0;
|
|
22
29
|
for (const skill of activeSkills) {
|
|
@@ -26,7 +33,7 @@ export function resolveImplicitSkill(text, activeSkills) {
|
|
|
26
33
|
if (candidate.length < MIN_IMPLICIT_NAME_LENGTH) {
|
|
27
34
|
continue;
|
|
28
35
|
}
|
|
29
|
-
if (!matchesName(
|
|
36
|
+
if (!matchesName(prose, candidate)) {
|
|
30
37
|
continue;
|
|
31
38
|
}
|
|
32
39
|
const weight = candidate.replace(/\+/g, '').length;
|
|
@@ -32,6 +32,7 @@ export function createWeatherLookupTool({ fetch = noRedirectFetch, resolveHost =
|
|
|
32
32
|
required: ['location']
|
|
33
33
|
},
|
|
34
34
|
effect: () => 'read',
|
|
35
|
+
stableRead: true,
|
|
35
36
|
resource: () => ({ kind: 'host', label: `${GEOCODER_HOST}, ${WEATHER_HOST}` }),
|
|
36
37
|
execute: (args, ctx) => execute(args.location, ctx?.signal, { fetch, resolveHost, timeoutMs: geocodeTimeoutMs })
|
|
37
38
|
};
|
|
@@ -322,7 +322,8 @@ export function registerHarnessTypes(ns) {
|
|
|
322
322
|
{ name: 'surfaceWhenUnsupported', type: 'bool' },
|
|
323
323
|
{ name: 'minCapabilityProfile', type: 'string' },
|
|
324
324
|
{ name: 'emoji', type: 'string' },
|
|
325
|
-
{ name: 'setupV2', type: '@harness/skill-setup-v2' }
|
|
325
|
+
{ name: 'setupV2', type: '@harness/skill-setup-v2' },
|
|
326
|
+
{ name: 'displayName', type: 'string' }
|
|
326
327
|
]
|
|
327
328
|
});
|
|
328
329
|
ns.register({
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// This file is autogenerated by the hyperschema compiler
|
|
2
|
-
// Schema Version:
|
|
2
|
+
// Schema Version: 21
|
|
3
3
|
/* eslint-disable camelcase */
|
|
4
4
|
/* eslint-disable quotes */
|
|
5
5
|
/* eslint-disable space-before-function-paren */
|
|
6
6
|
|
|
7
7
|
import { c } from 'hyperschema/runtime'
|
|
8
8
|
|
|
9
|
-
const VERSION =
|
|
9
|
+
const VERSION = 21
|
|
10
10
|
|
|
11
11
|
// eslint-disable-next-line no-unused-vars
|
|
12
12
|
let version = VERSION
|
|
@@ -1502,7 +1502,8 @@ const encoding28 = {
|
|
|
1502
1502
|
(m.surfaceWhenUnsupported ? 256 : 0) |
|
|
1503
1503
|
(m.minCapabilityProfile ? 512 : 0) |
|
|
1504
1504
|
(m.emoji ? 1024 : 0) |
|
|
1505
|
-
(m.setupV2 ? 2048 : 0)
|
|
1505
|
+
(m.setupV2 ? 2048 : 0) |
|
|
1506
|
+
((version >= 21 && m.displayName) ? 4096 : 0)
|
|
1506
1507
|
|
|
1507
1508
|
c.string.preencode(state, m.name)
|
|
1508
1509
|
c.string.preencode(state, m.description)
|
|
@@ -1519,6 +1520,7 @@ const encoding28 = {
|
|
|
1519
1520
|
if (m.minCapabilityProfile) c.string.preencode(state, m.minCapabilityProfile)
|
|
1520
1521
|
if (m.emoji) c.string.preencode(state, m.emoji)
|
|
1521
1522
|
if (m.setupV2) encoding28_14.preencode(state, m.setupV2)
|
|
1523
|
+
if (version >= 21 && m.displayName) c.string.preencode(state, m.displayName)
|
|
1522
1524
|
},
|
|
1523
1525
|
encode(state, m) {
|
|
1524
1526
|
const flags =
|
|
@@ -1533,7 +1535,8 @@ const encoding28 = {
|
|
|
1533
1535
|
(m.surfaceWhenUnsupported ? 256 : 0) |
|
|
1534
1536
|
(m.minCapabilityProfile ? 512 : 0) |
|
|
1535
1537
|
(m.emoji ? 1024 : 0) |
|
|
1536
|
-
(m.setupV2 ? 2048 : 0)
|
|
1538
|
+
(m.setupV2 ? 2048 : 0) |
|
|
1539
|
+
((version >= 21 && m.displayName) ? 4096 : 0)
|
|
1537
1540
|
|
|
1538
1541
|
c.string.encode(state, m.name)
|
|
1539
1542
|
c.string.encode(state, m.description)
|
|
@@ -1550,6 +1553,7 @@ const encoding28 = {
|
|
|
1550
1553
|
if (m.minCapabilityProfile) c.string.encode(state, m.minCapabilityProfile)
|
|
1551
1554
|
if (m.emoji) c.string.encode(state, m.emoji)
|
|
1552
1555
|
if (m.setupV2) encoding28_14.encode(state, m.setupV2)
|
|
1556
|
+
if (version >= 21 && m.displayName) c.string.encode(state, m.displayName)
|
|
1553
1557
|
},
|
|
1554
1558
|
decode(state) {
|
|
1555
1559
|
const r0 = c.string.decode(state)
|
|
@@ -1571,7 +1575,8 @@ const encoding28 = {
|
|
|
1571
1575
|
surfaceWhenUnsupported: (flags & 256) !== 0,
|
|
1572
1576
|
minCapabilityProfile: (flags & 512) !== 0 ? c.string.decode(state) : null,
|
|
1573
1577
|
emoji: (flags & 1024) !== 0 ? c.string.decode(state) : null,
|
|
1574
|
-
setupV2: (flags & 2048) !== 0 ? encoding28_14.decode(state) : null
|
|
1578
|
+
setupV2: (flags & 2048) !== 0 ? encoding28_14.decode(state) : null,
|
|
1579
|
+
displayName: (version >= 21 && (flags & 4096) !== 0) ? c.string.decode(state) : null
|
|
1575
1580
|
}
|
|
1576
1581
|
}
|
|
1577
1582
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// This file is autogenerated by the hyperschema compiler
|
|
2
|
-
// Schema Version:
|
|
2
|
+
// Schema Version: 21
|
|
3
3
|
/* eslint-disable camelcase */
|
|
4
4
|
/* eslint-disable quotes */
|
|
5
5
|
/* eslint-disable space-before-function-paren */
|
|
6
6
|
|
|
7
7
|
import { c } from 'hyperschema/runtime'
|
|
8
8
|
|
|
9
|
-
const VERSION =
|
|
9
|
+
const VERSION = 21
|
|
10
10
|
|
|
11
11
|
// eslint-disable-next-line no-unused-vars
|
|
12
12
|
let version = VERSION
|
|
@@ -1502,7 +1502,8 @@ const encoding28 = {
|
|
|
1502
1502
|
(m.surfaceWhenUnsupported ? 256 : 0) |
|
|
1503
1503
|
(m.minCapabilityProfile ? 512 : 0) |
|
|
1504
1504
|
(m.emoji ? 1024 : 0) |
|
|
1505
|
-
(m.setupV2 ? 2048 : 0)
|
|
1505
|
+
(m.setupV2 ? 2048 : 0) |
|
|
1506
|
+
((version >= 21 && m.displayName) ? 4096 : 0)
|
|
1506
1507
|
|
|
1507
1508
|
c.string.preencode(state, m.name)
|
|
1508
1509
|
c.string.preencode(state, m.description)
|
|
@@ -1519,6 +1520,7 @@ const encoding28 = {
|
|
|
1519
1520
|
if (m.minCapabilityProfile) c.string.preencode(state, m.minCapabilityProfile)
|
|
1520
1521
|
if (m.emoji) c.string.preencode(state, m.emoji)
|
|
1521
1522
|
if (m.setupV2) encoding28_14.preencode(state, m.setupV2)
|
|
1523
|
+
if (version >= 21 && m.displayName) c.string.preencode(state, m.displayName)
|
|
1522
1524
|
},
|
|
1523
1525
|
encode(state, m) {
|
|
1524
1526
|
const flags =
|
|
@@ -1533,7 +1535,8 @@ const encoding28 = {
|
|
|
1533
1535
|
(m.surfaceWhenUnsupported ? 256 : 0) |
|
|
1534
1536
|
(m.minCapabilityProfile ? 512 : 0) |
|
|
1535
1537
|
(m.emoji ? 1024 : 0) |
|
|
1536
|
-
(m.setupV2 ? 2048 : 0)
|
|
1538
|
+
(m.setupV2 ? 2048 : 0) |
|
|
1539
|
+
((version >= 21 && m.displayName) ? 4096 : 0)
|
|
1537
1540
|
|
|
1538
1541
|
c.string.encode(state, m.name)
|
|
1539
1542
|
c.string.encode(state, m.description)
|
|
@@ -1550,6 +1553,7 @@ const encoding28 = {
|
|
|
1550
1553
|
if (m.minCapabilityProfile) c.string.encode(state, m.minCapabilityProfile)
|
|
1551
1554
|
if (m.emoji) c.string.encode(state, m.emoji)
|
|
1552
1555
|
if (m.setupV2) encoding28_14.encode(state, m.setupV2)
|
|
1556
|
+
if (version >= 21 && m.displayName) c.string.encode(state, m.displayName)
|
|
1553
1557
|
},
|
|
1554
1558
|
decode(state) {
|
|
1555
1559
|
const r0 = c.string.decode(state)
|
|
@@ -1571,7 +1575,8 @@ const encoding28 = {
|
|
|
1571
1575
|
surfaceWhenUnsupported: (flags & 256) !== 0,
|
|
1572
1576
|
minCapabilityProfile: (flags & 512) !== 0 ? c.string.decode(state) : null,
|
|
1573
1577
|
emoji: (flags & 1024) !== 0 ? c.string.decode(state) : null,
|
|
1574
|
-
setupV2: (flags & 2048) !== 0 ? encoding28_14.decode(state) : null
|
|
1578
|
+
setupV2: (flags & 2048) !== 0 ? encoding28_14.decode(state) : null,
|
|
1579
|
+
displayName: (version >= 21 && (flags & 4096) !== 0) ? c.string.decode(state) : null
|
|
1575
1580
|
}
|
|
1576
1581
|
}
|
|
1577
1582
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version":
|
|
2
|
+
"version": 21,
|
|
3
3
|
"schema": [
|
|
4
4
|
{
|
|
5
5
|
"name": "empty",
|
|
@@ -1246,6 +1246,11 @@
|
|
|
1246
1246
|
"name": "setupV2",
|
|
1247
1247
|
"type": "@harness/skill-setup-v2",
|
|
1248
1248
|
"version": 1
|
|
1249
|
+
},
|
|
1250
|
+
{
|
|
1251
|
+
"name": "displayName",
|
|
1252
|
+
"type": "string",
|
|
1253
|
+
"version": 21
|
|
1249
1254
|
}
|
|
1250
1255
|
]
|
|
1251
1256
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// This file is autogenerated by hyperschema-ts
|
|
2
|
-
// Schema version:
|
|
2
|
+
// Schema version: 21
|
|
3
3
|
// Do not edit manually.
|
|
4
4
|
|
|
5
5
|
export interface HarnessEmpty {
|
|
@@ -235,6 +235,7 @@ export interface HarnessSkill {
|
|
|
235
235
|
minCapabilityProfile?: string | null
|
|
236
236
|
emoji?: string | null
|
|
237
237
|
setupV2?: HarnessSkillSetupV2 | null
|
|
238
|
+
displayName?: string | null
|
|
238
239
|
}
|
|
239
240
|
|
|
240
241
|
export interface HarnessSkillRequest {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qvac/harness",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"bare": ">=1.34.0"
|
|
@@ -747,7 +747,7 @@
|
|
|
747
747
|
"@qvac/inference": "0.21.0",
|
|
748
748
|
"@qvac/langdetect-text": "0.1.2",
|
|
749
749
|
"@qvac/sdk": "0.21.0",
|
|
750
|
-
"@qvac/skills": "0.1.
|
|
750
|
+
"@qvac/skills": "0.1.15",
|
|
751
751
|
"@xmldom/xmldom": "^0.9.12",
|
|
752
752
|
"asenion": "^0.1.0",
|
|
753
753
|
"b4a": "^1.9.0",
|