74 lines
4.2 KiB
TypeScript
74 lines
4.2 KiB
TypeScript
/** Resolve world families and definitions at each run boundary. All real requests retain the host proxy transport. */
|
|
import { AgentDoc, defineExtension, GenerationTask, hook } from '@earendil-works/pi-durable'
|
|
import { definitionRuns, discoverAgents, parseDefinition, parseOverlay, parseFamilies, parseFrontmatter, toolsFor } from '../core/agents.ts'
|
|
import { ModelsApi, Pin, Profile } from './api.ts'
|
|
import { repoRoot } from './files.ts'
|
|
export const order = -200
|
|
export default function models(host: any) {
|
|
const read = async (cwd: string) => {
|
|
const families = parseFamilies(await host.fs.read('/src/sausage/agents/models.json'))
|
|
const root = await repoRoot(host.fs, cwd)
|
|
const files = []
|
|
for (const folder of ['/src/sausage/agents', `${root}/.sausage/agents`]) {
|
|
let entries
|
|
try { entries = await host.fs.list(folder) } catch (error) { if (error.code !== 'not_found') throw error; entries = [] }
|
|
for (const entry of entries) if (entry.kind === 'file' && entry.name.endsWith('.md')) {
|
|
const path = `${folder}/${entry.name}`
|
|
files.push({path, text:await host.fs.read(path)})
|
|
}
|
|
}
|
|
const refs = Object.values(families).map(spec => spec.model.replace(/:(off|minimal|low|medium|high|xhigh|max)$/, ''))
|
|
// Explicit pins can name a proxy model outside the families.
|
|
for (const file of files) {
|
|
try {
|
|
const parsed = 'addTools' in parseFrontmatter(file.text).frontmatter ? parseOverlay(file.text,file.path).settings : parseDefinition(file.text,file.path)
|
|
if (parsed.model) refs.push(parsed.model.replace(/:(off|minimal|low|medium|high|xhigh|max)$/, ''))
|
|
} catch { /* discoverAgents records invalid files without replacing valid definitions. */ }
|
|
}
|
|
const found = discoverAgents(files, '/src/sausage', root, families, refs)
|
|
return {families, ...found}
|
|
}
|
|
const transport = host.models().getProvider('openai')
|
|
const template = host.models().getModels('openai')[0]
|
|
const register = (ref: any) => {
|
|
const store = host.models()
|
|
if (store.getModel(ref.provider, ref.modelId)) return
|
|
if (!transport || !template) throw new Error(`No proxy transport for ${ref.provider}/${ref.modelId}`)
|
|
const prior = store.getModels(ref.provider)
|
|
store.setProvider({...transport, id:ref.provider, getModels:()=>[...prior, {...template, provider:ref.provider, id:ref.modelId, name:ref.modelId}]})
|
|
}
|
|
const apply = async (tx: any, id: number) => {
|
|
const state = await tx.doc(AgentDoc, id)
|
|
const profile = await tx.doc(Profile, id)
|
|
const config = await read(state.cwd ?? '/')
|
|
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?.runs ?? definitionRuns({} as any,config.families,Object.values(config.families).map(spec=>spec.model.replace(/:(off|minimal|low|medium|high|xhigh|max)$/, '')))
|
|
const pinned = await host.want(Pin)?.pin(tx, id)
|
|
const ref = pinned ?? selected.model
|
|
register(ref)
|
|
state.model = ref
|
|
if (selected.thinking) state.thinkingLevel = selected.thinking
|
|
else delete state.thinkingLevel
|
|
if (definition) {
|
|
state.instructions = definition.instructions
|
|
if (profile.inheritedTools === undefined) profile.inheritedTools = state.tools ?? null
|
|
const inherited = profile.inheritedTools
|
|
if (definition.tools || Array.isArray(inherited)) state.tools = toolsFor(definition,inherited ?? [])
|
|
else state.tools = {remove:[...new Set([...(inherited?.remove ?? []),...(definition.denyTools ?? [])])]}
|
|
}
|
|
return ref
|
|
}
|
|
host.implement(ModelsApi, {
|
|
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)}})]})
|
|
}
|