@lota-sdk/core 0.1.46 → 0.1.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,7 @@
1
1
  import type { SerializableExecutionPlan } from '@lota-sdk/shared'
2
2
 
3
+ type ExecutionPlanPromptSummary = Pick<SerializableExecutionPlan, 'runId' | 'title'>
4
+
3
5
  const EXECUTION_PLAN_AGENT_PROTOCOL_PROMPT = `<execution-plan-protocol>
4
6
  - Create execution plans for multi-step work. Review existing plans before creating new ones.
5
7
  - The runtime executor owns lifecycle truth. Do not claim node completion until the executor confirms.
@@ -9,10 +11,14 @@ const EXECUTION_PLAN_AGENT_PROTOCOL_PROMPT = `<execution-plan-protocol>
9
11
  - If contracts or criteria materially change, replace the plan.
10
12
  </execution-plan-protocol>`
11
13
 
14
+ function toExecutionPlanPromptSummaries(plans: SerializableExecutionPlan[]): ExecutionPlanPromptSummary[] {
15
+ return plans.map(({ runId, title }) => ({ runId, title }))
16
+ }
17
+
12
18
  function formatExecutionPlansForPrompt(plans: SerializableExecutionPlan[]): string | undefined {
13
19
  if (plans.length === 0) return undefined
14
20
 
15
- const payload = { activePlans: plans, planCount: plans.length }
21
+ const payload = { activePlans: toExecutionPlanPromptSummaries(plans), planCount: plans.length }
16
22
 
17
23
  return ['<execution-plan-state>', JSON.stringify(payload, null, 2), '</execution-plan-state>'].join('\n')
18
24
  }
@@ -25,15 +25,3 @@ export * from './team-consultation-orchestrator'
25
25
  export * from './team-consultation-prompts'
26
26
  export * from './turn-lifecycle'
27
27
  export * from './workstream-chat-helpers'
28
- export {
29
- WorkstreamStateSchema,
30
- type WorkstreamState,
31
- type WorkstreamStateDelta,
32
- StructuredWorkstreamStateDeltaSchema,
33
- type StructuredWorkstreamStateDelta,
34
- createEmptyStructuredWorkstreamStateDelta,
35
- parseStructuredWorkstreamStateDelta,
36
- StructuredCompactionOutputSchema,
37
- type CompactionOutput,
38
- createEmptyWorkstreamState,
39
- } from './workstream-state'
@@ -28,7 +28,6 @@ import { workstreamService } from '../services/workstream.service'
28
28
  import type { NormalizedWorkstream, WorkstreamRecord } from '../services/workstream.types'
29
29
  import { safeEnqueue } from '../utils/async'
30
30
  import { toIsoDateTimeString } from '../utils/date-time'
31
- import type { WorkstreamState } from './workstream-state'
32
31
 
33
32
  function buildRecentActivityChatDeepLink(params: {
34
33
  workstream: NormalizedWorkstream
@@ -77,7 +76,6 @@ interface PostTurnSideEffectsParams {
77
76
  visibleWorkstreamAgentId: string | null | undefined
78
77
  defaultLeadAgentId: string
79
78
  latestWorkstreamRecord: WorkstreamRecord
80
- latestPersistedState: WorkstreamState | null
81
79
  isUserTurn: boolean
82
80
  }
83
81
 
@@ -87,7 +85,6 @@ export async function runPostTurnSideEffects(params: PostTurnSideEffectsParams):
87
85
  const agentMessages = buildAgentHistoryMessages(params.allAssistantMessages)
88
86
  const historyMessagesForMemory = appendPersistedWorkstreamContextToHistoryMessages(toHistoryMessages(recentHistory), {
89
87
  compactionSummary: params.latestWorkstreamRecord.compactionSummary,
90
- persistedState: params.latestPersistedState,
91
88
  })
92
89
 
93
90
  const userMessageText = params.referenceUserMessage ? extractMessageText(params.referenceUserMessage).trim() : ''
@@ -132,7 +132,6 @@ export interface AfterTurnParams {
132
132
  referenceUserMessage: unknown
133
133
  assistantMessages: unknown[]
134
134
  latestWorkstreamRecord: unknown
135
- latestPersistedState: unknown
136
135
  context: Record<string, unknown> | null
137
136
  [key: string]: unknown
138
137
  }
@@ -88,7 +88,7 @@ export function buildAgentHistoryMessages(messages: ChatMessageLike[]): Array<{
88
88
 
89
89
  export function appendPersistedWorkstreamContextToHistoryMessages(
90
90
  historyMessages: WorkstreamHistoryMessage[],
91
- params: { compactionSummary?: string | null; persistedState?: unknown },
91
+ params: { compactionSummary?: string | null },
92
92
  ): WorkstreamHistoryMessage[] {
93
93
  const nextHistoryMessages = [...historyMessages]
94
94
  const compactionSummary = typeof params.compactionSummary === 'string' ? params.compactionSummary.trim() : ''
@@ -96,13 +96,6 @@ export function appendPersistedWorkstreamContextToHistoryMessages(
96
96
  nextHistoryMessages.push({ role: 'agent', content: `Compacted chat summary:\n${compactionSummary}` })
97
97
  }
98
98
 
99
- if (params.persistedState !== null && params.persistedState !== undefined) {
100
- nextHistoryMessages.push({
101
- role: 'agent',
102
- content: `Structured workstream state:\n${JSON.stringify(params.persistedState)}`,
103
- })
104
- }
105
-
106
99
  return nextHistoryMessages
107
100
  }
108
101
 
@@ -7,10 +7,8 @@ import { databaseService } from '../db/service'
7
7
  import { TABLES } from '../db/tables'
8
8
  import { getRedisConnection } from '../redis/connection-accessor'
9
9
  import { withRedisLeaseLock } from '../redis/redis-lease-lock'
10
- import { parseWorkstreamState, toStateFieldsUpdated } from '../runtime/context-compaction'
11
10
  import { CONTEXT_WINDOW_TOKENS, WORKSTREAM_RAW_TAIL_MESSAGES } from '../runtime/context-compaction-constants'
12
- import type { WorkstreamState } from '../runtime/workstream-state'
13
- import { compactMemoryBlockSummary, contextCompactionRuntime } from './context-compaction-runtime.singleton'
11
+ import { contextCompactionRuntime, compactMemoryBlockSummary } from './context-compaction-runtime.singleton'
14
12
  import { workstreamMessageService } from './workstream-message.service'
15
13
  import { WorkstreamSchema } from './workstream.types'
16
14
 
@@ -24,8 +22,6 @@ interface PersistedCompactionMetrics {
24
22
  compactedMessageCount: number
25
23
  remainingMessageCount: number
26
24
  estimatedTokens: number
27
- stateFieldsUpdated: string[]
28
- conflictsDetected: number
29
25
  }
30
26
 
31
27
  class ContextCompactionService {
@@ -33,10 +29,6 @@ class ContextCompactionService {
33
29
  return contextCompactionRuntime.createSummaryMessage(summaryText)
34
30
  }
35
31
 
36
- formatWorkstreamStateForPrompt(state: WorkstreamState | null | undefined) {
37
- return contextCompactionRuntime.formatWorkstreamStateForPrompt(state)
38
- }
39
-
40
32
  estimateThreshold(contextSize = CONTEXT_WINDOW_TOKENS): number {
41
33
  return contextCompactionRuntime.estimateThreshold(contextSize)
42
34
  }
@@ -48,7 +40,7 @@ class ContextCompactionService {
48
40
  async compactWorkstreamHistory(params: {
49
41
  workstreamId: RecordIdRef
50
42
  contextSize?: number
51
- }): Promise<{ compacted: boolean; state: WorkstreamState | null }> {
43
+ }): Promise<{ compacted: boolean }> {
52
44
  const entityId = recordIdToString(params.workstreamId, TABLES.WORKSTREAM)
53
45
 
54
46
  return withRedisLeaseLock(
@@ -69,7 +61,6 @@ class ContextCompactionService {
69
61
  throw new Error(`Workstream not found for compaction: ${entityId}`)
70
62
  }
71
63
 
72
- const currentState = parseWorkstreamState(workstream.state)
73
64
  const liveMessages = await workstreamMessageService.listMessagesAfterCursor(
74
65
  params.workstreamId,
75
66
  typeof workstream.lastCompactedMessageId === 'string' ? workstream.lastCompactedMessageId : undefined,
@@ -80,11 +71,10 @@ class ContextCompactionService {
80
71
  liveMessages,
81
72
  tailMessageCount: WORKSTREAM_RAW_TAIL_MESSAGES,
82
73
  contextSize: params.contextSize,
83
- existingState: currentState,
84
74
  })
85
75
 
86
76
  if (!result.compacted || !result.lastCompactedMessageId) {
87
- return { compacted: false, state: currentState }
77
+ return { compacted: false }
88
78
  }
89
79
 
90
80
  if (result.compactedMessages.length > 0) {
@@ -97,11 +87,7 @@ class ContextCompactionService {
97
87
  await databaseService.update(
98
88
  TABLES.WORKSTREAM,
99
89
  params.workstreamId,
100
- {
101
- compactionSummary: result.summaryText,
102
- lastCompactedMessageId: result.lastCompactedMessageId,
103
- state: result.state,
104
- },
90
+ { compactionSummary: result.summaryText, lastCompactedMessageId: result.lastCompactedMessageId },
105
91
  WorkstreamSchema,
106
92
  )
107
93
 
@@ -115,11 +101,9 @@ class ContextCompactionService {
115
101
  compactedMessageCount: result.compactedMessageCount,
116
102
  remainingMessageCount: result.remainingMessageCount,
117
103
  estimatedTokens: result.estimatedTokens,
118
- stateFieldsUpdated: toStateFieldsUpdated(result.stateDelta),
119
- conflictsDetected: result.stateDelta.conflicts?.length ?? 0,
120
104
  })
121
105
 
122
- return { compacted: true, state: result.state }
106
+ return { compacted: true }
123
107
  },
124
108
  )
125
109
  }
@@ -34,7 +34,6 @@ import { hasApprovalRespondedParts } from '../runtime/approval-continuation'
34
34
  import { buildModelInputMessagesWithUploadMetadata, buildReadableUploadMetadataText } from '../runtime/chat-attachments'
35
35
  import { hasMessageContent } from '../runtime/chat-message'
36
36
  import { waitForCompactionIfNeeded } from '../runtime/chat-run-orchestration'
37
- import { parseWorkstreamState } from '../runtime/context-compaction'
38
37
  import { CONTEXT_WINDOW_TOKENS } from '../runtime/context-compaction-constants'
39
38
  import { createExecutionPlanInstructionSectionCache } from '../runtime/execution-plan'
40
39
  import { mergeInstructionSections } from '../runtime/instruction-sections'
@@ -55,7 +54,6 @@ import {
55
54
  buildPlanTurnSubmitToolDescription,
56
55
  } from '../runtime/workstream-plan-turn'
57
56
  import type { WorkstreamPlanTurnContext } from '../runtime/workstream-plan-turn'
58
- import type { WorkstreamState } from '../runtime/workstream-state'
59
57
  import { assembleWorkstreamTurnContext } from '../runtime/workstream-turn-context'
60
58
  import { chatRunRegistry } from '../services/chat-run-registry.service'
61
59
  import type { NormalizedWorkstream, WorkstreamRecord } from '../services/workstream.types'
@@ -88,15 +86,6 @@ function hasUIMessageStream(value: unknown): value is UIMessageStreamResult {
88
86
 
89
87
  const PRESEEDED_MEMORY_LOOKUP_LIMIT = 3
90
88
 
91
- function stripExecutionPlanFieldsFromWorkstreamState(
92
- state: WorkstreamState | null | undefined,
93
- hasExecutionPlan: boolean,
94
- ): WorkstreamState | null | undefined {
95
- if (!state || !hasExecutionPlan) return state
96
-
97
- return { ...state, currentPlan: null, tasks: [], artifacts: [] }
98
- }
99
-
100
89
  async function waitForWorkstreamCompactionIfNeeded(workstreamId: RecordIdRef): Promise<WorkstreamRecord> {
101
90
  return waitForCompactionIfNeeded({
102
91
  entityId: recordIdToString(workstreamId, TABLES.WORKSTREAM),
@@ -121,6 +110,26 @@ function optionalInstructionSection(value: unknown): string[] | undefined {
121
110
  return section ? [section] : undefined
122
111
  }
123
112
 
113
+ function writeMultiAgentEvent(
114
+ writer: UIMessageStreamWriter<ChatMessage> | undefined,
115
+ event: {
116
+ phase: 'routing' | 'waiting-for-agent' | 'agent-message-persisted' | 'complete'
117
+ agentId?: string
118
+ agentName?: string
119
+ messageId?: string
120
+ note?: string
121
+ },
122
+ ): void {
123
+ if (!writer) return
124
+
125
+ writer.write({
126
+ type: 'data-multi-agent-event',
127
+ id: `multi-agent-${Bun.randomUUIDv7()}`,
128
+ data: event,
129
+ transient: true,
130
+ } as unknown as ChatStreamChunk)
131
+ }
132
+
124
133
  function applyPlanTurnToolPolicy(tools: ToolSet, nodeSpec: PlanNodeSpecRecord): ToolSet {
125
134
  const blockedToolNames = new Set([...OWNERSHIP_DISPATCH_BLOCKED_TOOL_NAMES, ...nodeSpec.toolPolicy.deny])
126
135
  const allowList = nodeSpec.toolPolicy.allow.length > 0 ? new Set(nodeSpec.toolPolicy.allow) : null
@@ -219,7 +228,6 @@ interface StreamAgentResponseContext {
219
228
  buildContextResult: Record<string, unknown> | null
220
229
  getExecutionPlanInstructionSections: () => Promise<string[] | undefined>
221
230
  getPreSeededMemoriesSection: (agentId: string) => Promise<string | undefined>
222
- getWorkstreamStateSection: () => Promise<string | undefined>
223
231
  getLearnedSkillsSection: (agentId: string, queryText?: string) => Promise<string | undefined>
224
232
  promptContext: { systemWorkspaceDetails?: string }
225
233
  retrievedKnowledgeSection: string | undefined
@@ -245,7 +253,6 @@ interface StreamAgentResponseParams {
245
253
  stopWhen?: StopCondition<ToolSet> | Array<StopCondition<ToolSet>>
246
254
  prepareStep?: PrepareStepFunction<ToolSet>
247
255
  abortSignal?: AbortSignal
248
- suppressFinish?: boolean
249
256
  }
250
257
 
251
258
  async function streamAgentResponse(
@@ -278,12 +285,11 @@ async function streamAgentResponse(
278
285
  const resolvedAgentId = readOptionalString(agentResolution?.agentId) ?? streamParams.agentId
279
286
  const latestUserMessage = [...streamParams.messages].reverse().find((message) => message.role === 'user')
280
287
  const latestUserMessageText = latestUserMessage ? extractMessageText(latestUserMessage).trim() : undefined
281
- const [preSeededMemoriesSection, workstreamStateSection, learnedSkillsSection] = await Promise.all([
288
+ const [preSeededMemoriesSection, learnedSkillsSection] = await Promise.all([
282
289
  ctx.getPreSeededMemoriesSection(resolvedAgentId),
283
- ctx.getWorkstreamStateSection(),
284
290
  ctx.getLearnedSkillsSection(resolvedAgentId, latestUserMessageText),
285
291
  ])
286
- agentTimer.step('parallel-fetch(memories+state+skills)')
292
+ agentTimer.step('parallel-fetch(memories+skills)')
287
293
  const toolNames = new Set(Object.keys(streamParams.tools))
288
294
  const hasRetrievalTools = [
289
295
  'memorySearch',
@@ -308,7 +314,6 @@ async function streamAgentResponse(
308
314
  preSeededMemoriesSection,
309
315
  retrievedKnowledgeSection: ctx.retrievedKnowledgeSection,
310
316
  workstreamMemoryBlock: ctx.memoryBlock,
311
- workstreamStateSection,
312
317
  learnedSkillsSection,
313
318
  userMessageText: latestUserMessageText,
314
319
  ruleOptions: { includeMemr3Rule: hasRetrievalTools, includeDomainReasoningFallbackRule: hasDomainRoutingSkills },
@@ -395,15 +400,6 @@ async function streamAgentResponse(
395
400
  firstChunkLogged = true
396
401
  }
397
402
  if (streamParams.writer) {
398
- if (
399
- streamParams.suppressFinish &&
400
- typeof value === 'object' &&
401
- value !== null &&
402
- 'type' in value &&
403
- (value as { type: string }).type === 'finish'
404
- ) {
405
- continue
406
- }
407
403
  streamParams.writer.write(value)
408
404
  }
409
405
  }
@@ -497,7 +493,6 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
497
493
  timer.step('persist-approval-message')
498
494
  }
499
495
 
500
- const initialWorkstreamState = parseWorkstreamState(workstreamRecord.state)
501
496
  const persistedCompactionCursor = toOptionalTrimmedString(workstreamRecord.lastCompactedMessageId) ?? undefined
502
497
  const persistedLiveHistoryPromise = workstreamMessageService.listMessagesAfterCursor(
503
498
  workstreamRef,
@@ -630,7 +625,6 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
630
625
  } = assembledContext
631
626
 
632
627
  let memoryBlock = workstreamService.formatMemoryBlockForPrompt(workstreamRecord)
633
- let workstreamState = initialWorkstreamState
634
628
  const executionPlanInstructionSectionCache = createExecutionPlanInstructionSectionCache({
635
629
  disabled: false,
636
630
  loadPlans: async () => {
@@ -638,18 +632,11 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
638
632
  return Promise.all(runs.map((run) => planRunService.toSerializablePlan(run, { slim: true })))
639
633
  },
640
634
  })
641
- const getExecutionPlans = async () => await executionPlanInstructionSectionCache.getPlans()
642
635
  const getExecutionPlanInstructionSections = async (): Promise<string[] | undefined> =>
643
636
  await executionPlanInstructionSectionCache.getSections()
644
637
  const invalidateExecutionPlanInstructionSections = () => {
645
638
  executionPlanInstructionSectionCache.invalidate()
646
639
  }
647
- const getWorkstreamStateSection = async (): Promise<string | undefined> => {
648
- const executionPlans = await getExecutionPlans()
649
- return contextCompactionRuntime.formatWorkstreamStateForPrompt(
650
- stripExecutionPlanFieldsFromWorkstreamState(workstreamState, executionPlans.length > 0),
651
- )
652
- }
653
640
  if (userMessage) {
654
641
  const appliedHumanInput = await executionPlanService.applyHumanInputFromUserMessage({
655
642
  workstreamId: workstreamRef,
@@ -747,10 +734,13 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
747
734
  return {
748
735
  originalMessages,
749
736
  run: async (writer?: UIMessageStreamWriter<ChatMessage>) => {
750
- const executeRun = async (): Promise<PreparedWorkstreamTurnResult | void> => {
737
+ const executeRun = async (leaseAbortSignal?: AbortSignal): Promise<PreparedWorkstreamTurnResult | void> => {
751
738
  const runTimer = lotaDebugLogger.timer('run')
752
739
  const serverRunId = Bun.randomUUIDv7()
753
- const runAbort = createServerRunAbortController(params.abortSignal)
740
+ const runAbortSignals = leaseAbortSignal ? [params.abortSignal, leaseAbortSignal] : [params.abortSignal]
741
+ const runAbort = createServerRunAbortController(
742
+ runAbortSignals.filter((signal): signal is AbortSignal => Boolean(signal)),
743
+ )
754
744
  // Plan turns run without the chat lease — don't claim the active run slot.
755
745
  if (params.kind !== 'planTurn') {
756
746
  await workstreamService.setActiveTurn(workstreamRef, serverRunId, params.streamId ?? null)
@@ -809,7 +799,6 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
809
799
  buildContextResult,
810
800
  getExecutionPlanInstructionSections,
811
801
  getPreSeededMemoriesSection,
812
- getWorkstreamStateSection,
813
802
  getLearnedSkillsSection,
814
803
  promptContext,
815
804
  retrievedKnowledgeSection,
@@ -828,7 +817,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
828
817
  filterTools?: (tools: ToolSet) => ToolSet
829
818
  includeExecutionPlanTools?: boolean
830
819
  metadataPatch?: NonNullable<MessageMetadata>
831
- suppressFinish?: boolean
820
+ headless?: boolean
832
821
  }): Promise<ChatMessage> => {
833
822
  const visibleTimer = lotaDebugLogger.timer(`visible:${runParams.agentId}`)
834
823
  let runMemoryBlock = memoryBlock
@@ -863,8 +852,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
863
852
  skills: runParams.skills,
864
853
  additionalInstructionSections: runParams.additionalInstructionSections,
865
854
  includeExecutionPlanTools,
866
- writer,
867
- suppressFinish: runParams.suppressFinish,
855
+ writer: runParams.headless ? undefined : writer,
868
856
  })
869
857
 
870
858
  visibleTimer.step('stream-agent-response')
@@ -914,6 +902,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
914
902
  const wsMembers = (workstream as { members?: string[] }).members ?? []
915
903
  const members = wsMembers.length > 0 ? wsMembers : [...agentRoster]
916
904
  const fallbackAgentId = coreWorkstreamProfile?.config.agentId ?? defaultLeadAgentId
905
+ writeMultiAgentEvent(writer, { phase: 'routing', note: 'Routing this turn to the right agent.' })
917
906
 
918
907
  const recentContext = currentMessages
919
908
  .slice(-6)
@@ -929,7 +918,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
929
918
 
930
919
  const runGroupAgent = async (
931
920
  agentId: string,
932
- options?: { routingContext?: string; suppressFinish?: boolean },
921
+ options?: { routingContext?: string; headless?: boolean },
933
922
  ) => {
934
923
  const additionalSections = [...(coreInstructionSections ?? []), ...hookInstructionSections]
935
924
  if (options?.routingContext) {
@@ -945,22 +934,23 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
945
934
  mode: 'workstreamMode',
946
935
  skills: coreWorkstreamProfile?.skills ? [...coreWorkstreamProfile.skills] : undefined,
947
936
  additionalInstructionSections: additionalSections,
948
- suppressFinish: options?.suppressFinish,
937
+ headless: options?.headless,
949
938
  })
950
939
  }
951
940
 
952
941
  if (!triageResult) {
953
- // No specialist match — fallback to owner (core) or chief (non-core), single agent turn
942
+ // No specialist match — fallback to owner (core) or chief (non-core), single visible turn.
954
943
  await runGroupAgent(fallbackAgentId)
944
+ writeMultiAgentEvent(writer, { phase: 'complete' })
955
945
  } else {
956
- // Run first routed agent — let finish flow naturally so the stream resets between agents
957
946
  const respondedAgents: string[] = []
958
947
  let lastResponse = await runGroupAgent(triageResult.agentId, {
959
948
  routingContext: triageResult.routingContext,
960
949
  })
961
950
  respondedAgents.push(triageResult.agentId)
962
951
 
963
- // Check if more agents should respond (max 3 total)
952
+ // Follow-up specialists run headless, persist their own messages,
953
+ // and are surfaced by transient events plus cache refresh.
964
954
  while (respondedAgents.length < 3) {
965
955
  const lastResponseText = extractMessageText(lastResponse).slice(0, 500)
966
956
  const checkResult = await checkForNextAgent({
@@ -973,6 +963,13 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
973
963
 
974
964
  if (checkResult.done || !checkResult.agentId) break
975
965
 
966
+ writeMultiAgentEvent(writer, {
967
+ phase: 'waiting-for-agent',
968
+ agentId: checkResult.agentId,
969
+ agentName: agentDisplayNames[checkResult.agentId] ?? checkResult.agentId,
970
+ note: checkResult.routingContext,
971
+ })
972
+
976
973
  // Insert hidden bridge message between agent turns
977
974
  const bridgeMessage: ChatMessage = {
978
975
  id: Bun.randomUUIDv7(),
@@ -993,16 +990,24 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
993
990
 
994
991
  lastResponse = await runGroupAgent(checkResult.agentId, {
995
992
  routingContext: checkResult.routingContext,
993
+ headless: true,
996
994
  })
997
995
  respondedAgents.push(checkResult.agentId)
996
+ writeMultiAgentEvent(writer, {
997
+ phase: 'agent-message-persisted',
998
+ agentId: checkResult.agentId,
999
+ agentName: agentDisplayNames[checkResult.agentId] ?? checkResult.agentId,
1000
+ messageId: lastResponse.id,
1001
+ })
998
1002
  }
1003
+
1004
+ writeMultiAgentEvent(writer, { phase: 'complete' })
999
1005
  }
1000
1006
  }
1001
1007
  }
1002
1008
  } finally {
1003
1009
  try {
1004
1010
  const latestWorkstreamRecord = await workstreamService.getById(workstreamRef)
1005
- const latestPersistedState = parseWorkstreamState(latestWorkstreamRecord.state)
1006
1011
 
1007
1012
  await finalizeTurnRun({
1008
1013
  serverRunId,
@@ -1056,7 +1061,6 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
1056
1061
  visibleWorkstreamAgentId,
1057
1062
  defaultLeadAgentId,
1058
1063
  latestWorkstreamRecord,
1059
- latestPersistedState,
1060
1064
  isUserTurn: params.kind === 'userTurn',
1061
1065
  })
1062
1066
  }
@@ -1072,7 +1076,6 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
1072
1076
  referenceUserMessage,
1073
1077
  assistantMessages: allAssistantMessages,
1074
1078
  latestWorkstreamRecord,
1075
- latestPersistedState,
1076
1079
  context: buildContextResult,
1077
1080
  })
1078
1081
  }
@@ -1093,8 +1096,8 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
1093
1096
  }
1094
1097
 
1095
1098
  try {
1096
- return await workstreamService.withActiveRunLease(workstreamRef, async () => {
1097
- const runResult = await executeRun()
1099
+ return await workstreamService.withActiveRunLease(workstreamRef, async (leaseAbortSignal) => {
1100
+ const runResult = await executeRun(leaseAbortSignal)
1098
1101
  if (runResult) {
1099
1102
  return runResult
1100
1103
  }
@@ -10,30 +10,24 @@ import type { CreateHelperToolLoopAgentOptions } from '../runtime/agent-types'
10
10
  import { resolveHelperAgentOptions } from './helper-agent-options'
11
11
 
12
12
  const CONTEXT_COMPACTION_PROMPT = `<agent-instructions>
