@lota-sdk/core 0.1.8 → 0.1.9
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 +1 -1
- package/package.json +1 -1
- package/src/runtime/turn-lifecycle.ts +2 -2
- package/src/runtime/workstream-chat-helpers.ts +5 -5
- package/src/services/context-compaction.service.ts +6 -2
- package/src/services/workstream-turn-preparation.ts +16 -184
- package/src/services/workstream.service.ts +1 -185
- package/src/services/workstream.types.ts +1 -65
- package/src/services/workstream-change-tracker.service.ts +0 -313
- package/src/system-agents/workstream-tracker.agent.ts +0 -58
|
@@ -13,7 +13,7 @@ DEFINE FIELD IF NOT EXISTS updatedAt ON TABLE workstream TYPE datetime VALUE tim
|
|
|
13
13
|
DEFINE FIELD IF NOT EXISTS memoryBlock ON TABLE workstream TYPE option<string>;
|
|
14
14
|
DEFINE FIELD IF NOT EXISTS memoryBlockSummary ON TABLE workstream TYPE option<string>;
|
|
15
15
|
DEFINE FIELD IF NOT EXISTS activeRunId ON TABLE workstream TYPE option<string>;
|
|
16
|
-
DEFINE FIELD IF NOT EXISTS
|
|
16
|
+
DEFINE FIELD IF NOT EXISTS compactionSummary ON TABLE workstream TYPE option<string>;
|
|
17
17
|
DEFINE FIELD IF NOT EXISTS lastCompactedMessageId ON TABLE workstream TYPE option<string>;
|
|
18
18
|
DEFINE FIELD IF NOT EXISTS nameGenerated ON TABLE workstream TYPE bool DEFAULT false;
|
|
19
19
|
DEFINE FIELD IF NOT EXISTS isCompacting ON TABLE workstream TYPE bool DEFAULT false;
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@ import type { ChatMessage } from '@lota-sdk/shared/schemas/chat-message'
|
|
|
2
2
|
|
|
3
3
|
export async function finalizeTurnRun(params: {
|
|
4
4
|
serverRunId: string
|
|
5
|
-
getEntity: () => Promise<{ lastCompactedMessageId?: string | null;
|
|
5
|
+
getEntity: () => Promise<{ lastCompactedMessageId?: string | null; compactionSummary?: string | null }>
|
|
6
6
|
getUncompactedMessages: (cursor?: string) => Promise<ChatMessage[]>
|
|
7
7
|
assessCompaction: (summaryText: string, messages: ChatMessage[]) => { shouldCompact: boolean }
|
|
8
8
|
enqueueCompaction: () => Promise<void>
|
|
@@ -14,7 +14,7 @@ export async function finalizeTurnRun(params: {
|
|
|
14
14
|
const entity = await params.getEntity()
|
|
15
15
|
const cursor = typeof entity.lastCompactedMessageId === 'string' ? entity.lastCompactedMessageId : undefined
|
|
16
16
|
const uncompactedMessages = await params.getUncompactedMessages(cursor)
|
|
17
|
-
const summaryText = typeof entity.
|
|
17
|
+
const summaryText = typeof entity.compactionSummary === 'string' ? entity.compactionSummary : ''
|
|
18
18
|
const { shouldCompact } = params.assessCompaction(summaryText, uncompactedMessages)
|
|
19
19
|
|
|
20
20
|
if (shouldCompact) {
|
|
@@ -121,14 +121,14 @@ export function buildAgentHistoryMessages(messages: ChatMessageLike[]): Array<{
|
|
|
121
121
|
.map((message) => ({ content: message.content, ...(message.agentName ? { agentName: message.agentName } : {}) }))
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
export function
|
|
124
|
+
export function appendPersistedWorkstreamContextToHistoryMessages(
|
|
125
125
|
historyMessages: WorkstreamHistoryMessage[],
|
|
126
|
-
params: {
|
|
126
|
+
params: { compactionSummary?: string | null; persistedState?: unknown },
|
|
127
127
|
): WorkstreamHistoryMessage[] {
|
|
128
128
|
const nextHistoryMessages = [...historyMessages]
|
|
129
|
-
const
|
|
130
|
-
if (
|
|
131
|
-
nextHistoryMessages.push({ role: 'agent', content: `Compacted chat summary:\n${
|
|
129
|
+
const compactionSummary = typeof params.compactionSummary === 'string' ? params.compactionSummary.trim() : ''
|
|
130
|
+
if (compactionSummary) {
|
|
131
|
+
nextHistoryMessages.push({ role: 'agent', content: `Compacted chat summary:\n${compactionSummary}` })
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
if (params.persistedState !== undefined && params.persistedState !== null) {
|
|
@@ -61,7 +61,7 @@ class ContextCompactionService {
|
|
|
61
61
|
)
|
|
62
62
|
|
|
63
63
|
const result = await contextCompactionRuntime.compactHistory({
|
|
64
|
-
summaryText: typeof workstream.
|
|
64
|
+
summaryText: typeof workstream.compactionSummary === 'string' ? workstream.compactionSummary : '',
|
|
65
65
|
liveMessages,
|
|
66
66
|
tailMessageCount: WORKSTREAM_RAW_TAIL_MESSAGES,
|
|
67
67
|
contextSize: params.contextSize,
|
|
@@ -82,7 +82,11 @@ class ContextCompactionService {
|
|
|
82
82
|
await databaseService.update(
|
|
83
83
|
TABLES.WORKSTREAM,
|
|
84
84
|
params.workstreamId,
|
|
85
|
-
{
|
|
85
|
+
{
|
|
86
|
+
compactionSummary: result.summaryText,
|
|
87
|
+
lastCompactedMessageId: result.lastCompactedMessageId,
|
|
88
|
+
state: result.state,
|
|
89
|
+
},
|
|
86
90
|
WorkstreamSchema,
|
|
87
91
|
)
|
|
88
92
|
|
|
@@ -40,7 +40,7 @@ import { hasApprovalRespondedParts } from '../runtime/approval-continuation'
|
|
|
40
40
|
import { buildModelInputMessagesWithUploadMetadata, buildReadableUploadMetadataText } from '../runtime/chat-attachments'
|
|
41
41
|
import { hasMessageContent } from '../runtime/chat-message'
|
|
42
42
|
import { waitForCompactionIfNeeded } from '../runtime/chat-run-orchestration'
|
|
43
|
-
import {
|
|
43
|
+
import { parseWorkstreamState } from '../runtime/context-compaction'
|
|
44
44
|
import { CONTEXT_SIZE } from '../runtime/context-compaction-constants'
|
|
45
45
|
import { createExecutionPlanInstructionSectionCache } from '../runtime/execution-plan'
|
|
46
46
|
import { mergeInstructionSections } from '../runtime/instruction-sections'
|
|
@@ -54,13 +54,12 @@ import { getRuntimeAdapters, getToolProviders, getTurnHooks } from '../runtime/r
|
|
|
54
54
|
import { shouldEnqueueSkillExtraction } from '../runtime/skill-extraction-policy'
|
|
55
55
|
import { finalizeTurnRun } from '../runtime/turn-lifecycle'
|
|
56
56
|
import {
|
|
57
|
-
|
|
57
|
+
appendPersistedWorkstreamContextToHistoryMessages,
|
|
58
58
|
buildAgentHistoryMessages,
|
|
59
59
|
buildConversationSummary,
|
|
60
60
|
buildReadableUploadMetadataContext,
|
|
61
61
|
collectToolOutputErrors,
|
|
62
62
|
extractMessageText,
|
|
63
|
-
extractTrackerMessageText,
|
|
64
63
|
toHistoryMessages,
|
|
65
64
|
toOptionalTrimmedString,
|
|
66
65
|
} from '../runtime/workstream-chat-helpers'
|
|
@@ -69,8 +68,7 @@ import {
|
|
|
69
68
|
classifyPolicyClasses,
|
|
70
69
|
resolveReasoningProfile,
|
|
71
70
|
} from '../runtime/workstream-routing-policy'
|
|
72
|
-
import {
|
|
73
|
-
import type { WorkstreamState, WorkstreamStateDelta } from '../runtime/workstream-state'
|
|
71
|
+
import type { WorkstreamState } from '../runtime/workstream-state'
|
|
74
72
|
import { chatRunRegistry } from '../services/chat-run-registry.service'
|
|
75
73
|
import type { NormalizedWorkstream, WorkstreamRecord } from '../services/workstream.types'
|
|
76
74
|
import { createTeamThinkTool } from '../tools/team-think.tool'
|
|
@@ -84,7 +82,6 @@ import { executionPlanService } from './execution-plan.service'
|
|
|
84
82
|
import { learnedSkillService } from './learned-skill.service'
|
|
85
83
|
import { memoryService } from './memory.service'
|
|
86
84
|
import { recentActivityService } from './recent-activity.service'
|
|
87
|
-
import { updateWorkstreamChangeTracker } from './workstream-change-tracker.service'
|
|
88
85
|
import { workstreamMessageService } from './workstream-message.service'
|
|
89
86
|
import { workstreamService } from './workstream.service'
|
|
90
87
|
|
|
@@ -143,60 +140,6 @@ export function parsePersistedWorkstreamState(value: unknown): WorkstreamState |
|
|
|
143
140
|
return parseWorkstreamState(value)
|
|
144
141
|
}
|
|
145
142
|
|
|
146
|
-
export function extractFirstAbsoluteUrl(text: string): URL | null {
|
|
147
|
-
const match = text.match(/https?:\/\/[^\s)]+/i)
|
|
148
|
-
if (!match) return null
|
|
149
|
-
|
|
150
|
-
const normalized = match[0].replace(/[.,;!?]+$/g, '')
|
|
151
|
-
try {
|
|
152
|
-
return new URL(normalized)
|
|
153
|
-
} catch {
|
|
154
|
-
return null
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export function taskRequestsWebsiteRefresh(task: string, forceRefresh: boolean): boolean {
|
|
159
|
-
return forceRefresh || /\b(refresh|re-run|rerun|run again|extract again|re-extract|overwrite|recrawl)\b/i.test(task)
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export function buildInspectWebsiteTrackerTitle(input: unknown): string {
|
|
163
|
-
if (!input || typeof input !== 'object') return 'Inspect website intelligence'
|
|
164
|
-
|
|
165
|
-
const task = typeof (input as { task?: unknown }).task === 'string' ? (input as { task: string }).task.trim() : ''
|
|
166
|
-
const forceRefresh = (input as { forceRefresh?: unknown }).forceRefresh === true
|
|
167
|
-
const hostname = extractFirstAbsoluteUrl(task)?.hostname.replace(/^www\./, '') ?? null
|
|
168
|
-
|
|
169
|
-
if (hostname && taskRequestsWebsiteRefresh(task, forceRefresh)) {
|
|
170
|
-
return `Overwrite website-intelligence artifacts for ${hostname}`
|
|
171
|
-
}
|
|
172
|
-
if (hostname) {
|
|
173
|
-
return `Inspect website intelligence for ${hostname}`
|
|
174
|
-
}
|
|
175
|
-
return taskRequestsWebsiteRefresh(task, forceRefresh)
|
|
176
|
-
? 'Overwrite website-intelligence artifacts'
|
|
177
|
-
: 'Inspect website intelligence'
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
export function emitTransientWorkstreamTrackerState(params: {
|
|
181
|
-
writer?: UIMessageStreamWriter<ChatMessage>
|
|
182
|
-
workstreamRecord: WorkstreamRecord
|
|
183
|
-
existingState: WorkstreamState
|
|
184
|
-
delta: WorkstreamStateDelta
|
|
185
|
-
}): WorkstreamState {
|
|
186
|
-
const nextState = mergeStateDelta(params.existingState, params.delta, () => Date.now())
|
|
187
|
-
|
|
188
|
-
if (params.writer) {
|
|
189
|
-
params.writer.write({
|
|
190
|
-
type: 'data-workstreamTracker',
|
|
191
|
-
data: workstreamService.toPublicWorkstreamDetail({ ...params.workstreamRecord, state: nextState })
|
|
192
|
-
.workstreamState as unknown as Record<string, unknown>,
|
|
193
|
-
transient: true,
|
|
194
|
-
})
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return nextState
|
|
198
|
-
}
|
|
199
|
-
|
|
200
143
|
export async function waitForWorkstreamCompactionIfNeeded(workstreamId: RecordIdRef): Promise<WorkstreamRecord> {
|
|
201
144
|
return await waitForCompactionIfNeeded({
|
|
202
145
|
entityId: recordIdToString(workstreamId, TABLES.WORKSTREAM),
|
|
@@ -395,9 +338,10 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
395
338
|
|
|
396
339
|
const workspacePromise = workspaceProvider ? workspaceProvider.getWorkspace(orgRef) : Promise.resolve({})
|
|
397
340
|
const initialWorkstreamState = parsePersistedWorkstreamState(workstreamRecord.state)
|
|
341
|
+
const persistedCompactionCursor = toOptionalTrimmedString(workstreamRecord.lastCompactedMessageId) ?? undefined
|
|
398
342
|
const persistedLiveHistoryPromise = workstreamMessageService.listMessagesAfterCursor(
|
|
399
343
|
workstreamRef,
|
|
400
|
-
|
|
344
|
+
persistedCompactionCursor,
|
|
401
345
|
)
|
|
402
346
|
const persistedRecentHistoryPromise = workstreamMessageService.listRecentMessages(workstreamRef, 64)
|
|
403
347
|
|
|
@@ -630,9 +574,12 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
630
574
|
return section
|
|
631
575
|
}
|
|
632
576
|
|
|
633
|
-
const
|
|
577
|
+
const persistedCompactionSummary =
|
|
578
|
+
persistedCompactionCursor && typeof workstreamRecord.compactionSummary === 'string'
|
|
579
|
+
? workstreamRecord.compactionSummary
|
|
580
|
+
: ''
|
|
634
581
|
const messagesForContext = userMessage ? upsertChatHistoryMessage(liveHistory, userMessage) : liveHistory
|
|
635
|
-
let currentMessages = contextCompactionRuntime.prependSummaryMessage(
|
|
582
|
+
let currentMessages = contextCompactionRuntime.prependSummaryMessage(persistedCompactionSummary, messagesForContext)
|
|
636
583
|
const referenceUserMessageId = referenceUserMessage?.id ?? ''
|
|
637
584
|
const listReadableUploads = (extraMessages: ChatMessage[] = []) =>
|
|
638
585
|
listReadableUploadsFromChatMessages({
|
|
@@ -811,8 +758,6 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
811
758
|
},
|
|
812
759
|
}) as ReadableStream<ChatStreamChunk>
|
|
813
760
|
const reader = uiStream.getReader()
|
|
814
|
-
let liveTrackerState: WorkstreamState = workstreamState ?? parseWorkstreamState(null)
|
|
815
|
-
const liveTrackedToolCalls = new Map<string, string>()
|
|
816
761
|
try {
|
|
817
762
|
for (;;) {
|
|
818
763
|
const { done, value } = await reader.read()
|
|
@@ -820,71 +765,6 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
820
765
|
if (streamParams.writer) {
|
|
821
766
|
streamParams.writer.write(value)
|
|
822
767
|
}
|
|
823
|
-
if (value.type === 'tool-input-available' && value.toolName === 'inspectWebsite') {
|
|
824
|
-
const title = buildInspectWebsiteTrackerTitle(value.input)
|
|
825
|
-
liveTrackedToolCalls.set(value.toolCallId, title)
|
|
826
|
-
liveTrackerState = emitTransientWorkstreamTrackerState({
|
|
827
|
-
writer: streamParams.writer,
|
|
828
|
-
workstreamRecord,
|
|
829
|
-
existingState: liveTrackerState,
|
|
830
|
-
delta: {
|
|
831
|
-
taskUpdates: [
|
|
832
|
-
{
|
|
833
|
-
title,
|
|
834
|
-
status: 'in-progress',
|
|
835
|
-
owner: streamParams.agentId,
|
|
836
|
-
externalId: value.toolCallId,
|
|
837
|
-
sourceMessageIds: [],
|
|
838
|
-
},
|
|
839
|
-
],
|
|
840
|
-
},
|
|
841
|
-
})
|
|
842
|
-
}
|
|
843
|
-
if (
|
|
844
|
-
value.type === 'tool-output-available' &&
|
|
845
|
-
value.preliminary !== true &&
|
|
846
|
-
liveTrackedToolCalls.has(value.toolCallId)
|
|
847
|
-
) {
|
|
848
|
-
liveTrackerState = emitTransientWorkstreamTrackerState({
|
|
849
|
-
writer: streamParams.writer,
|
|
850
|
-
workstreamRecord,
|
|
851
|
-
existingState: liveTrackerState,
|
|
852
|
-
delta: {
|
|
853
|
-
taskUpdates: [
|
|
854
|
-
{
|
|
855
|
-
title: liveTrackedToolCalls.get(value.toolCallId) ?? 'Inspect website intelligence',
|
|
856
|
-
status: 'done',
|
|
857
|
-
owner: streamParams.agentId,
|
|
858
|
-
externalId: value.toolCallId,
|
|
859
|
-
sourceMessageIds: [],
|
|
860
|
-
},
|
|
861
|
-
],
|
|
862
|
-
},
|
|
863
|
-
})
|
|
864
|
-
liveTrackedToolCalls.delete(value.toolCallId)
|
|
865
|
-
}
|
|
866
|
-
if (
|
|
867
|
-
(value.type === 'tool-output-error' || value.type === 'tool-output-denied') &&
|
|
868
|
-
liveTrackedToolCalls.has(value.toolCallId)
|
|
869
|
-
) {
|
|
870
|
-
liveTrackerState = emitTransientWorkstreamTrackerState({
|
|
871
|
-
writer: streamParams.writer,
|
|
872
|
-
workstreamRecord,
|
|
873
|
-
existingState: liveTrackerState,
|
|
874
|
-
delta: {
|
|
875
|
-
taskUpdates: [
|
|
876
|
-
{
|
|
877
|
-
title: liveTrackedToolCalls.get(value.toolCallId) ?? 'Inspect website intelligence',
|
|
878
|
-
status: 'blocked',
|
|
879
|
-
owner: streamParams.agentId,
|
|
880
|
-
externalId: value.toolCallId,
|
|
881
|
-
sourceMessageIds: [],
|
|
882
|
-
},
|
|
883
|
-
],
|
|
884
|
-
},
|
|
885
|
-
})
|
|
886
|
-
liveTrackedToolCalls.delete(value.toolCallId)
|
|
887
|
-
}
|
|
888
768
|
}
|
|
889
769
|
} finally {
|
|
890
770
|
reader.releaseLock()
|
|
@@ -1141,9 +1021,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
1141
1021
|
} finally {
|
|
1142
1022
|
try {
|
|
1143
1023
|
const latestWorkstreamRecord = await workstreamService.getById(workstreamRef)
|
|
1144
|
-
const latestPersistedState =
|
|
1145
|
-
? WorkstreamStateSchema.parse(latestWorkstreamRecord.state)
|
|
1146
|
-
: null
|
|
1024
|
+
const latestPersistedState = parsePersistedWorkstreamState(latestWorkstreamRecord.state)
|
|
1147
1025
|
|
|
1148
1026
|
await finalizeTurnRun({
|
|
1149
1027
|
serverRunId,
|
|
@@ -1167,58 +1045,12 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
1167
1045
|
disposeAbort: () => runAbort.dispose(),
|
|
1168
1046
|
})
|
|
1169
1047
|
|
|
1170
|
-
let trackerAwareWorkstreamRecord = latestWorkstreamRecord
|
|
1171
|
-
let trackerAwarePersistedState = latestPersistedState
|
|
1172
|
-
|
|
1173
|
-
if (!onboardingActive && allAssistantMessages.length > 0) {
|
|
1174
|
-
const activeExecutionPlan = await executionPlanService.getActivePlanForWorkstream(workstreamRef)
|
|
1175
|
-
const trackerUpdated = await updateWorkstreamChangeTracker({
|
|
1176
|
-
workstreamId: workstreamRef,
|
|
1177
|
-
title: latestWorkstreamRecord.title ?? workstream.title,
|
|
1178
|
-
mode: workstream.mode,
|
|
1179
|
-
...(workstream.coreType ? { coreType: workstream.coreType } : {}),
|
|
1180
|
-
...(visibleWorkstreamAgentId ? { visibleAgentId: visibleWorkstreamAgentId } : {}),
|
|
1181
|
-
hasActiveExecutionPlan: activeExecutionPlan !== null,
|
|
1182
|
-
previousSummary:
|
|
1183
|
-
typeof latestWorkstreamRecord.chatSummary === 'string' ? latestWorkstreamRecord.chatSummary : null,
|
|
1184
|
-
existingState: latestPersistedState,
|
|
1185
|
-
userMessageText: referenceUserMessage ? extractMessageText(referenceUserMessage).trim() : null,
|
|
1186
|
-
assistantMessages: allAssistantMessages
|
|
1187
|
-
.map((message) => {
|
|
1188
|
-
const text = extractTrackerMessageText(message).trim()
|
|
1189
|
-
if (!text) return null
|
|
1190
|
-
const label =
|
|
1191
|
-
typeof message.metadata?.agentName === 'string' && message.metadata.agentName.trim().length > 0
|
|
1192
|
-
? message.metadata.agentName.trim()
|
|
1193
|
-
: typeof message.metadata?.agentId === 'string' && message.metadata.agentId.trim().length > 0
|
|
1194
|
-
? message.metadata.agentId.trim()
|
|
1195
|
-
: (visibleWorkstreamAgentId ?? 'Assistant')
|
|
1196
|
-
return { label, text }
|
|
1197
|
-
})
|
|
1198
|
-
.filter((message): message is { label: string; text: string } => Boolean(message)),
|
|
1199
|
-
})
|
|
1200
|
-
|
|
1201
|
-
if (trackerUpdated) {
|
|
1202
|
-
const trackedWorkstreamRecord = await workstreamService.getWorkstreamRecord(workstreamRef)
|
|
1203
|
-
trackerAwareWorkstreamRecord = trackedWorkstreamRecord
|
|
1204
|
-
trackerAwarePersistedState = parsePersistedWorkstreamState(trackedWorkstreamRecord.state)
|
|
1205
|
-
if (writer) {
|
|
1206
|
-
writer.write({
|
|
1207
|
-
type: 'data-workstreamTracker',
|
|
1208
|
-
data: workstreamService.toPublicWorkstreamDetail(trackedWorkstreamRecord)
|
|
1209
|
-
.workstreamState as unknown as Record<string, unknown>,
|
|
1210
|
-
transient: true,
|
|
1211
|
-
})
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
1048
|
if (allAssistantMessages.length > 0 && shouldProcessPostRunSideEffects) {
|
|
1217
1049
|
const turnCount = await workstreamService.incrementTurnCount(workstreamRef)
|
|
1218
1050
|
const agentMessages = buildAgentHistoryMessages(allAssistantMessages)
|
|
1219
|
-
const historyMessagesForMemory =
|
|
1051
|
+
const historyMessagesForMemory = appendPersistedWorkstreamContextToHistoryMessages(
|
|
1220
1052
|
toHistoryMessages(recentHistory),
|
|
1221
|
-
{
|
|
1053
|
+
{ compactionSummary: latestWorkstreamRecord.compactionSummary, persistedState: latestPersistedState },
|
|
1222
1054
|
)
|
|
1223
1055
|
|
|
1224
1056
|
const userMessageText = referenceUserMessage ? extractMessageText(referenceUserMessage).trim() : ''
|
|
@@ -1283,7 +1115,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
1283
1115
|
agentId: visibleWorkstreamAgentId ?? 'chief',
|
|
1284
1116
|
agentName: agentDisplayNames[visibleWorkstreamAgentId ?? 'chief'],
|
|
1285
1117
|
workstreamId: workstreamIdString,
|
|
1286
|
-
workstreamTitle:
|
|
1118
|
+
workstreamTitle: latestWorkstreamRecord.title ?? workstream.title,
|
|
1287
1119
|
workstreamMode: workstream.mode,
|
|
1288
1120
|
...(workstream.coreType ? { coreType: workstream.coreType } : {}),
|
|
1289
1121
|
userMessageText,
|
|
@@ -1350,8 +1182,8 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
1350
1182
|
onboardingActive,
|
|
1351
1183
|
referenceUserMessage,
|
|
1352
1184
|
assistantMessages: allAssistantMessages,
|
|
1353
|
-
latestWorkstreamRecord
|
|
1354
|
-
latestPersistedState
|
|
1185
|
+
latestWorkstreamRecord,
|
|
1186
|
+
latestPersistedState,
|
|
1355
1187
|
context: buildContextResult,
|
|
1356
1188
|
})
|
|
1357
1189
|
}
|
|
@@ -20,23 +20,12 @@ import {
|
|
|
20
20
|
parseMemoryBlock,
|
|
21
21
|
serializeMemoryBlock,
|
|
22
22
|
} from '../runtime/memory-block'
|
|
23
|
-
import { toOptionalTrimmedString } from '../runtime/workstream-chat-helpers'
|
|
24
|
-
import { WorkstreamStateSchema } from '../runtime/workstream-state'
|
|
25
|
-
import type { WorkstreamState } from '../runtime/workstream-state'
|
|
26
23
|
import { toIsoDateTimeString } from '../utils/date-time'
|
|
27
24
|
import { chatRunRegistry } from './chat-run-registry.service'
|
|
28
25
|
import { contextCompactionService } from './context-compaction.service'
|
|
29
26
|
import { workstreamMessageService } from './workstream-message.service'
|
|
30
27
|
import { WorkstreamSchema, WorkstreamStatusSchema } from './workstream.types'
|
|
31
|
-
import type {
|
|
32
|
-
NormalizedWorkstream,
|
|
33
|
-
PublicWorkstreamApprovalState,
|
|
34
|
-
PublicWorkstreamDetail,
|
|
35
|
-
PublicWorkstreamStateFocus,
|
|
36
|
-
PublicWorkstreamStatePayload,
|
|
37
|
-
PublicWorkstreamStateProgress,
|
|
38
|
-
WorkstreamRecord,
|
|
39
|
-
} from './workstream.types'
|
|
28
|
+
import type { NormalizedWorkstream, WorkstreamRecord } from './workstream.types'
|
|
40
29
|
|
|
41
30
|
// Uses SurrealQL directly to keep pagination/order logic close to queries.
|
|
42
31
|
|
|
@@ -146,155 +135,6 @@ function buildCoreWorkstreamId({
|
|
|
146
135
|
return new RecordId(TABLES.WORKSTREAM, `core_${typeValue}_user_${userValue}_organization_${orgValue}`)
|
|
147
136
|
}
|
|
148
137
|
|
|
149
|
-
function toOptionalIsoDateTimeString(value: number | null | undefined): string | null {
|
|
150
|
-
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return null
|
|
151
|
-
return toIsoDateTimeString(new Date(value))
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function getCompactedSummaryFocus(chatSummary: string | null): string | null {
|
|
155
|
-
if (!chatSummary) return null
|
|
156
|
-
|
|
157
|
-
const lines = chatSummary
|
|
158
|
-
.split('\n')
|
|
159
|
-
.map((line) => line.trim())
|
|
160
|
-
.filter(Boolean)
|
|
161
|
-
|
|
162
|
-
for (const line of lines) {
|
|
163
|
-
if (line.endsWith(':')) continue
|
|
164
|
-
if (line.startsWith('- ')) {
|
|
165
|
-
return toOptionalTrimmedString(line.slice(2))
|
|
166
|
-
}
|
|
167
|
-
return toOptionalTrimmedString(line)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return null
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function parsePersistedWorkstreamState(value: unknown): WorkstreamState | null {
|
|
174
|
-
const parsed = WorkstreamStateSchema.safeParse(value)
|
|
175
|
-
return parsed.success ? parsed.data : null
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function buildEmptyWorkstreamStateProgress(): PublicWorkstreamStateProgress {
|
|
179
|
-
return {
|
|
180
|
-
hasState: false,
|
|
181
|
-
lastUpdated: null,
|
|
182
|
-
completionRatio: null,
|
|
183
|
-
tasks: { total: 0, open: 0, inProgress: 0, done: 0, blocked: 0 },
|
|
184
|
-
constraints: { total: 0, approved: 0, candidate: 0 },
|
|
185
|
-
keyDecisions: 0,
|
|
186
|
-
openQuestions: 0,
|
|
187
|
-
risks: 0,
|
|
188
|
-
artifacts: 0,
|
|
189
|
-
agentContributions: 0,
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function buildWorkstreamStateProgress(state: WorkstreamState | null): PublicWorkstreamStateProgress {
|
|
194
|
-
if (!state) {
|
|
195
|
-
return buildEmptyWorkstreamStateProgress()
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const tasks = {
|
|
199
|
-
total: state.tasks.length,
|
|
200
|
-
open: state.tasks.filter((task) => task.status === 'open').length,
|
|
201
|
-
inProgress: state.tasks.filter((task) => task.status === 'in-progress').length,
|
|
202
|
-
done: state.tasks.filter((task) => task.status === 'done').length,
|
|
203
|
-
blocked: state.tasks.filter((task) => task.status === 'blocked').length,
|
|
204
|
-
}
|
|
205
|
-
const constraintsApproved = state.activeConstraints.filter((constraint) => constraint.approved).length
|
|
206
|
-
|
|
207
|
-
return {
|
|
208
|
-
hasState:
|
|
209
|
-
state.currentPlan !== null ||
|
|
210
|
-
state.activeConstraints.length > 0 ||
|
|
211
|
-
state.keyDecisions.length > 0 ||
|
|
212
|
-
state.tasks.length > 0 ||
|
|
213
|
-
state.openQuestions.length > 0 ||
|
|
214
|
-
state.risks.length > 0 ||
|
|
215
|
-
state.artifacts.length > 0 ||
|
|
216
|
-
state.agentContributions.length > 0 ||
|
|
217
|
-
toOptionalTrimmedString(state.approvedBy) !== null ||
|
|
218
|
-
typeof state.approvedAt === 'number' ||
|
|
219
|
-
toOptionalTrimmedString(state.approvalMessageId) !== null ||
|
|
220
|
-
toOptionalTrimmedString(state.approvalNote) !== null,
|
|
221
|
-
lastUpdated: toOptionalIsoDateTimeString(state.lastUpdated),
|
|
222
|
-
completionRatio: tasks.total > 0 ? Number((tasks.done / tasks.total).toFixed(4)) : null,
|
|
223
|
-
tasks,
|
|
224
|
-
constraints: {
|
|
225
|
-
total: state.activeConstraints.length,
|
|
226
|
-
approved: constraintsApproved,
|
|
227
|
-
candidate: state.activeConstraints.length - constraintsApproved,
|
|
228
|
-
},
|
|
229
|
-
keyDecisions: state.keyDecisions.length,
|
|
230
|
-
openQuestions: state.openQuestions.length,
|
|
231
|
-
risks: state.risks.length,
|
|
232
|
-
artifacts: state.artifacts.length,
|
|
233
|
-
agentContributions: state.agentContributions.length,
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function buildWorkstreamApprovalState(state: WorkstreamState | null): PublicWorkstreamApprovalState | null {
|
|
238
|
-
if (!state) return null
|
|
239
|
-
|
|
240
|
-
const approvedBy = toOptionalTrimmedString(state.approvedBy)
|
|
241
|
-
const approvedAt = toOptionalIsoDateTimeString(state.approvedAt)
|
|
242
|
-
const approvalMessageId = toOptionalTrimmedString(state.approvalMessageId)
|
|
243
|
-
const approvalNote = toOptionalTrimmedString(state.approvalNote)
|
|
244
|
-
|
|
245
|
-
if (!approvedBy && !approvedAt && !approvalMessageId && !approvalNote) {
|
|
246
|
-
return null
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
return { approvedBy, approvedAt, approvalMessageId, approvalNote }
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function buildWorkstreamStateFocus(
|
|
253
|
-
state: WorkstreamState | null,
|
|
254
|
-
chatSummary: string | null,
|
|
255
|
-
): PublicWorkstreamStateFocus | null {
|
|
256
|
-
if (!state) {
|
|
257
|
-
const compactedSummaryFocus = getCompactedSummaryFocus(chatSummary)
|
|
258
|
-
return compactedSummaryFocus ? { kind: 'chat-summary', text: compactedSummaryFocus } : null
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const currentPlan = toOptionalTrimmedString(state.currentPlan?.text)
|
|
262
|
-
if (currentPlan) {
|
|
263
|
-
return { kind: 'plan', text: currentPlan }
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const latestTask =
|
|
267
|
-
[...state.tasks].reverse().find((task) => task.status === 'blocked') ??
|
|
268
|
-
[...state.tasks].reverse().find((task) => task.status === 'in-progress') ??
|
|
269
|
-
[...state.tasks].reverse().find((task) => task.status === 'open')
|
|
270
|
-
const taskTitle = toOptionalTrimmedString(latestTask?.title)
|
|
271
|
-
if (taskTitle) {
|
|
272
|
-
return { kind: 'task', text: taskTitle }
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const latestQuestion = toOptionalTrimmedString(state.openQuestions.at(-1)?.text)
|
|
276
|
-
if (latestQuestion) {
|
|
277
|
-
return { kind: 'question', text: latestQuestion }
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const latestDecision = toOptionalTrimmedString(state.keyDecisions.at(-1)?.decision)
|
|
281
|
-
if (latestDecision) {
|
|
282
|
-
return { kind: 'decision', text: latestDecision }
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const latestAgentNote = toOptionalTrimmedString(state.agentContributions.at(-1)?.summary)
|
|
286
|
-
if (latestAgentNote) {
|
|
287
|
-
return { kind: 'agent-note', text: latestAgentNote }
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
const compactedSummaryFocus = getCompactedSummaryFocus(chatSummary)
|
|
291
|
-
if (compactedSummaryFocus) {
|
|
292
|
-
return { kind: 'chat-summary', text: compactedSummaryFocus }
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
return null
|
|
296
|
-
}
|
|
297
|
-
|
|
298
138
|
class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
|
|
299
139
|
constructor() {
|
|
300
140
|
super(TABLES.WORKSTREAM, WorkstreamSchema)
|
|
@@ -664,14 +504,6 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
|
|
|
664
504
|
await this.setActiveRunId(workstreamId, null)
|
|
665
505
|
}
|
|
666
506
|
|
|
667
|
-
async persistChangeTracker(
|
|
668
|
-
workstreamId: RecordIdRef,
|
|
669
|
-
payload: { chatSummary: string; state: WorkstreamState },
|
|
670
|
-
): Promise<void> {
|
|
671
|
-
const workstreamRef = ensureRecordId(workstreamId, TABLES.WORKSTREAM)
|
|
672
|
-
await this.update(workstreamRef, { chatSummary: payload.chatSummary, state: payload.state })
|
|
673
|
-
}
|
|
674
|
-
|
|
675
507
|
async stopActiveRun(workstreamId: RecordIdRef): Promise<boolean> {
|
|
676
508
|
const activeRunId = await this.getActiveRunId(workstreamId)
|
|
677
509
|
if (!activeRunId) return false
|
|
@@ -868,22 +700,6 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
|
|
|
868
700
|
}
|
|
869
701
|
}
|
|
870
702
|
|
|
871
|
-
toPublicWorkstreamDetail(workstream: WorkstreamRecord): PublicWorkstreamDetail {
|
|
872
|
-
const publicWorkstream = this.toPublicWorkstream(workstream)
|
|
873
|
-
const snapshot = parsePersistedWorkstreamState(workstream.state)
|
|
874
|
-
const chatSummary = toOptionalTrimmedString(workstream.chatSummary)
|
|
875
|
-
const progress = buildWorkstreamStateProgress(snapshot)
|
|
876
|
-
const workstreamState: PublicWorkstreamStatePayload = {
|
|
877
|
-
focus: buildWorkstreamStateFocus(snapshot, chatSummary),
|
|
878
|
-
chatSummary,
|
|
879
|
-
approval: buildWorkstreamApprovalState(snapshot),
|
|
880
|
-
progress,
|
|
881
|
-
snapshot,
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
return { ...publicWorkstream, workstreamState }
|
|
885
|
-
}
|
|
886
|
-
|
|
887
703
|
async incrementTurnCount(workstreamId: RecordIdRef): Promise<number> {
|
|
888
704
|
const workstreamRef = ensureRecordId(workstreamId, TABLES.WORKSTREAM)
|
|
889
705
|
const result = await databaseService.query<{ turnCount: number }>(surql`
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
|
|
3
|
-
import type { WorkstreamState } from '../runtime/workstream-state'
|
|
4
|
-
|
|
5
3
|
export interface Citation {
|
|
6
4
|
title?: string
|
|
7
5
|
url?: string
|
|
@@ -33,68 +31,6 @@ export interface NormalizedWorkstream {
|
|
|
33
31
|
organizationId: string
|
|
34
32
|
}
|
|
35
33
|
|
|
36
|
-
export interface PublicWorkstreamStateFocus {
|
|
37
|
-
kind: 'plan' | 'task' | 'question' | 'decision' | 'agent-note' | 'chat-summary'
|
|
38
|
-
text: string
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface PublicWorkstreamTaskProgress {
|
|
42
|
-
total: number
|
|
43
|
-
open: number
|
|
44
|
-
inProgress: number
|
|
45
|
-
done: number
|
|
46
|
-
blocked: number
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface PublicWorkstreamConstraintProgress {
|
|
50
|
-
total: number
|
|
51
|
-
approved: number
|
|
52
|
-
candidate: number
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface PublicWorkstreamStateProgress {
|
|
56
|
-
hasState: boolean
|
|
57
|
-
lastUpdated: string | null
|
|
58
|
-
completionRatio: number | null
|
|
59
|
-
tasks: PublicWorkstreamTaskProgress
|
|
60
|
-
constraints: PublicWorkstreamConstraintProgress
|
|
61
|
-
keyDecisions: number
|
|
62
|
-
openQuestions: number
|
|
63
|
-
risks: number
|
|
64
|
-
artifacts: number
|
|
65
|
-
agentContributions: number
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export interface PublicWorkstreamApprovalState {
|
|
69
|
-
approvedBy: string | null
|
|
70
|
-
approvedAt: string | null
|
|
71
|
-
approvalMessageId: string | null
|
|
72
|
-
approvalNote: string | null
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export interface PublicWorkstreamStatePayload {
|
|
76
|
-
focus: PublicWorkstreamStateFocus | null
|
|
77
|
-
chatSummary: string | null
|
|
78
|
-
approval: PublicWorkstreamApprovalState | null
|
|
79
|
-
progress: PublicWorkstreamStateProgress
|
|
80
|
-
snapshot: WorkstreamState | null
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export interface PublicWorkstreamDetail {
|
|
84
|
-
id: string
|
|
85
|
-
title: string
|
|
86
|
-
status: 'regular' | 'archived'
|
|
87
|
-
mode: 'direct' | 'group'
|
|
88
|
-
core: boolean
|
|
89
|
-
coreType?: string
|
|
90
|
-
isRunning: boolean
|
|
91
|
-
isCompacting: boolean
|
|
92
|
-
agentId?: string | null
|
|
93
|
-
createdAt: string
|
|
94
|
-
updatedAt: string
|
|
95
|
-
workstreamState: PublicWorkstreamStatePayload
|
|
96
|
-
}
|
|
97
|
-
|
|
98
34
|
export const WorkstreamSchema = z.object({
|
|
99
35
|
id: z.any(), // RecordId
|
|
100
36
|
mode: WorkstreamModeSchema.optional().default('group'),
|
|
@@ -106,7 +42,7 @@ export const WorkstreamSchema = z.object({
|
|
|
106
42
|
memoryBlock: z.string().nullish(),
|
|
107
43
|
memoryBlockSummary: z.string().nullish(),
|
|
108
44
|
activeRunId: z.string().nullish(),
|
|
109
|
-
|
|
45
|
+
compactionSummary: z.string().nullish(),
|
|
110
46
|
lastCompactedMessageId: z.string().nullish(),
|
|
111
47
|
nameGenerated: z.boolean().optional().default(false),
|
|
112
48
|
isCompacting: z.boolean().optional(),
|
|
@@ -1,313 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
|
|
3
|
-
import { aiLogger } from '../config/logger'
|
|
4
|
-
import type { RecordIdRef } from '../db/record-id'
|
|
5
|
-
import { mergeStateDelta } from '../runtime/context-compaction'
|
|
6
|
-
import { createHelperModelRuntime, extractJsonObjectCandidates } from '../runtime/helper-model'
|
|
7
|
-
import { toOptionalTrimmedString } from '../runtime/workstream-chat-helpers'
|
|
8
|
-
import {
|
|
9
|
-
StructuredWorkstreamStateDeltaSchema,
|
|
10
|
-
createEmptyStructuredWorkstreamStateDelta,
|
|
11
|
-
createEmptyWorkstreamState,
|
|
12
|
-
parseStructuredWorkstreamStateDelta,
|
|
13
|
-
} from '../runtime/workstream-state'
|
|
14
|
-
import type { WorkstreamState, WorkstreamStateDelta } from '../runtime/workstream-state'
|
|
15
|
-
import { createWorkstreamTrackerAgent } from '../system-agents/workstream-tracker.agent'
|
|
16
|
-
import { compactWhitespace, isRecord, truncateText } from '../utils/string'
|
|
17
|
-
import { workstreamService } from './workstream.service'
|
|
18
|
-
|
|
19
|
-
const helperModelRuntime = createHelperModelRuntime()
|
|
20
|
-
|
|
21
|
-
const TrackerOutputSchema = z.object({
|
|
22
|
-
summary: z.string().trim().min(1).max(1_200),
|
|
23
|
-
stateDelta: StructuredWorkstreamStateDeltaSchema,
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
type TrackerMessage = { label: string; text: string }
|
|
27
|
-
|
|
28
|
-
const TRACKER_JSON_WRAPPER_KEYS = ['output', 'result', 'data'] as const
|
|
29
|
-
const TRACKER_SUMMARY_KEYS = ['summary', 'chatSummary', 'summaryText', 'sidebarSummary', 'message'] as const
|
|
30
|
-
const TRACKER_STATE_DELTA_KEYS = ['stateDelta', 'delta', 'workstreamStateDelta', 'trackerStateDelta', 'state'] as const
|
|
31
|
-
const TRACKER_FALLBACK_SECTION_PREFIX =
|
|
32
|
-
/^(?:#{1,6}\s*)?(?:\*\*)?(?:state delta|current plan|new decisions|resolved questions|new questions|new constraints|new risks|task updates|artifacts|agent note|conflicts|approved by|approved at|approval message id|approval note)(?:\*\*)?\s*:?\s*/i
|
|
33
|
-
|
|
34
|
-
function renderMessages(messages: TrackerMessage[]): string {
|
|
35
|
-
if (messages.length === 0) return '- None'
|
|
36
|
-
return messages
|
|
37
|
-
.map((message, index) => `### Message ${index + 1}\n- Actor: ${message.label}\n- Content: ${message.text}`)
|
|
38
|
-
.join('\n\n')
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function getRecordStringField(record: Record<string, unknown>, keys: readonly string[]): string | null {
|
|
42
|
-
for (const key of keys) {
|
|
43
|
-
const field = toOptionalTrimmedString(typeof record[key] === 'string' ? record[key] : null)
|
|
44
|
-
if (field) return field
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return null
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function getTrackerJsonObjects(value: unknown): Record<string, unknown>[] {
|
|
51
|
-
if (!isRecord(value)) return []
|
|
52
|
-
|
|
53
|
-
const candidates: Record<string, unknown>[] = [value]
|
|
54
|
-
for (const key of TRACKER_JSON_WRAPPER_KEYS) {
|
|
55
|
-
const nested = value[key]
|
|
56
|
-
if (isRecord(nested)) {
|
|
57
|
-
candidates.push(nested)
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return candidates
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function parseTrackerJsonFallback(text: string): z.infer<typeof TrackerOutputSchema> | null {
|
|
65
|
-
for (const candidateText of extractJsonObjectCandidates(text)) {
|
|
66
|
-
let parsedJson: unknown
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
parsedJson = JSON.parse(candidateText) as unknown
|
|
70
|
-
} catch {
|
|
71
|
-
continue
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
for (const candidate of getTrackerJsonObjects(parsedJson)) {
|
|
75
|
-
const summary = getRecordStringField(candidate, TRACKER_SUMMARY_KEYS)
|
|
76
|
-
if (!summary) continue
|
|
77
|
-
|
|
78
|
-
const stateDeltaValue = TRACKER_STATE_DELTA_KEYS.map((key) => candidate[key]).find((value) => value !== undefined)
|
|
79
|
-
const parsedStateDelta = StructuredWorkstreamStateDeltaSchema.safeParse(stateDeltaValue)
|
|
80
|
-
|
|
81
|
-
return {
|
|
82
|
-
summary: truncateText(summary, 1_200),
|
|
83
|
-
stateDelta: parsedStateDelta.success ? parsedStateDelta.data : createEmptyStructuredWorkstreamStateDelta(),
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return null
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function extractTrackerFallbackSummary(text: string): string | null {
|
|
92
|
-
const normalized = text.replace(/\r/g, '').trim()
|
|
93
|
-
if (!normalized) return null
|
|
94
|
-
|
|
95
|
-
const summaryLineMatch = normalized.match(/(?:^|\n)(?:#{1,6}\s*)?(?:\*\*)?summary(?:\*\*)?\s*:?\s*(.+?)(?=\n|$)/i)
|
|
96
|
-
const summaryLine = summaryLineMatch?.[1]?.trim()
|
|
97
|
-
if (summaryLine && !summaryLine.startsWith('{') && !summaryLine.startsWith('```')) {
|
|
98
|
-
return truncateText(summaryLine, 1_200)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const summarySectionMatch = normalized.match(
|
|
102
|
-
/(?:^|\n)(?:#{1,6}\s*)?(?:\*\*)?summary(?:\*\*)?\s*:?\s*\n([\s\S]*?)(?=\n(?:#{1,6}\s*)?(?:\*\*)?(?:state delta|summary)(?:\*\*)?\s*:?\s*\n|$)/i,
|
|
103
|
-
)
|
|
104
|
-
const summarySource = summarySectionMatch?.[1]?.trim() || normalized
|
|
105
|
-
const paragraphs = summarySource
|
|
106
|
-
.split(/\n\s*\n/)
|
|
107
|
-
.map((paragraph) => paragraph.trim())
|
|
108
|
-
.filter((paragraph) => paragraph.length > 0)
|
|
109
|
-
|
|
110
|
-
for (const paragraph of paragraphs) {
|
|
111
|
-
const candidate = paragraph.replace(/^(?:#{1,6}\s*)?(?:\*\*)?summary(?:\*\*)?\s*:?\s*/i, '').trim()
|
|
112
|
-
if (
|
|
113
|
-
!candidate ||
|
|
114
|
-
candidate.startsWith('{') ||
|
|
115
|
-
candidate.startsWith('```') ||
|
|
116
|
-
TRACKER_FALLBACK_SECTION_PREFIX.test(candidate)
|
|
117
|
-
) {
|
|
118
|
-
continue
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return truncateText(candidate, 1_200)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return null
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export function parseTrackerTextFallback(text: string): z.infer<typeof TrackerOutputSchema> | null {
|
|
128
|
-
const jsonFallback = parseTrackerJsonFallback(text)
|
|
129
|
-
if (jsonFallback) {
|
|
130
|
-
return jsonFallback
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const summary = extractTrackerFallbackSummary(text)
|
|
134
|
-
if (!summary) return null
|
|
135
|
-
|
|
136
|
-
return { summary, stateDelta: createEmptyStructuredWorkstreamStateDelta() }
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function extractHeuristicSummaryCandidate(text: string | null | undefined): string | null {
|
|
140
|
-
const normalized = compactWhitespace(toOptionalTrimmedString(text) ?? '')
|
|
141
|
-
if (!normalized) return null
|
|
142
|
-
|
|
143
|
-
const match = /^(.+?[.!?])(?:\s|$)/.exec(normalized)
|
|
144
|
-
const candidate = match ? String(match[1]) : normalized
|
|
145
|
-
return truncateText(candidate, 1_200)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export function buildHeuristicTrackerSummary(params: {
|
|
149
|
-
previousSummary: string | null
|
|
150
|
-
userMessageText: string | null
|
|
151
|
-
assistantMessages: TrackerMessage[]
|
|
152
|
-
}): string | null {
|
|
153
|
-
for (const message of params.assistantMessages) {
|
|
154
|
-
const summary = extractHeuristicSummaryCandidate(message.text)
|
|
155
|
-
if (summary) {
|
|
156
|
-
return summary
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const userSummary = extractHeuristicSummaryCandidate(params.userMessageText)
|
|
161
|
-
if (userSummary) {
|
|
162
|
-
return truncateText(`User request: ${userSummary}`, 1_200)
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return extractHeuristicSummaryCandidate(params.previousSummary)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function formatTrackerPrompt(params: {
|
|
169
|
-
title: string
|
|
170
|
-
mode: 'direct' | 'group'
|
|
171
|
-
coreType?: string
|
|
172
|
-
visibleAgentId?: string
|
|
173
|
-
hasActiveExecutionPlan: boolean
|
|
174
|
-
previousSummary: string | null
|
|
175
|
-
existingState: WorkstreamState
|
|
176
|
-
userMessageText: string | null
|
|
177
|
-
assistantMessages: TrackerMessage[]
|
|
178
|
-
}): string {
|
|
179
|
-
return [
|
|
180
|
-
'# Workstream Turn',
|
|
181
|
-
'',
|
|
182
|
-
`- Title: ${params.title}`,
|
|
183
|
-
`- Mode: ${params.mode}`,
|
|
184
|
-
`- Visible agent: ${params.visibleAgentId ?? 'none'}`,
|
|
185
|
-
`- Active execution plan: ${params.hasActiveExecutionPlan ? 'yes' : 'no'}`,
|
|
186
|
-
...(params.coreType ? [`- Core type: ${params.coreType}`] : []),
|
|
187
|
-
'',
|
|
188
|
-
'## Previous Summary',
|
|
189
|
-
params.previousSummary ?? 'None',
|
|
190
|
-
'',
|
|
191
|
-
'## Existing State',
|
|
192
|
-
JSON.stringify(params.existingState),
|
|
193
|
-
'',
|
|
194
|
-
'## User Message',
|
|
195
|
-
params.userMessageText ?? 'None',
|
|
196
|
-
'',
|
|
197
|
-
'## Assistant Messages',
|
|
198
|
-
renderMessages(params.assistantMessages),
|
|
199
|
-
...(params.hasActiveExecutionPlan
|
|
200
|
-
? [
|
|
201
|
-
'',
|
|
202
|
-
'## Tracker Constraint',
|
|
203
|
-
'An active execution plan exists. Do not update currentPlan or taskUpdates. Track only decisions, questions, risks, artifacts, approvals, and agent notes.',
|
|
204
|
-
]
|
|
205
|
-
: []),
|
|
206
|
-
].join('\n')
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
export function applyTrackedStateDelta(params: {
|
|
210
|
-
existingState: WorkstreamState
|
|
211
|
-
delta: WorkstreamStateDelta
|
|
212
|
-
hasActiveExecutionPlan: boolean
|
|
213
|
-
now: () => number
|
|
214
|
-
}): WorkstreamState {
|
|
215
|
-
const mergedState = mergeStateDelta(params.existingState, params.delta, params.now)
|
|
216
|
-
if (!params.hasActiveExecutionPlan) {
|
|
217
|
-
return mergedState
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return { ...mergedState, currentPlan: null, tasks: [] }
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
export async function updateWorkstreamChangeTracker(params: {
|
|
224
|
-
workstreamId: RecordIdRef
|
|
225
|
-
title: string
|
|
226
|
-
mode: 'direct' | 'group'
|
|
227
|
-
coreType?: string
|
|
228
|
-
visibleAgentId?: string
|
|
229
|
-
hasActiveExecutionPlan: boolean
|
|
230
|
-
previousSummary: string | null
|
|
231
|
-
existingState: WorkstreamState | null
|
|
232
|
-
userMessageText: string | null
|
|
233
|
-
assistantMessages: TrackerMessage[]
|
|
234
|
-
}): Promise<boolean> {
|
|
235
|
-
const assistantMessages = params.assistantMessages
|
|
236
|
-
.map((message) => ({
|
|
237
|
-
label: toOptionalTrimmedString(message.label) ?? 'Assistant',
|
|
238
|
-
text: truncateText(message.text.trim(), 2_400),
|
|
239
|
-
}))
|
|
240
|
-
.filter((message) => message.text.length > 0)
|
|
241
|
-
.slice(0, 6)
|
|
242
|
-
|
|
243
|
-
if (!toOptionalTrimmedString(params.userMessageText) && assistantMessages.length === 0) {
|
|
244
|
-
return false
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const existingState = params.existingState ?? createEmptyWorkstreamState()
|
|
248
|
-
|
|
249
|
-
try {
|
|
250
|
-
const output = await helperModelRuntime.generateHelperStructured({
|
|
251
|
-
tag: 'workstream-change-tracker',
|
|
252
|
-
createAgent: createWorkstreamTrackerAgent,
|
|
253
|
-
schema: TrackerOutputSchema,
|
|
254
|
-
maxOutputTokens: 1_400,
|
|
255
|
-
textFallbackParser: parseTrackerTextFallback,
|
|
256
|
-
messages: [
|
|
257
|
-
{
|
|
258
|
-
role: 'user',
|
|
259
|
-
content: formatTrackerPrompt({
|
|
260
|
-
title: params.title,
|
|
261
|
-
mode: params.mode,
|
|
262
|
-
...(params.coreType ? { coreType: params.coreType } : {}),
|
|
263
|
-
...(params.visibleAgentId ? { visibleAgentId: params.visibleAgentId } : {}),
|
|
264
|
-
hasActiveExecutionPlan: params.hasActiveExecutionPlan,
|
|
265
|
-
previousSummary: params.previousSummary,
|
|
266
|
-
existingState,
|
|
267
|
-
userMessageText: toOptionalTrimmedString(params.userMessageText),
|
|
268
|
-
assistantMessages,
|
|
269
|
-
}),
|
|
270
|
-
},
|
|
271
|
-
],
|
|
272
|
-
})
|
|
273
|
-
|
|
274
|
-
const sparseStateDelta = parseStructuredWorkstreamStateDelta(output.stateDelta)
|
|
275
|
-
const nextState = applyTrackedStateDelta({
|
|
276
|
-
existingState,
|
|
277
|
-
delta: sparseStateDelta,
|
|
278
|
-
hasActiveExecutionPlan: params.hasActiveExecutionPlan,
|
|
279
|
-
now: () => Date.now(),
|
|
280
|
-
})
|
|
281
|
-
await workstreamService.persistChangeTracker(params.workstreamId, { chatSummary: output.summary, state: nextState })
|
|
282
|
-
return true
|
|
283
|
-
} catch (error) {
|
|
284
|
-
const fallbackSummary = buildHeuristicTrackerSummary({
|
|
285
|
-
previousSummary: params.previousSummary,
|
|
286
|
-
userMessageText: toOptionalTrimmedString(params.userMessageText),
|
|
287
|
-
assistantMessages,
|
|
288
|
-
})
|
|
289
|
-
|
|
290
|
-
if (!fallbackSummary) {
|
|
291
|
-
aiLogger.warn`Workstream change tracker update failed: ${error}`
|
|
292
|
-
return false
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
try {
|
|
296
|
-
const nextState = applyTrackedStateDelta({
|
|
297
|
-
existingState,
|
|
298
|
-
delta: {},
|
|
299
|
-
hasActiveExecutionPlan: params.hasActiveExecutionPlan,
|
|
300
|
-
now: () => Date.now(),
|
|
301
|
-
})
|
|
302
|
-
await workstreamService.persistChangeTracker(params.workstreamId, {
|
|
303
|
-
chatSummary: fallbackSummary,
|
|
304
|
-
state: nextState,
|
|
305
|
-
})
|
|
306
|
-
aiLogger.info`Workstream change tracker used heuristic fallback after helper failure`
|
|
307
|
-
return true
|
|
308
|
-
} catch (persistError) {
|
|
309
|
-
aiLogger.warn`Workstream change tracker update failed: ${error}; fallback_persist=${persistError}`
|
|
310
|
-
return false
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { ToolLoopAgent } from 'ai'
|
|
2
|
-
|
|
3
|
-
import { bifrostOpenRouterResponseHealingModel } from '../bifrost/bifrost'
|
|
4
|
-
import {
|
|
5
|
-
OPENROUTER_LOW_REASONING_PROVIDER_OPTIONS,
|
|
6
|
-
OPENROUTER_STRUCTURED_REASONING_MODEL_ID,
|
|
7
|
-
} from '../config/model-constants'
|
|
8
|
-
import type { CreateHelperToolLoopAgentOptions } from '../runtime/agent-types'
|
|
9
|
-
import { resolveHelperAgentOptions } from './helper-agent-options'
|
|
10
|
-
|
|
11
|
-
const WORKSTREAM_TRACKER_PROMPT = `<agent-instructions>
|
|
12
|
-
You are the **Workstream Tracker**.
|
|
13
|
-
|
|
14
|
-
<task>
|
|
15
|
-
Convert one completed workstream turn into:
|
|
16
|
-
- a concise change-tracker summary for the right sidebar
|
|
17
|
-
- a structured workstream state delta containing only the updates implied by this turn
|
|
18
|
-
</task>
|
|
19
|
-
|
|
20
|
-
<rules>
|
|
21
|
-
- Use only the provided turn evidence and prior state.
|
|
22
|
-
- Keep the summary short, concrete, and focused on what changed or what is blocked now.
|
|
23
|
-
- Prefer durable tracker items over prose: tasks, decisions, questions, risks, artifacts, and agent notes.
|
|
24
|
-
- Do not duplicate existing state unless this turn meaningfully changed it.
|
|
25
|
-
- Never invent analytics, repository facts, customer signals, or business decisions that were not stated.
|
|
26
|
-
</rules>
|
|
27
|
-
|
|
28
|
-
<state-guidance>
|
|
29
|
-
- Use taskUpdates for concrete next steps, ongoing work, completed work, or blocked work.
|
|
30
|
-
- When the turn includes concrete tool work or operational execution, capture that work as taskUpdates even if it was
|
|
31
|
-
completed in the same turn.
|
|
32
|
-
- For website inspection or plugin refresh flows, prefer task titles that describe the executed step clearly, such as
|
|
33
|
-
refreshing website intelligence, overwriting artifacts, or reviewing evidence gaps.
|
|
34
|
-
- If an owner is not explicit, default the task owner to the visible lead agent for the workstream.
|
|
35
|
-
- Use newDecisions only when a clear decision or tradeoff was made.
|
|
36
|
-
- Use newQuestions only for unresolved questions that matter for progress.
|
|
37
|
-
- Use newRisks only for concrete execution or business risks surfaced in this turn.
|
|
38
|
-
- Use agentNote for the main contribution from the lead agent when it is worth retaining.
|
|
39
|
-
- Keep the delta compact. A noisy tracker is worse than a sparse tracker.
|
|
40
|
-
</state-guidance>
|
|
41
|
-
|
|
42
|
-
<output>
|
|
43
|
-
The caller enforces a structured schema.
|
|
44
|
-
- Return every stateDelta field.
|
|
45
|
-
- Use empty arrays for unchanged list fields.
|
|
46
|
-
- Use null for unchanged nullable fields.
|
|
47
|
-
- For currentPlan, use \`{"action":"unchanged","text":null}\` when nothing changed, \`{"action":"clear","text":null}\` to clear it, and \`{"action":"set","text":"..."}\` to replace it.
|
|
48
|
-
</output>
|
|
49
|
-
</agent-instructions>`
|
|
50
|
-
|
|
51
|
-
export function createWorkstreamTrackerAgent(options: CreateHelperToolLoopAgentOptions) {
|
|
52
|
-
return new ToolLoopAgent({
|
|
53
|
-
id: 'workstream-tracker',
|
|
54
|
-
model: bifrostOpenRouterResponseHealingModel(OPENROUTER_STRUCTURED_REASONING_MODEL_ID),
|
|
55
|
-
providerOptions: OPENROUTER_LOW_REASONING_PROVIDER_OPTIONS,
|
|
56
|
-
...resolveHelperAgentOptions(options, { instructions: WORKSTREAM_TRACKER_PROMPT }),
|
|
57
|
-
})
|
|
58
|
-
}
|