update agents skill design

This commit is contained in:
2026-08-20 18:18:10 +05:30
parent 161b237695
commit b2e6868824
75 changed files with 14586 additions and 98 deletions

177
src/lib/skills/tools.js Normal file
View File

@@ -0,0 +1,177 @@
import { skillsForContext } from './registry';
import { ACTION_NAMES } from './actions';
/**
* Tools, described.
*
* Boundary 6. This adds **no capability**: every tool here is an action
* `actions.js` already performs, and `runAction` remains the only thing that
* performs them. What was missing was a description — what a tool does, what it
* needs, whether it changes anything, and whether a person should be asked
* first. Without that, a caller deciding whether to confirm an action had to
* hard-code a list of which ones were dangerous.
*
* Describing them separately is also what makes them exposable later. A future
* MCP surface publishes these descriptors and calls the same `runAction`;
* nothing in the business logic moves. That is the whole reason this file is a
* table rather than a set of wrappers.
*
* **The page boundary is inherited, not restated.** `toolsForContext` reads the
* skills that are reachable on the current page for the current agent, and
* collects what *they* declare. A tool is therefore reachable only when a skill
* on this page declares it and the agent carries that skill — so a tool can
* never reach data the page was not already offering, and selecting a different
* agent can only ever remove tools from that list.
*/
/**
* What each action is, in the terms a person confirming it would need.
*
* `requiresApproval` is a property of the action, never of the caller: an
* action that writes a record needs a person to agree whichever surface asked
* for it. `readOnly` actions move the reader somewhere and change nothing.
*/
export const TOOLS = [
{
name: 'create_position',
label: 'Create position',
summary: 'Writes a new job posting from a draft collected in conversation.',
params: ['draft', 'status'],
readOnly: false,
mutates: 'JobPosting',
/* Writes a record other people will act on. Always confirmed. */
requiresApproval: true,
},
{
name: 'open_create_skill_training',
label: 'Open Add Skill Training',
summary: 'Opens the Forge authoring form, prefilled from the conversation.',
params: ['prefill'],
readOnly: true,
mutates: null,
/* Opens a form. Nothing is written until the person submits it, so asking
twice would be asking about the same decision twice. */
requiresApproval: false,
},
{
name: 'open_create_training',
label: 'Open Add Training',
summary: 'Opens the training authoring form for a course.',
params: ['prefill', 'courseId'],
readOnly: true,
mutates: null,
requiresApproval: false,
},
{
name: 'navigate_to_positions',
label: 'Open Positions',
summary: 'Takes the reader to the Positions page.',
params: [],
readOnly: true,
mutates: null,
requiresApproval: false,
},
{
name: 'navigate_to_candidates',
label: 'Open Candidates',
summary: 'Takes the reader to the Candidates page.',
params: [],
readOnly: true,
mutates: null,
requiresApproval: false,
},
{
name: 'navigate_to_forge',
label: 'Open KROW Forge',
summary: 'Takes the reader to the Forge library.',
params: [],
readOnly: true,
mutates: null,
requiresApproval: false,
},
{
name: 'navigate_to_analytics',
label: 'Open Analytics',
summary: 'Takes the reader to the Analytics page.',
params: [],
readOnly: true,
mutates: null,
requiresApproval: false,
},
{
/**
* Open whichever Krow page a reading belongs to.
*
* The general form of the four fixed `navigate_to_*` actions above, which
* each name one destination. This one takes a page key and resolves it
* through the same placement table, so a skill can send the reader to the
* page its analysis was about without a new action per destination.
*
* Still bounded: `routeForPageKey` only knows addresses the product has,
* and an unknown key resolves to nothing rather than to a guess.
*/
name: 'open_related_page',
label: 'Open the related page',
summary: 'Takes the reader to the Krow page a reading came from.',
params: ['page'],
readOnly: true,
mutates: null,
requiresApproval: false,
},
];
const BY_NAME = new Map(TOOLS.map((tool) => [tool.name, tool]));
/** One tool's description, or null. */
export const describeTool = (name) => BY_NAME.get(name) || null;
export const TOOL_NAMES = TOOLS.map((tool) => tool.name);
/** Whether an action changes something a person should agree to first. */
export const toolRequiresApproval = (name) => Boolean(BY_NAME.get(name)?.requiresApproval);
/**
* Every action name a handler exists for but nothing describes.
*
* A handler with no descriptor is invisible to anything reasoning about tools —
* including whatever decides to ask for confirmation — so it would run
* unannounced. Asserted in the checks rather than left to review.
*/
export const undescribedActions = () =>
ACTION_NAMES.filter((name) => !BY_NAME.has(name));
/**
* The tools reachable on this page, for this agent.
*
* Derived from the scoped skill list, so the page boundary is inherited rather
* than re-implemented: a skill the page does not carry contributes no tools, and
* a skill the agent does not carry has already been removed from that list by
* `agentScopedDisabled`.
*
* `disabled` is expected to already carry the agent's scoping. Passing the raw
* account list yields the page's full tool set, which is what an unscoped
* caller should get.
*/
export function toolsForContext(contextId, disabled = [], customSkills = []) {
const reachable = skillsForContext(contextId, disabled, customSkills);
const names = new Set();
for (const skill of reachable) {
for (const action of skill.actions || []) names.add(action);
}
return [...names]
.map((name) => describeTool(name))
.filter(Boolean)
.sort((a, b) => a.label.localeCompare(b.label));
}
/**
* Whether this tool may run here.
*
* The check a caller makes before offering a control. Deliberately takes the
* resolved list rather than recomputing it, so a caller cannot accidentally ask
* the question against a wider scope than the one it rendered from.
*/
export const toolAllowed = (name, allowed = []) =>
allowed.some((tool) => tool.name === name);