13
- You are a **Context Compacter** that produces both:
14
- 1) a dense but shorter summary of prior context
15
- 2) a structured state delta for durable execution continuity
13
+ You are a **Context Compacter** that produces one dense replacement summary of prior context.
16
14
 
17
15
  <task>
18
16
  Compress historical context while preserving execution-critical details.
19
- When provided with existing state, update only changed fields.
20
17
  </task>
21
18
 
22
19
  <rules>
23
20
  - Preserve concrete decisions, constraints, assumptions, plans, open questions, and unresolved risks.
24
21
  - Preserve action ownership, specialist consultations, and the latest working direction.
25
- - Preserve provenance by attaching \`sourceMessageIds\` only from the provided message IDs.
26
22
  - Keep identifiers, numbers, dates, and proper nouns when present.
27
23
  - Remove filler and repetition.
28
24
  - Do not invent details.
29
- - If uncertain, prefer an empty array or null over guessing.
25
+ - If uncertain, omit the detail rather than guessing.
30
26
  </rules>
31
27
 
32
28
  <output-format>
33
- The caller enforces a structured output schema.
34
- Return valid data for:
35
- - summary: concise text summary
36
- - stateDelta: include every field; use empty arrays for unchanged list fields, null for unchanged nullable fields, and \`currentPlan.action\` to signal \`unchanged\`, \`clear\`, or \`set\`
29
+ The caller enforces a structured output schema with exactly one field:
30
+ - summary: concise replacement summary
37
31
  </output-format>
38
32
  </agent-instructions>`
