@lota-sdk/core 0.2.3 → 0.3.0

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 (102) hide show
  1. package/infrastructure/schema/00_identity.surql +2 -2
  2. package/infrastructure/schema/00_thread.surql +75 -0
  3. package/infrastructure/schema/02_execution_plan.surql +10 -11
  4. package/infrastructure/schema/10_autonomous_job.surql +3 -3
  5. package/package.json +2 -2
  6. package/src/ai/definitions.ts +1 -1
  7. package/src/config/agent-defaults.ts +5 -5
  8. package/src/config/index.ts +1 -1
  9. package/src/config/thread-defaults.ts +72 -0
  10. package/src/create-runtime.ts +89 -93
  11. package/src/db/tables.ts +3 -3
  12. package/src/db/{workstream-message-row.ts → thread-message-row.ts} +3 -3
  13. package/src/queues/context-compaction.queue.ts +6 -6
  14. package/src/queues/plan-agent-heartbeat.queue.ts +3 -3
  15. package/src/queues/post-chat-memory.queue.ts +1 -1
  16. package/src/queues/title-generation.queue.ts +10 -13
  17. package/src/redis/index.ts +1 -1
  18. package/src/redis/stream-context.ts +1 -1
  19. package/src/runtime/agent-identity-overrides.ts +1 -1
  20. package/src/runtime/agent-runtime-policy.ts +19 -21
  21. package/src/runtime/chat-request-routing.ts +1 -1
  22. package/src/runtime/context-compaction-constants.ts +1 -1
  23. package/src/runtime/context-compaction.ts +1 -1
  24. package/src/runtime/execution-plan.ts +1 -1
  25. package/src/runtime/index.ts +1 -1
  26. package/src/runtime/memory-digest-policy.ts +1 -1
  27. package/src/runtime/plugin-types.ts +1 -1
  28. package/src/runtime/post-turn-side-effects.ts +35 -35
  29. package/src/runtime/runtime-config.ts +12 -12
  30. package/src/runtime/runtime-extensions.ts +11 -11
  31. package/src/runtime/social-chat-agent-runner.ts +3 -3
  32. package/src/runtime/social-chat-history.ts +1 -1
  33. package/src/runtime/social-chat.ts +6 -6
  34. package/src/runtime/team-consultation-orchestrator.ts +1 -1
  35. package/src/runtime/{workstream-chat-helpers.ts → thread-chat-helpers.ts} +7 -7
  36. package/src/runtime/{workstream-plan-turn.ts → thread-plan-turn.ts} +11 -17
  37. package/src/runtime/{workstream-turn-context.ts → thread-turn-context.ts} +10 -10
  38. package/src/services/agent-activity.service.ts +39 -44
  39. package/src/services/agent-executor.service.ts +17 -19
  40. package/src/services/attachment.service.ts +4 -8
  41. package/src/services/autonomous-job.service.ts +29 -28
  42. package/src/services/context-compaction.service.ts +19 -29
  43. package/src/services/execution-plan.service.ts +58 -70
  44. package/src/services/global-orchestrator.service.ts +5 -5
  45. package/src/services/index.ts +6 -6
  46. package/src/services/memory.service.ts +1 -1
  47. package/src/services/monitoring-window.service.ts +2 -2
  48. package/src/services/mutating-approval.service.ts +7 -10
  49. package/src/services/node-workspace.service.ts +8 -7
  50. package/src/services/notification.service.ts +1 -1
  51. package/src/services/organization.service.ts +9 -9
  52. package/src/services/ownership-dispatcher.service.ts +13 -19
  53. package/src/services/plan-agent-heartbeat.service.ts +13 -13
  54. package/src/services/plan-agent-query.service.ts +7 -7
  55. package/src/services/plan-artifact.service.ts +1 -2
  56. package/src/services/plan-coordination.service.ts +4 -4
  57. package/src/services/plan-cycle.service.ts +7 -7
  58. package/src/services/plan-deadline.service.ts +4 -4
  59. package/src/services/plan-event-delivery.service.ts +8 -12
  60. package/src/services/plan-executor.service.ts +16 -37
  61. package/src/services/plan-run-data.ts +27 -8
  62. package/src/services/plan-run.service.ts +7 -9
  63. package/src/services/plan-scheduler.service.ts +4 -4
  64. package/src/services/plan-template.service.ts +2 -2
  65. package/src/services/plan-validator.service.ts +0 -11
  66. package/src/services/plugin-executor.service.ts +1 -1
  67. package/src/services/queue-job.service.ts +1 -1
  68. package/src/services/recent-activity-title.service.ts +1 -1
  69. package/src/services/recent-activity.service.ts +4 -4
  70. package/src/services/system-executor.service.ts +2 -2
  71. package/src/services/{workstream-message.service.ts → thread-message.service.ts} +72 -76
  72. package/src/services/thread-plan-registry.service.ts +22 -0
  73. package/src/services/thread-title.service.ts +39 -0
  74. package/src/services/{workstream-turn-preparation.service.ts → thread-turn-preparation.service.ts} +131 -143
  75. package/src/services/{workstream-turn.ts → thread-turn.ts} +27 -31
  76. package/src/services/thread.service.ts +707 -0
  77. package/src/services/thread.types.ts +17 -0
  78. package/src/storage/attachment-storage.service.ts +4 -4
  79. package/src/system-agents/index.ts +1 -1
  80. package/src/system-agents/memory.agent.ts +1 -1
  81. package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -2
  82. package/src/system-agents/regular-chat-memory-digest.agent.ts +1 -1
  83. package/src/system-agents/researcher.agent.ts +3 -3
  84. package/src/system-agents/{workstream-router.agent.ts → thread-router.agent.ts} +21 -21
  85. package/src/system-agents/title-generator.agent.ts +8 -8
  86. package/src/tools/execution-plan.tool.ts +39 -40
  87. package/src/tools/memory-block.tool.ts +4 -4
  88. package/src/tools/research-topic.tool.ts +1 -0
  89. package/src/tools/search-web.tool.ts +1 -1
  90. package/src/tools/search.tool.ts +4 -4
  91. package/src/tools/team-think.tool.ts +9 -9
  92. package/src/workers/regular-chat-memory-digest.helpers.ts +1 -1
  93. package/src/workers/regular-chat-memory-digest.runner.ts +43 -43
  94. package/src/workers/skill-extraction.runner.ts +9 -13
  95. package/src/workers/utils/{workstream-message-query.ts → thread-message-query.ts} +21 -21
  96. package/infrastructure/schema/00_workstream.surql +0 -64
  97. package/src/config/workstream-defaults.ts +0 -72
  98. package/src/services/workstream-plan-registry.service.ts +0 -22
  99. package/src/services/workstream-title.service.ts +0 -42
  100. package/src/services/workstream.service.ts +0 -803
  101. package/src/services/workstream.types.ts +0 -17
  102. /package/src/services/{workstream-constants.ts → thread-constants.ts} +0 -0
