@lota-sdk/core 0.1.5
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 +55 -0
- package/infrastructure/schema/01_memory.surql +47 -0
- package/infrastructure/schema/02_execution_plan.surql +62 -0
- package/infrastructure/schema/03_learned_skill.surql +32 -0
- package/infrastructure/schema/04_runtime_bootstrap.surql +8 -0
- package/package.json +128 -0
- package/src/ai/definitions.ts +308 -0
- package/src/bifrost/bifrost.ts +256 -0
- package/src/config/agent-defaults.ts +99 -0
- package/src/config/constants.ts +33 -0
- package/src/config/env-shapes.ts +122 -0
- package/src/config/logger.ts +29 -0
- package/src/config/model-constants.ts +31 -0
- package/src/config/search.ts +17 -0
- package/src/config/workstream-defaults.ts +68 -0
- package/src/db/base.service.ts +55 -0
- package/src/db/cursor-pagination.ts +73 -0
- package/src/db/memory-query-builder.ts +207 -0
- package/src/db/memory-store.helpers.ts +118 -0
- package/src/db/memory-store.rows.ts +29 -0
- package/src/db/memory-store.ts +974 -0
- package/src/db/memory-types.ts +193 -0
- package/src/db/memory.ts +505 -0
- package/src/db/record-id.ts +78 -0
- package/src/db/service.ts +932 -0
- package/src/db/startup.ts +152 -0
- package/src/db/tables.ts +20 -0
- package/src/document/org-document-chunking.ts +224 -0
- package/src/document/parsing.ts +40 -0
- package/src/embeddings/provider.ts +76 -0
- package/src/index.ts +302 -0
- package/src/queues/context-compaction.queue.ts +82 -0
- package/src/queues/document-processor.queue.ts +118 -0
- package/src/queues/memory-consolidation.queue.ts +65 -0
- package/src/queues/post-chat-memory.queue.ts +128 -0
- package/src/queues/recent-activity-title-refinement.queue.ts +69 -0
- package/src/queues/regular-chat-memory-digest.config.ts +12 -0
- package/src/queues/regular-chat-memory-digest.queue.ts +73 -0
- package/src/queues/skill-extraction.config.ts +9 -0
- package/src/queues/skill-extraction.queue.ts +62 -0
- package/src/redis/connection.ts +176 -0
- package/src/redis/index.ts +30 -0
- package/src/redis/org-memory-lock.ts +43 -0
- package/src/redis/redis-lease-lock.ts +158 -0
- package/src/runtime/agent-contract.ts +1 -0
- package/src/runtime/agent-prompt-context.ts +119 -0
- package/src/runtime/agent-runtime-policy.ts +192 -0
- package/src/runtime/agent-stream-helpers.ts +117 -0
- package/src/runtime/agent-types.ts +22 -0
- package/src/runtime/approval-continuation.ts +16 -0
- package/src/runtime/chat-attachments.ts +46 -0
- package/src/runtime/chat-message.ts +10 -0
- package/src/runtime/chat-request-routing.ts +21 -0
- package/src/runtime/chat-run-orchestration.ts +25 -0
- package/src/runtime/chat-run-registry.ts +20 -0
- package/src/runtime/chat-types.ts +18 -0
- package/src/runtime/context-compaction-constants.ts +11 -0
- package/src/runtime/context-compaction-runtime.ts +86 -0
- package/src/runtime/context-compaction.ts +909 -0
- package/src/runtime/execution-plan.ts +59 -0
- package/src/runtime/helper-model.ts +405 -0
- package/src/runtime/indexed-repositories-policy.ts +28 -0
- package/src/runtime/instruction-sections.ts +8 -0
- package/src/runtime/llm-content.ts +71 -0
- package/src/runtime/memory-block.ts +264 -0
- package/src/runtime/memory-digest-policy.ts +14 -0
- package/src/runtime/memory-format.ts +8 -0
- package/src/runtime/memory-pipeline.ts +570 -0
- package/src/runtime/memory-prompts-fact.ts +47 -0
- package/src/runtime/memory-prompts-parse.ts +3 -0
- package/src/runtime/memory-prompts-update.ts +37 -0
- package/src/runtime/memory-scope.ts +43 -0
- package/src/runtime/plugin-types.ts +10 -0
- package/src/runtime/retrieval-adapters.ts +25 -0
- package/src/runtime/retrieval-pipeline.ts +3 -0
- package/src/runtime/runtime-extensions.ts +154 -0
- package/src/runtime/skill-extraction-policy.ts +3 -0
- package/src/runtime/team-consultation-orchestrator.ts +245 -0
- package/src/runtime/team-consultation-prompts.ts +32 -0
- package/src/runtime/title-helpers.ts +12 -0
- package/src/runtime/turn-lifecycle.ts +28 -0
- package/src/runtime/workstream-chat-helpers.ts +187 -0
- package/src/runtime/workstream-routing-policy.ts +301 -0
- package/src/runtime/workstream-state.ts +261 -0
- package/src/services/attachment.service.ts +159 -0
- package/src/services/chat-attachments.service.ts +17 -0
- package/src/services/chat-run-registry.service.ts +3 -0
- package/src/services/context-compaction-runtime.ts +13 -0
- package/src/services/context-compaction.service.ts +115 -0
- package/src/services/document-chunk.service.ts +141 -0
- package/src/services/execution-plan.service.ts +890 -0
- package/src/services/learned-skill.service.ts +328 -0
- package/src/services/memory-assessment.service.ts +43 -0
- package/src/services/memory.service.ts +807 -0
- package/src/services/memory.utils.ts +84 -0
- package/src/services/mutating-approval.service.ts +110 -0
- package/src/services/recent-activity-title.service.ts +74 -0
- package/src/services/recent-activity.service.ts +397 -0
- package/src/services/workstream-change-tracker.service.ts +313 -0
- package/src/services/workstream-message.service.ts +283 -0
- package/src/services/workstream-title.service.ts +58 -0
- package/src/services/workstream-turn-preparation.ts +1340 -0
- package/src/services/workstream-turn.ts +37 -0
- package/src/services/workstream.service.ts +854 -0
- package/src/services/workstream.types.ts +118 -0
- package/src/storage/attachment-parser.ts +101 -0
- package/src/storage/attachment-storage.service.ts +391 -0
- package/src/storage/attachments.types.ts +11 -0
- package/src/storage/attachments.utils.ts +58 -0
- package/src/storage/generated-document-storage.service.ts +55 -0
- package/src/system-agents/agent-result.ts +27 -0
- package/src/system-agents/context-compacter.agent.ts +46 -0
- package/src/system-agents/delegated-agent-factory.ts +177 -0
- package/src/system-agents/helper-agent-options.ts +20 -0
- package/src/system-agents/memory-reranker.agent.ts +38 -0
- package/src/system-agents/memory.agent.ts +58 -0
- package/src/system-agents/recent-activity-title-refiner.agent.ts +53 -0
- package/src/system-agents/regular-chat-memory-digest.agent.ts +75 -0
- package/src/system-agents/researcher.agent.ts +34 -0
- package/src/system-agents/skill-extractor.agent.ts +88 -0
- package/src/system-agents/skill-manager.agent.ts +80 -0
- package/src/system-agents/title-generator.agent.ts +42 -0
- package/src/system-agents/workstream-tracker.agent.ts +58 -0
- package/src/tools/execution-plan.tool.ts +163 -0
- package/src/tools/fetch-webpage.tool.ts +132 -0
- package/src/tools/firecrawl-client.ts +12 -0
- package/src/tools/memory-block.tool.ts +55 -0
- package/src/tools/read-file-parts.tool.ts +80 -0
- package/src/tools/remember-memory.tool.ts +85 -0
- package/src/tools/research-topic.tool.ts +15 -0
- package/src/tools/search-tools.ts +55 -0
- package/src/tools/search-web.tool.ts +175 -0
- package/src/tools/team-think.tool.ts +125 -0
- package/src/tools/tool-contract.ts +21 -0
- package/src/tools/user-questions.tool.ts +18 -0
- package/src/utils/async.ts +50 -0
- package/src/utils/date-time.ts +34 -0
- package/src/utils/error.ts +10 -0
- package/src/utils/errors.ts +28 -0
- package/src/utils/hono-error-handler.ts +71 -0
- package/src/utils/string.ts +51 -0
- package/src/workers/bootstrap.ts +44 -0
- package/src/workers/memory-consolidation.worker.ts +318 -0
- package/src/workers/regular-chat-memory-digest.helpers.ts +100 -0
- package/src/workers/regular-chat-memory-digest.runner.ts +363 -0
- package/src/workers/regular-chat-memory-digest.worker.ts +22 -0
- package/src/workers/skill-extraction.runner.ts +331 -0
- package/src/workers/skill-extraction.worker.ts +22 -0
- package/src/workers/utils/repo-indexer-chunker.ts +331 -0
- package/src/workers/utils/repo-structure-extractor.ts +645 -0
- package/src/workers/utils/repomix-process-concurrency.ts +65 -0
- package/src/workers/utils/sandbox-error.ts +5 -0
- package/src/workers/worker-utils.ts +182 -0
|
@@ -0,0 +1,909 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
import type { ChatMessage } from '@lota-sdk/shared/schemas/chat-message'
|
|
4
|
+
|
|
5
|
+
import { readString } from '../utils/string'
|
|
6
|
+
import {
|
|
7
|
+
COMPACTION_CHUNK_MAX_CHARS,
|
|
8
|
+
CONTEXT_COMPACTION_INCLUDED_TOOL_NAMES,
|
|
9
|
+
CONTEXT_COMPACTION_INCLUDED_TOOL_PREFIXES,
|
|
10
|
+
CONTEXT_COMPACTION_THRESHOLD_RATIO,
|
|
11
|
+
CONTEXT_OUTPUT_RESERVE_TOKENS,
|
|
12
|
+
CONTEXT_SAFETY_MARGIN_TOKENS,
|
|
13
|
+
SUMMARY_ROLLUP_MAX_TOKENS,
|
|
14
|
+
} from './context-compaction-constants'
|
|
15
|
+
import {
|
|
16
|
+
StructuredCompactionOutputSchema,
|
|
17
|
+
WorkstreamStateDeltaSchema,
|
|
18
|
+
WORKSTREAM_STATE_MAX_ACTIVE_CONSTRAINTS,
|
|
19
|
+
WORKSTREAM_STATE_MAX_AGENT_CONTRIBUTIONS,
|
|
20
|
+
WORKSTREAM_STATE_MAX_ARTIFACTS,
|
|
21
|
+
WORKSTREAM_STATE_MAX_KEY_DECISIONS,
|
|
22
|
+
WORKSTREAM_STATE_MAX_OPEN_QUESTIONS,
|
|
23
|
+
WORKSTREAM_STATE_MAX_RISKS,
|
|
24
|
+
WORKSTREAM_STATE_MAX_TASKS,
|
|
25
|
+
WorkstreamStateSchema,
|
|
26
|
+
createEmptyWorkstreamState,
|
|
27
|
+
parseStructuredWorkstreamStateDelta,
|
|
28
|
+
} from './workstream-state'
|
|
29
|
+
import type { CompactionOutput, WorkstreamState, WorkstreamStateDelta } from './workstream-state'
|
|
30
|
+
|
|
31
|
+
export interface ContextMessage {
|
|
32
|
+
role: 'system' | 'user' | 'assistant'
|
|
33
|
+
text: string
|
|
34
|
+
sourceMessageId: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CompactionAssessment {
|
|
38
|
+
estimatedTokens: number
|
|
39
|
+
threshold: number
|
|
40
|
+
shouldCompact: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface CompactHistoryParams {
|
|
44
|
+
summaryText: string
|
|
45
|
+
liveMessages: ChatMessage[]
|
|
46
|
+
tailMessageCount: number
|
|
47
|
+
contextSize?: number
|
|
48
|
+
existingState: WorkstreamState
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface CompactHistoryResult {
|
|
52
|
+
compacted: boolean
|
|
53
|
+
summaryText: string
|
|
54
|
+
lastCompactedMessageId?: string
|
|
55
|
+
compactedMessages: ChatMessage[]
|
|
56
|
+
compactedMessageCount: number
|
|
57
|
+
remainingMessageCount: number
|
|
58
|
+
estimatedTokens: number
|
|
59
|
+
inputChars: number
|
|
60
|
+
outputChars: number
|
|
61
|
+
state: WorkstreamState
|
|
62
|
+
stateDelta: WorkstreamStateDelta
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ContextCompactionRunnerParams {
|
|
66
|
+
previousSummary: string
|
|
67
|
+
existingState: WorkstreamState
|
|
68
|
+
chunk: ContextMessage[]
|
|
69
|
+
transcript: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ContextCompactionPromptParams {
|
|
73
|
+
previousSummary: string
|
|
74
|
+
existingState: WorkstreamState
|
|
75
|
+
transcript: string
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface MemoryBlockCompactionPromptParams {
|
|
79
|
+
previousSummary: string
|
|
80
|
+
newEntriesText: string
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type ContextCompactionRunner = (params: ContextCompactionRunnerParams) => Promise<CompactionOutput>
|
|
84
|
+
|
|
85
|
+
export interface CreateContextCompactionRuntimeOptions {
|
|
86
|
+
runCompacter: ContextCompactionRunner
|
|
87
|
+
now?: () => number
|
|
88
|
+
randomId?: () => string
|
|
89
|
+
thresholdRatio?: number
|
|
90
|
+
outputReserveTokens?: number
|
|
91
|
+
safetyMarginTokens?: number
|
|
92
|
+
compactionChunkMaxChars?: number
|
|
93
|
+
summaryRollupMaxTokens?: number
|
|
94
|
+
includedToolNames?: readonly string[]
|
|
95
|
+
includedToolPrefixes?: readonly string[]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const PROMPT_INJECTION_PATTERN =
|
|
99
|
+
/\b(ignore (all )?(previous|prior|system|developer)? instructions?|system prompt|developer prompt|tool override|jailbreak|role ?override|do not follow|bypass)\b/i
|
|
100
|
+
|
|
101
|
+
function estimateTokens(text: string): number {
|
|
102
|
+
return Math.ceil(text.length / 3)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizeWhitespace(value: string): string {
|
|
106
|
+
return value.replace(/\s+/g, ' ').trim()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function sanitizeStateText(value: string): string | null {
|
|
110
|
+
const normalized = normalizeWhitespace(value)
|
|
111
|
+
if (!normalized) return null
|
|
112
|
+
if (PROMPT_INJECTION_PATTERN.test(normalized)) return null
|
|
113
|
+
return normalized
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function createStableId(prefix: string, ...parts: Array<string | number | undefined>): string {
|
|
117
|
+
const payload = parts
|
|
118
|
+
.map((part) => (part === undefined ? '' : String(part)))
|
|
119
|
+
.map((part) => normalizeWhitespace(part))
|
|
120
|
+
.join('|')
|
|
121
|
+
const hash = createHash('sha1').update(`${prefix}|${payload}`).digest('hex').slice(0, 20)
|
|
122
|
+
return `${prefix}_${hash}`
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function readRecord(value: unknown): Record<string, unknown> | null {
|
|
126
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function stringifyUnknown(value: unknown): string | null {
|
|
130
|
+
if (value === undefined) return null
|
|
131
|
+
if (typeof value === 'string') {
|
|
132
|
+
const normalized = value.trim()
|
|
133
|
+
return normalized.length > 0 ? normalized : null
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
return JSON.stringify(value)
|
|
138
|
+
} catch {
|
|
139
|
+
return null
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function appendUnique(values: string[], nextValues: string[]): string[] {
|
|
144
|
+
const seen = new Set(values.map((value) => normalizeWhitespace(value).toLowerCase()))
|
|
145
|
+
const merged = [...values]
|
|
146
|
+
|
|
147
|
+
for (const value of nextValues) {
|
|
148
|
+
const normalized = normalizeWhitespace(value)
|
|
149
|
+
if (!normalized) continue
|
|
150
|
+
const key = normalized.toLowerCase()
|
|
151
|
+
if (seen.has(key)) continue
|
|
152
|
+
seen.add(key)
|
|
153
|
+
merged.push(normalized)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return merged
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function formatSummary(summaryText: string): string {
|
|
160
|
+
return `Compacted context summary:\n${summaryText.trim()}`
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function normalizeSummary(summaryText: string): string {
|
|
164
|
+
return summaryText.trim()
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function buildSyntheticSummaryPayload(
|
|
168
|
+
summaryText: string,
|
|
169
|
+
): { role: 'system'; parts: Array<{ type: 'text'; text: string }> } | null {
|
|
170
|
+
const summary = normalizeSummary(summaryText)
|
|
171
|
+
if (!summary) return null
|
|
172
|
+
return { role: 'system', parts: [{ type: 'text', text: formatSummary(summary) }] }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function readIsCompacted(message: ChatMessage): boolean {
|
|
176
|
+
const metadata = message.metadata && typeof message.metadata === 'object' ? message.metadata : undefined
|
|
177
|
+
return metadata ? (metadata as Record<string, unknown>).isCompacted === true : false
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function markMessageCompacted(message: ChatMessage, now: () => number): ChatMessage {
|
|
181
|
+
if (readIsCompacted(message)) return message
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
...message,
|
|
185
|
+
metadata:
|
|
186
|
+
message.metadata && typeof message.metadata === 'object'
|
|
187
|
+
? { ...message.metadata, isCompacted: true }
|
|
188
|
+
: { createdAt: now(), isCompacted: true },
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function parseWorkstreamState(value: unknown): WorkstreamState {
|
|
193
|
+
const parsed = WorkstreamStateSchema.safeParse(value)
|
|
194
|
+
return parsed.success ? parsed.data : createEmptyWorkstreamState()
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function pickDefined<T extends Record<string, unknown>, K extends keyof T>(
|
|
198
|
+
next: T,
|
|
199
|
+
base: T,
|
|
200
|
+
key: K,
|
|
201
|
+
): Partial<Pick<T, K>> {
|
|
202
|
+
if (next[key] !== undefined) {
|
|
203
|
+
return { [key]: next[key] } as Partial<Pick<T, K>>
|
|
204
|
+
}
|
|
205
|
+
if (base[key] !== undefined) {
|
|
206
|
+
return { [key]: base[key] } as Partial<Pick<T, K>>
|
|
207
|
+
}
|
|
208
|
+
return {}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function toStateFieldsUpdated(delta: WorkstreamStateDelta): string[] {
|
|
212
|
+
const fields: string[] = []
|
|
213
|
+
if (delta.currentPlan !== undefined) fields.push('currentPlan')
|
|
214
|
+
if ((delta.newDecisions?.length ?? 0) > 0) fields.push('keyDecisions')
|
|
215
|
+
if ((delta.resolvedQuestions?.length ?? 0) > 0 || (delta.newQuestions?.length ?? 0) > 0) fields.push('openQuestions')
|
|
216
|
+
if ((delta.newConstraints?.length ?? 0) > 0) fields.push('activeConstraints')
|
|
217
|
+
if ((delta.newRisks?.length ?? 0) > 0) fields.push('risks')
|
|
218
|
+
if ((delta.taskUpdates?.length ?? 0) > 0) fields.push('tasks')
|
|
219
|
+
if ((delta.artifacts?.length ?? 0) > 0) fields.push('artifacts')
|
|
220
|
+
if (delta.agentNote) fields.push('agentContributions')
|
|
221
|
+
if ((delta.conflicts?.length ?? 0) > 0) fields.push('conflicts')
|
|
222
|
+
|
|
223
|
+
if (
|
|
224
|
+
delta.approvedBy !== undefined ||
|
|
225
|
+
delta.approvedAt !== undefined ||
|
|
226
|
+
delta.approvalMessageId !== undefined ||
|
|
227
|
+
delta.approvalNote !== undefined
|
|
228
|
+
) {
|
|
229
|
+
fields.push('approval')
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return fields
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function toCompactionTranscript(messages: ContextMessage[]): string {
|
|
236
|
+
return messages
|
|
237
|
+
.map((message, index) => {
|
|
238
|
+
const role = message.role.toUpperCase()
|
|
239
|
+
const content = message.text.trim() || '[no text]'
|
|
240
|
+
return `[${index + 1}] (id=${message.sourceMessageId}) ${role}: ${content}`
|
|
241
|
+
})
|
|
242
|
+
.join('\n\n')
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function splitByCharBudget(messages: ContextMessage[], maxChars: number): ContextMessage[][] {
|
|
246
|
+
if (messages.length === 0) return []
|
|
247
|
+
|
|
248
|
+
const chunks: ContextMessage[][] = []
|
|
249
|
+
let current: ContextMessage[] = []
|
|
250
|
+
let currentChars = 0
|
|
251
|
+
|
|
252
|
+
for (const message of messages) {
|
|
253
|
+
const serialized = `${message.role}:${message.text}`
|
|
254
|
+
const nextChars = serialized.length
|
|
255
|
+
|
|
256
|
+
if (current.length > 0 && currentChars + nextChars > maxChars) {
|
|
257
|
+
chunks.push(current)
|
|
258
|
+
current = [message]
|
|
259
|
+
currentChars = nextChars
|
|
260
|
+
continue
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
current.push(message)
|
|
264
|
+
currentChars += nextChars
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (current.length > 0) chunks.push(current)
|
|
268
|
+
return chunks
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function mergeDelta(base: WorkstreamStateDelta, next: WorkstreamStateDelta): WorkstreamStateDelta {
|
|
272
|
+
return {
|
|
273
|
+
...(next.currentPlan !== undefined
|
|
274
|
+
? { currentPlan: next.currentPlan }
|
|
275
|
+
: base.currentPlan !== undefined
|
|
276
|
+
? { currentPlan: base.currentPlan }
|
|
277
|
+
: {}),
|
|
278
|
+
...(appendUnique(base.newConstraints ?? [], next.newConstraints ?? []).length > 0
|
|
279
|
+
? { newConstraints: appendUnique(base.newConstraints ?? [], next.newConstraints ?? []) }
|
|
280
|
+
: {}),
|
|
281
|
+
...(appendUnique(base.newRisks ?? [], next.newRisks ?? []).length > 0
|
|
282
|
+
? { newRisks: appendUnique(base.newRisks ?? [], next.newRisks ?? []) }
|
|
283
|
+
: {}),
|
|
284
|
+
...(appendUnique(base.newQuestions ?? [], next.newQuestions ?? []).length > 0
|
|
285
|
+
? { newQuestions: appendUnique(base.newQuestions ?? [], next.newQuestions ?? []) }
|
|
286
|
+
: {}),
|
|
287
|
+
...(appendUnique(base.resolvedQuestions ?? [], next.resolvedQuestions ?? []).length > 0
|
|
288
|
+
? { resolvedQuestions: appendUnique(base.resolvedQuestions ?? [], next.resolvedQuestions ?? []) }
|
|
289
|
+
: {}),
|
|
290
|
+
...((base.newDecisions?.length ?? 0) + (next.newDecisions?.length ?? 0) > 0
|
|
291
|
+
? { newDecisions: [...(base.newDecisions ?? []), ...(next.newDecisions ?? [])] }
|
|
292
|
+
: {}),
|
|
293
|
+
...((base.taskUpdates?.length ?? 0) + (next.taskUpdates?.length ?? 0) > 0
|
|
294
|
+
? { taskUpdates: [...(base.taskUpdates ?? []), ...(next.taskUpdates ?? [])] }
|
|
295
|
+
: {}),
|
|
296
|
+
...((base.artifacts?.length ?? 0) + (next.artifacts?.length ?? 0) > 0
|
|
297
|
+
? { artifacts: [...(base.artifacts ?? []), ...(next.artifacts ?? [])] }
|
|
298
|
+
: {}),
|
|
299
|
+
...(next.agentNote ? { agentNote: next.agentNote } : base.agentNote ? { agentNote: base.agentNote } : {}),
|
|
300
|
+
...((base.conflicts?.length ?? 0) + (next.conflicts?.length ?? 0) > 0
|
|
301
|
+
? { conflicts: [...(base.conflicts ?? []), ...(next.conflicts ?? [])] }
|
|
302
|
+
: {}),
|
|
303
|
+
...pickDefined(next, base, 'approvedBy'),
|
|
304
|
+
...pickDefined(next, base, 'approvedAt'),
|
|
305
|
+
...pickDefined(next, base, 'approvalMessageId'),
|
|
306
|
+
...pickDefined(next, base, 'approvalNote'),
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function mergeStateDelta(
|
|
311
|
+
existingState: WorkstreamState,
|
|
312
|
+
delta: WorkstreamStateDelta,
|
|
313
|
+
now: () => number,
|
|
314
|
+
): WorkstreamState {
|
|
315
|
+
const timestamp = now()
|
|
316
|
+
const state: WorkstreamState = {
|
|
317
|
+
...existingState,
|
|
318
|
+
currentPlan: existingState.currentPlan ? { ...existingState.currentPlan } : null,
|
|
319
|
+
activeConstraints: [...existingState.activeConstraints],
|
|
320
|
+
keyDecisions: [...existingState.keyDecisions],
|
|
321
|
+
tasks: [...existingState.tasks],
|
|
322
|
+
openQuestions: [...existingState.openQuestions],
|
|
323
|
+
risks: [...existingState.risks],
|
|
324
|
+
artifacts: [...existingState.artifacts],
|
|
325
|
+
agentContributions: [...existingState.agentContributions],
|
|
326
|
+
lastUpdated: timestamp,
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (delta.currentPlan !== undefined) {
|
|
330
|
+
if (delta.currentPlan === null) {
|
|
331
|
+
state.currentPlan = null
|
|
332
|
+
} else {
|
|
333
|
+
const planText = sanitizeStateText(delta.currentPlan)
|
|
334
|
+
if (planText) {
|
|
335
|
+
const planId = createStableId('plan', planText)
|
|
336
|
+
const existingPlan = state.currentPlan && state.currentPlan.id === planId ? state.currentPlan : null
|
|
337
|
+
state.currentPlan = {
|
|
338
|
+
id: planId,
|
|
339
|
+
text: planText,
|
|
340
|
+
source: 'agent',
|
|
341
|
+
approved: existingPlan?.approved ?? false,
|
|
342
|
+
timestamp,
|
|
343
|
+
sourceMessageIds: existingPlan?.sourceMessageIds ?? [],
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (delta.newConstraints?.length) {
|
|
350
|
+
for (const rawConstraint of delta.newConstraints) {
|
|
351
|
+
const text = sanitizeStateText(rawConstraint)
|
|
352
|
+
if (!text) continue
|
|
353
|
+
const constraintId = createStableId('constraint', text)
|
|
354
|
+
const exists = state.activeConstraints.some((constraint) => constraint.id === constraintId)
|
|
355
|
+
if (exists) continue
|
|
356
|
+
state.activeConstraints.push({
|
|
357
|
+
id: constraintId,
|
|
358
|
+
text,
|
|
359
|
+
source: 'agent',
|
|
360
|
+
approved: false,
|
|
361
|
+
timestamp,
|
|
362
|
+
sourceMessageIds: [],
|
|
363
|
+
})
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (delta.newRisks?.length) {
|
|
368
|
+
state.risks = appendUnique(state.risks, delta.newRisks)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (delta.resolvedQuestions?.length) {
|
|
372
|
+
const resolvedIds = new Set(
|
|
373
|
+
delta.resolvedQuestions
|
|
374
|
+
.map((question) => sanitizeStateText(question))
|
|
375
|
+
.filter((question): question is string => Boolean(question))
|
|
376
|
+
.map((question) => createStableId('question', question)),
|
|
377
|
+
)
|
|
378
|
+
state.openQuestions = state.openQuestions.filter((question) => !resolvedIds.has(question.id))
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (delta.newQuestions?.length) {
|
|
382
|
+
for (const rawQuestion of delta.newQuestions) {
|
|
383
|
+
const text = sanitizeStateText(rawQuestion)
|
|
384
|
+
if (!text) continue
|
|
385
|
+
const questionId = createStableId('question', text)
|
|
386
|
+
const exists = state.openQuestions.some((question) => question.id === questionId)
|
|
387
|
+
if (exists) continue
|
|
388
|
+
state.openQuestions.push({ id: questionId, text, source: 'agent', timestamp, sourceMessageIds: [] })
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (delta.newDecisions?.length) {
|
|
393
|
+
for (const decision of delta.newDecisions) {
|
|
394
|
+
const normalizedDecision = sanitizeStateText(decision.decision)
|
|
395
|
+
const normalizedRationale = sanitizeStateText(decision.rationale)
|
|
396
|
+
if (!normalizedDecision || !normalizedRationale) continue
|
|
397
|
+
|
|
398
|
+
const sourceIds = [...new Set(decision.sourceMessageIds.map((id) => normalizeWhitespace(id)).filter(Boolean))]
|
|
399
|
+
const decisionId = createStableId('decision', normalizedDecision, normalizedRationale, sourceIds.sort().join('|'))
|
|
400
|
+
const alreadyExists = state.keyDecisions.some((item) => item.id === decisionId)
|
|
401
|
+
if (alreadyExists) continue
|
|
402
|
+
state.keyDecisions.push({
|
|
403
|
+
id: decisionId,
|
|
404
|
+
decision: normalizedDecision,
|
|
405
|
+
rationale: normalizedRationale,
|
|
406
|
+
agent: normalizeWhitespace(decision.agent),
|
|
407
|
+
sourceMessageIds: sourceIds,
|
|
408
|
+
confidence: decision.confidence,
|
|
409
|
+
timestamp,
|
|
410
|
+
})
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (delta.taskUpdates?.length) {
|
|
415
|
+
for (const update of delta.taskUpdates) {
|
|
416
|
+
const title = sanitizeStateText(update.title)
|
|
417
|
+
if (!title) continue
|
|
418
|
+
|
|
419
|
+
const externalId = sanitizeStateText(update.externalId ?? '')
|
|
420
|
+
const owner = normalizeWhitespace(update.owner)
|
|
421
|
+
const taskId = externalId ? createStableId('task-external', externalId) : createStableId('task', title, owner)
|
|
422
|
+
const sourceMessageIds = [
|
|
423
|
+
...new Set(update.sourceMessageIds.map((id) => normalizeWhitespace(id)).filter(Boolean)),
|
|
424
|
+
]
|
|
425
|
+
const existingIndex = state.tasks.findIndex((task) => task.id === taskId)
|
|
426
|
+
const nextTask = {
|
|
427
|
+
id: taskId,
|
|
428
|
+
title,
|
|
429
|
+
status: update.status,
|
|
430
|
+
owner,
|
|
431
|
+
...(externalId ? { externalId } : {}),
|
|
432
|
+
source: 'agent' as const,
|
|
433
|
+
sourceMessageIds,
|
|
434
|
+
timestamp,
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (existingIndex >= 0) {
|
|
438
|
+
const existingTask = state.tasks[existingIndex]
|
|
439
|
+
state.tasks[existingIndex] = {
|
|
440
|
+
...nextTask,
|
|
441
|
+
sourceMessageIds: [...new Set([...existingTask.sourceMessageIds, ...sourceMessageIds])],
|
|
442
|
+
}
|
|
443
|
+
} else {
|
|
444
|
+
state.tasks.push(nextTask)
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (delta.artifacts?.length) {
|
|
450
|
+
for (const artifact of delta.artifacts) {
|
|
451
|
+
const name = sanitizeStateText(artifact.name)
|
|
452
|
+
const pointer = sanitizeStateText(artifact.pointer)
|
|
453
|
+
if (!name || !pointer) continue
|
|
454
|
+
|
|
455
|
+
const artifactId = createStableId('artifact', name, pointer)
|
|
456
|
+
const exists = state.artifacts.some((item) => item.id === artifactId)
|
|
457
|
+
if (exists) continue
|
|
458
|
+
state.artifacts.push({ id: artifactId, name, type: normalizeWhitespace(artifact.type), pointer, timestamp })
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (delta.agentNote) {
|
|
463
|
+
const agent = sanitizeStateText(delta.agentNote.agent)
|
|
464
|
+
const summary = sanitizeStateText(delta.agentNote.summary)
|
|
465
|
+
if (agent && summary) {
|
|
466
|
+
const noteId = createStableId('agent-note', agent, summary)
|
|
467
|
+
const exists = state.agentContributions.some((note) => note.id === noteId)
|
|
468
|
+
if (!exists) {
|
|
469
|
+
state.agentContributions.push({ id: noteId, agent, summary, timestamp })
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (delta.conflicts?.length) {
|
|
475
|
+
for (const conflict of delta.conflicts) {
|
|
476
|
+
const text = sanitizeStateText(`Conflict: ${conflict.recommendation}`)
|
|
477
|
+
if (!text) continue
|
|
478
|
+
const questionId = createStableId('question', text)
|
|
479
|
+
const exists = state.openQuestions.some((question) => question.id === questionId)
|
|
480
|
+
if (exists) continue
|
|
481
|
+
state.openQuestions.push({ id: questionId, text, source: 'agent', timestamp, sourceMessageIds: [] })
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (delta.approvedBy !== undefined) {
|
|
486
|
+
state.approvedBy = sanitizeStateText(delta.approvedBy) ?? undefined
|
|
487
|
+
}
|
|
488
|
+
if (delta.approvedAt !== undefined) {
|
|
489
|
+
state.approvedAt = delta.approvedAt
|
|
490
|
+
}
|
|
491
|
+
if (delta.approvalMessageId !== undefined) {
|
|
492
|
+
state.approvalMessageId = sanitizeStateText(delta.approvalMessageId) ?? undefined
|
|
493
|
+
}
|
|
494
|
+
if (delta.approvalNote !== undefined) {
|
|
495
|
+
state.approvalNote = sanitizeStateText(delta.approvalNote) ?? undefined
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
state.keyDecisions = state.keyDecisions.slice(-WORKSTREAM_STATE_MAX_KEY_DECISIONS)
|
|
499
|
+
state.activeConstraints = state.activeConstraints.slice(-WORKSTREAM_STATE_MAX_ACTIVE_CONSTRAINTS)
|
|
500
|
+
state.tasks = state.tasks.slice(-WORKSTREAM_STATE_MAX_TASKS)
|
|
501
|
+
state.openQuestions = state.openQuestions.slice(-WORKSTREAM_STATE_MAX_OPEN_QUESTIONS)
|
|
502
|
+
state.risks = state.risks.slice(-WORKSTREAM_STATE_MAX_RISKS)
|
|
503
|
+
state.artifacts = state.artifacts.slice(-WORKSTREAM_STATE_MAX_ARTIFACTS)
|
|
504
|
+
state.agentContributions = state.agentContributions.slice(-WORKSTREAM_STATE_MAX_AGENT_CONTRIBUTIONS)
|
|
505
|
+
|
|
506
|
+
return WorkstreamStateSchema.parse(state)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export interface ContextCompactionRuntime {
|
|
510
|
+
createSummaryMessage: (summaryText: string) => ChatMessage | null
|
|
511
|
+
prependSummaryMessage: (summaryText: string, liveMessages: ChatMessage[]) => ChatMessage[]
|
|
512
|
+
formatWorkstreamStateForPrompt: (state: WorkstreamState | null | undefined) => string | undefined
|
|
513
|
+
estimateThreshold: (contextSize?: number) => number
|
|
514
|
+
shouldCompactHistory: (params: {
|
|
515
|
+
summaryText: string
|
|
516
|
+
liveMessages: ChatMessage[]
|
|
517
|
+
contextSize?: number
|
|
518
|
+
}) => CompactionAssessment
|
|
519
|
+
compactHistory: (params: CompactHistoryParams) => Promise<CompactHistoryResult>
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export function buildContextCompactionPrompt(params: ContextCompactionPromptParams): string {
|
|
523
|
+
return [
|
|
524
|
+
'<context-compaction-input>',
|
|
525
|
+
'<previous-summary>',
|
|
526
|
+
params.previousSummary.trim() || 'None',
|
|
527
|
+
'</previous-summary>',
|
|
528
|
+
'<existing-workstream-state>',
|
|
529
|
+
JSON.stringify(params.existingState),
|
|
530
|
+
'</existing-workstream-state>',
|
|
531
|
+
'<new-messages>',
|
|
532
|
+
params.transcript || 'None',
|
|
533
|
+
'</new-messages>',
|
|
534
|
+
'</context-compaction-input>',
|
|
535
|
+
'',
|
|
536
|
+
'Produce a concise replacement summary and a structured state delta.',
|
|
537
|
+
'Only include facts supported by the new messages.',
|
|
538
|
+
'Summary format is required and must use exactly these sections in order:',
|
|
539
|
+
'KEY FACTS:',
|
|
540
|
+
'- ...',
|
|
541
|
+
'CONVERSATION FLOW:',
|
|
542
|
+
'- ...',
|
|
543
|
+
'OPEN THREADS:',
|
|
544
|
+
'- ...',
|
|
545
|
+
'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.',
|
|
546
|
+
].join('\n')
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export function buildMemoryBlockCompactionPrompt(params: MemoryBlockCompactionPromptParams): string {
|
|
550
|
+
return [
|
|
551
|
+
'<memory-block-compaction>',
|
|
552
|
+
'Produce a compact replacement summary for the workstream memory block.',
|
|
553
|
+
'Preserve constraints, commitments, unresolved risks, and ownership.',
|
|
554
|
+
'Blend the previous summary with the new raw entries into one updated summary.',
|
|
555
|
+
'Return plain text only.',
|
|
556
|
+
'',
|
|
557
|
+
'<previous-summary>',
|
|
558
|
+
params.previousSummary.trim() || 'None',
|
|
559
|
+
'</previous-summary>',
|
|
560
|
+
'<new-entries>',
|
|
561
|
+
params.newEntriesText.trim() || 'None',
|
|
562
|
+
'</new-entries>',
|
|
563
|
+
'</memory-block-compaction>',
|
|
564
|
+
].join('\n')
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
export function createContextCompactionRuntime(
|
|
568
|
+
options: CreateContextCompactionRuntimeOptions,
|
|
569
|
+
): ContextCompactionRuntime {
|
|
570
|
+
const now = options.now ?? (() => Date.now())
|
|
571
|
+
const randomId = options.randomId ?? (() => randomUUID())
|
|
572
|
+
const thresholdRatio = options.thresholdRatio ?? CONTEXT_COMPACTION_THRESHOLD_RATIO
|
|
573
|
+
const outputReserveTokens = options.outputReserveTokens ?? CONTEXT_OUTPUT_RESERVE_TOKENS
|
|
574
|
+
const safetyMarginTokens = options.safetyMarginTokens ?? CONTEXT_SAFETY_MARGIN_TOKENS
|
|
575
|
+
const compactionChunkMaxChars = options.compactionChunkMaxChars ?? COMPACTION_CHUNK_MAX_CHARS
|
|
576
|
+
const summaryRollupMaxTokens = options.summaryRollupMaxTokens ?? SUMMARY_ROLLUP_MAX_TOKENS
|
|
577
|
+
const includedToolNames = new Set(options.includedToolNames ?? CONTEXT_COMPACTION_INCLUDED_TOOL_NAMES)
|
|
578
|
+
const includedToolPrefixes = options.includedToolPrefixes ?? CONTEXT_COMPACTION_INCLUDED_TOOL_PREFIXES
|
|
579
|
+
|
|
580
|
+
const shouldIncludeToolInCompaction = (toolName: string): boolean => {
|
|
581
|
+
if (includedToolNames.has(toolName)) return true
|
|
582
|
+
return includedToolPrefixes.some((prefix) => toolName.startsWith(prefix))
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const toContextTextFromChatMessage = (message: ChatMessage): string => {
|
|
586
|
+
const chunks: string[] = []
|
|
587
|
+
|
|
588
|
+
for (const part of message.parts) {
|
|
589
|
+
const record = readRecord(part)
|
|
590
|
+
if (!record) continue
|
|
591
|
+
|
|
592
|
+
const type = readString(record.type)
|
|
593
|
+
if (type === 'text') {
|
|
594
|
+
const text = readString(record.text)
|
|
595
|
+
if (text) chunks.push(text)
|
|
596
|
+
continue
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (type?.startsWith('tool-')) {
|
|
600
|
+
const toolName = type.slice('tool-'.length) || 'unknown'
|
|
601
|
+
if (!shouldIncludeToolInCompaction(toolName)) continue
|
|
602
|
+
const state = readString(record.state)
|
|
603
|
+
const output = stringifyUnknown(record.output)
|
|
604
|
+
const input = stringifyUnknown(record.input)
|
|
605
|
+
const payload = output ?? input
|
|
606
|
+
if (payload) {
|
|
607
|
+
chunks.push(`[tool:${toolName}${state ? `:${state}` : ''}] ${payload}`)
|
|
608
|
+
}
|
|
609
|
+
continue
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (type?.startsWith('data-')) {
|
|
613
|
+
const dataPayload = stringifyUnknown(record.data)
|
|
614
|
+
if (dataPayload) {
|
|
615
|
+
chunks.push(`[${type}] ${dataPayload}`)
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return chunks.join('\n').trim()
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const toContextMessageFromChatMessage = (message: ChatMessage): ContextMessage => {
|
|
624
|
+
return { role: message.role, text: toContextTextFromChatMessage(message), sourceMessageId: message.id }
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const formatWorkstreamStateForPrompt = (state: WorkstreamState | null | undefined): string | undefined => {
|
|
628
|
+
if (!state) return undefined
|
|
629
|
+
|
|
630
|
+
const approvedPlan =
|
|
631
|
+
state.currentPlan && state.currentPlan.approved ? sanitizeStateText(state.currentPlan.text) : null
|
|
632
|
+
const candidatePlan =
|
|
633
|
+
state.currentPlan && !state.currentPlan.approved ? sanitizeStateText(state.currentPlan.text) : null
|
|
634
|
+
|
|
635
|
+
const approvedConstraints = state.activeConstraints
|
|
636
|
+
.filter((constraint) => constraint.approved)
|
|
637
|
+
.map((constraint) => ({ ...constraint, text: sanitizeStateText(constraint.text) }))
|
|
638
|
+
.filter((constraint): constraint is typeof constraint & { text: string } => Boolean(constraint.text))
|
|
639
|
+
|
|
640
|
+
const candidateConstraints = state.activeConstraints
|
|
641
|
+
.filter((constraint) => !constraint.approved)
|
|
642
|
+
.map((constraint) => ({ ...constraint, text: sanitizeStateText(constraint.text) }))
|
|
643
|
+
.filter((constraint): constraint is typeof constraint & { text: string } => Boolean(constraint.text))
|
|
644
|
+
|
|
645
|
+
const openQuestions = state.openQuestions
|
|
646
|
+
.map((question) => ({ ...question, text: sanitizeStateText(question.text) }))
|
|
647
|
+
.filter((question): question is typeof question & { text: string } => Boolean(question.text))
|
|
648
|
+
|
|
649
|
+
const decisions = state.keyDecisions
|
|
650
|
+
.map((decision) => ({
|
|
651
|
+
...decision,
|
|
652
|
+
decision: sanitizeStateText(decision.decision),
|
|
653
|
+
rationale: sanitizeStateText(decision.rationale),
|
|
654
|
+
}))
|
|
655
|
+
.filter((decision): decision is typeof decision & { decision: string; rationale: string } =>
|
|
656
|
+
Boolean(decision.decision && decision.rationale),
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
const tasks = state.tasks
|
|
660
|
+
.map((task) => ({ ...task, title: sanitizeStateText(task.title), owner: sanitizeStateText(task.owner) }))
|
|
661
|
+
.filter((task): task is typeof task & { title: string; owner: string } => Boolean(task.title && task.owner))
|
|
662
|
+
|
|
663
|
+
const artifacts = state.artifacts
|
|
664
|
+
.map((artifact) => ({
|
|
665
|
+
...artifact,
|
|
666
|
+
name: sanitizeStateText(artifact.name),
|
|
667
|
+
pointer: sanitizeStateText(artifact.pointer),
|
|
668
|
+
}))
|
|
669
|
+
.filter((artifact): artifact is typeof artifact & { name: string; pointer: string } =>
|
|
670
|
+
Boolean(artifact.name && artifact.pointer),
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
const agentContributions = state.agentContributions
|
|
674
|
+
.map((note) => ({ ...note, summary: sanitizeStateText(note.summary) }))
|
|
675
|
+
.filter((note): note is typeof note & { summary: string } => Boolean(note.summary))
|
|
676
|
+
|
|
677
|
+
const payload = {
|
|
678
|
+
policy: { approvedConstraintsAreBinding: true, candidateStateIsAdvisoryOnly: true },
|
|
679
|
+
approved: {
|
|
680
|
+
currentPlan: approvedPlan ? { text: approvedPlan, source: state.currentPlan?.source } : null,
|
|
681
|
+
constraints: approvedConstraints.map((constraint) => ({
|
|
682
|
+
id: constraint.id,
|
|
683
|
+
text: constraint.text,
|
|
684
|
+
source: constraint.source,
|
|
685
|
+
})),
|
|
686
|
+
},
|
|
687
|
+
candidate: {
|
|
688
|
+
currentPlan: candidatePlan ? { text: candidatePlan, source: state.currentPlan?.source } : null,
|
|
689
|
+
constraints: candidateConstraints.map((constraint) => ({
|
|
690
|
+
id: constraint.id,
|
|
691
|
+
text: constraint.text,
|
|
692
|
+
source: constraint.source,
|
|
693
|
+
})),
|
|
694
|
+
},
|
|
695
|
+
decisions,
|
|
696
|
+
openQuestions,
|
|
697
|
+
risks: state.risks.map((risk) => sanitizeStateText(risk)).filter((risk): risk is string => Boolean(risk)),
|
|
698
|
+
tasks,
|
|
699
|
+
artifacts,
|
|
700
|
+
agentContributions,
|
|
701
|
+
advisory: {
|
|
702
|
+
approvedBy: state.approvedBy ?? null,
|
|
703
|
+
approvedAt: state.approvedAt ?? null,
|
|
704
|
+
approvalMessageId: state.approvalMessageId ?? null,
|
|
705
|
+
approvalNote: state.approvalNote ?? null,
|
|
706
|
+
},
|
|
707
|
+
lastUpdated: state.lastUpdated,
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return ['<workstream-state>', JSON.stringify(payload, null, 2), '</workstream-state>'].join('\n')
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const createSummaryMessage = (summaryText: string): ChatMessage | null => {
|
|
714
|
+
const summary = normalizeSummary(summaryText)
|
|
715
|
+
if (!summary) return null
|
|
716
|
+
|
|
717
|
+
return {
|
|
718
|
+
id: `summary-${randomId()}`,
|
|
719
|
+
role: 'system',
|
|
720
|
+
parts: [{ type: 'text', text: formatSummary(summary) }],
|
|
721
|
+
metadata: { createdAt: now() },
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const prependSummaryMessage = (summaryText: string, liveMessages: ChatMessage[]): ChatMessage[] => {
|
|
726
|
+
const summaryMessage = createSummaryMessage(summaryText)
|
|
727
|
+
return summaryMessage ? [summaryMessage, ...liveMessages] : [...liveMessages]
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const estimateThreshold = (contextSize = 256_000): number => {
|
|
731
|
+
const reservedOutput = Math.min(outputReserveTokens, Math.floor(contextSize * 0.35))
|
|
732
|
+
const safetyMargin = Math.min(safetyMarginTokens, Math.floor(contextSize * 0.1))
|
|
733
|
+
const reservedThreshold = contextSize - (reservedOutput + safetyMargin)
|
|
734
|
+
const ratioThreshold = Math.floor(contextSize * thresholdRatio)
|
|
735
|
+
return Math.max(1_000, Math.min(contextSize - 1, Math.min(reservedThreshold, ratioThreshold)))
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const shouldCompactHistory = (params: {
|
|
739
|
+
summaryText: string
|
|
740
|
+
liveMessages: ChatMessage[]
|
|
741
|
+
contextSize?: number
|
|
742
|
+
}): CompactionAssessment => {
|
|
743
|
+
const threshold = estimateThreshold(params.contextSize)
|
|
744
|
+
const summaryPayload = buildSyntheticSummaryPayload(params.summaryText)
|
|
745
|
+
const promptPayload = JSON.stringify([...(summaryPayload ? [summaryPayload] : []), ...params.liveMessages])
|
|
746
|
+
const estimatedTokens = estimateTokens(promptPayload)
|
|
747
|
+
return { estimatedTokens, threshold, shouldCompact: estimatedTokens >= threshold }
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const compactContextMessages = async (params: {
|
|
751
|
+
previousSummary: string
|
|
752
|
+
existingState: WorkstreamState
|
|
753
|
+
newMessages: ContextMessage[]
|
|
754
|
+
}): Promise<CompactionOutput> => {
|
|
755
|
+
const chunks = splitByCharBudget(params.newMessages, compactionChunkMaxChars)
|
|
756
|
+
let summary = normalizeSummary(params.previousSummary)
|
|
757
|
+
let delta: WorkstreamStateDelta = {}
|
|
758
|
+
let currentState = params.existingState
|
|
759
|
+
|
|
760
|
+
for (const chunk of chunks) {
|
|
761
|
+
const transcript = toCompactionTranscript(chunk)
|
|
762
|
+
const output = await options.runCompacter({
|
|
763
|
+
previousSummary: summary,
|
|
764
|
+
existingState: currentState,
|
|
765
|
+
chunk,
|
|
766
|
+
transcript,
|
|
767
|
+
})
|
|
768
|
+
summary = normalizeSummary(output.summary)
|
|
769
|
+
const parsedDelta = WorkstreamStateDeltaSchema.parse(output.stateDelta)
|
|
770
|
+
delta = mergeDelta(delta, parsedDelta)
|
|
771
|
+
currentState = mergeStateDelta(currentState, parsedDelta, now)
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
return { summary, stateDelta: delta }
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const compactHistory = async (params: CompactHistoryParams): Promise<CompactHistoryResult> => {
|
|
778
|
+
let summaryText = normalizeSummary(params.summaryText)
|
|
779
|
+
let remainingMessages = [...params.liveMessages]
|
|
780
|
+
let state = params.existingState
|
|
781
|
+
let mergedDelta: WorkstreamStateDelta = {}
|
|
782
|
+
let compactedMessages: ChatMessage[] = []
|
|
783
|
+
let lastCompactedMessageId: string | undefined
|
|
784
|
+
const summaryPayload = buildSyntheticSummaryPayload(summaryText)
|
|
785
|
+
const initialPayload = JSON.stringify([...(summaryPayload ? [summaryPayload] : []), ...remainingMessages])
|
|
786
|
+
const inputChars = initialPayload.length
|
|
787
|
+
|
|
788
|
+
for (;;) {
|
|
789
|
+
const assessment = shouldCompactHistory({
|
|
790
|
+
summaryText,
|
|
791
|
+
liveMessages: remainingMessages,
|
|
792
|
+
contextSize: params.contextSize,
|
|
793
|
+
})
|
|
794
|
+
|
|
795
|
+
if (!assessment.shouldCompact) {
|
|
796
|
+
const summaryPayload = buildSyntheticSummaryPayload(summaryText)
|
|
797
|
+
const outputPayload = JSON.stringify([...(summaryPayload ? [summaryPayload] : []), ...remainingMessages])
|
|
798
|
+
return {
|
|
799
|
+
compacted: compactedMessages.length > 0,
|
|
800
|
+
summaryText,
|
|
801
|
+
...(lastCompactedMessageId ? { lastCompactedMessageId } : {}),
|
|
802
|
+
compactedMessages,
|
|
803
|
+
compactedMessageCount: compactedMessages.length,
|
|
804
|
+
remainingMessageCount: remainingMessages.length,
|
|
805
|
+
estimatedTokens: assessment.estimatedTokens,
|
|
806
|
+
inputChars,
|
|
807
|
+
outputChars: outputPayload.length,
|
|
808
|
+
state,
|
|
809
|
+
stateDelta: mergedDelta,
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const boundary = Math.max(0, remainingMessages.length - params.tailMessageCount)
|
|
814
|
+
if (boundary <= 0) {
|
|
815
|
+
const summaryPayload = buildSyntheticSummaryPayload(summaryText)
|
|
816
|
+
const outputPayload = JSON.stringify([...(summaryPayload ? [summaryPayload] : []), ...remainingMessages])
|
|
817
|
+
return {
|
|
818
|
+
compacted: compactedMessages.length > 0,
|
|
819
|
+
summaryText,
|
|
820
|
+
...(lastCompactedMessageId ? { lastCompactedMessageId } : {}),
|
|
821
|
+
compactedMessages,
|
|
822
|
+
compactedMessageCount: compactedMessages.length,
|
|
823
|
+
remainingMessageCount: remainingMessages.length,
|
|
824
|
+
estimatedTokens: assessment.estimatedTokens,
|
|
825
|
+
inputChars,
|
|
826
|
+
outputChars: outputPayload.length,
|
|
827
|
+
state,
|
|
828
|
+
stateDelta: mergedDelta,
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const candidatePrefix = remainingMessages.slice(0, boundary)
|
|
833
|
+
const messagesToCompact = candidatePrefix.filter((message) => !readIsCompacted(message))
|
|
834
|
+
const contextMessages = messagesToCompact.map(toContextMessageFromChatMessage)
|
|
835
|
+
const sourceText = toCompactionTranscript(contextMessages)
|
|
836
|
+
|
|
837
|
+
if (!normalizeWhitespace(sourceText)) {
|
|
838
|
+
const summaryPayload = buildSyntheticSummaryPayload(summaryText)
|
|
839
|
+
const outputPayload = JSON.stringify([...(summaryPayload ? [summaryPayload] : []), ...remainingMessages])
|
|
840
|
+
return {
|
|
841
|
+
compacted: compactedMessages.length > 0,
|
|
842
|
+
summaryText,
|
|
843
|
+
...(lastCompactedMessageId ? { lastCompactedMessageId } : {}),
|
|
844
|
+
compactedMessages,
|
|
845
|
+
compactedMessageCount: compactedMessages.length,
|
|
846
|
+
remainingMessageCount: remainingMessages.length,
|
|
847
|
+
estimatedTokens: assessment.estimatedTokens,
|
|
848
|
+
inputChars,
|
|
849
|
+
outputChars: outputPayload.length,
|
|
850
|
+
state,
|
|
851
|
+
stateDelta: mergedDelta,
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
let compactionOutput = await compactContextMessages({
|
|
856
|
+
previousSummary: summaryText,
|
|
857
|
+
existingState: state,
|
|
858
|
+
newMessages: contextMessages,
|
|
859
|
+
})
|
|
860
|
+
|
|
861
|
+
let nextSummary = normalizeSummary(compactionOutput.summary)
|
|
862
|
+
let nextState = mergeStateDelta(state, compactionOutput.stateDelta, now)
|
|
863
|
+
|
|
864
|
+
if (estimateTokens(nextSummary) > summaryRollupMaxTokens) {
|
|
865
|
+
const rollupOutput = await compactContextMessages({
|
|
866
|
+
previousSummary: '',
|
|
867
|
+
existingState: nextState,
|
|
868
|
+
newMessages: [{ role: 'assistant', text: nextSummary, sourceMessageId: 'summary-rollup' }],
|
|
869
|
+
})
|
|
870
|
+
nextSummary = normalizeSummary(rollupOutput.summary)
|
|
871
|
+
nextState = mergeStateDelta(nextState, rollupOutput.stateDelta, now)
|
|
872
|
+
compactionOutput = rollupOutput
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
if (nextSummary.length >= sourceText.length) {
|
|
876
|
+
throw new Error('Compaction summary is not shorter than compacted source')
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
summaryText = nextSummary
|
|
880
|
+
state = nextState
|
|
881
|
+
mergedDelta = mergeDelta(mergedDelta, compactionOutput.stateDelta)
|
|
882
|
+
compactedMessages = [
|
|
883
|
+
...compactedMessages,
|
|
884
|
+
...candidatePrefix.map((message) => markMessageCompacted(message, now)),
|
|
885
|
+
]
|
|
886
|
+
lastCompactedMessageId = candidatePrefix.at(-1)?.id ?? lastCompactedMessageId
|
|
887
|
+
remainingMessages = remainingMessages.slice(boundary)
|
|
888
|
+
|
|
889
|
+
if (remainingMessages.length <= params.tailMessageCount) {
|
|
890
|
+
continue
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
return {
|
|
896
|
+
createSummaryMessage,
|
|
897
|
+
prependSummaryMessage,
|
|
898
|
+
formatWorkstreamStateForPrompt,
|
|
899
|
+
estimateThreshold,
|
|
900
|
+
shouldCompactHistory,
|
|
901
|
+
compactHistory,
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
export function parseCompactionOutput(value: unknown): CompactionOutput {
|
|
906
|
+
const parsed = StructuredCompactionOutputSchema.parse(value)
|
|
907
|
+
|
|
908
|
+
return { summary: parsed.summary, stateDelta: parseStructuredWorkstreamStateDelta(parsed.stateDelta) }
|
|
909
|
+
}
|