@lota-sdk/core 0.1.13 → 0.1.15
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 +5 -5
- package/src/ai/embedding-cache.ts +7 -6
- package/src/ai/index.ts +1 -0
- package/src/bifrost/bifrost.ts +12 -7
- package/src/config/agent-defaults.ts +1 -1
- package/src/config/logger.ts +7 -9
- package/src/{runtime.ts → create-runtime.ts} +6 -6
- package/src/db/cursor-pagination.ts +1 -1
- package/src/db/memory-store.ts +10 -6
- package/src/db/memory.ts +6 -4
- package/src/db/schema-fingerprint.ts +1 -0
- package/src/db/service.ts +45 -51
- package/src/db/startup.ts +3 -3
- package/src/index.ts +1 -1
- package/src/queues/context-compaction.queue.ts +4 -8
- package/src/queues/document-processor.queue.ts +7 -7
- package/src/queues/memory-consolidation.queue.ts +7 -8
- package/src/queues/post-chat-memory.queue.ts +2 -6
- package/src/queues/recent-activity-title-refinement.queue.ts +2 -6
- package/src/queues/regular-chat-memory-digest.queue.ts +4 -7
- package/src/queues/skill-extraction.queue.ts +4 -7
- package/src/queues/workstream-title-generation.queue.ts +2 -6
- package/src/redis/connection.ts +6 -3
- package/src/redis/index.ts +1 -0
- package/src/redis/org-memory-lock.ts +1 -1
- package/src/redis/redis-lease-lock.ts +41 -8
- package/src/runtime/agent-stream-helpers.ts +2 -1
- package/src/runtime/context-compaction-constants.ts +1 -1
- package/src/runtime/context-compaction-runtime.ts +6 -4
- package/src/runtime/context-compaction.ts +19 -38
- package/src/runtime/execution-plan.ts +2 -2
- package/src/runtime/helper-model.ts +3 -1
- package/src/runtime/index.ts +12 -1
- package/src/runtime/memory-block.ts +3 -2
- package/src/runtime/memory-pipeline.ts +24 -5
- package/src/runtime/plugin-types.ts +1 -1
- package/src/runtime/runtime-extensions.ts +89 -13
- package/src/runtime/title-helpers.ts +11 -2
- package/src/runtime/workstream-chat-helpers.ts +5 -6
- package/src/runtime/workstream-routing-policy.ts +0 -30
- package/src/runtime/workstream-state.ts +17 -7
- package/src/services/attachment.service.ts +1 -1
- package/src/services/context-compaction.service.ts +3 -3
- package/src/services/document-chunk.service.ts +37 -32
- package/src/services/execution-plan.service.ts +2 -0
- package/src/services/learned-skill.service.ts +6 -10
- package/src/services/{memory.utils.ts → memory-utils.ts} +4 -8
- package/src/services/memory.service.ts +21 -18
- package/src/services/organization-member.service.ts +1 -1
- package/src/services/plan-artifact.service.ts +1 -0
- package/src/services/plan-executor.service.ts +2 -18
- package/src/services/plan-helpers.ts +15 -0
- package/src/services/plan-validator.service.ts +3 -18
- package/src/services/recent-activity-title.service.ts +3 -10
- package/src/services/recent-activity.service.ts +6 -12
- package/src/services/workstream-message.service.ts +26 -16
- package/src/services/workstream-title.service.ts +1 -9
- package/src/services/{workstream-turn-preparation.ts → workstream-turn-preparation.service.ts} +401 -314
- package/src/services/workstream-turn.ts +2 -2
- package/src/services/workstream.service.ts +22 -10
- package/src/services/workstream.types.ts +7 -16
- package/src/storage/attachment-storage.service.ts +4 -4
- package/src/storage/{attachments.utils.ts → attachment-utils.ts} +1 -4
- package/src/storage/index.ts +2 -2
- package/src/system-agents/{context-compacter.agent.ts → context-compaction.agent.ts} +4 -4
- package/src/system-agents/delegated-agent-factory.ts +3 -2
- package/src/system-agents/index.ts +8 -0
- package/src/system-agents/memory-reranker.agent.ts +1 -1
- package/src/system-agents/memory.agent.ts +1 -1
- package/src/system-agents/recent-activity-title-refiner.agent.ts +1 -1
- package/src/tools/execution-plan.tool.ts +6 -2
- package/src/tools/fetch-webpage.tool.ts +20 -18
- package/src/tools/index.ts +2 -2
- package/src/tools/read-file-parts.tool.ts +1 -1
- package/src/tools/search-web.tool.ts +18 -15
- package/src/tools/{search-tools.ts → search.tool.ts} +1 -1
- package/src/tools/team-think.tool.ts +9 -5
- package/src/tools/{tool-contract.ts → tool-contracts.ts} +9 -2
- package/src/utils/async.ts +1 -1
- package/src/utils/errors.ts +15 -0
- package/src/utils/hono-error-handler.ts +1 -2
- package/src/utils/index.ts +10 -2
- package/src/utils/string.ts +14 -0
- package/src/workers/bootstrap.ts +2 -2
- package/src/workers/memory-consolidation.worker.ts +12 -12
- package/src/workers/regular-chat-memory-digest.helpers.ts +2 -7
- package/src/workers/regular-chat-memory-digest.runner.ts +9 -103
- package/src/workers/skill-extraction.runner.ts +7 -101
- package/src/workers/utils/file-section-chunker.ts +5 -3
- package/src/workers/utils/workstream-message-query.ts +106 -0
- package/src/workers/worker-utils.ts +4 -0
- package/src/runtime/retrieval-pipeline.ts +0 -3
- package/src/utils/error.ts +0 -10
- /package/src/services/{context-compaction-runtime.ts → context-compaction-runtime.singleton.ts} +0 -0
- /package/src/storage/{attachments.types.ts → attachment-types.ts} +0 -0
|
@@ -3,7 +3,7 @@ import { setTimeout as delay } from 'node:timers/promises'
|
|
|
3
3
|
|
|
4
4
|
import type IORedis from 'ioredis'
|
|
5
5
|
|
|
6
|
-
import { getErrorMessage } from '../utils/
|
|
6
|
+
import { getErrorMessage } from '../utils/errors'
|
|
7
7
|
|
|
8
8
|
interface RedisLeaseLockLogger {
|
|
9
9
|
debug?: (message: string) => void
|
|
@@ -95,16 +95,30 @@ async function releaseLeaseLock(options: RedisLeaseLockOptions & { lockValue: st
|
|
|
95
95
|
await options.redis.eval(RELEASE_LOCK_SCRIPT, 1, options.lockKey, options.lockValue)
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
export class LeaseLockLostError extends Error {
|
|
99
|
+
constructor(message: string) {
|
|
100
|
+
super(message)
|
|
101
|
+
this.name = 'LeaseLockLostError'
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
98
105
|
function startLeaseLockRefreshLoop(
|
|
99
106
|
options: Required<Pick<RedisLeaseLockOptions, 'refreshIntervalMs'>> &
|
|
100
107
|
RedisLeaseLockOptions & { lockValue: string; label: string },
|
|
108
|
+
ac: AbortController,
|
|
101
109
|
): () => void {
|
|
102
110
|
let stopped = false
|
|
103
111
|
|
|
104
112
|
const timer = setInterval(() => {
|
|
105
113
|
if (stopped) return
|
|
106
114
|
void refreshLeaseLock(options).catch((error: unknown) => {
|
|
107
|
-
|
|
115
|
+
stopped = true
|
|
116
|
+
clearInterval(timer)
|
|
117
|
+
const message = `Failed to refresh ${options.label} (${options.lockKey}): ${getErrorMessage(error)}`
|
|
118
|
+
log(options.logger, 'warn', message)
|
|
119
|
+
if (!ac.signal.aborted) {
|
|
120
|
+
ac.abort(new LeaseLockLostError(message))
|
|
121
|
+
}
|
|
108
122
|
})
|
|
109
123
|
}, options.refreshIntervalMs)
|
|
110
124
|
|
|
@@ -116,7 +130,10 @@ function startLeaseLockRefreshLoop(
|
|
|
116
130
|
}
|
|
117
131
|
}
|
|
118
132
|
|
|
119
|
-
export async function withRedisLeaseLock<T>(
|
|
133
|
+
export async function withRedisLeaseLock<T>(
|
|
134
|
+
options: RedisLeaseLockOptions,
|
|
135
|
+
fn: (signal: AbortSignal) => Promise<T>,
|
|
136
|
+
): Promise<T> {
|
|
120
137
|
const lockKey = options.lockKey.trim()
|
|
121
138
|
if (!lockKey) {
|
|
122
139
|
throw new Error('Redis lease lock requires a non-empty lock key')
|
|
@@ -138,17 +155,33 @@ export async function withRedisLeaseLock<T>(options: RedisLeaseLockOptions, fn:
|
|
|
138
155
|
log(options.logger, 'info', `Acquired ${label} (${lockKey}) after waiting waitedMs=${waitedMs}`)
|
|
139
156
|
}
|
|
140
157
|
|
|
141
|
-
const
|
|
158
|
+
const ac = new AbortController()
|
|
159
|
+
const stopRefreshLoop = startLeaseLockRefreshLoop({ ...options, lockKey, lockValue, label, refreshIntervalMs }, ac)
|
|
142
160
|
const holdStart = Date.now()
|
|
143
161
|
|
|
144
162
|
try {
|
|
145
|
-
|
|
163
|
+
const abortPromise = new Promise<never>((_, reject) => {
|
|
164
|
+
if (ac.signal.aborted) {
|
|
165
|
+
reject(ac.signal.reason as Error)
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
ac.signal.addEventListener(
|
|
169
|
+
'abort',
|
|
170
|
+
() => {
|
|
171
|
+
reject(ac.signal.reason as Error)
|
|
172
|
+
},
|
|
173
|
+
{ once: true },
|
|
174
|
+
)
|
|
175
|
+
})
|
|
176
|
+
return await Promise.race([fn(ac.signal), abortPromise])
|
|
146
177
|
} finally {
|
|
147
178
|
stopRefreshLoop()
|
|
148
179
|
const heldMs = Date.now() - holdStart
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
180
|
+
if (!ac.signal.aborted) {
|
|
181
|
+
await releaseLeaseLock({ ...options, lockKey, lockValue }).catch((error: unknown) => {
|
|
182
|
+
log(options.logger, 'warn', `Failed to release ${label} (${lockKey}): ${getErrorMessage(error)}`)
|
|
183
|
+
})
|
|
184
|
+
}
|
|
152
185
|
if (heldMs >= heldInfoThresholdMs) {
|
|
153
186
|
log(options.logger, 'info', `Released ${label} (${lockKey}) heldMs=${heldMs}`)
|
|
154
187
|
} else {
|
|
@@ -2,13 +2,14 @@ import type { ChatMessage } from '@lota-sdk/shared'
|
|
|
2
2
|
import type { LanguageModelUsage, UIMessageStreamOptions } from 'ai'
|
|
3
3
|
|
|
4
4
|
import { agentDisplayNames, getLeadAgentDisplayName } from '../config/agent-defaults'
|
|
5
|
+
import { readRecord as _readRecord } from '../utils/string'
|
|
5
6
|
|
|
6
7
|
export function readFiniteNumber(value: unknown): number | undefined {
|
|
7
8
|
return typeof value === 'number' && Number.isFinite(value) ? value : undefined
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export function readRecord(value: unknown): Record<string, unknown> | undefined {
|
|
11
|
-
return
|
|
12
|
+
return _readRecord(value) ?? undefined
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export function readOpenRouterUsageCost(usage: LanguageModelUsage): { cost?: number; upstreamInferenceCost?: number } {
|
|
@@ -7,5 +7,5 @@ export const CONTEXT_COMPACTION_INCLUDED_TOOL_NAMES = ['userQuestions', 'proceed
|
|
|
7
7
|
export const CONTEXT_COMPACTION_INCLUDED_TOOL_PREFIXES = ['linear'] as const
|
|
8
8
|
export const MEMORY_BLOCK_COMPACTION_TRIGGER_ENTRIES = 15
|
|
9
9
|
export const MEMORY_BLOCK_COMPACTION_CHUNK_ENTRIES = 10
|
|
10
|
-
export const
|
|
10
|
+
export const CONTEXT_WINDOW_TOKENS = 200_000
|
|
11
11
|
export const WORKSTREAM_RAW_TAIL_MESSAGES = 6
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createContextCompactionAgent } from '../system-agents/context-compaction.agent'
|
|
2
2
|
import {
|
|
3
3
|
buildContextCompactionPrompt,
|
|
4
4
|
buildMemoryBlockCompactionPrompt,
|
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
import type { GenerateHelperStructuredParams, GenerateHelperTextParams } from './helper-model'
|
|
19
19
|
import { StructuredCompactionOutputSchema } from './workstream-state'
|
|
20
20
|
|
|
21
|
+
const CONTEXT_COMPACTION_MAX_OUTPUT_TOKENS = 512
|
|
22
|
+
|
|
21
23
|
interface HelperModelRuntime {
|
|
22
24
|
generateHelperStructured<T>(params: GenerateHelperStructuredParams<T>): Promise<T>
|
|
23
25
|
generateHelperText(params: GenerateHelperTextParams): Promise<string>
|
|
@@ -32,7 +34,7 @@ interface CreateContextCompactionRuntimeDeps {
|
|
|
32
34
|
async function runContextCompacter(helperModelRuntime: HelperModelRuntime, params: ContextCompactionRunnerParams) {
|
|
33
35
|
const output = await helperModelRuntime.generateHelperStructured({
|
|
34
36
|
tag: 'context-compaction',
|
|
35
|
-
createAgent:
|
|
37
|
+
createAgent: createContextCompactionAgent,
|
|
36
38
|
messages: [
|
|
37
39
|
{
|
|
38
40
|
role: 'user',
|
|
@@ -76,9 +78,9 @@ export function createWiredContextCompactionRuntime(deps: CreateContextCompactio
|
|
|
76
78
|
|
|
77
79
|
return await helperModelRuntime.generateHelperText({
|
|
78
80
|
tag: 'memory-block-compaction',
|
|
79
|
-
createAgent:
|
|
81
|
+
createAgent: createContextCompactionAgent,
|
|
80
82
|
messages: [{ role: 'user', content: buildMemoryBlockCompactionPrompt({ previousSummary, newEntriesText }) }],
|
|
81
|
-
maxOutputTokens:
|
|
83
|
+
maxOutputTokens: CONTEXT_COMPACTION_MAX_OUTPUT_TOKENS,
|
|
82
84
|
})
|
|
83
85
|
}
|
|
84
86
|
|
|
@@ -2,7 +2,7 @@ import { createHash, randomUUID } from 'node:crypto'
|
|
|
2
2
|
|
|
3
3
|
import type { ChatMessage } from '@lota-sdk/shared'
|
|
4
4
|
|
|
5
|
-
import { readString } from '../utils/string'
|
|
5
|
+
import { CHARS_PER_TOKEN_ESTIMATE, compactWhitespace, readRecord, readString } from '../utils/string'
|
|
6
6
|
import {
|
|
7
7
|
COMPACTION_CHUNK_MAX_CHARS,
|
|
8
8
|
CONTEXT_COMPACTION_INCLUDED_TOOL_NAMES,
|
|
@@ -15,14 +15,8 @@ import {
|
|
|
15
15
|
import {
|
|
16
16
|
StructuredCompactionOutputSchema,
|
|
17
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
18
|
WorkstreamStateSchema,
|
|
19
|
+
applyWorkstreamStateCaps,
|
|
26
20
|
createEmptyWorkstreamState,
|
|
27
21
|
parseStructuredWorkstreamStateDelta,
|
|
28
22
|
} from './workstream-state'
|
|
@@ -99,15 +93,11 @@ const PROMPT_INJECTION_PATTERN =
|
|
|
99
93
|
/\b(ignore (all )?(previous|prior|system|developer)? instructions?|system prompt|developer prompt|tool override|jailbreak|role ?override|do not follow|bypass)\b/i
|
|
100
94
|
|
|
101
95
|
function estimateTokens(text: string): number {
|
|
102
|
-
return Math.ceil(text.length /
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function normalizeWhitespace(value: string): string {
|
|
106
|
-
return value.replace(/\s+/g, ' ').trim()
|
|
96
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN_ESTIMATE)
|
|
107
97
|
}
|
|
108
98
|
|
|
109
99
|
function sanitizeStateText(value: string): string | null {
|
|
110
|
-
const normalized =
|
|
100
|
+
const normalized = compactWhitespace(value)
|
|
111
101
|
if (!normalized) return null
|
|
112
102
|
if (PROMPT_INJECTION_PATTERN.test(normalized)) return null
|
|
113
103
|
return normalized
|
|
@@ -116,16 +106,12 @@ function sanitizeStateText(value: string): string | null {
|
|
|
116
106
|
function createStableId(prefix: string, ...parts: Array<string | number | undefined>): string {
|
|
117
107
|
const payload = parts
|
|
118
108
|
.map((part) => (part === undefined ? '' : String(part)))
|
|
119
|
-
.map((part) =>
|
|
109
|
+
.map((part) => compactWhitespace(part))
|
|
120
110
|
.join('|')
|
|
121
111
|
const hash = createHash('sha1').update(`${prefix}|${payload}`).digest('hex').slice(0, 20)
|
|
122
112
|
return `${prefix}_${hash}`
|
|
123
113
|
}
|
|
124
114
|
|
|
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
115
|
function stringifyUnknown(value: unknown): string | null {
|
|
130
116
|
if (value === undefined) return null
|
|
131
117
|
if (typeof value === 'string') {
|
|
@@ -141,11 +127,11 @@ function stringifyUnknown(value: unknown): string | null {
|
|
|
141
127
|
}
|
|
142
128
|
|
|
143
129
|
function appendUnique(values: string[], nextValues: string[]): string[] {
|
|
144
|
-
const seen = new Set(values.map((value) =>
|
|
130
|
+
const seen = new Set(values.map((value) => compactWhitespace(value).toLowerCase()))
|
|
145
131
|
const merged = [...values]
|
|
146
132
|
|
|
147
133
|
for (const value of nextValues) {
|
|
148
|
-
const normalized =
|
|
134
|
+
const normalized = compactWhitespace(value)
|
|
149
135
|
if (!normalized) continue
|
|
150
136
|
const key = normalized.toLowerCase()
|
|
151
137
|
if (seen.has(key)) continue
|
|
@@ -395,7 +381,7 @@ export function mergeStateDelta(
|
|
|
395
381
|
const normalizedRationale = sanitizeStateText(decision.rationale)
|
|
396
382
|
if (!normalizedDecision || !normalizedRationale) continue
|
|
397
383
|
|
|
398
|
-
const sourceIds = [...new Set(decision.sourceMessageIds.map((id) =>
|
|
384
|
+
const sourceIds = [...new Set(decision.sourceMessageIds.map((id) => compactWhitespace(id)).filter(Boolean))]
|
|
399
385
|
const decisionId = createStableId('decision', normalizedDecision, normalizedRationale, sourceIds.sort().join('|'))
|
|
400
386
|
const alreadyExists = state.keyDecisions.some((item) => item.id === decisionId)
|
|
401
387
|
if (alreadyExists) continue
|
|
@@ -403,7 +389,7 @@ export function mergeStateDelta(
|
|
|
403
389
|
id: decisionId,
|
|
404
390
|
decision: normalizedDecision,
|
|
405
391
|
rationale: normalizedRationale,
|
|
406
|
-
agent:
|
|
392
|
+
agent: compactWhitespace(decision.agent),
|
|
407
393
|
sourceMessageIds: sourceIds,
|
|
408
394
|
confidence: decision.confidence,
|
|
409
395
|
timestamp,
|
|
@@ -417,11 +403,9 @@ export function mergeStateDelta(
|
|
|
417
403
|
if (!title) continue
|
|
418
404
|
|
|
419
405
|
const externalId = sanitizeStateText(update.externalId ?? '')
|
|
420
|
-
const owner =
|
|
406
|
+
const owner = compactWhitespace(update.owner)
|
|
421
407
|
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
|
-
]
|
|
408
|
+
const sourceMessageIds = [...new Set(update.sourceMessageIds.map((id) => compactWhitespace(id)).filter(Boolean))]
|
|
425
409
|
const existingIndex = state.tasks.findIndex((task) => task.id === taskId)
|
|
426
410
|
const nextTask = {
|
|
427
411
|
id: taskId,
|
|
@@ -455,7 +439,7 @@ export function mergeStateDelta(
|
|
|
455
439
|
const artifactId = createStableId('artifact', name, pointer)
|
|
456
440
|
const exists = state.artifacts.some((item) => item.id === artifactId)
|
|
457
441
|
if (exists) continue
|
|
458
|
-
state.artifacts.push({ id: artifactId, name, type:
|
|
442
|
+
state.artifacts.push({ id: artifactId, name, type: compactWhitespace(artifact.type), pointer, timestamp })
|
|
459
443
|
}
|
|
460
444
|
}
|
|
461
445
|
|
|
@@ -495,13 +479,7 @@ export function mergeStateDelta(
|
|
|
495
479
|
state.approvalNote = sanitizeStateText(delta.approvalNote) ?? undefined
|
|
496
480
|
}
|
|
497
481
|
|
|
498
|
-
state
|
|
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)
|
|
482
|
+
applyWorkstreamStateCaps(state)
|
|
505
483
|
|
|
506
484
|
return WorkstreamStateSchema.parse(state)
|
|
507
485
|
}
|
|
@@ -727,9 +705,12 @@ export function createContextCompactionRuntime(
|
|
|
727
705
|
return summaryMessage ? [summaryMessage, ...liveMessages] : [...liveMessages]
|
|
728
706
|
}
|
|
729
707
|
|
|
708
|
+
const CONTEXT_OUTPUT_RESERVE_MAX_RATIO = 0.35
|
|
709
|
+
const CONTEXT_SAFETY_MARGIN_MAX_RATIO = 0.1
|
|
710
|
+
|
|
730
711
|
const estimateThreshold = (contextSize = 256_000): number => {
|
|
731
|
-
const reservedOutput = Math.min(outputReserveTokens, Math.floor(contextSize *
|
|
732
|
-
const safetyMargin = Math.min(safetyMarginTokens, Math.floor(contextSize *
|
|
712
|
+
const reservedOutput = Math.min(outputReserveTokens, Math.floor(contextSize * CONTEXT_OUTPUT_RESERVE_MAX_RATIO))
|
|
713
|
+
const safetyMargin = Math.min(safetyMarginTokens, Math.floor(contextSize * CONTEXT_SAFETY_MARGIN_MAX_RATIO))
|
|
733
714
|
const reservedThreshold = contextSize - (reservedOutput + safetyMargin)
|
|
734
715
|
const ratioThreshold = Math.floor(contextSize * thresholdRatio)
|
|
735
716
|
return Math.max(1_000, Math.min(contextSize - 1, Math.min(reservedThreshold, ratioThreshold)))
|
|
@@ -834,7 +815,7 @@ export function createContextCompactionRuntime(
|
|
|
834
815
|
const contextMessages = messagesToCompact.map(toContextMessageFromChatMessage)
|
|
835
816
|
const sourceText = toCompactionTranscript(contextMessages)
|
|
836
817
|
|
|
837
|
-
if (!
|
|
818
|
+
if (!compactWhitespace(sourceText)) {
|
|
838
819
|
const summaryPayload = buildSyntheticSummaryPayload(summaryText)
|
|
839
820
|
const outputPayload = JSON.stringify([...(summaryPayload ? [summaryPayload] : []), ...remainingMessages])
|
|
840
821
|
return {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SerializableExecutionPlan } from '@lota-sdk/shared'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const EXECUTION_PLAN_AGENT_PROTOCOL_PROMPT = `<execution-plan-protocol>
|
|
4
4
|
- Before doing multi-step work, create a contract-driven execution plan instead of tracking steps only in prose.
|
|
5
5
|
- Plans are graph-capable workflow contracts. Every execution node must define objective, instructions, deliverables, success criteria, completion checks, retry policy, failure policy, and tool/context policy.
|
|
6
6
|
- The runtime executor owns lifecycle truth. Do not claim that a node is complete until submitExecutionNodeResult succeeds.
|
|
@@ -10,7 +10,7 @@ export const EXECUTION_PLAN_AGENT_PROTOCOL_PROMPT = `<execution-plan-protocol>
|
|
|
10
10
|
- If the graph, contracts, or success criteria materially change, replace the plan instead of silently drifting.
|
|
11
11
|
</execution-plan-protocol>`
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
function formatExecutionPlanForPrompt(plan: SerializableExecutionPlan | null | undefined): string | undefined {
|
|
14
14
|
if (!plan) return undefined
|
|
15
15
|
|
|
16
16
|
const payload = {
|
|
@@ -7,6 +7,8 @@ import type {
|
|
|
7
7
|
} from 'ai'
|
|
8
8
|
import type { ZodSchema } from 'zod'
|
|
9
9
|
|
|
10
|
+
import { compactWhitespace } from '../utils/string'
|
|
11
|
+
|
|
10
12
|
export interface HelperToolLoopAgentOptions {
|
|
11
13
|
instructions?: string
|
|
12
14
|
maxOutputTokens?: number
|
|
@@ -102,7 +104,7 @@ function stringifyUnknown(value: unknown, maxChars: number): string | null {
|
|
|
102
104
|
}
|
|
103
105
|
})()
|
|
104
106
|
|
|
105
|
-
const normalized = raw
|
|
107
|
+
const normalized = compactWhitespace(raw)
|
|
106
108
|
if (!normalized) return null
|
|
107
109
|
return normalized.length > maxChars ? `${normalized.slice(0, maxChars)}...` : normalized
|
|
108
110
|
}
|
package/src/runtime/index.ts
CHANGED
|
@@ -23,4 +23,15 @@ export * from './team-consultation-prompts'
|
|
|
23
23
|
export * from './turn-lifecycle'
|
|
24
24
|
export * from './workstream-chat-helpers'
|
|
25
25
|
export * from './workstream-routing-policy'
|
|
26
|
-
export
|
|
26
|
+
export {
|
|
27
|
+
WorkstreamStateSchema,
|
|
28
|
+
type WorkstreamState,
|
|
29
|
+
type WorkstreamStateDelta,
|
|
30
|
+
StructuredWorkstreamStateDeltaSchema,
|
|
31
|
+
type StructuredWorkstreamStateDelta,
|
|
32
|
+
createEmptyStructuredWorkstreamStateDelta,
|
|
33
|
+
parseStructuredWorkstreamStateDelta,
|
|
34
|
+
StructuredCompactionOutputSchema,
|
|
35
|
+
type CompactionOutput,
|
|
36
|
+
createEmptyWorkstreamState,
|
|
37
|
+
} from './workstream-state'
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { agentDisplayNames, agentShortDisplayNames, resolveAgentNameAlias } from '../config/agent-defaults'
|
|
2
|
+
import { compactWhitespace } from '../utils/string'
|
|
2
3
|
|
|
3
4
|
function escapeRegex(value: string): string {
|
|
4
5
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
@@ -173,8 +174,7 @@ export function createMemoryBlockRuntime(options: CreateMemoryBlockRuntimeOption
|
|
|
173
174
|
)
|
|
174
175
|
.filter(Boolean)
|
|
175
176
|
|
|
176
|
-
const
|
|
177
|
-
const collapsed = candidate.replace(/\s+/g, ' ').trim()
|
|
177
|
+
const collapsed = compactWhitespace(normalizedLines.join(' '))
|
|
178
178
|
if (!collapsed) return ''
|
|
179
179
|
return collapsed
|
|
180
180
|
}
|
|
@@ -186,6 +186,7 @@ export function createMemoryBlockRuntime(options: CreateMemoryBlockRuntimeOption
|
|
|
186
186
|
try {
|
|
187
187
|
const parsed: unknown = JSON.parse(trimmed)
|
|
188
188
|
if (!Array.isArray(parsed)) return []
|
|
189
|
+
if (!parsed.every((item: unknown) => typeof item === 'object' && item !== null)) return []
|
|
189
190
|
return parsed as MemoryBlockEntry[]
|
|
190
191
|
} catch {
|
|
191
192
|
return []
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import { compactWhitespace } from '../utils/string'
|
|
2
|
+
|
|
3
|
+
const SCORE_WEIGHTS = {
|
|
4
|
+
durability: { core: 0.35, standard: 0.2, weak: 0.05 },
|
|
5
|
+
type: { decision: 0.25, preference: 0.18, default: 0.1 },
|
|
6
|
+
maxContentLength: 120,
|
|
7
|
+
} as const
|
|
8
|
+
|
|
1
9
|
interface MemoryFactInput {
|
|
2
10
|
content: string
|
|
3
11
|
confidence: number
|
|
@@ -117,9 +125,20 @@ function scoreFact<T extends MemoryFactInput>(fact: T): number {
|
|
|
117
125
|
const durability = fact.durability ?? 'standard'
|
|
118
126
|
const type = fact.type ?? 'fact'
|
|
119
127
|
|
|
120
|
-
const durabilityWeight =
|
|
121
|
-
|
|
122
|
-
|
|
128
|
+
const durabilityWeight =
|
|
129
|
+
durability === 'core'
|
|
130
|
+
? SCORE_WEIGHTS.durability.core
|
|
131
|
+
: durability === 'standard'
|
|
132
|
+
? SCORE_WEIGHTS.durability.standard
|
|
133
|
+
: SCORE_WEIGHTS.durability.weak
|
|
134
|
+
const typeWeight =
|
|
135
|
+
type === 'decision'
|
|
136
|
+
? SCORE_WEIGHTS.type.decision
|
|
137
|
+
: type === 'fact'
|
|
138
|
+
? SCORE_WEIGHTS.type.preference
|
|
139
|
+
: SCORE_WEIGHTS.type.default
|
|
140
|
+
const lengthWeight =
|
|
141
|
+
Math.min(fact.content.length, SCORE_WEIGHTS.maxContentLength) / SCORE_WEIGHTS.maxContentLength / 10
|
|
123
142
|
|
|
124
143
|
return confidence + durabilityWeight + typeWeight + lengthWeight
|
|
125
144
|
}
|
|
@@ -160,7 +179,7 @@ export function postProcessMemoryFacts<T extends MemoryFactInput>(
|
|
|
160
179
|
const deduped = new Map<string, T>()
|
|
161
180
|
|
|
162
181
|
for (const fact of rawFacts) {
|
|
163
|
-
const content = typeof fact.content === 'string' ? fact.content
|
|
182
|
+
const content = typeof fact.content === 'string' ? compactWhitespace(fact.content) : ''
|
|
164
183
|
if (!content || content.length < minChars || content.length > maxChars) continue
|
|
165
184
|
const normalizedFact = { ...fact, content }
|
|
166
185
|
const key = normalizeFactForDedupe(content)
|
|
@@ -464,7 +483,7 @@ export function createMemoryActionPlan<TRelation extends string = string>(params
|
|
|
464
483
|
}
|
|
465
484
|
|
|
466
485
|
for (const [index, item] of params.updates.memory.entries()) {
|
|
467
|
-
const text = typeof item.text === 'string' ? item.text
|
|
486
|
+
const text = typeof item.text === 'string' ? compactWhitespace(item.text) : ''
|
|
468
487
|
const itemId = typeof item.id === 'string' ? item.id.trim() : ''
|
|
469
488
|
|
|
470
489
|
switch (item.event) {
|
|
@@ -3,7 +3,7 @@ export interface LotaPluginContributions {
|
|
|
3
3
|
schemaFiles: readonly (string | URL)[]
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
-
export interface LotaPlugin<TServices =
|
|
6
|
+
export interface LotaPlugin<TServices = Record<string, unknown>, TTools = Record<string, unknown>> {
|
|
7
7
|
services: TServices
|
|
8
8
|
tools?: TTools
|
|
9
9
|
contributions: LotaPluginContributions
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ToolSet } from 'ai'
|
|
2
2
|
|
|
3
3
|
import type { RecordIdRef } from '../db/record-id'
|
|
4
|
-
import type { ReadableUploadMetadata } from '../
|
|
4
|
+
import type { ReadableUploadMetadata } from '../storage/attachment-types'
|
|
5
5
|
import type { LotaRuntimeWorkerExtensions } from './runtime-worker-registry'
|
|
6
6
|
|
|
7
7
|
export interface LotaRuntimeBackgroundCursor {
|
|
@@ -34,7 +34,7 @@ export interface LotaRuntimeWorkspaceProvider {
|
|
|
34
34
|
): Promise<LotaRuntimeWorkspaceLifecycleState> | LotaRuntimeWorkspaceLifecycleState
|
|
35
35
|
readProfileProjectionState?(
|
|
36
36
|
workspace: Record<string, unknown>,
|
|
37
|
-
): Promise<LotaRuntimeWorkspaceProjectionState |
|
|
37
|
+
): Promise<LotaRuntimeWorkspaceProjectionState | undefined> | LotaRuntimeWorkspaceProjectionState | undefined
|
|
38
38
|
buildPromptSummary?(workspaceId: RecordIdRef): Promise<string | undefined>
|
|
39
39
|
listRecentDomainEvents?(workspaceId: RecordIdRef, limit?: number): Promise<Array<Record<string, unknown>>>
|
|
40
40
|
hasActiveKnowledgeSources?(workspaceId: string): Promise<boolean>
|
|
@@ -76,11 +76,87 @@ export interface LotaRuntimeTeamThinkToolsParams {
|
|
|
76
76
|
toolProviders?: ToolSet
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
export interface BuildContextParams {
|
|
80
|
+
workstream: unknown
|
|
81
|
+
workstreamRef: RecordIdRef
|
|
82
|
+
orgRef: RecordIdRef
|
|
83
|
+
userRef: RecordIdRef
|
|
84
|
+
userName?: string | null
|
|
85
|
+
workspace: Record<string, unknown>
|
|
86
|
+
onboardingActive: boolean
|
|
87
|
+
messageText: string
|
|
88
|
+
linearInstalled: boolean
|
|
89
|
+
githubInstalled: boolean
|
|
90
|
+
indexedRepoContext: unknown
|
|
91
|
+
promptContext: unknown
|
|
92
|
+
workspaceLifecycleState: unknown
|
|
93
|
+
workspaceProfileState: unknown
|
|
94
|
+
promptSummary: string | undefined
|
|
95
|
+
recentDomainEvents: Array<Record<string, unknown>>
|
|
96
|
+
retrievedKnowledgeSection: string | undefined
|
|
97
|
+
[key: string]: unknown
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface AfterTurnParams {
|
|
101
|
+
workstream: unknown
|
|
102
|
+
workstreamRef: RecordIdRef
|
|
103
|
+
orgRef: RecordIdRef
|
|
104
|
+
userRef: RecordIdRef
|
|
105
|
+
userName?: string | null
|
|
106
|
+
onboardingActive: boolean
|
|
107
|
+
referenceUserMessage: unknown
|
|
108
|
+
assistantMessages: unknown[]
|
|
109
|
+
latestWorkstreamRecord: unknown
|
|
110
|
+
latestPersistedState: unknown
|
|
111
|
+
context: Record<string, unknown> | null
|
|
112
|
+
[key: string]: unknown
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface ResolveAgentParams {
|
|
116
|
+
agentId: string
|
|
117
|
+
mode: string
|
|
118
|
+
workstream: unknown
|
|
119
|
+
workstreamRef: RecordIdRef
|
|
120
|
+
orgRef: RecordIdRef
|
|
121
|
+
userRef: RecordIdRef
|
|
122
|
+
userName?: string | null
|
|
123
|
+
onboardingActive: boolean
|
|
124
|
+
linearInstalled: boolean
|
|
125
|
+
githubInstalled: boolean
|
|
126
|
+
reasoningProfile: string
|
|
127
|
+
skills?: string[]
|
|
128
|
+
additionalInstructionSections?: string[]
|
|
129
|
+
context: Record<string, unknown> | null
|
|
130
|
+
[key: string]: unknown
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface BuildExtraInstructionSectionsParams {
|
|
134
|
+
workstream: unknown
|
|
135
|
+
workstreamRef: RecordIdRef
|
|
136
|
+
orgRef: RecordIdRef
|
|
137
|
+
userRef: RecordIdRef
|
|
138
|
+
userName?: string | null
|
|
139
|
+
workspace: Record<string, unknown>
|
|
140
|
+
onboardingActive: boolean
|
|
141
|
+
messageText: string
|
|
142
|
+
linearInstalled: boolean
|
|
143
|
+
githubInstalled: boolean
|
|
144
|
+
indexedRepoContext: unknown
|
|
145
|
+
promptContext: unknown
|
|
146
|
+
workspaceLifecycleState: unknown
|
|
147
|
+
workspaceProfileState: unknown
|
|
148
|
+
promptSummary: string | undefined
|
|
149
|
+
recentDomainEvents: Array<Record<string, unknown>>
|
|
150
|
+
retrievedKnowledgeSection: string | undefined
|
|
151
|
+
context: Record<string, unknown> | null
|
|
152
|
+
[key: string]: unknown
|
|
153
|
+
}
|
|
154
|
+
|
|
79
155
|
export interface LotaRuntimeTurnHooks {
|
|
80
|
-
buildContext?: (params:
|
|
81
|
-
afterTurn?: (params:
|
|
82
|
-
resolveAgent?: (params:
|
|
83
|
-
buildExtraInstructionSections?: (params:
|
|
156
|
+
buildContext?: (params: BuildContextParams) => Promise<Record<string, unknown> | void>
|
|
157
|
+
afterTurn?: (params: AfterTurnParams) => Promise<void>
|
|
158
|
+
resolveAgent?: (params: ResolveAgentParams) => Promise<Record<string, unknown> | void>
|
|
159
|
+
buildExtraInstructionSections?: (params: BuildExtraInstructionSectionsParams) => Promise<string[] | void>
|
|
84
160
|
}
|
|
85
161
|
|
|
86
162
|
export interface LotaRuntimeAdapters {
|
|
@@ -90,8 +166,8 @@ export interface LotaRuntimeAdapters {
|
|
|
90
166
|
buildTeamThinkAgentTools?: (params: LotaRuntimeTeamThinkToolsParams) => Promise<{ tools: ToolSet }>
|
|
91
167
|
}
|
|
92
168
|
queues?: {
|
|
93
|
-
enqueuePostChatOrgAction?: (job: Record<string, unknown>) => Promise<void>
|
|
94
|
-
enqueueOnboardingRepoIndexFollowUp?: (job: Record<string, unknown>) => Promise<void>
|
|
169
|
+
enqueuePostChatOrgAction?: (job: Record<string, unknown>) => Promise<void>
|
|
170
|
+
enqueueOnboardingRepoIndexFollowUp?: (job: Record<string, unknown>) => Promise<void>
|
|
95
171
|
}
|
|
96
172
|
workers?: {
|
|
97
173
|
connectPluginDatabases?: () => Promise<void>
|
|
@@ -115,17 +191,17 @@ let runtimeExtensionsState: RuntimeExtensionsState = {
|
|
|
115
191
|
extraWorkers: {},
|
|
116
192
|
}
|
|
117
193
|
|
|
118
|
-
export function configureRuntimeExtensions(params
|
|
194
|
+
export function configureRuntimeExtensions(params: {
|
|
119
195
|
adapters?: LotaRuntimeAdapters
|
|
120
196
|
turnHooks?: LotaRuntimeTurnHooks
|
|
121
197
|
toolProviders?: ToolSet
|
|
122
198
|
extraWorkers?: LotaRuntimeWorkerExtensions
|
|
123
199
|
}): void {
|
|
124
200
|
runtimeExtensionsState = {
|
|
125
|
-
adapters: params
|
|
126
|
-
turnHooks: params
|
|
127
|
-
toolProviders: params
|
|
128
|
-
extraWorkers: params
|
|
201
|
+
adapters: params.adapters ?? {},
|
|
202
|
+
turnHooks: params.turnHooks ?? {},
|
|
203
|
+
toolProviders: params.toolProviders ?? EMPTY_TOOLS,
|
|
204
|
+
extraWorkers: params.extraWorkers ?? {},
|
|
129
205
|
}
|
|
130
206
|
}
|
|
131
207
|
|
|
@@ -1,12 +1,21 @@
|
|
|
1
|
+
import { compactWhitespace } from '../utils/string'
|
|
2
|
+
|
|
1
3
|
const TITLE_WORD_LIMIT = 5
|
|
2
4
|
|
|
3
5
|
export function limitTitleWords(text: string): string {
|
|
4
|
-
const words = text
|
|
6
|
+
const words = compactWhitespace(text).split(' ').filter(Boolean)
|
|
5
7
|
return words.slice(0, TITLE_WORD_LIMIT).join(' ')
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
export function deriveTitle(text: string): string {
|
|
9
|
-
const trimmed = text
|
|
11
|
+
const trimmed = compactWhitespace(text)
|
|
10
12
|
if (trimmed.length <= 60) return trimmed
|
|
11
13
|
return `${trimmed.slice(0, 57)}...`
|
|
12
14
|
}
|
|
15
|
+
|
|
16
|
+
export function normalizeTitle(value: string): string {
|
|
17
|
+
const normalized = compactWhitespace(value)
|
|
18
|
+
.replace(/^["'`]+|["'`]+$/g, '')
|
|
19
|
+
.replace(/[.!?,;:]+$/g, '')
|
|
20
|
+
return normalized.length <= 80 ? normalized : normalized.slice(0, 80).trim()
|
|
21
|
+
}
|
|
@@ -114,14 +114,13 @@ export function collectToolOutputErrors(params: {
|
|
|
114
114
|
if (typeof part !== 'object') continue
|
|
115
115
|
if (part.type !== undefined && typeof part.type !== 'string') continue
|
|
116
116
|
if (!part.type?.startsWith('tool-')) continue
|
|
117
|
-
|
|
117
|
+
|
|
118
|
+
const p = part as Record<string, unknown>
|
|
119
|
+
if (p.state !== 'output-error') continue
|
|
118
120
|
|
|
119
121
|
const toolName = part.type.slice('tool-'.length) || 'unknown'
|
|
120
|
-
const toolCallId =
|
|
121
|
-
|
|
122
|
-
? ((part as Record<string, unknown>).toolCallId as string)
|
|
123
|
-
: 'unknown'
|
|
124
|
-
const errorTextRaw = (part as Record<string, unknown>).errorText
|
|
122
|
+
const toolCallId = typeof p.toolCallId === 'string' && p.toolCallId ? p.toolCallId : 'unknown'
|
|
123
|
+
const errorTextRaw = p.errorText
|
|
125
124
|
const errorText =
|
|
126
125
|
typeof errorTextRaw === 'string' && errorTextRaw.trim()
|
|
127
126
|
? errorTextRaw.trim()
|