@lota-sdk/core 0.1.24 → 0.1.25
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/package.json +2 -2
- package/src/ai/definitions.ts +5 -59
- package/src/ai-gateway/ai-gateway.ts +36 -28
- package/src/ai-gateway/cache-headers.ts +9 -0
- package/src/config/model-constants.ts +6 -2
- package/src/create-runtime.ts +1 -17
- package/src/db/memory-types.ts +13 -8
- package/src/db/memory.ts +74 -53
- package/src/queues/autonomous-job.queue.ts +1 -8
- package/src/queues/context-compaction.queue.ts +2 -2
- package/src/queues/index.ts +2 -6
- package/src/queues/organization-learning.queue.ts +78 -0
- package/src/queues/plan-agent-heartbeat.queue.ts +10 -16
- package/src/queues/title-generation.queue.ts +62 -0
- package/src/runtime/agent-prompt-context.ts +0 -18
- package/src/runtime/agent-runtime-policy.ts +9 -2
- package/src/runtime/context-compaction-constants.ts +4 -2
- package/src/runtime/context-compaction.ts +135 -118
- package/src/runtime/memory-pipeline.ts +70 -1
- package/src/runtime/memory-prompts-fact.ts +16 -0
- package/src/runtime/plugin-resolution.ts +3 -2
- package/src/runtime/plugin-types.ts +1 -42
- package/src/runtime/post-turn-side-effects.ts +212 -0
- package/src/runtime/runtime-config.ts +0 -13
- package/src/runtime/runtime-extensions.ts +10 -16
- package/src/runtime/runtime-worker-registry.ts +8 -19
- package/src/runtime/social-chat-agent-runner.ts +119 -0
- package/src/runtime/social-chat-history.ts +110 -0
- package/src/runtime/social-chat-prompts.ts +58 -0
- package/src/runtime/social-chat.ts +104 -340
- package/src/runtime/specialist-runner.ts +18 -0
- package/src/runtime/workstream-chat-helpers.ts +19 -0
- package/src/runtime/workstream-plan-turn.ts +195 -0
- package/src/runtime/workstream-state.ts +11 -8
- package/src/runtime/workstream-turn-context.ts +183 -0
- package/src/services/autonomous-job.service.ts +1 -8
- package/src/services/execution-plan.service.ts +205 -334
- package/src/services/index.ts +1 -4
- package/src/services/memory.service.ts +54 -44
- package/src/services/ownership-dispatcher.service.ts +2 -19
- package/src/services/plan-completion-side-effects.ts +80 -0
- package/src/services/plan-event-delivery.service.ts +1 -1
- package/src/services/plan-executor.service.ts +42 -190
- package/src/services/plan-node-spec.ts +60 -0
- package/src/services/plan-run-data.ts +88 -0
- package/src/services/plan-validator.service.ts +10 -8
- package/src/services/workstream-constants.ts +2 -0
- package/src/services/workstream-title.service.ts +1 -1
- package/src/services/workstream-turn-preparation.service.ts +208 -715
- package/src/services/workstream.service.ts +162 -192
- package/src/services/workstream.types.ts +12 -44
- package/src/system-agents/regular-chat-memory-digest.agent.ts +3 -0
- package/src/tools/execution-plan.tool.ts +7 -6
- package/src/tools/remember-memory.tool.ts +7 -10
- package/src/tools/research-topic.tool.ts +1 -1
- package/src/tools/team-think.tool.ts +1 -1
- package/src/tools/user-questions.tool.ts +1 -1
- package/src/utils/autonomous-job-ids.ts +7 -0
- package/src/workers/organization-learning.worker.ts +31 -0
- package/src/workers/regular-chat-memory-digest.runner.ts +9 -3
- package/src/workers/skill-extraction.runner.ts +2 -2
- package/src/queues/recent-activity-title-refinement.queue.ts +0 -30
- package/src/queues/regular-chat-memory-digest.config.ts +0 -12
- package/src/queues/regular-chat-memory-digest.queue.ts +0 -34
- package/src/queues/skill-extraction.config.ts +0 -9
- package/src/queues/skill-extraction.queue.ts +0 -27
- package/src/queues/workstream-title-generation.queue.ts +0 -33
- package/src/services/context-enrichment.service.ts +0 -33
- package/src/services/coordination-registry.service.ts +0 -117
- package/src/services/domain-agent-executor.service.ts +0 -71
- package/src/services/memory-assessment.service.ts +0 -44
- package/src/services/playbook-registry.service.ts +0 -67
- package/src/workers/regular-chat-memory-digest.worker.ts +0 -22
- package/src/workers/skill-extraction.worker.ts +0 -22
|
@@ -25,6 +25,12 @@ export interface PlanAgentHeartbeatSweepJob {
|
|
|
25
25
|
export type PlanAgentHeartbeatJob = PlanAgentHeartbeatWakeJob | PlanAgentHeartbeatSweepJob
|
|
26
26
|
|
|
27
27
|
export const PLAN_AGENT_HEARTBEAT_QUEUE = 'plan-agent-heartbeat'
|
|
28
|
+
const PLAN_AGENT_HEARTBEAT_SWEEP_INTERVAL_MS = 30_000
|
|
29
|
+
const PLAN_AGENT_HEARTBEAT_SWEEP_JOB_ID = 'plan-agent-heartbeat-sweep'
|
|
30
|
+
|
|
31
|
+
async function enqueueDelayedPlanAgentHeartbeatSweep(delayMs = PLAN_AGENT_HEARTBEAT_SWEEP_INTERVAL_MS): Promise<void> {
|
|
32
|
+
await planAgentHeartbeatQueue.enqueue({ type: 'sweep' }, { delay: delayMs, jobId: PLAN_AGENT_HEARTBEAT_SWEEP_JOB_ID })
|
|
33
|
+
}
|
|
28
34
|
|
|
29
35
|
async function processPlanAgentHeartbeatJob(job: Job<PlanAgentHeartbeatJob>): Promise<void> {
|
|
30
36
|
await databaseService.connect()
|
|
@@ -35,6 +41,9 @@ async function processPlanAgentHeartbeatJob(job: Job<PlanAgentHeartbeatJob>): Pr
|
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
await planAgentHeartbeatService.sweep({ organizationId: job.data.organizationId })
|
|
44
|
+
if (!job.data.organizationId) {
|
|
45
|
+
await enqueueDelayedPlanAgentHeartbeatSweep()
|
|
46
|
+
}
|
|
38
47
|
}
|
|
39
48
|
|
|
40
49
|
const planAgentHeartbeatQueue = createQueueFactory<PlanAgentHeartbeatJob>({
|
|
@@ -43,8 +52,6 @@ const planAgentHeartbeatQueue = createQueueFactory<PlanAgentHeartbeatJob>({
|
|
|
43
52
|
jobName: 'plan-agent-heartbeat-job',
|
|
44
53
|
concurrency: 2,
|
|
45
54
|
lockDuration: LONG_JOB_LOCK_DURATION_MS,
|
|
46
|
-
stalledInterval: 120_000,
|
|
47
|
-
maxStalledCount: 5,
|
|
48
55
|
defaultJobOptions: { ...DEFAULT_JOB_RETENTION, attempts: 3, backoff: { type: 'exponential', delay: 5_000 } },
|
|
49
56
|
processor: processPlanAgentHeartbeatJob,
|
|
50
57
|
})
|
|
@@ -72,23 +79,10 @@ export async function enqueuePlanAgentHeartbeatWake(params: {
|
|
|
72
79
|
await planAgentHeartbeatQueue.enqueue({ type: 'wake-node', ...params }, { jobId: buildWakeJobId(params) })
|
|
73
80
|
}
|
|
74
81
|
|
|
75
|
-
const PLAN_AGENT_HEARTBEAT_SCHEDULER_ID = 'plan-agent-heartbeat-sweep'
|
|
76
|
-
|
|
77
|
-
export async function schedulePlanAgentHeartbeatSweep(params?: { everyMs?: number }): Promise<void> {
|
|
78
|
-
const everyMs = params?.everyMs ?? 30_000
|
|
79
|
-
await planAgentHeartbeatQueue
|
|
80
|
-
.getQueue()
|
|
81
|
-
.upsertJobScheduler(
|
|
82
|
-
PLAN_AGENT_HEARTBEAT_SCHEDULER_ID,
|
|
83
|
-
{ every: everyMs },
|
|
84
|
-
{ name: 'plan-agent-heartbeat-job', data: { type: 'sweep' }, opts: { ...DEFAULT_JOB_RETENTION } },
|
|
85
|
-
)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
82
|
export function startPlanAgentHeartbeatWorker(options: { registerSignals?: boolean } = {}): WorkerHandle {
|
|
89
83
|
const handle = planAgentHeartbeatQueue.startWorker(options)
|
|
90
84
|
|
|
91
|
-
|
|
85
|
+
enqueueDelayedPlanAgentHeartbeatSweep().catch((error: unknown) => {
|
|
92
86
|
serverLogger.error`Plan agent heartbeat scheduler setup failed: ${error}`
|
|
93
87
|
})
|
|
94
88
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Job } from 'bullmq'
|
|
2
|
+
|
|
3
|
+
import { ensureRecordId } from '../db/record-id'
|
|
4
|
+
import { databaseService } from '../db/service'
|
|
5
|
+
import { recentActivityTitleService } from '../services/recent-activity-title.service'
|
|
6
|
+
import { workstreamTitleService } from '../services/workstream-title.service'
|
|
7
|
+
import { createQueueFactory } from './queue-factory'
|
|
8
|
+
|
|
9
|
+
export const TITLE_GENERATION_QUEUE = 'title-generation'
|
|
10
|
+
|
|
11
|
+
// This queue merges workstream title generation and recent-activity title
|
|
12
|
+
// refinement because both are short-lived title synthesis jobs with the same
|
|
13
|
+
// operational shape.
|
|
14
|
+
|
|
15
|
+
interface WorkstreamTitleGenerationJob {
|
|
16
|
+
kind: 'workstream-title'
|
|
17
|
+
workstreamId: string
|
|
18
|
+
sourceText: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface RecentActivityTitleRefinementJob {
|
|
22
|
+
kind: 'recent-activity-title'
|
|
23
|
+
activityId: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type TitleGenerationJob = WorkstreamTitleGenerationJob | RecentActivityTitleRefinementJob
|
|
27
|
+
|
|
28
|
+
async function processTitleGenerationJob(job: Job<TitleGenerationJob>): Promise<void> {
|
|
29
|
+
await databaseService.connect()
|
|
30
|
+
if (job.data.kind === 'workstream-title') {
|
|
31
|
+
await workstreamTitleService.generateAndPersistTitle(ensureRecordId(job.data.workstreamId), job.data.sourceText)
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
await recentActivityTitleService.refineRecentActivityTitle(job.data.activityId)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const titleGeneration = createQueueFactory<TitleGenerationJob>({
|
|
39
|
+
name: TITLE_GENERATION_QUEUE,
|
|
40
|
+
displayName: 'Title generation',
|
|
41
|
+
jobName: 'title-generation',
|
|
42
|
+
concurrency: 10,
|
|
43
|
+
lockDuration: 300_000,
|
|
44
|
+
defaultJobOptions: { attempts: 3, backoff: { type: 'exponential', delay: 2_000 } },
|
|
45
|
+
processor: processTitleGenerationJob,
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
export function enqueueWorkstreamTitleGeneration(job: Omit<WorkstreamTitleGenerationJob, 'kind'>) {
|
|
49
|
+
return titleGeneration.enqueue(
|
|
50
|
+
{ kind: 'workstream-title', ...job },
|
|
51
|
+
{ jobId: `workstream-title:${job.workstreamId}` },
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function enqueueRecentActivityTitleRefinement(job: Omit<RecentActivityTitleRefinementJob, 'kind'>) {
|
|
56
|
+
return titleGeneration.enqueue(
|
|
57
|
+
{ kind: 'recent-activity-title', ...job },
|
|
58
|
+
{ jobId: `recent-activity-title:${job.activityId}` },
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const startTitleGenerationWorker = titleGeneration.startWorker
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { toOptionalIsoDateTimeString } from '../utils/date-time'
|
|
2
2
|
|
|
3
|
-
type StructuredProfile = Record<string, unknown>
|
|
4
|
-
|
|
5
3
|
interface AgentPromptContext {
|
|
6
4
|
systemWorkspaceDetails: string
|
|
7
5
|
}
|
|
@@ -18,7 +16,6 @@ interface DomainEventLike {
|
|
|
18
16
|
interface BuildAgentPromptContextParams {
|
|
19
17
|
workspaceName?: string
|
|
20
18
|
summaryBlock?: string
|
|
21
|
-
structuredProfile?: StructuredProfile
|
|
22
19
|
promptSummary?: string
|
|
23
20
|
userName?: string | null
|
|
24
21
|
recentDomainEvents: DomainEventLike[]
|
|
@@ -39,18 +36,6 @@ function normalizeWorkspaceName(name?: string): string {
|
|
|
39
36
|
return normalized && normalized.length > 0 ? normalized : 'Workspace'
|
|
40
37
|
}
|
|
41
38
|
|
|
42
|
-
function hasStructuredProfileData(profile?: StructuredProfile): boolean {
|
|
43
|
-
return Boolean(profile && Object.keys(profile).length > 0)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function formatStructuredProfile(profile?: StructuredProfile): string {
|
|
47
|
-
if (!hasStructuredProfileData(profile)) {
|
|
48
|
-
return 'No structured workspace profile has been recorded yet.'
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return JSON.stringify(profile, null, 2)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
39
|
function formatDomainEvent(event: DomainEventLike, index: number): string {
|
|
55
40
|
const summary = typeof event.summary === 'string' && event.summary.trim().length > 0 ? event.summary.trim() : null
|
|
56
41
|
if (summary) {
|
|
@@ -95,7 +80,6 @@ function formatRecentDomainEvents(events: DomainEventLike[]): string {
|
|
|
95
80
|
export function buildAgentPromptContext(params: BuildAgentPromptContextParams): AgentPromptContext {
|
|
96
81
|
const workspaceName = normalizeWorkspaceName(params.workspaceName)
|
|
97
82
|
const summaryBlock = normalizeSummaryBlock(params.summaryBlock)
|
|
98
|
-
const structuredProfile = formatStructuredProfile(params.structuredProfile)
|
|
99
83
|
const promptSummary = params.promptSummary?.trim()
|
|
100
84
|
const userName = normalizeUserName(params.userName)
|
|
101
85
|
const recentDomainEvents = formatRecentDomainEvents(params.recentDomainEvents)
|
|
@@ -105,8 +89,6 @@ export function buildAgentPromptContext(params: BuildAgentPromptContextParams):
|
|
|
105
89
|
`name=${workspaceName}`,
|
|
106
90
|
'summary-block:',
|
|
107
91
|
summaryBlock,
|
|
108
|
-
'structured-profile-json:',
|
|
109
|
-
structuredProfile,
|
|
110
92
|
...(promptSummary ? ['prompt-summary:', promptSummary] : []),
|
|
111
93
|
'</workspace-profile>',
|
|
112
94
|
].join('\n')
|
|
@@ -17,6 +17,12 @@ export interface AgentRuntimeConfig<TAgent extends string> {
|
|
|
17
17
|
maxSteps: number
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
export interface AgentRuntimeRuleOptions {
|
|
21
|
+
includeExecutionPlanRule?: boolean
|
|
22
|
+
includeMemr3Rule?: boolean
|
|
23
|
+
includeDomainReasoningFallbackRule?: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
20
26
|
export interface AgentToolPolicy<TSkill extends PropertyKey> {
|
|
21
27
|
resolvedMode: ChatMode
|
|
22
28
|
skills: TSkill[]
|
|
@@ -129,13 +135,14 @@ export function buildAgentRuntimeConfig<TAgent extends string, TSkill extends Pr
|
|
|
129
135
|
responseGuardSection?: string
|
|
130
136
|
learnedSkillsSection?: string
|
|
131
137
|
additionalInstructionSections?: string[]
|
|
138
|
+
ruleOptions?: AgentRuntimeRuleOptions
|
|
132
139
|
getAgentSkills: (agentId: TAgent, mode: ChatMode) => TSkill[]
|
|
133
|
-
buildGlobalRuleInstructionSection: () => string
|
|
140
|
+
buildGlobalRuleInstructionSection: (options?: AgentRuntimeRuleOptions) => string
|
|
134
141
|
buildSkillInstructionSection: (skills: TSkill[]) => string
|
|
135
142
|
buildOnboardingPromptSection: () => string
|
|
136
143
|
}): AgentRuntimeConfig<TAgent> {
|
|
137
144
|
const mode = params.mode ?? toChatMode(params.workstreamMode)
|
|
138
|
-
const rulesSection = params.buildGlobalRuleInstructionSection()
|
|
145
|
+
const rulesSection = params.buildGlobalRuleInstructionSection(params.ruleOptions)
|
|
139
146
|
const skillsSection =
|
|
140
147
|
params.skills && params.skills.length > 0 ? params.buildSkillInstructionSection(params.skills) : ''
|
|
141
148
|
const onboardingPromptSection = params.onboardingActive ? params.buildOnboardingPromptSection() : ''
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
export const CONTEXT_COMPACTION_THRESHOLD_RATIO = 0.7
|
|
2
2
|
export const CONTEXT_OUTPUT_RESERVE_TOKENS = 32_000
|
|
3
3
|
export const CONTEXT_SAFETY_MARGIN_TOKENS = 8_000
|
|
4
|
+
// Cap the output reserve against the model context window so oversized windows do not over-reserve.
|
|
5
|
+
export const CONTEXT_OUTPUT_RESERVE_MAX_RATIO = 0.35
|
|
6
|
+
// Keep a fixed guard band for provider-side overhead without letting it dominate larger context windows.
|
|
7
|
+
export const CONTEXT_SAFETY_MARGIN_MAX_RATIO = 0.1
|
|
4
8
|
export const COMPACTION_CHUNK_MAX_CHARS = 120_000
|
|
5
9
|
export const SUMMARY_ROLLUP_MAX_TOKENS = 80_000
|
|
6
10
|
export const CONTEXT_COMPACTION_INCLUDED_TOOL_NAMES = ['userQuestions', 'proceedInOnboarding'] as const
|
|
7
11
|
export const CONTEXT_COMPACTION_INCLUDED_TOOL_PREFIXES = ['linear'] as const
|
|
8
|
-
export const MEMORY_BLOCK_COMPACTION_TRIGGER_ENTRIES = 15
|
|
9
|
-
export const MEMORY_BLOCK_COMPACTION_CHUNK_ENTRIES = 10
|
|
10
12
|
export const CONTEXT_WINDOW_TOKENS = 200_000
|
|
11
13
|
export const WORKSTREAM_RAW_TAIL_MESSAGES = 6
|
|
@@ -5,8 +5,10 @@ import {
|
|
|
5
5
|
COMPACTION_CHUNK_MAX_CHARS,
|
|
6
6
|
CONTEXT_COMPACTION_INCLUDED_TOOL_NAMES,
|
|
7
7
|
CONTEXT_COMPACTION_INCLUDED_TOOL_PREFIXES,
|
|
8
|
+
CONTEXT_OUTPUT_RESERVE_MAX_RATIO,
|
|
8
9
|
CONTEXT_COMPACTION_THRESHOLD_RATIO,
|
|
9
10
|
CONTEXT_OUTPUT_RESERVE_TOKENS,
|
|
11
|
+
CONTEXT_SAFETY_MARGIN_MAX_RATIO,
|
|
10
12
|
CONTEXT_SAFETY_MARGIN_TOKENS,
|
|
11
13
|
SUMMARY_ROLLUP_MAX_TOKENS,
|
|
12
14
|
} from './context-compaction-constants'
|
|
@@ -94,74 +96,92 @@ function estimateTokens(text: string): number {
|
|
|
94
96
|
return Math.ceil(text.length / CHARS_PER_TOKEN_ESTIMATE)
|
|
95
97
|
}
|
|
96
98
|
|
|
97
|
-
function sanitizeStateText(value: string): string | null {
|
|
99
|
+
function sanitizeStateText(value: string | null | undefined): string | null {
|
|
100
|
+
if (typeof value !== 'string') return null
|
|
98
101
|
const normalized = compactWhitespace(value)
|
|
99
102
|
if (!normalized) return null
|
|
100
103
|
if (PROMPT_INJECTION_PATTERN.test(normalized)) return null
|
|
101
104
|
return normalized
|
|
102
105
|
}
|
|
103
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
|
+
|
|
104
123
|
function buildExistingWorkstreamStateForCompactionPrompt(state: WorkstreamState): Record<string, unknown> {
|
|
105
124
|
const currentPlanText = state.currentPlan ? sanitizeStateText(state.currentPlan.text) : null
|
|
106
125
|
|
|
107
|
-
const activeConstraints = state.activeConstraints
|
|
108
|
-
|
|
126
|
+
const activeConstraints = sanitizeAndMapStateField(state.activeConstraints, (constraint) => {
|
|
127
|
+
const text = sanitizeStateText(constraint.text)
|
|
128
|
+
if (!text) return null
|
|
129
|
+
return {
|
|
109
130
|
id: constraint.id,
|
|
110
|
-
text
|
|
131
|
+
text,
|
|
111
132
|
source: constraint.source,
|
|
112
133
|
approved: constraint.approved,
|
|
113
134
|
sourceMessageIds: constraint.sourceMessageIds,
|
|
114
|
-
}
|
|
115
|
-
|
|
135
|
+
}
|
|
136
|
+
})
|
|
116
137
|
|
|
117
|
-
const keyDecisions = state.keyDecisions
|
|
118
|
-
|
|
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 {
|
|
119
143
|
id: decision.id,
|
|
120
|
-
decision:
|
|
121
|
-
rationale
|
|
144
|
+
decision: normalizedDecision,
|
|
145
|
+
rationale,
|
|
122
146
|
agent: decision.agent,
|
|
123
147
|
sourceMessageIds: decision.sourceMessageIds,
|
|
124
148
|
confidence: decision.confidence,
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
Boolean(decision.decision && decision.rationale),
|
|
128
|
-
)
|
|
149
|
+
}
|
|
150
|
+
})
|
|
129
151
|
|
|
130
|
-
const tasks = state.tasks
|
|
131
|
-
|
|
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 {
|
|
132
157
|
id: task.id,
|
|
133
|
-
title
|
|
158
|
+
title,
|
|
134
159
|
status: task.status,
|
|
135
|
-
owner
|
|
160
|
+
owner,
|
|
136
161
|
externalId: task.externalId,
|
|
137
162
|
source: task.source,
|
|
138
163
|
sourceMessageIds: task.sourceMessageIds,
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const openQuestions = state.openQuestions
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
const agentContributions = state.agentContributions
|
|
163
|
-
.map((note) => ({ id: note.id, agent: note.agent, summary: sanitizeStateText(note.summary) }))
|
|
164
|
-
.filter((note): note is typeof note & { summary: string } => Boolean(note.summary))
|
|
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
|
+
})
|
|
165
185
|
|
|
166
186
|
return {
|
|
167
187
|
currentPlan: currentPlanText
|
|
@@ -177,7 +197,7 @@ function buildExistingWorkstreamStateForCompactionPrompt(state: WorkstreamState)
|
|
|
177
197
|
keyDecisions,
|
|
178
198
|
tasks,
|
|
179
199
|
openQuestions,
|
|
180
|
-
risks: state.risks
|
|
200
|
+
risks: sanitizeStateStrings(state.risks),
|
|
181
201
|
artifacts,
|
|
182
202
|
agentContributions,
|
|
183
203
|
approvedBy: state.approvedBy ? sanitizeStateText(state.approvedBy) : undefined,
|
|
@@ -323,24 +343,21 @@ function splitByCharBudget(messages: ContextMessage[], maxChars: number): Contex
|
|
|
323
343
|
}
|
|
324
344
|
|
|
325
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
|
+
|
|
326
351
|
return {
|
|
327
352
|
...(next.currentPlan !== undefined
|
|
328
353
|
? { currentPlan: next.currentPlan }
|
|
329
354
|
: base.currentPlan !== undefined
|
|
330
355
|
? { currentPlan: base.currentPlan }
|
|
331
356
|
: {}),
|
|
332
|
-
...(
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
...(
|
|
336
|
-
? { newRisks: appendUnique(base.newRisks ?? [], next.newRisks ?? []) }
|
|
337
|
-
: {}),
|
|
338
|
-
...(appendUnique(base.newQuestions ?? [], next.newQuestions ?? []).length > 0
|
|
339
|
-
? { newQuestions: appendUnique(base.newQuestions ?? [], next.newQuestions ?? []) }
|
|
340
|
-
: {}),
|
|
341
|
-
...(appendUnique(base.resolvedQuestions ?? [], next.resolvedQuestions ?? []).length > 0
|
|
342
|
-
? { resolvedQuestions: appendUnique(base.resolvedQuestions ?? [], next.resolvedQuestions ?? []) }
|
|
343
|
-
: {}),
|
|
357
|
+
...(mergedConstraints.length > 0 ? { newConstraints: mergedConstraints } : {}),
|
|
358
|
+
...(mergedRisks.length > 0 ? { newRisks: mergedRisks } : {}),
|
|
359
|
+
...(mergedQuestions.length > 0 ? { newQuestions: mergedQuestions } : {}),
|
|
360
|
+
...(mergedResolvedQuestions.length > 0 ? { resolvedQuestions: mergedResolvedQuestions } : {}),
|
|
344
361
|
...((base.newDecisions?.length ?? 0) + (next.newDecisions?.length ?? 0) > 0
|
|
345
362
|
? { newDecisions: [...(base.newDecisions ?? []), ...(next.newDecisions ?? [])] }
|
|
346
363
|
: {}),
|
|
@@ -538,7 +555,7 @@ export function mergeStateDelta(
|
|
|
538
555
|
state.approvedBy = sanitizeStateText(delta.approvedBy) ?? undefined
|
|
539
556
|
}
|
|
540
557
|
if (delta.approvedAt !== undefined) {
|
|
541
|
-
state.approvedAt = delta.approvedAt
|
|
558
|
+
state.approvedAt = delta.approvedAt ?? undefined
|
|
542
559
|
}
|
|
543
560
|
if (delta.approvalMessageId !== undefined) {
|
|
544
561
|
state.approvalMessageId = sanitizeStateText(delta.approvalMessageId) ?? undefined
|
|
@@ -678,54 +695,44 @@ export function createContextCompactionRuntime(
|
|
|
678
695
|
const candidatePlan =
|
|
679
696
|
state.currentPlan && !state.currentPlan.approved ? sanitizeStateText(state.currentPlan.text) : null
|
|
680
697
|
|
|
681
|
-
const approvedConstraints =
|
|
682
|
-
.filter((constraint) => constraint.approved)
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
const candidateConstraints =
|
|
687
|
-
.filter((constraint) => !constraint.approved)
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
const openQuestions = state.openQuestions
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
.
|
|
718
|
-
|
|
719
|
-
type: artifact.type,
|
|
720
|
-
pointer: sanitizeStateText(artifact.pointer),
|
|
721
|
-
}))
|
|
722
|
-
.filter((artifact): artifact is typeof artifact & { name: string; pointer: string } =>
|
|
723
|
-
Boolean(artifact.name && artifact.pointer),
|
|
724
|
-
)
|
|
725
|
-
|
|
726
|
-
const agentContributions = state.agentContributions
|
|
727
|
-
.map((note) => ({ agent: note.agent, summary: sanitizeStateText(note.summary) }))
|
|
728
|
-
.filter((note): note is typeof note & { summary: string } => Boolean(note.summary))
|
|
698
|
+
const approvedConstraints = sanitizeTextFieldItems(
|
|
699
|
+
state.activeConstraints.filter((constraint) => constraint.approved),
|
|
700
|
+
'text',
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
const candidateConstraints = sanitizeTextFieldItems(
|
|
704
|
+
state.activeConstraints.filter((constraint) => !constraint.approved),
|
|
705
|
+
'text',
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
const openQuestions = sanitizeStateStrings(state.openQuestions.map((question) => question.text))
|
|
709
|
+
|
|
710
|
+
const decisions = sanitizeAndMapStateField(state.keyDecisions, (decision) => {
|
|
711
|
+
const normalizedDecision = sanitizeStateText(decision.decision)
|
|
712
|
+
const rationale = sanitizeStateText(decision.rationale)
|
|
713
|
+
if (!normalizedDecision || !rationale) return null
|
|
714
|
+
return { agent: decision.agent, decision: normalizedDecision, rationale, confidence: decision.confidence }
|
|
715
|
+
})
|
|
716
|
+
|
|
717
|
+
const tasks = sanitizeAndMapStateField(state.tasks, (task) => {
|
|
718
|
+
const title = sanitizeStateText(task.title)
|
|
719
|
+
const owner = sanitizeStateText(task.owner)
|
|
720
|
+
if (!title || !owner) return null
|
|
721
|
+
return { title, status: task.status, owner, externalId: task.externalId, source: task.source }
|
|
722
|
+
})
|
|
723
|
+
|
|
724
|
+
const artifacts = sanitizeAndMapStateField(state.artifacts, (artifact) => {
|
|
725
|
+
const name = sanitizeStateText(artifact.name)
|
|
726
|
+
const pointer = sanitizeStateText(artifact.pointer)
|
|
727
|
+
if (!name || !pointer) return null
|
|
728
|
+
return { name, type: artifact.type, pointer }
|
|
729
|
+
})
|
|
730
|
+
|
|
731
|
+
const agentContributions = sanitizeAndMapStateField(state.agentContributions, (note) => {
|
|
732
|
+
const summary = sanitizeStateText(note.summary)
|
|
733
|
+
if (!summary) return null
|
|
734
|
+
return { agent: note.agent, summary }
|
|
735
|
+
})
|
|
729
736
|
|
|
730
737
|
const payload = {
|
|
731
738
|
policy: { approvedConstraintsAreBinding: true, candidateStateIsAdvisoryOnly: true },
|
|
@@ -747,7 +754,7 @@ export function createContextCompactionRuntime(
|
|
|
747
754
|
},
|
|
748
755
|
decisions,
|
|
749
756
|
openQuestions,
|
|
750
|
-
risks: state.risks
|
|
757
|
+
risks: sanitizeStateStrings(state.risks),
|
|
751
758
|
tasks,
|
|
752
759
|
artifacts,
|
|
753
760
|
agentContributions,
|
|
@@ -777,9 +784,6 @@ export function createContextCompactionRuntime(
|
|
|
777
784
|
return summaryMessage ? [summaryMessage, ...liveMessages] : [...liveMessages]
|
|
778
785
|
}
|
|
779
786
|
|
|
780
|
-
const CONTEXT_OUTPUT_RESERVE_MAX_RATIO = 0.35
|
|
781
|
-
const CONTEXT_SAFETY_MARGIN_MAX_RATIO = 0.1
|
|
782
|
-
|
|
783
787
|
const estimateThreshold = (contextSize = 256_000): number => {
|
|
784
788
|
const reservedOutput = Math.min(outputReserveTokens, Math.floor(contextSize * CONTEXT_OUTPUT_RESERVE_MAX_RATIO))
|
|
785
789
|
const safetyMargin = Math.min(safetyMarginTokens, Math.floor(contextSize * CONTEXT_SAFETY_MARGIN_MAX_RATIO))
|
|
@@ -827,6 +831,23 @@ export function createContextCompactionRuntime(
|
|
|
827
831
|
return { summary, stateDelta: delta }
|
|
828
832
|
}
|
|
829
833
|
|
|
834
|
+
const rollupSummaryIfOversized = async (
|
|
835
|
+
summary: string,
|
|
836
|
+
state: WorkstreamState,
|
|
837
|
+
): Promise<{ summary: string; state: WorkstreamState; output: CompactionOutput }> => {
|
|
838
|
+
if (estimateTokens(summary) <= summaryRollupMaxTokens) {
|
|
839
|
+
return { summary, state, output: { summary, stateDelta: {} } }
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const output = await compactContextMessages({
|
|
843
|
+
previousSummary: '',
|
|
844
|
+
existingState: state,
|
|
845
|
+
newMessages: [{ role: 'assistant', text: summary, sourceMessageId: 'summary-rollup' }],
|
|
846
|
+
})
|
|
847
|
+
|
|
848
|
+
return { summary: normalizeSummary(output.summary), state: mergeStateDelta(state, output.stateDelta, now), output }
|
|
849
|
+
}
|
|
850
|
+
|
|
830
851
|
const compactHistory = async (params: CompactHistoryParams): Promise<CompactHistoryResult> => {
|
|
831
852
|
let summaryText = normalizeSummary(params.summaryText)
|
|
832
853
|
let remainingMessages = [...params.liveMessages]
|
|
@@ -891,14 +912,10 @@ export function createContextCompactionRuntime(
|
|
|
891
912
|
let nextState = mergeStateDelta(state, compactionOutput.stateDelta, now)
|
|
892
913
|
|
|
893
914
|
if (estimateTokens(nextSummary) > summaryRollupMaxTokens) {
|
|
894
|
-
const
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
})
|
|
899
|
-
nextSummary = normalizeSummary(rollupOutput.summary)
|
|
900
|
-
nextState = mergeStateDelta(nextState, rollupOutput.stateDelta, now)
|
|
901
|
-
compactionOutput = rollupOutput
|
|
915
|
+
const rollup = await rollupSummaryIfOversized(nextSummary, nextState)
|
|
916
|
+
nextSummary = rollup.summary
|
|
917
|
+
nextState = rollup.state
|
|
918
|
+
compactionOutput = rollup.output
|
|
902
919
|
}
|
|
903
920
|
|
|
904
921
|
if (nextSummary.length >= sourceText.length) {
|