Files

139 lines
9.1 KiB
JavaScript
Raw Permalink Normal View History

/**
* Two capabilities, two tools.
*
* Controlling sessions and driving a page are different intents, and a single
* tool description covering both is vaguer than either — which is how a model
* ends up calling the wrong one. Separate tools also mean turning one off
* removes it entirely, parameters included, rather than leaving its inputs
* visible in a shared schema.
*/
export const OPENCHAMBER_CONTROL_ACTION_DEFINITIONS = Object.freeze([
{ action: 'projects.list', title: 'List configured projects', description: 'List configured projects; no parameters' },
{ action: 'models.list', title: 'Show model preferences', description: 'Show default, favorite, and recent model preferences; no parameters' },
{ action: 'session.list', title: 'List sessions', description: 'List sessions; optional directory, limit (default 10), all, or withStatus' },
{ action: 'session.create', title: 'Create a session', description: 'Create a session in the current directory by default; prompt is optional' },
{ action: 'session.send', title: 'Send a prompt', description: 'Send a new prompt to sessionId; scope with projectId or directory' },
{ action: 'session.fork', title: 'Fork a session', description: 'Fork sessionId; messageId selects the boundary; prompt is optional' },
{ action: 'session.status', title: 'Check session status', description: 'Check sessionId status; directory defaults to the current session' },
{ action: 'session.messages', title: 'Read session messages', description: 'Read text-only messages and current sessionStatus for sessionId; directory and limit 10 are defaults' },
{ action: 'schedule.status', title: 'Check scheduler status', description: 'Check scheduler status; no parameters', agentExposed: false },
{ action: 'schedule.list', title: 'List scheduled tasks', description: 'List tasks and scheduler status; scope with projectId or directory' },
{ action: 'schedule.create', title: 'Create a scheduled task', description: 'Create task; requires name, prompt, model, and one schedule selector' },
{ action: 'schedule.run', title: 'Run a scheduled task', description: 'Run taskId; scope with projectId or directory' },
{ action: 'schedule.delete', title: 'Delete a scheduled task', description: 'Delete taskId; scope with projectId or directory' },
{ action: 'schedule.toggle', title: 'Enable or disable a scheduled task', description: 'Enable or disable taskId; requires the disabled boolean' },
]);
const OPENCHAMBER_CONTROL_ACTIONS = Object.freeze(
OPENCHAMBER_CONTROL_ACTION_DEFINITIONS.map(({ action }) => action),
);
export const OPENCHAMBER_AGENT_TOOL_ACTION_DEFINITIONS = Object.freeze(
OPENCHAMBER_CONTROL_ACTION_DEFINITIONS.filter(({ agentExposed }) => agentExposed !== false),
);
export const OPENCHAMBER_AGENT_TOOL_ACTIONS = Object.freeze(
OPENCHAMBER_AGENT_TOOL_ACTION_DEFINITIONS.map(({ action }) => action),
);
export const OPENCHAMBER_WEB_ACTION_DEFINITIONS = Object.freeze([
{ action: 'browser.open', title: 'Open a page in the browser panel', description: 'Open url in the in-app browser panel; use it to look at the running app. Set viewport to mobile, tablet or desktop to lay the page out at that size' },
{ action: 'browser.snapshot', title: 'Read the open page', description: 'Read the open page: url, title, visible text, and interactive elements with the selectors the other browser actions accept. Pass selector to read only that part of a long page. Reports any errors the page logged' },
{ action: 'browser.click', title: 'Click on the open page', description: 'Click an element; give selector, or text to match a link or button by its visible label' },
{ action: 'browser.type', title: 'Type into the open page', description: 'Type value into the field matched by selector; set submit to press Enter afterwards' },
{ action: 'browser.scroll', title: 'Scroll the open page', description: 'Scroll the page; direction is up, down, top, or bottom, or pass selector to bring one element into view' },
{ action: 'browser.back', title: 'Go back in the browser panel', description: 'Return to the previous page in this tab; no parameters' },
{ action: 'browser.forward', title: 'Go forward in the browser panel', description: 'Move forward again in this tab; no parameters' },
{ action: 'browser.inspect', title: 'Read how an element renders', description: 'Read the computed styles of the element matched by selector — colours, fonts, spacing, borders — as the page actually renders them' },
{ action: 'browser.capture', title: 'Save a screenshot of the page', description: 'Save what is currently visible in the browser panel as an image file in the project and return its path, so a change can be shown rather than described. Pass label to name it (for example before-fix); the result reports the page, layout and path to reference in your answer' },
{ action: 'browser.resize', title: 'Change the page viewport', description: 'Lay the open page out at a different size; viewport is mobile, tablet, desktop, or fill to use the whole panel' },
]);
export const OPENCHAMBER_WEB_ACTIONS = Object.freeze(
OPENCHAMBER_WEB_ACTION_DEFINITIONS.map(({ action }) => action),
);
/**
* Memory is its own tool for the same reason web is: remembering across
* sessions is a distinct intent from controlling one, and a shared description
* would blur both. It also has to switch off cleanly and completely, which a
* shared schema cannot do.
*
* The session already carries an index of stored titles, so the descriptions
* push the model toward reading one entry it can already see rather than
* listing everything again — and toward reading it at all, since a title that
* reads as a complete fact is exactly the one whose conditions get lost.
*/
export const OPENCHAMBER_MEMORY_ACTION_DEFINITIONS = Object.freeze([
{ action: 'memory.read', title: 'Read a stored memory', description: 'Read the full text of one memory listed in the session index. The index shows titles only, and a title omits the conditions that decide how the memory applies, so read before acting rather than working from the title. Requires title (as the index spells it) or memoryId; scope is optional and both stores are searched without it' },
{ action: 'memory.list', title: 'List stored memories', description: 'List stored memory titles when the session index is missing or stale; scope is global, project, or both (default)' },
{ action: 'memory.save', title: 'Remember something', description: 'Store a durable fact, preference, or reference; requires title and body, plus scope global (about the user) or project (about this codebase). Restating something already stored updates it. Do not store secrets, one-off task state, or anything the user asked you not to keep' },
{ action: 'memory.delete', title: 'Forget a memory', description: 'Delete a memory that turned out to be wrong or obsolete; requires memoryId and scope' },
]);
export const OPENCHAMBER_MEMORY_ACTIONS = Object.freeze(
OPENCHAMBER_MEMORY_ACTION_DEFINITIONS.map(({ action }) => action),
);
/**
* Which actions each managed tool may ask for.
*
* The callback needs this because models routinely drop the namespace: asked
* for `memory.read` from a tool already called `openchamber_memory`, they send
* `read`, since the tool's own name appears to have said "memory" already. The
* name is unambiguous inside one tool's action set even when it is not across
* all of them (`delete` belongs to both schedule and memory), so resolution
* starts from the tool that asked.
*/
const ACTIONS_BY_TOOL = Object.freeze({
openchamber: OPENCHAMBER_AGENT_TOOL_ACTIONS,
openchamber_web: OPENCHAMBER_WEB_ACTIONS,
openchamber_memory: OPENCHAMBER_MEMORY_ACTIONS,
});
const bareName = (action) => {
const separator = action.indexOf('.');
return separator === -1 ? action : action.slice(separator + 1);
};
const uniqueMatch = (candidates, requested) => {
const matches = candidates.filter((candidate) => bareName(candidate) === requested);
return matches.length === 1 ? matches[0] : null;
};
/**
* The canonical action for what a tool asked, or the reason it could not be
* resolved. The reason lists what the tool can actually do: an error that only
* says "unsupported" leaves the model to guess again, which is how one wrong
* name becomes three.
*/
export const resolveAgentToolAction = (requested, toolName) => {
const value = typeof requested === 'string' ? requested.trim() : '';
const scoped = ACTIONS_BY_TOOL[toolName] ?? null;
const known = scoped ?? OPENCHAMBER_ALL_ACTIONS;
if (value && known.includes(value)) {
return { action: value };
}
if (value) {
const resolved = uniqueMatch(known, value)
// A tool that did not identify itself still gets the benefit when the
// bare name means only one thing across every action.
?? (scoped ? null : uniqueMatch(OPENCHAMBER_ALL_ACTIONS, value));
if (resolved) {
return { action: resolved };
}
}
return {
error: `Unsupported OpenChamber action: ${value || 'missing'}. Use one of: ${known.join(', ')}`,
};
};
/** Everything the callback route will dispatch, whichever tool asked. */
export const OPENCHAMBER_ALL_ACTIONS = Object.freeze([
...OPENCHAMBER_CONTROL_ACTIONS,
...OPENCHAMBER_WEB_ACTIONS,
...OPENCHAMBER_MEMORY_ACTIONS,
]);