Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ed0619b
feat(agent): depersonalize the load_skill tool schema (shared cache p…
mattboon Aug 6, 2026
7564c82
docs(api): regenerate agent API reference for the load_skill change
mattboon Aug 6, 2026
9aa1ea8
Keep the skill-schema slice current with the runtime
kojiwakayama Aug 14, 2026
ea84f2c
Keep generated agent references aligned with the static schema
kojiwakayama Aug 14, 2026
115049b
Keep truncated skill IDs discoverable
kojiwakayama Aug 15, 2026
08aece2
Keep every authorized skill discoverable
kojiwakayama Aug 15, 2026
c44d39a
Integrate concurrent skill discovery coverage
kojiwakayama Aug 15, 2026
f6eaf6f
Keep canonical skill IDs callable from provider schemas
kojiwakayama Aug 15, 2026
ff897e6
Keep skill schema coverage on the BDD API
kojiwakayama Aug 15, 2026
7e2d507
Keep skill IDs discoverable with authored catalogs
kojiwakayama Aug 15, 2026
4d90142
Bound and refresh skill ID prompt inventories
kojiwakayama Aug 15, 2026
8b17e3c
Name the exhaustive skill IDs by authority
kojiwakayama Aug 15, 2026
759bb72
Override stale authored skills with an empty inventory
kojiwakayama Aug 15, 2026
f4cf037
fix(agent): page runtime skill discovery
kojiwakayama Aug 15, 2026
eb5545b
docs(agent): clarify hosted skill references
kojiwakayama Aug 15, 2026
344148f
Keep skill discovery complete under static tool copy
kojiwakayama Aug 15, 2026
a8f7118
fix(agent): bound skill discovery steps
kojiwakayama Aug 15, 2026
cf16916
Start hidden skill discovery after visible IDs
kojiwakayama Aug 15, 2026
563a462
fix(agent): continue paged skill discovery
kojiwakayama Aug 15, 2026
92a7880
Bound authored skill discovery fallback
kojiwakayama Aug 15, 2026
5ae35eb
fix(agent): bound authorized skill discovery
kojiwakayama Aug 15, 2026
aa706a3
Keep authorized skill discovery parseable
kojiwakayama Aug 15, 2026
6dcde65
Keep load_skill schema constraints provider-visible
kojiwakayama Aug 15, 2026
b98c16e
fix(agent): preserve skill input variants for providers
kojiwakayama Aug 15, 2026
c560a29
fix(agent): refresh skill discovery context
kojiwakayama Aug 15, 2026
c30e0d7
style(agent): format skill context refresh
kojiwakayama Aug 15, 2026
30ab742
fix(agent): replace generated skill catalogs on refresh
kojiwakayama Aug 15, 2026
be75774
fix(agent): preserve skill catalog provenance
kojiwakayama Aug 15, 2026
ce333ec
test(agent): pin generated skill catalog marker
kojiwakayama Aug 15, 2026
c8a45e4
fix(agent): deduplicate skill discovery cursors
kojiwakayama Aug 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 18 additions & 17 deletions docs/api-reference/veryfront/agent.md

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions docs/guides/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,8 @@ boundary for `load_skill`, not just a prompt filter.

Local and project runtimes also expose `load_skill_reference` and
`execute_skill_script`. Hosted chat reads an advertised reference through
`load_skill({ skillId, file })` and does not execute skill scripts directly.
`load_skill({ load: { skillId, file } })` and does not execute skill scripts
directly.

See [Project structure](./project-structure.md) for `skills/` conventions and
[Configuration](./configuration.md) for discovery paths.
Expand All @@ -291,9 +292,9 @@ See [Project structure](./project-structure.md) for `skills/` conventions and

When an agent uses a skill, the flow is:

1. Call `load_skill({ skillId })` to load the skill instructions and policy.
1. Call `load_skill({ load: { skillId } })` to load the skill instructions and policy.
2. Read an advertised reference with `load_skill_reference(...)` on local and
project runtimes, or `load_skill({ skillId, file })` in hosted chat.
project runtimes, or `load_skill({ load: { skillId, file } })` in hosted chat.
3. On local and project runtimes, optionally call
`execute_skill_script(...)` to run scripts from `scripts/`.
4. Continue with normal tool calls. Loading a skill does not change which
Expand Down
16 changes: 13 additions & 3 deletions docs/guides/skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,22 @@ supporting skill tools:

| Tool | Availability | Description |
| ---------------------- | -------------------------- | ---------------------------------------------------------- |
| `load_skill` | Every runtime | Load a skill's full instructions by ID |
| `load_skill` | Every runtime | List authorized IDs or load full instructions by ID |
Comment thread
kojiwakayama marked this conversation as resolved.
| `load_skill_reference` | Local and project runtimes | Read a file from `references/`, `resources/`, or `assets/` |
| `execute_skill_script` | Local and project runtimes | Execute a script from a skill (5-minute timeout) |

Hosted chat reads an advertised reference through
`load_skill({ skillId, file })`. It does not execute skill scripts directly.
After loading a skill, hosted chat can read only a reference listed by that
skill through `load_skill({ load: { skillId, file } })`. It does not execute
skill scripts directly. Direct tool consumers can continue to use the legacy
flat input forms.

When the prompt provides a discovery cursor, call
`load_skill({ inventory: { cursor: <CURSOR> } })`. Otherwise, call
`load_skill({ inventory: {} })` when the prompt does not show the complete
authorized skill inventory. The result contains a bounded `skillIds` page. If
it also contains `nextCursor`, call
`load_skill({ inventory: { cursor: nextCursor } })` until the response omits
`nextCursor`. Then call `load_skill({ load: { skillId } })` with a listed ID.

Discovered skills visible to the agent are advertised by default:

Expand Down
20 changes: 20 additions & 0 deletions src/agent/hosted/cloud-runtime-system-messages.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { assertEquals, assertStringIncludes } from "#veryfront/testing/assert.ts";
import { it } from "#veryfront/testing/bdd.ts";
import {
buildVeryfrontCloudRuntimeInstructions,
createVeryfrontCloudRuntimeSystemMessages,
Expand Down Expand Up @@ -139,3 +140,22 @@ Deno.test("buildVeryfrontCloudRuntimeInstructions adapts hosted preparation inpu
assertEquals(environmentMessage?.role, "system");
assertStringIncludes(environmentMessage?.content ?? "", "Runtime facts");
});

it("buildVeryfrontCloudRuntimeInstructions preserves an authoritative empty skill set", () => {
const [message] = buildVeryfrontCloudRuntimeInstructions({
agentConfig: createAgent({
instructions:
"Base\n\n<available_skills>\n- stale: Stale authored skill\n</available_skills>",
}),
projectId: "project-123",
branchId: null,
environmentContext: "",
instructions: "",
skills: [],
});

assertStringIncludes(
message?.content ?? "",
"<authorized_skill_ids>\n[]\n</authorized_skill_ids>",
);
});
2 changes: 1 addition & 1 deletion src/agent/hosted/cloud-runtime-system-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export function buildVeryfrontCloudRuntimeInstructions(
return createVeryfrontCloudRuntimeSystemMessages({
agent: input.agentConfig,
instructions: input.instructions || undefined,
skills: input.skills.length > 0 ? input.skills : undefined,
skills: input.skills,
projectId: input.projectId,
branchId: input.branchId,
environmentContext: input.environmentContext,
Expand Down
1 change: 1 addition & 0 deletions src/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1362,6 +1362,7 @@ export {
RUNTIME_LOAD_SKILL_DESCRIPTION,
type RuntimeLoadSkillBuiltinStore,
type RuntimeLoadSkillErrorOutput,
type RuntimeLoadSkillInventoryOutput,
type RuntimeLoadSkillReferenceFileOutput,
type RuntimeLoadSkillToolContext,
type RuntimeLoadSkillToolInput,
Expand Down
106 changes: 103 additions & 3 deletions src/agent/runtime/call-context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,18 +243,118 @@ describe("agent/runtime/call-context", () => {
assertEquals((messages[0]?.content ?? "").includes("Runtime facts"), false);
});

it("skips the skills block when the instructions already carry one", () => {
it("keeps authorized skill IDs when instructions already carry a skills block", () => {
const [message] = buildAgentCallContext({
instructions:
"Base\n\n<available_skills>\n- authored: An authored catalog\n</available_skills>",
skills: createSkills(),
});

assertEquals(
message?.content,
assertStringIncludes(
message?.content ?? "",
"Base\n\n<available_skills>\n- authored: An authored catalog\n</available_skills>",
);
assertEquals((message?.content ?? "").includes("Deployment guidance"), false);
assertStringIncludes(
message?.content ?? "",
'<authorized_skill_ids>\n["deploy","review"]\n</authorized_skill_ids>',
);
});

it("bounds authorized skill IDs beside an authored skills block", () => {
const skills = Array.from(
{ length: 1_000 },
(_, index) => ({
id: `skill-${index}-${"x".repeat(240)}`,
name: `skill-${index}`,
description: `Description ${index}`,
instructions: `Instructions ${index}`,
}),
);
const [message] = buildAgentCallContext({
instructions:
"Base\n\n<available_skills>\n- authored: An authored catalog\n</available_skills>",
skills,
});
const content = message?.content ?? "";
const cursorMatch = /Call load_skill\(\{ inventory: \{ cursor: (\d+) \} \}\)/.exec(
content,
);

assertEquals(cursorMatch === null, false);
assertEquals(Number(cursorMatch?.[1]), content.match(/"skill-/g)?.length);
assertEquals(content.length < 21_000, true);
});

it("emits an empty authorized inventory for an authoritative empty skill set", () => {
const [message] = buildAgentCallContext({
instructions:
"Base\n\n<available_skills>\n- stale: An authored catalog\n</available_skills>",
skills: [],
});

assertStringIncludes(
message?.content ?? "",
"<authorized_skill_ids>\n[]\n</authorized_skill_ids>",
);
});

it("replaces an earlier generated skill-ID fallback during recomposition", () => {
const [first] = buildAgentCallContext({
instructions:
"Base\n\n<available_skills>\n- authored: An authored catalog\n</available_skills>",
skills: createSkills(),
});
const [second] = buildAgentCallContext({
instructions: first?.content ?? "",
skills: [{
id: "audit",
name: "Audit",
description: "Audit guidance",
instructions: "Audit the change",
}],
});

assertEquals((second?.content ?? "").match(/<authorized_skill_ids>/g)?.length, 1);
assertStringIncludes(
second?.content ?? "",
'<authorized_skill_ids>\n["audit"]\n</authorized_skill_ids>',
);
assertEquals((second?.content ?? "").includes('["deploy"'), false);
});

it("removes an earlier skill-ID discovery cursor during recomposition", () => {
const largeCatalog = Array.from(
{ length: 1_000 },
(_, index) => ({
id: `skill-${index}-${"x".repeat(240)}`,
name: `skill-${index}`,
description: `Description ${index}`,
instructions: `Instructions ${index}`,
}),
);
const [first] = buildAgentCallContext({
instructions:
"Base\n\n<available_skills>\n- authored: An authored catalog\n</available_skills>",
skills: largeCatalog,
});
assertStringIncludes(first?.content ?? "", "<authorized_skill_id_discovery>");

const [second] = buildAgentCallContext({
instructions: first?.content ?? "",
skills: [{
id: "audit",
name: "Audit",
description: "Audit guidance",
instructions: "Audit the change",
}],
});

assertEquals((second?.content ?? "").includes("<authorized_skill_id_discovery>"), false);
assertStringIncludes(
second?.content ?? "",
'<authorized_skill_ids>\n["audit"]\n</authorized_skill_ids>',
);
});

it("still emits the skills block when the instructions only name the tag in prose", () => {
Expand Down
55 changes: 46 additions & 9 deletions src/agent/runtime/call-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
* 1. One cached system message holding, in order, the instructions before the
* runtime-context marker, `<project_instructions>`, `<project_context>`,
* any caller-supplied extra blocks, the instructions after the marker, and
* `<available_skills>`.
* `<available_skills>`, or an `<authorized_skill_ids>` fallback
* when the instructions already carry an authored skill catalog.
* 2. An uncached `<environment_context>` message.
*
* Only the instructions are unconditional: each block appears only when the
Expand All @@ -31,14 +32,19 @@

import type { ChatSystemMessage } from "#veryfront/chat/types.ts";
import { createRuntimePromptBlock } from "./prompt-block.ts";
import { buildRuntimeAvailableSkillsPromptBlock } from "./skill-prompt.ts";
import {
buildRuntimeAuthorizedSkillIdsPromptBlock,
buildRuntimeAvailableSkillsPromptBlock,
} from "./skill-prompt.ts";
import type { RuntimeSkillDefinition } from "./skill-metadata.ts";

/** Marker authored instructions use to place runtime blocks mid-prompt. */
export const DEFAULT_RUNTIME_AGENT_CONTEXT_MARKER = "<!-- veryfront-runtime-context -->";

const ENVIRONMENT_CONTEXT_BLOCK_NAME = "environment_context";
const AVAILABLE_SKILLS_BLOCK_NAME = "available_skills";
const AUTHORIZED_SKILL_IDS_BLOCK_NAME = "authorized_skill_ids";
const AUTHORIZED_SKILL_ID_DISCOVERY_BLOCK_NAME = "authorized_skill_id_discovery";

/** Project the call runs against, rendered as the `<project_context>` block. */
export type AgentCallProjectContext = {
Expand Down Expand Up @@ -123,11 +129,37 @@ function hasBlock(instructions: string, blockName: string): boolean {
return instructions.indexOf(`</${blockName}>`, openIndex) > openIndex;
}

function removeCompleteBlocks(instructions: string, blockName: string): string {
const openTag = `<${blockName}>`;
const closeTag = `</${blockName}>`;
let result = instructions;
let openIndex = result.indexOf(openTag);

while (openIndex >= 0) {
const closeIndex = result.indexOf(closeTag, openIndex + openTag.length);
if (closeIndex < 0) {
break;
}
const before = result.slice(0, openIndex).trimEnd();
const after = result.slice(closeIndex + closeTag.length).trimStart();
result = before.length > 0 && after.length > 0 ? `${before}\n\n${after}` : `${before}${after}`;
openIndex = result.indexOf(openTag);
}

return result;
}

/** Builds the complete system-message set for one provider call. */
export function buildAgentCallContext(input: BuildAgentCallContextInput): ChatSystemMessage[] {
const runtimeContextMarker = input.runtimeContextMarker ?? DEFAULT_RUNTIME_AGENT_CONTEXT_MARKER;
const sourceInstructions = input.skills === undefined
? input.instructions
: removeCompleteBlocks(
removeCompleteBlocks(input.instructions, AUTHORIZED_SKILL_IDS_BLOCK_NAME),
AUTHORIZED_SKILL_ID_DISCOVERY_BLOCK_NAME,
);
const instructions = splitInstructionsAtMarker({
instructions: input.instructions,
instructions: sourceInstructions,
runtimeContextMarker,
});

Expand All @@ -151,7 +183,7 @@ export function buildAgentCallContext(input: BuildAgentCallContextInput): ChatSy
continue;
}
const blockName = getBlockName(block);
if (blockName !== null && hasBlock(input.instructions, blockName)) {
if (blockName !== null && hasBlock(sourceInstructions, blockName)) {
continue;
}
staticParts.push(block);
Expand All @@ -161,10 +193,15 @@ export function buildAgentCallContext(input: BuildAgentCallContextInput): ChatSy
staticParts.push(instructions.after);
}

if (input.skills?.length && !hasBlock(input.instructions, AVAILABLE_SKILLS_BLOCK_NAME)) {
staticParts.push(
buildRuntimeAvailableSkillsPromptBlock(input.skills),
);
if (input.skills !== undefined) {
const hasAuthoredSkillCatalog = hasBlock(sourceInstructions, AVAILABLE_SKILLS_BLOCK_NAME);
if (input.skills.length > 0 || hasAuthoredSkillCatalog) {
Comment thread
kojiwakayama marked this conversation as resolved.
staticParts.push(
hasAuthoredSkillCatalog
? buildRuntimeAuthorizedSkillIdsPromptBlock(input.skills)
Comment thread
kojiwakayama marked this conversation as resolved.
: buildRuntimeAvailableSkillsPromptBlock(input.skills),
);
}
}

const messages: ChatSystemMessage[] = [
Expand All @@ -177,7 +214,7 @@ export function buildAgentCallContext(input: BuildAgentCallContextInput): ChatSy
},
];

if (input.environmentContext && !hasBlock(input.instructions, ENVIRONMENT_CONTEXT_BLOCK_NAME)) {
if (input.environmentContext && !hasBlock(sourceInstructions, ENVIRONMENT_CONTEXT_BLOCK_NAME)) {
messages.push({
role: "system",
content: createRuntimePromptBlock({
Expand Down
Loading
Loading