@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,67 @@
|
|
|
1
|
+
import type { PlanTemplateRecord } from '@lota-sdk/shared'
|
|
2
|
+
|
|
3
|
+
import type { RecordIdInput } from '../db/record-id'
|
|
4
|
+
import type { PlaybookContribution } from '../runtime/plugin-types'
|
|
5
|
+
import { getRuntimeConfig } from '../runtime/runtime-config'
|
|
6
|
+
import { planTemplateService } from './plan-template.service'
|
|
7
|
+
|
|
8
|
+
class PlaybookRegistryService {
|
|
9
|
+
collectPlaybooks(): PlaybookContribution[] {
|
|
10
|
+
const plugins = getRuntimeConfig().pluginRuntime ?? {}
|
|
11
|
+
const playbooks: PlaybookContribution[] = []
|
|
12
|
+
for (const plugin of Object.values(plugins)) {
|
|
13
|
+
if (plugin.playbookContributor?.playbooks) {
|
|
14
|
+
playbooks.push(...plugin.playbookContributor.playbooks)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return playbooks
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async syncPlaybookTemplates(organizationId: RecordIdInput): Promise<PlanTemplateRecord[]> {
|
|
21
|
+
const playbooks = this.collectPlaybooks()
|
|
22
|
+
const templates: PlanTemplateRecord[] = []
|
|
23
|
+
const existing = await planTemplateService.listTemplates(organizationId, { source: 'playbook' })
|
|
24
|
+
|
|
25
|
+
for (const pb of playbooks) {
|
|
26
|
+
const match = existing.find((t) => t.sourceRef === pb.name)
|
|
27
|
+
if (match) {
|
|
28
|
+
const updated = await planTemplateService.updateTemplate(match.id, { draft: pb.draft, tags: pb.tags })
|
|
29
|
+
templates.push(updated)
|
|
30
|
+
} else {
|
|
31
|
+
const created = await planTemplateService.createTemplate({
|
|
32
|
+
organizationId,
|
|
33
|
+
name: pb.name,
|
|
34
|
+
description: pb.description,
|
|
35
|
+
draft: pb.draft,
|
|
36
|
+
tags: pb.tags,
|
|
37
|
+
source: 'playbook',
|
|
38
|
+
sourceRef: pb.name,
|
|
39
|
+
})
|
|
40
|
+
templates.push(created)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return templates
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async instantiatePlaybook(params: {
|
|
48
|
+
name: string
|
|
49
|
+
organizationId: RecordIdInput
|
|
50
|
+
workstreamId: RecordIdInput
|
|
51
|
+
leadAgentId: string
|
|
52
|
+
}): Promise<unknown> {
|
|
53
|
+
const templates = await planTemplateService.listTemplates(params.organizationId, { source: 'playbook' })
|
|
54
|
+
const template = templates.find((t) => t.sourceRef === params.name)
|
|
55
|
+
if (!template) {
|
|
56
|
+
throw new Error(`Playbook "${params.name}" not found.`)
|
|
57
|
+
}
|
|
58
|
+
return planTemplateService.instantiate({
|
|
59
|
+
templateId: template.id,
|
|
60
|
+
organizationId: params.organizationId,
|
|
61
|
+
workstreamId: params.workstreamId,
|
|
62
|
+
leadAgentId: params.leadAgentId,
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const playbookRegistryService = new PlaybookRegistryService()
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { OwnershipDispatchContext, PlanNodeResult, PlanNodeSpec, PluginPlanNodeOwner } from '@lota-sdk/shared'
|
|
2
|
+
|
|
3
|
+
import type { LotaPlugin, PluginNodeExecutionParams } from '../runtime/plugin-types'
|
|
4
|
+
import { getRuntimeConfig } from '../runtime/runtime-config'
|
|
5
|
+
import type { PlanValidationIssueInput } from './plan-validator.service'
|
|
6
|
+
|
|
7
|
+
function getPluginRuntime() {
|
|
8
|
+
return (getRuntimeConfig().pluginRuntime ?? {}) as Record<string, LotaPlugin | undefined>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function buildPluginExecutionParams(params: {
|
|
12
|
+
owner: PluginPlanNodeOwner
|
|
13
|
+
nodeSpec: PlanNodeSpec
|
|
14
|
+
resolvedInput: Record<string, unknown>
|
|
15
|
+
context: OwnershipDispatchContext
|
|
16
|
+
}): PluginNodeExecutionParams {
|
|
17
|
+
return {
|
|
18
|
+
operation: params.owner.operation,
|
|
19
|
+
nodeSpec: params.nodeSpec,
|
|
20
|
+
inputs: params.resolvedInput,
|
|
21
|
+
context: {
|
|
22
|
+
organizationId: params.context.organizationId,
|
|
23
|
+
workstreamId: params.context.workstreamId,
|
|
24
|
+
planId: params.context.planId,
|
|
25
|
+
nodeId: params.context.nodeId,
|
|
26
|
+
...(params.context.userId ? { userId: params.context.userId } : {}),
|
|
27
|
+
...(params.context.userName ? { userName: params.context.userName } : {}),
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class PluginExecutorService {
|
|
33
|
+
validateOwner(owner: PluginPlanNodeOwner, nodeId: string): PlanValidationIssueInput[] {
|
|
34
|
+
const plugin = getPluginRuntime()[owner.ref]
|
|
35
|
+
if (!plugin) {
|
|
36
|
+
return [
|
|
37
|
+
{
|
|
38
|
+
severity: 'blocking',
|
|
39
|
+
code: 'plugin_executor_missing',
|
|
40
|
+
message: `Node "${nodeId}" references unknown plugin executor "${owner.ref}".`,
|
|
41
|
+
nodeId,
|
|
42
|
+
detail: { pluginRef: owner.ref },
|
|
43
|
+
},
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const nodeExecutor = plugin.nodeExecutor
|
|
48
|
+
if (!nodeExecutor) {
|
|
49
|
+
return [
|
|
50
|
+
{
|
|
51
|
+
severity: 'blocking',
|
|
52
|
+
code: 'plugin_node_executor_missing',
|
|
53
|
+
message: `Plugin "${owner.ref}" does not expose a node executor.`,
|
|
54
|
+
nodeId,
|
|
55
|
+
detail: { pluginRef: owner.ref },
|
|
56
|
+
},
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!nodeExecutor.supportedOperations.includes(owner.operation)) {
|
|
61
|
+
return [
|
|
62
|
+
{
|
|
63
|
+
severity: 'blocking',
|
|
64
|
+
code: 'plugin_operation_missing',
|
|
65
|
+
message: `Plugin "${owner.ref}" does not support operation "${owner.operation}".`,
|
|
66
|
+
nodeId,
|
|
67
|
+
detail: { pluginRef: owner.ref, operation: owner.operation },
|
|
68
|
+
},
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return []
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async executeNode(params: {
|
|
76
|
+
nodeSpec: PlanNodeSpec
|
|
77
|
+
resolvedInput: Record<string, unknown>
|
|
78
|
+
context: OwnershipDispatchContext
|
|
79
|
+
}): Promise<PlanNodeResult> {
|
|
80
|
+
if (params.nodeSpec.owner.executorType !== 'plugin') {
|
|
81
|
+
throw new Error(`PluginExecutor cannot execute owner type "${params.nodeSpec.owner.executorType}".`)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const plugin = getPluginRuntime()[params.nodeSpec.owner.ref]
|
|
85
|
+
const nodeExecutor = plugin?.nodeExecutor
|
|
86
|
+
if (!plugin || !nodeExecutor || !nodeExecutor.supportedOperations.includes(params.nodeSpec.owner.operation)) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Plugin executor ${params.nodeSpec.owner.ref}.${params.nodeSpec.owner.operation} is not registered.`,
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return nodeExecutor.executeNode(
|
|
93
|
+
buildPluginExecutionParams({
|
|
94
|
+
owner: params.nodeSpec.owner,
|
|
95
|
+
nodeSpec: params.nodeSpec,
|
|
96
|
+
resolvedInput: params.resolvedInput,
|
|
97
|
+
context: params.context,
|
|
98
|
+
}),
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const pluginExecutorService = new PluginExecutorService()
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { NodeQualityMetricsSchema } from '@lota-sdk/shared'
|
|
2
|
+
import type { NodeQualityMetrics } from '@lota-sdk/shared'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
|
|
5
|
+
import { ensureRecordId } from '../db/record-id'
|
|
6
|
+
import { databaseService } from '../db/service'
|
|
7
|
+
import { TABLES } from '../db/tables'
|
|
8
|
+
|
|
9
|
+
const QualityMetricRowSchema = NodeQualityMetricsSchema.extend({
|
|
10
|
+
id: z.unknown(),
|
|
11
|
+
organizationId: z.unknown(),
|
|
12
|
+
runId: z.unknown(),
|
|
13
|
+
nodeId: z.string().optional(),
|
|
14
|
+
createdAt: z.coerce.date().optional(),
|
|
15
|
+
updatedAt: z.coerce.date().optional(),
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
class QualityMetricsService {
|
|
19
|
+
async recordNodeMetrics(params: {
|
|
20
|
+
organizationId: string
|
|
21
|
+
runId: string
|
|
22
|
+
nodeId: string
|
|
23
|
+
metrics: NodeQualityMetrics
|
|
24
|
+
}): Promise<void> {
|
|
25
|
+
await databaseService.create(
|
|
26
|
+
TABLES.QUALITY_METRIC,
|
|
27
|
+
{
|
|
28
|
+
organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
|
|
29
|
+
runId: ensureRecordId(params.runId, TABLES.PLAN_RUN),
|
|
30
|
+
nodeId: params.nodeId,
|
|
31
|
+
ownerRef: params.metrics.ownerRef,
|
|
32
|
+
ownerType: params.metrics.ownerType,
|
|
33
|
+
nodeType: params.metrics.nodeType,
|
|
34
|
+
executionTimeMs: params.metrics.executionTimeMs,
|
|
35
|
+
attemptCount: params.metrics.attemptCount,
|
|
36
|
+
artifactCount: params.metrics.artifactCount,
|
|
37
|
+
validationIssueCount: params.metrics.validationIssueCount,
|
|
38
|
+
},
|
|
39
|
+
QualityMetricRowSchema,
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async recordCycleMetrics(params: { organizationId: string; runId: string }): Promise<void> {
|
|
44
|
+
const existing = await databaseService.findMany(
|
|
45
|
+
TABLES.QUALITY_METRIC,
|
|
46
|
+
{ runId: ensureRecordId(params.runId, TABLES.PLAN_RUN) },
|
|
47
|
+
QualityMetricRowSchema,
|
|
48
|
+
{ orderBy: 'createdAt', orderDir: 'ASC' },
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if (!Array.isArray(existing) || existing.length === 0) return
|
|
52
|
+
|
|
53
|
+
const totalExecutionTime = existing.reduce(
|
|
54
|
+
(sum, m) => sum + (typeof m.executionTimeMs === 'number' ? m.executionTimeMs : 0),
|
|
55
|
+
0,
|
|
56
|
+
)
|
|
57
|
+
const totalAttempts = existing.reduce(
|
|
58
|
+
(sum, m) => sum + (typeof m.attemptCount === 'number' ? m.attemptCount : 0),
|
|
59
|
+
0,
|
|
60
|
+
)
|
|
61
|
+
const totalIssues = existing.reduce(
|
|
62
|
+
(sum, m) => sum + (typeof m.validationIssueCount === 'number' ? m.validationIssueCount : 0),
|
|
63
|
+
0,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
await databaseService.create(
|
|
67
|
+
TABLES.QUALITY_METRIC,
|
|
68
|
+
{
|
|
69
|
+
organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
|
|
70
|
+
runId: ensureRecordId(params.runId, TABLES.PLAN_RUN),
|
|
71
|
+
ownerRef: 'cycle-summary',
|
|
72
|
+
ownerType: 'system',
|
|
73
|
+
nodeType: 'action',
|
|
74
|
+
executionTimeMs: totalExecutionTime,
|
|
75
|
+
attemptCount: totalAttempts,
|
|
76
|
+
artifactCount: existing.length,
|
|
77
|
+
validationIssueCount: totalIssues,
|
|
78
|
+
},
|
|
79
|
+
QualityMetricRowSchema,
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
detectRegression(params: {
|
|
84
|
+
metrics: Array<{ executionTimeMs: number; validationIssueCount: number }>
|
|
85
|
+
window?: number
|
|
86
|
+
}): { detected: boolean; trend?: 'improving' | 'stable' | 'regressing' } {
|
|
87
|
+
const window = params.window ?? 5
|
|
88
|
+
const recent = params.metrics.slice(-window)
|
|
89
|
+
if (recent.length < 2) return { detected: false, trend: 'stable' }
|
|
90
|
+
|
|
91
|
+
const mid = Math.floor(recent.length / 2)
|
|
92
|
+
const firstHalf = recent.slice(0, mid)
|
|
93
|
+
const secondHalf = recent.slice(mid)
|
|
94
|
+
|
|
95
|
+
const avgFirst = firstHalf.reduce((sum, m) => sum + m.validationIssueCount, 0) / firstHalf.length
|
|
96
|
+
const avgSecond = secondHalf.reduce((sum, m) => sum + m.validationIssueCount, 0) / secondHalf.length
|
|
97
|
+
|
|
98
|
+
const timeFirst = firstHalf.reduce((sum, m) => sum + m.executionTimeMs, 0) / firstHalf.length
|
|
99
|
+
const timeSecond = secondHalf.reduce((sum, m) => sum + m.executionTimeMs, 0) / secondHalf.length
|
|
100
|
+
|
|
101
|
+
const issueRegression = avgFirst > 0 && avgSecond > avgFirst * 1.5
|
|
102
|
+
const timeRegression = timeFirst > 0 && timeSecond > timeFirst * 2
|
|
103
|
+
|
|
104
|
+
if (issueRegression || timeRegression) {
|
|
105
|
+
return { detected: true, trend: 'regressing' }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const issueImproving = avgFirst > 0 && avgSecond < avgFirst * 0.7
|
|
109
|
+
const timeImproving = timeFirst > 0 && timeSecond < timeFirst * 0.7
|
|
110
|
+
|
|
111
|
+
if (issueImproving || timeImproving) {
|
|
112
|
+
return { detected: false, trend: 'improving' }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { detected: false, trend: 'stable' }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async getMetricsByOwner(params: {
|
|
119
|
+
organizationId: string
|
|
120
|
+
ownerRef: string
|
|
121
|
+
limit?: number
|
|
122
|
+
}): Promise<z.infer<typeof QualityMetricRowSchema>[]> {
|
|
123
|
+
return databaseService.findMany(
|
|
124
|
+
TABLES.QUALITY_METRIC,
|
|
125
|
+
{ organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION), ownerRef: params.ownerRef },
|
|
126
|
+
QualityMetricRowSchema,
|
|
127
|
+
{ orderBy: 'createdAt', orderDir: 'DESC', limit: params.limit ?? 20 },
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export const qualityMetricsService = new QualityMetricsService()
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
RecentActivitySchema,
|
|
9
9
|
RecentActivitySourceSchema,
|
|
10
10
|
RecentActivityTitleSourceSchema,
|
|
11
|
+
recordIdSchema,
|
|
11
12
|
} from '@lota-sdk/shared'
|
|
12
13
|
import type {
|
|
13
14
|
RecentActivity,
|
|
@@ -23,12 +24,12 @@ import type { RecordIdInput, RecordIdRef } from '../db/record-id'
|
|
|
23
24
|
import { databaseService } from '../db/service'
|
|
24
25
|
import { TABLES } from '../db/tables'
|
|
25
26
|
import { toIsoDateTimeString, toOptionalIsoDateTimeString } from '../utils/date-time'
|
|
26
|
-
import { compactWhitespace } from '../utils/string'
|
|
27
|
+
import { compactRecord, compactWhitespace, truncateText } from '../utils/string'
|
|
27
28
|
|
|
28
29
|
const RecentActivityEventRowSchema = z.object({
|
|
29
|
-
id:
|
|
30
|
-
organizationId:
|
|
31
|
-
userId:
|
|
30
|
+
id: recordIdSchema,
|
|
31
|
+
organizationId: recordIdSchema,
|
|
32
|
+
userId: recordIdSchema,
|
|
32
33
|
sourceEventId: z.string(),
|
|
33
34
|
source: RecentActivitySourceSchema,
|
|
34
35
|
kind: RecentActivityEventInputSchema.shape.kind,
|
|
@@ -39,14 +40,14 @@ const RecentActivityEventRowSchema = z.object({
|
|
|
39
40
|
sourceLabel: z.string(),
|
|
40
41
|
deepLink: RecentActivityDeepLinkSchema,
|
|
41
42
|
metadata: RecentActivityMetadataSchema.optional(),
|
|
42
|
-
occurredAt: z.
|
|
43
|
-
createdAt: z.
|
|
43
|
+
occurredAt: z.coerce.date(),
|
|
44
|
+
createdAt: z.coerce.date(),
|
|
44
45
|
})
|
|
45
46
|
|
|
46
47
|
const RecentActivityRowSchema = z.object({
|
|
47
|
-
id:
|
|
48
|
-
organizationId:
|
|
49
|
-
userId:
|
|
48
|
+
id: recordIdSchema,
|
|
49
|
+
organizationId: recordIdSchema,
|
|
50
|
+
userId: recordIdSchema,
|
|
50
51
|
mergeKey: z.string(),
|
|
51
52
|
kind: RecentActivityEventInputSchema.shape.kind,
|
|
52
53
|
targetKind: RecentActivityEventInputSchema.shape.targetKind,
|
|
@@ -57,27 +58,17 @@ const RecentActivityRowSchema = z.object({
|
|
|
57
58
|
sourceLabel: z.string(),
|
|
58
59
|
deepLink: RecentActivityDeepLinkSchema,
|
|
59
60
|
metadata: RecentActivityMetadataSchema.optional(),
|
|
60
|
-
latestEventId:
|
|
61
|
+
latestEventId: recordIdSchema.optional(),
|
|
61
62
|
latestSourceEventId: z.string().optional(),
|
|
62
|
-
latestEventAt: z.
|
|
63
|
-
titleRefinedAt: z.
|
|
64
|
-
createdAt: z.
|
|
65
|
-
updatedAt: z.
|
|
63
|
+
latestEventAt: z.coerce.date(),
|
|
64
|
+
titleRefinedAt: z.coerce.date().optional(),
|
|
65
|
+
createdAt: z.coerce.date(),
|
|
66
|
+
updatedAt: z.coerce.date(),
|
|
66
67
|
})
|
|
67
68
|
|
|
68
69
|
type RecentActivityEventRow = z.infer<typeof RecentActivityEventRowSchema>
|
|
69
70
|
type RecentActivityRow = z.infer<typeof RecentActivityRowSchema>
|
|
70
71
|
|
|
71
|
-
function compactRecord(value: Record<string, unknown>): Record<string, unknown> {
|
|
72
|
-
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== null && entry !== undefined))
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function clampText(value: string, maxLength: number): string {
|
|
76
|
-
const normalized = compactWhitespace(value)
|
|
77
|
-
if (normalized.length <= maxLength) return normalized
|
|
78
|
-
return normalized.slice(0, maxLength).trim()
|
|
79
|
-
}
|
|
80
|
-
|
|
81
72
|
function buildDeterministicRecordId(table: string, key: string): RecordId {
|
|
82
73
|
const digest = createHash('sha256').update(key).digest('hex')
|
|
83
74
|
return new RecordId(table, digest)
|
|
@@ -90,6 +81,11 @@ function shouldKeepExistingAgentTitle(existing: RecentActivityRow | null): boole
|
|
|
90
81
|
function buildRecentActivityAreaKey(
|
|
91
82
|
row: Pick<RecentActivityRow, 'targetKind' | 'targetId' | 'kind' | 'mergeKey' | 'metadata'>,
|
|
92
83
|
): string {
|
|
84
|
+
const workstreamId = row.metadata?.workstreamId
|
|
85
|
+
if (workstreamId) {
|
|
86
|
+
return `workstream:${compactWhitespace(workstreamId)}`
|
|
87
|
+
}
|
|
88
|
+
|
|
93
89
|
if (row.targetId) {
|
|
94
90
|
return `${row.targetKind}:${row.targetId}`
|
|
95
91
|
}
|
|
@@ -149,11 +145,11 @@ class RecentActivityService {
|
|
|
149
145
|
private sanitizeEvent(input: RecentActivityEventInput): RecentActivityEventInput {
|
|
150
146
|
return {
|
|
151
147
|
...input,
|
|
152
|
-
sourceEventId:
|
|
153
|
-
...(input.targetId ? { targetId:
|
|
154
|
-
mergeKey:
|
|
155
|
-
title:
|
|
156
|
-
sourceLabel:
|
|
148
|
+
sourceEventId: truncateText(input.sourceEventId, 200),
|
|
149
|
+
...(input.targetId ? { targetId: truncateText(input.targetId, 200) } : {}),
|
|
150
|
+
mergeKey: truncateText(input.mergeKey, 200),
|
|
151
|
+
title: truncateText(input.title, 140),
|
|
152
|
+
sourceLabel: truncateText(input.sourceLabel, 40),
|
|
157
153
|
...(input.metadata ? { metadata: RecentActivityMetadataSchema.parse(input.metadata) } : {}),
|
|
158
154
|
}
|
|
159
155
|
}
|
|
@@ -309,7 +305,7 @@ class RecentActivityService {
|
|
|
309
305
|
const existing = await databaseService.findOne(TABLES.RECENT_ACTIVITY, { id: activityRef }, RecentActivityRowSchema)
|
|
310
306
|
if (!existing) return null
|
|
311
307
|
|
|
312
|
-
const nextTitle =
|
|
308
|
+
const nextTitle = truncateText(params.title, 80)
|
|
313
309
|
if (!nextTitle) return this.toPublicItem(existing)
|
|
314
310
|
if (compactWhitespace(nextTitle).toLowerCase() === compactWhitespace(existing.title).toLowerCase()) {
|
|
315
311
|
return this.toPublicItem(existing)
|
|
@@ -347,7 +343,7 @@ class RecentActivityService {
|
|
|
347
343
|
systemTitle: row.systemTitle,
|
|
348
344
|
sourceLabel: row.sourceLabel,
|
|
349
345
|
kind: row.kind,
|
|
350
|
-
metadata: RecentActivityMetadataSchema.parse(row.metadata
|
|
346
|
+
metadata: RecentActivityMetadataSchema.parse(row.metadata),
|
|
351
347
|
}
|
|
352
348
|
}
|
|
353
349
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
class SkillResolverService {
|
|
2
|
+
async resolve(params: {
|
|
3
|
+
skillRef: string
|
|
4
|
+
organizationId: string
|
|
5
|
+
}): Promise<{ executorType: 'agent' | 'plugin'; ref: string; operation?: string } | null> {
|
|
6
|
+
// Dynamic import to avoid circular dependencies and enable testability
|
|
7
|
+
const { learnedSkillService } = await import('./learned-skill.service')
|
|
8
|
+
const skill = await learnedSkillService.findByNameOrTag(params.organizationId, params.skillRef)
|
|
9
|
+
if (!skill) return null
|
|
10
|
+
|
|
11
|
+
if (skill.agentId) {
|
|
12
|
+
return { executorType: 'agent', ref: skill.agentId }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const skillResolverService = new SkillResolverService()
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { OwnershipDispatchContext, PlanNodeResult, PlanNodeSpec, SystemPlanNodeOwner } from '@lota-sdk/shared'
|
|
2
|
+
|
|
3
|
+
import type { SystemNodeExecutor, PluginNodeExecutionParams } from '../runtime/plugin-types'
|
|
4
|
+
import { getRuntimeConfig } from '../runtime/runtime-config'
|
|
5
|
+
import type { PlanValidationIssueInput } from './plan-validator.service'
|
|
6
|
+
|
|
7
|
+
const BUILT_IN_SYSTEM_EXECUTORS = Object.freeze({
|
|
8
|
+
'plan-runtime': {
|
|
9
|
+
supportedOperations: ['echo-input'],
|
|
10
|
+
async executeNode(params: PluginNodeExecutionParams): Promise<PlanNodeResult> {
|
|
11
|
+
return { structuredOutput: structuredClone(params.inputs), artifacts: [] }
|
|
12
|
+
},
|
|
13
|
+
} satisfies SystemNodeExecutor,
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export function getBuiltInSystemExecutors(): Record<string, SystemNodeExecutor> {
|
|
17
|
+
return Object.fromEntries(Object.entries(BUILT_IN_SYSTEM_EXECUTORS))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getSystemExecutors() {
|
|
21
|
+
return (getRuntimeConfig().systemExecutors ?? getBuiltInSystemExecutors()) as Record<
|
|
22
|
+
string,
|
|
23
|
+
SystemNodeExecutor | undefined
|
|
24
|
+
>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function buildSystemExecutionParams(params: {
|
|
28
|
+
owner: SystemPlanNodeOwner
|
|
29
|
+
nodeSpec: PlanNodeSpec
|
|
30
|
+
resolvedInput: Record<string, unknown>
|
|
31
|
+
context: OwnershipDispatchContext
|
|
32
|
+
}): PluginNodeExecutionParams {
|
|
33
|
+
return {
|
|
34
|
+
operation: params.owner.operation,
|
|
35
|
+
nodeSpec: params.nodeSpec,
|
|
36
|
+
inputs: params.resolvedInput,
|
|
37
|
+
context: {
|
|
38
|
+
organizationId: params.context.organizationId,
|
|
39
|
+
workstreamId: params.context.workstreamId,
|
|
40
|
+
planId: params.context.planId,
|
|
41
|
+
nodeId: params.context.nodeId,
|
|
42
|
+
...(params.context.userId ? { userId: params.context.userId } : {}),
|
|
43
|
+
...(params.context.userName ? { userName: params.context.userName } : {}),
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
class SystemExecutorService {
|
|
49
|
+
validateOwner(owner: SystemPlanNodeOwner, nodeId: string): PlanValidationIssueInput[] {
|
|
50
|
+
const executor = getSystemExecutors()[owner.ref]
|
|
51
|
+
if (!executor) {
|
|
52
|
+
return [
|
|
53
|
+
{
|
|
54
|
+
severity: 'blocking',
|
|
55
|
+
code: 'system_executor_missing',
|
|
56
|
+
message: `Node "${nodeId}" references unknown system executor "${owner.ref}".`,
|
|
57
|
+
nodeId,
|
|
58
|
+
detail: { systemRef: owner.ref },
|
|
59
|
+
},
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!executor.supportedOperations.includes(owner.operation)) {
|
|
64
|
+
return [
|
|
65
|
+
{
|
|
66
|
+
severity: 'blocking',
|
|
67
|
+
code: 'system_operation_missing',
|
|
68
|
+
message: `System executor "${owner.ref}" does not support operation "${owner.operation}".`,
|
|
69
|
+
nodeId,
|
|
70
|
+
detail: { systemRef: owner.ref, operation: owner.operation },
|
|
71
|
+
},
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return []
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async executeNode(params: {
|
|
79
|
+
nodeSpec: PlanNodeSpec
|
|
80
|
+
resolvedInput: Record<string, unknown>
|
|
81
|
+
context: OwnershipDispatchContext
|
|
82
|
+
}): Promise<PlanNodeResult> {
|
|
83
|
+
if (params.nodeSpec.owner.executorType !== 'system') {
|
|
84
|
+
throw new Error(`SystemExecutor cannot execute owner type "${params.nodeSpec.owner.executorType}".`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const executor = getSystemExecutors()[params.nodeSpec.owner.ref]
|
|
88
|
+
if (!executor || !executor.supportedOperations.includes(params.nodeSpec.owner.operation)) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`System executor ${params.nodeSpec.owner.ref}.${params.nodeSpec.owner.operation} is not registered.`,
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return executor.executeNode(
|
|
95
|
+
buildSystemExecutionParams({
|
|
96
|
+
owner: params.nodeSpec.owner,
|
|
97
|
+
nodeSpec: params.nodeSpec,
|
|
98
|
+
resolvedInput: params.resolvedInput,
|
|
99
|
+
context: params.context,
|
|
100
|
+
}),
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const systemExecutorService = new SystemExecutorService()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto'
|
|
2
2
|
|
|
3
|
-
import { parseRowMetadata,
|
|
3
|
+
import { parseRowMetadata, recordIdSchema, requireTimestamp, withCreatedAtMetadata } from '@lota-sdk/shared'
|
|
4
4
|
import type { ChatMessage } from '@lota-sdk/shared'
|
|
5
5
|
import { RecordId, surql } from 'surrealdb'
|
|
6
6
|
import { z } from 'zod'
|
|
@@ -12,24 +12,10 @@ import { recordIdToString } from '../db/record-id'
|
|
|
12
12
|
import type { RecordIdRef } from '../db/record-id'
|
|
13
13
|
import { databaseService } from '../db/service'
|
|
14
14
|
import { TABLES } from '../db/tables'
|
|
15
|
+
import { WorkstreamMessageRowSchema } from '../db/workstream-message-row'
|
|
16
|
+
import type { WorkstreamMessageRow } from '../db/workstream-message-row'
|
|
15
17
|
|
|
16
|
-
const
|
|
17
|
-
id: z.unknown(),
|
|
18
|
-
workstreamId: z.unknown(),
|
|
19
|
-
messageId: z.string(),
|
|
20
|
-
role: z.enum(['system', 'user', 'assistant']),
|
|
21
|
-
parts: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
22
|
-
metadata: z.record(z.string(), z.unknown()).nullish(),
|
|
23
|
-
createdAt: z.union([z.date(), z.string(), z.number()]),
|
|
24
|
-
updatedAt: z.union([z.date(), z.string(), z.number()]).optional(),
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
type WorkstreamMessageRow = z.infer<typeof WorkstreamMessageRowSchema>
|
|
28
|
-
|
|
29
|
-
const WorkstreamMessageExistingRowSchema = z.object({
|
|
30
|
-
id: z.unknown(),
|
|
31
|
-
createdAt: z.union([z.date(), z.string(), z.number()]),
|
|
32
|
-
})
|
|
18
|
+
const WorkstreamMessageExistingRowSchema = z.object({ id: recordIdSchema, createdAt: z.coerce.date() })
|
|
33
19
|
|
|
34
20
|
function toMessageId(value: string | RecordIdRef): string {
|
|
35
21
|
return recordIdToString(value, TABLES.WORKSTREAM_MESSAGE)
|
|
@@ -49,10 +35,10 @@ function toWorkstreamMessageRowId(workstreamId: RecordIdRef, messageId: string):
|
|
|
49
35
|
}
|
|
50
36
|
|
|
51
37
|
function toChatMessage(row: WorkstreamMessageRow): ChatMessage {
|
|
52
|
-
const rowCreatedAt =
|
|
38
|
+
const rowCreatedAt = requireTimestamp(row.createdAt)
|
|
53
39
|
const metadata = withCreatedAtMetadata(parseRowMetadata(row.metadata), rowCreatedAt)
|
|
54
40
|
|
|
55
|
-
return { id: row.messageId, role: row.role, parts:
|
|
41
|
+
return { id: row.messageId, role: row.role, parts: row.parts as ChatMessage['parts'], metadata }
|
|
56
42
|
}
|
|
57
43
|
|
|
58
44
|
const workstreamPaginationConfig: CursorPaginationConfig = {
|
|
@@ -102,9 +88,7 @@ class WorkstreamMessageService {
|
|
|
102
88
|
WorkstreamMessageExistingRowSchema,
|
|
103
89
|
)
|
|
104
90
|
const persistedCreatedAt =
|
|
105
|
-
existingRow === null
|
|
106
|
-
? (toTimestamp(message.metadata?.createdAt) ?? Date.now())
|
|
107
|
-
: (toTimestamp(existingRow.createdAt) ?? Date.now())
|
|
91
|
+
existingRow === null ? requireTimestamp(message.metadata?.createdAt) : requireTimestamp(existingRow.createdAt)
|
|
108
92
|
const metadata = withCreatedAtMetadata({ ...message.metadata, createdAt: persistedCreatedAt })
|
|
109
93
|
|
|
110
94
|
await databaseService.upsert(
|
|
@@ -116,9 +100,7 @@ class WorkstreamMessageService {
|
|
|
116
100
|
role,
|
|
117
101
|
parts,
|
|
118
102
|
metadata,
|
|
119
|
-
createdAt: existingRow
|
|
120
|
-
? new Date(toTimestamp(existingRow.createdAt) ?? Date.now())
|
|
121
|
-
: new Date(persistedCreatedAt),
|
|
103
|
+
createdAt: existingRow ? existingRow.createdAt : new Date(persistedCreatedAt),
|
|
122
104
|
},
|
|
123
105
|
WorkstreamMessageRowSchema,
|
|
124
106
|
{ mutation: 'content' },
|
|
@@ -152,7 +134,7 @@ class WorkstreamMessageService {
|
|
|
152
134
|
async listMessagesAfterCursor(workstreamId: RecordIdRef, afterMessageId?: string): Promise<ChatMessage[]> {
|
|
153
135
|
const cursorMessageId = afterMessageId?.trim()
|
|
154
136
|
if (!cursorMessageId) {
|
|
155
|
-
return
|
|
137
|
+
return this.listMessages(workstreamId)
|
|
156
138
|
}
|
|
157
139
|
|
|
158
140
|
const cursorRow = await databaseService.findOne(
|
|
@@ -165,7 +147,7 @@ class WorkstreamMessageService {
|
|
|
165
147
|
throw new Error(`Workstream cursor message not found: ${cursorMessageId}`)
|
|
166
148
|
}
|
|
167
149
|
|
|
168
|
-
const cursorCreatedAt =
|
|
150
|
+
const cursorCreatedAt = cursorRow.createdAt
|
|
169
151
|
const cursorId = toWorkstreamMessageRowId(workstreamId, cursorMessageId)
|
|
170
152
|
const rows = await databaseService.query<unknown>(surql`
|
|
171
153
|
SELECT * FROM workstreamMessage
|
|
@@ -209,7 +191,7 @@ class WorkstreamMessageService {
|
|
|
209
191
|
.map((message) => ({
|
|
210
192
|
id: message.id,
|
|
211
193
|
role: message.role as 'user' | 'assistant',
|
|
212
|
-
createdAt: new Date(
|
|
194
|
+
createdAt: new Date(requireTimestamp(message.metadata?.createdAt)).toISOString(),
|
|
213
195
|
content: message.parts
|
|
214
196
|
.flatMap((part) => (part.type === 'text' && typeof part.text === 'string' ? [part.text] : []))
|
|
215
197
|
.join('\n')
|
|
@@ -245,7 +227,7 @@ class WorkstreamMessageService {
|
|
|
245
227
|
id: toMessageId(params.messageId),
|
|
246
228
|
role: 'assistant',
|
|
247
229
|
parts: params.parts,
|
|
248
|
-
metadata: withCreatedAtMetadata(params.metadata),
|
|
230
|
+
metadata: withCreatedAtMetadata(params.metadata, Date.now()),
|
|
249
231
|
}
|
|
250
232
|
|
|
251
233
|
await this.upsertMessages({ workstreamId: params.workstreamId, messages: [message] })
|
|
@@ -283,10 +265,6 @@ class WorkstreamMessageService {
|
|
|
283
265
|
],
|
|
284
266
|
})
|
|
285
267
|
}
|
|
286
|
-
|
|
287
|
-
async listAllMessages(workstreamId: RecordIdRef): Promise<ChatMessage[]> {
|
|
288
|
-
return await this.listMessages(workstreamId)
|
|
289
|
-
}
|
|
290
268
|
}
|
|
291
269
|
|
|
292
270
|
export const workstreamMessageService = new WorkstreamMessageService()
|