/ src / lib / server / execution-brief.ts
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  }