/** * Public Diarist (起居郎) Role run — #708 / ADR 0075(起居郎是 LLM 角色)/ #779. * Same admit → post-admission → settle shape as the other instruction seats. * Semantic collection is the role's own turn: LLM finds materials itself * (no frozen candidate catalog, no path/attachment burden on the caller). * Who calls it and in what order is the caller's business (ADR 0010; ADR 0075 不规定调用顺序). */ import type { DurablePrincipalAuthority, RoleTurnRequest } from "../host-contracts.ts"; import { engineSessionMaterialFromOptions, pickEngineAxis } from "../package-resources/engine-material.ts"; import { CliUsageError } from "./cli-errors.ts"; import { admitDiaristInvocation, bindAdmittedTicketNumber, buildInstructionTransportPrompt, relocateAdmittedRunToTicket, recordAdmittedCorrelation, type AdmittedDiaristInvocation, type ParseDiaristArgvResult, } from "./invocation.ts"; import { prepareSummonsResumeMaterials, runPostAdmissionOneShot, runPostAdmissionSeatResume, resumeTurnRequestProjectionOptions, type PostAdmissionAdapters, type PostAdmissionEnv, } from "./post-admission.ts"; import { loadResumableDiaristRun, markRunAdmitted, type PublicResumeRequest, type RunWriterLease, } from "./run-lifecycle.ts"; import { presentStructuralRejection, trySettleDiaristTerminalResult, } from "./settlement.ts"; import type { CliIo } from "./cli-io.ts"; import type { TerminalResult } from "./terminal.ts"; import { projectRoleTurnRequest, type RoleTurnRequestProjectionOptions, } from "./turn-request.ts"; import { readBoardTicketNumber } from "../run-ticket-number.ts"; export type DiaristRunEnv = PostAdmissionEnv & { principalAuthority: DurablePrincipalAuthority; createRunId?: () => string; /** * Typed handoff from a caller that already holds a verified ticket key * (countersign refresh / post-assert). Never derived from summons prose. * When set: same-ticket resume under that key, else bind before the turn * so identity is already on the run pages (ADR 0075 / 0081). */ boundTicketNumber?: number; }; /** Project admitted invocation onto the host-neutral turn request. */ export function buildDiaristTurnRequest( admitted: AdmittedDiaristInvocation, options: RoleTurnRequestProjectionOptions, ): RoleTurnRequest { return projectRoleTurnRequest( admitted, { activation: { role: "diarist" as const } }, options, ); } /** * Board-bound ticket on pages → memory bind + unbound→ticket relocate under lease. * Shared by resume (interrupted board-bound unbound) and post-turn first assert. */ async function bindAndRelocateDiaristIfBoardBound( admitted: AdmittedDiaristInvocation, authority: DiaristRunEnv["principalAuthority"], lease?: RunWriterLease, ): Promise { const boardTicket = await readBoardTicketNumber(admitted.runDirectory); if (boardTicket === undefined) return; if (admitted.ticketNumber === undefined) { await bindAdmittedTicketNumber(admitted, boardTicket); } await relocateAdmittedRunToTicket(admitted, authority, lease); } function diaristAdapters( env: DiaristRunEnv, ): PostAdmissionAdapters { return { trySettle: (admitted, authority, scope) => trySettleDiaristTerminalResult(admitted, authority, scope), shouldPresentSettled: () => true, // Resume interrupted state: board ticket already on pages, run still under unbound/. beforeDispatch: async (admitted, lease) => { if (env.correlationId !== undefined && env.correlationId.trim() !== "") { await recordAdmittedCorrelation(admitted, env.correlationId); } await bindAndRelocateDiaristIfBoardBound(admitted, env.principalAuthority, lease); }, // First post-turn assert: accept hook wrote board pages; relocate before lease release. afterDispatch: async (admitted, lease) => { await bindAndRelocateDiaristIfBoardBound(admitted, env.principalAuthority, lease); }, }; } export async function runPublicDiarist( argv: readonly string[], env: DiaristRunEnv, io: CliIo, parseDiaristArgv: (args: readonly string[]) => ParseDiaristArgvResult, ): Promise<{ exitCode: number; admitted?: AdmittedDiaristInvocation; terminal?: TerminalResult; }> { let parsed: ParseDiaristArgvResult; try { parsed = parseDiaristArgv(argv); } catch (error) { if (error instanceof CliUsageError) { presentStructuralRejection(error, io); return { exitCode: 2 }; } throw error; } // #637 / #771 / ADR 0075 / #779 / #987: ticket identity is the LLM's typed // assertion on this turn, OR a typed handoff key already held by the caller // (countersign refresh bind). Code never pre-judges the summons text and never // freezes a candidate catalog. Public same-ticket resume-by-ticketNumber was // deleted (#987 Result 7); callers continue an existing diarist run only via // explicit `ak-role resume `. Handoff ticket remains a bind key only. const handoffTicket = env.boundTicketNumber; let admitted: AdmittedDiaristInvocation; try { admitted = await admitDiaristInvocation({ home: env.home, principalAuthority: env.principalAuthority, cwd: env.cwd, instruction: parsed.instruction, attachmentPaths: parsed.attachmentPaths, ...(parsed.project === undefined ? {} : { project: parsed.project }), ...(env.createRunId === undefined ? {} : { createRunId: env.createRunId }), ...(env.model === undefined ? {} : { model: env.model }), ...(env.correlationId === undefined ? {} : { correlationId: env.correlationId }), ...(handoffTicket === undefined ? {} : { assertedTicketNumber: handoffTicket }), }); } catch (error) { if (error instanceof CliUsageError) { presentStructuralRejection(error, io); return { exitCode: 2 }; } throw error; } await markRunAdmitted(admitted, env.principalAuthority); const turnProjection: RoleTurnRequestProjectionOptions = { packageRoot: env.packageRoot, home: env.home, agentDir: env.agentDir, ...(env.model === undefined ? {} : { model: env.model }), ...pickEngineAxis(env), ...(env.timeoutMs === undefined ? {} : { timeoutMs: env.timeoutMs }), ...(env.correlationId === undefined || env.correlationId.trim() === "" ? {} : { correlationId: env.correlationId }), continuation: { kind: "initial", prompt: buildInstructionTransportPrompt( admitted, engineSessionMaterialFromOptions({ ...pickEngineAxis(env), packageRoot: env.packageRoot, }), ), }, }; const turnRequest = buildDiaristTurnRequest(admitted, turnProjection); return await runPostAdmissionOneShot({ admitted, env, io, request: turnRequest, adapters: diaristAdapters(env), ...(env.engine === undefined ? {} : { effectiveEngine: env.engine }), }); } /** * Resume a previously admitted Diarist run (#708 / ADR 0079 同票传召 = resume). * LLM re-finds materials; entry identity keeps the volume idempotent. */ export async function runPublicDiaristResume( request: PublicResumeRequest, env: DiaristRunEnv, io: CliIo, ): Promise<{ exitCode: number; admitted?: AdmittedDiaristInvocation; terminal?: TerminalResult; }> { return await runPostAdmissionSeatResume({ request, env, io, load: (effective) => loadResumableDiaristRun(env.home, effective.runId, env.principalAuthority), buildTurnRequest: async (admitted, effective) => { const summonsPrepared = await prepareSummonsResumeMaterials( admitted.runDirectory, effective.summons, ); return buildDiaristTurnRequest( admitted, resumeTurnRequestProjectionOptions( admitted, effective, env, summonsPrepared, ), ); }, adapters: diaristAdapters(env), ...(env.engine === undefined ? {} : { effectiveEngine: env.engine }), }); }