@lota-sdk/core 0.1.15 → 0.1.17
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 +12 -8
- package/src/ai/definitions.ts +81 -3
- package/src/ai/embedding-cache.ts +2 -4
- package/src/ai/index.ts +0 -2
- package/src/bifrost/bifrost.ts +2 -7
- package/src/bifrost/cache-headers.ts +8 -0
- package/src/bifrost/index.ts +1 -0
- 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 +269 -178
- package/src/db/cursor-pagination.ts +3 -6
- package/src/db/index.ts +2 -0
- package/src/db/memory-store.helpers.ts +1 -3
- 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/schema-fingerprint.ts +1 -3
- 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/document-processor.queue.ts +2 -4
- 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 +20 -55
- 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/redis-lease-lock.ts +1 -2
- package/src/redis/stream-context.ts +11 -0
- package/src/runtime/agent-runtime-policy.ts +109 -35
- package/src/runtime/approval-continuation.ts +12 -6
- package/src/runtime/context-compaction-runtime.ts +1 -1
- package/src/runtime/context-compaction.ts +24 -64
- 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 +3 -1
- 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 +111 -14
- package/src/runtime/runtime-extensions.ts +2 -3
- package/src/runtime/runtime-worker-registry.ts +6 -0
- package/src/runtime/social-chat.ts +752 -0
- package/src/runtime/team-consultation-orchestrator.ts +45 -32
- 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 +292 -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 +2 -4
- 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 +27 -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 +49 -61
- 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 +28 -34
- package/src/services/skill-resolver.service.ts +19 -0
- package/src/services/social-chat-history.service.ts +197 -0
- package/src/services/system-executor.service.ts +105 -0
- package/src/services/workstream-message.service.ts +13 -37
- 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 -89
- 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/context-compaction.agent.ts +2 -0
- package/src/system-agents/delegated-agent-factory.ts +5 -0
- package/src/system-agents/memory-reranker.agent.ts +4 -2
- package/src/system-agents/memory.agent.ts +2 -0
- package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -0
- package/src/system-agents/regular-chat-memory-digest.agent.ts +2 -0
- package/src/system-agents/skill-extractor.agent.ts +2 -0
- package/src/system-agents/skill-manager.agent.ts +2 -0
- package/src/system-agents/title-generator.agent.ts +2 -0
- package/src/tools/execution-plan.tool.ts +17 -23
- package/src/tools/index.ts +0 -1
- package/src/tools/research-topic.tool.ts +2 -0
- package/src/tools/team-think.tool.ts +5 -6
- 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.helpers.ts +1 -1
- package/src/workers/regular-chat-memory-digest.runner.ts +45 -12
- package/src/workers/skill-extraction.runner.ts +26 -6
- package/src/workers/utils/file-section-chunker.ts +2 -1
- package/src/workers/utils/repo-structure-extractor.ts +2 -2
- 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 +14 -25
- package/src/workers/worker-utils.ts +2 -2
- package/src/runtime/workstream-routing-policy.ts +0 -267
- 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()
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto'
|
|
2
|
-
|
|
3
1
|
import {
|
|
4
2
|
RecentActivityDeepLinkSchema,
|
|
5
3
|
RecentActivityEventInputSchema,
|
|
@@ -8,6 +6,7 @@ import {
|
|
|
8
6
|
RecentActivitySchema,
|
|
9
7
|
RecentActivitySourceSchema,
|
|
10
8
|
RecentActivityTitleSourceSchema,
|
|
9
|
+
recordIdSchema,
|
|
11
10
|
} from '@lota-sdk/shared'
|
|
12
11
|
import type {
|
|
13
12
|
RecentActivity,
|
|
@@ -23,12 +22,12 @@ import type { RecordIdInput, RecordIdRef } from '../db/record-id'
|
|
|
23
22
|
import { databaseService } from '../db/service'
|
|
24
23
|
import { TABLES } from '../db/tables'
|
|
25
24
|
import { toIsoDateTimeString, toOptionalIsoDateTimeString } from '../utils/date-time'
|
|
26
|
-
import { compactWhitespace } from '../utils/string'
|
|
25
|
+
import { compactRecord, compactWhitespace, truncateText } from '../utils/string'
|
|
27
26
|
|
|
28
27
|
const RecentActivityEventRowSchema = z.object({
|
|
29
|
-
id:
|
|
30
|
-
organizationId:
|
|
31
|
-
userId:
|
|
28
|
+
id: recordIdSchema,
|
|
29
|
+
organizationId: recordIdSchema,
|
|
30
|
+
userId: recordIdSchema,
|
|
32
31
|
sourceEventId: z.string(),
|
|
33
32
|
source: RecentActivitySourceSchema,
|
|
34
33
|
kind: RecentActivityEventInputSchema.shape.kind,
|
|
@@ -39,14 +38,14 @@ const RecentActivityEventRowSchema = z.object({
|
|
|
39
38
|
sourceLabel: z.string(),
|
|
40
39
|
deepLink: RecentActivityDeepLinkSchema,
|
|
41
40
|
metadata: RecentActivityMetadataSchema.optional(),
|
|
42
|
-
occurredAt: z.
|
|
43
|
-
createdAt: z.
|
|
41
|
+
occurredAt: z.coerce.date(),
|
|
42
|
+
createdAt: z.coerce.date(),
|
|
44
43
|
})
|
|
45
44
|
|
|
46
45
|
const RecentActivityRowSchema = z.object({
|
|
47
|
-
id:
|
|
48
|
-
organizationId:
|
|
49
|
-
userId:
|
|
46
|
+
id: recordIdSchema,
|
|
47
|
+
organizationId: recordIdSchema,
|
|
48
|
+
userId: recordIdSchema,
|
|
50
49
|
mergeKey: z.string(),
|
|
51
50
|
kind: RecentActivityEventInputSchema.shape.kind,
|
|
52
51
|
targetKind: RecentActivityEventInputSchema.shape.targetKind,
|
|
@@ -57,29 +56,19 @@ const RecentActivityRowSchema = z.object({
|
|
|
57
56
|
sourceLabel: z.string(),
|
|
58
57
|
deepLink: RecentActivityDeepLinkSchema,
|
|
59
58
|
metadata: RecentActivityMetadataSchema.optional(),
|
|
60
|
-
latestEventId:
|
|
59
|
+
latestEventId: recordIdSchema.optional(),
|
|
61
60
|
latestSourceEventId: z.string().optional(),
|
|
62
|
-
latestEventAt: z.
|
|
63
|
-
titleRefinedAt: z.
|
|
64
|
-
createdAt: z.
|
|
65
|
-
updatedAt: z.
|
|
61
|
+
latestEventAt: z.coerce.date(),
|
|
62
|
+
titleRefinedAt: z.coerce.date().optional(),
|
|
63
|
+
createdAt: z.coerce.date(),
|
|
64
|
+
updatedAt: z.coerce.date(),
|
|
66
65
|
})
|
|
67
66
|
|
|
68
67
|
type RecentActivityEventRow = z.infer<typeof RecentActivityEventRowSchema>
|
|
69
68
|
type RecentActivityRow = z.infer<typeof RecentActivityRowSchema>
|
|
70
69
|
|
|
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
70
|
function buildDeterministicRecordId(table: string, key: string): RecordId {
|
|
82
|
-
const digest =
|
|
71
|
+
const digest = new Bun.CryptoHasher('sha256').update(key).digest('hex')
|
|
83
72
|
return new RecordId(table, digest)
|
|
84
73
|
}
|
|
85
74
|
|
|
@@ -90,6 +79,11 @@ function shouldKeepExistingAgentTitle(existing: RecentActivityRow | null): boole
|
|
|
90
79
|
function buildRecentActivityAreaKey(
|
|
91
80
|
row: Pick<RecentActivityRow, 'targetKind' | 'targetId' | 'kind' | 'mergeKey' | 'metadata'>,
|
|
92
81
|
): string {
|
|
82
|
+
const workstreamId = row.metadata?.workstreamId
|
|
83
|
+
if (workstreamId) {
|
|
84
|
+
return `workstream:${compactWhitespace(workstreamId)}`
|
|
85
|
+
}
|
|
86
|
+
|
|
93
87
|
if (row.targetId) {
|
|
94
88
|
return `${row.targetKind}:${row.targetId}`
|
|
95
89
|
}
|
|
@@ -149,11 +143,11 @@ class RecentActivityService {
|
|
|
149
143
|
private sanitizeEvent(input: RecentActivityEventInput): RecentActivityEventInput {
|
|
150
144
|
return {
|
|
151
145
|
...input,
|
|
152
|
-
sourceEventId:
|
|
153
|
-
...(input.targetId ? { targetId:
|
|
154
|
-
mergeKey:
|
|
155
|
-
title:
|
|
156
|
-
sourceLabel:
|
|
146
|
+
sourceEventId: truncateText(input.sourceEventId, 200),
|
|
147
|
+
...(input.targetId ? { targetId: truncateText(input.targetId, 200) } : {}),
|
|
148
|
+
mergeKey: truncateText(input.mergeKey, 200),
|
|
149
|
+
title: truncateText(input.title, 140),
|
|
150
|
+
sourceLabel: truncateText(input.sourceLabel, 40),
|
|
157
151
|
...(input.metadata ? { metadata: RecentActivityMetadataSchema.parse(input.metadata) } : {}),
|
|
158
152
|
}
|
|
159
153
|
}
|
|
@@ -309,7 +303,7 @@ class RecentActivityService {
|
|
|
309
303
|
const existing = await databaseService.findOne(TABLES.RECENT_ACTIVITY, { id: activityRef }, RecentActivityRowSchema)
|
|
310
304
|
if (!existing) return null
|
|
311
305
|
|
|
312
|
-
const nextTitle =
|
|
306
|
+
const nextTitle = truncateText(params.title, 80)
|
|
313
307
|
if (!nextTitle) return this.toPublicItem(existing)
|
|
314
308
|
if (compactWhitespace(nextTitle).toLowerCase() === compactWhitespace(existing.title).toLowerCase()) {
|
|
315
309
|
return this.toPublicItem(existing)
|
|
@@ -347,7 +341,7 @@ class RecentActivityService {
|
|
|
347
341
|
systemTitle: row.systemTitle,
|
|
348
342
|
sourceLabel: row.sourceLabel,
|
|
349
343
|
kind: row.kind,
|
|
350
|
-
metadata: RecentActivityMetadataSchema.parse(row.metadata
|
|
344
|
+
metadata: RecentActivityMetadataSchema.parse(row.metadata),
|
|
351
345
|
}
|
|
352
346
|
}
|
|
353
347
|
|
|
@@ -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,197 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
import { getRedisConnection } from '../redis'
|
|
4
|
+
import type { LotaRuntimeBackgroundCursor, LotaRuntimeBackgroundCursorKind } from '../runtime/runtime-extensions'
|
|
5
|
+
|
|
6
|
+
const DEFAULT_SOCIAL_CHAT_HISTORY_PREFIX = 'lota:social:history'
|
|
7
|
+
|
|
8
|
+
const SocialChatMessageRoleSchema = z.enum(['user', 'assistant'])
|
|
9
|
+
const SocialChatSourceSchema = z.literal('social')
|
|
10
|
+
const SocialChatPlatformSchema = z.literal('slack')
|
|
11
|
+
|
|
12
|
+
const SocialChatHistoryMessageSchema = z.object({
|
|
13
|
+
source: SocialChatSourceSchema,
|
|
14
|
+
sourceId: z.string().trim().min(1),
|
|
15
|
+
platform: SocialChatPlatformSchema,
|
|
16
|
+
workspaceId: z.string().trim().min(1),
|
|
17
|
+
channelId: z.string().trim().min(1),
|
|
18
|
+
threadId: z.string().trim().min(1),
|
|
19
|
+
messageId: z.string().trim().min(1),
|
|
20
|
+
role: SocialChatMessageRoleSchema,
|
|
21
|
+
parts: z.array(z.record(z.string(), z.unknown())),
|
|
22
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
23
|
+
cursor: z.object({ createdAt: z.coerce.date(), id: z.string().trim().min(1) }),
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export type SocialChatHistoryMessage = z.infer<typeof SocialChatHistoryMessageSchema>
|
|
27
|
+
|
|
28
|
+
let socialChatHistoryPrefix = DEFAULT_SOCIAL_CHAT_HISTORY_PREFIX
|
|
29
|
+
|
|
30
|
+
function trimConfiguredPrefix(value: string | undefined): string {
|
|
31
|
+
const normalized = value?.trim()
|
|
32
|
+
return normalized && normalized.length > 0 ? normalized : DEFAULT_SOCIAL_CHAT_HISTORY_PREFIX
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function compareCursorOrder(left: LotaRuntimeBackgroundCursor, right: LotaRuntimeBackgroundCursor): number {
|
|
36
|
+
const timeDiff = left.createdAt.getTime() - right.createdAt.getTime()
|
|
37
|
+
if (timeDiff !== 0) return timeDiff
|
|
38
|
+
return left.id.localeCompare(right.id)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class SocialChatHistoryService {
|
|
42
|
+
configure(params?: { keyPrefix?: string }): void {
|
|
43
|
+
socialChatHistoryPrefix = trimConfiguredPrefix(params?.keyPrefix)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private messageStorageKey(message: {
|
|
47
|
+
platform: 'slack'
|
|
48
|
+
workspaceId: string
|
|
49
|
+
threadId: string
|
|
50
|
+
messageId: string
|
|
51
|
+
}): string {
|
|
52
|
+
return `${socialChatHistoryPrefix}:message:${message.platform}:${message.workspaceId}:${message.threadId}:${message.messageId}`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private threadIndexKey(workspaceId: string, threadId: string): string {
|
|
56
|
+
return `${socialChatHistoryPrefix}:thread:${workspaceId}:${threadId}`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private workspaceIndexKey(workspaceId: string): string {
|
|
60
|
+
return `${socialChatHistoryPrefix}:workspace:${workspaceId}`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private cursorKey(kind: LotaRuntimeBackgroundCursorKind, workspaceId: string): string {
|
|
64
|
+
return `${socialChatHistoryPrefix}:cursor:${kind}:${workspaceId}`
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private serializeMessage(message: SocialChatHistoryMessage): string {
|
|
68
|
+
return JSON.stringify({
|
|
69
|
+
...message,
|
|
70
|
+
cursor: { ...message.cursor, createdAt: message.cursor.createdAt.toISOString() },
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private parseStoredMessage(value: string | null): SocialChatHistoryMessage | null {
|
|
75
|
+
if (!value) return null
|
|
76
|
+
try {
|
|
77
|
+
const parsed = SocialChatHistoryMessageSchema.safeParse(JSON.parse(value))
|
|
78
|
+
return parsed.success ? parsed.data : null
|
|
79
|
+
} catch {
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private serializeCursor(cursor: LotaRuntimeBackgroundCursor): string {
|
|
85
|
+
return JSON.stringify({ ...cursor, createdAt: cursor.createdAt.toISOString() })
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private parseCursor(value: string | null): LotaRuntimeBackgroundCursor | null {
|
|
89
|
+
if (!value) return null
|
|
90
|
+
try {
|
|
91
|
+
const parsed = z.object({ createdAt: z.coerce.date(), id: z.string().trim().min(1) }).safeParse(JSON.parse(value))
|
|
92
|
+
return parsed.success ? parsed.data : null
|
|
93
|
+
} catch {
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async upsertMessages(messages: SocialChatHistoryMessage[]): Promise<SocialChatHistoryMessage[]> {
|
|
99
|
+
if (messages.length === 0) return []
|
|
100
|
+
|
|
101
|
+
const redis = getRedisConnection()
|
|
102
|
+
const normalizedMessages = messages.map((message) => SocialChatHistoryMessageSchema.parse(message))
|
|
103
|
+
const multi = redis.multi()
|
|
104
|
+
|
|
105
|
+
for (const message of normalizedMessages) {
|
|
106
|
+
const storageKey = this.messageStorageKey(message)
|
|
107
|
+
const score = message.cursor.createdAt.getTime()
|
|
108
|
+
multi.set(storageKey, this.serializeMessage(message))
|
|
109
|
+
multi.zadd(this.threadIndexKey(message.workspaceId, message.threadId), score, storageKey)
|
|
110
|
+
multi.zadd(this.workspaceIndexKey(message.workspaceId), score, storageKey)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await multi.exec()
|
|
114
|
+
return normalizedMessages
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async listThreadMessages(params: { workspaceId: string; threadId: string }): Promise<SocialChatHistoryMessage[]> {
|
|
118
|
+
const redis = getRedisConnection()
|
|
119
|
+
const storageKeys = await redis.zrange(this.threadIndexKey(params.workspaceId, params.threadId), 0, -1)
|
|
120
|
+
if (storageKeys.length === 0) return []
|
|
121
|
+
|
|
122
|
+
const storedValues = await redis.mget(storageKeys)
|
|
123
|
+
return storedValues
|
|
124
|
+
.map((value) => this.parseStoredMessage(value))
|
|
125
|
+
.filter((message): message is SocialChatHistoryMessage => message !== null)
|
|
126
|
+
.sort((left, right) => compareCursorOrder(left.cursor, right.cursor))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async listWorkspaceMessages(params: {
|
|
130
|
+
workspaceId: string
|
|
131
|
+
cursor: LotaRuntimeBackgroundCursor | null
|
|
132
|
+
onboardingCutoff: Date | null
|
|
133
|
+
}): Promise<SocialChatHistoryMessage[]> {
|
|
134
|
+
const redis = getRedisConnection()
|
|
135
|
+
const indexKey = this.workspaceIndexKey(params.workspaceId)
|
|
136
|
+
const scoreStart =
|
|
137
|
+
params.cursor?.createdAt.getTime() ??
|
|
138
|
+
(params.onboardingCutoff ? params.onboardingCutoff.getTime() : Number.NEGATIVE_INFINITY)
|
|
139
|
+
const storageKeys =
|
|
140
|
+
params.cursor || params.onboardingCutoff
|
|
141
|
+
? await redis.zrangebyscore(indexKey, scoreStart, '+inf')
|
|
142
|
+
: await redis.zrange(indexKey, 0, -1)
|
|
143
|
+
|
|
144
|
+
if (storageKeys.length === 0) return []
|
|
145
|
+
|
|
146
|
+
const storedValues = await redis.mget(storageKeys)
|
|
147
|
+
return storedValues
|
|
148
|
+
.map((value) => this.parseStoredMessage(value))
|
|
149
|
+
.filter((message): message is SocialChatHistoryMessage => message !== null)
|
|
150
|
+
.filter((message) => {
|
|
151
|
+
if (params.cursor) {
|
|
152
|
+
return compareCursorOrder(message.cursor, params.cursor) > 0
|
|
153
|
+
}
|
|
154
|
+
if (params.onboardingCutoff) {
|
|
155
|
+
return message.cursor.createdAt.getTime() > params.onboardingCutoff.getTime()
|
|
156
|
+
}
|
|
157
|
+
return true
|
|
158
|
+
})
|
|
159
|
+
.sort((left, right) => compareCursorOrder(left.cursor, right.cursor))
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async hasWorkspaceMessages(params: {
|
|
163
|
+
workspaceId: string
|
|
164
|
+
cursor: LotaRuntimeBackgroundCursor | null
|
|
165
|
+
onboardingCutoff: Date | null
|
|
166
|
+
}): Promise<boolean> {
|
|
167
|
+
const messages = await this.listWorkspaceMessages({
|
|
168
|
+
workspaceId: params.workspaceId,
|
|
169
|
+
cursor: params.cursor,
|
|
170
|
+
onboardingCutoff: params.onboardingCutoff,
|
|
171
|
+
})
|
|
172
|
+
return messages.length > 0
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async getBackgroundCursor(
|
|
176
|
+
kind: LotaRuntimeBackgroundCursorKind,
|
|
177
|
+
workspaceId: string,
|
|
178
|
+
): Promise<LotaRuntimeBackgroundCursor | null> {
|
|
179
|
+
const redis = getRedisConnection()
|
|
180
|
+
return this.parseCursor(await redis.get(this.cursorKey(kind, workspaceId)))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async setBackgroundCursor(
|
|
184
|
+
kind: LotaRuntimeBackgroundCursorKind,
|
|
185
|
+
workspaceId: string,
|
|
186
|
+
cursor: LotaRuntimeBackgroundCursor,
|
|
187
|
+
): Promise<void> {
|
|
188
|
+
const redis = getRedisConnection()
|
|
189
|
+
await redis.set(this.cursorKey(kind, workspaceId), this.serializeCursor(cursor))
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export const socialChatHistoryService = new SocialChatHistoryService()
|
|
194
|
+
|
|
195
|
+
export function configureSocialChatHistory(params?: { keyPrefix?: string }): void {
|
|
196
|
+
socialChatHistoryService.configure(params)
|
|
197
|
+
}
|