Files
sausage/extensions/core/agents.ts
T

57 lines
3.2 KiB
TypeScript

/** Markdown roster files, YAML settings and skill metadata, model references, and inherited tools. */
import { parse } from 'yaml'
export const CONFIG = '/etc/sausage/config.yaml'
export const thinkingLevels = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'] as const
export type Thinking = typeof thinkingLevels[number]
export interface Definition {
name: string
description: string
path: string
instructions: string
model: string
thinking?: Thinking
tools?: string[]
addTools?: string[]
denyTools?: string[]
isolate?: 'none' | 'worktree'
resident?: boolean
fast?: boolean
summaries?: boolean
icon?: string
tint?: string
}
export function parseFrontmatter(text: string) {
const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)([\s\S]*)$/.exec(text)
if (!match) throw new Error('Expected frontmatter fields between --- lines')
return {frontmatter:parse(match[1]) as Record<string,unknown>, body:match[2].trim()}
}
export const ROSTER = '/etc/sausage/roster'
export function parseDefinition(text: string, path: string, inherited?: Definition): Definition {
const {frontmatter, body} = parseFrontmatter(text)
const name = path.split('/').pop()!.replace(/\.md$/, '')
const settings = {...(inherited ?? {}), ...frontmatter}
const instructions = inherited && frontmatter.addTools !== undefined ? [inherited.instructions, body].filter(Boolean).join('\n\n## Project additions\n\n') : body
const definition = {...settings, name, path, instructions} as Definition
if (!/^[a-zA-Z0-9_-]+$/.test(name)) throw new Error(`${path}: invalid roster name`)
for (const key of ['description','model','instructions']) if (typeof definition[key] !== 'string' || !definition[key].trim()) throw new Error(`${path}: ${key} is required`)
if (definition.thinking !== undefined && !thinkingLevels.includes(definition.thinking)) throw new Error(`${path}: invalid thinking`)
for (const key of ['tools','addTools','denyTools']) if (definition[key] !== undefined && (!Array.isArray(definition[key]) || !definition[key].every((tool: any)=>typeof tool === 'string'))) throw new Error(`${path}: ${key} must be a list of tool names`)
if (inherited && frontmatter.addTools !== undefined) definition.tools = toolsFor(definition, inherited.tools ?? [])
definitionRuns(definition)
return definition
}
export function parseConfig(text: string) {
const value = parse(text)
if (typeof value?.prompt !== 'string') throw new Error(`${CONFIG}: prompt must be a string`)
if (typeof value.main_agent_name !== 'string' || !value.main_agent_name) throw new Error(`${CONFIG}: main_agent_name must name a roster file`)
return value
}
export function definitionRuns(definition: Definition) {
if (!/^[^/\s]+\/\S+$/.test(definition.model)) throw new Error(`${definition.path}: invalid model for ${definition.name}`)
const slash = definition.model.indexOf('/')
return {model:{provider:definition.model.slice(0,slash),modelId:definition.model.slice(slash+1)},thinking:definition.thinking}
}
export function toolsFor(definition: Definition, parent: readonly string[]): string[] {
return [...new Set([...(definition.tools ?? parent), ...(definition.addTools ?? [])])].filter(tool => !definition.denyTools?.includes(tool))
}