mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 20:03:05 +00:00
Move skill command handling to AgentSession, update docs
- Skill commands (/skill:name) now expanded in AgentSession instead of interactive mode, enabling them in RPC and print modes - Input event can now intercept /skill:name before expansion - Updated extensions.md with clearer input event docs and processing order - Updated rpc.md: hook -> extension terminology, added skill expansion mentions - Added PR attribution to changelog entries for #761
This commit is contained in:
parent
3e5d91f287
commit
b4a05cbcab
7 changed files with 211 additions and 420 deletions
|
|
@ -13,6 +13,7 @@
|
|||
* Modes use this class and add their own I/O layer on top.
|
||||
*/
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import type {
|
||||
Agent,
|
||||
AgentEvent,
|
||||
|
|
@ -25,6 +26,7 @@ import type { AssistantMessage, ImageContent, Message, Model, TextContent } from
|
|||
import { isContextOverflow, modelsAreEqual, supportsXhigh } from "@mariozechner/pi-ai";
|
||||
import { getAuthPath } from "../config.js";
|
||||
import { theme } from "../modes/interactive/theme/theme.js";
|
||||
import { stripFrontmatter } from "../utils/frontmatter.js";
|
||||
import { type BashResult, executeBash as executeBashCommand, executeBashWithOperations } from "./bash-executor.js";
|
||||
import {
|
||||
type CompactionResult,
|
||||
|
|
@ -569,7 +571,7 @@ export class AgentSession {
|
|||
}
|
||||
}
|
||||
|
||||
// Emit input event for extension interception (before template expansion)
|
||||
// Emit input event for extension interception (before skill/template expansion)
|
||||
let currentText = text;
|
||||
let currentImages = options?.images;
|
||||
if (this._extensionRunner?.hasHandlers("input")) {
|
||||
|
|
@ -587,10 +589,12 @@ export class AgentSession {
|
|||
}
|
||||
}
|
||||
|
||||
// Expand file-based prompt templates if requested
|
||||
const expandedText = expandPromptTemplates
|
||||
? expandPromptTemplate(currentText, [...this._promptTemplates])
|
||||
: currentText;
|
||||
// Expand skill commands (/skill:name args) and prompt templates (/template args)
|
||||
let expandedText = currentText;
|
||||
if (expandPromptTemplates) {
|
||||
expandedText = this._expandSkillCommand(expandedText);
|
||||
expandedText = expandPromptTemplate(expandedText, [...this._promptTemplates]);
|
||||
}
|
||||
|
||||
// If streaming, queue via steer() or followUp() based on option
|
||||
if (this.isStreaming) {
|
||||
|
|
@ -718,10 +722,42 @@ export class AgentSession {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand skill commands (/skill:name args) to their full content.
|
||||
* Returns the expanded text, or the original text if not a skill command or skill not found.
|
||||
* Emits errors via extension runner if file read fails.
|
||||
*/
|
||||
private _expandSkillCommand(text: string): string {
|
||||
if (!text.startsWith("/skill:")) return text;
|
||||
|
||||
const spaceIndex = text.indexOf(" ");
|
||||
const skillName = spaceIndex === -1 ? text.slice(7) : text.slice(7, spaceIndex);
|
||||
const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim();
|
||||
|
||||
const skill = this._skills.find((s) => s.name === skillName);
|
||||
if (!skill) return text; // Unknown skill, pass through
|
||||
|
||||
try {
|
||||
const content = readFileSync(skill.filePath, "utf-8");
|
||||
const body = stripFrontmatter(content).trim();
|
||||
const header = `Skill location: ${skill.filePath}\nReferences are relative to ${skill.baseDir}.`;
|
||||
const skillMessage = `${header}\n\n${body}`;
|
||||
return args ? `${skillMessage}\n\n---\n\nUser: ${args}` : skillMessage;
|
||||
} catch (err) {
|
||||
// Emit error like extension commands do
|
||||
this._extensionRunner?.emitError({
|
||||
extensionPath: skill.filePath,
|
||||
event: "skill_expansion",
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
return text; // Return original on error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue a steering message to interrupt the agent mid-run.
|
||||
* Delivered after current tool execution, skips remaining tools.
|
||||
* Expands file-based prompt templates. Errors on extension commands.
|
||||
* Expands skill commands and prompt templates. Errors on extension commands.
|
||||
* @throws Error if text is an extension command
|
||||
*/
|
||||
async steer(text: string): Promise<void> {
|
||||
|
|
@ -730,8 +766,9 @@ export class AgentSession {
|
|||
this._throwIfExtensionCommand(text);
|
||||
}
|
||||
|
||||
// Expand file-based prompt templates
|
||||
const expandedText = expandPromptTemplate(text, [...this._promptTemplates]);
|
||||
// Expand skill commands and prompt templates
|
||||
let expandedText = this._expandSkillCommand(text);
|
||||
expandedText = expandPromptTemplate(expandedText, [...this._promptTemplates]);
|
||||
|
||||
await this._queueSteer(expandedText);
|
||||
}
|
||||
|
|
@ -739,7 +776,7 @@ export class AgentSession {
|
|||
/**
|
||||
* Queue a follow-up message to be processed after the agent finishes.
|
||||
* Delivered only when agent has no more tool calls or steering messages.
|
||||
* Expands file-based prompt templates. Errors on extension commands.
|
||||
* Expands skill commands and prompt templates. Errors on extension commands.
|
||||
* @throws Error if text is an extension command
|
||||
*/
|
||||
async followUp(text: string): Promise<void> {
|
||||
|
|
@ -748,8 +785,9 @@ export class AgentSession {
|
|||
this._throwIfExtensionCommand(text);
|
||||
}
|
||||
|
||||
// Expand file-based prompt templates
|
||||
const expandedText = expandPromptTemplate(text, [...this._promptTemplates]);
|
||||
// Expand skill commands and prompt templates
|
||||
let expandedText = this._expandSkillCommand(text);
|
||||
expandedText = expandPromptTemplate(expandedText, [...this._promptTemplates]);
|
||||
|
||||
await this._queueFollowUp(expandedText);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,7 +60,6 @@ import type { TruncationResult } from "../../core/tools/truncate.js";
|
|||
import { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/changelog.js";
|
||||
import { copyToClipboard } from "../../utils/clipboard.js";
|
||||
import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
|
||||
import { stripFrontmatter } from "../../utils/frontmatter.js";
|
||||
import { ensureTool } from "../../utils/tools-manager.js";
|
||||
import { ArminComponent } from "./components/armin.js";
|
||||
import { AssistantMessageComponent } from "./components/assistant-message.js";
|
||||
|
|
@ -1508,20 +1507,6 @@ export class InteractiveMode {
|
|||
return;
|
||||
}
|
||||
|
||||
// Handle skill commands (/skill:name [args])
|
||||
if (text.startsWith("/skill:")) {
|
||||
const spaceIndex = text.indexOf(" ");
|
||||
const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
|
||||
const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim();
|
||||
const skillPath = this.skillCommands.get(commandName);
|
||||
if (skillPath) {
|
||||
this.editor.addToHistory?.(text);
|
||||
this.editor.setText("");
|
||||
await this.handleSkillCommand(skillPath, args);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle bash command (! for normal, !! for excluded from context)
|
||||
if (text.startsWith("!")) {
|
||||
const isExcluded = text.startsWith("!!");
|
||||
|
|
@ -3313,20 +3298,6 @@ export class InteractiveMode {
|
|||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
private async handleSkillCommand(skillPath: string, args: string): Promise<void> {
|
||||
try {
|
||||
const content = fs.readFileSync(skillPath, "utf-8");
|
||||
const body = stripFrontmatter(content).trim();
|
||||
const skillDir = path.dirname(skillPath);
|
||||
const header = `Skill location: ${skillPath}\nReferences are relative to ${skillDir}.`;
|
||||
const skillMessage = `${header}\n\n${body}`;
|
||||
const message = args ? `${skillMessage}\n\n---\n\nUser: ${args}` : skillMessage;
|
||||
await this.session.prompt(message);
|
||||
} catch (err) {
|
||||
this.showError(`Failed to load skill: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
private handleChangelogCommand(): void {
|
||||
const changelogPath = getChangelogPath();
|
||||
const allEntries = parseChangelog(changelogPath);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue