@lota-sdk/core 0.1.15 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/infrastructure/schema/00_identity.surql +0 -2
- package/infrastructure/schema/01_memory.surql +1 -1
- package/infrastructure/schema/02_execution_plan.surql +62 -1
- package/infrastructure/schema/03_learned_skill.surql +1 -1
- package/infrastructure/schema/06_playbook.surql +25 -0
- package/infrastructure/schema/07_institutional_memory.surql +13 -0
- package/infrastructure/schema/08_quality_metrics.surql +17 -0
- package/package.json +8 -7
- package/src/ai/definitions.ts +80 -2
- package/src/ai/index.ts +0 -2
- package/src/bifrost/bifrost.ts +2 -7
- package/src/config/agent-defaults.ts +31 -21
- package/src/config/agent-types.ts +11 -0
- package/src/config/constants.ts +2 -14
- package/src/config/debug-logger.ts +5 -1
- package/src/config/index.ts +3 -0
- package/src/config/model-constants.ts +16 -34
- package/src/config/search.ts +1 -15
- package/src/create-runtime.ts +244 -178
- package/src/db/cursor-pagination.ts +3 -6
- package/src/db/index.ts +2 -0
- package/src/db/memory-store.rows.ts +7 -7
- package/src/db/memory-store.ts +14 -18
- package/src/db/memory.ts +13 -13
- package/src/db/service.ts +153 -79
- package/src/db/startup.ts +6 -10
- package/src/db/surreal-mutation.ts +43 -0
- package/src/db/tables.ts +7 -0
- package/src/db/workstream-message-row.ts +15 -0
- package/src/embeddings/provider.ts +1 -1
- package/src/queues/context-compaction.queue.ts +15 -46
- package/src/queues/delayed-node-promotion.queue.ts +41 -0
- package/src/queues/index.ts +3 -0
- package/src/queues/memory-consolidation.queue.ts +16 -51
- package/src/queues/plan-scheduler.queue.ts +97 -0
- package/src/queues/post-chat-memory.queue.ts +15 -56
- package/src/queues/queue-factory.ts +100 -0
- package/src/queues/recent-activity-title-refinement.queue.ts +15 -50
- package/src/queues/regular-chat-memory-digest.queue.ts +16 -52
- package/src/queues/skill-extraction.queue.ts +15 -47
- package/src/queues/workstream-title-generation.queue.ts +15 -47
- package/src/redis/connection.ts +6 -0
- package/src/redis/index.ts +1 -1
- package/src/redis/stream-context.ts +11 -0
- package/src/runtime/agent-runtime-policy.ts +106 -21
- package/src/runtime/approval-continuation.ts +12 -6
- package/src/runtime/context-compaction-runtime.ts +1 -1
- package/src/runtime/context-compaction.ts +22 -60
- package/src/runtime/execution-plan.ts +22 -18
- package/src/runtime/graph-designer.ts +15 -0
- package/src/runtime/helper-model.ts +9 -197
- package/src/runtime/index.ts +2 -0
- package/src/runtime/llm-content.ts +1 -1
- package/src/runtime/memory-block.ts +9 -11
- package/src/runtime/memory-pipeline.ts +6 -9
- package/src/runtime/plugin-resolution.ts +35 -0
- package/src/runtime/plugin-types.ts +72 -0
- package/src/runtime/retrieval-adapters.ts +1 -1
- package/src/runtime/runtime-config.ts +25 -12
- package/src/runtime/runtime-extensions.ts +2 -2
- package/src/runtime/runtime-worker-registry.ts +6 -0
- package/src/runtime/team-consultation-orchestrator.ts +45 -28
- package/src/runtime/team-consultation-prompts.ts +11 -2
- package/src/runtime/title-helpers.ts +2 -4
- package/src/runtime/workstream-chat-helpers.ts +1 -1
- package/src/services/adaptive-playbook.service.ts +152 -0
- package/src/services/agent-executor.service.ts +293 -0
- package/src/services/artifact-provenance.service.ts +172 -0
- package/src/services/attachment.service.ts +6 -11
- package/src/services/context-compaction.service.ts +72 -55
- package/src/services/context-enrichment.service.ts +33 -0
- package/src/services/coordination-registry.service.ts +117 -0
- package/src/services/document-chunk.service.ts +1 -1
- package/src/services/domain-agent-executor.service.ts +71 -0
- package/src/services/execution-plan.service.ts +269 -50
- package/src/services/feedback-loop.service.ts +96 -0
- package/src/services/global-orchestrator.service.ts +148 -0
- package/src/services/index.ts +26 -0
- package/src/services/institutional-memory.service.ts +145 -0
- package/src/services/learned-skill.service.ts +24 -5
- package/src/services/memory-assessment.service.ts +3 -2
- package/src/services/memory-utils.ts +3 -8
- package/src/services/memory.service.ts +42 -59
- package/src/services/monitoring-window.service.ts +86 -0
- package/src/services/mutating-approval.service.ts +1 -1
- package/src/services/node-workspace.service.ts +155 -0
- package/src/services/notification.service.ts +39 -0
- package/src/services/organization-member.service.ts +11 -4
- package/src/services/organization.service.ts +5 -5
- package/src/services/ownership-dispatcher.service.ts +403 -0
- package/src/services/plan-approval.service.ts +1 -1
- package/src/services/plan-builder.service.ts +1 -0
- package/src/services/plan-checkpoint.service.ts +30 -2
- package/src/services/plan-compiler.service.ts +5 -0
- package/src/services/plan-coordination.service.ts +152 -0
- package/src/services/plan-cycle.service.ts +284 -0
- package/src/services/plan-deadline.service.ts +287 -0
- package/src/services/plan-executor.service.ts +384 -40
- package/src/services/plan-run.service.ts +41 -7
- package/src/services/plan-scheduler.service.ts +240 -0
- package/src/services/plan-template.service.ts +117 -0
- package/src/services/plan-validator.service.ts +84 -2
- package/src/services/plan-workspace.service.ts +83 -0
- package/src/services/playbook-registry.service.ts +67 -0
- package/src/services/plugin-executor.service.ts +103 -0
- package/src/services/quality-metrics.service.ts +132 -0
- package/src/services/recent-activity.service.ts +27 -31
- package/src/services/skill-resolver.service.ts +19 -0
- package/src/services/system-executor.service.ts +105 -0
- package/src/services/workstream-message.service.ts +12 -34
- package/src/services/workstream-plan-registry.service.ts +22 -0
- package/src/services/workstream-title.service.ts +3 -1
- package/src/services/workstream-turn-preparation.service.ts +34 -66
- package/src/services/workstream.service.ts +33 -55
- package/src/services/workstream.types.ts +9 -9
- package/src/services/write-intent-validator.service.ts +81 -0
- package/src/storage/attachment-parser.ts +1 -1
- package/src/storage/attachment-utils.ts +1 -1
- package/src/storage/generated-document-storage.service.ts +3 -2
- package/src/system-agents/delegated-agent-factory.ts +2 -0
- package/src/tools/execution-plan.tool.ts +17 -23
- package/src/tools/index.ts +0 -1
- package/src/tools/team-think.tool.ts +6 -4
- package/src/utils/async.ts +2 -1
- package/src/utils/date-time.ts +4 -32
- package/src/utils/env.ts +8 -0
- package/src/utils/errors.ts +42 -10
- package/src/utils/index.ts +9 -0
- package/src/utils/string.ts +114 -1
- package/src/workers/index.ts +1 -0
- package/src/workers/regular-chat-memory-digest.runner.ts +2 -2
- package/src/workers/skill-extraction.runner.ts +1 -1
- package/src/workers/utils/file-section-chunker.ts +2 -1
- package/src/workers/utils/repomix-file-sections.ts +2 -2
- package/src/workers/utils/sandbox-error.ts +11 -2
- package/src/workers/utils/workstream-message-query.ts +11 -20
- package/src/workers/worker-utils.ts +2 -2
- package/src/tools/log-hello-world.tool.ts +0 -17
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { ConvergenceState, PlanFailureClass } from '@lota-sdk/shared'
|
|
2
|
+
|
|
3
|
+
import { serverLogger } from '../config/logger'
|
|
4
|
+
import { recordIdToString } from '../db/record-id'
|
|
5
|
+
import { TABLES } from '../db/tables'
|
|
6
|
+
|
|
7
|
+
function classifyDispatchFailure(ownerType: string, error: unknown): PlanFailureClass {
|
|
8
|
+
const errorMessage = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase()
|
|
9
|
+
if (errorMessage.includes('timeout')) return 'timeout_exceeded'
|
|
10
|
+
if (ownerType === 'plugin' || ownerType === 'system') return 'external_system_unavailable'
|
|
11
|
+
return 'non_recoverable_logic_error'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function formatDispatchError(error: unknown): string {
|
|
15
|
+
return error instanceof Error ? error.message : String(error)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const STABLE_RUN_STATUSES = new Set(['awaiting-human', 'blocked', 'failed', 'completed', 'aborted'])
|
|
19
|
+
|
|
20
|
+
class GlobalOrchestratorService {
|
|
21
|
+
detectConvergence(params: {
|
|
22
|
+
totalNodes: number
|
|
23
|
+
completedNodes: number
|
|
24
|
+
failedNodes: number
|
|
25
|
+
previousCompletedNodes?: number
|
|
26
|
+
previousFailedNodes?: number
|
|
27
|
+
}): ConvergenceState {
|
|
28
|
+
const completionRatio = params.totalNodes > 0 ? params.completedNodes / params.totalNodes : 0
|
|
29
|
+
const failureRatio = params.totalNodes > 0 ? params.failedNodes / params.totalNodes : 0
|
|
30
|
+
|
|
31
|
+
if (params.previousCompletedNodes !== undefined) {
|
|
32
|
+
const completionVelocity = params.completedNodes - params.previousCompletedNodes
|
|
33
|
+
const failureVelocity = params.failedNodes - (params.previousFailedNodes ?? 0)
|
|
34
|
+
|
|
35
|
+
if (completionVelocity > 0 && failureVelocity === 0) return 'converging'
|
|
36
|
+
if (completionVelocity === 0 && failureVelocity === 0) return 'stalled'
|
|
37
|
+
if (failureVelocity > 0) return 'diverging'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (completionRatio > 0.5 && failureRatio === 0) return 'converging'
|
|
41
|
+
if (failureRatio > 0.3) return 'diverging'
|
|
42
|
+
return 'progressing'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
decideRerouteAction(params: {
|
|
46
|
+
failedNodeId: string
|
|
47
|
+
retryCount: number
|
|
48
|
+
maxRetries: number
|
|
49
|
+
convergenceState: ConvergenceState
|
|
50
|
+
}): 'retry' | 'skip' | 'abort' {
|
|
51
|
+
if (params.retryCount < params.maxRetries) return 'retry'
|
|
52
|
+
if (params.convergenceState === 'converging') return 'skip'
|
|
53
|
+
if (params.convergenceState === 'diverging') return 'abort'
|
|
54
|
+
return 'skip'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async routeGraphFull(params: { workstreamId: string; runId: string }): Promise<void> {
|
|
58
|
+
const MAX_ROUNDS = 32
|
|
59
|
+
const STRUCTURAL_TYPES = new Set(['switch', 'join', 'deliberation-fork'])
|
|
60
|
+
|
|
61
|
+
// Dynamic imports to avoid circular dependencies
|
|
62
|
+
const { planRunService } = await import('./plan-run.service')
|
|
63
|
+
const { ownershipDispatcherService } = await import('./ownership-dispatcher.service')
|
|
64
|
+
const { planExecutorService } = await import('./plan-executor.service')
|
|
65
|
+
|
|
66
|
+
let round = 0
|
|
67
|
+
for (; round < MAX_ROUNDS; round++) {
|
|
68
|
+
const run = await planRunService.getRunById(params.runId)
|
|
69
|
+
if (STABLE_RUN_STATUSES.has(run.status)) break
|
|
70
|
+
|
|
71
|
+
const spec = await planRunService.getPlanSpecById(run.planSpecId)
|
|
72
|
+
const nodeSpecs = await planRunService.listNodeSpecs(spec.id)
|
|
73
|
+
const nodeRuns = await planRunService.listNodeRuns(run.id)
|
|
74
|
+
|
|
75
|
+
// Find ready action nodes (not structural, not human — those are handled by syncRunGraph)
|
|
76
|
+
const readyNodes = nodeRuns.filter((nr) => {
|
|
77
|
+
if (nr.status !== 'ready') return false
|
|
78
|
+
const ns = nodeSpecs.find((s) => s.nodeId === nr.nodeId)
|
|
79
|
+
return ns && ns.owner.executorType !== 'user' && !STRUCTURAL_TYPES.has(ns.type)
|
|
80
|
+
})
|
|
81
|
+
if (readyNodes.length === 0) break
|
|
82
|
+
|
|
83
|
+
// Transition all ready nodes to 'running' BEFORE dispatching
|
|
84
|
+
for (const nodeRun of readyNodes) {
|
|
85
|
+
await planExecutorService.transitionNodeToRunning({ runId: params.runId, nodeId: nodeRun.nodeId })
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Re-fetch run after transitions for accurate state in dispatch context
|
|
89
|
+
const updatedRun = await planRunService.getRunById(params.runId)
|
|
90
|
+
|
|
91
|
+
// Dispatch all in parallel with LINEAR mode override (prevents recursion)
|
|
92
|
+
const results = await Promise.allSettled(
|
|
93
|
+
readyNodes.map(async (nodeRun) => {
|
|
94
|
+
const nodeSpecRecord = nodeSpecs.find((ns) => ns.nodeId === nodeRun.nodeId)
|
|
95
|
+
if (!nodeSpecRecord) {
|
|
96
|
+
throw new Error(`Node spec not found for node "${nodeRun.nodeId}".`)
|
|
97
|
+
}
|
|
98
|
+
// Re-fetch the node run to get the updated 'running' state with resolvedInput
|
|
99
|
+
const updatedNodeRun = await planRunService.getNodeRunByNodeId(updatedRun.id, nodeRun.nodeId)
|
|
100
|
+
const result = await ownershipDispatcherService.dispatchReadyNode({
|
|
101
|
+
run: updatedRun,
|
|
102
|
+
nodeSpecRecord,
|
|
103
|
+
nodeRun: updatedNodeRun,
|
|
104
|
+
spec,
|
|
105
|
+
executionModeOverride: 'linear',
|
|
106
|
+
})
|
|
107
|
+
return { nodeId: nodeRun.nodeId, ownerRef: nodeSpecRecord.owner.ref, result }
|
|
108
|
+
}),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
const workstreamId = recordIdToString(updatedRun.workstreamId, TABLES.WORKSTREAM)
|
|
112
|
+
const runId = recordIdToString(updatedRun.id, TABLES.PLAN_RUN)
|
|
113
|
+
|
|
114
|
+
// Submit results sequentially (each triggers syncRunGraph internally)
|
|
115
|
+
for (let i = 0; i < results.length; i++) {
|
|
116
|
+
const settled = results[i]
|
|
117
|
+
const nodeRun = readyNodes[i]
|
|
118
|
+
const nodeSpecRecord = nodeSpecs.find((ns) => ns.nodeId === nodeRun.nodeId)
|
|
119
|
+
|
|
120
|
+
if (settled.status === 'fulfilled') {
|
|
121
|
+
await planExecutorService.submitNodeResult({
|
|
122
|
+
workstreamId,
|
|
123
|
+
runId,
|
|
124
|
+
nodeId: settled.value.nodeId,
|
|
125
|
+
emittedBy: settled.value.ownerRef,
|
|
126
|
+
result: settled.value.result,
|
|
127
|
+
})
|
|
128
|
+
} else {
|
|
129
|
+
serverLogger.warn`routeGraphFull: dispatch failed for node "${nodeRun.nodeId}": ${settled.reason}`
|
|
130
|
+
await planExecutorService.blockNodeOnDispatchFailure({
|
|
131
|
+
workstreamId,
|
|
132
|
+
runId,
|
|
133
|
+
nodeId: nodeRun.nodeId,
|
|
134
|
+
emittedBy: nodeSpecRecord?.owner.ref ?? 'unknown',
|
|
135
|
+
message: formatDispatchError(settled.reason),
|
|
136
|
+
failureClass: classifyDispatchFailure(nodeSpecRecord?.owner.executorType ?? 'agent', settled.reason),
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (round === MAX_ROUNDS - 1) {
|
|
143
|
+
serverLogger.warn`graph-full execution reached max rounds (${MAX_ROUNDS}) for run ${params.runId} — possible non-converging graph`
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export const globalOrchestratorService = new GlobalOrchestratorService()
|
package/src/services/index.ts
CHANGED
|
@@ -1,14 +1,40 @@
|
|
|
1
|
+
export * from './adaptive-playbook.service'
|
|
2
|
+
export * from './agent-executor.service'
|
|
3
|
+
export * from './artifact-provenance.service'
|
|
1
4
|
export * from './attachment.service'
|
|
5
|
+
export * from './context-enrichment.service'
|
|
6
|
+
export * from './coordination-registry.service'
|
|
2
7
|
export * from './document-chunk.service'
|
|
8
|
+
export * from './domain-agent-executor.service'
|
|
3
9
|
export * from './execution-plan.service'
|
|
10
|
+
export * from './institutional-memory.service'
|
|
11
|
+
export * from './feedback-loop.service'
|
|
12
|
+
export * from './global-orchestrator.service'
|
|
4
13
|
export * from './memory.service'
|
|
14
|
+
export * from './node-workspace.service'
|
|
15
|
+
export * from './notification.service'
|
|
16
|
+
export * from './ownership-dispatcher.service'
|
|
5
17
|
export * from './organization-member.service'
|
|
6
18
|
export * from './organization.service'
|
|
19
|
+
export * from './plan-coordination.service'
|
|
20
|
+
export * from './plan-cycle.service'
|
|
21
|
+
export * from './plan-deadline.service'
|
|
22
|
+
export * from './plan-workspace.service'
|
|
7
23
|
export * from './plan-run.service'
|
|
24
|
+
export * from './plan-scheduler.service'
|
|
25
|
+
export * from './plan-template.service'
|
|
26
|
+
export * from './playbook-registry.service'
|
|
27
|
+
export * from './quality-metrics.service'
|
|
28
|
+
export * from './monitoring-window.service'
|
|
29
|
+
export * from './plugin-executor.service'
|
|
8
30
|
export * from './recent-activity-title.service'
|
|
9
31
|
export * from './recent-activity.service'
|
|
32
|
+
export * from './skill-resolver.service'
|
|
33
|
+
export * from './system-executor.service'
|
|
10
34
|
export * from './user.service'
|
|
11
35
|
export * from './workstream-message.service'
|
|
12
36
|
export * from './workstream-title.service'
|
|
13
37
|
export * from './workstream-turn'
|
|
38
|
+
export * from './workstream-plan-registry.service'
|
|
14
39
|
export * from './workstream.service'
|
|
40
|
+
export * from './write-intent-validator.service'
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { InstitutionalMemory, InstitutionalMemoryType } from '@lota-sdk/shared'
|
|
2
|
+
import { InstitutionalMemorySchema } from '@lota-sdk/shared'
|
|
3
|
+
import { BoundQuery } from 'surrealdb'
|
|
4
|
+
|
|
5
|
+
import { ensureRecordId } from '../db/record-id'
|
|
6
|
+
import { databaseService } from '../db/service'
|
|
7
|
+
import { TABLES } from '../db/tables'
|
|
8
|
+
|
|
9
|
+
class InstitutionalMemoryService {
|
|
10
|
+
async extractPatterns(params: { organizationId: string; runId: string }): Promise<InstitutionalMemory[]> {
|
|
11
|
+
const { planRunService } = await import('./plan-run.service')
|
|
12
|
+
|
|
13
|
+
const run = await planRunService.getRunById(params.runId)
|
|
14
|
+
const nodeRuns = await planRunService.listNodeRuns(run.id)
|
|
15
|
+
const nodeSpecs = await planRunService.listNodeSpecs(run.planSpecId)
|
|
16
|
+
const nodeSpecsByNodeId = new Map(nodeSpecs.map((n) => [n.nodeId, n]))
|
|
17
|
+
|
|
18
|
+
const patterns: InstitutionalMemory[] = []
|
|
19
|
+
const orgRef = ensureRecordId(params.organizationId, TABLES.ORGANIZATION)
|
|
20
|
+
const ownerRef = (nodeId: string): string => nodeSpecsByNodeId.get(nodeId)?.owner.ref ?? 'unknown'
|
|
21
|
+
|
|
22
|
+
const completedNodes = nodeRuns.filter((nr) => nr.status === 'completed' && nr.startedAt && nr.completedAt)
|
|
23
|
+
if (completedNodes.length > 0) {
|
|
24
|
+
const timings = completedNodes.map((nr) => ({
|
|
25
|
+
nodeId: nr.nodeId,
|
|
26
|
+
owner: ownerRef(nr.nodeId),
|
|
27
|
+
durationMs: new Date(nr.completedAt ?? 0).getTime() - new Date(nr.startedAt ?? 0).getTime(),
|
|
28
|
+
}))
|
|
29
|
+
|
|
30
|
+
const record = await this.persistPattern({
|
|
31
|
+
organizationId: orgRef,
|
|
32
|
+
type: 'timing-pattern',
|
|
33
|
+
pattern: { runId: params.runId, timings },
|
|
34
|
+
confidence: 0.7,
|
|
35
|
+
sampleCount: completedNodes.length,
|
|
36
|
+
})
|
|
37
|
+
patterns.push(record)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const failedNodes = nodeRuns.filter((nr) => nr.status === 'failed')
|
|
41
|
+
if (failedNodes.length > 0) {
|
|
42
|
+
const failures = failedNodes.map((nr) => ({
|
|
43
|
+
nodeId: nr.nodeId,
|
|
44
|
+
owner: ownerRef(nr.nodeId),
|
|
45
|
+
attemptCount: nr.attemptCount,
|
|
46
|
+
}))
|
|
47
|
+
|
|
48
|
+
const record = await this.persistPattern({
|
|
49
|
+
organizationId: orgRef,
|
|
50
|
+
type: 'failure-pattern',
|
|
51
|
+
pattern: { runId: params.runId, failures },
|
|
52
|
+
confidence: 0.6,
|
|
53
|
+
sampleCount: failedNodes.length,
|
|
54
|
+
})
|
|
55
|
+
patterns.push(record)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const agentCompletions = new Map<string, number>()
|
|
59
|
+
for (const nr of completedNodes) {
|
|
60
|
+
const owner = ownerRef(nr.nodeId)
|
|
61
|
+
agentCompletions.set(owner, (agentCompletions.get(owner) ?? 0) + 1)
|
|
62
|
+
}
|
|
63
|
+
if (agentCompletions.size > 0) {
|
|
64
|
+
const record = await this.persistPattern({
|
|
65
|
+
organizationId: orgRef,
|
|
66
|
+
type: 'agent-affinity',
|
|
67
|
+
pattern: { runId: params.runId, completions: Object.fromEntries(agentCompletions) },
|
|
68
|
+
confidence: 0.5,
|
|
69
|
+
sampleCount: completedNodes.length,
|
|
70
|
+
})
|
|
71
|
+
patterns.push(record)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return patterns
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async queryRelevant(params: {
|
|
78
|
+
organizationId: string
|
|
79
|
+
objective: string
|
|
80
|
+
limit?: number
|
|
81
|
+
}): Promise<InstitutionalMemory[]> {
|
|
82
|
+
const orgRef = ensureRecordId(params.organizationId, TABLES.ORGANIZATION)
|
|
83
|
+
const limit = params.limit ?? 10
|
|
84
|
+
|
|
85
|
+
// Fetch a broader set, then rank by relevance to the objective.
|
|
86
|
+
// SurrealDB does not support full-text scoring on FLEXIBLE object fields,
|
|
87
|
+
// so we over-fetch and rank in application code.
|
|
88
|
+
const fetchLimit = Math.max(limit * 3, 30)
|
|
89
|
+
const records = await databaseService.queryMany(
|
|
90
|
+
new BoundQuery(
|
|
91
|
+
`SELECT * FROM ${TABLES.INSTITUTIONAL_MEMORY}
|
|
92
|
+
WHERE organizationId = $orgId
|
|
93
|
+
ORDER BY confidence DESC, createdAt DESC
|
|
94
|
+
LIMIT $fetchLimit`,
|
|
95
|
+
{ orgId: orgRef, fetchLimit },
|
|
96
|
+
),
|
|
97
|
+
InstitutionalMemorySchema,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
// Score each record by how many objective keywords appear in its pattern JSON
|
|
101
|
+
const objectiveTerms = params.objective
|
|
102
|
+
.toLowerCase()
|
|
103
|
+
.split(/\s+/)
|
|
104
|
+
.filter((t) => t.length > 2)
|
|
105
|
+
|
|
106
|
+
if (objectiveTerms.length === 0) return records.slice(0, limit)
|
|
107
|
+
|
|
108
|
+
const scored = records.map((record) => {
|
|
109
|
+
const patternStr = JSON.stringify(record.pattern).toLowerCase()
|
|
110
|
+
const matchCount = objectiveTerms.filter((term) => patternStr.includes(term)).length
|
|
111
|
+
return { record, relevance: matchCount / objectiveTerms.length }
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
scored.sort((a, b) => {
|
|
115
|
+
// Primary: relevance score, secondary: confidence, tertiary: recency
|
|
116
|
+
if (b.relevance !== a.relevance) return b.relevance - a.relevance
|
|
117
|
+
if (b.record.confidence !== a.record.confidence) return b.record.confidence - a.record.confidence
|
|
118
|
+
return 0
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
return scored.slice(0, limit).map((s) => s.record)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private async persistPattern(params: {
|
|
125
|
+
organizationId: ReturnType<typeof ensureRecordId>
|
|
126
|
+
type: InstitutionalMemoryType
|
|
127
|
+
pattern: Record<string, unknown>
|
|
128
|
+
confidence: number
|
|
129
|
+
sampleCount: number
|
|
130
|
+
}): Promise<InstitutionalMemory> {
|
|
131
|
+
return databaseService.create(
|
|
132
|
+
TABLES.INSTITUTIONAL_MEMORY,
|
|
133
|
+
{
|
|
134
|
+
organizationId: params.organizationId,
|
|
135
|
+
type: params.type,
|
|
136
|
+
pattern: params.pattern,
|
|
137
|
+
confidence: params.confidence,
|
|
138
|
+
sampleCount: params.sampleCount,
|
|
139
|
+
},
|
|
140
|
+
InstitutionalMemorySchema,
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const institutionalMemoryService = new InstitutionalMemoryService()
|
|
@@ -5,7 +5,7 @@ import { z } from 'zod'
|
|
|
5
5
|
import { renderLearnedSkillInstructions } from '../ai/definitions'
|
|
6
6
|
import { lotaDebugLogger } from '../config/debug-logger'
|
|
7
7
|
import { serverLogger } from '../config/logger'
|
|
8
|
-
import { ensureRecordId } from '../db/record-id'
|
|
8
|
+
import { ensureRecordId, recordIdToString } from '../db/record-id'
|
|
9
9
|
import { databaseService } from '../db/service'
|
|
10
10
|
import { TABLES } from '../db/tables'
|
|
11
11
|
import { getDefaultEmbeddings } from '../embeddings/provider'
|
|
@@ -124,7 +124,7 @@ class LearnedSkillService {
|
|
|
124
124
|
|
|
125
125
|
async update(skillId: string, input: UpdateLearnedSkillInput): Promise<LearnedSkillRow> {
|
|
126
126
|
const ref = ensureRecordId(skillId, TABLES.LEARNED_SKILL)
|
|
127
|
-
const data: Record<string, unknown> = {
|
|
127
|
+
const data: Record<string, unknown> = {}
|
|
128
128
|
|
|
129
129
|
if (input.name !== undefined) data.name = input.name
|
|
130
130
|
if (input.description !== undefined) data.description = input.description
|
|
@@ -149,11 +149,11 @@ class LearnedSkillService {
|
|
|
149
149
|
await databaseService.update(
|
|
150
150
|
TABLES.LEARNED_SKILL,
|
|
151
151
|
ref,
|
|
152
|
-
{ status: 'archived', archivedAt: new Date()
|
|
152
|
+
{ status: 'archived', archivedAt: new Date() },
|
|
153
153
|
LearnedSkillRowSchema,
|
|
154
154
|
)
|
|
155
155
|
if (skill) {
|
|
156
|
-
await this.invalidateSkillExistsCache(
|
|
156
|
+
await this.invalidateSkillExistsCache(recordIdToString(skill.organizationId), skill.agentId ?? null)
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
|
|
@@ -292,7 +292,7 @@ class LearnedSkillService {
|
|
|
292
292
|
await databaseService.update(
|
|
293
293
|
TABLES.LEARNED_SKILL,
|
|
294
294
|
ref,
|
|
295
|
-
{ status: 'verified', confidence: Math.min(skill.confidence + 0.1, 1.0)
|
|
295
|
+
{ status: 'verified', confidence: Math.min(skill.confidence + 0.1, 1.0) },
|
|
296
296
|
LearnedSkillRowSchema,
|
|
297
297
|
)
|
|
298
298
|
return true
|
|
@@ -346,6 +346,25 @@ class LearnedSkillService {
|
|
|
346
346
|
return hasher.digest('hex')
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
+
async findByNameOrTag(orgId: string, nameOrTag: string): Promise<LearnedSkillRow | null> {
|
|
350
|
+
const orgRef = ensureRecordId(orgId, TABLES.ORGANIZATION)
|
|
351
|
+
const normalizedRef = nameOrTag.trim().toLowerCase()
|
|
352
|
+
const rows = await databaseService.queryMany(
|
|
353
|
+
new BoundQuery(
|
|
354
|
+
`SELECT *, type::string(id) AS id, type::string(organizationId) AS organizationId
|
|
355
|
+
FROM ${TABLES.LEARNED_SKILL}
|
|
356
|
+
WHERE organizationId = $organizationId
|
|
357
|
+
${ACTIVE_SKILL_FILTER}
|
|
358
|
+
AND (string::lowercase(name) = $nameRef OR $nameRef IN tags)
|
|
359
|
+
ORDER BY confidence DESC
|
|
360
|
+
LIMIT 1`,
|
|
361
|
+
{ organizationId: orgRef, nameRef: normalizedRef },
|
|
362
|
+
),
|
|
363
|
+
LearnedSkillRowSchema,
|
|
364
|
+
)
|
|
365
|
+
return rows[0] ?? null
|
|
366
|
+
}
|
|
367
|
+
|
|
349
368
|
async findByHash(orgId: string, hash: string): Promise<LearnedSkillRow | null> {
|
|
350
369
|
const orgRef = ensureRecordId(orgId, TABLES.ORGANIZATION)
|
|
351
370
|
const rows = await databaseService.queryMany(
|
|
@@ -3,13 +3,14 @@ import type { z } from 'zod'
|
|
|
3
3
|
import { MemoryImportanceAssessmentSchema } from '../db/memory-types'
|
|
4
4
|
import { createHelperModelRuntime } from '../runtime/helper-model'
|
|
5
5
|
import { createOrgMemoryAgent } from '../system-agents/memory.agent'
|
|
6
|
+
import { clampImportance } from '../utils/string'
|
|
6
7
|
|
|
7
8
|
type MemoryImportanceAssessment = z.infer<typeof MemoryImportanceAssessmentSchema>
|
|
8
9
|
const MEMORY_IMPORTANCE_ASSESSMENT_TIMEOUT_MS = 10 * 60 * 1000
|
|
9
10
|
const helperModelRuntime = createHelperModelRuntime()
|
|
10
11
|
|
|
11
12
|
export function clampMemoryImportance(value: number): number {
|
|
12
|
-
return Math.max(0.2, Math.min(0.95, value))
|
|
13
|
+
return clampImportance(Math.max(0.2, Math.min(0.95, value)))
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export async function assessMemoryImportance(params: {
|
|
@@ -32,7 +33,7 @@ export async function assessMemoryImportance(params: {
|
|
|
32
33
|
'Return only schema fields.',
|
|
33
34
|
].join('\n')
|
|
34
35
|
|
|
35
|
-
return
|
|
36
|
+
return helperModelRuntime.generateHelperStructured({
|
|
36
37
|
tag: params.tag ?? 'memory-importance-assessment',
|
|
37
38
|
createAgent: createOrgMemoryAgent,
|
|
38
39
|
systemPrompt: 'You are a strict long-term memory quality assessor for an AI agent.',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { MEMORY } from '../config/constants'
|
|
2
2
|
import { VECTOR_SEARCH_OVERFETCH_MULTIPLIER } from '../config/search'
|
|
3
3
|
import type { MemorySearchResult } from '../db/memory-types'
|
|
4
|
-
import { compactWhitespace } from '../utils/string'
|
|
4
|
+
import { compactWhitespace, truncateText } from '../utils/string'
|
|
5
5
|
import type { MemoryRerankOutput } from './memory.service'
|
|
6
6
|
|
|
7
7
|
export function getCandidateLimit(limit: number): number {
|
|
@@ -11,11 +11,7 @@ export function getCandidateLimit(limit: number): number {
|
|
|
11
11
|
export function formatMemoryResults(results: MemorySearchResult[]): string {
|
|
12
12
|
if (results.length === 0) return 'No stored memories.'
|
|
13
13
|
|
|
14
|
-
const normalize = (value: string) =>
|
|
15
|
-
const trimmed = compactWhitespace(value)
|
|
16
|
-
if (trimmed.length <= 400) return trimmed
|
|
17
|
-
return `${trimmed.slice(0, 400)}...`
|
|
18
|
-
}
|
|
14
|
+
const normalize = (value: string) => truncateText(compactWhitespace(value), 400)
|
|
19
15
|
|
|
20
16
|
return results
|
|
21
17
|
.map((item) => {
|
|
@@ -61,8 +57,7 @@ export function formatRerankedResults(
|
|
|
61
57
|
used.add(candidate.id)
|
|
62
58
|
total += 1
|
|
63
59
|
const reason = item.relevance ? ` — ${item.relevance}` : ''
|
|
64
|
-
const
|
|
65
|
-
const content = trimmed.length <= 400 ? trimmed : `${trimmed.slice(0, 400)}...`
|
|
60
|
+
const content = truncateText(compactWhitespace(candidate.content), 400)
|
|
66
61
|
lines.push(`- ${content}${reason}`)
|
|
67
62
|
if (total >= limit) break
|
|
68
63
|
}
|
|
@@ -26,8 +26,7 @@ import {
|
|
|
26
26
|
import { getRuntimeConfig } from '../runtime/runtime-config'
|
|
27
27
|
import { createMemoryRerankerAgent, MEMORY_RERANKER_PROMPT } from '../system-agents/memory-reranker.agent'
|
|
28
28
|
import { createOrgMemoryAgent, ORG_MEMORY_PROMPT } from '../system-agents/memory.agent'
|
|
29
|
-
import {
|
|
30
|
-
import { compactWhitespace } from '../utils/string'
|
|
29
|
+
import { compactWhitespace, truncateText } from '../utils/string'
|
|
31
30
|
import { assessMemoryImportance, clampMemoryImportance } from './memory-assessment.service'
|
|
32
31
|
import { formatMemoryResults, formatRerankedResults, getCandidateLimit } from './memory-utils'
|
|
33
32
|
|
|
@@ -44,6 +43,7 @@ const MAX_CONVERSATION_MEMORY_BLOCK_CHARS = 2_000
|
|
|
44
43
|
const MAX_CONVERSATION_ATTACHMENT_CONTEXT_CHARS = 6_000
|
|
45
44
|
const MAX_CONVERSATION_ASSESSMENT_CHARS = 7_000
|
|
46
45
|
const ONBOARDING_MEMORY_MAX_FACTS = 16
|
|
46
|
+
const MAX_ORG_MEMORY_CLIENTS = 128
|
|
47
47
|
const ONBOARDING_MEMORY_EXTRACTION_PROMPT =
|
|
48
48
|
'Onboarding mode is active. Extract multiple concrete startup facts from user-provided context: company mission, product capabilities, customer segments, pricing, traction, go-to-market plans, roadmap, team composition, technical stack, risks, and referenced URLs. Prefer one fact per concrete claim.'
|
|
49
49
|
|
|
@@ -65,17 +65,29 @@ const MemoryRerankOutputSchema = z.object({
|
|
|
65
65
|
|
|
66
66
|
export type MemoryRerankOutput = z.infer<typeof MemoryRerankOutputSchema>
|
|
67
67
|
|
|
68
|
-
const isRoutableAgentName = (value?: string):
|
|
68
|
+
const isRoutableAgentName = (value?: string): value is string => Boolean(value && agentRoster.includes(value))
|
|
69
69
|
|
|
70
70
|
class MemoryService {
|
|
71
71
|
private orgMemoryCache = new Map<string, Memory>()
|
|
72
72
|
|
|
73
73
|
private getOrCreateMemory(cacheKey: string, cache: Map<string, Memory>): Memory {
|
|
74
74
|
const cached = cache.get(cacheKey)
|
|
75
|
-
if (cached)
|
|
75
|
+
if (cached) {
|
|
76
|
+
cache.delete(cacheKey)
|
|
77
|
+
cache.set(cacheKey, cached)
|
|
78
|
+
return cached
|
|
79
|
+
}
|
|
76
80
|
|
|
77
81
|
const memory = new Memory({ createAgent: createOrgMemoryAgent }, { customPrompt: ORG_MEMORY_PROMPT })
|
|
78
82
|
|
|
83
|
+
if (cache.size >= MAX_ORG_MEMORY_CLIENTS) {
|
|
84
|
+
const oldestKey = cache.keys().next().value
|
|
85
|
+
if (typeof oldestKey === 'string') {
|
|
86
|
+
cache.delete(oldestKey)
|
|
87
|
+
aiLogger.debug`Evicted cached org memory client for ${oldestKey}`
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
79
91
|
cache.set(cacheKey, memory)
|
|
80
92
|
aiLogger.debug`Memory client created and cached for ${cacheKey}`
|
|
81
93
|
return memory
|
|
@@ -87,9 +99,7 @@ class MemoryService {
|
|
|
87
99
|
}
|
|
88
100
|
|
|
89
101
|
private truncateCandidateText(value: string): string {
|
|
90
|
-
|
|
91
|
-
if (normalized.length <= RERANK_CANDIDATE_MAX_CHARS) return normalized
|
|
92
|
-
return `${normalized.slice(0, RERANK_CANDIDATE_MAX_CHARS)}...`
|
|
102
|
+
return truncateText(compactWhitespace(value), RERANK_CANDIDATE_MAX_CHARS)
|
|
93
103
|
}
|
|
94
104
|
|
|
95
105
|
private readNumericMetadata(metadata: Record<string, unknown>, key: string): number {
|
|
@@ -129,8 +139,7 @@ class MemoryService {
|
|
|
129
139
|
private normalizeConversationText(value: string, maxChars: number): string {
|
|
130
140
|
const normalized = compactWhitespace(value)
|
|
131
141
|
if (!normalized) return ''
|
|
132
|
-
|
|
133
|
-
return `${normalized.slice(0, maxChars - 3)}...`
|
|
142
|
+
return truncateText(normalized, maxChars)
|
|
134
143
|
}
|
|
135
144
|
|
|
136
145
|
private resolveAgentScopeNames(agentName?: string, agentNames: string[] = []): string[] {
|
|
@@ -148,9 +157,7 @@ class MemoryService {
|
|
|
148
157
|
}
|
|
149
158
|
|
|
150
159
|
private normalizePreSeededMemoryText(value: string): string {
|
|
151
|
-
|
|
152
|
-
if (normalized.length <= PRESEEDED_MEMORY_MAX_CHARS) return normalized
|
|
153
|
-
return `${normalized.slice(0, PRESEEDED_MEMORY_MAX_CHARS - 3)}...`
|
|
160
|
+
return truncateText(compactWhitespace(value), PRESEEDED_MEMORY_MAX_CHARS)
|
|
154
161
|
}
|
|
155
162
|
|
|
156
163
|
private formatPreSeededMemoriesSection(memories: MemoryRecord[]): string | undefined {
|
|
@@ -331,15 +338,9 @@ class MemoryService {
|
|
|
331
338
|
aiLogger.debug`Organization memory search requested (orgId: ${orgId}, scopeId: ${orgScopeId}, queryLength: ${query.length})`
|
|
332
339
|
const memory = this.getOrgMemory(orgId)
|
|
333
340
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
return results
|
|
338
|
-
} catch (error: unknown) {
|
|
339
|
-
const normalizedError = toError(error)
|
|
340
|
-
aiLogger.error`Organization memory search failed: ${normalizedError}`
|
|
341
|
-
throw normalizedError
|
|
342
|
-
}
|
|
341
|
+
const results = await this.searchMemories({ query, memory, scopeId: orgScopeId, memoryType: ORG_MEMORY_TYPE })
|
|
342
|
+
aiLogger.debug`Organization memory search completed (resultLength: ${results.length}, preview: ${results.slice(0, 100)})`
|
|
343
|
+
return results
|
|
343
344
|
}
|
|
344
345
|
|
|
345
346
|
async getStaleMemories(orgId: string): Promise<string> {
|
|
@@ -368,21 +369,15 @@ class MemoryService {
|
|
|
368
369
|
const searchK = getRuntimeConfig().memory.searchK
|
|
369
370
|
const limit = options?.limit ?? (fastMode ? Math.min(searchK, 4) : searchK)
|
|
370
371
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
return formatMemoryResults(candidates)
|
|
381
|
-
} catch (error: unknown) {
|
|
382
|
-
const normalizedError = toError(error)
|
|
383
|
-
aiLogger.error`Organization memory search (raw) failed: ${normalizedError}`
|
|
384
|
-
throw normalizedError
|
|
385
|
-
}
|
|
372
|
+
const candidates = await memory.searchCandidates(query, {
|
|
373
|
+
scopeId: orgScopeId,
|
|
374
|
+
limit,
|
|
375
|
+
memoryType: ORG_MEMORY_TYPE,
|
|
376
|
+
fastMode,
|
|
377
|
+
includeNeighborContext: !fastMode,
|
|
378
|
+
})
|
|
379
|
+
aiLogger.debug`Organization memory search (raw) completed (candidates: ${candidates.length})`
|
|
380
|
+
return formatMemoryResults(candidates)
|
|
386
381
|
}
|
|
387
382
|
|
|
388
383
|
async searchAgentMemories(orgId: string, agentName: string, query: string): Promise<string> {
|
|
@@ -395,20 +390,14 @@ class MemoryService {
|
|
|
395
390
|
aiLogger.debug`Agent memory search requested (orgId: ${orgId}, agentName: ${agentName}, scopeId: ${scoped}, queryLength: ${query.length})`
|
|
396
391
|
const memory = this.getOrgMemory(orgId)
|
|
397
392
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
return results
|
|
402
|
-
} catch (error: unknown) {
|
|
403
|
-
const normalizedError = toError(error)
|
|
404
|
-
aiLogger.error`Agent memory search failed: ${normalizedError}`
|
|
405
|
-
throw normalizedError
|
|
406
|
-
}
|
|
393
|
+
const results = await this.searchMemories({ query, memory, scopeId: scoped, memoryType: ORG_MEMORY_TYPE })
|
|
394
|
+
aiLogger.debug`Agent memory search completed (agentName: ${agentName}, resultLength: ${results.length}, preview: ${results.slice(0, 100)})`
|
|
395
|
+
return results
|
|
407
396
|
}
|
|
408
397
|
|
|
409
398
|
async searchOrgMemoriesForAgent(orgId: string, agentName: string, query: string): Promise<string> {
|
|
410
399
|
if (!isRoutableAgentName(agentName)) {
|
|
411
|
-
return
|
|
400
|
+
return this.searchOrganizationMemories(orgId, query)
|
|
412
401
|
}
|
|
413
402
|
|
|
414
403
|
const [agentResult, orgResult] = await Promise.all([
|
|
@@ -429,7 +418,7 @@ class MemoryService {
|
|
|
429
418
|
}): Promise<MemoryRecord[]> {
|
|
430
419
|
const { orgId, ...listOptions } = params
|
|
431
420
|
const orgMemory = this.getOrgMemory(orgId)
|
|
432
|
-
return
|
|
421
|
+
return orgMemory.list({ scopeId: scopeId(ORG_SCOPE_PREFIX, orgId), ...listOptions })
|
|
433
422
|
}
|
|
434
423
|
|
|
435
424
|
async getTopMemories(params: { orgId: string; agentName?: string; limit?: number }): Promise<string | undefined> {
|
|
@@ -513,7 +502,7 @@ class MemoryService {
|
|
|
513
502
|
]
|
|
514
503
|
|
|
515
504
|
if (isRoutableAgentName(agentName)) {
|
|
516
|
-
const agentScoped = agentScopeId(orgId, agentName
|
|
505
|
+
const agentScoped = agentScopeId(orgId, agentName)
|
|
517
506
|
retrievalTasks.push({
|
|
518
507
|
scopeTag: `agent:${agentName}`,
|
|
519
508
|
retrieve: async () =>
|
|
@@ -649,7 +638,7 @@ class MemoryService {
|
|
|
649
638
|
importance?: number
|
|
650
639
|
}): Promise<string> {
|
|
651
640
|
if (!isRoutableAgentName(agentName)) {
|
|
652
|
-
throw new Error(`Invalid agentName for agent memory: ${agentName}`)
|
|
641
|
+
throw new Error(`Invalid agentName for agent memory: ${agentName as string}`)
|
|
653
642
|
}
|
|
654
643
|
|
|
655
644
|
const memory = this.getOrgMemory(orgId)
|
|
@@ -830,16 +819,10 @@ class MemoryService {
|
|
|
830
819
|
const preparedUpdates = await orgMemory.prepareFactsToScopes(extractedFacts, scopes)
|
|
831
820
|
if (preparedUpdates.length === 0) return
|
|
832
821
|
|
|
833
|
-
|
|
834
|
-
await
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
aiLogger.debug`Conversation memories added to ${scopes.length} scope(s) from ${messages.length} message(s)`
|
|
838
|
-
} catch (error: unknown) {
|
|
839
|
-
const normalizedError = toError(error)
|
|
840
|
-
aiLogger.error`Memory write failed: ${normalizedError}`
|
|
841
|
-
throw normalizedError
|
|
842
|
-
}
|
|
822
|
+
await withOrgMemoryLock(orgId, async () => {
|
|
823
|
+
await orgMemory.applyPreparedScopeUpdates(preparedUpdates)
|
|
824
|
+
})
|
|
825
|
+
aiLogger.debug`Conversation memories added to ${scopes.length} scope(s) from ${messages.length} message(s)`
|
|
843
826
|
}
|
|
844
827
|
}
|
|
845
828
|
|