Something went wrong. Try again.
A collaborative coding-agent orchestrator for atproto radl.app
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434import type { ArtifactLinks, ArtifactRecord, ArtifactRequestRecord, CheckrunRecord, CheckrunResult, MessageRecord, ReviewFinding, ReviewRecord, StrongRef,} from './generated/records.js'import { ANSWER_TYPE_NAME, REVIEW_TYPE_NAME, type GoalView, type IndexedRecord, type MaterializedIndex, type ProjectView,} from './materializer.js'import { compareCodePoints, compareInstant } from './order.js'
export interface BundleArtifact { uri: string cid: string type: string /** The artifact's own short title, so a turn can tell two `adr` records apart — or find the one * system document it needs to revise — without reading every body in the bundle. Absent for a * record written before the field existed; a bundle built from one is otherwise unchanged. */ title?: string body: string criteria?: string[] links: ArtifactLinks createdAt: string}
export interface TurnBundle { request: { uri: string; cid: string; type: string; brief?: string; basedOn: StrongRef[] } /** The anchoring goal. Present for a goal-scoped request (its own goal) and for a project-scoped * CAPTURE request (the source artifact's goal, reached through basedOn). Absent for a direct * project-scoped request that isn't distilling from any goal artifact. */ goal?: { uri: string cid: string title: string body: string /** The goal's labels, as the space currently holds them — how its members have categorized this * work. Additive and often empty; a turn planning against a goal reads them the way it reads the * title, as context and never as instruction. Author order, and always present (possibly `[]`) * so a reader never has to distinguish "no labels" from "an older bundle". */ labels: readonly string[] } project: { uri: string; cid: string; name: string; gitUrl: string; defaultBranch: string } basedOn: BundleArtifact[] /** For a `review`-type request: the artifact under review. It is also seeded into the basedOn * ancestry walk, so its version history, prior review findings and checkruns flow into the bundle * the same way basedOn artifacts do. Absent for non-review requests. */ subject?: BundleArtifact /** For an `answer`-type request: the exact message version being answered. The whole thread is in * `thread` regardless; this is the one line the turn was commissioned to reply to, pinned by the * uri#cid the request named, so a turn never has to guess which message it is answering. */ answering?: { uri: string; cid: string; author: string; body: string; createdAt: string } reviewFindings: Array<{ subject: StrongRef; verdict: 'approve' | 'request_changes'; findings: ReviewFinding[] }> checkruns: Array<{ artifact: StrongRef; results: CheckrunResult[] }> currentSystemArtifacts: BundleArtifact[] thread: Array<{ uri: string cid: string author: string body: string re?: StrongRef parent?: StrongRef /** Who the message addressed, when it addressed anybody. Addressing, never dispatch (design §10) — * a turn reads it to know an exchange was directed at it, and nothing runs because of it. */ mentions?: string[] createdAt: string }> /** * Comments by non-members that an active member has explicitly blessed — the ONLY way text from * outside the space reaches an agent, and it reaches it as bytes a member signed rather than as * whatever the guest's PDS serves at dispatch time (`GoalView.blessedComments`). * * Deliberately NOT merged into `thread`. Two reasons, and the second is the load-bearing one: * a turn can always tell member conversation from guest input, whatever a body claims about * itself; and an `answer` request resolves its subject out of `threadMessages` alone, so a guest * comment can never be the message an agent is commissioned to reply to. * * `uri`/`cid` pin the guest's own record so a reader can go and audit the copy while it is up; * `body` is the snapshot the member vouched for, and it is what is rendered. `blessedAt` is when * the blessing happened — the guest record's own timestamp is not in the bless and the fold cannot * go and fetch it. */ guestComments: Array<{ uri: string cid: string guestDid: string body: string blessedBy: string blessedAt: string }> /** Selected Roomy messages copied into protocol state by a member. Live attachments never appear here. */ importedMessages: Array<{ uri: string body: string importedBy: string importedAt: string source: { kind: string; service: string; roomId: string; messageId: string; authorDid: string; authorName?: string; timestamp: string } }> missingRefs: StrongRef[]}
// Local, self-contained equivalents of materializer.ts's refKey/recordRefKey/compareRecord// helpers. Not exported from materializer.ts on purpose (keeps this module dependency-light).const refKey = (reference: StrongRef): string => `${reference.uri}#${reference.cid}`const recordRefKey = (record: Pick<StrongRef, 'uri' | 'cid'>): string => `${record.uri}#${record.cid}`
const byCreatedThenUri = ( left: { createdAt: string; uri: string }, right: { createdAt: string; uri: string },): number => compareInstant(left.createdAt, right.createdAt) || compareCodePoints(left.uri, right.uri)
function toBundleArtifact(artifact: IndexedRecord<ArtifactRecord>): BundleArtifact { return { uri: artifact.uri, cid: artifact.cid, type: artifact.value.type, ...(artifact.value.title !== undefined ? { title: artifact.value.title } : {}), body: artifact.value.body, ...(artifact.value.criteria !== undefined ? { criteria: artifact.value.criteria } : {}), links: artifact.value.links, createdAt: artifact.value.createdAt, }}
export function buildTurnBundle( index: MaterializedIndex, requestUri: string, requestCid: string,): TurnBundle { // Owner search: goal-scoped requests live under a goal (index.goals); project-scoped requests // (both direct and capture) live under a project (index.projects). Track which kind owns it. let owningGoal: GoalView | undefined let owningProject: ProjectView | undefined let request: IndexedRecord<ArtifactRequestRecord> | undefined for (const goal of index.goals) { const candidate = goal.requests.find((entry) => entry.uri === requestUri) if (candidate) { owningGoal = goal request = candidate break } } if (!request) { for (const project of index.projects) { const candidate = project.requests.find((entry) => entry.uri === requestUri) if (candidate) { owningProject = project request = candidate break } } } if (!request) { throw new Error(`buildTurnBundle: no request found at ${requestUri}`) } if (request.cid !== requestCid) { throw new Error(`buildTurnBundle: cid mismatch for ${requestUri}`) }
// projectView: a goal-scoped request → the goal's project; a project-scoped request → its own // owning project view. const projectView: ProjectView | undefined = owningGoal ? index.projects.find((view) => view.target.uri === owningGoal.target.value.project.uri) : owningProject if (!projectView) { throw new Error(`buildTurnBundle: no project found for request ${requestUri}`) }
// basedOn can cross goal/project scopes, so lookup maps are built across the whole index. const allArtifacts: Array<IndexedRecord<ArtifactRecord>> = [ ...index.goals.flatMap((view) => view.artifacts), ...index.projects.flatMap((view) => view.artifacts), ] const allReviews: Array<IndexedRecord<ReviewRecord>> = [ ...index.goals.flatMap((view) => view.reviews), ...index.projects.flatMap((view) => view.reviews), ] const allCheckruns: Array<IndexedRecord<CheckrunRecord>> = [ ...index.goals.flatMap((view) => view.checkruns), ...index.projects.flatMap((view) => view.checkruns), ] const artifactByRef = new Map(allArtifacts.map((artifact) => [recordRefKey(artifact), artifact]))
const missingRefs = new Map<string, StrongRef>() const resolve = (reference: StrongRef): IndexedRecord<ArtifactRecord> | undefined => { const found = artifactByRef.get(refKey(reference)) if (!found) missingRefs.set(refKey(reference), reference) return found }
// Same shape as materializer.ts's targetUriForArtifact, kept local since that // helper isn't exported. const targetUriFor = (artifact: IndexedRecord<ArtifactRecord>): string | undefined => artifact.value.goal?.uri ?? artifact.value.project?.uri
const resolvedArtifacts = new Map<string, IndexedRecord<ArtifactRecord>>() // Walk an artifact's `prev` ancestry, adding each resolved version to `resolvedArtifacts`. // Keyed by uri#cid, so seeding the same artifact from more than one entry point (e.g. a review // subject that also appears in basedOn) is naturally idempotent. const walkAncestry = (reference: StrongRef): void => { let current = resolve(reference) while (current) { const key = recordRefKey(current) if (resolvedArtifacts.has(key)) break // already walked (or a cycle) — stop resolvedArtifacts.set(key, current) if (!current.value.prev) break const previous = resolve(current.value.prev) if (!previous) break // unresolved prev — already recorded in missingRefs by resolve() // Parity with materializer.ts's buildChains(): a prev edge only continues // the ancestry chain when the previous artifact has BOTH the same `type` // and the same goal/project target as the artifact being walked from // (buildChains breaks the chain on a type mismatch; we additionally guard // the target so a same-type artifact from an unrelated goal/project can't // be walked in either). On mismatch we stop the walk here — mirroring // buildChains's `break` — without importing the mismatched ancestor, so // its review findings/checkruns never enter the bundle. This is distinct // from an unresolved ref: a resolvable-but-mismatched prev is not added // to missingRefs, it's simply not followed. if (previous.value.type !== current.value.type || targetUriFor(previous) !== targetUriFor(current)) { break } current = previous } } for (const reference of request.value.basedOn) walkAncestry(reference)
// A review request names the artifact under review via `subject`. Seed it (and its ancestry) // into the same walk so its prior findings/checkruns flow into the bundle, and surface it // explicitly as `bundle.subject`. The materializer only materializes a review request whose // subject resolves under the same target (materializer.ts), so the resolve here always succeeds // for a real review turn — no unresolved-subject branch is needed. let subject: BundleArtifact | undefined if (request.value.type === REVIEW_TYPE_NAME && request.value.subject) { walkAncestry(request.value.subject) const resolvedSubject = artifactByRef.get(refKey(request.value.subject)) if (resolvedSubject) subject = toBundleArtifact(resolvedSubject) }
// The bundle's anchoring goal. A goal-owned request uses its own goal. A project-owned (capture) // request distils from a goal artifact: among its DIRECTLY resolved basedOn artifacts, any that // carry a `goal` anchor identify the source goal. On more than one distinct source goal, pick the // lowest goal uri deterministically. None → the bundle has no goal (a direct project request). let goalView: GoalView | undefined = owningGoal if (!owningGoal) { const sourceGoalUris = new Set<string>() for (const reference of request.value.basedOn) { const artifact = artifactByRef.get(refKey(reference)) if (artifact?.value.goal) sourceGoalUris.add(artifact.value.goal.uri) } if (sourceGoalUris.size > 0) { const chosen = [...sourceGoalUris].sort()[0] goalView = index.goals.find((view) => view.target.uri === chosen) } }
const basedOn = [...resolvedArtifacts.values()].map(toBundleArtifact).sort(byCreatedThenUri)
const reviewFindings = allReviews .filter((review) => resolvedArtifacts.has(refKey(review.value.subject))) .sort( (a, b) => compareCodePoints(a.value.subject.uri, b.value.subject.uri) || compareCodePoints(a.value.subject.cid, b.value.subject.cid) || compareInstant(a.value.createdAt, b.value.createdAt) || compareCodePoints(a.uri, b.uri), ) .map((review) => ({ subject: review.value.subject, verdict: review.value.verdict, findings: review.value.findings, }))
const checkruns = allCheckruns .filter((checkrun) => resolvedArtifacts.has(refKey(checkrun.value.artifact))) .sort( (a, b) => compareCodePoints(a.value.artifact.uri, b.value.artifact.uri) || compareCodePoints(a.value.artifact.cid, b.value.artifact.cid) || compareInstant(a.value.createdAt, b.value.createdAt) || compareCodePoints(a.uri, b.uri), ) .map((checkrun) => ({ artifact: checkrun.value.artifact, results: checkrun.value.results }))
const currentSystemArtifacts = projectView.currentSystemArtifacts .map(toBundleArtifact) .sort(byCreatedThenUri)
// Thread: the anchoring goal's messages (owning goal, or the capture source goal), PLUS — for a // project-owned request — the owning project view's messages anchored to a resolved artifact // (system-artifact threads that carry no goal). Merge, dedupe by uri, then sort with the shared // byCreatedThenUri so the result is arrival-order invariant. const threadMessages = new Map<string, IndexedRecord<MessageRecord>>() if (goalView) { for (const message of goalView.messages) threadMessages.set(message.uri, message) } if (owningProject) { for (const message of projectView.messages) { if (message.value.artifact && resolvedArtifacts.has(refKey(message.value.artifact))) { threadMessages.set(message.uri, message) } } } const thread = [...threadMessages.values()] .map((message) => ({ uri: message.uri, // The cid is what makes a thread line addressable: an `answer` request pins the exact message // version it asks about, and a turn reading the thread can only name one if it has it. cid: message.cid, author: message.did, body: message.value.body, ...(message.value.re !== undefined ? { re: message.value.re } : {}), ...(message.value.parent !== undefined ? { parent: message.value.parent } : {}), ...(message.value.mentions.length > 0 ? { mentions: message.value.mentions } : {}), createdAt: message.value.createdAt, })) .sort(byCreatedThenUri)
// The message an `answer` turn was commissioned to reply to, resolved against the goal's own // messages by exact uri#cid. The materializer only materializes an answer request whose subject // resolves under the request's goal, so this always resolves for a real answer turn; the guard is // defensive, in the same register as the review-subject branch above. let answering: TurnBundle['answering'] if (request.value.type === ANSWER_TYPE_NAME && request.value.subject) { const subjectRef = request.value.subject const found = threadMessages.get(subjectRef.uri) if (found && found.cid === subjectRef.cid) { answering = { uri: found.uri, cid: found.cid, author: found.did, body: found.value.body, createdAt: found.value.createdAt, } } else { missingRefs.set(refKey(subjectRef), subjectRef) } }
// Built from the anchoring goal's blessed comments and from nothing else — there is no other // source in the index, because an unblessed guest comment is not in the index at all. Ordered by // the blessing (createdAt, uri), so the section is arrival-order invariant like every other list. const guestComments = [...(goalView?.blessedComments ?? [])] .sort((left, right) => byCreatedThenUri( { createdAt: left.value.createdAt, uri: left.uri }, { createdAt: right.value.createdAt, uri: right.uri }, ), ) .map((bless) => ({ uri: bless.value.comment.uri, cid: bless.value.comment.cid, // The guest's DID, read off the record they wrote it in. An at-uri's authority IS the repo it // came from, so this is provenance rather than a claim the bless makes about somebody else. guestDid: bless.value.comment.uri.slice('at://'.length).split('/')[0] ?? '', body: bless.value.body, blessedBy: bless.did, blessedAt: bless.value.createdAt, }))
const importedMessages = [...(goalView?.importedMessages ?? [])] .sort((left, right) => byCreatedThenUri( { createdAt: left.value.createdAt, uri: left.uri }, { createdAt: right.value.createdAt, uri: right.uri }, )) .map((entry) => ({ uri: entry.uri, body: entry.value.body, importedBy: entry.did, importedAt: entry.value.createdAt, source: entry.value.source, }))
const missingRefsOut = [...missingRefs.values()].sort( (a, b) => compareCodePoints(a.uri, b.uri) || compareCodePoints(a.cid, b.cid), )
return { request: { uri: request.uri, cid: requestCid, type: request.value.type, ...(request.value.brief !== undefined ? { brief: request.value.brief } : {}), basedOn: request.value.basedOn, }, ...(goalView ? { goal: { uri: goalView.target.uri, cid: goalView.target.cid, title: goalView.target.value.title, body: goalView.target.value.body, labels: goalView.labels, }, } : {}), project: { uri: projectView.target.uri, cid: projectView.target.cid, // The EFFECTIVE name, remote and branch — the record's own overlaid by its author's // `editProject` records. A turn clones what the project says now, not what it said when it was // written; `uri`/`cid` still pin the record, which is what makes the provenance honest. name: projectView.name, gitUrl: projectView.gitUrl, defaultBranch: projectView.defaultBranch, }, basedOn, ...(subject ? { subject } : {}), ...(answering ? { answering } : {}), reviewFindings, checkruns, currentSystemArtifacts, thread, guestComments, importedMessages, missingRefs: missingRefsOut, }}
/** * Back-compat aliases. The bundle generalized from plan-only to any goal-scoped artifact type * (implementation, custom types); the old names are kept so existing callers/tests keep compiling. */export type PlanTurnBundle = TurnBundleexport const buildPlanTurnBundle = buildTurnBundle