@lota-sdk/core 0.1.46 → 0.1.47
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/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 +47 -47
- package/src/system-agents/context-compaction.agent.ts +4 -10
- package/src/runtime/workstream-state.ts +0 -274
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ChatMessage } from '@lota-sdk/shared'
|
|
2
|
+
import { z } from 'zod'
|
|
2
3
|
|
|
3
4
|
import { CHARS_PER_TOKEN_ESTIMATE, compactWhitespace, readRecord, readString, stringifyUnknown } from '../utils/string'
|
|
4
5
|
import {
|
|
@@ -12,15 +13,6 @@ import {
|
|
|
12
13
|
CONTEXT_SAFETY_MARGIN_TOKENS,
|
|
13
14
|
SUMMARY_ROLLUP_MAX_TOKENS,
|
|
14
15
|
} from './context-compaction-constants'
|
|
15
|
-
import {
|
|
16
|
-
StructuredCompactionOutputSchema,
|
|
17
|
-
WorkstreamStateDeltaSchema,
|
|
18
|
-
WorkstreamStateSchema,
|
|
19
|
-
applyWorkstreamStateCaps,
|
|
20
|
-
createEmptyWorkstreamState,
|
|
21
|
-
parseStructuredWorkstreamStateDelta,
|
|
22
|
-
} from './workstream-state'
|
|
23
|
-
import type { CompactionOutput, WorkstreamState, WorkstreamStateDelta } from './workstream-state'
|
|
24
16
|
|
|
25
17
|
export interface ContextMessage {
|
|
26
18
|
role: 'system' | 'user' | 'assistant'
|
|
@@ -39,7 +31,6 @@ export interface CompactHistoryParams {
|
|
|
39
31
|
liveMessages: ChatMessage[]
|
|
40
32
|
tailMessageCount: number
|
|
41
33
|
contextSize?: number
|
|
42
|
-
existingState: WorkstreamState
|
|
43
34
|
}
|
|
44
35
|
|
|
45
36
|
export interface CompactHistoryResult {
|
|
@@ -52,20 +43,16 @@ export interface CompactHistoryResult {
|
|
|
52
43
|
estimatedTokens: number
|
|
53
44
|
inputChars: number
|
|
54
45
|
outputChars: number
|
|
55
|
-
state: WorkstreamState
|
|
56
|
-
stateDelta: WorkstreamStateDelta
|
|
57
46
|
}
|
|
58
47
|
|
|
59
48
|
export interface ContextCompactionRunnerParams {
|
|
60
49
|
previousSummary: string
|
|
61
|
-
existingState: WorkstreamState
|
|
62
50
|
chunk: ContextMessage[]
|
|
63
51
|
transcript: string
|
|
64
52
|
}
|
|
65
53
|
|
|
66
54
|
export interface ContextCompactionPromptParams {
|
|
67
55
|
previousSummary: string
|
|
68
|
-
existingState: WorkstreamState
|
|
69
56
|
transcript: string
|
|
70
57
|
}
|
|
71
58
|
|
|
@@ -74,6 +61,12 @@ export interface MemoryBlockCompactionPromptParams {
|
|
|
74
61
|
newEntriesText: string
|
|
75
62
|
}
|
|
76
63
|
|
|
64
|
+
export const ContextCompactionOutputSchema = z.object({ summary: z.string().trim().min(1) })
|
|
65
|
+
|
|
66
|
+
export interface CompactionOutput {
|
|
67
|
+
summary: string
|
|
68
|
+
}
|
|
69
|
+
|
|
77
70
|
export type ContextCompactionRunner = (params: ContextCompactionRunnerParams) => Promise<CompactionOutput>
|
|
78
71
|
|
|
79
72
|
export interface CreateContextCompactionRuntimeOptions {
|
|
@@ -89,155 +82,30 @@ export interface CreateContextCompactionRuntimeOptions {
|
|
|
89
82
|
includedToolPrefixes?: readonly string[]
|
|
90
83
|
}
|
|
91
84
|
|
|
92
|
-
|
|
93
|
-
|
|
85
|
+
export interface ContextCompactionRuntime {
|
|
86
|
+
createSummaryMessage: (summaryText: string) => ChatMessage | null
|
|
87
|
+
prependSummaryMessage: (summaryText: string, liveMessages: ChatMessage[]) => ChatMessage[]
|
|
88
|
+
estimateThreshold: (contextSize?: number) => number
|
|
89
|
+
shouldCompactHistory: (params: {
|
|
90
|
+
summaryText: string
|
|
91
|
+
liveMessages: ChatMessage[]
|
|
92
|
+
contextSize?: number
|
|
93
|
+
}) => CompactionAssessment
|
|
94
|
+
compactHistory: (params: CompactHistoryParams) => Promise<CompactHistoryResult>
|
|
95
|
+
}
|
|
94
96
|
|
|
95
97
|
function estimateTokens(text: string): number {
|
|
96
98
|
return Math.ceil(text.length / CHARS_PER_TOKEN_ESTIMATE)
|
|
97
99
|
}
|
|
98
100
|
|
|
99
|
-
function
|
|
100
|
-
|
|
101
|
-
const normalized = compactWhitespace(value)
|
|
102
|
-
if (!normalized) return null
|
|
103
|
-
if (PROMPT_INJECTION_PATTERN.test(normalized)) return null
|
|
104
|
-
return normalized
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function sanitizeAndMapStateField<T, U>(items: T[], mapItem: (item: T) => U | null): U[] {
|
|
108
|
-
return items.map(mapItem).filter((item): item is U => item !== null)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function sanitizeStateStrings(values: Array<string | null | undefined>): string[] {
|
|
112
|
-
return values.map((value) => sanitizeStateText(value)).filter((value): value is string => Boolean(value))
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function sanitizeTextFieldItems<T extends Record<string, unknown>, K extends keyof T>(items: T[], textField: K): T[] {
|
|
116
|
-
return sanitizeAndMapStateField(items, (item) => {
|
|
117
|
-
const text = sanitizeStateText(item[textField] as string | null | undefined)
|
|
118
|
-
if (!text) return null
|
|
119
|
-
return { ...item, [textField]: text }
|
|
120
|
-
})
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function buildExistingWorkstreamStateForCompactionPrompt(state: WorkstreamState): Record<string, unknown> {
|
|
124
|
-
const currentPlanText = state.currentPlan ? sanitizeStateText(state.currentPlan.text) : null
|
|
125
|
-
|
|
126
|
-
const activeConstraints = sanitizeAndMapStateField(state.activeConstraints, (constraint) => {
|
|
127
|
-
const text = sanitizeStateText(constraint.text)
|
|
128
|
-
if (!text) return null
|
|
129
|
-
return {
|
|
130
|
-
id: constraint.id,
|
|
131
|
-
text,
|
|
132
|
-
source: constraint.source,
|
|
133
|
-
approved: constraint.approved,
|
|
134
|
-
sourceMessageIds: constraint.sourceMessageIds,
|
|
135
|
-
}
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
const keyDecisions = sanitizeAndMapStateField(state.keyDecisions, (decision) => {
|
|
139
|
-
const normalizedDecision = sanitizeStateText(decision.decision)
|
|
140
|
-
const rationale = sanitizeStateText(decision.rationale)
|
|
141
|
-
if (!normalizedDecision || !rationale) return null
|
|
142
|
-
return {
|
|
143
|
-
id: decision.id,
|
|
144
|
-
decision: normalizedDecision,
|
|
145
|
-
rationale,
|
|
146
|
-
agent: decision.agent,
|
|
147
|
-
sourceMessageIds: decision.sourceMessageIds,
|
|
148
|
-
confidence: decision.confidence,
|
|
149
|
-
}
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
const tasks = sanitizeAndMapStateField(state.tasks, (task) => {
|
|
153
|
-
const title = sanitizeStateText(task.title)
|
|
154
|
-
const owner = sanitizeStateText(task.owner)
|
|
155
|
-
if (!title || !owner) return null
|
|
156
|
-
return {
|
|
157
|
-
id: task.id,
|
|
158
|
-
title,
|
|
159
|
-
status: task.status,
|
|
160
|
-
owner,
|
|
161
|
-
externalId: task.externalId,
|
|
162
|
-
source: task.source,
|
|
163
|
-
sourceMessageIds: task.sourceMessageIds,
|
|
164
|
-
}
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
const openQuestions = sanitizeAndMapStateField(state.openQuestions, (question) => {
|
|
168
|
-
const text = sanitizeStateText(question.text)
|
|
169
|
-
if (!text) return null
|
|
170
|
-
return { id: question.id, text, source: question.source, sourceMessageIds: question.sourceMessageIds }
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
const artifacts = sanitizeAndMapStateField(state.artifacts, (artifact) => {
|
|
174
|
-
const name = sanitizeStateText(artifact.name)
|
|
175
|
-
const pointer = sanitizeStateText(artifact.pointer)
|
|
176
|
-
if (!name || !pointer) return null
|
|
177
|
-
return { id: artifact.id, name, type: artifact.type, pointer }
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
const agentContributions = sanitizeAndMapStateField(state.agentContributions, (note) => {
|
|
181
|
-
const summary = sanitizeStateText(note.summary)
|
|
182
|
-
if (!summary) return null
|
|
183
|
-
return { id: note.id, agent: note.agent, summary }
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
return {
|
|
187
|
-
currentPlan: currentPlanText
|
|
188
|
-
? {
|
|
189
|
-
id: state.currentPlan?.id,
|
|
190
|
-
text: currentPlanText,
|
|
191
|
-
source: state.currentPlan?.source,
|
|
192
|
-
approved: state.currentPlan?.approved,
|
|
193
|
-
sourceMessageIds: state.currentPlan?.sourceMessageIds ?? [],
|
|
194
|
-
}
|
|
195
|
-
: null,
|
|
196
|
-
activeConstraints,
|
|
197
|
-
keyDecisions,
|
|
198
|
-
tasks,
|
|
199
|
-
openQuestions,
|
|
200
|
-
risks: sanitizeStateStrings(state.risks),
|
|
201
|
-
artifacts,
|
|
202
|
-
agentContributions,
|
|
203
|
-
approvedBy: state.approvedBy ? sanitizeStateText(state.approvedBy) : undefined,
|
|
204
|
-
approvalNote: state.approvalNote ? sanitizeStateText(state.approvalNote) : undefined,
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function createStableId(prefix: string, ...parts: Array<string | number | undefined>): string {
|
|
209
|
-
const payload = parts
|
|
210
|
-
.map((part) => (part === undefined ? '' : String(part)))
|
|
211
|
-
.map((part) => compactWhitespace(part))
|
|
212
|
-
.join('|')
|
|
213
|
-
const hash = new Bun.CryptoHasher('sha1').update(`${prefix}|${payload}`).digest('hex').slice(0, 20)
|
|
214
|
-
return `${prefix}_${hash}`
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function appendUnique(values: string[], nextValues: string[]): string[] {
|
|
218
|
-
const seen = new Set(values.map((value) => compactWhitespace(value).toLowerCase()))
|
|
219
|
-
const merged = [...values]
|
|
220
|
-
|
|
221
|
-
for (const value of nextValues) {
|
|
222
|
-
const normalized = compactWhitespace(value)
|
|
223
|
-
if (!normalized) continue
|
|
224
|
-
const key = normalized.toLowerCase()
|
|
225
|
-
if (seen.has(key)) continue
|
|
226
|
-
seen.add(key)
|
|
227
|
-
merged.push(normalized)
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
return merged
|
|
101
|
+
function normalizeSummary(summaryText: string): string {
|
|
102
|
+
return summaryText.trim()
|
|
231
103
|
}
|
|
232
104
|
|
|
233
105
|
function formatSummary(summaryText: string): string {
|
|
234
106
|
return `Compacted context summary:\n${summaryText.trim()}`
|
|
235
107
|
}
|
|
236
108
|
|
|
237
|
-
function normalizeSummary(summaryText: string): string {
|
|
238
|
-
return summaryText.trim()
|
|
239
|
-
}
|
|
240
|
-
|
|
241
109
|
function buildSyntheticSummaryPayload(
|
|
242
110
|
summaryText: string,
|
|
243
111
|
): { role: 'system'; parts: Array<{ type: 'text'; text: string }> } | null {
|
|
@@ -263,49 +131,6 @@ function markMessageCompacted(message: ChatMessage, now: () => number): ChatMess
|
|
|
263
131
|
}
|
|
264
132
|
}
|
|
265
133
|
|
|
266
|
-
export function parseWorkstreamState(value: unknown): WorkstreamState {
|
|
267
|
-
const parsed = WorkstreamStateSchema.safeParse(value)
|
|
268
|
-
return parsed.success ? parsed.data : createEmptyWorkstreamState()
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function pickDefined<T extends Record<string, unknown>, K extends keyof T>(
|
|
272
|
-
next: T,
|
|
273
|
-
base: T,
|
|
274
|
-
key: K,
|
|
275
|
-
): Partial<Pick<T, K>> {
|
|
276
|
-
if (next[key] !== undefined) {
|
|
277
|
-
return { [key]: next[key] } as Partial<Pick<T, K>>
|
|
278
|
-
}
|
|
279
|
-
if (base[key] !== undefined) {
|
|
280
|
-
return { [key]: base[key] } as Partial<Pick<T, K>>
|
|
281
|
-
}
|
|
282
|
-
return {}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
export function toStateFieldsUpdated(delta: WorkstreamStateDelta): string[] {
|
|
286
|
-
const fields: string[] = []
|
|
287
|
-
if (delta.currentPlan !== undefined) fields.push('currentPlan')
|
|
288
|
-
if ((delta.newDecisions?.length ?? 0) > 0) fields.push('keyDecisions')
|
|
289
|
-
if ((delta.resolvedQuestions?.length ?? 0) > 0 || (delta.newQuestions?.length ?? 0) > 0) fields.push('openQuestions')
|
|
290
|
-
if ((delta.newConstraints?.length ?? 0) > 0) fields.push('activeConstraints')
|
|
291
|
-
if ((delta.newRisks?.length ?? 0) > 0) fields.push('risks')
|
|
292
|
-
if ((delta.taskUpdates?.length ?? 0) > 0) fields.push('tasks')
|
|
293
|
-
if ((delta.artifacts?.length ?? 0) > 0) fields.push('artifacts')
|
|
294
|
-
if (delta.agentNote) fields.push('agentContributions')
|
|
295
|
-
if ((delta.conflicts?.length ?? 0) > 0) fields.push('conflicts')
|
|
296
|
-
|
|
297
|
-
if (
|
|
298
|
-
delta.approvedBy !== undefined ||
|
|
299
|
-
delta.approvedAt !== undefined ||
|
|
300
|
-
delta.approvalMessageId !== undefined ||
|
|
301
|
-
delta.approvalNote !== undefined
|
|
302
|
-
) {
|
|
303
|
-
fields.push('approval')
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
return fields
|
|
307
|
-
}
|
|
308
|
-
|
|
309
134
|
function toCompactionTranscript(messages: ContextMessage[]): string {
|
|
310
135
|
return messages
|
|
311
136
|
.map((message, index) => {
|
|
@@ -338,248 +163,11 @@ function splitByCharBudget(messages: ContextMessage[], maxChars: number): Contex
|
|
|
338
163
|
currentChars += nextChars
|
|
339
164
|
}
|
|
340
165
|
|
|
341
|
-
if (current.length > 0)
|
|
342
|
-
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
function mergeDelta(base: WorkstreamStateDelta, next: WorkstreamStateDelta): WorkstreamStateDelta {
|
|
346
|
-
const mergedConstraints = appendUnique(base.newConstraints ?? [], next.newConstraints ?? [])
|
|
347
|
-
const mergedRisks = appendUnique(base.newRisks ?? [], next.newRisks ?? [])
|
|
348
|
-
const mergedQuestions = appendUnique(base.newQuestions ?? [], next.newQuestions ?? [])
|
|
349
|
-
const mergedResolvedQuestions = appendUnique(base.resolvedQuestions ?? [], next.resolvedQuestions ?? [])
|
|
350
|
-
|
|
351
|
-
return {
|
|
352
|
-
...(next.currentPlan !== undefined
|
|
353
|
-
? { currentPlan: next.currentPlan }
|
|
354
|
-
: base.currentPlan !== undefined
|
|
355
|
-
? { currentPlan: base.currentPlan }
|
|
356
|
-
: {}),
|
|
357
|
-
...(mergedConstraints.length > 0 ? { newConstraints: mergedConstraints } : {}),
|
|
358
|
-
...(mergedRisks.length > 0 ? { newRisks: mergedRisks } : {}),
|
|
359
|
-
...(mergedQuestions.length > 0 ? { newQuestions: mergedQuestions } : {}),
|
|
360
|
-
...(mergedResolvedQuestions.length > 0 ? { resolvedQuestions: mergedResolvedQuestions } : {}),
|
|
361
|
-
...((base.newDecisions?.length ?? 0) + (next.newDecisions?.length ?? 0) > 0
|
|
362
|
-
? { newDecisions: [...(base.newDecisions ?? []), ...(next.newDecisions ?? [])] }
|
|
363
|
-
: {}),
|
|
364
|
-
...((base.taskUpdates?.length ?? 0) + (next.taskUpdates?.length ?? 0) > 0
|
|
365
|
-
? { taskUpdates: [...(base.taskUpdates ?? []), ...(next.taskUpdates ?? [])] }
|
|
366
|
-
: {}),
|
|
367
|
-
...((base.artifacts?.length ?? 0) + (next.artifacts?.length ?? 0) > 0
|
|
368
|
-
? { artifacts: [...(base.artifacts ?? []), ...(next.artifacts ?? [])] }
|
|
369
|
-
: {}),
|
|
370
|
-
...(next.agentNote ? { agentNote: next.agentNote } : base.agentNote ? { agentNote: base.agentNote } : {}),
|
|
371
|
-
...((base.conflicts?.length ?? 0) + (next.conflicts?.length ?? 0) > 0
|
|
372
|
-
? { conflicts: [...(base.conflicts ?? []), ...(next.conflicts ?? [])] }
|
|
373
|
-
: {}),
|
|
374
|
-
...pickDefined(next, base, 'approvedBy'),
|
|
375
|
-
...pickDefined(next, base, 'approvedAt'),
|
|
376
|
-
...pickDefined(next, base, 'approvalMessageId'),
|
|
377
|
-
...pickDefined(next, base, 'approvalNote'),
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
export function mergeStateDelta(
|
|
382
|
-
existingState: WorkstreamState,
|
|
383
|
-
delta: WorkstreamStateDelta,
|
|
384
|
-
now: () => number,
|
|
385
|
-
): WorkstreamState {
|
|
386
|
-
const timestamp = now()
|
|
387
|
-
const state: WorkstreamState = {
|
|
388
|
-
...existingState,
|
|
389
|
-
currentPlan: existingState.currentPlan ? { ...existingState.currentPlan } : null,
|
|
390
|
-
activeConstraints: [...existingState.activeConstraints],
|
|
391
|
-
keyDecisions: [...existingState.keyDecisions],
|
|
392
|
-
tasks: [...existingState.tasks],
|
|
393
|
-
openQuestions: [...existingState.openQuestions],
|
|
394
|
-
risks: [...existingState.risks],
|
|
395
|
-
artifacts: [...existingState.artifacts],
|
|
396
|
-
agentContributions: [...existingState.agentContributions],
|
|
397
|
-
lastUpdated: timestamp,
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
if (delta.currentPlan !== undefined) {
|
|
401
|
-
if (delta.currentPlan === null) {
|
|
402
|
-
state.currentPlan = null
|
|
403
|
-
} else {
|
|
404
|
-
const planText = sanitizeStateText(delta.currentPlan)
|
|
405
|
-
if (planText) {
|
|
406
|
-
const planId = createStableId('plan', planText)
|
|
407
|
-
const existingPlan = state.currentPlan && state.currentPlan.id === planId ? state.currentPlan : null
|
|
408
|
-
state.currentPlan = {
|
|
409
|
-
id: planId,
|
|
410
|
-
text: planText,
|
|
411
|
-
source: 'agent',
|
|
412
|
-
approved: existingPlan?.approved ?? false,
|
|
413
|
-
timestamp,
|
|
414
|
-
sourceMessageIds: existingPlan?.sourceMessageIds ?? [],
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
if (delta.newConstraints?.length) {
|
|
421
|
-
for (const rawConstraint of delta.newConstraints) {
|
|
422
|
-
const text = sanitizeStateText(rawConstraint)
|
|
423
|
-
if (!text) continue
|
|
424
|
-
const constraintId = createStableId('constraint', text)
|
|
425
|
-
const exists = state.activeConstraints.some((constraint) => constraint.id === constraintId)
|
|
426
|
-
if (exists) continue
|
|
427
|
-
state.activeConstraints.push({
|
|
428
|
-
id: constraintId,
|
|
429
|
-
text,
|
|
430
|
-
source: 'agent',
|
|
431
|
-
approved: false,
|
|
432
|
-
timestamp,
|
|
433
|
-
sourceMessageIds: [],
|
|
434
|
-
})
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
if (delta.newRisks?.length) {
|
|
439
|
-
state.risks = appendUnique(state.risks, delta.newRisks)
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
if (delta.resolvedQuestions?.length) {
|
|
443
|
-
const resolvedIds = new Set(
|
|
444
|
-
delta.resolvedQuestions
|
|
445
|
-
.map((question) => sanitizeStateText(question))
|
|
446
|
-
.filter((question): question is string => Boolean(question))
|
|
447
|
-
.map((question) => createStableId('question', question)),
|
|
448
|
-
)
|
|
449
|
-
state.openQuestions = state.openQuestions.filter((question) => !resolvedIds.has(question.id))
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
if (delta.newQuestions?.length) {
|
|
453
|
-
for (const rawQuestion of delta.newQuestions) {
|
|
454
|
-
const text = sanitizeStateText(rawQuestion)
|
|
455
|
-
if (!text) continue
|
|
456
|
-
const questionId = createStableId('question', text)
|
|
457
|
-
const exists = state.openQuestions.some((question) => question.id === questionId)
|
|
458
|
-
if (exists) continue
|
|
459
|
-
state.openQuestions.push({ id: questionId, text, source: 'agent', timestamp, sourceMessageIds: [] })
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
if (delta.newDecisions?.length) {
|
|
464
|
-
for (const decision of delta.newDecisions) {
|
|
465
|
-
const normalizedDecision = sanitizeStateText(decision.decision)
|
|
466
|
-
const normalizedRationale = sanitizeStateText(decision.rationale)
|
|
467
|
-
if (!normalizedDecision || !normalizedRationale) continue
|
|
468
|
-
|
|
469
|
-
const sourceIds = [...new Set(decision.sourceMessageIds.map((id) => compactWhitespace(id)).filter(Boolean))]
|
|
470
|
-
const decisionId = createStableId('decision', normalizedDecision, normalizedRationale, sourceIds.sort().join('|'))
|
|
471
|
-
const alreadyExists = state.keyDecisions.some((item) => item.id === decisionId)
|
|
472
|
-
if (alreadyExists) continue
|
|
473
|
-
state.keyDecisions.push({
|
|
474
|
-
id: decisionId,
|
|
475
|
-
decision: normalizedDecision,
|
|
476
|
-
rationale: normalizedRationale,
|
|
477
|
-
agent: compactWhitespace(decision.agent),
|
|
478
|
-
sourceMessageIds: sourceIds,
|
|
479
|
-
confidence: decision.confidence,
|
|
480
|
-
timestamp,
|
|
481
|
-
})
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
if (delta.taskUpdates?.length) {
|
|
486
|
-
for (const update of delta.taskUpdates) {
|
|
487
|
-
const title = sanitizeStateText(update.title)
|
|
488
|
-
if (!title) continue
|
|
489
|
-
|
|
490
|
-
const externalId = sanitizeStateText(update.externalId ?? '')
|
|
491
|
-
const owner = compactWhitespace(update.owner)
|
|
492
|
-
const taskId = externalId ? createStableId('task-external', externalId) : createStableId('task', title, owner)
|
|
493
|
-
const sourceMessageIds = [...new Set(update.sourceMessageIds.map((id) => compactWhitespace(id)).filter(Boolean))]
|
|
494
|
-
const existingIndex = state.tasks.findIndex((task) => task.id === taskId)
|
|
495
|
-
const nextTask = {
|
|
496
|
-
id: taskId,
|
|
497
|
-
title,
|
|
498
|
-
status: update.status,
|
|
499
|
-
owner,
|
|
500
|
-
...(externalId ? { externalId } : {}),
|
|
501
|
-
source: 'agent' as const,
|
|
502
|
-
sourceMessageIds,
|
|
503
|
-
timestamp,
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
if (existingIndex >= 0) {
|
|
507
|
-
const existingTask = state.tasks[existingIndex]
|
|
508
|
-
state.tasks[existingIndex] = {
|
|
509
|
-
...nextTask,
|
|
510
|
-
sourceMessageIds: [...new Set([...existingTask.sourceMessageIds, ...sourceMessageIds])],
|
|
511
|
-
}
|
|
512
|
-
} else {
|
|
513
|
-
state.tasks.push(nextTask)
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
if (delta.artifacts?.length) {
|
|
519
|
-
for (const artifact of delta.artifacts) {
|
|
520
|
-
const name = sanitizeStateText(artifact.name)
|
|
521
|
-
const pointer = sanitizeStateText(artifact.pointer)
|
|
522
|
-
if (!name || !pointer) continue
|
|
523
|
-
|
|
524
|
-
const artifactId = createStableId('artifact', name, pointer)
|
|
525
|
-
const exists = state.artifacts.some((item) => item.id === artifactId)
|
|
526
|
-
if (exists) continue
|
|
527
|
-
state.artifacts.push({ id: artifactId, name, type: compactWhitespace(artifact.type), pointer, timestamp })
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
if (delta.agentNote) {
|
|
532
|
-
const agent = sanitizeStateText(delta.agentNote.agent)
|
|
533
|
-
const summary = sanitizeStateText(delta.agentNote.summary)
|
|
534
|
-
if (agent && summary) {
|
|
535
|
-
const noteId = createStableId('agent-note', agent, summary)
|
|
536
|
-
const exists = state.agentContributions.some((note) => note.id === noteId)
|
|
537
|
-
if (!exists) {
|
|
538
|
-
state.agentContributions.push({ id: noteId, agent, summary, timestamp })
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
if (delta.conflicts?.length) {
|
|
544
|
-
for (const conflict of delta.conflicts) {
|
|
545
|
-
const text = sanitizeStateText(`Conflict: ${conflict.recommendation}`)
|
|
546
|
-
if (!text) continue
|
|
547
|
-
const questionId = createStableId('question', text)
|
|
548
|
-
const exists = state.openQuestions.some((question) => question.id === questionId)
|
|
549
|
-
if (exists) continue
|
|
550
|
-
state.openQuestions.push({ id: questionId, text, source: 'agent', timestamp, sourceMessageIds: [] })
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
if (delta.approvedBy !== undefined) {
|
|
555
|
-
state.approvedBy = sanitizeStateText(delta.approvedBy) ?? undefined
|
|
166
|
+
if (current.length > 0) {
|
|
167
|
+
chunks.push(current)
|
|
556
168
|
}
|
|
557
|
-
if (delta.approvedAt !== undefined) {
|
|
558
|
-
state.approvedAt = delta.approvedAt ?? undefined
|
|
559
|
-
}
|
|
560
|
-
if (delta.approvalMessageId !== undefined) {
|
|
561
|
-
state.approvalMessageId = sanitizeStateText(delta.approvalMessageId) ?? undefined
|
|
562
|
-
}
|
|
563
|
-
if (delta.approvalNote !== undefined) {
|
|
564
|
-
state.approvalNote = sanitizeStateText(delta.approvalNote) ?? undefined
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
applyWorkstreamStateCaps(state)
|
|
568
|
-
|
|
569
|
-
return WorkstreamStateSchema.parse(state)
|
|
570
|
-
}
|
|
571
169
|
|
|
572
|
-
|
|
573
|
-
createSummaryMessage: (summaryText: string) => ChatMessage | null
|
|
574
|
-
prependSummaryMessage: (summaryText: string, liveMessages: ChatMessage[]) => ChatMessage[]
|
|
575
|
-
formatWorkstreamStateForPrompt: (state: WorkstreamState | null | undefined) => string | undefined
|
|
576
|
-
estimateThreshold: (contextSize?: number) => number
|
|
577
|
-
shouldCompactHistory: (params: {
|
|
578
|
-
summaryText: string
|
|
579
|
-
liveMessages: ChatMessage[]
|
|
580
|
-
contextSize?: number
|
|
581
|
-
}) => CompactionAssessment
|
|
582
|
-
compactHistory: (params: CompactHistoryParams) => Promise<CompactHistoryResult>
|
|
170
|
+
return chunks
|
|
583
171
|
}
|
|
584
172
|
|
|
585
173
|
export function buildContextCompactionPrompt(params: ContextCompactionPromptParams): string {
|
|
@@ -588,24 +176,22 @@ export function buildContextCompactionPrompt(params: ContextCompactionPromptPara
|
|
|
588
176
|
'<previous-summary>',
|
|
589
177
|
params.previousSummary.trim() || 'None',
|
|
590
178
|
'</previous-summary>',
|
|
591
|
-
'<existing-workstream-state>',
|
|
592
|
-
JSON.stringify(buildExistingWorkstreamStateForCompactionPrompt(params.existingState)),
|
|
593
|
-
'</existing-workstream-state>',
|
|
594
179
|
'<new-messages>',
|
|
595
180
|
params.transcript || 'None',
|
|
596
181
|
'</new-messages>',
|
|
597
182
|
'</context-compaction-input>',
|
|
598
183
|
'',
|
|
599
|
-
'Produce
|
|
600
|
-
'
|
|
601
|
-
'
|
|
184
|
+
'Produce one concise replacement summary for the compacted history.',
|
|
185
|
+
'Preserve durable facts, decisions, blockers, ownership, numbers, and unresolved questions.',
|
|
186
|
+
'Do not invent details or preserve filler.',
|
|
187
|
+
'Return plain JSON with one field: summary.',
|
|
188
|
+
'Summary format must use these sections in order:',
|
|
602
189
|
'KEY FACTS:',
|
|
603
190
|
'- ...',
|
|
604
191
|
'CONVERSATION FLOW:',
|
|
605
192
|
'- ...',
|
|
606
193
|
'OPEN THREADS:',
|
|
607
194
|
'- ...',
|
|
608
|
-
'Return every stateDelta field. Use empty arrays for unchanged list fields, null for unchanged nullable fields, and currentPlan.action to signal unchanged, clear, or set.',
|
|
609
195
|
].join('\n')
|
|
610
196
|
}
|
|
611
197
|
|
|
@@ -683,101 +269,11 @@ export function createContextCompactionRuntime(
|
|
|
683
269
|
return chunks.join('\n').trim()
|
|
684
270
|
}
|
|
685
271
|
|
|
686
|
-
const toContextMessageFromChatMessage = (message: ChatMessage): ContextMessage => {
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
if (!state) return undefined
|
|
692
|
-
|
|
693
|
-
// Skip serialization when all fields are empty
|
|
694
|
-
const hasContent =
|
|
695
|
-
(state.currentPlan !== null && state.currentPlan !== undefined) ||
|
|
696
|
-
state.activeConstraints.length > 0 ||
|
|
697
|
-
state.keyDecisions.length > 0 ||
|
|
698
|
-
state.openQuestions.length > 0 ||
|
|
699
|
-
state.risks.length > 0 ||
|
|
700
|
-
state.tasks.length > 0 ||
|
|
701
|
-
state.artifacts.length > 0 ||
|
|
702
|
-
state.agentContributions.length > 0
|
|
703
|
-
if (!hasContent) return undefined
|
|
704
|
-
|
|
705
|
-
const approvedPlan =
|
|
706
|
-
state.currentPlan && state.currentPlan.approved ? sanitizeStateText(state.currentPlan.text) : null
|
|
707
|
-
const candidatePlan =
|
|
708
|
-
state.currentPlan && !state.currentPlan.approved ? sanitizeStateText(state.currentPlan.text) : null
|
|
709
|
-
|
|
710
|
-
const approvedConstraints = sanitizeTextFieldItems(
|
|
711
|
-
state.activeConstraints.filter((constraint) => constraint.approved),
|
|
712
|
-
'text',
|
|
713
|
-
)
|
|
714
|
-
|
|
715
|
-
const candidateConstraints = sanitizeTextFieldItems(
|
|
716
|
-
state.activeConstraints.filter((constraint) => !constraint.approved),
|
|
717
|
-
'text',
|
|
718
|
-
)
|
|
719
|
-
|
|
720
|
-
const openQuestions = sanitizeStateStrings(state.openQuestions.map((question) => question.text))
|
|
721
|
-
|
|
722
|
-
const decisions = sanitizeAndMapStateField(state.keyDecisions, (decision) => {
|
|
723
|
-
const normalizedDecision = sanitizeStateText(decision.decision)
|
|
724
|
-
const rationale = sanitizeStateText(decision.rationale)
|
|
725
|
-
if (!normalizedDecision || !rationale) return null
|
|
726
|
-
return { agent: decision.agent, decision: normalizedDecision, rationale, confidence: decision.confidence }
|
|
727
|
-
})
|
|
728
|
-
|
|
729
|
-
const tasks = sanitizeAndMapStateField(state.tasks, (task) => {
|
|
730
|
-
const title = sanitizeStateText(task.title)
|
|
731
|
-
const owner = sanitizeStateText(task.owner)
|
|
732
|
-
if (!title || !owner) return null
|
|
733
|
-
return { title, status: task.status, owner, externalId: task.externalId, source: task.source }
|
|
734
|
-
})
|
|
735
|
-
|
|
736
|
-
const artifacts = sanitizeAndMapStateField(state.artifacts, (artifact) => {
|
|
737
|
-
const name = sanitizeStateText(artifact.name)
|
|
738
|
-
const pointer = sanitizeStateText(artifact.pointer)
|
|
739
|
-
if (!name || !pointer) return null
|
|
740
|
-
return { name, type: artifact.type, pointer }
|
|
741
|
-
})
|
|
742
|
-
|
|
743
|
-
const agentContributions = sanitizeAndMapStateField(state.agentContributions, (note) => {
|
|
744
|
-
const summary = sanitizeStateText(note.summary)
|
|
745
|
-
if (!summary) return null
|
|
746
|
-
return { agent: note.agent, summary }
|
|
747
|
-
})
|
|
748
|
-
|
|
749
|
-
const payload = {
|
|
750
|
-
policy: { approvedConstraintsAreBinding: true, candidateStateIsAdvisoryOnly: true },
|
|
751
|
-
approved: {
|
|
752
|
-
currentPlan: approvedPlan ? { text: approvedPlan, source: state.currentPlan?.source } : null,
|
|
753
|
-
constraints: approvedConstraints.map((constraint) => ({
|
|
754
|
-
id: constraint.id,
|
|
755
|
-
text: constraint.text,
|
|
756
|
-
source: constraint.source,
|
|
757
|
-
})),
|
|
758
|
-
},
|
|
759
|
-
candidate: {
|
|
760
|
-
currentPlan: candidatePlan ? { text: candidatePlan, source: state.currentPlan?.source } : null,
|
|
761
|
-
constraints: candidateConstraints.map((constraint) => ({
|
|
762
|
-
id: constraint.id,
|
|
763
|
-
text: constraint.text,
|
|
764
|
-
source: constraint.source,
|
|
765
|
-
})),
|
|
766
|
-
},
|
|
767
|
-
decisions,
|
|
768
|
-
openQuestions,
|
|
769
|
-
risks: sanitizeStateStrings(state.risks),
|
|
770
|
-
tasks,
|
|
771
|
-
artifacts,
|
|
772
|
-
agentContributions,
|
|
773
|
-
advisory: {
|
|
774
|
-
approvedBy: state.approvedBy ? sanitizeStateText(state.approvedBy) : null,
|
|
775
|
-
approvalNote: state.approvalNote ? sanitizeStateText(state.approvalNote) : null,
|
|
776
|
-
},
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
return ['<workstream-state>', JSON.stringify(payload, null, 2), '</workstream-state>'].join('\n')
|
|
780
|
-
}
|
|
272
|
+
const toContextMessageFromChatMessage = (message: ChatMessage): ContextMessage => ({
|
|
273
|
+
role: message.role,
|
|
274
|
+
text: toContextTextFromChatMessage(message),
|
|
275
|
+
sourceMessageId: message.id,
|
|
276
|
+
})
|
|
781
277
|
|
|
782
278
|
const createSummaryMessage = (summaryText: string): ChatMessage | null => {
|
|
783
279
|
const summary = normalizeSummary(summaryText)
|
|
@@ -818,53 +314,36 @@ export function createContextCompactionRuntime(
|
|
|
818
314
|
|
|
819
315
|
const compactContextMessages = async (params: {
|
|
820
316
|
previousSummary: string
|
|
821
|
-
existingState: WorkstreamState
|
|
822
317
|
newMessages: ContextMessage[]
|
|
823
318
|
}): Promise<CompactionOutput> => {
|
|
824
319
|
const chunks = splitByCharBudget(params.newMessages, compactionChunkMaxChars)
|
|
825
320
|
let summary = normalizeSummary(params.previousSummary)
|
|
826
|
-
let delta: WorkstreamStateDelta = {}
|
|
827
|
-
let currentState = params.existingState
|
|
828
321
|
|
|
829
322
|
for (const chunk of chunks) {
|
|
830
323
|
const transcript = toCompactionTranscript(chunk)
|
|
831
|
-
const output = await options.runCompacter({
|
|
832
|
-
previousSummary: summary,
|
|
833
|
-
existingState: currentState,
|
|
834
|
-
chunk,
|
|
835
|
-
transcript,
|
|
836
|
-
})
|
|
324
|
+
const output = await options.runCompacter({ previousSummary: summary, chunk, transcript })
|
|
837
325
|
summary = normalizeSummary(output.summary)
|
|
838
|
-
const parsedDelta = WorkstreamStateDeltaSchema.parse(output.stateDelta)
|
|
839
|
-
delta = mergeDelta(delta, parsedDelta)
|
|
840
|
-
currentState = mergeStateDelta(currentState, parsedDelta, now)
|
|
841
326
|
}
|
|
842
327
|
|
|
843
|
-
return { summary
|
|
328
|
+
return { summary }
|
|
844
329
|
}
|
|
845
330
|
|
|
846
|
-
const rollupSummaryIfOversized = async (
|
|
847
|
-
summary: string,
|
|
848
|
-
state: WorkstreamState,
|
|
849
|
-
): Promise<{ summary: string; state: WorkstreamState; output: CompactionOutput }> => {
|
|
331
|
+
const rollupSummaryIfOversized = async (summary: string): Promise<string> => {
|
|
850
332
|
if (estimateTokens(summary) <= summaryRollupMaxTokens) {
|
|
851
|
-
return
|
|
333
|
+
return summary
|
|
852
334
|
}
|
|
853
335
|
|
|
854
336
|
const output = await compactContextMessages({
|
|
855
337
|
previousSummary: '',
|
|
856
|
-
existingState: state,
|
|
857
338
|
newMessages: [{ role: 'assistant', text: summary, sourceMessageId: 'summary-rollup' }],
|
|
858
339
|
})
|
|
859
340
|
|
|
860
|
-
return
|
|
341
|
+
return normalizeSummary(output.summary)
|
|
861
342
|
}
|
|
862
343
|
|
|
863
344
|
const compactHistory = async (params: CompactHistoryParams): Promise<CompactHistoryResult> => {
|
|
864
345
|
let summaryText = normalizeSummary(params.summaryText)
|
|
865
346
|
let remainingMessages = [...params.liveMessages]
|
|
866
|
-
let state = params.existingState
|
|
867
|
-
let mergedDelta: WorkstreamStateDelta = {}
|
|
868
347
|
let compactedMessages: ChatMessage[] = []
|
|
869
348
|
let lastCompactedMessageId: string | undefined
|
|
870
349
|
const summaryPayload = buildSyntheticSummaryPayload(summaryText)
|
|
@@ -884,8 +363,6 @@ export function createContextCompactionRuntime(
|
|
|
884
363
|
estimatedTokens,
|
|
885
364
|
inputChars,
|
|
886
365
|
outputChars: outputPayload.length,
|
|
887
|
-
state,
|
|
888
|
-
stateDelta: mergedDelta,
|
|
889
366
|
}
|
|
890
367
|
}
|
|
891
368
|
|
|
@@ -907,36 +384,25 @@ export function createContextCompactionRuntime(
|
|
|
907
384
|
|
|
908
385
|
const candidatePrefix = remainingMessages.slice(0, boundary)
|
|
909
386
|
const messagesToCompact = candidatePrefix.filter((message) => !readIsCompacted(message))
|
|
910
|
-
const contextMessages = messagesToCompact
|
|
387
|
+
const contextMessages = messagesToCompact
|
|
388
|
+
.map(toContextMessageFromChatMessage)
|
|
389
|
+
.filter((message) => compactWhitespace(message.text).length > 0)
|
|
911
390
|
const sourceText = toCompactionTranscript(contextMessages)
|
|
912
391
|
|
|
913
392
|
if (!compactWhitespace(sourceText)) {
|
|
914
393
|
return buildEarlyExitResult(assessment.estimatedTokens)
|
|
915
394
|
}
|
|
916
395
|
|
|
917
|
-
let
|
|
918
|
-
previousSummary: summaryText,
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
})
|
|
922
|
-
|
|
923
|
-
let nextSummary = normalizeSummary(compactionOutput.summary)
|
|
924
|
-
let nextState = mergeStateDelta(state, compactionOutput.stateDelta, now)
|
|
925
|
-
|
|
926
|
-
if (estimateTokens(nextSummary) > summaryRollupMaxTokens) {
|
|
927
|
-
const rollup = await rollupSummaryIfOversized(nextSummary, nextState)
|
|
928
|
-
nextSummary = rollup.summary
|
|
929
|
-
nextState = rollup.state
|
|
930
|
-
compactionOutput = rollup.output
|
|
931
|
-
}
|
|
396
|
+
let nextSummary = normalizeSummary(
|
|
397
|
+
(await compactContextMessages({ previousSummary: summaryText, newMessages: contextMessages })).summary,
|
|
398
|
+
)
|
|
399
|
+
nextSummary = await rollupSummaryIfOversized(nextSummary)
|
|
932
400
|
|
|
933
401
|
if (nextSummary.length >= sourceText.length) {
|
|
934
402
|
throw new Error('Compaction summary is not shorter than compacted source')
|
|
935
403
|
}
|
|
936
404
|
|
|
937
405
|
summaryText = nextSummary
|
|
938
|
-
state = nextState
|
|
939
|
-
mergedDelta = mergeDelta(mergedDelta, compactionOutput.stateDelta)
|
|
940
406
|
compactedMessages = [
|
|
941
407
|
...compactedMessages,
|
|
942
408
|
...candidatePrefix.map((message) => markMessageCompacted(message, now)),
|
|
@@ -950,18 +416,10 @@ export function createContextCompactionRuntime(
|
|
|
950
416
|
}
|
|
951
417
|
}
|
|
952
418
|
|
|
953
|
-
return {
|
|
954
|
-
createSummaryMessage,
|
|
955
|
-
prependSummaryMessage,
|
|
956
|
-
formatWorkstreamStateForPrompt,
|
|
957
|
-
estimateThreshold,
|
|
958
|
-
shouldCompactHistory,
|
|
959
|
-
compactHistory,
|
|
960
|
-
}
|
|
419
|
+
return { createSummaryMessage, prependSummaryMessage, estimateThreshold, shouldCompactHistory, compactHistory }
|
|
961
420
|
}
|
|
962
421
|
|
|
963
422
|
export function parseCompactionOutput(value: unknown): CompactionOutput {
|
|
964
|
-
const parsed =
|
|
965
|
-
|
|
966
|
-
return { summary: parsed.summary, stateDelta: parseStructuredWorkstreamStateDelta(parsed.stateDelta) }
|
|
423
|
+
const parsed = ContextCompactionOutputSchema.parse(value)
|
|
424
|
+
return { summary: parsed.summary }
|
|
967
425
|
}
|