@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.
@@ -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
- const PROMPT_INJECTION_PATTERN =
93
- /\b(ignore (all )?(previous|prior|system|developer)? instructions?|system prompt|developer prompt|tool override|jailbreak|role ?override|do not follow|bypass)\b/i
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 sanitizeStateText(value: string | null | undefined): string | null {
100
- if (typeof value !== 'string') return null
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) chunks.push(current)
342
- return chunks
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
- export interface ContextCompactionRuntime {
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 a concise replacement summary and a structured state delta.',
600
- 'Only include facts supported by the new messages.',
601
- 'Summary format is required and must use exactly these sections in order:',
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
- return { role: message.role, text: toContextTextFromChatMessage(message), sourceMessageId: message.id }
688
- }
689
-
690
- const formatWorkstreamStateForPrompt = (state: WorkstreamState | null | undefined): string | undefined => {
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, stateDelta: delta }
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 { summary, state, output: { summary, stateDelta: {} } }
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 { summary: normalizeSummary(output.summary), state: mergeStateDelta(state, output.stateDelta, now), output }
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.map(toContextMessageFromChatMessage)
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 compactionOutput = await compactContextMessages({
918
- previousSummary: summaryText,
919
- existingState: state,
920
- newMessages: contextMessages,
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 = StructuredCompactionOutputSchema.parse(value)
965
-
966
- return { summary: parsed.summary, stateDelta: parseStructuredWorkstreamStateDelta(parsed.stateDelta) }
423
+ const parsed = ContextCompactionOutputSchema.parse(value)
424
+ return { summary: parsed.summary }
967
425
  }