@lota-sdk/core 0.2.3 → 0.3.1

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 (106) hide show
  1. package/infrastructure/schema/00_identity.surql +2 -2
  2. package/infrastructure/schema/00_thread.surql +73 -0
  3. package/infrastructure/schema/02_execution_plan.surql +10 -11
  4. package/infrastructure/schema/04_runtime_bootstrap.surql +1 -0
  5. package/infrastructure/schema/10_autonomous_job.surql +3 -3
  6. package/package.json +2 -2
  7. package/src/ai/definitions.ts +1 -1
  8. package/src/config/agent-defaults.ts +5 -5
  9. package/src/config/index.ts +1 -1
  10. package/src/config/thread-defaults.ts +72 -0
  11. package/src/create-runtime.ts +90 -94
  12. package/src/db/record-id.ts +21 -21
  13. package/src/db/service.ts +44 -40
  14. package/src/db/tables.ts +3 -3
  15. package/src/db/{workstream-message-row.ts → thread-message-row.ts} +3 -3
  16. package/src/queues/context-compaction.queue.ts +6 -6
  17. package/src/queues/plan-agent-heartbeat.queue.ts +3 -3
  18. package/src/queues/post-chat-memory.queue.ts +1 -1
  19. package/src/queues/title-generation.queue.ts +10 -13
  20. package/src/redis/index.ts +1 -1
  21. package/src/redis/stream-context.ts +1 -1
  22. package/src/runtime/agent-identity-overrides.ts +1 -1
  23. package/src/runtime/agent-runtime-policy.ts +19 -21
  24. package/src/runtime/chat-request-routing.ts +1 -1
  25. package/src/runtime/context-compaction-constants.ts +1 -1
  26. package/src/runtime/context-compaction.ts +1 -1
  27. package/src/runtime/execution-plan.ts +1 -1
  28. package/src/runtime/index.ts +1 -1
  29. package/src/runtime/memory-digest-policy.ts +1 -1
  30. package/src/runtime/plugin-types.ts +1 -1
  31. package/src/runtime/post-turn-side-effects.ts +35 -35
  32. package/src/runtime/runtime-config.ts +24 -21
  33. package/src/runtime/runtime-extensions.ts +11 -11
  34. package/src/runtime/social-chat-agent-runner.ts +3 -3
  35. package/src/runtime/social-chat-history.ts +1 -1
  36. package/src/runtime/social-chat.ts +6 -6
  37. package/src/runtime/team-consultation-orchestrator.ts +1 -1
  38. package/src/runtime/{workstream-chat-helpers.ts → thread-chat-helpers.ts} +7 -7
  39. package/src/runtime/{workstream-plan-turn.ts → thread-plan-turn.ts} +11 -17
  40. package/src/runtime/{workstream-turn-context.ts → thread-turn-context.ts} +10 -10
  41. package/src/services/agent-activity.service.ts +39 -44
  42. package/src/services/agent-executor.service.ts +17 -19
  43. package/src/services/attachment.service.ts +4 -8
  44. package/src/services/autonomous-job.service.ts +29 -28
  45. package/src/services/context-compaction.service.ts +19 -29
  46. package/src/services/execution-plan.service.ts +58 -70
  47. package/src/services/global-orchestrator.service.ts +5 -5
  48. package/src/services/index.ts +6 -6
  49. package/src/services/memory.service.ts +1 -1
  50. package/src/services/monitoring-window.service.ts +2 -2
  51. package/src/services/mutating-approval.service.ts +7 -10
  52. package/src/services/node-workspace.service.ts +8 -7
  53. package/src/services/notification.service.ts +1 -1
  54. package/src/services/organization.service.ts +9 -9
  55. package/src/services/ownership-dispatcher.service.ts +13 -19
  56. package/src/services/plan-agent-heartbeat.service.ts +13 -13
  57. package/src/services/plan-agent-query.service.ts +7 -7
  58. package/src/services/plan-artifact.service.ts +1 -2
  59. package/src/services/plan-coordination.service.ts +4 -4
  60. package/src/services/plan-cycle.service.ts +7 -7
  61. package/src/services/plan-deadline.service.ts +4 -4
  62. package/src/services/plan-event-delivery.service.ts +8 -12
  63. package/src/services/plan-executor.service.ts +25 -39
  64. package/src/services/plan-run-data.ts +27 -8
  65. package/src/services/plan-run.service.ts +7 -9
  66. package/src/services/plan-scheduler.service.ts +4 -4
  67. package/src/services/plan-template.service.ts +2 -2
  68. package/src/services/plan-validator.service.ts +0 -11
  69. package/src/services/plugin-executor.service.ts +1 -1
  70. package/src/services/queue-job.service.ts +1 -1
  71. package/src/services/recent-activity-title.service.ts +1 -1
  72. package/src/services/recent-activity.service.ts +4 -4
  73. package/src/services/system-executor.service.ts +2 -2
  74. package/src/services/{workstream-message.service.ts → thread-message.service.ts} +72 -76
  75. package/src/services/thread-plan-registry.service.ts +22 -0
  76. package/src/services/thread-title.service.ts +39 -0
  77. package/src/services/{workstream-turn-preparation.service.ts → thread-turn-preparation.service.ts} +148 -171
  78. package/src/services/{workstream-turn.ts → thread-turn.ts} +27 -31
  79. package/src/services/thread.service.ts +853 -0
  80. package/src/services/thread.types.ts +17 -0
  81. package/src/storage/attachment-storage.service.ts +4 -4
  82. package/src/system-agents/index.ts +1 -1
  83. package/src/system-agents/memory.agent.ts +1 -1
  84. package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -2
  85. package/src/system-agents/regular-chat-memory-digest.agent.ts +1 -1
  86. package/src/system-agents/researcher.agent.ts +3 -3
  87. package/src/system-agents/{workstream-router.agent.ts → thread-router.agent.ts} +68 -135
  88. package/src/system-agents/title-generator.agent.ts +8 -8
  89. package/src/tools/execution-plan.tool.ts +39 -40
  90. package/src/tools/memory-block.tool.ts +4 -4
  91. package/src/tools/research-topic.tool.ts +1 -0
  92. package/src/tools/search-web.tool.ts +1 -1
  93. package/src/tools/search.tool.ts +4 -4
  94. package/src/tools/team-think.tool.ts +9 -9
  95. package/src/utils/async.ts +6 -7
  96. package/src/workers/regular-chat-memory-digest.helpers.ts +1 -1
  97. package/src/workers/regular-chat-memory-digest.runner.ts +43 -43
  98. package/src/workers/skill-extraction.runner.ts +9 -13
  99. package/src/workers/utils/{workstream-message-query.ts → thread-message-query.ts} +21 -21
  100. package/infrastructure/schema/00_workstream.surql +0 -64
  101. package/src/config/workstream-defaults.ts +0 -72
  102. package/src/services/workstream-plan-registry.service.ts +0 -22
  103. package/src/services/workstream-title.service.ts +0 -42
  104. package/src/services/workstream.service.ts +0 -803
  105. package/src/services/workstream.types.ts +0 -17
  106. /package/src/services/{workstream-constants.ts → thread-constants.ts} +0 -0
