@selesai/code 0.13.32 → 0.13.34
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 +28 -0
- package/dist/extensions/capability-gateway/catalog.ts +4 -1
- package/dist/extensions/capability-gateway/index.test.ts +93 -4
- package/dist/extensions/capability-gateway/index.ts +294 -59
- package/dist/extensions/capability-gateway/integration.test.ts +71 -0
- package/dist/extensions/capability-gateway/routing.test.ts +154 -1
- package/dist/extensions/capability-gateway/routing.ts +227 -45
- package/dist/extensions/jev/decisions.test.ts +37 -0
- package/dist/extensions/jev/decisions.ts +148 -43
- package/dist/extensions/jev-ask-tool.test.ts +436 -0
- package/dist/extensions/jev-ask-tool.ts +587 -0
- package/dist/extensions/package.json +1 -0
- package/dist/extensions/pi-hermes-memory/README.md +11 -36
- package/dist/extensions/pi-hermes-memory/src/config.ts +36 -8
- package/dist/extensions/pi-hermes-memory/src/constants.ts +5 -5
- package/dist/extensions/pi-hermes-memory/src/handlers/auto-consolidate.ts +60 -46
- package/dist/extensions/pi-hermes-memory/tests/config.test.ts +26 -2
- package/dist/extensions/pi-hermes-memory/tests/handlers/auto-consolidate.test.ts +7 -1
- package/dist/extensions/pi-intercom/index.ts +5 -1
- package/dist/extensions/pi-subagents/agents/worker.md +3 -2
- package/dist/extensions/pi-subagents/docs/agents.md +2 -0
- package/dist/extensions/pi-subagents/docs/extension-api.md +3 -1
- package/dist/extensions/pi-subagents/docs/observability.md +2 -0
- package/dist/extensions/pi-subagents/docs/tool-reference.md +2 -2
- package/dist/extensions/pi-subagents/docs/workflows.md +1 -1
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +10 -1
- package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +3 -21
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +2 -1
- package/dist/extensions/pi-subagents/src/runs/background/run-child-session.ts +1 -0
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +5 -1
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +10 -3
- package/dist/extensions/pi-subagents/src/runs/background/workflow-terminal-proof.ts +67 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +6 -2
- package/dist/extensions/pi-subagents/src/runs/shared/child-tool-plan.ts +67 -5
- package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +6 -0
- package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +2 -1
- package/dist/extensions/pi-subagents/src/runs/shared/git-environment.ts +29 -0
- package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +69 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +20 -0
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +4 -238
- package/dist/extensions/pi-subagents/src/slash/subagent-cost.ts +280 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.part-3.test.ts +67 -0
- package/dist/extensions/pi-subagents/test/integration/in-process-child.test.ts +29 -1
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +6 -3
- package/dist/extensions/pi-subagents/test/integration/single-execution.part-2.test.ts +25 -0
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +5 -5
- package/dist/extensions/pi-subagents/test/unit/async-spawn-preload.test.ts +21 -0
- package/dist/extensions/pi-subagents/test/unit/child-tool-plan-permission-system.test.ts +98 -0
- package/dist/extensions/pi-subagents/test/unit/child-tool-plan.test.ts +11 -0
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +12 -0
- package/dist/extensions/pi-subagents/test/unit/external-cli-runner.test.ts +25 -0
- package/dist/extensions/pi-subagents/test/unit/git-environment.test.ts +38 -0
- package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +3 -1
- package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +105 -1
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +36 -0
- package/dist/extensions/pi-subagents/test/unit/structured-output-rejection.test.ts +67 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-terminal-proof.test.ts +98 -0
- package/dist/extensions/rtk.test.ts +21 -13
- package/dist/extensions/tps.test.ts +32 -1
- package/dist/extensions/tps.ts +3 -1
- package/dist/skills/pi-subagents/references/constraints-and-recipes.md +8 -8
- package/dist/skills/pi-subagents/references/execution-controls.md +1 -1
- package/dist/skills/pi-subagents/references/prompting-and-roles.md +1 -1
- package/docs/settings.md +46 -7
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@selesai/code` will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.13.34] - 2026-09-29
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **`ask_jev` lets the agent ask Jev typed questions.** One bounded call over the agent's own prose, up to eight file paths, and one command returns choice, score, or noul answers with confidences; file contents and command output are sent to Jev but never returned to the agent. On by default through `jevAdvisory.routes.ask`; without Token-In credentials the tool leaves the loadout until `/tokenin add`, and nothing is read or run.
|
|
9
|
+
- **`capability_discover({ job })` lets Jev pick the capability.** The agent describes what it is about to do and Jev answers which tool and which skill fit, each possibly `none`. It has its own timeout (`discoverTimeoutMs`, 5 s, capped at 15 s), and the option is left out of the instructions while Jev has no credential.
|
|
10
|
+
- **pi-hermes-memory reads its options from `settings.json`.** The `hermesMemory` object in the global settings takes precedence over the legacy `hermes-memory-config.json`, and consolidation shows progress in the status line.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **`ask_jev` paths are checked after resolving symlinks.** A symlink inside the working directory can no longer expose a file outside it, or a secret file under an innocent name.
|
|
14
|
+
- **Agents check capabilities before saying a tool is unavailable.** The capability instruction tells the agent to activate a named tool through `capability_discover` first, and an intercom message received while `intercom` is inactive says how to activate it.
|
|
15
|
+
- **Live tokens-per-second no longer reads ~1 token on Anthropic streams.** The live count takes the larger of the official and estimated counts, because the official count is only final at `message_delta`.
|
|
16
|
+
|
|
17
|
+
## [0.13.33] - 2026-09-26
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- **Subagent cost is exposed through extension RPC.** `cost` joins the versioned `subagents:rpc:v1` method list and returns the same parent-plus-child accounting `/subagent-cost` renders; `ping` advertises the report version. It is read-only, but it walks the current session branch and existing run artifacts, so callers should request it on turn boundaries rather than on a timer.
|
|
21
|
+
- **Async workflow status reports terminal proof.** `subagent({ action: "status" })` details now include `workflowTerminalProof`, the shared child-exit evidence for workflow runs.
|
|
22
|
+
- **Native children can use the scoped capability gateway.** A child launch loads the gateway when extension policy permits it, exposing `capability_catalog`, `capability_discover`, and `capability_skill_show` for tools in that child's effective registry. The gateway never installs or provides a missing tool provider, and `SELESAI_CAPABILITY_GATEWAY=0` still disables it.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- **The packaged `worker` agent starts from fresh context.** `worker` now declares `defaultContext: fresh` and `acceptanceRole: writer`; an explicit `context: "fork"` still wins.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- **Capability gateway controls no longer count as mutation capability.** A read-only child that receives `capability_catalog`, `capability_discover`, and `capability_skill_show` is still rejected for an implementation task with no mutation-capable tools, so the gateway does not disarm the implementation guard.
|
|
29
|
+
- **Structured output rejections keep their evidence.** Foreground and background runs now report the bounded rejection diagnostic — schema and validator failures summarized, submitted values redacted — instead of a generic missing-call error, and background results mark `structuredOutputFailed`.
|
|
30
|
+
- **`subagent_supervisor` requires fanout authorization.** A child that declares the supervisor reply tool without `subagent` in its effective tools allowlist (or `allowNestedSubagents`) now fails the launch instead of silently receiving the tool.
|
|
31
|
+
- **Git routing environment is stripped from child processes.** Background runners and external CLI default launches no longer inherit `GIT_DIR`, `GIT_WORK_TREE`, `GIT_INDEX_FILE`, and Git's other local environment variables, so a child's Git commands stay in its own working directory.
|
|
32
|
+
|
|
5
33
|
## [0.13.32] - 2026-09-24
|
|
6
34
|
|
|
7
35
|
### Fixed
|
|
@@ -52,8 +52,11 @@ export function buildToolCatalog(tools: ToolInfo[], gatewayToolNames: Set<string
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
export function buildSkillCatalog(skills: ResolvedSkillInfo[]): CatalogEntry[] {
|
|
55
|
+
// The same skill resolves once per source (user dir, package, project); the first one wins, as it
|
|
56
|
+
// does for loading. Duplicates would only spend a bounded Jev request twice on one name.
|
|
57
|
+
const seen = new Set<string>();
|
|
55
58
|
return skills
|
|
56
|
-
.filter((skill) => !skill.disableModelInvocation)
|
|
59
|
+
.filter((skill) => !skill.disableModelInvocation && !seen.has(skill.name) && seen.add(skill.name))
|
|
57
60
|
.map((skill) => ({
|
|
58
61
|
name: skill.name,
|
|
59
62
|
kind: "skill" as const,
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
toolSummary,
|
|
13
13
|
type CatalogEntry,
|
|
14
14
|
} from "./catalog.ts";
|
|
15
|
+
import { capabilityInstruction, describePick, invocationContract, schemaShape } from "./index.ts";
|
|
15
16
|
|
|
16
17
|
const tool = (overrides: Partial<ToolInfo>): ToolInfo =>
|
|
17
18
|
({
|
|
@@ -67,6 +68,16 @@ describe("catalog metadata", () => {
|
|
|
67
68
|
expect(entries[0]!.kind).toBe("skill");
|
|
68
69
|
});
|
|
69
70
|
|
|
71
|
+
it("lists a skill resolved from several sources once, keeping the first", () => {
|
|
72
|
+
const entries = buildSkillCatalog([
|
|
73
|
+
skill({ name: "to-prd", description: "Turn the conversation into a PRD.", category: "docs" }),
|
|
74
|
+
skill({ name: "research" }),
|
|
75
|
+
skill({ name: "to-prd", description: "Older copy." }),
|
|
76
|
+
]);
|
|
77
|
+
expect(entries.map((e) => e.name)).toEqual(["to-prd", "research"]);
|
|
78
|
+
expect(entries[0]!.category).toBe("docs");
|
|
79
|
+
});
|
|
80
|
+
|
|
70
81
|
it("falls back to the file frontmatter description when the resolved description is empty", () => {
|
|
71
82
|
const dir = mkdtempSync(join(tmpdir(), "gw-skill-"));
|
|
72
83
|
const filePath = join(dir, "SKILL.md");
|
|
@@ -85,9 +96,9 @@ describe("catalog metadata", () => {
|
|
|
85
96
|
|
|
86
97
|
describe("deterministic routing", () => {
|
|
87
98
|
const catalog: CatalogEntry[] = [
|
|
88
|
-
{ name: "grep_app_search", kind: "tool", summary: "Search public GitHub code", aliases: ["github-search"], category: "web" },
|
|
89
|
-
{ name: "grep_app_fetch", kind: "tool", summary: "Fetch a public GitHub file", aliases: [], category: "web" },
|
|
90
|
-
{ name: "research", kind: "skill", summary: "Investigate a question against primary sources", aliases: [], category: "research" },
|
|
99
|
+
{ name: "grep_app_search", kind: "tool", summary: "Search public GitHub code", aliases: ["github-search"], category: "web", eligible: true },
|
|
100
|
+
{ name: "grep_app_fetch", kind: "tool", summary: "Fetch a public GitHub file", aliases: [], category: "web", eligible: true },
|
|
101
|
+
{ name: "research", kind: "skill", summary: "Investigate a question against primary sources", aliases: [], category: "research", eligible: true },
|
|
91
102
|
];
|
|
92
103
|
|
|
93
104
|
it("auto-activates a unique high-confidence tool match", () => {
|
|
@@ -105,7 +116,7 @@ describe("deterministic routing", () => {
|
|
|
105
116
|
it("auto-activates a uniquely identifiable one-character tool typo", () => {
|
|
106
117
|
const result = route("activate inercom", [
|
|
107
118
|
...catalog,
|
|
108
|
-
{ name: "intercom", kind: "tool", summary: "Coordinate with other local sessions", aliases: [], category: "coordination" },
|
|
119
|
+
{ name: "intercom", kind: "tool", summary: "Coordinate with other local sessions", aliases: [], category: "coordination", eligible: true },
|
|
109
120
|
]);
|
|
110
121
|
expect(result.action).toBe("activate");
|
|
111
122
|
expect(result.entry?.name).toBe("intercom");
|
|
@@ -135,3 +146,81 @@ describe("deterministic routing", () => {
|
|
|
135
146
|
expect(a).toEqual(b);
|
|
136
147
|
});
|
|
137
148
|
});
|
|
149
|
+
|
|
150
|
+
describe("invocation contract", () => {
|
|
151
|
+
const toolWith = (parameters: unknown, overrides: Partial<ToolInfo> = {}) =>
|
|
152
|
+
tool({ name: "graft_find_code", description: "Find code by question.", parameters: parameters as never, ...overrides });
|
|
153
|
+
|
|
154
|
+
it("lists each parameter with its shape and required flag", () => {
|
|
155
|
+
const contract = invocationContract(
|
|
156
|
+
toolWith({
|
|
157
|
+
type: "object",
|
|
158
|
+
properties: {
|
|
159
|
+
question: { type: "string", description: "What you want to understand, in plain words." },
|
|
160
|
+
limit: { type: "number" },
|
|
161
|
+
},
|
|
162
|
+
required: ["question"],
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
expect(contract).toContain("graft_find_code — Find code by question.");
|
|
166
|
+
expect(contract).toContain("- question (string, required): What you want to understand, in plain words.");
|
|
167
|
+
expect(contract).toContain("- limit (number, optional)");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("names nested shapes instead of expanding them", () => {
|
|
171
|
+
expect(schemaShape({ type: "array", items: { type: "object" } })).toBe("array<object>");
|
|
172
|
+
expect(schemaShape({ anyOf: [{ type: "string" }, { type: "number" }] })).toBe("one of several");
|
|
173
|
+
expect(schemaShape({ enum: ["a", "b"] })).toBe('"a" | "b"');
|
|
174
|
+
expect(schemaShape({})).toBe("value");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("says so when a tool takes no parameters", () => {
|
|
178
|
+
expect(invocationContract(toolWith({ type: "object", properties: {} }))).toContain("Parameters: none");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("clips long text and never carries the schema itself", () => {
|
|
182
|
+
const contract = invocationContract(
|
|
183
|
+
toolWith({
|
|
184
|
+
type: "object",
|
|
185
|
+
properties: { question: { type: "string", description: "x".repeat(500) } },
|
|
186
|
+
required: ["question"],
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
expect(contract).toContain("…");
|
|
190
|
+
expect(contract).not.toContain('"type"');
|
|
191
|
+
expect(contract.split("\n").every((line) => line.length <= 200)).toBe(true);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe("capability instruction", () => {
|
|
196
|
+
it("mentions the job route only when Jev can answer it", () => {
|
|
197
|
+
expect(capabilityInstruction(true)).toContain("capability_discover with `job`");
|
|
198
|
+
const offline = capabilityInstruction(false);
|
|
199
|
+
expect(offline).not.toContain("`job`");
|
|
200
|
+
expect(offline).toContain("capability_catalog");
|
|
201
|
+
expect(offline).toContain("exact `name`");
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe("describePick", () => {
|
|
206
|
+
const side = (pick: Parameters<typeof describePick>[1]["pick"], offered = 3, dropped = 0) => ({ pick, offered, dropped });
|
|
207
|
+
|
|
208
|
+
it("reads a hesitant Jev as no confident pick rather than a failure", () => {
|
|
209
|
+
expect(describePick("Skill", side({ selected: false, reason: "low-confidence" }), undefined, "x")).toBe(
|
|
210
|
+
"Skill: no confident pick.",
|
|
211
|
+
);
|
|
212
|
+
expect(describePick("Tool", side({ selected: false, reason: "unquantified" }), undefined, "x")).toBe(
|
|
213
|
+
"Tool: no confident pick.",
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("keeps real failures, picks, and an honest none distinct", () => {
|
|
218
|
+
expect(describePick("Tool", side({ selected: false, reason: "timeout" }), undefined, "x")).toBe("Tool: no decision (timeout).");
|
|
219
|
+
expect(describePick("Tool", side({ selected: true, name: "t", confidence: 0.9 }), "t", "x")).toBe(
|
|
220
|
+
'Tool: Jev picked "t" (confidence 0.90).',
|
|
221
|
+
);
|
|
222
|
+
expect(describePick("Skill", side({ selected: false, reason: "none" }, 40, 9), undefined, "no procedure")).toContain(
|
|
223
|
+
"among 40 of 49 offered",
|
|
224
|
+
);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
@@ -10,20 +10,27 @@
|
|
|
10
10
|
* tools and Graft's code-context tools: they stay registered but are removed
|
|
11
11
|
* from the active tool set. Built-in tools are never touched.
|
|
12
12
|
* - A compact catalog tool lists eligible tools/skills with one-line summaries.
|
|
13
|
-
* - capability_discover
|
|
14
|
-
* definition for the current agent
|
|
15
|
-
* the
|
|
13
|
+
* - capability_discover readies exactly one capability. With `name` it validates
|
|
14
|
+
* a catalogued tool and activates its native definition for the current agent
|
|
15
|
+
* run; with `job` the agent describes what it is about to do and Jev answers,
|
|
16
|
+
* from catalog metadata alone, which tool and which skill fit (each possibly
|
|
17
|
+
* none), so the decision costs a bounded request instead of loading every schema. Either way the agent gets the
|
|
18
|
+
* chosen tool's compact invocation contract, and the real schema is live on
|
|
19
|
+
* its next turn. capability_skill_show loads exactly the selected skill
|
|
20
|
+
* instructions.
|
|
16
21
|
* - A deterministic router activates a uniquely matched tool before the run.
|
|
17
22
|
* Skills and ambiguous matches remain discoverable through the catalog
|
|
18
23
|
* without injecting fuzzy hints into the model context.
|
|
19
24
|
* - Default-on Jev-assisted routing (capabilityGateway.routing.jev in settings.json)
|
|
20
|
-
* is only a bounded tie-breaker: when the
|
|
21
|
-
* ambiguous lexical hint among optional tools,
|
|
22
|
-
* hinted tools (two or three) and the current
|
|
23
|
-
* model as one constrained choice question.
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
* has two callers. The host-side one is only a bounded tie-breaker: when the
|
|
26
|
+
* deterministic router returns an ambiguous lexical hint among optional tools,
|
|
27
|
+
* the gateway offers just those hinted tools (two or three) and the current
|
|
28
|
+
* prompt to the Jev decisions model as one constrained choice question. The
|
|
29
|
+
* agent-side one is capability_discover's `job` argument, which offers the
|
|
30
|
+
* catalog metadata of every offered capability. Jev may answer `none` or one
|
|
31
|
+
* canonical name; the gateway revalidates the choice against the live catalog
|
|
32
|
+
* and activates it for the current run only. No hint, a unique activation, a
|
|
33
|
+
* skill match, or an already-activated tool never reaches the host-side path.
|
|
27
34
|
* Every Jev failure is an ordinary abstention that leaves deterministic
|
|
28
35
|
* behavior in place. Without Token-In credentials, no Jev request is sent and
|
|
29
36
|
* the user is prompted to add an account with `/tokenin add`.
|
|
@@ -37,7 +44,7 @@
|
|
|
37
44
|
|
|
38
45
|
import { readFileSync } from "node:fs";
|
|
39
46
|
import { dirname } from "node:path";
|
|
40
|
-
import { stripFrontmatter, type ExtensionAPI, type ToolInfo } from "@selesai/code";
|
|
47
|
+
import { stripFrontmatter, type ExtensionAPI, type ExtensionContext, type ToolInfo } from "@selesai/code";
|
|
41
48
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
42
49
|
import { Text } from "@earendil-works/pi-tui";
|
|
43
50
|
import { Type } from "typebox";
|
|
@@ -45,24 +52,30 @@ import {
|
|
|
45
52
|
buildSkillCatalog,
|
|
46
53
|
buildToolCatalog,
|
|
47
54
|
BUILTIN_TOOL_NAMES,
|
|
55
|
+
firstSentence,
|
|
48
56
|
route,
|
|
49
57
|
type CatalogEntry,
|
|
50
58
|
} from "./catalog.ts";
|
|
51
59
|
import {
|
|
60
|
+
gatewayJevConnection,
|
|
52
61
|
hintedToolCandidates,
|
|
53
62
|
MIN_GATEWAY_JEV_CANDIDATES,
|
|
54
63
|
readGatewayJevConfig,
|
|
64
|
+
routeJobToJev,
|
|
55
65
|
routeToJevTool,
|
|
66
|
+
type JevJobPick,
|
|
56
67
|
JEV_UNAVAILABLE_REASONS,
|
|
57
68
|
} from "./routing.ts";
|
|
58
|
-
import { confidenceBucket } from "../jev/decisions.ts";
|
|
69
|
+
import { confidenceBucket, jevUnavailable, warnJevUnavailableOnce } from "../jev/decisions.ts";
|
|
59
70
|
|
|
60
71
|
export const GATEWAY_ENV = "SELESAI_CAPABILITY_GATEWAY";
|
|
61
72
|
export const GATEWAY_TOOLS = new Set(["capability_catalog", "capability_discover", "capability_skill_show"]);
|
|
62
73
|
|
|
63
74
|
// Graft supplies pre-turn hybrid context and must remain callable for precise
|
|
64
|
-
// follow-ups; making it dormant defeats both paths.
|
|
75
|
+
// follow-ups; making it dormant defeats both paths. `ask_jev` is the agent's own
|
|
76
|
+
// decision surface for Jev, so it is never something the agent must discover.
|
|
65
77
|
const ALWAYS_ACTIVE_EXTENSION_TOOLS = new Set([
|
|
78
|
+
"ask_jev",
|
|
66
79
|
"graft_check_freshness",
|
|
67
80
|
"graft_file_api",
|
|
68
81
|
"graft_find_all",
|
|
@@ -71,11 +84,88 @@ const ALWAYS_ACTIVE_EXTENSION_TOOLS = new Set([
|
|
|
71
84
|
"graft_trace_calls",
|
|
72
85
|
]);
|
|
73
86
|
|
|
74
|
-
|
|
75
|
-
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
87
|
+
const JOB_ROUTE_LINE =
|
|
88
|
+
"- Call capability_discover with `job` (what you are about to do, in your own words) when a task may need a specialized capability you do not have active. Jev answers two things from metadata alone: which tool (callable code, usable many times) and which skill (a written procedure, read once), each possibly none. A chosen tool comes back with its parameters and is callable on your next turn; a chosen skill is loaded with capability_skill_show.";
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The single skill-index entry the gateway installs. The `job` line is offered only while Jev can
|
|
92
|
+
* answer it: telling the agent about a route that always fails costs it a turn every time.
|
|
93
|
+
*/
|
|
94
|
+
export function capabilityInstruction(jobRoute: boolean): string {
|
|
95
|
+
return [
|
|
96
|
+
"Optional capabilities (extension tools and skills) are not listed here by default. To use one:",
|
|
97
|
+
...(jobRoute ? [JOB_ROUTE_LINE] : []),
|
|
98
|
+
jobRoute
|
|
99
|
+
? '- Call capability_discover with an exact `name`, or search the compact catalog with capability_catalog (kind: "tool" or "skill", natural-language query) when you already know what you are looking for.'
|
|
100
|
+
: '- Search the compact catalog with capability_catalog (kind: "tool" or "skill", natural-language query), then call capability_discover with the exact `name`.',
|
|
101
|
+
"- Load a skill's full instructions with capability_skill_show before applying it.",
|
|
102
|
+
"Never invent optional tool names, actions, or fields; discover them first.",
|
|
103
|
+
"Never tell the user a tool is unavailable, or that you cannot do something, until you have checked for it here: a tool named in a message, instruction, or task that is not in your tool list is usually an optional capability, so call capability_discover with its `name` and use it.",
|
|
104
|
+
].join("\n");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const CAPABILITY_INSTRUCTION = capabilityInstruction(true);
|
|
108
|
+
|
|
109
|
+
/** Longest description kept in an invocation contract; the full text is live in the schema. */
|
|
110
|
+
export const MAX_CONTRACT_DESCRIPTION_CHARS = 240;
|
|
111
|
+
/** Longest per-parameter description kept in an invocation contract. */
|
|
112
|
+
export const MAX_CONTRACT_PARAMETER_CHARS = 120;
|
|
113
|
+
|
|
114
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
115
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Gateway tool results: text for the model, plus a shape-only details payload persisted in the session. */
|
|
119
|
+
type ToolAnswer = { content: Array<{ type: "text"; text: string }>; details: Record<string, unknown> };
|
|
120
|
+
|
|
121
|
+
function clip(text: string, max: number): string {
|
|
122
|
+
const trimmed = text.trim();
|
|
123
|
+
return trimmed.length > max ? `${trimmed.slice(0, max)}…` : trimmed;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** A parameter's shape in one token; nested schemas are named, never expanded. */
|
|
127
|
+
export function schemaShape(schema: Record<string, unknown>): string {
|
|
128
|
+
if (Array.isArray(schema.enum)) return schema.enum.map((value) => JSON.stringify(value)).join(" | ");
|
|
129
|
+
if (Array.isArray(schema.anyOf) || Array.isArray(schema.oneOf)) return "one of several";
|
|
130
|
+
if (schema.type === "array") {
|
|
131
|
+
const items = isRecord(schema.items) ? schema.items : {};
|
|
132
|
+
return `array<${typeof items.type === "string" ? items.type : "value"}>`;
|
|
133
|
+
}
|
|
134
|
+
return typeof schema.type === "string" ? schema.type : "value";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The compact invocation contract for one tool: what it does and the parameters it takes.
|
|
139
|
+
*
|
|
140
|
+
* Deliberately not the schema itself. An activated tool is callable on the next turn, where the
|
|
141
|
+
* provider already carries the full schema, so repeating it here would pay for the same tokens
|
|
142
|
+
* twice. This is enough to plan the call and to abandon it before loading anything.
|
|
143
|
+
*/
|
|
144
|
+
export function invocationContract(tool: ToolInfo): string {
|
|
145
|
+
const schema = isRecord(tool.parameters) ? tool.parameters : {};
|
|
146
|
+
const properties = isRecord(schema.properties) ? schema.properties : {};
|
|
147
|
+
const required = new Set(
|
|
148
|
+
Array.isArray(schema.required) ? schema.required.filter((name): name is string => typeof name === "string") : [],
|
|
149
|
+
);
|
|
150
|
+
const lines = [
|
|
151
|
+
`${tool.name} — ${clip(tool.discovery?.summary?.trim() || firstSentence(tool.description) || tool.name, MAX_CONTRACT_DESCRIPTION_CHARS)}`,
|
|
152
|
+
];
|
|
153
|
+
const parameters = Object.entries(properties);
|
|
154
|
+
if (parameters.length === 0) {
|
|
155
|
+
lines.push("Parameters: none");
|
|
156
|
+
return lines.join("\n");
|
|
157
|
+
}
|
|
158
|
+
lines.push("Parameters:");
|
|
159
|
+
for (const [name, raw] of parameters) {
|
|
160
|
+
const property = isRecord(raw) ? raw : {};
|
|
161
|
+
const description =
|
|
162
|
+
typeof property.description === "string"
|
|
163
|
+
? `: ${clip(firstSentence(property.description), MAX_CONTRACT_PARAMETER_CHARS)}`
|
|
164
|
+
: "";
|
|
165
|
+
lines.push(`- ${name} (${schemaShape(property)}, ${required.has(name) ? "required" : "optional"})${description}`);
|
|
166
|
+
}
|
|
167
|
+
return lines.join("\n");
|
|
168
|
+
}
|
|
79
169
|
|
|
80
170
|
function isEnabled(): boolean {
|
|
81
171
|
return process.env[GATEWAY_ENV] !== "0";
|
|
@@ -143,6 +233,24 @@ function emitTelemetry(pi: ExtensionAPI, event: string, data: Record<string, unk
|
|
|
143
233
|
}
|
|
144
234
|
}
|
|
145
235
|
|
|
236
|
+
/** One kind's line: the pick, `none` (honest about what Jev did not see), or why there is no answer. */
|
|
237
|
+
export function describePick(label: string, side: JevJobPick, chosen: string | undefined, noneMeans: string): string {
|
|
238
|
+
const notSeen =
|
|
239
|
+
side.dropped > 0
|
|
240
|
+
? ` — among ${side.offered} of ${side.offered + side.dropped} offered; the rest were not considered, so search capability_catalog if one might fit`
|
|
241
|
+
: "";
|
|
242
|
+
if (side.pick.selected) {
|
|
243
|
+
return chosen
|
|
244
|
+
? `${label}: Jev picked "${chosen}" (confidence ${side.pick.confidence.toFixed(2)}).`
|
|
245
|
+
: `${label}: Jev picked "${side.pick.name}", which is no longer catalogued; nothing was readied.`;
|
|
246
|
+
}
|
|
247
|
+
if (side.pick.reason === "none") return `${label}: none — ${noneMeans}${notSeen}.`;
|
|
248
|
+
if (side.offered === 0) return `${label}: none catalogued.`;
|
|
249
|
+
// Jev leaned somewhere but not firmly enough to act on: for the agent that is "nothing clearly fits".
|
|
250
|
+
if (side.pick.reason === "low-confidence" || side.pick.reason === "unquantified") return `${label}: no confident pick${notSeen}.`;
|
|
251
|
+
return `${label}: no decision (${side.pick.reason}).`;
|
|
252
|
+
}
|
|
253
|
+
|
|
146
254
|
/** Where a run-local tool activation came from; recorded so use telemetry can attribute it. */
|
|
147
255
|
type ActivationSource = "deterministic" | "jev" | "discover";
|
|
148
256
|
|
|
@@ -152,7 +260,22 @@ export default function capabilityGatewayExtension(pi: ExtensionAPI): void {
|
|
|
152
260
|
// Tools this gateway activated for the current run, and whether they were invoked.
|
|
153
261
|
// Cleared at agent_settled with the activations themselves.
|
|
154
262
|
const activations = new Map<string, { source: ActivationSource; used: boolean }>();
|
|
155
|
-
|
|
263
|
+
// Whether the skill index currently offers the `job` route; re-derived before every run.
|
|
264
|
+
let jobRouteOffered = true;
|
|
265
|
+
|
|
266
|
+
/** Replace the eager skill index with one compact capability entry. Full instructions load only on show. */
|
|
267
|
+
function installSkillIndex(): void {
|
|
268
|
+
pi.setSkillsIndexFilter(() => [
|
|
269
|
+
{
|
|
270
|
+
name: "capability-gateway",
|
|
271
|
+
description: capabilityInstruction(jobRouteOffered),
|
|
272
|
+
filePath: "<capability-gateway>",
|
|
273
|
+
baseDir: "<capability-gateway>",
|
|
274
|
+
sourceInfo: { path: "<capability-gateway>", source: "builtin", scope: "user", origin: "top-level" },
|
|
275
|
+
disableModelInvocation: false,
|
|
276
|
+
},
|
|
277
|
+
]);
|
|
278
|
+
}
|
|
156
279
|
|
|
157
280
|
/** Activate one tool for the current run, keeping the rest of the loadout untouched. */
|
|
158
281
|
function activateTool(name: string, source: ActivationSource): void {
|
|
@@ -173,18 +296,7 @@ export default function capabilityGatewayExtension(pi: ExtensionAPI): void {
|
|
|
173
296
|
(name) => !eligibleTools(pi).some((tool) => tool.name === name),
|
|
174
297
|
);
|
|
175
298
|
pi.setActiveTools(keep);
|
|
176
|
-
|
|
177
|
-
// instruction entry. Full skill instructions load only on show/invoke.
|
|
178
|
-
pi.setSkillsIndexFilter(() => [
|
|
179
|
-
{
|
|
180
|
-
name: "capability-gateway",
|
|
181
|
-
description: CAPABILITY_INSTRUCTION,
|
|
182
|
-
filePath: "<capability-gateway>",
|
|
183
|
-
baseDir: "<capability-gateway>",
|
|
184
|
-
sourceInfo: { path: "<capability-gateway>", source: "builtin", scope: "user", origin: "top-level" },
|
|
185
|
-
disableModelInvocation: false,
|
|
186
|
-
},
|
|
187
|
-
]);
|
|
299
|
+
installSkillIndex();
|
|
188
300
|
emitTelemetry(pi, "session_start", { baselineCount: baseline.length, dormantCount: baseline.length - keep.length });
|
|
189
301
|
void ctx;
|
|
190
302
|
});
|
|
@@ -202,7 +314,7 @@ export default function capabilityGatewayExtension(pi: ExtensionAPI): void {
|
|
|
202
314
|
query: Type.Optional(Type.String({ minLength: 1, description: "Natural-language query; omit to list all." })),
|
|
203
315
|
kind: Type.Optional(StringEnum(["tool", "skill"] as const, { description: "Filter by capability kind." })),
|
|
204
316
|
}),
|
|
205
|
-
async execute(_id, params) {
|
|
317
|
+
async execute(_id, params): Promise<ToolAnswer> {
|
|
206
318
|
const entries = catalogEntries(pi);
|
|
207
319
|
const filtered = entries.filter(
|
|
208
320
|
(entry) => !params.kind || entry.kind === params.kind,
|
|
@@ -240,39 +352,161 @@ export default function capabilityGatewayExtension(pi: ExtensionAPI): void {
|
|
|
240
352
|
},
|
|
241
353
|
});
|
|
242
354
|
|
|
355
|
+
/** Make one catalogued tool callable for this run and return how to invoke it. */
|
|
356
|
+
function readyTool(entry: CatalogEntry, source: ActivationSource): string {
|
|
357
|
+
// Already in the loadout (routed before the turn, or discovered earlier): re-activating would
|
|
358
|
+
// only misattribute it, and repeating the contract teaches nothing. Say what the pick means.
|
|
359
|
+
if (pi.getActiveTools().includes(entry.name)) {
|
|
360
|
+
return `"${entry.name}" is already active, so nothing changed. If it failed, the problem is in the tool itself (its error says what), not in choosing it: fix that, or pick a different capability by name.`;
|
|
361
|
+
}
|
|
362
|
+
activateTool(entry.name, source);
|
|
363
|
+
const tool = eligibleTools(pi).find((candidate) => candidate.name === entry.name);
|
|
364
|
+
return [
|
|
365
|
+
`Activated "${entry.name}" for this run: call it on your next turn, with its full schema live in your tool list.`,
|
|
366
|
+
"",
|
|
367
|
+
tool ? invocationContract(tool) : `${entry.name} — ${entry.summary}`,
|
|
368
|
+
].join("\n");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* The agent's on-demand route. Jev decides over catalog metadata only, so the decision costs one
|
|
373
|
+
* bounded request instead of every schema. A tool and a skill are separate answers: a picked tool
|
|
374
|
+
* is activated with its invocation contract, a picked skill is named for capability_skill_show.
|
|
375
|
+
*/
|
|
376
|
+
async function discoverByJob(job: string, kind: "tool" | "skill" | "both", ctx: ExtensionContext): Promise<ToolAnswer> {
|
|
377
|
+
const answer = (text: string, details: Record<string, unknown> = {}): ToolAnswer => ({
|
|
378
|
+
content: [{ type: "text" as const, text }],
|
|
379
|
+
details,
|
|
380
|
+
});
|
|
381
|
+
const config = readGatewayJevConfig();
|
|
382
|
+
if (!config.enabled) {
|
|
383
|
+
return answer(
|
|
384
|
+
"Jev capability routing is off (capabilityGateway.routing.jev.enabled is false). Browse with capability_catalog and pass an exact `name`.",
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
const entries = catalogEntries(pi);
|
|
388
|
+
const tools = kind === "skill" ? [] : entries.filter((entry) => entry.kind === "tool");
|
|
389
|
+
const skills = kind === "tool" ? [] : entries.filter((entry) => entry.kind === "skill");
|
|
390
|
+
const route = await routeJobToJev(tools, skills, job, ctx, config);
|
|
391
|
+
if (route.tool.offered + route.skill.offered === 0) {
|
|
392
|
+
return answer(
|
|
393
|
+
"No catalogued capability fits one decision request. Narrow it with capability_catalog and pass an exact `name`.",
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Revalidate each accepted name against the live catalog: one that vanished mid-call is gone.
|
|
398
|
+
const live = catalogEntries(pi);
|
|
399
|
+
const resolve = ({ pick }: JevJobPick, of: "tool" | "skill") =>
|
|
400
|
+
pick.selected ? live.find((entry) => entry.kind === of && entry.name === pick.name) : undefined;
|
|
401
|
+
const tool = resolve(route.tool, "tool");
|
|
402
|
+
const skill = resolve(route.skill, "skill");
|
|
403
|
+
// Jev could not be reached at all (as opposed to answering `none`): one reason covers both questions.
|
|
404
|
+
const unavailable = [route.tool.pick, route.skill.pick].flatMap((pick) =>
|
|
405
|
+
!pick.selected && JEV_UNAVAILABLE_REASONS.has(pick.reason) ? [pick.reason] : [],
|
|
406
|
+
)[0];
|
|
407
|
+
emitTelemetry(pi, "route", {
|
|
408
|
+
source: "agent",
|
|
409
|
+
outcome: tool || skill ? "selected" : unavailable ? "unavailable" : "abstained",
|
|
410
|
+
...(tool ? { tool: tool.name } : {}),
|
|
411
|
+
...(skill ? { skill: skill.name } : {}),
|
|
412
|
+
candidates: route.tool.offered + route.skill.offered,
|
|
413
|
+
dropped: route.tool.dropped + route.skill.dropped,
|
|
414
|
+
durationMs: route.elapsedMs,
|
|
415
|
+
});
|
|
416
|
+
if (!tool && !skill && (unavailable === "no-credential" || unavailable === "no-template")) {
|
|
417
|
+
if (ctx.hasUI) warnJevUnavailableOnce(ctx.ui, config.provider);
|
|
418
|
+
return answer(
|
|
419
|
+
"Choosing by `job` needs Jev, which has no credential in this session. Search capability_catalog and pass an exact `name` instead, or use the built-in tools.",
|
|
420
|
+
{ selected: false, reason: unavailable },
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
if (!tool && !skill && unavailable) {
|
|
424
|
+
return answer(
|
|
425
|
+
`Jev could not decide this job (${unavailable}). Browse with capability_catalog and pass an exact \`name\`, or use the built-in tools.`,
|
|
426
|
+
{ selected: false, reason: unavailable },
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const lines: string[] = [];
|
|
431
|
+
const details: Record<string, unknown> = { selected: Boolean(tool || skill) };
|
|
432
|
+
if (kind !== "skill") {
|
|
433
|
+
lines.push(describePick("Tool", route.tool, tool?.name, "the built-in tools (read, bash, edit, write, grep, find, ls) or a direct answer are enough"));
|
|
434
|
+
details.tool = tool?.name ?? null;
|
|
435
|
+
}
|
|
436
|
+
if (kind !== "tool") {
|
|
437
|
+
lines.push(describePick("Skill", route.skill, skill?.name, "no written procedure is needed"));
|
|
438
|
+
if (skill) lines.push(` ${skill.summary}\n Load it with capability_skill_show before applying it.`);
|
|
439
|
+
details.skill = skill?.name ?? null;
|
|
440
|
+
}
|
|
441
|
+
if (tool) lines.push("", readyTool(tool, "jev"));
|
|
442
|
+
return answer(lines.join("\n"), details);
|
|
443
|
+
}
|
|
444
|
+
|
|
243
445
|
pi.registerTool({
|
|
244
446
|
name: "capability_discover",
|
|
245
447
|
label: "Capability Discover",
|
|
246
448
|
description:
|
|
247
|
-
"
|
|
248
|
-
promptSnippet: "
|
|
449
|
+
"Ready optional capabilities without loading them. Pass `job` (what you are about to do) and Jev answers, from catalog metadata alone, which tool (callable code from an extension or MCP server, usable many times) and which skill (a written procedure from a user, agent, or teammate, read once) fit it — each may be `none`. A chosen tool is activated for this run and its parameters are returned, so you can call it on your next turn; a chosen skill comes back by name for capability_skill_show. Pass `name` instead when capability_catalog already gave you one.",
|
|
450
|
+
promptSnippet: "Let Jev pick the capability for a job (or name one), then see how to invoke it",
|
|
249
451
|
parameters: Type.Object({
|
|
250
|
-
|
|
452
|
+
job: Type.Optional(
|
|
453
|
+
Type.String({ minLength: 1, description: "What you are about to do, in your own words. Jev picks the capability." }),
|
|
454
|
+
),
|
|
455
|
+
name: Type.Optional(Type.String({ minLength: 1, description: "Exact catalogued name, when you already know it." })),
|
|
456
|
+
kind: Type.Optional(
|
|
457
|
+
StringEnum(["tool", "skill", "both"] as const, {
|
|
458
|
+
description: "Ask only about tools or only about skills. Default: both, answered separately.",
|
|
459
|
+
}),
|
|
460
|
+
),
|
|
251
461
|
}),
|
|
252
|
-
async execute(_id, params) {
|
|
462
|
+
async execute(_id, params, _signal, _onUpdate, ctx: ExtensionContext): Promise<ToolAnswer> {
|
|
463
|
+
const job = params.job?.trim();
|
|
464
|
+
const name = params.name?.trim();
|
|
465
|
+
if (job && name) {
|
|
466
|
+
return {
|
|
467
|
+
content: [{ type: "text", text: "Pass either `job` (let Jev choose) or `name` (you know it), not both." }],
|
|
468
|
+
details: {},
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
if (job) return discoverByJob(job, params.kind ?? "both", ctx);
|
|
472
|
+
if (!name) {
|
|
473
|
+
return {
|
|
474
|
+
content: [
|
|
475
|
+
{ type: "text", text: "Pass `job` to have Jev choose a capability, or `name` from the catalog." },
|
|
476
|
+
],
|
|
477
|
+
details: {},
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
253
481
|
const entries = catalogEntries(pi);
|
|
254
|
-
const entry = findEntry(entries,
|
|
255
|
-
if (!entry
|
|
482
|
+
const entry = findEntry(entries, name);
|
|
483
|
+
if (!entry) {
|
|
256
484
|
return {
|
|
257
485
|
content: [
|
|
258
486
|
{
|
|
259
487
|
type: "text",
|
|
260
|
-
text: `Unknown
|
|
488
|
+
text: `Unknown capability "${name}". Search capability_catalog for the exact name, or pass a \`job\` and let Jev choose.`,
|
|
261
489
|
},
|
|
262
490
|
],
|
|
263
491
|
details: { activated: false },
|
|
264
492
|
};
|
|
265
493
|
}
|
|
266
|
-
|
|
267
|
-
|
|
494
|
+
if (entry.kind === "skill") {
|
|
495
|
+
return {
|
|
496
|
+
content: [
|
|
497
|
+
{
|
|
498
|
+
type: "text",
|
|
499
|
+
text: `"${entry.name}" is a skill: call capability_skill_show with that name to load its instructions.`,
|
|
500
|
+
},
|
|
501
|
+
],
|
|
502
|
+
details: { activated: false, skill: entry.name },
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
emitTelemetry(pi, "discover", { source: "name", tool: entry.name });
|
|
506
|
+
const alreadyActive = pi.getActiveTools().includes(entry.name);
|
|
268
507
|
return {
|
|
269
|
-
content: [
|
|
270
|
-
|
|
271
|
-
type: "text",
|
|
272
|
-
text: `Activated "${entry.name}" for this run. Call it normally on the next turn; it is removed when the run ends.`,
|
|
273
|
-
},
|
|
274
|
-
],
|
|
275
|
-
details: { activated: true, tool: entry.name },
|
|
508
|
+
content: [{ type: "text", text: readyTool(entry, "discover") }],
|
|
509
|
+
details: { activated: true, tool: entry.name, ...(alreadyActive ? { alreadyActive: true } : {}) },
|
|
276
510
|
};
|
|
277
511
|
},
|
|
278
512
|
});
|
|
@@ -286,7 +520,7 @@ export default function capabilityGatewayExtension(pi: ExtensionAPI): void {
|
|
|
286
520
|
parameters: Type.Object({
|
|
287
521
|
name: Type.String({ minLength: 1, description: "Exact skill name." }),
|
|
288
522
|
}),
|
|
289
|
-
async execute(_id, params) {
|
|
523
|
+
async execute(_id, params): Promise<ToolAnswer> {
|
|
290
524
|
const skill = pi.getResolvedSkills().find((s) => s.name === params.name);
|
|
291
525
|
if (!skill) {
|
|
292
526
|
return {
|
|
@@ -324,6 +558,15 @@ export default function capabilityGatewayExtension(pi: ExtensionAPI): void {
|
|
|
324
558
|
// only a bounded tie-breaker for its ambiguous/hint result.
|
|
325
559
|
// ------------------------------------------------------------------
|
|
326
560
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
561
|
+
// Offer the `job` route only while Jev can answer it; `/tokenin add` brings it back on the
|
|
562
|
+
// next run without a reload. Silent: the warning belongs to a path that actually needed Jev.
|
|
563
|
+
const jevConfig = readGatewayJevConfig();
|
|
564
|
+
const jobReady = jevConfig.enabled && (await jevUnavailable(ctx, gatewayJevConnection(jevConfig))) === undefined;
|
|
565
|
+
if (jobReady !== jobRouteOffered) {
|
|
566
|
+
jobRouteOffered = jobReady;
|
|
567
|
+
installSkillIndex();
|
|
568
|
+
}
|
|
569
|
+
|
|
327
570
|
const entries = catalogEntries(pi);
|
|
328
571
|
const result = routePrompt(event.prompt, entries);
|
|
329
572
|
if (result.action === "activate" && result.entry) {
|
|
@@ -351,16 +594,8 @@ export default function capabilityGatewayExtension(pi: ExtensionAPI): void {
|
|
|
351
594
|
emitTelemetry(pi, "route", { source: "jev", outcome: "attempt", candidates: candidates.length });
|
|
352
595
|
const jevRoute = await routeToJevTool(candidates, event.prompt, ctx, config);
|
|
353
596
|
if (!jevRoute.selected) {
|
|
354
|
-
if (
|
|
355
|
-
|
|
356
|
-
config.provider === "tokenin" &&
|
|
357
|
-
(jevRoute.reason === "no-credential" || jevRoute.reason === "no-template")
|
|
358
|
-
) {
|
|
359
|
-
tokenInSetupPrompted = true;
|
|
360
|
-
ctx.ui.notify(
|
|
361
|
-
"Jev tool tie-breaking needs a Token-In account. Add one with /tokenin add; deterministic routing will keep working meanwhile.",
|
|
362
|
-
"warning",
|
|
363
|
-
);
|
|
597
|
+
if ((jevRoute.reason === "no-credential" || jevRoute.reason === "no-template") && ctx.hasUI) {
|
|
598
|
+
warnJevUnavailableOnce(ctx.ui, config.provider);
|
|
364
599
|
}
|
|
365
600
|
emitTelemetry(pi, "route", {
|
|
366
601
|
source: "jev",
|