@lota-sdk/core 0.1.24 → 0.1.26
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lota-sdk/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"@chat-adapter/slack": "^4.23.0",
|
|
33
33
|
"@chat-adapter/state-ioredis": "^4.23.0",
|
|
34
34
|
"@logtape/logtape": "^2.0.5",
|
|
35
|
-
"@lota-sdk/shared": "0.1.
|
|
35
|
+
"@lota-sdk/shared": "0.1.26",
|
|
36
36
|
"@mendable/firecrawl-js": "^4.18.0",
|
|
37
37
|
"@surrealdb/node": "^3.0.3",
|
|
38
38
|
"ai": "^6.0.141",
|
package/src/ai/definitions.ts
CHANGED
|
@@ -296,65 +296,11 @@ export const memr3Rule = defineRule({
|
|
|
296
296
|
name: 'memr3',
|
|
297
297
|
instructions: `# MemR3 Evidence-Gap Protocol
|
|
298
298
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
- **Evidence**: a fenced code block starting with \`evidence\`.
|
|
305
|
-
- **Gaps**: a fenced code block starting with \`gaps\`.
|
|
306
|
-
- Evidence must be grounded; never speculate or include missing info in the evidence block.
|
|
307
|
-
|
|
308
|
-
## What Counts As A Gap
|
|
309
|
-
|
|
310
|
-
Treat the \`gaps\` block as **specific, queryable blocking gaps only**: missing information that materially changes the
|
|
311
|
-
recommendation, action, or confidence level, or makes an answer unsafe/unreliable.
|
|
312
|
-
|
|
313
|
-
- Each gap should be atomic enough to resolve with one targeted retrieval query or one user question.
|
|
314
|
-
- If you can provide a useful answer with reasonable startup-stage assumptions and the remaining unknowns would not
|
|
315
|
-
materially change it, set \`gaps\` to \`- None\` and proceed.
|
|
316
|
-
- Non-blocking unknowns should be handled as:
|
|
317
|
-
- explicit assumptions in the answer, and/or
|
|
318
|
-
- 1-3 follow-up questions at the end (do not block the main recommendation).
|
|
319
|
-
|
|
320
|
-
## Retrieval Loop
|
|
321
|
-
|
|
322
|
-
1. **Recall**: Use internal knowledge and the current conversation.
|
|
323
|
-
2. **Retrieve**: If a retrieval tool is available, call it to fill gaps.
|
|
324
|
-
- Prefer \`memorySearch\` for stored memories (semantic retrieval + graph expansion).
|
|
325
|
-
- If retrieved memory context is already provided in-system for this turn and blocking gaps are already closed, you
|
|
326
|
-
may skip \`memorySearch\`.
|
|
327
|
-
- If any blocking gap remains after reviewing provided context, retrieval is mandatory.
|
|
328
|
-
- Use web tools only if you have them.
|
|
329
|
-
- If multiple gaps are independent, issue multiple tool calls concurrently in the same turn.
|
|
330
|
-
- For web research, call multiple \`researchTopic\` instances in parallel for different sub-questions.
|
|
331
|
-
3. **Reflect**: Update the evidence and gaps blocks.
|
|
332
|
-
- Remove resolved gaps.
|
|
333
|
-
- Split vague gaps into smaller, searchable gaps.
|
|
334
|
-
- Drop gaps that no longer matter because the answer is robust under explicit assumptions.
|
|
335
|
-
4. **Iterate**: If the gaps block still contains blocking items, issue targeted new queries in parallel and repeat.
|
|
336
|
-
- Keep queries short (5-15 tokens) and specific.
|
|
337
|
-
- Limit to 3 retrieve/reflect cycles unless the user explicitly asks for deeper research.
|
|
338
|
-
- Stop early once remaining unknowns are non-material; do not retrieve for completeness alone.
|
|
339
|
-
5. **Answer**: Answer once blocking gaps are \`None\`.
|
|
340
|
-
- If retrieval returns no useful evidence and the answer is still robust under stated assumptions, proceed and make
|
|
341
|
-
those assumptions explicit rather than stalling.
|
|
342
|
-
- Do not say "memory search yielded nothing" or mention tool names; translate it into plain-language uncertainty.
|
|
343
|
-
|
|
344
|
-
## Output Format
|
|
345
|
-
|
|
346
|
-
\`\`\`evidence
|
|
347
|
-
- ...
|
|
348
|
-
\`\`\`
|
|
349
|
-
|
|
350
|
-
\`\`\`gaps
|
|
351
|
-
- ... (or "None")
|
|
352
|
-
\`\`\`
|
|
353
|
-
|
|
354
|
-
- Provide your response using your normal response format.
|
|
355
|
-
- Never output raw tool payloads (JSON, object dumps, or memory IDs like \`memory:...\`) in the final answer; summarize
|
|
356
|
-
them in plain language.
|
|
357
|
-
- If blocking gaps remain after the loop, do not answer; ask for the missing information instead.`,
|
|
299
|
+
When a factual answer depends on information you may not have:
|
|
300
|
+
1. Search memory and knowledge before responding.
|
|
301
|
+
2. Cite retrieved evidence; state remaining gaps explicitly.
|
|
302
|
+
3. Never fabricate facts; say "I don't have information on that" when appropriate.
|
|
303
|
+
4. Ask the user only for missing information that would materially change the answer.`,
|
|
358
304
|
})
|
|
359
305
|
|
|
360
306
|
export const domainReasoningFallbackRule = defineRule({
|
|
@@ -5,7 +5,7 @@ import type { LanguageModelMiddleware } from 'ai'
|
|
|
5
5
|
|
|
6
6
|
import { getRuntimeConfig } from '../runtime/runtime-config'
|
|
7
7
|
import { isRecord, readString } from '../utils/string'
|
|
8
|
-
import { buildAiGatewayCacheHeaders } from './cache-headers'
|
|
8
|
+
import { buildAiGatewayCacheHeaders, toAiGatewayCacheKeyPart } from './cache-headers'
|
|
9
9
|
|
|
10
10
|
type AiGatewayLanguageModel = Parameters<typeof wrapLanguageModel>[0]['model']
|
|
11
11
|
type AiGatewayExtraParams = Record<string, unknown>
|
|
@@ -29,15 +29,6 @@ const OPENROUTER_RESPONSE_HEALING_EXTRA_PARAMS = {
|
|
|
29
29
|
plugins: [{ id: 'response-healing' }],
|
|
30
30
|
} as const satisfies AiGatewayExtraParams
|
|
31
31
|
|
|
32
|
-
function toAiGatewayCacheKeyPart(value: string): string {
|
|
33
|
-
const normalized = value
|
|
34
|
-
.trim()
|
|
35
|
-
.toLowerCase()
|
|
36
|
-
.replace(/[^a-z0-9:_-]+/g, '-')
|
|
37
|
-
.replace(/-+/g, '-')
|
|
38
|
-
return normalized.replace(/^-+|-+$/g, '') || 'request'
|
|
39
|
-
}
|
|
40
|
-
|
|
41
32
|
function mergeAiGatewayHeaders(
|
|
42
33
|
existingHeaders: AiGatewayCallOptions['headers'] | undefined,
|
|
43
34
|
additionalHeaders: Record<string, string>,
|
|
@@ -64,16 +55,6 @@ function parseAiGatewayJsonRequestBody(body: BodyInit | null | undefined): Recor
|
|
|
64
55
|
return isRecord(parsed) ? parsed : null
|
|
65
56
|
}
|
|
66
57
|
|
|
67
|
-
function isAiGatewayOpenAIModelRequest(body: BodyInit | null | undefined): boolean {
|
|
68
|
-
const parsed = parseAiGatewayJsonRequestBody(body)
|
|
69
|
-
return readString(parsed?.model)?.startsWith('openai/') ?? false
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function hasAiGatewayPromptCacheRetention(body: BodyInit | null | undefined): boolean {
|
|
73
|
-
const parsed = parseAiGatewayJsonRequestBody(body)
|
|
74
|
-
return readString(parsed?.prompt_cache_retention) !== null
|
|
75
|
-
}
|
|
76
|
-
|
|
77
58
|
function withDefaultAiGatewayCacheHeaders(params: AiGatewayCallOptions, modelId: string): AiGatewayCallOptions {
|
|
78
59
|
return {
|
|
79
60
|
...params,
|
|
@@ -343,22 +324,49 @@ export function injectAiGatewayOpenAIPromptCacheRetentionRequestBody(
|
|
|
343
324
|
|
|
344
325
|
function createAiGatewayFetch(extraParams?: AiGatewayExtraParams): typeof fetch {
|
|
345
326
|
const fetchWithMutations = (input: RequestInfo | URL, init?: RequestInit | BunFetchRequestInit) => {
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
327
|
+
const parsedBody = parseAiGatewayJsonRequestBody(init?.body)
|
|
328
|
+
let nextBody = init?.body
|
|
329
|
+
let nextParsedBody = parsedBody
|
|
330
|
+
|
|
331
|
+
if (
|
|
332
|
+
nextParsedBody &&
|
|
333
|
+
readString(nextParsedBody.model)?.startsWith('openai/') &&
|
|
334
|
+
!readString(nextParsedBody.prompt_cache_retention)
|
|
335
|
+
) {
|
|
336
|
+
nextParsedBody = { ...nextParsedBody, prompt_cache_retention: OPENAI_PROMPT_CACHE_RETENTION }
|
|
337
|
+
nextBody = JSON.stringify(nextParsedBody)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (nextParsedBody && extraParams !== undefined) {
|
|
341
|
+
nextParsedBody = {
|
|
342
|
+
...nextParsedBody,
|
|
343
|
+
extra_params: isRecord(nextParsedBody.extra_params)
|
|
344
|
+
? { ...nextParsedBody.extra_params, ...extraParams }
|
|
345
|
+
: { ...extraParams },
|
|
346
|
+
}
|
|
347
|
+
nextBody = JSON.stringify(nextParsedBody)
|
|
348
|
+
}
|
|
351
349
|
|
|
352
350
|
const headers = new Headers(init?.headers)
|
|
353
|
-
if (
|
|
351
|
+
if (
|
|
352
|
+
extraParams !== undefined ||
|
|
353
|
+
(readString(nextParsedBody?.model)?.startsWith('openai/') &&
|
|
354
|
+
readString(nextParsedBody?.prompt_cache_retention) !== null)
|
|
355
|
+
) {
|
|
354
356
|
// Bifrost only forwards provider-specific extra params when passthrough is enabled.
|
|
355
357
|
headers.set(AI_GATEWAY_EXTRA_PARAMS_HEADER, 'true')
|
|
356
358
|
}
|
|
357
359
|
|
|
358
|
-
return globalThis.fetch(input, { ...init, headers, body })
|
|
360
|
+
return globalThis.fetch(input, { ...init, headers, body: nextBody })
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const preconnect = globalThis.fetch.preconnect
|
|
364
|
+
|
|
365
|
+
if (typeof preconnect !== 'function') {
|
|
366
|
+
return fetchWithMutations as typeof fetch
|
|
359
367
|
}
|
|
360
368
|
|
|
361
|
-
return Object.assign(fetchWithMutations, { preconnect:
|
|
369
|
+
return Object.assign(fetchWithMutations, { preconnect: preconnect.bind(globalThis.fetch) })
|
|
362
370
|
}
|
|
363
371
|
|
|
364
372
|
function createAiGatewayProvider(extraParams?: AiGatewayExtraParams) {
|
|
@@ -7,6 +7,15 @@ export const AI_GATEWAY_STRICT_SEMANTIC_CACHE_THRESHOLD = 0.975
|
|
|
7
7
|
|
|
8
8
|
export type AiGatewayCacheType = 'direct' | 'semantic'
|
|
9
9
|
|
|
10
|
+
export function toAiGatewayCacheKeyPart(value: string): string {
|
|
11
|
+
const normalized = value
|
|
12
|
+
.trim()
|
|
13
|
+
.toLowerCase()
|
|
14
|
+
.replace(/[^a-z0-9:_-]+/g, '-')
|
|
15
|
+
.replace(/-+/g, '-')
|
|
16
|
+
return normalized.replace(/^-+|-+$/g, '') || 'request'
|
|
17
|
+
}
|
|
18
|
+
|
|
10
19
|
export function buildAiGatewayCacheHeaders(
|
|
11
20
|
cacheKey: string,
|
|
12
21
|
ttl?: string,
|
|
@@ -2,15 +2,19 @@ export {
|
|
|
2
2
|
AI_GATEWAY_REASONING_SUMMARY_LEVEL,
|
|
3
3
|
OPENAI_HIGH_REASONING_PROVIDER_OPTIONS,
|
|
4
4
|
OPENAI_REASONING_MODEL_ID,
|
|
5
|
-
OPENROUTER_DELEGATED_REASONING_MODEL_ID,
|
|
6
5
|
OPENROUTER_FAST_REASONING_MODEL_ID,
|
|
6
|
+
OPENROUTER_GEMINI_FLASH_MODEL_ID,
|
|
7
7
|
OPENROUTER_HIGH_REASONING_PROVIDER_OPTIONS,
|
|
8
8
|
OPENROUTER_LOW_REASONING_PROVIDER_OPTIONS,
|
|
9
9
|
OPENROUTER_MEDIUM_REASONING_PROVIDER_OPTIONS,
|
|
10
10
|
OPENROUTER_MINIMAL_REASONING_PROVIDER_OPTIONS,
|
|
11
|
-
OPENROUTER_STRUCTURED_HELPER_MODEL_ID,
|
|
12
11
|
OPENROUTER_STRUCTURED_REASONING_MODEL_ID,
|
|
13
12
|
OPENROUTER_TEAM_AGENT_MODEL_ID,
|
|
14
13
|
OPENROUTER_WEB_RESEARCH_MODEL_ID,
|
|
15
14
|
OPENROUTER_XHIGH_REASONING_PROVIDER_OPTIONS,
|
|
16
15
|
} from '@lota-sdk/shared'
|
|
16
|
+
|
|
17
|
+
// Both aliases point to the same underlying model. Keep the names separate so
|
|
18
|
+
// SDK system agents and host delegated agents can diverge independently later.
|
|
19
|
+
export { OPENROUTER_GEMINI_FLASH_MODEL_ID as OPENROUTER_STRUCTURED_HELPER_MODEL_ID } from '@lota-sdk/shared'
|
|
20
|
+
export { OPENROUTER_GEMINI_FLASH_MODEL_ID as OPENROUTER_DELEGATED_REASONING_MODEL_ID } from '@lota-sdk/shared'
|
package/src/create-runtime.ts
CHANGED
|
@@ -33,10 +33,8 @@ import type { attachmentService } from './services/attachment.service'
|
|
|
33
33
|
import { attachmentService as attachmentServiceSingleton } from './services/attachment.service'
|
|
34
34
|
import type { autonomousJobService } from './services/autonomous-job.service'
|
|
35
35
|
import { autonomousJobService as autonomousJobServiceSingleton } from './services/autonomous-job.service'
|
|
36
|
-
import { coordinationRegistryService as coordinationRegistryServiceSingleton } from './services/coordination-registry.service'
|
|
37
36
|
import type { documentChunkService } from './services/document-chunk.service'
|
|
38
37
|
import { documentChunkService as documentChunkServiceSingleton } from './services/document-chunk.service'
|
|
39
|
-
import { domainAgentExecutorService } from './services/domain-agent-executor.service'
|
|
40
38
|
import type { executionPlanService } from './services/execution-plan.service'
|
|
41
39
|
import { executionPlanService as executionPlanServiceSingleton } from './services/execution-plan.service'
|
|
42
40
|
import type { memoryService } from './services/memory.service'
|
|
@@ -50,7 +48,6 @@ import type { organizationService } from './services/organization.service'
|
|
|
50
48
|
import { organizationService as organizationServiceSingleton } from './services/organization.service'
|
|
51
49
|
import type { planAgentQueryService } from './services/plan-agent-query.service'
|
|
52
50
|
import { planAgentQueryService as planAgentQueryServiceSingleton } from './services/plan-agent-query.service'
|
|
53
|
-
import { playbookRegistryService } from './services/playbook-registry.service'
|
|
54
51
|
import type { recentActivityTitleService } from './services/recent-activity-title.service'
|
|
55
52
|
import { recentActivityTitleService as recentActivityTitleServiceSingleton } from './services/recent-activity-title.service'
|
|
56
53
|
import type { recentActivityService } from './services/recent-activity.service'
|
|
@@ -143,7 +140,6 @@ export interface LotaRuntime {
|
|
|
143
140
|
isApprovalContinuationRequest: typeof isApprovalContinuationRequest
|
|
144
141
|
runWorkstreamTurnInBackground: typeof runWorkstreamTurnInBackground
|
|
145
142
|
triggerPlanNodeTurn: typeof triggerPlanNodeTurn
|
|
146
|
-
syncPlaybookTemplates: typeof playbookRegistryService.syncPlaybookTemplates
|
|
147
143
|
}
|
|
148
144
|
lota: {
|
|
149
145
|
organizations: {
|
|
@@ -237,7 +233,7 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
|
|
|
237
233
|
const redisManager = createRedisConnectionManager({ url: runtimeConfig.redis.url })
|
|
238
234
|
setRedisConnectionManager(redisManager)
|
|
239
235
|
configureEmbeddingCache(redisManager.getConnection(), runtimeConfig.memory.embeddingCacheTtlSeconds)
|
|
240
|
-
configureBackgroundProcessing(
|
|
236
|
+
configureBackgroundProcessing()
|
|
241
237
|
configureSocialChatHistory({ keyPrefix: runtimeConfig.socialChat?.historyRedisKeyPrefix })
|
|
242
238
|
|
|
243
239
|
const socialChatAgentId = runtimeConfig.socialChat?.agentId?.trim() || 'socialChat'
|
|
@@ -273,21 +269,10 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
|
|
|
273
269
|
})
|
|
274
270
|
|
|
275
271
|
const pluginRuntime = runtimeConfig.pluginRuntime ?? {}
|
|
276
|
-
domainAgentExecutorService.configure(pluginRuntime)
|
|
277
272
|
if (runtimeConfig.graphDesigner) {
|
|
278
273
|
configureGraphDesigner(runtimeConfig.graphDesigner)
|
|
279
274
|
}
|
|
280
275
|
|
|
281
|
-
for (const [pluginRef, plugin] of Object.entries(pluginRuntime)) {
|
|
282
|
-
const signals = plugin.contributions.signals
|
|
283
|
-
if (signals && signals.length > 0) {
|
|
284
|
-
coordinationRegistryServiceSingleton.register(pluginRef, [...signals])
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
coordinationRegistryServiceSingleton.validate()
|
|
288
|
-
// Collect playbook contributions early to fail fast on misconfiguration
|
|
289
|
-
playbookRegistryService.collectPlaybooks()
|
|
290
|
-
|
|
291
276
|
const pluginContributions = Object.values(pluginRuntime).map((plugin) => plugin.contributions)
|
|
292
277
|
const schemaFiles = [...getBuiltInSchemaFiles(), ...(runtimeConfig.extraSchemaFiles ?? [])]
|
|
293
278
|
const hostContributionSchemaFiles = pluginContributions.flatMap((plugin) => plugin.schemaFiles)
|
|
@@ -417,7 +402,6 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
|
|
|
417
402
|
isApprovalContinuationRequest: isApprovalContinuationRequestSingleton,
|
|
418
403
|
runWorkstreamTurnInBackground: runWorkstreamTurnInBackgroundSingleton,
|
|
419
404
|
triggerPlanNodeTurn: triggerPlanNodeTurnSingleton,
|
|
420
|
-
syncPlaybookTemplates: playbookRegistryService.syncPlaybookTemplates.bind(playbookRegistryService),
|
|
421
405
|
},
|
|
422
406
|
lota,
|
|
423
407
|
redis: {
|
package/src/db/memory-types.ts
CHANGED
|
@@ -14,6 +14,9 @@ export type MemoryType = z.infer<typeof MemoryTypeSchema>
|
|
|
14
14
|
export const DurabilitySchema = z.enum(['core', 'standard', 'ephemeral'])
|
|
15
15
|
export type Durability = z.infer<typeof DurabilitySchema>
|
|
16
16
|
|
|
17
|
+
export const MemoryImportanceClassificationSchema = z.enum(['durable', 'transient', 'uncertain'])
|
|
18
|
+
export type MemoryImportanceClassification = z.infer<typeof MemoryImportanceClassificationSchema>
|
|
19
|
+
|
|
17
20
|
export const MemoryEventSchema = z.enum(['ADD', 'UPDATE', 'DELETE', 'NONE'])
|
|
18
21
|
export type MemoryEvent = z.infer<typeof MemoryEventSchema>
|
|
19
22
|
|
|
@@ -103,6 +106,15 @@ const ExtractedFactSchema = z.object({
|
|
|
103
106
|
.describe(
|
|
104
107
|
'core: business decisions, technical architecture, confirmed requirements. standard: general facts, moderate inferences. ephemeral: preferences, one-off interactions, formatting choices.',
|
|
105
108
|
),
|
|
109
|
+
importance: z
|
|
110
|
+
.number()
|
|
111
|
+
.min(0)
|
|
112
|
+
.max(1)
|
|
113
|
+
.describe('Long-term usefulness score from 0 to 1 for storing this fact as memory.'),
|
|
114
|
+
classification: MemoryImportanceClassificationSchema.describe(
|
|
115
|
+
'Whether this fact is durable enough for long-term memory.',
|
|
116
|
+
),
|
|
117
|
+
rationale: z.string().min(1).describe('Concise rationale for the importance and classification.'),
|
|
106
118
|
})
|
|
107
119
|
|
|
108
120
|
export type ExtractedFact = z.infer<typeof ExtractedFactSchema>
|
|
@@ -186,14 +198,7 @@ const MemoryDeltaItemSchema = z
|
|
|
186
198
|
export const MemoryDeltaSchema = z
|
|
187
199
|
.object({ deltas: z.array(MemoryDeltaItemSchema).describe('Classification output for each new fact.') })
|
|
188
200
|
.strict()
|
|
189
|
-
export
|
|
190
|
-
.object({
|
|
191
|
-
importance: z.number().min(0).max(1).describe('Long-term usefulness score from 0 to 1 for storing this memory.'),
|
|
192
|
-
durability: DurabilitySchema.describe('Expected durability for this memory.'),
|
|
193
|
-
classification: z.enum(['durable', 'transient', 'uncertain']).describe('Durability classification for storage.'),
|
|
194
|
-
rationale: z.string().min(1).describe('Concise rationale for the score/classification.'),
|
|
195
|
-
})
|
|
196
|
-
.strict()
|
|
201
|
+
export type MemoryDeltaOutput = z.infer<typeof MemoryDeltaSchema>
|
|
197
202
|
export interface Message {
|
|
198
203
|
role: 'user' | 'agent'
|
|
199
204
|
content: string
|
package/src/db/memory.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { z } from 'zod'
|
|
2
|
+
|
|
1
3
|
import { aiLogger } from '../config/logger'
|
|
2
4
|
import type { CreateHelperAgentFn } from '../runtime/helper-model'
|
|
3
5
|
import { createHelperModelRuntime } from '../runtime/helper-model'
|
|
@@ -7,6 +9,7 @@ import {
|
|
|
7
9
|
compileMemoryUpdatesFromDelta,
|
|
8
10
|
createMemoryActionPlan,
|
|
9
11
|
postProcessMemoryFacts,
|
|
12
|
+
projectMemoryDeltaToScope,
|
|
10
13
|
} from '../runtime/memory-pipeline'
|
|
11
14
|
import { getFactRetrievalMessages } from '../runtime/memory-prompts-fact'
|
|
12
15
|
import { parseMessages } from '../runtime/memory-prompts-parse'
|
|
@@ -50,6 +53,12 @@ interface PreparedScopeUpdate {
|
|
|
50
53
|
existingMemories: Array<{ id: string; text: string }>
|
|
51
54
|
}
|
|
52
55
|
|
|
56
|
+
interface ScopedExistingMemories {
|
|
57
|
+
options: AddOptions
|
|
58
|
+
existingMemories: Array<{ id: string; text: string }>
|
|
59
|
+
scopeMemoryIdsByUnionId: Record<string, string[]>
|
|
60
|
+
}
|
|
61
|
+
|
|
53
62
|
export class Memory {
|
|
54
63
|
private store: SurrealMemoryStore
|
|
55
64
|
private createAgent: CreateHelperAgentFn
|
|
@@ -72,6 +81,12 @@ export class Memory {
|
|
|
72
81
|
return sections.join('\n\n')
|
|
73
82
|
}
|
|
74
83
|
|
|
84
|
+
private buildMemoryUnionId(text: string): string | null {
|
|
85
|
+
const normalized = this.normalizeMemoryDeltaText(text, MEMORY_DELTA_MEMORY_TEXT_MAX_CHARS)
|
|
86
|
+
if (!normalized) return null
|
|
87
|
+
return `union_${new Bun.CryptoHasher('sha256').update(normalized).digest('hex')}`
|
|
88
|
+
}
|
|
89
|
+
|
|
75
90
|
async insert(
|
|
76
91
|
content: string,
|
|
77
92
|
options: {
|
|
@@ -166,33 +181,6 @@ export class Memory {
|
|
|
166
181
|
return this.store.getStaleMemories(scopeId, limit)
|
|
167
182
|
}
|
|
168
183
|
|
|
169
|
-
async add(
|
|
170
|
-
messages: Message[],
|
|
171
|
-
options: AddOptions,
|
|
172
|
-
extractionOptions?: { customPrompt?: string; maxFacts?: number },
|
|
173
|
-
): Promise<void> {
|
|
174
|
-
const facts = await this.extractFactsFromMessages(messages, extractionOptions)
|
|
175
|
-
if (facts.length === 0) return
|
|
176
|
-
|
|
177
|
-
aiLogger.debug`Extracted ${facts.length} facts from conversation`
|
|
178
|
-
|
|
179
|
-
await this.applyFactsToScope(facts, options)
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async addMultiScope(
|
|
183
|
-
messages: Message[],
|
|
184
|
-
scopes: AddOptions[],
|
|
185
|
-
extractionOptions?: { customPrompt?: string; maxFacts?: number },
|
|
186
|
-
): Promise<void> {
|
|
187
|
-
if (scopes.length === 0) return
|
|
188
|
-
const facts = await this.extractFactsFromMessages(messages, extractionOptions)
|
|
189
|
-
if (facts.length === 0) return
|
|
190
|
-
|
|
191
|
-
aiLogger.debug`Extracted ${facts.length} facts, applying to ${scopes.length} scopes`
|
|
192
|
-
|
|
193
|
-
await this.applyFactsToScopes(facts, scopes)
|
|
194
|
-
}
|
|
195
|
-
|
|
196
184
|
async extractFactsFromMessages(
|
|
197
185
|
messages: Message[],
|
|
198
186
|
extractionOptions?: { customPrompt?: string; maxFacts?: number },
|
|
@@ -220,11 +208,52 @@ export class Memory {
|
|
|
220
208
|
async prepareFactsToScopes(facts: ExtractedFact[], scopes: AddOptions[]): Promise<PreparedScopeUpdate[]> {
|
|
221
209
|
if (facts.length === 0 || scopes.length === 0) return []
|
|
222
210
|
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
211
|
+
const factMaps = buildMemoryFactMaps(facts)
|
|
212
|
+
const factContents = facts.map((fact) => fact.content)
|
|
213
|
+
const scopePayloads: ScopedExistingMemories[] = await Promise.all(
|
|
214
|
+
scopes.map(async (scopeOptions) => {
|
|
215
|
+
const existingMemories = await this.store.list({
|
|
216
|
+
scopeId: scopeOptions.scopeId,
|
|
217
|
+
memoryType: scopeOptions.memoryType,
|
|
218
|
+
})
|
|
219
|
+
const normalizedMemories = existingMemories.map((memory) => ({ id: memory.id, text: memory.content }))
|
|
220
|
+
const scopeMemoryIdsByUnionId: Record<string, string[]> = {}
|
|
221
|
+
for (const memory of normalizedMemories) {
|
|
222
|
+
const unionId = this.buildMemoryUnionId(memory.text)
|
|
223
|
+
if (!unionId) continue
|
|
224
|
+
;(scopeMemoryIdsByUnionId[unionId] ??= []).push(memory.id)
|
|
225
|
+
}
|
|
226
|
+
return { options: scopeOptions, existingMemories: normalizedMemories, scopeMemoryIdsByUnionId }
|
|
227
|
+
}),
|
|
228
|
+
)
|
|
229
|
+
const unionMemories = new Map<string, { id: string; text: string }>()
|
|
230
|
+
for (const scopePayload of scopePayloads) {
|
|
231
|
+
for (const memory of scopePayload.existingMemories) {
|
|
232
|
+
const unionId = this.buildMemoryUnionId(memory.text)
|
|
233
|
+
if (!unionId || unionMemories.has(unionId)) continue
|
|
234
|
+
const normalizedText = this.normalizeMemoryDeltaText(memory.text, MEMORY_DELTA_MEMORY_TEXT_MAX_CHARS)
|
|
235
|
+
if (!normalizedText) continue
|
|
236
|
+
unionMemories.set(unionId, { id: unionId, text: normalizedText })
|
|
237
|
+
}
|
|
226
238
|
}
|
|
227
|
-
|
|
239
|
+
|
|
240
|
+
const delta = await this.determineDelta([...unionMemories.values()], factContents)
|
|
241
|
+
return scopePayloads.map(({ options, existingMemories, scopeMemoryIdsByUnionId }) => ({
|
|
242
|
+
options,
|
|
243
|
+
updates: MemoryUpdateSchema.parse(
|
|
244
|
+
compileMemoryUpdatesFromDelta({
|
|
245
|
+
existingMemories,
|
|
246
|
+
newFacts: factContents,
|
|
247
|
+
delta: projectMemoryDeltaToScope({
|
|
248
|
+
delta,
|
|
249
|
+
scopeMemoryIds: existingMemories.map((memory) => memory.id),
|
|
250
|
+
scopeMemoryIdsByUnionId,
|
|
251
|
+
}),
|
|
252
|
+
}),
|
|
253
|
+
),
|
|
254
|
+
factMaps,
|
|
255
|
+
existingMemories,
|
|
256
|
+
}))
|
|
228
257
|
}
|
|
229
258
|
|
|
230
259
|
async applyPreparedScopeUpdates(prepared: PreparedScopeUpdate[]): Promise<void> {
|
|
@@ -235,23 +264,6 @@ export class Memory {
|
|
|
235
264
|
}
|
|
236
265
|
}
|
|
237
266
|
|
|
238
|
-
private async applyFactsToScope(facts: ExtractedFact[], options: AddOptions): Promise<void> {
|
|
239
|
-
const prepared = await this.prepareFactsForScope(facts, options)
|
|
240
|
-
await this.applyPreparedScopeUpdates([prepared])
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
private async prepareFactsForScope(facts: ExtractedFact[], options: AddOptions): Promise<PreparedScopeUpdate> {
|
|
244
|
-
const factMaps = buildMemoryFactMaps(facts)
|
|
245
|
-
|
|
246
|
-
const existingMemories = await this.store.list({ scopeId: options.scopeId, memoryType: options.memoryType })
|
|
247
|
-
const memoryDeltaInput = existingMemories.map((m) => ({ id: m.id, text: m.content }))
|
|
248
|
-
|
|
249
|
-
const factContents = facts.map((f) => f.content)
|
|
250
|
-
const updates = await this.determineUpdates(memoryDeltaInput, factContents)
|
|
251
|
-
|
|
252
|
-
return { options, updates, factMaps, existingMemories: memoryDeltaInput }
|
|
253
|
-
}
|
|
254
|
-
|
|
255
267
|
private async extractFacts(
|
|
256
268
|
parsedMessages: string,
|
|
257
269
|
extractionOptions?: { customPrompt?: string; maxFacts?: number },
|
|
@@ -284,12 +296,21 @@ export class Memory {
|
|
|
284
296
|
}
|
|
285
297
|
}
|
|
286
298
|
|
|
287
|
-
private async
|
|
299
|
+
private async determineDelta(
|
|
288
300
|
existingMemories: { id: string; text: string }[],
|
|
289
301
|
newFacts: string[],
|
|
290
|
-
): Promise<
|
|
302
|
+
): Promise<{ deltas: Array<z.infer<typeof MemoryDeltaSchema>['deltas'][number]> }> {
|
|
291
303
|
if (existingMemories.length === 0) {
|
|
292
|
-
return {
|
|
304
|
+
return {
|
|
305
|
+
deltas: newFacts.map((fact) => ({
|
|
306
|
+
fact,
|
|
307
|
+
classification: 'new' as const,
|
|
308
|
+
targetMemoryIds: [],
|
|
309
|
+
invalidateTargetIds: [],
|
|
310
|
+
relations: [],
|
|
311
|
+
rationale: 'No existing memories in scope.',
|
|
312
|
+
})),
|
|
313
|
+
}
|
|
293
314
|
}
|
|
294
315
|
|
|
295
316
|
const candidateMemories = this.selectDeltaCandidateMemories(existingMemories, newFacts)
|
|
@@ -306,8 +327,7 @@ export class Memory {
|
|
|
306
327
|
messages: [{ role: 'user', content: userPrompt }],
|
|
307
328
|
schema: MemoryDeltaSchema,
|
|
308
329
|
})
|
|
309
|
-
|
|
310
|
-
return MemoryUpdateSchema.parse(compiled)
|
|
330
|
+
return deltas
|
|
311
331
|
} catch (error) {
|
|
312
332
|
aiLogger.error`Failed to determine memory updates: ${error}`
|
|
313
333
|
throw error
|
|
@@ -431,6 +451,7 @@ export class Memory {
|
|
|
431
451
|
updates,
|
|
432
452
|
memoryType: options.memoryType,
|
|
433
453
|
explicitImportance: options.importance,
|
|
454
|
+
extractedImportanceByKey: factMaps.extractedImportanceByKey,
|
|
434
455
|
confidenceByKey: factMaps.confidenceByKey,
|
|
435
456
|
durabilityByKey: factMaps.durabilityByKey,
|
|
436
457
|
categoryByKey: factMaps.categoryByKey,
|
|
@@ -5,6 +5,7 @@ import { serverLogger } from '../config/logger'
|
|
|
5
5
|
import { databaseService } from '../db/service'
|
|
6
6
|
import { autonomousJobService } from '../services/autonomous-job.service'
|
|
7
7
|
import { queueJobService } from '../services/queue-job.service'
|
|
8
|
+
import { buildAutonomousAtJobId } from '../utils/autonomous-job-ids'
|
|
8
9
|
import type { WorkerHandle } from '../workers/worker-utils'
|
|
9
10
|
import { DEFAULT_JOB_RETENTION } from '../workers/worker-utils'
|
|
10
11
|
import { createQueueFactory } from './queue-factory'
|
|
@@ -43,14 +44,6 @@ function buildAutonomousSchedulerId(autonomousJobId: string): string {
|
|
|
43
44
|
return `autonomous:${autonomousJobId}`
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
function encodeBullmqId(raw: string): string {
|
|
47
|
-
return Buffer.from(raw).toString('base64url')
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function buildAutonomousAtJobId(autonomousJobId: string): string {
|
|
51
|
-
return `autonomous-at-${encodeBullmqId(autonomousJobId)}`
|
|
52
|
-
}
|
|
53
|
-
|
|
54
47
|
export async function enqueueAutonomousJobRun(params: {
|
|
55
48
|
payload: AutonomousJobQueuePayload
|
|
56
49
|
delayMs?: number
|
|
@@ -18,11 +18,11 @@ async function processContextCompactionJob(job: Job<ContextCompactionJob>): Prom
|
|
|
18
18
|
|
|
19
19
|
const { entityId, contextSize } = job.data
|
|
20
20
|
const workstreamRef = ensureRecordId(entityId, TABLES.WORKSTREAM)
|
|
21
|
-
await workstreamService.
|
|
21
|
+
await workstreamService.setCompacting(workstreamRef, true)
|
|
22
22
|
try {
|
|
23
23
|
await contextCompactionService.compactWorkstreamHistory({ workstreamId: workstreamRef, contextSize })
|
|
24
24
|
} finally {
|
|
25
|
-
await workstreamService.
|
|
25
|
+
await workstreamService.setCompacting(workstreamRef, false)
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
package/src/queues/index.ts
CHANGED
|
@@ -4,12 +4,8 @@ export * from './context-compaction.queue'
|
|
|
4
4
|
export * from './delayed-node-promotion.queue'
|
|
5
5
|
export * from './document-processor.queue'
|
|
6
6
|
export * from './memory-consolidation.queue'
|
|
7
|
+
export * from './organization-learning.queue'
|
|
7
8
|
export * from './plan-agent-heartbeat.queue'
|
|
8
9
|
export * from './plan-scheduler.queue'
|
|
9
10
|
export * from './post-chat-memory.queue'
|
|
10
|
-
export * from './
|
|
11
|
-
export * from './regular-chat-memory-digest.config'
|
|
12
|
-
export * from './regular-chat-memory-digest.queue'
|
|
13
|
-
export * from './skill-extraction.config'
|
|
14
|
-
export * from './skill-extraction.queue'
|
|
15
|
-
export * from './workstream-title-generation.queue'
|
|
11
|
+
export * from './title-generation.queue'
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { getWorkerPath, LONG_JOB_LOCK_DURATION_MS } from '../workers/worker-utils'
|
|
2
|
+
import { createQueueFactory } from './queue-factory'
|
|
3
|
+
|
|
4
|
+
export const ORGANIZATION_LEARNING_QUEUE = 'organization-learning'
|
|
5
|
+
|
|
6
|
+
const ORGANIZATION_LEARNING_DELAY_MS = 15 * 60 * 1000
|
|
7
|
+
|
|
8
|
+
// This queue merges the two organization-scoped background learning jobs:
|
|
9
|
+
// delayed regular-chat memory digestion and skill extraction.
|
|
10
|
+
|
|
11
|
+
export interface RegularChatMemoryDigestJob {
|
|
12
|
+
kind: 'regular-chat-memory-digest'
|
|
13
|
+
orgId: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SkillExtractionJob {
|
|
17
|
+
kind: 'skill-extraction'
|
|
18
|
+
orgId: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type OrganizationLearningJob = RegularChatMemoryDigestJob | SkillExtractionJob
|
|
22
|
+
|
|
23
|
+
function buildOrganizationLearningDeduplicationId(kind: OrganizationLearningJob['kind'], orgId: string): string {
|
|
24
|
+
return kind === 'regular-chat-memory-digest' ? `regular-chat-digest:${orgId}` : `skill-extraction:${orgId}`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function buildRegularChatMemoryDigestDeduplicationId(orgId: string): string {
|
|
28
|
+
return buildOrganizationLearningDeduplicationId('regular-chat-memory-digest', orgId)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function buildRegularChatMemoryDigestJobOptions(orgId: string) {
|
|
32
|
+
return {
|
|
33
|
+
delay: ORGANIZATION_LEARNING_DELAY_MS,
|
|
34
|
+
deduplication: { id: buildRegularChatMemoryDigestDeduplicationId(orgId) },
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function buildSkillExtractionDeduplicationId(orgId: string): string {
|
|
39
|
+
return buildOrganizationLearningDeduplicationId('skill-extraction', orgId)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function buildSkillExtractionJobOptions(orgId: string) {
|
|
43
|
+
return { delay: ORGANIZATION_LEARNING_DELAY_MS, deduplication: { id: buildSkillExtractionDeduplicationId(orgId) } }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const organizationLearningQueue = createQueueFactory<OrganizationLearningJob>({
|
|
47
|
+
name: ORGANIZATION_LEARNING_QUEUE,
|
|
48
|
+
displayName: 'Organization learning',
|
|
49
|
+
jobName: 'organization-learning',
|
|
50
|
+
concurrency: 4,
|
|
51
|
+
lockDuration: LONG_JOB_LOCK_DURATION_MS,
|
|
52
|
+
defaultJobOptions: { attempts: 2, backoff: { type: 'exponential', delay: 5_000 } },
|
|
53
|
+
processorPath: getWorkerPath('organization-learning.worker.ts'),
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
export function enqueueRegularChatMemoryDigest(job: Omit<RegularChatMemoryDigestJob, 'kind'>) {
|
|
57
|
+
return organizationLearningQueue.enqueue(
|
|
58
|
+
{ kind: 'regular-chat-memory-digest', ...job },
|
|
59
|
+
buildRegularChatMemoryDigestJobOptions(job.orgId),
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function enqueueSkillExtraction(job: Omit<SkillExtractionJob, 'kind'>) {
|
|
64
|
+
return organizationLearningQueue.enqueue(
|
|
65
|
+
{ kind: 'skill-extraction', ...job },
|
|
66
|
+
buildSkillExtractionJobOptions(job.orgId),
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function clearRegularChatMemoryDigestDeduplicationKey(orgId: string): Promise<void> {
|
|
71
|
+
await organizationLearningQueue.getQueue().removeDeduplicationKey(buildRegularChatMemoryDigestDeduplicationId(orgId))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const startOrganizationLearningWorker = organizationLearningQueue.startWorker
|
|
75
|
+
|
|
76
|
+
if (import.meta.main) {
|
|
77
|
+
startOrganizationLearningWorker()
|
|
78
|
+
}
|