82 lines
3.5 KiB
TypeScript
82 lines
3.5 KiB
TypeScript
/** Who a message is from and to: resolving addresses, naming senders, and a subagent's parent. */
|
|
import type { Context } from '@earendil-works/chord'
|
|
import { defineDoc, ROOT_CONVERSATION_ID } from '@earendil-works/pi-durable'
|
|
import type { ConversationId } from '@earendil-works/pi-durable'
|
|
import type { Host as ExtensionHost } from '../../host/js/extensions.ts'
|
|
import { MainAgent, Subagents, SubagentsApi } from '../subagents/api.ts'
|
|
import { Roots } from '../lineage/api.ts'
|
|
import { Tally } from '../lineage/server.ts'
|
|
|
|
/**
|
|
* Who started a branch through `branch`, so its later final answers reach that conversation. `yielded` is the newest
|
|
* generation observed, so a replayed hook creates no second observer.
|
|
*/
|
|
export const Started = defineDoc<{ by?: number; yielded?: number }>({
|
|
kind: 'board.started',
|
|
checkpointWhen: () => false,
|
|
version: 1,
|
|
scope: 'conversation',
|
|
history: 'latest',
|
|
fork: 'initial',
|
|
initial: () => ({}),
|
|
})
|
|
/** The conversation that started branch `id`, which hears its final answers; undefined for anything else. */
|
|
export async function starterOf(host: ExtensionHost, id: number | 'user', ctx: Context) {
|
|
if (typeof id !== 'number') return undefined
|
|
return (await host.harness().snapshot(Started, id as ConversationId, ctx))?.by
|
|
}
|
|
/** The conversation that started `id` as a subagent, which is the only one it may message. */
|
|
export async function parentOf(host: ExtensionHost, id: number, ctx: Context) {
|
|
const owner = (await host.harness().commit((tx) => tx.conversation(id as ConversationId), ctx))
|
|
?.owner?.conversationId
|
|
if (owner === undefined) return undefined
|
|
return (await host.harness().snapshot(Subagents, owner, ctx))?.agents[String(id)]
|
|
? owner
|
|
: undefined
|
|
}
|
|
export async function senderName(host: ExtensionHost, id: number | 'user', ctx: Context) {
|
|
if (id === 'user') return 'user'
|
|
if (typeof id !== 'number') return 'another Main agent'
|
|
const index = await host.harness().snapshot(MainAgent, ROOT_CONVERSATION_ID, ctx)
|
|
const name = Object.keys(index ?? {}).find((candidate) => candidate !== '$main-agent' && index?.[candidate] === id)
|
|
return `#${id}${name ? ` (@${name})` : ''}`
|
|
}
|
|
export async function resolve(
|
|
host: ExtensionHost,
|
|
to: string,
|
|
from: number | 'user',
|
|
ctx: Context,
|
|
) {
|
|
if (to === 'user') return to
|
|
let id: number | undefined
|
|
if (to.startsWith('@')) {
|
|
const service = host.need(SubagentsApi)
|
|
const valid = await service.residents(ctx)
|
|
if (!valid.some((item) => `@${item.name}` === to))
|
|
throw new Error(
|
|
`Unknown address ${to}. Valid main agents: ${valid.map((item) => `@${item.name}`).join(', ') || '(none)'}`,
|
|
)
|
|
id = await service.mainAgent(to.slice(1), ctx)
|
|
} else if (/^#?\d+$/.test(to)) id = Number(to.replace('#', ''))
|
|
if (id === undefined) throw new Error(`No recipient for ${to}`)
|
|
if (id === from) throw new Error('You cannot message yourself')
|
|
await host.harness().commit(async (tx) => {
|
|
if (!(await tx.conversation(id as ConversationId)))
|
|
throw new Error(`Unknown conversation #${id}`)
|
|
const roots = Roots
|
|
const tally = Tally
|
|
if (roots && tally) {
|
|
const index = (await tx.doc(roots, ROOT_CONVERSATION_ID)) as {
|
|
roots?: number[]
|
|
}
|
|
const state = (await tx.doc(tally, id as ConversationId)) as {
|
|
since?: number
|
|
converting?: unknown
|
|
}
|
|
if (index.roots?.includes(id) && (state.since !== undefined || state.converting))
|
|
throw new Error(`#${id} is a tally and takes no inputs; use note instead`)
|
|
}
|
|
}, ctx)
|
|
return id
|
|
}
|