@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.
Files changed (138) 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 +8 -7
  9. package/src/ai/definitions.ts +80 -2
  10. package/src/ai/index.ts +0 -2
  11. package/src/bifrost/bifrost.ts +2 -7
  12. package/src/config/agent-defaults.ts +31 -21
  13. package/src/config/agent-types.ts +11 -0
  14. package/src/config/constants.ts +2 -14
  15. package/src/config/debug-logger.ts +5 -1
  16. package/src/config/index.ts +3 -0
  17. package/src/config/model-constants.ts +16 -34
  18. package/src/config/search.ts +1 -15
  19. package/src/create-runtime.ts +244 -178
  20. package/src/db/cursor-pagination.ts +3 -6
  21. package/src/db/index.ts +2 -0
  22. package/src/db/memory-store.rows.ts +7 -7
  23. package/src/db/memory-store.ts +14 -18
  24. package/src/db/memory.ts +13 -13
  25. package/src/db/service.ts +153 -79
  26. package/src/db/startup.ts +6 -10
  27. package/src/db/surreal-mutation.ts +43 -0
  28. package/src/db/tables.ts +7 -0
  29. package/src/db/workstream-message-row.ts +15 -0
  30. package/src/embeddings/provider.ts +1 -1
  31. package/src/queues/context-compaction.queue.ts +15 -46
  32. package/src/queues/delayed-node-promotion.queue.ts +41 -0
  33. package/src/queues/index.ts +3 -0
  34. package/src/queues/memory-consolidation.queue.ts +16 -51
  35. package/src/queues/plan-scheduler.queue.ts +97 -0
  36. package/src/queues/post-chat-memory.queue.ts +15 -56
  37. package/src/queues/queue-factory.ts +100 -0
  38. package/src/queues/recent-activity-title-refinement.queue.ts +15 -50
  39. package/src/queues/regular-chat-memory-digest.queue.ts +16 -52
  40. package/src/queues/skill-extraction.queue.ts +15 -47
  41. package/src/queues/workstream-title-generation.queue.ts +15 -47
  42. package/src/redis/connection.ts +6 -0
  43. package/src/redis/index.ts +1 -1
  44. package/src/redis/stream-context.ts +11 -0
  45. package/src/runtime/agent-runtime-policy.ts +106 -21
  46. package/src/runtime/approval-continuation.ts +12 -6
  47. package/src/runtime/context-compaction-runtime.ts +1 -1
  48. package/src/runtime/context-compaction.ts +22 -60
  49. package/src/runtime/execution-plan.ts +22 -18
  50. package/src/runtime/graph-designer.ts +15 -0
  51. package/src/runtime/helper-model.ts +9 -197
  52. package/src/runtime/index.ts +2 -0
  53. package/src/runtime/llm-content.ts +1 -1
  54. package/src/runtime/memory-block.ts +9 -11
  55. package/src/runtime/memory-pipeline.ts +6 -9
  56. package/src/runtime/plugin-resolution.ts +35 -0
  57. package/src/runtime/plugin-types.ts +72 -0
  58. package/src/runtime/retrieval-adapters.ts +1 -1
  59. package/src/runtime/runtime-config.ts +25 -12
  60. package/src/runtime/runtime-extensions.ts +2 -2
  61. package/src/runtime/runtime-worker-registry.ts +6 -0
  62. package/src/runtime/team-consultation-orchestrator.ts +45 -28
  63. package/src/runtime/team-consultation-prompts.ts +11 -2
  64. package/src/runtime/title-helpers.ts +2 -4
  65. package/src/runtime/workstream-chat-helpers.ts +1 -1
  66. package/src/services/adaptive-playbook.service.ts +152 -0
  67. package/src/services/agent-executor.service.ts +293 -0
  68. package/src/services/artifact-provenance.service.ts +172 -0
  69. package/src/services/attachment.service.ts +6 -11
  70. package/src/services/context-compaction.service.ts +72 -55
  71. package/src/services/context-enrichment.service.ts +33 -0
  72. package/src/services/coordination-registry.service.ts +117 -0
  73. package/src/services/document-chunk.service.ts +1 -1
  74. package/src/services/domain-agent-executor.service.ts +71 -0
  75. package/src/services/execution-plan.service.ts +269 -50
  76. package/src/services/feedback-loop.service.ts +96 -0
  77. package/src/services/global-orchestrator.service.ts +148 -0
  78. package/src/services/index.ts +26 -0
  79. package/src/services/institutional-memory.service.ts +145 -0
  80. package/src/services/learned-skill.service.ts +24 -5
  81. package/src/services/memory-assessment.service.ts +3 -2
  82. package/src/services/memory-utils.ts +3 -8
  83. package/src/services/memory.service.ts +42 -59
  84. package/src/services/monitoring-window.service.ts +86 -0
  85. package/src/services/mutating-approval.service.ts +1 -1
  86. package/src/services/node-workspace.service.ts +155 -0
  87. package/src/services/notification.service.ts +39 -0
  88. package/src/services/organization-member.service.ts +11 -4
  89. package/src/services/organization.service.ts +5 -5
  90. package/src/services/ownership-dispatcher.service.ts +403 -0
  91. package/src/services/plan-approval.service.ts +1 -1
  92. package/src/services/plan-builder.service.ts +1 -0
  93. package/src/services/plan-checkpoint.service.ts +30 -2
  94. package/src/services/plan-compiler.service.ts +5 -0
  95. package/src/services/plan-coordination.service.ts +152 -0
  96. package/src/services/plan-cycle.service.ts +284 -0
  97. package/src/services/plan-deadline.service.ts +287 -0
  98. package/src/services/plan-executor.service.ts +384 -40
  99. package/src/services/plan-run.service.ts +41 -7
  100. package/src/services/plan-scheduler.service.ts +240 -0
  101. package/src/services/plan-template.service.ts +117 -0
  102. package/src/services/plan-validator.service.ts +84 -2
  103. package/src/services/plan-workspace.service.ts +83 -0
  104. package/src/services/playbook-registry.service.ts +67 -0
  105. package/src/services/plugin-executor.service.ts +103 -0
  106. package/src/services/quality-metrics.service.ts +132 -0
  107. package/src/services/recent-activity.service.ts +27 -31
  108. package/src/services/skill-resolver.service.ts +19 -0
  109. package/src/services/system-executor.service.ts +105 -0
  110. package/src/services/workstream-message.service.ts +12 -34
  111. package/src/services/workstream-plan-registry.service.ts +22 -0
  112. package/src/services/workstream-title.service.ts +3 -1
  113. package/src/services/workstream-turn-preparation.service.ts +34 -66
  114. package/src/services/workstream.service.ts +33 -55
  115. package/src/services/workstream.types.ts +9 -9
  116. package/src/services/write-intent-validator.service.ts +81 -0
  117. package/src/storage/attachment-parser.ts +1 -1
  118. package/src/storage/attachment-utils.ts +1 -1
  119. package/src/storage/generated-document-storage.service.ts +3 -2
  120. package/src/system-agents/delegated-agent-factory.ts +2 -0
  121. package/src/tools/execution-plan.tool.ts +17 -23
  122. package/src/tools/index.ts +0 -1
  123. package/src/tools/team-think.tool.ts +6 -4
  124. package/src/utils/async.ts +2 -1
  125. package/src/utils/date-time.ts +4 -32
  126. package/src/utils/env.ts +8 -0
  127. package/src/utils/errors.ts +42 -10
  128. package/src/utils/index.ts +9 -0
  129. package/src/utils/string.ts +114 -1
  130. package/src/workers/index.ts +1 -0
  131. package/src/workers/regular-chat-memory-digest.runner.ts +2 -2
  132. package/src/workers/skill-extraction.runner.ts +1 -1
  133. package/src/workers/utils/file-section-chunker.ts +2 -1
  134. package/src/workers/utils/repomix-file-sections.ts +2 -2
  135. package/src/workers/utils/sandbox-error.ts +11 -2
  136. package/src/workers/utils/workstream-message-query.ts +11 -20
  137. package/src/workers/worker-utils.ts +2 -2
  138. 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: 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
  }
@@ -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 = clampText(params.title, 80)
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, toTimestamp, withCreatedAtMetadata } from '@lota-sdk/shared'
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 WorkstreamMessageRowSchema = z.object({
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 = toTimestamp(row.createdAt) ?? Date.now()
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: (row.parts ?? []) as ChatMessage['parts'], metadata }
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 await this.listMessages(workstreamId)
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 = new Date(toTimestamp(cursorRow.createdAt) ?? Date.now())
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(toTimestamp(message.metadata?.createdAt) ?? Date.now()).toISOString(),
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()