@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.
Files changed (174) hide show
  1. package/infrastructure/schema/00_identity.surql +0 -2
  2. package/infrastructure/schema/01_memory.surql +1 -1
  3. package/infrastructure/schema/02_execution_plan.surql +62 -1
  4. package/infrastructure/schema/03_learned_skill.surql +1 -1
  5. package/infrastructure/schema/06_playbook.surql +25 -0
  6. package/infrastructure/schema/07_institutional_memory.surql +13 -0
  7. package/infrastructure/schema/08_quality_metrics.surql +17 -0
  8. package/package.json +9 -8
  9. package/src/ai/definitions.ts +80 -2
  10. package/src/ai/embedding-cache.ts +7 -6
  11. package/src/ai/index.ts +0 -1
  12. package/src/bifrost/bifrost.ts +14 -14
  13. package/src/config/agent-defaults.ts +32 -22
  14. package/src/config/agent-types.ts +11 -0
  15. package/src/config/constants.ts +2 -14
  16. package/src/config/debug-logger.ts +5 -1
  17. package/src/config/index.ts +3 -0
  18. package/src/config/logger.ts +7 -9
  19. package/src/config/model-constants.ts +16 -34
  20. package/src/config/search.ts +1 -15
  21. package/src/create-runtime.ts +453 -0
  22. package/src/db/cursor-pagination.ts +3 -6
  23. package/src/db/index.ts +2 -0
  24. package/src/db/memory-store.rows.ts +7 -7
  25. package/src/db/memory-store.ts +24 -24
  26. package/src/db/memory.ts +18 -16
  27. package/src/db/schema-fingerprint.ts +1 -0
  28. package/src/db/service.ts +193 -122
  29. package/src/db/startup.ts +9 -13
  30. package/src/db/surreal-mutation.ts +43 -0
  31. package/src/db/tables.ts +7 -0
  32. package/src/db/workstream-message-row.ts +15 -0
  33. package/src/embeddings/provider.ts +1 -1
  34. package/src/index.ts +1 -1
  35. package/src/queues/context-compaction.queue.ts +17 -52
  36. package/src/queues/delayed-node-promotion.queue.ts +41 -0
  37. package/src/queues/document-processor.queue.ts +7 -7
  38. package/src/queues/index.ts +3 -0
  39. package/src/queues/memory-consolidation.queue.ts +18 -54
  40. package/src/queues/plan-scheduler.queue.ts +97 -0
  41. package/src/queues/post-chat-memory.queue.ts +15 -60
  42. package/src/queues/queue-factory.ts +100 -0
  43. package/src/queues/recent-activity-title-refinement.queue.ts +15 -54
  44. package/src/queues/regular-chat-memory-digest.queue.ts +16 -55
  45. package/src/queues/skill-extraction.queue.ts +15 -50
  46. package/src/queues/workstream-title-generation.queue.ts +15 -51
  47. package/src/redis/connection.ts +12 -3
  48. package/src/redis/index.ts +2 -1
  49. package/src/redis/org-memory-lock.ts +1 -1
  50. package/src/redis/redis-lease-lock.ts +41 -8
  51. package/src/redis/stream-context.ts +11 -0
  52. package/src/runtime/agent-runtime-policy.ts +106 -21
  53. package/src/runtime/agent-stream-helpers.ts +2 -1
  54. package/src/runtime/approval-continuation.ts +12 -6
  55. package/src/runtime/context-compaction-constants.ts +1 -1
  56. package/src/runtime/context-compaction-runtime.ts +7 -5
  57. package/src/runtime/context-compaction.ts +40 -97
  58. package/src/runtime/execution-plan.ts +23 -19
  59. package/src/runtime/graph-designer.ts +15 -0
  60. package/src/runtime/helper-model.ts +10 -196
  61. package/src/runtime/index.ts +14 -1
  62. package/src/runtime/llm-content.ts +1 -1
  63. package/src/runtime/memory-block.ts +11 -12
  64. package/src/runtime/memory-pipeline.ts +26 -10
  65. package/src/runtime/plugin-resolution.ts +35 -0
  66. package/src/runtime/plugin-types.ts +73 -1
  67. package/src/runtime/retrieval-adapters.ts +1 -1
  68. package/src/runtime/runtime-config.ts +25 -12
  69. package/src/runtime/runtime-extensions.ts +91 -15
  70. package/src/runtime/runtime-worker-registry.ts +6 -0
  71. package/src/runtime/team-consultation-orchestrator.ts +45 -28
  72. package/src/runtime/team-consultation-prompts.ts +11 -2
  73. package/src/runtime/title-helpers.ts +11 -4
  74. package/src/runtime/workstream-chat-helpers.ts +6 -7
  75. package/src/runtime/workstream-routing-policy.ts +0 -30
  76. package/src/runtime/workstream-state.ts +17 -7
  77. package/src/services/adaptive-playbook.service.ts +152 -0
  78. package/src/services/agent-executor.service.ts +293 -0
  79. package/src/services/artifact-provenance.service.ts +172 -0
  80. package/src/services/attachment.service.ts +7 -12
  81. package/src/services/context-compaction.service.ts +75 -58
  82. package/src/services/context-enrichment.service.ts +33 -0
  83. package/src/services/coordination-registry.service.ts +117 -0
  84. package/src/services/document-chunk.service.ts +38 -33
  85. package/src/services/domain-agent-executor.service.ts +71 -0
  86. package/src/services/execution-plan.service.ts +271 -50
  87. package/src/services/feedback-loop.service.ts +96 -0
  88. package/src/services/global-orchestrator.service.ts +148 -0
  89. package/src/services/index.ts +26 -0
  90. package/src/services/institutional-memory.service.ts +145 -0
  91. package/src/services/learned-skill.service.ts +30 -15
  92. package/src/services/memory-assessment.service.ts +3 -2
  93. package/src/services/{memory.utils.ts → memory-utils.ts} +4 -13
  94. package/src/services/memory.service.ts +55 -69
  95. package/src/services/monitoring-window.service.ts +86 -0
  96. package/src/services/mutating-approval.service.ts +1 -1
  97. package/src/services/node-workspace.service.ts +155 -0
  98. package/src/services/notification.service.ts +39 -0
  99. package/src/services/organization-member.service.ts +12 -5
  100. package/src/services/organization.service.ts +5 -5
  101. package/src/services/ownership-dispatcher.service.ts +403 -0
  102. package/src/services/plan-approval.service.ts +1 -1
  103. package/src/services/plan-artifact.service.ts +1 -0
  104. package/src/services/plan-builder.service.ts +1 -0
  105. package/src/services/plan-checkpoint.service.ts +30 -2
  106. package/src/services/plan-compiler.service.ts +5 -0
  107. package/src/services/plan-coordination.service.ts +152 -0
  108. package/src/services/plan-cycle.service.ts +284 -0
  109. package/src/services/plan-deadline.service.ts +287 -0
  110. package/src/services/plan-executor.service.ts +386 -58
  111. package/src/services/plan-helpers.ts +15 -0
  112. package/src/services/plan-run.service.ts +41 -7
  113. package/src/services/plan-scheduler.service.ts +240 -0
  114. package/src/services/plan-template.service.ts +117 -0
  115. package/src/services/plan-validator.service.ts +87 -20
  116. package/src/services/plan-workspace.service.ts +83 -0
  117. package/src/services/playbook-registry.service.ts +67 -0
  118. package/src/services/plugin-executor.service.ts +103 -0
  119. package/src/services/quality-metrics.service.ts +132 -0
  120. package/src/services/recent-activity-title.service.ts +3 -10
  121. package/src/services/recent-activity.service.ts +33 -43
  122. package/src/services/skill-resolver.service.ts +19 -0
  123. package/src/services/system-executor.service.ts +105 -0
  124. package/src/services/workstream-message.service.ts +29 -41
  125. package/src/services/workstream-plan-registry.service.ts +22 -0
  126. package/src/services/workstream-title.service.ts +3 -9
  127. package/src/services/{workstream-turn-preparation.ts → workstream-turn-preparation.service.ts} +428 -373
  128. package/src/services/workstream-turn.ts +2 -2
  129. package/src/services/workstream.service.ts +55 -65
  130. package/src/services/workstream.types.ts +10 -19
  131. package/src/services/write-intent-validator.service.ts +81 -0
  132. package/src/storage/attachment-parser.ts +1 -1
  133. package/src/storage/attachment-storage.service.ts +4 -4
  134. package/src/storage/{attachments.utils.ts → attachment-utils.ts} +2 -5
  135. package/src/storage/generated-document-storage.service.ts +3 -2
  136. package/src/storage/index.ts +2 -2
  137. package/src/system-agents/{context-compacter.agent.ts → context-compaction.agent.ts} +4 -4
  138. package/src/system-agents/delegated-agent-factory.ts +5 -2
  139. package/src/system-agents/index.ts +8 -0
  140. package/src/system-agents/memory-reranker.agent.ts +1 -1
  141. package/src/system-agents/memory.agent.ts +1 -1
  142. package/src/system-agents/recent-activity-title-refiner.agent.ts +1 -1
  143. package/src/tools/execution-plan.tool.ts +17 -19
  144. package/src/tools/fetch-webpage.tool.ts +20 -18
  145. package/src/tools/index.ts +2 -3
  146. package/src/tools/read-file-parts.tool.ts +1 -1
  147. package/src/tools/search-web.tool.ts +18 -15
  148. package/src/tools/{search-tools.ts → search.tool.ts} +1 -1
  149. package/src/tools/team-think.tool.ts +14 -8
  150. package/src/tools/{tool-contract.ts → tool-contracts.ts} +9 -2
  151. package/src/utils/async.ts +3 -2
  152. package/src/utils/date-time.ts +4 -32
  153. package/src/utils/env.ts +8 -0
  154. package/src/utils/errors.ts +47 -0
  155. package/src/utils/hono-error-handler.ts +1 -2
  156. package/src/utils/index.ts +19 -2
  157. package/src/utils/string.ts +128 -1
  158. package/src/workers/bootstrap.ts +2 -2
  159. package/src/workers/index.ts +1 -0
  160. package/src/workers/memory-consolidation.worker.ts +12 -12
  161. package/src/workers/regular-chat-memory-digest.helpers.ts +2 -7
  162. package/src/workers/regular-chat-memory-digest.runner.ts +11 -105
  163. package/src/workers/skill-extraction.runner.ts +8 -102
  164. package/src/workers/utils/file-section-chunker.ts +6 -3
  165. package/src/workers/utils/repomix-file-sections.ts +2 -2
  166. package/src/workers/utils/sandbox-error.ts +11 -2
  167. package/src/workers/utils/workstream-message-query.ts +97 -0
  168. package/src/workers/worker-utils.ts +6 -2
  169. package/src/runtime/retrieval-pipeline.ts +0 -3
  170. package/src/runtime.ts +0 -387
  171. package/src/tools/log-hello-world.tool.ts +0 -17
  172. package/src/utils/error.ts +0 -10
  173. /package/src/services/{context-compaction-runtime.ts → context-compaction-runtime.singleton.ts} +0 -0
  174. /package/src/storage/{attachments.types.ts → attachment-types.ts} +0 -0
