execution-brief.ts
1 import type { 2 EvidenceRef, 3 ExecutionBrief, 4 ExecutionBriefPlanStep, 5 Session, 6 SessionWorkingState, 7 WorkingPlanStep, 8 WorkingStateItemStatus, 9 WorkingStateStatus, 10 } from '@/types' 11 import { getSession } from '@/lib/server/sessions/session-repository' 12 import { loadSessionWorkingState } from '@/lib/server/working-state/service' 13 import { ensureRunContext } from '@/lib/server/run-context' 14 import { cleanText, cleanMultiline } from '@/lib/server/text-normalization' 15 import { resolveEffectiveGoal, getGoalChain, formatGoalChainForBrief } from '@/lib/server/goals/goal-service' 16 17 const MAX_PLAN_ITEMS = 8 18 const MAX_FACTS = 8 19 const MAX_BLOCKERS = 6 20 const MAX_ARTIFACTS = 6 21 const MAX_EVIDENCE = 6 22 const MAX_DELEGATION_PLAN_ITEMS = 4 23 const MAX_DELEGATION_FACTS = 4 24 const MAX_DELEGATION_BLOCKERS = 4 25 const MAX_DELEGATION_ARTIFACTS = 4 26 const DELEGATION_BUDGET = 1_200 27 28 function uniqueStrings(values: Array<unknown>, maxItems: number, maxChars = 240): string[] { 29 const out: string[] = [] 30 const seen = new Set<string>() 31 for (const value of values) { 32 const normalized = cleanText(value, maxChars) 33 if (!normalized) continue 34 const key = normalized.toLowerCase() 35 if (seen.has(key)) continue 36 seen.add(key) 37 out.push(normalized) 38 if (out.length >= maxItems) break 39 } 40 return out 41 } 42 43 function summarizeArtifact(value: unknown): string { 44 if (!value || typeof value !== 'object' || Array.isArray(value)) return '' 45 const artifact = value as Record<string, unknown> 46 return cleanText(artifact.path || artifact.url || artifact.label, 220) 47 } 48 49 function summarizeEvidenceRef(ref: EvidenceRef): string { 50 const summary = cleanText(ref.summary, 180) 51 if (!summary) return '' 52 const value = cleanText(ref.value, 140) 53 return value ? `[${ref.type}] ${summary}: ${value}` : `[${ref.type}] ${summary}` 54 } 55 56 function planStatus(step: WorkingPlanStep): WorkingStateItemStatus { 57 return step.status === 'resolved' || step.status === 'superseded' ? step.status : 'active' 58 } 59 60 function dedupePlan(steps: ExecutionBriefPlanStep[]): ExecutionBriefPlanStep[] { 61 const out: ExecutionBriefPlanStep[] = [] 62 const seen = new Set<string>() 63 for (const step of steps) { 64 const text = cleanText(step.text, 240) 65 if (!text) continue 66 const key = text.toLowerCase() 67 if (seen.has(key)) continue 68 seen.add(key) 69 out.push({ 70 text, 71 status: step.status === 'resolved' || step.status === 'superseded' ? step.status : 'active', 72 }) 73 if (out.length >= MAX_PLAN_ITEMS) break 74 } 75 return out 76 } 77 78 function inferStatus(workingState: SessionWorkingState | null): WorkingStateStatus { 79 if (workingState?.status) return workingState.status 80 return 'idle' 81 } 82 83 function buildPlan(workingState: SessionWorkingState | null, session: Session | null): ExecutionBriefPlanStep[] { 84 if (workingState && Array.isArray(workingState.planSteps) && workingState.planSteps.length > 0) { 85 return dedupePlan( 86 workingState.planSteps 87 .filter((step) => step.status !== 'superseded') 88 .map((step) => ({ 89 text: cleanText(step.text, 240), 90 status: planStatus(step), 91 })), 92 ) 93 } 94 const runContext = session?.runContext ? ensureRunContext(session.runContext) : null 95 if (!runContext || !Array.isArray(runContext.currentPlan) || runContext.currentPlan.length === 0) return [] 96 const completed = new Set((runContext.completedSteps || []).map((value) => cleanText(value, 240).toLowerCase()).filter(Boolean)) 97 return dedupePlan( 98 runContext.currentPlan.map((step) => { 99 const text = cleanText(step, 240) 100 return { 101 text, 102 status: completed.has(text.toLowerCase()) ? 'resolved' : 'active', 103 } 104 }), 105 ) 106 } 107 108 function buildFacts(workingState: SessionWorkingState | null, session: Session | null): string[] { 109 const activeFacts = workingState 110 ? workingState.confirmedFacts 111 .filter((fact) => fact.status === 'active') 112 .map((fact) => fact.statement) 113 : [] 114 const runContextFacts = session?.runContext ? ensureRunContext(session.runContext).keyFacts : [] 115 return uniqueStrings([...activeFacts, ...runContextFacts], MAX_FACTS, 240) 116 } 117 118 function buildBlockers(workingState: SessionWorkingState | null, session: Session | null): string[] { 119 const activeBlockers = workingState 120 ? workingState.blockers 121 .filter((blocker) => blocker.status === 'active') 122 .map((blocker) => blocker.nextAction ? `${blocker.summary} | next: ${blocker.nextAction}` : blocker.summary) 123 : [] 124 const runContextBlockers = session?.runContext ? ensureRunContext(session.runContext).blockers : [] 125 return uniqueStrings([...activeBlockers, ...runContextBlockers], MAX_BLOCKERS, 280) 126 } 127 128 function buildArtifacts(workingState: SessionWorkingState | null): string[] { 129 const artifacts = workingState 130 ? workingState.artifacts 131 .filter((artifact) => artifact.status === 'active') 132 .map((artifact) => summarizeArtifact(artifact)) 133 : [] 134 return uniqueStrings(artifacts, MAX_ARTIFACTS, 220) 135 } 136 137 function buildConstraints(workingState: SessionWorkingState | null, session: Session | null): string[] { 138 const workingConstraints = workingState?.constraints || [] 139 const runContext = session?.runContext ? ensureRunContext(session.runContext) : null 140 return uniqueStrings([...(workingConstraints || []), ...(runContext?.constraints || [])], 10, 220) 141 } 142 143 function buildSuccessCriteria(workingState: SessionWorkingState | null): string[] { 144 return uniqueStrings([...(workingState?.successCriteria || [])], 10, 220) 145 } 146 147 function buildEvidenceRefs(workingState: SessionWorkingState | null): EvidenceRef[] { 148 if (!workingState || !Array.isArray(workingState.evidenceRefs) || workingState.evidenceRefs.length === 0) return [] 149 return [...workingState.evidenceRefs] 150 .filter((ref) => Boolean(cleanText(ref.summary, 180))) 151 .slice(-MAX_EVIDENCE) 152 } 153 154 export function buildExecutionBrief(params: { 155 sessionId?: string | null 156 session?: Session | null 157 mission?: null 158 workingState?: SessionWorkingState | null 159 }): ExecutionBrief { 160 const session = params.session 161 || (params.sessionId ? getSession(params.sessionId) || null : null) 162 const workingState = params.workingState 163 || (session?.id ? loadSessionWorkingState(session.id) : null) 164 const runContext = session?.runContext ? ensureRunContext(session.runContext) : null 165 const plan = buildPlan(workingState, session) 166 const nextAction = cleanText( 167 workingState?.nextAction 168 || plan.find((step) => step.status === 'active')?.text, 169 240, 170 ) || null 171 172 return { 173 sessionId: session?.id || params.sessionId || null, 174 objective: cleanMultiline( 175 workingState?.objective 176 || runContext?.objective, 177 900, 178 ) || null, 179 summary: cleanMultiline( 180 workingState?.summary, 181 700, 182 ) || null, 183 status: inferStatus(workingState), 184 nextAction, 185 plan, 186 blockers: buildBlockers(workingState, session), 187 facts: buildFacts(workingState, session), 188 artifacts: buildArtifacts(workingState), 189 constraints: buildConstraints(workingState, session), 190 successCriteria: buildSuccessCriteria(workingState), 191 evidenceRefs: buildEvidenceRefs(workingState), 192 parentContext: cleanMultiline(runContext?.parentContext, 900) || null, 193 } 194 } 195 196 function buildListSection(title: string, values: string[]): string | null { 197 if (!values.length) return null 198 return [title, ...values.map((value) => `- ${value}`)].join('\n') 199 } 200 201 function formatPlan(plan: ExecutionBriefPlanStep[]): string | null { 202 if (!plan.length) return null 203 return [ 204 'Plan', 205 ...plan.map((step) => `- [${step.status === 'resolved' ? 'x' : ' '}] ${step.text}`), 206 ].join('\n') 207 } 208 209 export function buildExecutionBriefContextBlock( 210 brief: ExecutionBrief | null | undefined, 211 options?: { title?: string }, 212 ): string { 213 if (!brief) return '' 214 const hasContent = Boolean( 215 brief.parentContext 216 || brief.objective 217 || brief.summary 218 || brief.nextAction 219 || brief.plan.length > 0 220 || brief.blockers.length > 0 221 || brief.facts.length > 0 222 || brief.artifacts.length > 0 223 || brief.constraints.length > 0 224 || brief.successCriteria.length > 0 225 || brief.evidenceRefs.length > 0, 226 ) 227 if (!hasContent && brief.status === 'idle') return '' 228 // Resolve goal chain for the session's agent/task/project context 229 let goalBlock = '' 230 if (brief.sessionId) { 231 const session = getSession(brief.sessionId) 232 if (session) { 233 const goal = resolveEffectiveGoal({ 234 agentId: session.agentId || null, 235 projectId: session.projectId || null, 236 }) 237 if (goal) { 238 const chain = getGoalChain(goal.id) 239 goalBlock = formatGoalChainForBrief(chain) 240 } 241 } 242 } 243 244 const sections = [ 245 options?.title || '## Execution Brief', 246 goalBlock, 247 brief.parentContext ? `Parent context:\n${brief.parentContext}` : '', 248 brief.objective ? `Objective: ${brief.objective}` : '', 249 brief.summary ? `Summary: ${brief.summary}` : '', 250 `Status: ${brief.status}`, 251 brief.nextAction ? `Next action: ${brief.nextAction}` : '', 252 brief.successCriteria.length > 0 ? `Success criteria: ${brief.successCriteria.join(' | ')}` : '', 253 brief.constraints.length > 0 ? `Constraints: ${brief.constraints.join(' | ')}` : '', 254 formatPlan(brief.plan), 255 buildListSection('Blockers', brief.blockers), 256 buildListSection('Facts', brief.facts), 257 buildListSection('Artifacts', brief.artifacts), 258 buildListSection('Evidence', brief.evidenceRefs.map((ref) => summarizeEvidenceRef(ref)).filter(Boolean)), 259 'Trust this execution brief before reconstructing state from the raw transcript or older assistant text.', 260 ].filter(Boolean) 261 return sections.join('\n') 262 } 263 264 export function serializeExecutionBriefForDelegation( 265 brief: ExecutionBrief | null | undefined, 266 ): string | null { 267 if (!brief) return null 268 const parts: string[] = [] 269 let budget = DELEGATION_BUDGET 270 271 const append = (line: string): void => { 272 if (!line) return 273 if (budget - line.length - 1 < 0) return 274 parts.push(line) 275 budget -= line.length + 1 276 } 277 278 append(brief.objective ? `Objective: ${brief.objective}` : '') 279 append(brief.summary ? `Summary: ${cleanText(brief.summary, 280)}` : '') 280 append(`Status: ${brief.status}`) 281 append(brief.nextAction ? `Next action: ${brief.nextAction}` : '') 282 append(brief.successCriteria.length > 0 ? `Success criteria: ${brief.successCriteria.slice(0, 4).join('; ')}` : '') 283 append(brief.constraints.length > 0 ? `Constraints: ${brief.constraints.slice(0, 4).join('; ')}` : '') 284 append(brief.plan.length > 0 ? `Plan: ${brief.plan.slice(0, MAX_DELEGATION_PLAN_ITEMS).map((step) => step.text).join('; ')}` : '') 285 append(brief.blockers.length > 0 ? `Blockers: ${brief.blockers.slice(0, MAX_DELEGATION_BLOCKERS).join('; ')}` : '') 286 append(brief.facts.length > 0 ? `Facts: ${brief.facts.slice(0, MAX_DELEGATION_FACTS).join('; ')}` : '') 287 append(brief.artifacts.length > 0 ? `Artifacts: ${brief.artifacts.slice(0, MAX_DELEGATION_ARTIFACTS).join('; ')}` : '') 288 append(brief.parentContext ? `Parent context: ${cleanText(brief.parentContext, 280)}` : '') 289 return parts.length > 0 ? parts.join('\n') : null 290 }