@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.
- package/infrastructure/schema/00_workstream.surql +0 -1
- package/package.json +2 -2
- package/src/runtime/agent-runtime-policy.ts +0 -2
- package/src/runtime/agent-stream-helpers.ts +26 -9
- package/src/runtime/context-compaction-runtime.ts +2 -3
- package/src/runtime/context-compaction.ts +48 -590
- package/src/runtime/execution-plan.ts +7 -1
- package/src/runtime/index.ts +0 -12
- package/src/runtime/post-turn-side-effects.ts +0 -3
- package/src/runtime/runtime-extensions.ts +0 -1
- package/src/runtime/workstream-chat-helpers.ts +1 -8
- package/src/services/context-compaction.service.ts +5 -21
- package/src/services/workstream-turn-preparation.service.ts +54 -51
- package/src/system-agents/context-compaction.agent.ts +4 -10
- package/src/system-agents/workstream-router.agent.ts +84 -2
- package/src/runtime/workstream-state.ts +0 -274
|
@@ -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
|
}
|
package/src/runtime/index.ts
CHANGED
|
@@ -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() : ''
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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+
|
|
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
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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,
|
|
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
|
-
|
|
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 {
|
|
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) =>
|
|
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
|
|