@@ -0,0 +1,83 @@
1
+ import { z } from 'zod'
2
+
3
+ import { getRedisConnection } from '../redis'
4
+
5
+ const PlanWorkspaceEntrySchema = z.object({
6
+ value: z.unknown(),
7
+ version: z.number(),
8
+ writeSequence: z.number(),
9
+ nodeId: z.string(),
10
+ timestamp: z.number(),
11
+ })
12
+
13
+ export type PlanWorkspaceEntry = z.infer<typeof PlanWorkspaceEntrySchema>
14
+
15
+ class PlanWorkspaceService {
16
+ private keyPrefix = 'plan-workspace:'
17
+
18
+ async write(params: {
19
+ runId: string
20
+ nodeId: string
21
+ key: string
22
+ value: unknown
23
+ version: number
24
+ checkpointSequence: number
25
+ }): Promise<void> {
26
+ const redis = getRedisConnection()
27
+ const hashKey = `${this.keyPrefix}${params.runId}`
28
+ const fieldKey = `${params.nodeId}:${params.key}`
29
+ const entry: PlanWorkspaceEntry = {
30
+ value: params.value,
31
+ version: params.version,
32
+ writeSequence: params.checkpointSequence,
33
+ nodeId: params.nodeId,
34
+ timestamp: Date.now(),
35
+ }
36
+ await redis.hset(hashKey, fieldKey, JSON.stringify(entry))
37
+ }
38
+
39
+ async snapshotRead(params: {
40
+ runId: string
41
+ readerNodeId: string
42
+ snapshotSequence?: number
43
+ }): Promise<Record<string, PlanWorkspaceEntry>> {
44
+ const redis = getRedisConnection()
45
+ const hashKey = `${this.keyPrefix}${params.runId}`
46
+ const nodePrefix = `${params.readerNodeId}:`
47
+ const all = await redis.hgetall(hashKey)
48
+ const result: Record<string, PlanWorkspaceEntry> = {}
49
+ for (const [fieldKey, raw] of Object.entries(all)) {
50
+ if (!fieldKey.startsWith(nodePrefix)) continue
51
+ const entry = PlanWorkspaceEntrySchema.parse(JSON.parse(raw))
52
+ if (params.snapshotSequence !== undefined && entry.writeSequence > params.snapshotSequence) {
53
+ continue
54
+ }
55
+ result[fieldKey] = entry
56
+ }
57
+ return result
58
+ }
59
+
60
+ async currentRead(params: { runId: string; key?: string }): Promise<Record<string, PlanWorkspaceEntry>> {
61
+ const redis = getRedisConnection()
62
+ const hashKey = `${this.keyPrefix}${params.runId}`
63
+ if (params.key) {
64
+ // Support both raw field keys (e.g. 'nodeId:key') and plain keys
65
+ const raw = await redis.hget(hashKey, params.key)
66
+ if (!raw) return {}
67
+ return { [params.key]: PlanWorkspaceEntrySchema.parse(JSON.parse(raw)) }
68
+ }
69
+ const all = await redis.hgetall(hashKey)
70
+ const result: Record<string, PlanWorkspaceEntry> = {}
71
+ for (const [k, v] of Object.entries(all)) {
72
+ result[k] = PlanWorkspaceEntrySchema.parse(JSON.parse(v))
73
+ }
74
+ return result
75
+ }
76
+
77
+ async cleanup(runId: string): Promise<void> {
78
+ const redis = getRedisConnection()
79
+ await redis.del(`${this.keyPrefix}${runId}`)
80
+ }
81
+ }
82
+
83
+ export const planWorkspaceService = new PlanWorkspaceService()
@@ -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,20 +1,13 @@
1
1
  import { createHelperModelRuntime } from '../runtime/helper-model'
