@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.
- package/infrastructure/schema/00_identity.surql +2 -2
- package/infrastructure/schema/00_thread.surql +75 -0
- package/infrastructure/schema/02_execution_plan.surql +10 -11
- package/infrastructure/schema/10_autonomous_job.surql +3 -3
- package/package.json +2 -2
- package/src/ai/definitions.ts +1 -1
- package/src/config/agent-defaults.ts +5 -5
- package/src/config/index.ts +1 -1
- package/src/config/thread-defaults.ts +72 -0
- package/src/create-runtime.ts +89 -93
- package/src/db/tables.ts +3 -3
- package/src/db/{workstream-message-row.ts → thread-message-row.ts} +3 -3
- package/src/queues/context-compaction.queue.ts +6 -6
- package/src/queues/plan-agent-heartbeat.queue.ts +3 -3
- package/src/queues/post-chat-memory.queue.ts +1 -1
- package/src/queues/title-generation.queue.ts +10 -13
- package/src/redis/index.ts +1 -1
- package/src/redis/stream-context.ts +1 -1
- package/src/runtime/agent-identity-overrides.ts +1 -1
- package/src/runtime/agent-runtime-policy.ts +19 -21
- package/src/runtime/chat-request-routing.ts +1 -1
- package/src/runtime/context-compaction-constants.ts +1 -1
- package/src/runtime/context-compaction.ts +1 -1
- package/src/runtime/execution-plan.ts +1 -1
- package/src/runtime/index.ts +1 -1
- package/src/runtime/memory-digest-policy.ts +1 -1
- package/src/runtime/plugin-types.ts +1 -1
- package/src/runtime/post-turn-side-effects.ts +35 -35
- package/src/runtime/runtime-config.ts +12 -12
- package/src/runtime/runtime-extensions.ts +11 -11
- package/src/runtime/social-chat-agent-runner.ts +3 -3
- package/src/runtime/social-chat-history.ts +1 -1
- package/src/runtime/social-chat.ts +6 -6
- package/src/runtime/team-consultation-orchestrator.ts +1 -1
- package/src/runtime/{workstream-chat-helpers.ts → thread-chat-helpers.ts} +7 -7
- package/src/runtime/{workstream-plan-turn.ts → thread-plan-turn.ts} +11 -17
- package/src/runtime/{workstream-turn-context.ts → thread-turn-context.ts} +10 -10
- package/src/services/agent-activity.service.ts +39 -44
- package/src/services/agent-executor.service.ts +17 -19
- package/src/services/attachment.service.ts +4 -8
- package/src/services/autonomous-job.service.ts +29 -28
- package/src/services/context-compaction.service.ts +19 -29
- package/src/services/execution-plan.service.ts +58 -70
- package/src/services/global-orchestrator.service.ts +5 -5
- package/src/services/index.ts +6 -6
- package/src/services/memory.service.ts +1 -1
- package/src/services/monitoring-window.service.ts +2 -2
- package/src/services/mutating-approval.service.ts +7 -10
- package/src/services/node-workspace.service.ts +8 -7
- package/src/services/notification.service.ts +1 -1
- package/src/services/organization.service.ts +9 -9
- package/src/services/ownership-dispatcher.service.ts +13 -19
- package/src/services/plan-agent-heartbeat.service.ts +13 -13
- package/src/services/plan-agent-query.service.ts +7 -7
- package/src/services/plan-artifact.service.ts +1 -2
- package/src/services/plan-coordination.service.ts +4 -4
- package/src/services/plan-cycle.service.ts +7 -7
- package/src/services/plan-deadline.service.ts +4 -4
- package/src/services/plan-event-delivery.service.ts +8 -12
- package/src/services/plan-executor.service.ts +16 -37
- package/src/services/plan-run-data.ts +27 -8
- package/src/services/plan-run.service.ts +7 -9
- package/src/services/plan-scheduler.service.ts +4 -4
- package/src/services/plan-template.service.ts +2 -2
- package/src/services/plan-validator.service.ts +0 -11
- package/src/services/plugin-executor.service.ts +1 -1
- package/src/services/queue-job.service.ts +1 -1
- package/src/services/recent-activity-title.service.ts +1 -1
- package/src/services/recent-activity.service.ts +4 -4
- package/src/services/system-executor.service.ts +2 -2
- package/src/services/{workstream-message.service.ts → thread-message.service.ts} +72 -76
- package/src/services/thread-plan-registry.service.ts +22 -0
- package/src/services/thread-title.service.ts +39 -0
- package/src/services/{workstream-turn-preparation.service.ts → thread-turn-preparation.service.ts} +131 -143
- package/src/services/{workstream-turn.ts → thread-turn.ts} +27 -31
- package/src/services/thread.service.ts +707 -0
- package/src/services/thread.types.ts +17 -0
- package/src/storage/attachment-storage.service.ts +4 -4
- package/src/system-agents/index.ts +1 -1
- package/src/system-agents/memory.agent.ts +1 -1
- package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -2
- package/src/system-agents/regular-chat-memory-digest.agent.ts +1 -1
- package/src/system-agents/researcher.agent.ts +3 -3
- package/src/system-agents/{workstream-router.agent.ts → thread-router.agent.ts} +21 -21
- package/src/system-agents/title-generator.agent.ts +8 -8
- package/src/tools/execution-plan.tool.ts +39 -40
- package/src/tools/memory-block.tool.ts +4 -4
- package/src/tools/research-topic.tool.ts +1 -0
- package/src/tools/search-web.tool.ts +1 -1
- package/src/tools/search.tool.ts +4 -4
- package/src/tools/team-think.tool.ts +9 -9
- package/src/workers/regular-chat-memory-digest.helpers.ts +1 -1
- package/src/workers/regular-chat-memory-digest.runner.ts +43 -43
- package/src/workers/skill-extraction.runner.ts +9 -13
- package/src/workers/utils/{workstream-message-query.ts → thread-message-query.ts} +21 -21
- package/infrastructure/schema/00_workstream.surql +0 -64
- package/src/config/workstream-defaults.ts +0 -72
- package/src/services/workstream-plan-registry.service.ts +0 -22
- package/src/services/workstream-title.service.ts +0 -42
- package/src/services/workstream.service.ts +0 -803
- package/src/services/workstream.types.ts +0 -17
- /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
|
-
|
|
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:
|
|
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
|
-
...(
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
171
|
+
async listPlanSpecsByThread(threadId: RecordIdInput): Promise<PlanSpecRecord[]> {
|
|
172
172
|
return databaseService.findMany(
|
|
173
173
|
TABLES.PLAN_SPEC,
|
|
174
|
-
{
|
|
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(
|
|
223
|
-
const runs = await this.getActiveRunRecords(
|
|
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(
|
|
227
|
+
async getActiveRunRecords(threadId: RecordIdInput): Promise<PlanRunRecord[]> {
|
|
228
228
|
const runs = await databaseService.findMany(
|
|
229
229
|
TABLES.PLAN_RUN,
|
|
230
|
-
{
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
230
|
+
async listSchedules(threadId: RecordIdInput): Promise<PlanScheduleRecord[]> {
|
|
231
231
|
return databaseService.findMany(
|
|
232
232
|
TABLES.PLAN_SCHEDULE,
|
|
233
|
-
{
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
83
|
-
if (
|
|
84
|
-
return `
|
|
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
|
-
'
|
|
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
|
-
|
|
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 {
|
|
14
|
-
import type {
|
|
13
|
+
import { ThreadMessageRowSchema } from '../db/thread-message-row'
|
|
14
|
+
import type { ThreadMessageRow } from '../db/thread-message-row'
|
|
15
15
|
|
|
16
|
-
const
|
|
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.
|
|
19
|
+
return recordIdToString(value, TABLES.THREAD_MESSAGE)
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
* Builds a collision-free row id by hashing the
|
|
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
|
|
30
|
-
const
|
|
31
|
-
const digest = new Bun.CryptoHasher('sha256').update(`${
|
|
32
|
-
return new RecordId(TABLES.
|
|
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
|
|
36
|
-
return ensureRecordId(
|
|
35
|
+
function toThreadRef(threadId: RecordIdRef): RecordId {
|
|
36
|
+
return ensureRecordId(threadId, TABLES.THREAD)
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
function toChatMessage(row:
|
|
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
|
|
47
|
-
table: TABLES.
|
|
48
|
-
parentFilterField: '
|
|
49
|
-
toRowId:
|
|
50
|
-
parseRow: (row: unknown) =>
|
|
51
|
-
toMessage: (row: unknown) => toChatMessage(
|
|
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
|
|
54
|
-
WHERE
|
|
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
|
|
60
|
-
WHERE
|
|
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
|
|
71
|
-
async upsertMessages(params: {
|
|
72
|
-
const
|
|
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
|
|
84
|
+
throw new Error(`Refusing to persist thread message "${messageId}" with empty parts`)
|
|
85
85
|
}
|
|
86
|
-
const rowId =
|
|
86
|
+
const rowId = toThreadMessageRowId(threadId, messageId)
|
|
87
87
|
const existingRow = await databaseService.findOne(
|
|
88
|
-
TABLES.
|
|
89
|
-
{
|
|
90
|
-
|
|
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.
|
|
97
|
+
TABLES.THREAD_MESSAGE,
|
|
98
98
|
rowId,
|
|
99
99
|
{
|
|
100
|
-
|
|
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
|
-
|
|
107
|
+
ThreadMessageRowSchema,
|
|
108
108
|
{ mutation: 'content' },
|
|
109
109
|
)
|
|
110
110
|
})
|
|
111
111
|
await Promise.all(upsertPromises)
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
async listMessages(
|
|
115
|
-
const
|
|
114
|
+
async listMessages(threadId: RecordIdRef): Promise<ChatMessage[]> {
|
|
115
|
+
const threadRef = toThreadRef(threadId)
|
|
116
116
|
const rows = await databaseService.query<unknown>(surql`
|
|
117
|
-
SELECT * FROM
|
|
118
|
-
WHERE
|
|
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) =>
|
|
122
|
+
return rows.map((row) => ThreadMessageRowSchema.parse(row)).map((row) => toChatMessage(row))
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
async listMessageHistoryPage(params: {
|
|
126
|
-
|
|
126
|
+
threadId: RecordIdRef
|
|
127
127
|
take: number
|
|
128
128
|
beforeMessageId?: string
|
|
129
129
|
}): Promise<MessageHistoryPage> {
|
|
130
|
-
const
|
|
131
|
-
return listMessageHistoryPage(
|
|
132
|
-
parentId:
|
|
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(
|
|
139
|
-
const
|
|
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(
|
|
142
|
+
return this.listMessages(threadRef)
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
const cursorRow = await databaseService.findOne(
|
|
146
|
-
TABLES.
|
|
147
|
-
{
|
|
146
|
+
TABLES.THREAD_MESSAGE,
|
|
147
|
+
{ threadId: threadRef, messageId: cursorMessageId },
|
|
148
148
|
CursorRowSchema,
|
|
149
149
|
)
|
|
150
150
|
|
|
151
151
|
if (!cursorRow) {
|
|
152
|
-
throw new Error(`
|
|
152
|
+
throw new Error(`Thread cursor message not found: ${cursorMessageId}`)
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
const cursorCreatedAt = cursorRow.createdAt
|
|
156
|
-
const cursorId =
|
|
156
|
+
const cursorId = toThreadMessageRowId(threadRef, cursorMessageId)
|
|
157
157
|
const rows = await databaseService.query<unknown>(surql`
|
|
158
|
-
SELECT * FROM
|
|
159
|
-
WHERE
|
|
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) =>
|
|
167
|
+
return rows.map((row) => ThreadMessageRowSchema.parse(row)).map((row) => toChatMessage(row))
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
async listRecentMessages(
|
|
171
|
-
const
|
|
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
|
|
174
|
-
WHERE
|
|
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) =>
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
212
|
+
threadId: RecordIdRef
|
|
213
213
|
content: string
|
|
214
214
|
}): Promise<ChatMessage> {
|
|
215
|
-
const
|
|
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({
|
|
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
|
-
|
|
229
|
+
threadId: RecordIdRef
|
|
230
230
|
parts: ChatMessage['parts']
|
|
231
231
|
metadata?: ChatMessage['metadata']
|
|
232
232
|
}): Promise<ChatMessage> {
|
|
233
|
-
const
|
|
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({
|
|
241
|
+
await this.upsertMessages({ threadId: threadRef, messages: [message] })
|
|
242
242
|
return message
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
-
async ensureBootstrapWelcomeMessage(params: {
|
|
246
|
-
|
|
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.
|
|
253
|
-
{
|
|
254
|
-
|
|
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
|
-
|
|
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
|
|
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()
|