Files
sausage/extensions/models/server.ts
T

80 lines
4.3 KiB
TypeScript

/** Read provider settings and Markdown definitions at each run boundary. */
import { AgentDoc, defineExtension, GenerationTask, hook } from '@earendil-works/pi-durable'
import { CONFIG, ROSTER, definitionRuns, parseConfig, parseDefinition, toolsFor } from '../core/agents.ts'
import { repoRoot } from './files.ts'
import { ModelsApi, Pin, Profile } from './api.ts'
export const order = -200
export default function models(host: any) {
const read = async (cwd: string) => {
const config = parseConfig(await host.fs.read(CONFIG))
const definitions = new Map<string, any>()
for (const folder of [ROSTER, `${await repoRoot(host.fs,cwd)}/.sausage/roster`]) {
let entries
try { entries = await host.fs.list(folder) } catch (error) { if (error.code === 'not_found') continue; throw error }
for (const entry of entries) {
if (!entry.name.endsWith('.md') || entry.kind === 'directory') continue
const path = `${folder}/${entry.name}`, name = entry.name.slice(0,-3)
definitions.set(name,parseDefinition(await host.fs.read(path),path,definitions.get(name)))
}
}
if (!definitions.has(config.main_agent_name)) throw new Error(`${CONFIG}: main_agent_name ${config.main_agent_name} needs ${ROSTER}/${config.main_agent_name}.md`)
return {...config, definitions:[...definitions.values()].map((definition: any)=>({...definition,
resident:definition.name === config.main_agent_name || definition.resident,
runs:definitionRuns(definition)})), errors:[], replaced:[]}
}
const register = (ref: any) => {
const store = host.models()
if (store.getModel(ref.provider, ref.modelId)) return
throw new Error(`${CONFIG}: unknown provider or unconfigured model ${ref.provider}/${ref.modelId}`)
}
const apply = async (tx: any, id: number) => {
const state = await tx.doc(AgentDoc, id)
const profile = await tx.doc(Profile, id)
await host.models().refreshConfig?.()
const config = await read(state.cwd ?? '/')
if (profile.mainAgent) profile.agent = config.main_agent_name
const definition = profile.agent ? config.definitions.find(d => d.name === profile.agent) : undefined
if (profile.agent && !definition) throw new Error(`Unknown agent ${profile.agent}: ${JSON.stringify(config.errors)}`)
const selected = (definition ?? config.definitions.find((d: any)=>d.name === config.main_agent_name)).runs
const pinned = await host.want(Pin)?.pin(tx, id)
const ref = pinned ?? profile.model ?? selected.model
register(ref)
state.model = ref
if (profile.thinking ?? selected.thinking) state.thinkingLevel = profile.thinking ?? selected.thinking
else delete state.thinkingLevel
state.instructions = [config.prompt, definition ? `You are @${definition.name}.\n\n${definition.instructions}` : ''].filter(Boolean).join('\n\n')
if (definition) {
if (profile.inheritedTools === undefined) profile.inheritedTools = state.tools ?? null
const inherited = profile.inheritedTools
if (definition.tools || Array.isArray(inherited)) state.tools = toolsFor(definition,inherited ?? []).filter(name=>!profile.excludeTools?.includes(name))
else state.tools = {remove:[...new Set([...(inherited?.remove ?? []),...(definition.denyTools ?? []),...(profile.excludeTools ?? [])])]}
}
return ref
}
host.implement(ModelsApi, {
discover: read,
configure: async (tx: any, id: number, options: any) => {
const profile = await tx.doc(Profile,id)
profile.agent = options.agent
profile.mainAgent = options.mainAgent ?? false
profile.inheritedTools = options.inheritedTools
profile.excludeTools = options.excludeTools ?? []
if (options.model) profile.model = options.model
else delete profile.model
if (options.thinking) profile.thinking = options.thinking
else delete profile.thinking
return apply(tx,id)
},
resolve: apply,
start: async ({agent, cwd, ownership = {kind:'ownerless'}}: any, context: any) => {
const conversation = await host.harness().createConversation({ownership, agent:{cwd}, init:async (tx: any, id: number)=>{
(await tx.doc(Profile,id)).agent = agent
await apply(tx,id)
}}, context)
return conversation
},
})
return defineExtension({name:'models', hooks:[hook(GenerationTask,{beforeInput:async (_input, {tx,conversationId})=>{await apply(tx,conversationId)}})]})
}