@@ -1,4 +1,4 @@
1
- import type { ExecutionPlanToolResultData, PlanRunRecord } from '@lota-sdk/shared'
1
+ import type { ExecutionPlanToolResultData, PlanRunRecord, SerializableExecutionPlan } from '@lota-sdk/shared'
2
2
 
3
3
  import type { RecordIdInput } from '../db/record-id'
4
4
  import { ensureRecordId } from '../db/record-id'
@@ -21,7 +21,7 @@ export function toRunData(run: PlanRunRecord, patch: PlanRunUpdate) {
21
21
  return {
22
22
  planSpecId: ensureRecordId(run.planSpecId, TABLES.PLAN_SPEC),
23
23
  organizationId: ensureRecordId(run.organizationId, TABLES.ORGANIZATION),
24
- workstreamId: ensureRecordId(run.workstreamId, TABLES.WORKSTREAM),
24
+ threadId: ensureRecordId(run.threadId, TABLES.THREAD),
25
25
  leadAgentId: patch.leadAgentId ?? run.leadAgentId,
26
26
  status: patch.status ?? run.status,
27
27
  ...(patch.currentNodeId === null
@@ -71,18 +71,37 @@ export function toRunData(run: PlanRunRecord, patch: PlanRunUpdate) {
71
71
  }
72
72
  }
73
73
 
74
+ function toSlimPlanSummary(plan: SerializableExecutionPlan): NonNullable<ExecutionPlanToolResultData['plan']> {
75
+ const completed = plan.progress.completed + plan.progress.partial
76
+ return {
77
+ runId: plan.runId,
78
+ title: plan.title,
79
+ objective: plan.objective,
80
+ status: plan.status,
81
+ progress: { completed, total: plan.progress.total },
82
+ nodes: plan.nodes.map((node) => ({
83
+ id: node.id,
84
+ label: node.label,
85
+ type: node.type,
86
+ status: node.status,
87
+ ownerRef: node.owner.ref,
88
+ })),
89
+ activeNodeIds: plan.activeNodeIds,
90
+ readyNodeIds: plan.readyNodeIds,
91
+ }
92
+ }
93
+
74
94
  export function buildExecutionPlanToolResult(params: {
75
95
  action: ExecutionPlanToolResultData['action']
76
- plan: ExecutionPlanToolResultData['plan'] | null
96
+ plan: SerializableExecutionPlan | null
77
97
  message: string
78
- changedNodeId?: string
79
98
  }): ExecutionPlanToolResultData {
99
+ const slim = params.plan ? toSlimPlanSummary(params.plan) : null
80
100
  return {
81
101
  action: params.action,
82
102
  message: params.message,
83
- ...(params.changedNodeId ? { changedNodeId: params.changedNodeId } : {}),
84
- ...(params.plan ? { plan: params.plan } : {}),
85
- hasPlan: params.plan !== null,
86
- status: params.plan?.status,
103
+ ...(slim ? { plan: slim } : {}),
104
+ hasPlan: slim !== null,
105
+ status: slim?.status,
87
106
  }
88
107
  }
@@ -168,10 +168,10 @@ class PlanRunService {
168
168
  return spec
169
169
  }
170
170
 
171
- async listPlanSpecsByWorkstream(workstreamId: RecordIdInput): Promise<PlanSpecRecord[]> {
171
+ async listPlanSpecsByThread(threadId: RecordIdInput): Promise<PlanSpecRecord[]> {
172
172
  return databaseService.findMany(
173
173
  TABLES.PLAN_SPEC,
174
- { workstreamId: ensureRecordId(workstreamId, TABLES.WORKSTREAM) },
174
+ { threadId: ensureRecordId(threadId, TABLES.THREAD) },
175
175
  PlanSpecSchema,
176
176
  { orderBy: 'createdAt', orderDir: 'DESC' },
177
177
  )
@@ -219,15 +219,15 @@ class PlanRunService {
219
219
  return run
220
220
  }
221
221
 
222
- async getActiveRunRecord(workstreamId: RecordIdInput): Promise<PlanRunRecord | null> {
223
- const runs = await this.getActiveRunRecords(workstreamId)
222
+ async getActiveRunRecord(threadId: RecordIdInput): Promise<PlanRunRecord | null> {
223
+ const runs = await this.getActiveRunRecords(threadId)
224
224
  return runs[0] ?? null
225
225
  }
226
226
 
227
- async getActiveRunRecords(workstreamId: RecordIdInput): Promise<PlanRunRecord[]> {
227
+ async getActiveRunRecords(threadId: RecordIdInput): Promise<PlanRunRecord[]> {
228
228
  const runs = await databaseService.findMany(
229
229
  TABLES.PLAN_RUN,
230
- { workstreamId: ensureRecordId(workstreamId, TABLES.WORKSTREAM) },
230
+ { threadId: ensureRecordId(threadId, TABLES.THREAD) },
231
231
  PlanRunSchema,
232
232
  { orderBy: 'updatedAt', orderDir: 'DESC' },
233
233
  )
@@ -379,7 +379,6 @@ class PlanRunService {
379
379
  status: nodeRun.status,
380
380
  upstreamNodeIds: [...nodeSpec.upstreamNodeIds],
381
381
  downstreamNodeIds: [...nodeSpec.downstreamNodeIds],
382
- ...(nodeRun.handoffContext ? { handoffContext: nodeRun.handoffContext } : {}),
383
382
  ...(nodeRun.completedAt ? { completedAt: toOptionalIsoDateTimeString(nodeRun.completedAt) } : {}),
384
383
  } as SerializablePlanNode
385
384
  }
@@ -418,7 +417,6 @@ class PlanRunService {
418
417
  resolvedInput: nodeRun.resolvedInput,
419
418
  latestStructuredOutput: nodeRun.latestStructuredOutput,
420
419
  latestNotes: nodeRun.latestNotes,
421
- handoffContext: nodeRun.handoffContext,
422
420
  blockedReason: nodeRun.blockedReason,
423
421
  failureClass: nodeRun.failureClass,
424
422
  upstreamNodeIds: [...nodeSpec.upstreamNodeIds],
@@ -432,7 +430,7 @@ class PlanRunService {
432
430
  return {
433
431
  specId: recordIdToString(spec.id, TABLES.PLAN_SPEC),
434
432
  runId: recordIdToString(run.id, TABLES.PLAN_RUN),
435
- workstreamId: recordIdToString(run.workstreamId, TABLES.WORKSTREAM),
433
+ threadId: recordIdToString(run.threadId, TABLES.THREAD),
436
434
  organizationId: recordIdToString(run.organizationId, TABLES.ORGANIZATION),
437
435
  title: spec.title,
438
436
  objective: spec.objective,
@@ -41,7 +41,7 @@ class PlanSchedulerService {
41
41
 
42
42
  async createSchedule(params: {
43
43
  organizationId: RecordIdInput
44
- workstreamId: RecordIdInput
44
+ threadId: RecordIdInput
45
45
  planSpecId?: RecordIdInput
46
46
  runId?: RecordIdInput
47
47
  nodeId?: string
@@ -54,7 +54,7 @@ class PlanSchedulerService {
54
54
  TABLES.PLAN_SCHEDULE,
55
55
  {
56
56
  organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
57
- workstreamId: ensureRecordId(params.workstreamId, TABLES.WORKSTREAM),
57
+ threadId: ensureRecordId(params.threadId, TABLES.THREAD),
58
58
  planSpecId: params.planSpecId ? ensureRecordId(params.planSpecId, TABLES.PLAN_SPEC) : undefined,
59
59
  runId: params.runId ? ensureRecordId(params.runId, TABLES.PLAN_RUN) : undefined,
60
60
  nodeId: params.nodeId,
@@ -227,10 +227,10 @@ class PlanSchedulerService {
227
227
  }
228
228
  }
229
229
 
230
- async listSchedules(workstreamId: RecordIdInput): Promise<PlanScheduleRecord[]> {
230
+ async listSchedules(threadId: RecordIdInput): Promise<PlanScheduleRecord[]> {
231
231
  return databaseService.findMany(
232
232
  TABLES.PLAN_SCHEDULE,
233
- { workstreamId: ensureRecordId(workstreamId, TABLES.WORKSTREAM) },
233
+ { threadId: ensureRecordId(threadId, TABLES.THREAD) },
234
234
  PlanScheduleRecordSchema,
235
235
  { orderBy: 'createdAt', orderDir: 'ASC' },
236
236
  )
@@ -87,7 +87,7 @@ class PlanTemplateService {
87
87
  async instantiate(params: {
88
88
  templateId: RecordIdInput
89
89
  organizationId: RecordIdInput
90
- workstreamId: RecordIdInput
90
+ threadId: RecordIdInput
91
91
  leadAgentId: string
92
92
  overrides?: Partial<PlanDraft>
93
93
  carryForwardArtifacts?: PlanArtifactRecord[]
@@ -106,7 +106,7 @@ class PlanTemplateService {
106
106
 
107
107
  return executionPlanService.createPlan({
108
108
  organizationId: params.organizationId,
109
- workstreamId: params.workstreamId,
109
+ threadId: params.threadId,
110
110
  leadAgentId: params.leadAgentId,
111
111
  input: draft,
112
112
  })
@@ -582,16 +582,6 @@ class PlanValidatorService {
582
582
  }
583
583
 
584
584
  if (deliverable.schemaRef) {
585
- if (artifact.schemaRef !== deliverable.schemaRef) {
586
- blocking.push(
587
- createIssue({
588
- code: 'artifact_schema_mismatch',
589
- message: `Artifact "${deliverable.name}" must declare schemaRef "${deliverable.schemaRef}".`,
590
- nodeId: params.node.id,
591
- }),
592
- )
593
- }
594
-
595
585
  const artifactSchema = resolveSchemaRef(params.draft, deliverable.schemaRef)
596
586
  if (!artifact.payload) {
597
587
  blocking.push(
@@ -658,7 +648,6 @@ class PlanValidatorService {
658
648
  if (check.type === 'schema') {
659
649
  const schemaRef =
660
650
  (typeof check.config.schemaRef === 'string' ? check.config.schemaRef : undefined) ??
661
- artifact?.schemaRef ??
662
651
  (artifactName ? node.deliverables.find((candidate) => candidate.name === artifactName)?.schemaRef : undefined)
663
652
  if (!schemaRef) {
664
653
  return createIssue({
@@ -20,7 +20,7 @@ function buildPluginExecutionParams(params: {
20
20
  inputs: params.resolvedInput,
21
21
  context: {
22
22
  organizationId: params.context.organizationId,
23
- workstreamId: params.context.workstreamId,
23
+ threadId: params.context.threadId,
24
24
  planId: params.context.planId,
25
25
  nodeId: params.context.nodeId,
26
26
  ...(params.context.userId ? { userId: params.context.userId } : {}),
@@ -141,7 +141,7 @@ function extractJobContext(data: unknown): Record<string, unknown> | undefined {
141
141
 
142
142
  const context = compactRecord({
143
143
  organizationId: readStringField(record, 'organizationId') ?? readStringField(record, 'orgId'),
144
- workstreamId: readStringField(record, 'workstreamId'),
144
+ threadId: readStringField(record, 'threadId'),
145
145
  userId: readStringField(record, 'userId'),
146
146
  agentId: readStringField(record, 'agentId'),
147
147
  sourceId: readStringField(record, 'sourceId'),
@@ -18,7 +18,7 @@ function buildRefinementPromptInput(
18
18
  `sourceLabel=${candidate.sourceLabel}`,
19
19
  `systemTitle=${candidate.systemTitle}`,
20
20
  metadata.agentName ? `agentName=${metadata.agentName}` : null,
21
- metadata.workstreamTitle ? `workstreamTitle=${metadata.workstreamTitle}` : null,
21
+ metadata.threadTitle ? `threadTitle=${metadata.threadTitle}` : null,
22
22
  metadata.userMessageText ? `userMessage=${metadata.userMessageText}` : null,
23
23
  metadata.assistantSummary ? `assistantSummary=${metadata.assistantSummary}` : null,
24
24
  ].filter((line): line is string => Boolean(line))
@@ -79,9 +79,9 @@ function shouldKeepExistingAgentTitle(existing: RecentActivityRow | null): boole
79
79
  function buildRecentActivityAreaKey(
80
80
  row: Pick<RecentActivityRow, 'targetKind' | 'targetId' | 'kind' | 'mergeKey' | 'metadata'>,
81
81
  ): string {
82
- const workstreamId = row.metadata?.workstreamId
83
- if (workstreamId) {
84
- return `workstream:${compactWhitespace(workstreamId)}`
82
+ const threadId = row.metadata?.threadId
83
+ if (threadId) {
84
+ return `thread:${compactWhitespace(threadId)}`
85
85
  }
86
86
 
87
87
  if (row.targetId) {
@@ -373,7 +373,7 @@ class RecentActivityService {
373
373
  'chat',
374
374
  'agent task',
375
375
  'recent activity',
376
- 'workstream update',
376
+ 'thread update',
377
377
  ])
378
378
 
379
379
  return !bannedTitles.has(normalizedCandidate)
@@ -8,7 +8,7 @@ const BUILT_IN_SYSTEM_EXECUTORS = Object.freeze({
8
8
  'plan-runtime': {
9
9
  supportedOperations: ['echo-input'],
10
10
  async executeNode(params: PluginNodeExecutionParams): Promise<PlanNodeResult> {
11
- return { structuredOutput: structuredClone(params.inputs), artifacts: [] }
11
+ return { notes: 'System echo-input completed.', structuredOutput: structuredClone(params.inputs), artifacts: [] }
12
12
  },
13
13
  } satisfies SystemNodeExecutor,
14
14
  })
@@ -36,7 +36,7 @@ function buildSystemExecutionParams(params: {
36
36
  inputs: params.resolvedInput,
37
37
  context: {
38
38
  organizationId: params.context.organizationId,
39
- workstreamId: params.context.workstreamId,
39
+ threadId: params.context.threadId,
40
40
  planId: params.context.planId,
41
41
  nodeId: params.context.nodeId,
42
42
  ...(params.context.userId ? { userId: params.context.userId } : {}),
@@ -10,54 +10,54 @@ import { ensureRecordId, recordIdToString } from '../db/record-id'
10
10
  import type { RecordIdRef } from '../db/record-id'
11
11
  import { databaseService } from '../db/service'
12
12
  import { TABLES } from '../db/tables'
13
- import { WorkstreamMessageRowSchema } from '../db/workstream-message-row'
14
- import type { WorkstreamMessageRow } from '../db/workstream-message-row'
13
+ import { ThreadMessageRowSchema } from '../db/thread-message-row'
14
+ import type { ThreadMessageRow } from '../db/thread-message-row'
15
15
 
16
- const WorkstreamMessageExistingRowSchema = z.object({ id: recordIdSchema, createdAt: z.coerce.date() })
16
+ const ThreadMessageExistingRowSchema = z.object({ id: recordIdSchema, createdAt: z.coerce.date() })
17
17
 
18
18
  function toMessageId(value: string | RecordIdRef): string {
19
- return recordIdToString(value, TABLES.WORKSTREAM_MESSAGE)
19
+ return recordIdToString(value, TABLES.THREAD_MESSAGE)
20
20
  }
21
21
 
22
22
  /**
23
- * Builds a collision-free row id by hashing the workstream + message id pair.
23
+ * Builds a collision-free row id by hashing the thread + message id pair.
24
24
  * Previous implementation replaced non-alphanumeric chars with '_', which was
25
25
  * lossy (e.g. "msg:foo" and "msg_foo" mapped to the same row id).
26
26
  * Now uses a 32-char SHA-256 hex prefix -- short enough for ergonomic ids,
27
27
  * long enough (128 bits) to make collisions negligible.
28
28
  */
29
- function toWorkstreamMessageRowId(workstreamId: RecordIdRef, messageId: string): RecordId {
30
- const workstreamStr = recordIdToString(workstreamId, TABLES.WORKSTREAM)
31
- const digest = new Bun.CryptoHasher('sha256').update(`${workstreamStr}\0${messageId}`).digest('hex').slice(0, 32)
32
- return new RecordId(TABLES.WORKSTREAM_MESSAGE, digest)
29
+ function toThreadMessageRowId(threadId: RecordIdRef, messageId: string): RecordId {
30
+ const threadStr = recordIdToString(threadId, TABLES.THREAD)
31
+ const digest = new Bun.CryptoHasher('sha256').update(`${threadStr}\0${messageId}`).digest('hex').slice(0, 32)
32
+ return new RecordId(TABLES.THREAD_MESSAGE, digest)
33
33
  }
34
34
 
35
- function toWorkstreamRef(workstreamId: RecordIdRef): RecordId {
36
- return ensureRecordId(workstreamId, TABLES.WORKSTREAM)
35
+ function toThreadRef(threadId: RecordIdRef): RecordId {
36
+ return ensureRecordId(threadId, TABLES.THREAD)
37
37
  }
38
38
 
39
- function toChatMessage(row: WorkstreamMessageRow): ChatMessage {
39
+ function toChatMessage(row: ThreadMessageRow): ChatMessage {
40
40
  const rowCreatedAt = requireTimestamp(row.createdAt)
41
41
  const metadata = withCreatedAtMetadata(parseRowMetadata(row.metadata), rowCreatedAt)
42
42
 
43
43
  return { id: row.messageId, role: row.role, parts: row.parts as ChatMessage['parts'], metadata }
44
44
  }
45
45
 
46
- const workstreamPaginationConfig: CursorPaginationConfig = {
47
- table: TABLES.WORKSTREAM_MESSAGE,
48
- parentFilterField: 'workstreamId',
49
- toRowId: toWorkstreamMessageRowId,
50
- parseRow: (row: unknown) => WorkstreamMessageRowSchema.parse(row),
51
- toMessage: (row: unknown) => toChatMessage(WorkstreamMessageRowSchema.parse(row)),
46
+ const threadPaginationConfig: CursorPaginationConfig = {
47
+ table: TABLES.THREAD_MESSAGE,
48
+ parentFilterField: 'threadId',
49
+ toRowId: toThreadMessageRowId,
50
+ parseRow: (row: unknown) => ThreadMessageRowSchema.parse(row),
51
+ toMessage: (row: unknown) => toChatMessage(ThreadMessageRowSchema.parse(row)),
52
52
  queryLatest: (parentId, limit) => surql`
53
- SELECT * FROM workstreamMessage
54
- WHERE workstreamId = ${parentId}
53
+ SELECT * FROM threadMessage
54
+ WHERE threadId = ${parentId}
55
55
  ORDER BY createdAt DESC, id DESC
56
56
  LIMIT ${limit}
57
57
  `,
58
58
  queryBefore: (parentId, cursorCreatedAt, cursorId, limit) => surql`
59
- SELECT * FROM workstreamMessage
60
- WHERE workstreamId = ${parentId}
59
+ SELECT * FROM threadMessage
60
+ WHERE threadId = ${parentId}
61
61
  AND (
62
62
  createdAt < ${cursorCreatedAt}
63
63
  OR (createdAt = ${cursorCreatedAt} AND id < ${cursorId})
@@ -67,9 +67,9 @@ const workstreamPaginationConfig: CursorPaginationConfig = {
67
67
  `,
68
68
  }
69
69
 
70
- class WorkstreamMessageService {
71
- async upsertMessages(params: { workstreamId: RecordIdRef; messages: ChatMessage[] }): Promise<void> {
72
- const workstreamId = toWorkstreamRef(params.workstreamId)
70
+ class ThreadMessageService {
71
+ async upsertMessages(params: { threadId: RecordIdRef; messages: ChatMessage[] }): Promise<void> {
72
+ const threadId = toThreadRef(params.threadId)
73
73
 
74
74
  const upsertPromises = params.messages.map(async (message) => {
75
75
  const messageId = message.id.trim()
@@ -81,82 +81,82 @@ class WorkstreamMessageService {
81
81
  : []
82
82
  if (parts.length === 0) {
83
83
  if (role === 'assistant') return
84
- throw new Error(`Refusing to persist workstream message "${messageId}" with empty parts`)
84
+ throw new Error(`Refusing to persist thread message "${messageId}" with empty parts`)
85
85
  }
86
- const rowId = toWorkstreamMessageRowId(workstreamId, messageId)
86
+ const rowId = toThreadMessageRowId(threadId, messageId)
87
87
  const existingRow = await databaseService.findOne(
88
- TABLES.WORKSTREAM_MESSAGE,
89
- { workstreamId, messageId },
90
- WorkstreamMessageExistingRowSchema,
88
+ TABLES.THREAD_MESSAGE,
89
+ { threadId, messageId },
90
+ ThreadMessageExistingRowSchema,
91
91
  )
92
92
  const persistedCreatedAt =
93
93
  existingRow === null ? requireTimestamp(message.metadata?.createdAt) : requireTimestamp(existingRow.createdAt)
94
94
  const metadata = withCreatedAtMetadata({ ...message.metadata, createdAt: persistedCreatedAt })
95
95
 
96
96
  await databaseService.upsert(
97
- TABLES.WORKSTREAM_MESSAGE,
97
+ TABLES.THREAD_MESSAGE,
98
98
  rowId,
99
99
  {
100
- workstreamId,
100
+ threadId,
101
101
  messageId,
102
102
  role,
103
103
  parts,
104
104
  metadata,
105
105
  createdAt: existingRow ? existingRow.createdAt : new Date(persistedCreatedAt),
106
106
  },
107
- WorkstreamMessageRowSchema,
107
+ ThreadMessageRowSchema,
108
108
  { mutation: 'content' },
109
109
  )
110
110
  })
111
111
  await Promise.all(upsertPromises)
112
112
  }
113
113
 
114
- async listMessages(workstreamId: RecordIdRef): Promise<ChatMessage[]> {
115
- const workstreamRef = toWorkstreamRef(workstreamId)
114
+ async listMessages(threadId: RecordIdRef): Promise<ChatMessage[]> {
115
+ const threadRef = toThreadRef(threadId)
116
116
  const rows = await databaseService.query<unknown>(surql`
117
- SELECT * FROM workstreamMessage
118
- WHERE workstreamId = ${workstreamRef}
117
+ SELECT * FROM threadMessage
118
+ WHERE threadId = ${threadRef}
119
119
  ORDER BY createdAt ASC, id ASC
120
120
  `)
121
121
 
122
- return rows.map((row) => WorkstreamMessageRowSchema.parse(row)).map((row) => toChatMessage(row))
122
+ return rows.map((row) => ThreadMessageRowSchema.parse(row)).map((row) => toChatMessage(row))
123
123
  }
124
124
 
125
125
  async listMessageHistoryPage(params: {
126
- workstreamId: RecordIdRef
126
+ threadId: RecordIdRef
127
127
  take: number
128
128
  beforeMessageId?: string
129
129
  }): Promise<MessageHistoryPage> {
130
- const workstreamRef = toWorkstreamRef(params.workstreamId)
131
- return listMessageHistoryPage(workstreamPaginationConfig, {
132
- parentId: workstreamRef,
130
+ const threadRef = toThreadRef(params.threadId)
131
+ return listMessageHistoryPage(threadPaginationConfig, {
132
+ parentId: threadRef,
133
133
  take: params.take,
134
134
  beforeMessageId: params.beforeMessageId,
135
135
  })
136
136
  }
137
137
 
138
- async listMessagesAfterCursor(workstreamId: RecordIdRef, afterMessageId?: string): Promise<ChatMessage[]> {
139
- const workstreamRef = toWorkstreamRef(workstreamId)
138
+ async listMessagesAfterCursor(threadId: RecordIdRef, afterMessageId?: string): Promise<ChatMessage[]> {
139
+ const threadRef = toThreadRef(threadId)
140
140
  const cursorMessageId = afterMessageId?.trim()
141
141
  if (!cursorMessageId) {
142
- return this.listMessages(workstreamRef)
142
+ return this.listMessages(threadRef)
143
143
  }
144
144
 
145
145
  const cursorRow = await databaseService.findOne(
146
- TABLES.WORKSTREAM_MESSAGE,
147
- { workstreamId: workstreamRef, messageId: cursorMessageId },
146
+ TABLES.THREAD_MESSAGE,
147
+ { threadId: threadRef, messageId: cursorMessageId },
148
148
  CursorRowSchema,
149
149
  )
150
150
 
151
151
  if (!cursorRow) {
152
- throw new Error(`Workstream cursor message not found: ${cursorMessageId}`)
152
+ throw new Error(`Thread cursor message not found: ${cursorMessageId}`)
153
153
  }
154
154
 
155
155
  const cursorCreatedAt = cursorRow.createdAt
156
- const cursorId = toWorkstreamMessageRowId(workstreamRef, cursorMessageId)
156
+ const cursorId = toThreadMessageRowId(threadRef, cursorMessageId)
157
157
  const rows = await databaseService.query<unknown>(surql`
158
- SELECT * FROM workstreamMessage
159
- WHERE workstreamId = ${workstreamRef}
158
+ SELECT * FROM threadMessage
159
+ WHERE threadId = ${threadRef}
160
160
  AND (
161
161
  createdAt > ${cursorCreatedAt}
162
162
  OR (createdAt = ${cursorCreatedAt} AND id > ${cursorId})
@@ -164,26 +164,26 @@ class WorkstreamMessageService {
164
164
  ORDER BY createdAt ASC, id ASC
165
165
  `)
166
166
 
167
- return rows.map((row) => WorkstreamMessageRowSchema.parse(row)).map((row) => toChatMessage(row))
167
+ return rows.map((row) => ThreadMessageRowSchema.parse(row)).map((row) => toChatMessage(row))
168
168
  }
169
169
 
170
- async listRecentMessages(workstreamId: RecordIdRef, limit: number): Promise<ChatMessage[]> {
171
- const workstreamRef = toWorkstreamRef(workstreamId)
170
+ async listRecentMessages(threadId: RecordIdRef, limit: number): Promise<ChatMessage[]> {
171
+ const threadRef = toThreadRef(threadId)
172
172
  const rows = await databaseService.query<unknown>(surql`
173
- SELECT * FROM workstreamMessage
174
- WHERE workstreamId = ${workstreamRef}
173
+ SELECT * FROM threadMessage
174
+ WHERE threadId = ${threadRef}
175
175
  ORDER BY createdAt DESC, id DESC
176
176
  LIMIT ${Math.max(1, limit)}
177
177
  `)
178
178
 
179
179
  return rows
180
- .map((row) => WorkstreamMessageRowSchema.parse(row))
180
+ .map((row) => ThreadMessageRowSchema.parse(row))
181
181
  .reverse()
182
182
  .map((row) => toChatMessage(row))
183
183
  }
184
184
 
185
185
  async searchMessages(params: {
186
- workstreamId: RecordIdRef
186
+ threadId: RecordIdRef
187
187
  role: 'user' | 'assistant'
188
188
  query: string
189
189
  limit: number
@@ -191,7 +191,7 @@ class WorkstreamMessageService {
191
191
  const normalizedQuery = params.query.trim().toLowerCase()
192
192
  if (!normalizedQuery) return []
193
193
 
194
- const messages = await this.listMessages(toWorkstreamRef(params.workstreamId))
194
+ const messages = await this.listMessages(toThreadRef(params.threadId))
195
195
  return messages
196
196
  .filter((message) => message.role === params.role)
197
197
  .map((message) => ({
@@ -209,10 +209,10 @@ class WorkstreamMessageService {
209
209
 
210
210
  async addUserMessage(params: {
211
211
  messageId: RecordIdRef
212
- workstreamId: RecordIdRef
212
+ threadId: RecordIdRef
213
213
  content: string
214
214
  }): Promise<ChatMessage> {
215
- const workstreamRef = toWorkstreamRef(params.workstreamId)
215
+ const threadRef = toThreadRef(params.threadId)
216
216
  const message: ChatMessage = {
217
217
  id: toMessageId(params.messageId),
218
218
  role: 'user',
@@ -220,17 +220,17 @@ class WorkstreamMessageService {
220
220
  metadata: { createdAt: Date.now() },
221
221
  }
222
222
 
223
- await this.upsertMessages({ workstreamId: workstreamRef, messages: [message] })
223
+ await this.upsertMessages({ threadId: threadRef, messages: [message] })
224
224
  return message
225
225
  }
226
226
 
227
227
  async addAgentMessage(params: {
228
228
  messageId: RecordIdRef
229
- workstreamId: RecordIdRef
229
+ threadId: RecordIdRef
230
230
  parts: ChatMessage['parts']
231
231
  metadata?: ChatMessage['metadata']
232
232
  }): Promise<ChatMessage> {
233
- const workstreamRef = toWorkstreamRef(params.workstreamId)
233
+ const threadRef = toThreadRef(params.threadId)
234
234
  const message: ChatMessage = {
235
235
  id: toMessageId(params.messageId),
236
236
  role: 'assistant',
@@ -238,20 +238,16 @@ class WorkstreamMessageService {
238
238
  metadata: withCreatedAtMetadata(params.metadata, Date.now()),
239
239
  }
240
240
 
241
- await this.upsertMessages({ workstreamId: workstreamRef, messages: [message] })
241
+ await this.upsertMessages({ threadId: threadRef, messages: [message] })
242
242
  return message
243
243
  }
244
244
 
245
- async ensureBootstrapWelcomeMessage(params: {
246
- workstreamId: RecordIdRef
247
- agentId: string
248
- text: string
249
- }): Promise<void> {
250
- const workstreamRef = toWorkstreamRef(params.workstreamId)
245
+ async ensureBootstrapWelcomeMessage(params: { threadId: RecordIdRef; agentId: string; text: string }): Promise<void> {
246
+ const threadRef = toThreadRef(params.threadId)
251
247
  const existingRow = await databaseService.findOne(
252
- TABLES.WORKSTREAM_MESSAGE,
253
- { workstreamId: workstreamRef },
254
- WorkstreamMessageExistingRowSchema,
248
+ TABLES.THREAD_MESSAGE,
249
+ { threadId: threadRef },
250
+ ThreadMessageExistingRowSchema,
255
251
  )
256
252
  if (existingRow) return
257
253
 
@@ -259,7 +255,7 @@ class WorkstreamMessageService {
259
255
  if (!messageText) return
260
256
 
261
257
  await this.upsertMessages({
262
- workstreamId: workstreamRef,
258
+ threadId: threadRef,
263
259
  messages: [
264
260
  {
265
261
  id: Bun.randomUUIDv7(),
@@ -276,4 +272,4 @@ class WorkstreamMessageService {
276
272
  }
277
273
  }
278
274
 
279
- export const workstreamMessageService = new WorkstreamMessageService()
275
+ export const threadMessageService = new ThreadMessageService()
@@ -0,0 +1,22 @@
1
+ import type { PlanRunRecord, SerializableExecutionPlan } from '@lota-sdk/shared'
2
+
3
+ import type { RecordIdInput } from '../db/record-id'
4
+ import { planRunService } from './plan-run.service'
5
+
6
+ class ThreadPlanRegistryService {
7
+ async listActiveRuns(threadId: RecordIdInput): Promise<PlanRunRecord[]> {
8
+ return planRunService.getActiveRunRecords(threadId)
9
+ }
10
+
11
+ async countActiveRuns(threadId: RecordIdInput): Promise<number> {
12
+ const runs = await this.listActiveRuns(threadId)
13
+ return runs.length
14
+ }
15
+
16
+ async listActivePlans(threadId: RecordIdInput): Promise<SerializableExecutionPlan[]> {
17
+ const runs = await this.listActiveRuns(threadId)
18
+ return Promise.all(runs.map((run) => planRunService.toSerializablePlan(run)))
19
+ }
20
+ }
21
+
22
+ export const threadPlanRegistryService = new ThreadPlanRegistryService()
@@ -0,0 +1,39 @@
1
+ import { THREAD } from '@lota-sdk/shared'
2
+
3
+ import { chatLogger } from '../config/logger'
4
+ import type { RecordIdRef } from '../db/record-id'
5
+ import { createHelperModelRuntime } from '../runtime/helper-model'
6
+ import { deriveTitle, limitTitleWords, normalizeTitle } from '../runtime/title-helpers'
7
+ import { createThreadTitleGeneratorAgent, THREAD_TITLE_GENERATOR_PROMPT } from '../system-agents/title-generator.agent'
8
+ import { threadService } from './thread.service'
9
+
10
+ const THREAD_TITLE_TIMEOUT_MS = 30_000
11
+
12
+ class ThreadTitleService {
13
+ helperRuntime = createHelperModelRuntime()
14
+
15
+ async generateAndPersistTitle(threadId: RecordIdRef, sourceText: string): Promise<void> {
16
+ let title = ''
17
+ try {
18
+ title = normalizeTitle(
19
+ await this.helperRuntime.generateHelperText({
20
+ tag: 'thread-title',
21
+ createAgent: createThreadTitleGeneratorAgent,
22
+ defaultSystemPrompt: THREAD_TITLE_GENERATOR_PROMPT,
23
+ timeoutMs: THREAD_TITLE_TIMEOUT_MS,
24
+ messages: [{ role: 'user', content: sourceText }],
25
+ }),
26
+ )
27
+ } catch (error) {
28
+ chatLogger.warn`Failed to generate thread title via LLM (non-fatal): ${error}`
29
+ }
30
+
31
+ if (!title) {
32
+ title = limitTitleWords(deriveTitle(sourceText || THREAD.DEFAULT_TITLE))
33
+ }
34
+
35
+ await threadService.update(threadId, { title, nameGenerated: true })
36
+ }
37
+ }
38
+
39
+ export const threadTitleService = new ThreadTitleService()