@@ -0,0 +1,17 @@
1
+ import { sdkPublicThreadSchema, sdkThreadRecordSchema, sdkThreadSchema } from '@lota-sdk/shared'
2
+ import type { SdkPublicThread, SdkThreadRecord } from '@lota-sdk/shared'
3
+ import { z } from 'zod'
4
+
5
+ export const ThreadSchema = sdkThreadRecordSchema
6
+ export type ThreadRecord = SdkThreadRecord
7
+
8
+ export const NormalizedThreadSchema = sdkThreadSchema.extend({
9
+ agentId: z.string().optional(),
10
+ threadType: z.string().optional(),
11
+ memoryBlock: z.string(),
12
+ })
13
+
14
+ export type NormalizedThread = z.infer<typeof NormalizedThreadSchema>
15
+ export type PublicThread = SdkPublicThread
16
+
17
+ export { sdkPublicThreadSchema as PublicThreadSchema }
@@ -26,7 +26,7 @@ import {
26
26
 
27
27
  const READ_FILE_PARTS_PAGES_PER_PART = 25
28
28
 
29
- export type UploadedWorkstreamAttachment = {
29
+ export type UploadedThreadAttachment = {
30
30
  filename: string
31
31
  mediaType: string
32
32
  sizeBytes: number
@@ -86,7 +86,7 @@ export class AttachmentStorageService {
86
86
  orgId: string
87
87
  namespace: string
88
88
  relativePath: string
89
- }): Promise<UploadedWorkstreamAttachment> {
89
+ }): Promise<UploadedThreadAttachment> {
90
90
  const filename = file.name || 'document'
91
91
  const mediaType = file.type || inferContentType(filename)
92
92
  const storageKey = buildOrganizationDocumentStorageKey({
@@ -107,7 +107,7 @@ export class AttachmentStorageService {
107
107
  }
108
108
  }
109
109
 
110
- async uploadWorkstreamAttachment({
110
+ async uploadThreadAttachment({
111
111
  file,
112
112
  orgId,
113
113
  userId,
@@ -115,7 +115,7 @@ export class AttachmentStorageService {
115
115
  file: File
116
116
  orgId: string
117
117
  userId: string
118
- }): Promise<UploadedWorkstreamAttachment> {
118
+ }): Promise<UploadedThreadAttachment> {
119
119
  const filename = file.name || 'attachment'
120
120
  const mediaType = file.type || inferContentType(filename)
121
121
  const sizeBytes = file.size
@@ -10,4 +10,4 @@ export * from './researcher.agent'
10
10
  export * from './skill-extractor.agent'
11
11
  export * from './skill-manager.agent'
12
12
  export * from './title-generator.agent'
13
- export * from './workstream-router.agent'
13
+ export * from './thread-router.agent'
@@ -40,7 +40,7 @@ Extract organization-relevant facts from the conversation.
40
40
  - User personal preferences
41
41
  - Transient requests
42
42
  - General knowledge questions
43
- - Temporary discussion workstreams
43
+ - Temporary discussion threads
44
44
  </ignore>
45
45
 
46
46
  <format>
@@ -27,7 +27,7 @@ Turn recent activity context into a short label that helps the user quickly reco
27
27
  - Prefer concrete task language over generic labels.
28
28
  - Mention the agent only when it adds useful context.
29
29
  - Focus on what the user was trying to accomplish.
30
- - Avoid generic phrases like "Chat", "Conversation", "Recent activity", "Task", or "Workstream update".
30
+ - Avoid generic phrases like "Chat", "Conversation", "Recent activity", "Task", or "Thread update".
31
31
  - Avoid punctuation at the end.
32
32
  - Do not invent details that are not present in the input.
33
33
  - If the input is too weak for improvement, return the current system title.
@@ -58,7 +58,7 @@ Turn recent activity context into a short label that helps the user quickly reco
58
58
  - Prefer concrete task language over generic labels.
59
59
  - Mention the agent only when it adds useful context.
60
60
  - Focus on what the user was trying to accomplish.
61
- - Avoid generic phrases like "Chat", "Conversation", "Recent activity", "Task", or "Workstream update".
61
+ - Avoid generic phrases like "Chat", "Conversation", "Recent activity", "Task", or "Thread update".
62
62
  - Avoid punctuation at the end.
63
63
  - Do not invent details that are not present in the input.
64
64
  - If the input is too weak for improvement, return the current system title.
@@ -16,7 +16,7 @@ Synthesize an updated workspace profile summary and durable memory facts from co
16
16
 
17
17
  <rules>
18
18
  - Evidence-grounded only. Do not invent details. Exclude routing/tool chatter.
19
- - Treat [workstream:...] prefixes as thread context only.
19
+ - Treat [thread:...] prefixes as thread context only.
20
20
  - Preserve existing profile format. Merge corrections; remove stale claims only when contradicted.
21
21
  - Facts must be standalone, one concrete claim each, understandable without transcript context.
22
22
  - If no durable updates exist, return current summary unchanged and empty facts.
@@ -2,12 +2,12 @@ export const RESEARCHER_PROMPT = `<agent-instructions>
2
2
  You are a **Research Agent** that gathers accurate, up-to-date information from the web and synthesizes it into a clear markdown report.
3
3
 
4
4
  <workflow>
5
- 1. Break the research task into 2-5 independent search queries.
5
+ 1. Break the research task into 2-3 focused search queries.
6
6
  2. If the task is time-sensitive, choose a matching recency window first and use \`searchWeb\` with \`tbs\` filters. Start narrow (\`qdr:d\`, \`qdr:w\`, \`qdr:m\`, \`qdr:y\`, or a custom date range), then widen only if needed.
7
7
  3. Run all searchWeb calls in parallel.
8
- 4. Review results and identify the 3-5 most authoritative/relevant URLs.
8
+ 4. Review results and identify the 2-3 most authoritative/relevant URLs.
9
9
  5. Fetch those pages in parallel using fetchWebpage.
10
- 6. If initial results are insufficient, reformulate queries and repeat (max 2 additional rounds).
10
+ 6. If initial results are insufficient, reformulate 1-2 queries and retry once.
11
11
  7. Synthesize findings into a structured markdown report.
12
12
  </workflow>
13
13
 
@@ -1,30 +1,23 @@
1
- import { ToolLoopAgent } from 'ai'
1
+ import { generateObject } from 'ai'
2
2
  import { z } from 'zod'
3
3
 
4
4
  import { aiGatewayChatModel } from '../ai-gateway/ai-gateway'
5
5
  import { buildAiGatewayDirectCacheHeaders } from '../ai-gateway/cache-headers'
6
6
  import { agentDescriptions, agentDisplayNames, agentShortDisplayNames, routerModelId } from '../config/agent-defaults'
7
-
8
- // ---------------------------------------------------------------------------
9
- // Schemas
10
- // ---------------------------------------------------------------------------
7
+ import { chatLogger } from '../config/logger'
8
+ import { withTimeout } from '../utils/async'
11
9
 
12
10
  const TriageResultSchema = z.object({ agentId: z.string(), routingContext: z.string() })
13
11
 
14
- const CheckResultSchema = z.object({
12
+ const CheckResultObjectSchema = z.object({
15
13
  done: z.boolean(),
16
- agentId: z.string().optional(),
17
- routingContext: z.string().optional(),
14
+ agentId: z.string().nullable(),
15
+ routingContext: z.string().nullable(),
18
16
  })
19
-
20
- const ROUTER_OUTPUT_PREVIEW_CHARS = 300
17
+ type RouterCheckContinueResult = { done: false; agentId: string; routingContext: string | null }
21
18
 
22
19
  export type RouterTriageResult = z.infer<typeof TriageResultSchema>
23
- export type RouterCheckResult = z.infer<typeof CheckResultSchema>
24
-
25
- // ---------------------------------------------------------------------------
26
- // Helpers
27
- // ---------------------------------------------------------------------------
20
+ export type RouterCheckResult = { done: true } | RouterCheckContinueResult
28
21
 
29
22
  interface RouterDisplayOptions {
30
23
  displayNamesById?: Partial<Record<string, string>>
@@ -136,56 +129,13 @@ function extractExplicitAgentTargets(
136
129
  })
137
130
  }
138
131
 
139
- function extractJson(text: string): unknown {
140
- const match = text.match(/\{[\s\S]*\}/)
141
- if (!match) return null
142
- try {
143
- return JSON.parse(match[0])
144
- } catch {
145
- return null
146
- }
147
- }
148
-
149
- /** Extract usable text from agent result — reasoning-only models put output in reasoning tokens */
150
- function extractResultText(result: { text?: string; reasoning?: unknown }): string {
151
- const text = typeof result.text === 'string' ? result.text : ''
152
- if (text.trim()) return text
153
- // Reasoning can be a string or an array of { type, text } objects
154
- const reasoning = result.reasoning
155
- if (typeof reasoning === 'string') return reasoning
156
- if (Array.isArray(reasoning)) {
157
- return reasoning
158
- .map((r) => {
159
- if (typeof r === 'string') return r
160
- if (typeof r !== 'object' || r === null || !('text' in r)) return ''
161
-
162
- const text = (r as { text?: unknown }).text
163
- return typeof text === 'string' ? text : ''
164
- })
165
- .join('')
166
- }
167
- return ''
168
- }
169
-
170
- function logRouterRaw(label: 'triage' | 'check', text: string): void {
171
- const preview = text.trim().slice(0, ROUTER_OUTPUT_PREVIEW_CHARS)
172
- if (!preview) return
173
- console.log(`[workstream-router] ${label} raw:`, preview)
174
- }
175
-
176
- // ---------------------------------------------------------------------------
177
- // Prompts
178
- // ---------------------------------------------------------------------------
179
-
180
- const TRIAGE_SYSTEM_PROMPT = `You are a workstream message router. Decide which team member should respond FIRST to the user message.
132
+ const TRIAGE_SYSTEM_PROMPT = `You are a thread message router. Decide which team member should respond FIRST to the user message.
181
133
 
182
134
  Rules:
183
135
  - Pick the single best-fit agent from the members list based on domain expertise.
184
136
  - If the user explicitly addresses an agent by name or role (e.g. "CTO: ..." or "CMO: ..."), route to that agent.
185
137
  - If no specialist clearly matches (general chat, greetings, coordination), respond with agentId "".
186
- - Be decisive. Reply with ONLY a JSON object, no other text.
187
-
188
- Format: {"agentId":"<id>","routingContext":"<1-sentence instruction>"}`
138
+ - Be decisive.`
189
139
 
190
140
  const CHECK_SYSTEM_PROMPT = `You decide if another team member should ALSO respond after the previous agent's response.
191
141
 
@@ -199,29 +149,41 @@ Rules:
199
149
  - If the user explicitly addressed multiple agents (e.g. "CTO: ... CMO: ...") and one hasn't responded yet, they MUST respond. Return done:false.
200
150
  - If the last agent's response explicitly defers to or recommends another specialist, that specialist SHOULD respond. Return done:false.
201
151
  - If there is a clearly separate dimension of the user's question not yet covered by any responded agent, add the best-fit remaining agent.
202
- - Do NOT add agents just for agreement, acknowledgement, or minor additions.
203
- - Reply with ONLY a JSON object, no other text.
152
+ - Do NOT add agents just for agreement, acknowledgement, or minor additions.`
204
153
 
205
- Format: {"done":true} or {"done":false,"agentId":"<id>","routingContext":"<1-sentence>"}`
154
+ const THREAD_ROUTER_TIMEOUT_MS = 30_000
206
155
 
207
- // ---------------------------------------------------------------------------
208
- // Agent functions
209
- // ---------------------------------------------------------------------------
210
-
211
- function createRouterAgent(systemPrompt: string) {
156
+ async function generateRouterObject<TSchema extends z.ZodTypeAny>(params: {
157
+ schema: TSchema
158
+ system: string
159
+ prompt: string
160
+ label: 'triage' | 'check'
161
+ }): Promise<z.infer<TSchema> | null> {
212
162
  const modelId = routerModelId ?? 'openai/gpt-5.4-nano'
213
- return new ToolLoopAgent({
214
- id: 'workstream-router',
215
- model: aiGatewayChatModel(modelId),
216
- headers: buildAiGatewayDirectCacheHeaders('workstream-router'),
217
- providerOptions: { openai: { reasoningEffort: 'low' } },
218
- instructions: systemPrompt,
219
- maxOutputTokens: 256,
220
- })
163
+
164
+ try {
165
+ const { object } = await withTimeout(
166
+ generateObject({
167
+ model: aiGatewayChatModel(modelId),
168
+ headers: buildAiGatewayDirectCacheHeaders('thread-router'),
169
+ providerOptions: { openai: { reasoningEffort: 'low' } },
170
+ schema: params.schema,
171
+ system: params.system,
172
+ prompt: params.prompt,
173
+ maxOutputTokens: 256,
174
+ }),
175
+ THREAD_ROUTER_TIMEOUT_MS,
176
+ `thread-router ${params.label}`,
177
+ )
178
+ return params.schema.parse(object)
179
+ } catch (error) {
180
+ chatLogger.error`[thread-router] ${params.label} failed: ${error instanceof Error ? error.message : String(error)}`
181
+ return null
182
+ }
221
183
  }
222
184
 
223
- export async function triageWorkstreamMessage(params: {
224
- workstreamTitle: string
185
+ export async function triageThreadMessage(params: {
186
+ threadTitle: string
225
187
  members: readonly string[]
226
188
  messageText: string
227
189
  recentContext?: string
@@ -245,7 +207,7 @@ export async function triageWorkstreamMessage(params: {
245
207
 
246
208
  const membersDesc = buildMembersDescription(params.members, displayOptions)
247
209
  const prompt = [
248
- `Workstream: "${params.workstreamTitle}"`,
210
+ `Thread: "${params.threadTitle}"`,
249
211
  `Members:\n${membersDesc}`,
250
212
  params.recentContext ? `Recent context:\n${params.recentContext}` : '',
251
213
  `User message: "${params.messageText}"`,
@@ -253,44 +215,31 @@ export async function triageWorkstreamMessage(params: {
253
215
  .filter(Boolean)
254
216
  .join('\n\n')
255
217
 
256
- const agent = createRouterAgent(TRIAGE_SYSTEM_PROMPT)
257
- let result: Awaited<ReturnType<typeof agent.generate>>
258
- try {
259
- result = await agent.generate({ messages: [{ role: 'user', content: prompt }], timeout: { totalMs: 30_000 } })
260
- } catch (error) {
261
- console.error('[workstream-router] triage failed:', error instanceof Error ? error.message : error)
262
- return null
263
- }
218
+ const parsed = await generateRouterObject({
219
+ schema: TriageResultSchema,
220
+ system: TRIAGE_SYSTEM_PROMPT,
221
+ prompt,
222
+ label: 'triage',
223
+ })
264
224
 
265
- const effectiveText = extractResultText(result as { text?: string; reasoning?: unknown })
266
- logRouterRaw('triage', effectiveText)
267
- const json = extractJson(effectiveText)
268
- if (json === null) {
269
- if (effectiveText.trim()) {
270
- console.log('[workstream-router] triage ignored non-json output')
271
- }
272
- return null
273
- }
274
- const parsed = TriageResultSchema.safeParse(json)
275
- if (!parsed.success) {
276
- console.log('[workstream-router] triage parse failed:', JSON.stringify(parsed.error.issues))
225
+ if (!parsed) {
277
226
  return null
278
227
  }
279
- if (!parsed.data.agentId) {
280
- console.log('[workstream-router] triage returned empty agentId — fallback to owner')
228
+ if (!parsed.agentId) {
229
+ chatLogger.debug`[thread-router] triage returned empty agentId`
281
230
  return null
282
231
  }
283
- if (!params.members.includes(parsed.data.agentId)) {
284
- console.log('[workstream-router] triage returned unknown agent:', parsed.data.agentId)
232
+ if (!params.members.includes(parsed.agentId)) {
233
+ chatLogger.warn`[thread-router] triage returned unknown agent: ${parsed.agentId}`
285
234
  return null
286
235
  }
287
236
 
288
- console.log('[workstream-router] triage routed to:', parsed.data.agentId)
289
- return parsed.data
237
+ chatLogger.debug`[thread-router] triage routed to ${parsed.agentId}`
238
+ return parsed
290
239
  }
291
240
 
292
241
  export async function checkForNextAgent(params: {
293
- workstreamTitle: string
242
+ threadTitle: string
294
243
  members: readonly string[]
295
244
  messageText: string
296
245
  respondedAgents: string[]
@@ -321,45 +270,29 @@ export async function checkForNextAgent(params: {
321
270
  const respondedList = params.respondedAgents.map((id) => readDisplayName(id, displayOptions)).join(', ')
322
271
 
323
272
  const prompt = [
324
- `Workstream: "${params.workstreamTitle}"`,
273
+ `Thread: "${params.threadTitle}"`,
325
274
  `Remaining members:\n${membersDesc}`,
326
275
  `Already responded: ${respondedList}`,
327
276
  `User message: "${params.messageText}"`,
328
277
  `Last agent response:\n"${params.lastResponseSummary}"`,
329
278
  ].join('\n\n')
330
279
 
331
- const agent = createRouterAgent(CHECK_SYSTEM_PROMPT)
332
- let result: Awaited<ReturnType<typeof agent.generate>>
333
- try {
334
- result = await agent.generate({ messages: [{ role: 'user', content: prompt }], timeout: { totalMs: 30_000 } })
335
- } catch (error) {
336
- console.error('[workstream-router] check failed:', error instanceof Error ? error.message : error)
337
- return { done: true }
338
- }
280
+ const parsed = await generateRouterObject({
281
+ schema: CheckResultObjectSchema,
282
+ system: CHECK_SYSTEM_PROMPT,
283
+ prompt,
284
+ label: 'check',
285
+ })
339
286
 
340
- const effectiveText = extractResultText(result as { text?: string; reasoning?: unknown })
341
- logRouterRaw('check', effectiveText)
342
- const json = extractJson(effectiveText)
343
- if (json === null) {
344
- if (effectiveText.trim()) {
345
- console.log('[workstream-router] check ignored non-json output')
346
- }
347
- return { done: true }
348
- }
349
- const parsed = CheckResultSchema.safeParse(json)
350
- if (!parsed.success) {
351
- console.log('[workstream-router] check parse failed:', JSON.stringify(parsed.error.issues))
352
- return { done: true }
353
- }
354
- if (parsed.data.done) {
355
- console.log('[workstream-router] check: done, no more agents needed')
287
+ if (!parsed || parsed.done) {
288
+ chatLogger.debug`[thread-router] check finished without another agent`
356
289
  return { done: true }
357
290
  }
358
- if (!parsed.data.agentId || !remainingMembers.includes(parsed.data.agentId)) {
359
- console.log('[workstream-router] check: invalid agentId:', parsed.data.agentId)
291
+ if (!parsed.agentId || !remainingMembers.includes(parsed.agentId)) {
292
+ chatLogger.warn`[thread-router] check returned invalid agent: ${parsed.agentId ?? 'missing'}`
360
293
  return { done: true }
361
294
  }
362
295
 
363
- console.log('[workstream-router] check: next agent:', parsed.data.agentId)
364
- return parsed.data
296
+ chatLogger.debug`[thread-router] check selected ${parsed.agentId}`
297
+ return { done: false, agentId: parsed.agentId, routingContext: parsed.routingContext ?? null }
365
298
  }
@@ -9,9 +9,9 @@ import {
9
9
  import type { CreateHelperToolLoopAgentOptions } from '../runtime/agent-types'
10
10
  import { resolveHelperAgentOptions } from './helper-agent-options'
11
11
 
12
- const WORKSTREAM_TITLE_MAX_TOKENS = 512
12
+ const THREAD_TITLE_MAX_TOKENS = 512
13
13
 
14
- export const WORKSTREAM_TITLE_GENERATOR_PROMPT = `<agent-instructions>
14
+ export const THREAD_TITLE_GENERATOR_PROMPT = `<agent-instructions>
15
15
  You are a **Title Generator** that creates concise chat titles.
16
16
 
17
17
  <task>
@@ -20,7 +20,7 @@ Generate a chat title based only on the user's message.
20
20
 
21
21
  <constraints>
22
22
  - Maximum 3-4 words
23
- - Capture the core workstream or intent
23
+ - Capture the core thread or intent
24
24
  - Use natural, readable language
25
25
  - No punctuation at the end
26
26
  </constraints>
@@ -30,15 +30,15 @@ Return only the title text. No quotes, no labels, no explanation.
30
30
  </output-format>
31
31
  </agent-instructions>`
32
32
 
33
- export function createWorkstreamTitleGeneratorAgent(options: CreateHelperToolLoopAgentOptions) {
33
+ export function createThreadTitleGeneratorAgent(options: CreateHelperToolLoopAgentOptions) {
34
34
  return new ToolLoopAgent({
35
- id: 'workstream-title-generator',
35
+ id: 'thread-title-generator',
36
36
  model: aiGatewayModel(OPENROUTER_FAST_REASONING_MODEL_ID),
37
- headers: buildAiGatewayDirectCacheHeaders('workstream-title-generator'),
37
+ headers: buildAiGatewayDirectCacheHeaders('thread-title-generator'),
38
38
  providerOptions: OPENROUTER_MINIMAL_REASONING_PROVIDER_OPTIONS,
39
39
  ...resolveHelperAgentOptions(options, {
40
- instructions: WORKSTREAM_TITLE_GENERATOR_PROMPT,
41
- maxOutputTokens: WORKSTREAM_TITLE_MAX_TOKENS,
40
+ instructions: THREAD_TITLE_GENERATOR_PROMPT,
41
+ maxOutputTokens: THREAD_TITLE_MAX_TOKENS,
42
42
  }),
43
43
  })
44
44
  }
@@ -5,19 +5,16 @@ import {
5
5
  expandAgentPlanDraft,
6
6
  getLatestExecutionPlanResult,
7
7
  } from '@lota-sdk/shared'
8
- import type { ExecutionPlanAction, ExecutionPlanArgs, CreateProjectWithPlanResultData } from '@lota-sdk/shared'
8
+ import type { CreateProjectWithPlanResultData, ExecutionPlanAction, ExecutionPlanArgs } from '@lota-sdk/shared'
9
9
  import { tool } from 'ai'
10
10
 
11
11
  import type { RecordIdRef } from '../db/record-id'
12
12
  import { recordIdToString } from '../db/record-id'
13
13
  import { TABLES } from '../db/tables'
14
14
  import { executionPlanService } from '../services/execution-plan.service'
15
- import { workstreamService } from '../services/workstream.service'
15
+ import { threadService } from '../services/thread.service'
16
16
 
17
- type ExecutionPlanWorkstreamService = Pick<
18
- typeof workstreamService,
19
- 'createWorkstream' | 'deleteWorkstream' | 'getWorkstream'
20
- >
17
+ type ExecutionPlanThreadService = Pick<typeof threadService, 'createThread' | 'deleteThread' | 'getThread'>
21
18
 
22
19
  type ExecutionPlanExecutionPlanService = Pick<
23
20
  typeof executionPlanService,
@@ -26,90 +23,92 @@ type ExecutionPlanExecutionPlanService = Pick<
26
23
  | 'resumeRun'
27
24
  | 'listActivePlanSummaries'
28
25
  | 'getActivePlanToolResult'
29
- | 'getActivePlansForWorkstream'
26
+ | 'getActivePlansForThread'
30
27
  >
31
28
 
32
29
  function extractDraft(input: ExecutionPlanArgs) {
33
- const { action, projectTitle, targetWorkstreamId, runId, reason, ...draftInput } = input
34
- return { action, projectTitle, targetWorkstreamId, runId, reason, draft: expandAgentPlanDraft(draftInput) }
30
+ const { action, projectTitle, targetThreadId, runId, reason, ...draftInput } = input
31
+ return { action, projectTitle, targetThreadId, runId, reason, draft: expandAgentPlanDraft(draftInput) }
35
32
  }
36
33
 
37
34
  export function createExecutionPlanTool(params: {
38
35
  orgId: RecordIdRef
39
36
  userId: RecordIdRef
40
- workstreamId: RecordIdRef
37
+ threadId: RecordIdRef
41
38
  agentId: string
42
39
  executionPlanService?: ExecutionPlanExecutionPlanService
43
- workstreamService?: ExecutionPlanWorkstreamService
40
+ threadService?: ExecutionPlanThreadService
44
41
  onPlanChanged?: () => void
45
42
  validateInlinePlan?: (draft: ReturnType<typeof expandAgentPlanDraft>) => void
46
43
  }) {
47
44
  const resolvedEpService = params.executionPlanService ?? executionPlanService
48
- const resolvedWsService = params.workstreamService ?? workstreamService
45
+ const resolvedWsService = params.threadService ?? threadService
49
46
 
50
47
  return tool({
51
48
  description:
52
- 'Manage execution plans. Actions: create (inline, 1-2 nodes), create-project (dedicated project workstream, 3+ nodes), replace (swap active plan), resume (resume interrupted plan).',
49
+ 'Manage execution plans. Actions: create (inline, 1-2 nodes), create-project (dedicated project thread, 3+ nodes), replace (swap active plan), resume (resume interrupted plan).',
53
50
  inputSchema: ExecutionPlanArgsSchema,
54
51
  execute: async (input) => {
55
- const { action, projectTitle, targetWorkstreamId, runId, reason, draft } = extractDraft(input)
52
+ const { action, projectTitle, targetThreadId, runId, reason, draft } = extractDraft(input)
56
53
 
57
54
  const handler: Record<ExecutionPlanAction, () => Promise<unknown>> = {
58
55
  create: async () => {
59
56
  params.validateInlinePlan?.(draft)
60
57
  return await resolvedEpService.createPlan({
61
58
  organizationId: params.orgId,
62
- workstreamId: targetWorkstreamId ?? params.workstreamId,
59
+ threadId: targetThreadId ?? params.threadId,
63
60
  leadAgentId: params.agentId,
64
61
  input: draft,
65
62
  })
66
63
  },
67
64
 
68
65
  'create-project': async () => {
69
- const targetWorkstream = targetWorkstreamId
70
- ? await resolvedWsService.getWorkstream(targetWorkstreamId)
66
+ const targetThread = targetThreadId
67
+ ? await resolvedWsService.getThread(targetThreadId)
71
68
  : await (() => {
72
69
  if (!projectTitle) {
73
70
  throw new Error('projectTitle is required when action is "create-project".')
74
71
  }
75
72
 
76
- return resolvedWsService.createWorkstream(params.userId, params.orgId, {
73
+ return resolvedWsService.createThread({
74
+ userId: params.userId,
75
+ organizationId: params.orgId,
77
76
  title: projectTitle,
78
- mode: 'group',
77
+ type: 'group',
79
78
  })
80
79
  })()
81
80
 
82
- if (targetWorkstream.organizationId !== recordIdToString(params.orgId, TABLES.ORGANIZATION)) {
83
- throw new Error('Target workstream belongs to a different organization.')
81
+ if (targetThread.organizationId !== recordIdToString(params.orgId, TABLES.ORGANIZATION)) {
82
+ throw new Error('Target thread belongs to a different organization.')
84
83
  }
85
- if (targetWorkstream.userId !== recordIdToString(params.userId, TABLES.USER)) {
86
- throw new Error('Target workstream belongs to a different user.')
84
+ if (targetThread.userId !== recordIdToString(params.userId, TABLES.USER)) {
85
+ throw new Error('Target thread belongs to a different user.')
87
86
  }
88
87
 
89
- const existingPlans = await resolvedEpService.getActivePlansForWorkstream(targetWorkstream.id)
90
- if (!targetWorkstream.core && existingPlans.length > 0) {
88
+ const existingPlans = await resolvedEpService.getActivePlansForThread(targetThread.id)
89
+ if (targetThread.type !== 'thread' && existingPlans.length > 0) {
91
90
  throw new Error(
92
- 'This workstream already has an active execution plan. Use action "replace" or target a core workstream.',
91
+ 'This thread already has an active execution plan. Use action "replace" or target a core thread.',
93
92
  )
94
93
  }
95
94
 
96
- const createdWorkstream = !targetWorkstreamId
95
+ const createdThread = !targetThreadId
97
96
  try {
98
97
  const result = await resolvedEpService.createPlan({
99
98
  organizationId: params.orgId,
100
- workstreamId: targetWorkstream.id,
99
+ threadId: targetThread.id,
101
100
  leadAgentId: params.agentId,
102
101
  input: draft,
103
102
  })
104
103
  return {
105
104
  ...result,
106
- workstreamId: targetWorkstream.id,
107
- workstreamTitle: targetWorkstream.title,
108
- createdWorkstream,
105
+ threadId: targetThread.id,
106
+ threadTitle: targetThread.title,
107
+ createdThread,
109
108
  } satisfies CreateProjectWithPlanResultData
110
109
  } catch (error) {
111
- if (createdWorkstream) {
112
- await resolvedWsService.deleteWorkstream(targetWorkstream.id).catch(() => {})
110
+ if (createdThread) {
111
+ await resolvedWsService.deleteThread(targetThread.id).catch(() => {})
113
112
  }
114
113
  throw error
115
114
  }
@@ -122,7 +121,7 @@ export function createExecutionPlanTool(params: {
122
121
 
123
122
  return await resolvedEpService.replacePlan({
124
123
  organizationId: params.orgId,
125
- workstreamId: params.workstreamId,
124
+ threadId: params.threadId,
126
125
  leadAgentId: params.agentId,
127
126
  input: { runId, reason, ...draft },
128
127
  })
@@ -134,7 +133,7 @@ export function createExecutionPlanTool(params: {
134
133
  }
135
134
 
136
135
  return await resolvedEpService.resumeRun({
137
- workstreamId: params.workstreamId,
136
+ threadId: params.threadId,
138
137
  emittedBy: params.agentId,
139
138
  input: { runId },
140
139
  })
@@ -148,17 +147,17 @@ export function createExecutionPlanTool(params: {
148
147
  })
149
148
  }
150
149
 
151
- export function createExecutionPlanQueryTool(params: { workstreamId: RecordIdRef }) {
150
+ export function createExecutionPlanQueryTool(params: { threadId: RecordIdRef }) {
152
151
  return tool({
153
152
  description:
154
153
  'Query execution plans. Omit runId to list all active plans. Provide runId to load a specific plan run.',
155
154
  inputSchema: ExecutionPlanQueryArgsSchema,
156
155
  execute: async (input) => {
157
156
  if (!input.runId) {
158
- return await executionPlanService.listActivePlanSummaries(params.workstreamId)
157
+ return await executionPlanService.listActivePlanSummaries(params.threadId)
159
158
  }
160
159
  return await executionPlanService.getActivePlanToolResult({
161
- workstreamId: params.workstreamId,
160
+ threadId: params.threadId,
162
161
  runId: input.runId,
163
162
  includeEvents: input.includeEvents,
164
163
  includeArtifacts: input.includeArtifacts,
@@ -171,7 +170,7 @@ export function createExecutionPlanQueryTool(params: { workstreamId: RecordIdRef
171
170
  }
172
171
 
173
172
  export function createSubmitExecutionNodeResultTool(params: {
174
- workstreamId: RecordIdRef
173
+ threadId: RecordIdRef
175
174
  agentId: string
176
175
  onPlanChanged?: () => void
177
176
  }) {
@@ -181,7 +180,7 @@ export function createSubmitExecutionNodeResultTool(params: {
181
180
  inputSchema: SubmitExecutionNodeResultArgsSchema,
182
181
  execute: async (input) => {
183
182
  const result = await executionPlanService.submitNodeResult({
184
- workstreamId: params.workstreamId,
183
+ threadId: params.threadId,
185
184
  emittedBy: params.agentId,
186
185
  input,
187
186
  })