Release 2.9.2: trigger-based skill injection + vision support for all adapters
- skills-manager: conditional skill injection via triggers frontmatter field (skills only sent to model when message matches trigger keywords) - skills: added triggers to all skill SKILL.md files; cleaned up headers/disclaimers - anthropic-adapter: toAnthropicContent() converts OpenAI image_url → Anthropic image blocks - multi-agent: vision flag in SecondaryProfile; secondarySupportsVision() checks config - config: secondary set to mistral-large-3:675b-cloud with vision:true Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5306,7 +5306,7 @@ ANTI-HALLUCINATION: When a tool returns a result, report EXACTLY what the tool r
|
||||
IMAGE EDITING RULE: NEVER call image_edit (or any editing tool) when a user uploads a photo without explicitly requesting edits. Uploading a photo is NOT a request to edit it. Only call image_edit when the user's message explicitly asks for an edit (e.g. "수채화로 바꿔줘", "회전해줘"). Violating this rule is a critical error.
|
||||
BROWSER RULE: NEVER call browser_open, browser_snapshot, browser_click, browser_fill, browser_scroll, or any browser_* tool unless the user EXPLICITLY asks to use the browser (e.g. "브라우저로 열어줘", "Chrome으로 봐줘", "사이트 직접 들어가봐"). "열어줘" or "보여줘" alone is NOT a browser request — answer from knowledge, use web_fetch/web_search, or output the relevant URL/embed. Pasting a URL or asking a question is also NOT a browser request.
|
||||
CODE OUTPUT: When writing code in a fenced code block, start with a filename comment on line 1: \`# filename: snake_game.py\` (Python), \`// filename: app.js\` (JS/C), \`<!-- filename: index.html -->\` (HTML). Never repeat code already written in this conversation. For modifications to existing files, use replace_lines or insert_after (not create_file — it only works for NEW files). All code changes are presented as diffs for the user to review before being applied. Write code directly — do not ask for permission.
|
||||
PACKAGE INSTALL: NEVER run pip install, npm install, apt-get, or any package installation command. If a package is missing, just write the code and mention the package name in a comment — let the user decide whether to install it. Do NOT attempt to install packages yourself.${callerContext ? '\n\n' + callerContext : ''}${browserStateCtx}${personalityCtx}${skillsManager.buildPromptContextForUser(username ? getUserWorkspace(username) : null, 16000)}${workflowCtx ? '\n\n' + workflowCtx : ''}`,
|
||||
PACKAGE INSTALL: NEVER run pip install, npm install, apt-get, or any package installation command. If a package is missing, just write the code and mention the package name in a comment — let the user decide whether to install it. Do NOT attempt to install packages yourself.${callerContext ? '\n\n' + callerContext : ''}${browserStateCtx}${personalityCtx}${skillsManager.buildPromptContextForUser(username ? getUserWorkspace(username) : null, 16000, message)}${workflowCtx ? '\n\n' + workflowCtx : ''}`,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface Skill {
|
||||
emoji: string; // from frontmatter or default
|
||||
version: string; // from frontmatter
|
||||
model?: string; // optional model override from frontmatter
|
||||
triggers?: string[]; // if set, only inject when message contains one of these words
|
||||
enabled: boolean; // from config
|
||||
instructions: string; // markdown body (everything after frontmatter)
|
||||
filePath: string; // full path to SKILL.md
|
||||
@@ -62,7 +63,12 @@ function parseFrontmatter(content: string): { frontmatter: SkillFrontmatter; bod
|
||||
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
||||
val = val.slice(1, -1);
|
||||
}
|
||||
frontmatter[key] = val;
|
||||
// Parse inline array: [a, b, c]
|
||||
if (val.startsWith('[') && val.endsWith(']')) {
|
||||
frontmatter[key] = val.slice(1, -1).split(',').map(s => s.trim()).filter(Boolean);
|
||||
} else {
|
||||
frontmatter[key] = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,6 +146,7 @@ export class SkillsManager {
|
||||
emoji: frontmatter.emoji || '🧩',
|
||||
version: frontmatter.version || '1.0.0',
|
||||
model: frontmatter.model || undefined,
|
||||
triggers: Array.isArray(frontmatter.triggers) ? frontmatter.triggers : undefined,
|
||||
enabled: this.enabledState[entry.name] ?? false,
|
||||
instructions: body,
|
||||
filePath: skillMd,
|
||||
@@ -251,13 +258,19 @@ export class SkillsManager {
|
||||
}
|
||||
|
||||
/** 유저 state 기반 prompt context 빌드. userDir 없으면 글로벌 fallback. */
|
||||
buildPromptContextForUser(userDir: string | null, maxCharsPerSkill = 300): string {
|
||||
if (!userDir) return this.buildPromptContext(maxCharsPerSkill);
|
||||
buildPromptContextForUser(userDir: string | null, maxCharsPerSkill = 300, userMessage = ''): string {
|
||||
if (!userDir) return this.buildPromptContext(maxCharsPerSkill, userMessage);
|
||||
const userState = this.getUserState(userDir);
|
||||
const msgLower = userMessage.toLowerCase();
|
||||
const enabled = this.getAll().filter(s => {
|
||||
if (this.GLOBAL_ONLY_SKILLS.has(s.id)) return s.enabled;
|
||||
if (userState !== null && s.id in userState) return userState[s.id];
|
||||
return s.enabled;
|
||||
const isEnabled = userState !== null && s.id in userState ? userState[s.id] : s.enabled;
|
||||
if (!isEnabled) return false;
|
||||
// Trigger filtering: if skill has triggers, only inject when message matches
|
||||
if (s.triggers && s.triggers.length > 0) {
|
||||
return s.triggers.some(t => msgLower.includes(t.toLowerCase()));
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (enabled.length === 0) return '';
|
||||
const parts: string[] = ['[ACTIVE SKILLS]'];
|
||||
@@ -406,8 +419,14 @@ export class SkillsManager {
|
||||
* Build the skills context string for the system prompt.
|
||||
* Only includes enabled skills. Keeps it compact for 4B context.
|
||||
*/
|
||||
buildPromptContext(maxCharsPerSkill: number = 300): string {
|
||||
const enabled = this.getEnabledSkills();
|
||||
buildPromptContext(maxCharsPerSkill: number = 300, userMessage = ''): string {
|
||||
const msgLower = userMessage.toLowerCase();
|
||||
const enabled = this.getEnabledSkills().filter(s => {
|
||||
if (s.triggers && s.triggers.length > 0) {
|
||||
return s.triggers.some(t => msgLower.includes(t.toLowerCase()));
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (enabled.length === 0) return '';
|
||||
|
||||
const parts: string[] = ['[ACTIVE SKILLS]'];
|
||||
|
||||
@@ -21,6 +21,7 @@ export type PreflightMode = 'off' | 'complex_only' | 'always';
|
||||
export interface SecondaryProfile {
|
||||
provider: string;
|
||||
model: string;
|
||||
vision?: boolean;
|
||||
}
|
||||
|
||||
export interface OrchestrationConfig {
|
||||
@@ -163,6 +164,7 @@ export function getOrchestrationConfig(): OrchestrationConfig | null {
|
||||
secondary: {
|
||||
provider: String(oc.secondary.provider || '').trim(),
|
||||
model: String(oc.secondary.model || '').trim(),
|
||||
vision: typeof oc.secondary.vision === 'boolean' ? oc.secondary.vision : undefined,
|
||||
},
|
||||
...clamped,
|
||||
};
|
||||
@@ -1159,6 +1161,9 @@ async function buildSecondaryProvider(): Promise<{ provider: LLMProvider; config
|
||||
* that can actually reason about the screenshot.
|
||||
*/
|
||||
function secondarySupportsVision(config: OrchestrationConfig): boolean {
|
||||
if (config.secondary.vision === true) return true;
|
||||
if (config.secondary.vision === false) return false;
|
||||
// Default: openai providers support vision
|
||||
const p = config.secondary.provider;
|
||||
return p === 'openai' || p === 'openai_codex';
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import type {
|
||||
LLMProvider, ChatMessage, ChatOptions, ChatResult,
|
||||
LLMProvider, ChatMessage, ContentPart, ChatOptions, ChatResult,
|
||||
GenerateOptions, GenerateResult, ModelInfo,
|
||||
} from './LLMProvider';
|
||||
import { contentToString } from './content-utils';
|
||||
@@ -136,13 +136,13 @@ function toAnthropicMessages(messages: ChatMessage[]): any[] {
|
||||
out.push({ role: 'assistant', content });
|
||||
|
||||
} else if (msg.role === 'user' || msg.role === 'assistant') {
|
||||
const text = contentToString(msg.content);
|
||||
const anthropicContent = toAnthropicContent(msg.content);
|
||||
const last = out[out.length - 1];
|
||||
if (last?.role === msg.role && typeof last.content === 'string') {
|
||||
if (last?.role === msg.role && typeof last.content === 'string' && typeof anthropicContent === 'string') {
|
||||
// Merge consecutive same-role text messages
|
||||
last.content += '\n' + text;
|
||||
last.content += '\n' + anthropicContent;
|
||||
} else {
|
||||
out.push({ role: msg.role, content: text });
|
||||
out.push({ role: msg.role, content: anthropicContent });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,6 +150,34 @@ function toAnthropicMessages(messages: ChatMessage[]): any[] {
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert OpenAI-style content (string | ContentPart[]) to Anthropic content blocks.
|
||||
* - string → string (text only)
|
||||
* - ContentPart[] → Anthropic content block array (text + image)
|
||||
*/
|
||||
function toAnthropicContent(content: string | ContentPart[] | null | undefined): string | any[] {
|
||||
if (!content) return '';
|
||||
if (typeof content === 'string') return content;
|
||||
|
||||
const blocks: any[] = [];
|
||||
for (const part of content) {
|
||||
if (part.type === 'text') {
|
||||
blocks.push({ type: 'text', text: part.text });
|
||||
} else if (part.type === 'image_url') {
|
||||
const url = part.image_url?.url || '';
|
||||
if (url.startsWith('data:')) {
|
||||
const match = url.match(/^data:([^;]+);base64,(.+)$/);
|
||||
if (match) {
|
||||
blocks.push({ type: 'image', source: { type: 'base64', media_type: match[1], data: match[2] } });
|
||||
}
|
||||
} else if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
blocks.push({ type: 'image', source: { type: 'url', url } });
|
||||
}
|
||||
}
|
||||
}
|
||||
return blocks.length > 0 ? blocks : '';
|
||||
}
|
||||
|
||||
/** Convert OpenAI tool definitions to Anthropic tool definitions. */
|
||||
function toAnthropicTools(tools: any[]): any[] {
|
||||
return tools
|
||||
|
||||
Reference in New Issue
Block a user