@lota-sdk/core 0.1.14 → 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 +9 -8
- package/src/ai/definitions.ts +80 -2
- package/src/ai/embedding-cache.ts +7 -6
- package/src/ai/index.ts +0 -1
- package/src/bifrost/bifrost.ts +14 -14
- package/src/config/agent-defaults.ts +32 -22
- 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/logger.ts +7 -9
- package/src/config/model-constants.ts +16 -34
- package/src/config/search.ts +1 -15
- package/src/create-runtime.ts +453 -0
- 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 +24 -24
- package/src/db/memory.ts +18 -16
- package/src/db/schema-fingerprint.ts +1 -0
- package/src/db/service.ts +193 -122
- package/src/db/startup.ts +9 -13
- 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/index.ts +1 -1
- package/src/queues/context-compaction.queue.ts +17 -52
- package/src/queues/delayed-node-promotion.queue.ts +41 -0
- package/src/queues/document-processor.queue.ts +7 -7
- package/src/queues/index.ts +3 -0
- package/src/queues/memory-consolidation.queue.ts +18 -54
- package/src/queues/plan-scheduler.queue.ts +97 -0
- package/src/queues/post-chat-memory.queue.ts +15 -60
- package/src/queues/queue-factory.ts +100 -0
- package/src/queues/recent-activity-title-refinement.queue.ts +15 -54
- package/src/queues/regular-chat-memory-digest.queue.ts +16 -55
- package/src/queues/skill-extraction.queue.ts +15 -50
- package/src/queues/workstream-title-generation.queue.ts +15 -51
- package/src/redis/connection.ts +12 -3
- package/src/redis/index.ts +2 -1
- package/src/redis/org-memory-lock.ts +1 -1
- package/src/redis/redis-lease-lock.ts +41 -8
- package/src/redis/stream-context.ts +11 -0
- package/src/runtime/agent-runtime-policy.ts +106 -21
- package/src/runtime/agent-stream-helpers.ts +2 -1
- package/src/runtime/approval-continuation.ts +12 -6
- package/src/runtime/context-compaction-constants.ts +1 -1
- package/src/runtime/context-compaction-runtime.ts +7 -5
- package/src/runtime/context-compaction.ts +40 -97
- package/src/runtime/execution-plan.ts +23 -19
- package/src/runtime/graph-designer.ts +15 -0
- package/src/runtime/helper-model.ts +10 -196
- package/src/runtime/index.ts +14 -1
- package/src/runtime/llm-content.ts +1 -1
- package/src/runtime/memory-block.ts +11 -12
- package/src/runtime/memory-pipeline.ts +26 -10
- package/src/runtime/plugin-resolution.ts +35 -0
- package/src/runtime/plugin-types.ts +73 -1
- package/src/runtime/retrieval-adapters.ts +1 -1
- package/src/runtime/runtime-config.ts +25 -12
- package/src/runtime/runtime-extensions.ts +91 -15
- 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 +11 -4
- package/src/runtime/workstream-chat-helpers.ts +6 -7
- package/src/runtime/workstream-routing-policy.ts +0 -30
- package/src/runtime/workstream-state.ts +17 -7
- 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 +7 -12
- package/src/services/context-compaction.service.ts +75 -58
- 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 +38 -33
- package/src/services/domain-agent-executor.service.ts +71 -0
- package/src/services/execution-plan.service.ts +271 -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 +30 -15
- package/src/services/memory-assessment.service.ts +3 -2
- package/src/services/{memory.utils.ts → memory-utils.ts} +4 -13
- package/src/services/memory.service.ts +55 -69
- 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 +12 -5
- 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-artifact.service.ts +1 -0
- 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 +386 -58
- package/src/services/plan-helpers.ts +15 -0
- 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 +87 -20
- 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-title.service.ts +3 -10
- package/src/services/recent-activity.service.ts +33 -43
- 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 +29 -41
- package/src/services/workstream-plan-registry.service.ts +22 -0
- package/src/services/workstream-title.service.ts +3 -9
- package/src/services/{workstream-turn-preparation.ts → workstream-turn-preparation.service.ts} +428 -373
- package/src/services/workstream-turn.ts +2 -2
- package/src/services/workstream.service.ts +55 -65
- package/src/services/workstream.types.ts +10 -19
- package/src/services/write-intent-validator.service.ts +81 -0
- package/src/storage/attachment-parser.ts +1 -1
- package/src/storage/attachment-storage.service.ts +4 -4
- package/src/storage/{attachments.utils.ts → attachment-utils.ts} +2 -5
- package/src/storage/generated-document-storage.service.ts +3 -2
- 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 +5 -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 +17 -19
- package/src/tools/fetch-webpage.tool.ts +20 -18
- package/src/tools/index.ts +2 -3
- 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 +14 -8
- package/src/tools/{tool-contract.ts → tool-contracts.ts} +9 -2
- package/src/utils/async.ts +3 -2
- package/src/utils/date-time.ts +4 -32
- package/src/utils/env.ts +8 -0
- package/src/utils/errors.ts +47 -0
- package/src/utils/hono-error-handler.ts +1 -2
- package/src/utils/index.ts +19 -2
- package/src/utils/string.ts +128 -1
- package/src/workers/bootstrap.ts +2 -2
- package/src/workers/index.ts +1 -0
- 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 +11 -105
- package/src/workers/skill-extraction.runner.ts +8 -102
- package/src/workers/utils/file-section-chunker.ts +6 -3
- 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 +97 -0
- package/src/workers/worker-utils.ts +6 -2
- package/src/runtime/retrieval-pipeline.ts +0 -3
- package/src/runtime.ts +0 -387
- package/src/tools/log-hello-world.tool.ts +0 -17
- 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
|
@@ -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'
|
|
@@ -16,6 +16,7 @@ const embeddings = getDefaultEmbeddings()
|
|
|
16
16
|
const PROMOTION_MIN_USES = 5
|
|
17
17
|
const PROMOTION_MIN_SUCCESS_RATE = 0.6
|
|
18
18
|
|
|
19
|
+
const ACTIVE_SKILL_FILTER = "AND status IN ['learned', 'verified', 'promoted'] AND archivedAt IS NONE"
|
|
19
20
|
const SKILL_EXISTS_TTL_SECONDS = 120
|
|
20
21
|
const SKILL_EXISTS_KEY_PREFIX = 'skill-exists'
|
|
21
22
|
|
|
@@ -123,7 +124,7 @@ class LearnedSkillService {
|
|
|
123
124
|
|
|
124
125
|
async update(skillId: string, input: UpdateLearnedSkillInput): Promise<LearnedSkillRow> {
|
|
125
126
|
const ref = ensureRecordId(skillId, TABLES.LEARNED_SKILL)
|
|
126
|
-
const data: Record<string, unknown> = {
|
|
127
|
+
const data: Record<string, unknown> = {}
|
|
127
128
|
|
|
128
129
|
if (input.name !== undefined) data.name = input.name
|
|
129
130
|
if (input.description !== undefined) data.description = input.description
|
|
@@ -148,11 +149,11 @@ class LearnedSkillService {
|
|
|
148
149
|
await databaseService.update(
|
|
149
150
|
TABLES.LEARNED_SKILL,
|
|
150
151
|
ref,
|
|
151
|
-
{ status: 'archived', archivedAt: new Date()
|
|
152
|
+
{ status: 'archived', archivedAt: new Date() },
|
|
152
153
|
LearnedSkillRowSchema,
|
|
153
154
|
)
|
|
154
155
|
if (skill) {
|
|
155
|
-
await this.invalidateSkillExistsCache(
|
|
156
|
+
await this.invalidateSkillExistsCache(recordIdToString(skill.organizationId), skill.agentId ?? null)
|
|
156
157
|
}
|
|
157
158
|
}
|
|
158
159
|
|
|
@@ -176,8 +177,7 @@ class LearnedSkillService {
|
|
|
176
177
|
new BoundQuery(
|
|
177
178
|
`SELECT id FROM ${TABLES.LEARNED_SKILL}
|
|
178
179
|
WHERE organizationId = $orgRef
|
|
179
|
-
|
|
180
|
-
AND archivedAt IS NONE
|
|
180
|
+
${ACTIVE_SKILL_FILTER}
|
|
181
181
|
AND (agentId IS NONE OR agentId = $agentId)
|
|
182
182
|
LIMIT 1`,
|
|
183
183
|
{ orgRef, agentId },
|
|
@@ -227,8 +227,7 @@ class LearnedSkillService {
|
|
|
227
227
|
vector::similarity::cosine(embedding, $embedding) AS similarity
|
|
228
228
|
FROM ${TABLES.LEARNED_SKILL}
|
|
229
229
|
WHERE organizationId = $organizationId
|
|
230
|
-
|
|
231
|
-
AND archivedAt IS NONE
|
|
230
|
+
${ACTIVE_SKILL_FILTER}
|
|
232
231
|
AND confidence >= $minConfidence
|
|
233
232
|
AND (agentId IS NONE OR agentId = $agentId)
|
|
234
233
|
AND embedding <|${params.limit}|> $embedding
|
|
@@ -293,7 +292,7 @@ class LearnedSkillService {
|
|
|
293
292
|
await databaseService.update(
|
|
294
293
|
TABLES.LEARNED_SKILL,
|
|
295
294
|
ref,
|
|
296
|
-
{ status: 'verified', confidence: Math.min(skill.confidence + 0.1, 1.0)
|
|
295
|
+
{ status: 'verified', confidence: Math.min(skill.confidence + 0.1, 1.0) },
|
|
297
296
|
LearnedSkillRowSchema,
|
|
298
297
|
)
|
|
299
298
|
return true
|
|
@@ -311,8 +310,7 @@ class LearnedSkillService {
|
|
|
311
310
|
vector::similarity::cosine(embedding, $embedding) AS similarity
|
|
312
311
|
FROM ${TABLES.LEARNED_SKILL}
|
|
313
312
|
WHERE organizationId = $organizationId
|
|
314
|
-
|
|
315
|
-
AND archivedAt IS NONE
|
|
313
|
+
${ACTIVE_SKILL_FILTER}
|
|
316
314
|
AND embedding <|3|> $embedding
|
|
317
315
|
ORDER BY similarity DESC
|
|
318
316
|
LIMIT 1
|
|
@@ -333,8 +331,7 @@ class LearnedSkillService {
|
|
|
333
331
|
`SELECT *, type::string(id) AS id, type::string(organizationId) AS organizationId
|
|
334
332
|
FROM ${TABLES.LEARNED_SKILL}
|
|
335
333
|
WHERE organizationId = $organizationId
|
|
336
|
-
|
|
337
|
-
AND archivedAt IS NONE
|
|
334
|
+
${ACTIVE_SKILL_FILTER}
|
|
338
335
|
ORDER BY createdAt DESC`,
|
|
339
336
|
{ organizationId: orgRef },
|
|
340
337
|
),
|
|
@@ -349,6 +346,25 @@ class LearnedSkillService {
|
|
|
349
346
|
return hasher.digest('hex')
|
|
350
347
|
}
|
|
351
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
|
+
|
|
352
368
|
async findByHash(orgId: string, hash: string): Promise<LearnedSkillRow | null> {
|
|
353
369
|
const orgRef = ensureRecordId(orgId, TABLES.ORGANIZATION)
|
|
354
370
|
const rows = await databaseService.queryMany(
|
|
@@ -357,8 +373,7 @@ class LearnedSkillService {
|
|
|
357
373
|
FROM ${TABLES.LEARNED_SKILL}
|
|
358
374
|
WHERE organizationId = $organizationId
|
|
359
375
|
AND hash = $hash
|
|
360
|
-
|
|
361
|
-
AND archivedAt IS NONE
|
|
376
|
+
${ACTIVE_SKILL_FILTER}
|
|
362
377
|
LIMIT 1`,
|
|
363
378
|
{ organizationId: orgRef, hash },
|
|
364
379
|
),
|
|
@@ -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,25 +1,17 @@
|
|
|
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 {
|
|
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 {
|
|
8
|
-
return
|
|
9
|
-
limit,
|
|
10
|
-
multiplier: VECTOR_SEARCH_OVERFETCH_MULTIPLIER,
|
|
11
|
-
minimum: MEMORY.DEFAULT_CANDIDATE_LIMIT,
|
|
12
|
-
})
|
|
8
|
+
return Math.max(limit * VECTOR_SEARCH_OVERFETCH_MULTIPLIER, MEMORY.DEFAULT_CANDIDATE_LIMIT)
|
|
13
9
|
}
|
|
14
10
|
|
|
15
11
|
export function formatMemoryResults(results: MemorySearchResult[]): string {
|
|
16
12
|
if (results.length === 0) return 'No stored memories.'
|
|
17
13
|
|
|
18
|
-
const normalize = (value: string) =>
|
|
19
|
-
const trimmed = value.replace(/\s+/g, ' ').trim()
|
|
20
|
-
if (trimmed.length <= 400) return trimmed
|
|
21
|
-
return `${trimmed.slice(0, 400)}...`
|
|
22
|
-
}
|
|
14
|
+
const normalize = (value: string) => truncateText(compactWhitespace(value), 400)
|
|
23
15
|
|
|
24
16
|
return results
|
|
25
17
|
.map((item) => {
|
|
@@ -65,8 +57,7 @@ export function formatRerankedResults(
|
|
|
65
57
|
used.add(candidate.id)
|
|
66
58
|
total += 1
|
|
67
59
|
const reason = item.relevance ? ` — ${item.relevance}` : ''
|
|
68
|
-
const
|
|
69
|
-
const content = trimmed.length <= 400 ? trimmed : `${trimmed.slice(0, 400)}...`
|
|
60
|
+
const content = truncateText(compactWhitespace(candidate.content), 400)
|
|
70
61
|
lines.push(`- ${content}${reason}`)
|
|
71
62
|
if (total >= limit) break
|
|
72
63
|
}
|