39
33
 
@@ -3,7 +3,13 @@ import { z } from 'zod'
3
3
 
4
4
  import { aiGatewayChatModel } from '../ai-gateway/ai-gateway'
5
5
  import { buildAiGatewayDirectCacheHeaders } from '../ai-gateway/cache-headers'
6
- import { agentDescriptions, agentDisplayNames, routerModelId } from '../config/agent-defaults'
6
+ import {
7
+ agentDescriptions,
8
+ agentDisplayNames,
9
+ agentShortDisplayNames,
10
+ resolveAgentNameAlias,
11
+ routerModelId,
12
+ } from '../config/agent-defaults'
7
13
 
8
14
  // ---------------------------------------------------------------------------
9
15
  // Schemas
@@ -34,6 +40,60 @@ function buildMembersDescription(members: readonly string[]): string {
34
40
  .join('\n')
35
41
  }
36
42
 
43
+ function escapeRegex(value: string): string {
44
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
45
+ }
46
+
47
+ function buildExplicitAgentRoutingContext(agentId: string): string {
48
+ const displayName = agentDisplayNames[agentId] ?? agentId
49
+ return `Respond directly to the part of the user's request explicitly addressed to ${displayName}.`
50
+ }
51
+
52
+ function extractExplicitAgentTargets(messageText: string, members: readonly string[]): string[] {
53
+ const normalizedMessage = messageText.trim()
54
+ if (!normalizedMessage) return []
55
+
56
+ const memberSet = new Set(members)
57
+ const aliases = new Map<string, string>()
58
+
59
+ for (const member of members) {
60
+ for (const rawAlias of [member, agentDisplayNames[member], agentShortDisplayNames[member]]) {
61
+ if (typeof rawAlias !== 'string') continue
62
+ const alias = rawAlias.trim().toLowerCase()
63
+ if (!alias) continue
64
+ aliases.set(alias, member)
65
+ }
66
+ }
67
+
68
+ if (aliases.size === 0) return []
69
+
70
+ const orderedAliases = [...aliases.keys()].sort((left, right) => right.length - left.length)
71
+ const matches: Array<{ agentId: string; index: number }> = []
72
+ const directAddressRegex = new RegExp(
73
+ `(^|[^\\w@])(?<alias>${orderedAliases.map(escapeRegex).join('|')})(?=\\s*(?::|\\-|—))`,
74
+ 'gi',
75
+ )
76
+
77
+ for (const match of normalizedMessage.matchAll(directAddressRegex)) {
78
+ const alias = match.groups?.alias
79
+ const agentId = alias ? resolveAgentNameAlias(alias) : undefined
80
+ if (!agentId || !memberSet.has(agentId)) continue
81
+
82
+ const prefix = match[1]
83
+ const index = match.index + prefix.length
84
+ matches.push({ agentId, index })
85
+ }
86
+
87
+ const seenAgents = new Set<string>()
88
+ return matches
89
+ .sort((left, right) => left.index - right.index)
90
+ .flatMap(({ agentId }) => {
91
+ if (seenAgents.has(agentId)) return []
92
+ seenAgents.add(agentId)
93
+ return [agentId]
94
+ })
95
+ }
96
+
37
97
  function extractJson(text: string): unknown {
38
98
  const match = text.match(/\{[\s\S]*\}/)
39
99
  if (!match) return null
@@ -53,7 +113,13 @@ function extractResultText(result: { text?: string; reasoning?: unknown }): stri
53
113
  if (typeof reasoning === 'string') return reasoning
54
114
  if (Array.isArray(reasoning)) {
55
115
  return reasoning
56
- .map((r) => (typeof r === 'string' ? r : typeof r === 'object' && r && 'text' in r ? String(r.text) : ''))
116
+ .map((r) => {
117
+ if (typeof r === 'string') return r
118
+ if (typeof r !== 'object' || r === null || !('text' in r)) return ''
119
+
120
+ const text = (r as { text?: unknown }).text
121
+ return typeof text === 'string' ? text : ''
122
+ })
57
123
  .join('')
58
124
  }
59
125
  return ''
@@ -112,6 +178,12 @@ export async function triageWorkstreamMessage(params: {
112
178
  messageText: string
113
179
  recentContext?: string
114
180
  }): Promise<RouterTriageResult | null> {
181
+ const explicitTargets = extractExplicitAgentTargets(params.messageText, params.members)
182
+ const firstExplicitTarget = explicitTargets[0]
183
+ if (firstExplicitTarget) {
184
+ return { agentId: firstExplicitTarget, routingContext: buildExplicitAgentRoutingContext(firstExplicitTarget) }
185
+ }
186
+
115
187
  const membersDesc = buildMembersDescription(params.members)
116
188
  const prompt = [
117
189
  `Workstream: "${params.workstreamTitle}"`,
@@ -159,6 +231,16 @@ export async function checkForNextAgent(params: {
159
231
  respondedAgents: string[]
160
232
  lastResponseSummary: string
161
233
  }): Promise<RouterCheckResult> {
234
+ const explicitTargets = extractExplicitAgentTargets(params.messageText, params.members)
235
+ const nextExplicitTarget = explicitTargets.find((agentId) => !params.respondedAgents.includes(agentId))
236
+ if (nextExplicitTarget) {
237
+ return {
238
+ done: false,
239
+ agentId: nextExplicitTarget,
240
+ routingContext: buildExplicitAgentRoutingContext(nextExplicitTarget),
241
+ }
242
+ }
243
+
162
244
  const remainingMembers = params.members.filter((id) => !params.respondedAgents.includes(id))
163
245
  if (remainingMembers.length === 0) return { done: true }
164
246