2
+ import { normalizeTitle } from '../runtime/title-helpers'
2
3
  import {
3
4
  createRecentActivityTitleRefinerAgent,
4
- recentActivityTitleRefinerPrompt,
5
+ RECENT_ACTIVITY_TITLE_REFINER_PROMPT,
5
6
  } from '../system-agents/recent-activity-title-refiner.agent'
6
- import { compactWhitespace } from '../utils/string'
7
7
  import { recentActivityService } from './recent-activity.service'
8
8
 
9
9
  const RECENT_ACTIVITY_TITLE_TIMEOUT_MS = 60_000
10
10
 
11
- function normalizeTitle(value: string): string {
12
- const normalized = compactWhitespace(value)
13
- .replace(/^["'`]+|["'`]+$/g, '')
14
- .replace(/[.!?,;:]+$/g, '')
15
- return normalized.length <= 80 ? normalized : normalized.slice(0, 80).trim()
16
- }
17
-
18
11
  function buildRefinementPromptInput(
19
12
  candidate: Awaited<ReturnType<typeof recentActivityService.getRefinementCandidate>>,
20
13
  ) {
@@ -52,7 +45,7 @@ class RecentActivityTitleService {
52
45
  await this.helperRuntime.generateHelperText({
53
46
  tag: 'recent-activity-title-refinement',
54
47
  createAgent: createRecentActivityTitleRefinerAgent,
55
- defaultSystemPrompt: recentActivityTitleRefinerPrompt,
48
+ defaultSystemPrompt: RECENT_ACTIVITY_TITLE_REFINER_PROMPT,
56
49
  timeoutMs: RECENT_ACTIVITY_TITLE_TIMEOUT_MS,
57
50
  messages: [{ role: 'user', content: promptInput }],
58
51
  }),
@@ -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: z.unknown(),
30
- organizationId: z.unknown(),
31
- userId: z.unknown(),
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.union([z.string(), z.date(), z.number()]),
43
- createdAt: z.union([z.string(), z.date(), z.number()]),
43
+ occurredAt: z.coerce.date(),
44
+ createdAt: z.coerce.date(),
44
45
  })
45
46
 
46
47
  const RecentActivityRowSchema = z.object({
47
- id: z.unknown(),
48
- organizationId: z.unknown(),
49
- userId: z.unknown(),
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: z.unknown().optional(),
61
+ latestEventId: recordIdSchema.optional(),
61
62
  latestSourceEventId: z.string().optional(),
62
- latestEventAt: z.union([z.string(), z.date(), z.number()]),
63
- titleRefinedAt: z.union([z.string(), z.date(), z.number()]).optional(),
64
- createdAt: z.union([z.string(), z.date(), z.number()]),
65
- updatedAt: z.union([z.string(), z.date(), z.number()]),
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: clampText(input.sourceEventId, 200),
153
- ...(input.targetId ? { targetId: clampText(input.targetId, 200) } : {}),
154
- mergeKey: clampText(input.mergeKey, 200),
155
- title: clampText(input.title, 140),
156
- sourceLabel: clampText(input.sourceLabel, 40),
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
  }
@@ -166,18 +162,12 @@ class RecentActivityService {
166
162
  }): Promise<RecentActivity[]> {
167
163
  await databaseService.connect()
168
164
 
169
- const items: RecentActivity[] = []
170
- for (const candidate of params.events) {
171
- const recorded = await this.recordEvent({
172
- orgId: params.orgId,
173
- userId: params.userId,
174
- source: params.source,
175
- event: candidate,
176
- })
177
- items.push(recorded.item)
178
- }
179
-
180
- return items
165
+ const results = await Promise.all(
166
+ params.events.map((candidate) =>
167
+ this.recordEvent({ orgId: params.orgId, userId: params.userId, source: params.source, event: candidate }),
168
+ ),
169
+ )
170
+ return results.map((r) => r.item)
181
171
  }
182
172
 
183
173
  async recordEvent(params: {
@@ -315,7 +305,7 @@ class RecentActivityService {
315
305
  const existing = await databaseService.findOne(TABLES.RECENT_ACTIVITY, { id: activityRef }, RecentActivityRowSchema)
316
306
  if (!existing) return null
317
307
 
318
- const nextTitle = clampText(params.title, 80)
308
+ const nextTitle = truncateText(params.title, 80)
319
309
  if (!nextTitle) return this.toPublicItem(existing)
320
310
  if (compactWhitespace(nextTitle).toLowerCase() === compactWhitespace(existing.title).toLowerCase()) {
321
311
  return this.toPublicItem(existing)
@@ -353,7 +343,7 @@ class RecentActivityService {
353
343
  systemTitle: row.systemTitle,
354
344
  sourceLabel: row.sourceLabel,
355
345
  kind: row.kind,
356
- metadata: RecentActivityMetadataSchema.parse(row.metadata ?? {}),
346
+ metadata: RecentActivityMetadataSchema.parse(row.metadata),
357
347
  }
358
348
  }
359
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()