Research Agents: Agents you define that answer research chats from SEC filings, your documents, and the web, citing every passage.
| 1 | import type { BackendContext } from '@jaspers-ai/sdk' |
| 2 | import { AGENT_ID, agentIdOf, parseAgent, serializeAgent, type Agent, type AgentInput } from './agents.ts' |
| 3 | import { errorText } from './errors.ts' |
| 4 | |
| 5 | // Agents on disk, in the plugin's data folder's defs/, one Markdown file each. The list is the live |
| 6 | // value `agents`, which the view reads, published again whenever the folder changes. |
| 7 | |
| 8 | const FOLDER = 'defs' |
| 9 | |
| 10 | export async function loadAgents(ctx: BackendContext): Promise<Agent[]> { |
| 11 | const agents: Agent[] = [] |
| 12 | for (const entry of await ctx.files.list(FOLDER)) { |
| 13 | const id = entry.dir ? null : agentIdOf(entry.path) |
| 14 | if (id === null) continue |
| 15 | try { |
| 16 | agents.push(parseAgent(id, await ctx.files.read(entry.path))) |
| 17 | } catch (err) { |
| 18 | // Gone between the listing and the read, or unreadable: listed with why, not a broken list. |
| 19 | agents.push({ ...parseAgent(id, ''), error: `could not be read: ${errorText(err, 'unreadable')}` }) |
| 20 | } |
| 21 | } |
| 22 | return agents.sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id)) |
| 23 | } |
| 24 | |
| 25 | export async function publishAgents(ctx: BackendContext): Promise<Agent[]> { |
| 26 | const agents = await loadAgents(ctx) |
| 27 | ctx.live.set('agents', agents) |
| 28 | return agents |
| 29 | } |
| 30 | |
| 31 | /** Writes one agent, refusing what would not read back as a working one. */ |
| 32 | export async function saveAgent(ctx: BackendContext, input: AgentInput): Promise<void> { |
| 33 | const text = serializeAgent(input) |
| 34 | const checked = parseAgent(input.id, text) |
| 35 | if (checked.error) throw new Error(`Not saved: ${checked.error}.`) |
| 36 | await ctx.files.write(`${FOLDER}/${input.id}.md`, text) |
| 37 | } |
| 38 | |
| 39 | export async function deleteAgent(ctx: BackendContext, id: string): Promise<void> { |
| 40 | if (!AGENT_ID.test(id)) throw new Error(`No agent ${id}.`) |
| 41 | await ctx.files.remove(`${FOLDER}/${id}.md`) |
| 42 | } |
| 43 | |
| 44 | export function watchAgents(ctx: BackendContext): void { |
| 45 | ctx.files.watch(FOLDER, () => { |
| 46 | publishAgents(ctx).catch((err: unknown) => console.warn(`agents: ${errorText(err, 'failed')}`)) |
| 47 | }) |
| 48 | } |