@lota-sdk/core 0.1.24 → 0.1.26

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 (74) hide show
  1. package/package.json +2 -2
  2. package/src/ai/definitions.ts +5 -59
  3. package/src/ai-gateway/ai-gateway.ts +36 -28
  4. package/src/ai-gateway/cache-headers.ts +9 -0
  5. package/src/config/model-constants.ts +6 -2
  6. package/src/create-runtime.ts +1 -17
  7. package/src/db/memory-types.ts +13 -8
  8. package/src/db/memory.ts +74 -53
  9. package/src/queues/autonomous-job.queue.ts +1 -8
  10. package/src/queues/context-compaction.queue.ts +2 -2
  11. package/src/queues/index.ts +2 -6
  12. package/src/queues/organization-learning.queue.ts +78 -0
  13. package/src/queues/plan-agent-heartbeat.queue.ts +10 -16
  14. package/src/queues/title-generation.queue.ts +62 -0
  15. package/src/runtime/agent-prompt-context.ts +0 -18
  16. package/src/runtime/agent-runtime-policy.ts +9 -2
  17. package/src/runtime/context-compaction-constants.ts +4 -2
  18. package/src/runtime/context-compaction.ts +135 -118
  19. package/src/runtime/memory-pipeline.ts +70 -1
  20. package/src/runtime/memory-prompts-fact.ts +16 -0
  21. package/src/runtime/plugin-resolution.ts +3 -2
  22. package/src/runtime/plugin-types.ts +1 -42
  23. package/src/runtime/post-turn-side-effects.ts +212 -0
  24. package/src/runtime/runtime-config.ts +0 -13
  25. package/src/runtime/runtime-extensions.ts +10 -16
  26. package/src/runtime/runtime-worker-registry.ts +8 -19
  27. package/src/runtime/social-chat-agent-runner.ts +119 -0
  28. package/src/runtime/social-chat-history.ts +110 -0
  29. package/src/runtime/social-chat-prompts.ts +58 -0
  30. package/src/runtime/social-chat.ts +104 -340
  31. package/src/runtime/specialist-runner.ts +18 -0
  32. package/src/runtime/workstream-chat-helpers.ts +19 -0
  33. package/src/runtime/workstream-plan-turn.ts +195 -0
  34. package/src/runtime/workstream-state.ts +11 -8
  35. package/src/runtime/workstream-turn-context.ts +183 -0
  36. package/src/services/autonomous-job.service.ts +1 -8
  37. package/src/services/execution-plan.service.ts +205 -334
  38. package/src/services/index.ts +1 -4
  39. package/src/services/memory.service.ts +54 -44
  40. package/src/services/ownership-dispatcher.service.ts +2 -19
  41. package/src/services/plan-completion-side-effects.ts +80 -0
  42. package/src/services/plan-event-delivery.service.ts +1 -1
  43. package/src/services/plan-executor.service.ts +42 -190
  44. package/src/services/plan-node-spec.ts +60 -0
  45. package/src/services/plan-run-data.ts +88 -0
  46. package/src/services/plan-validator.service.ts +10 -8
  47. package/src/services/workstream-constants.ts +2 -0
  48. package/src/services/workstream-title.service.ts +1 -1
  49. package/src/services/workstream-turn-preparation.service.ts +208 -715
  50. package/src/services/workstream.service.ts +162 -192
  51. package/src/services/workstream.types.ts +12 -44
  52. package/src/system-agents/regular-chat-memory-digest.agent.ts +3 -0
  53. package/src/tools/execution-plan.tool.ts +7 -6
  54. package/src/tools/remember-memory.tool.ts +7 -10
  55. package/src/tools/research-topic.tool.ts +1 -1
  56. package/src/tools/team-think.tool.ts +1 -1
  57. package/src/tools/user-questions.tool.ts +1 -1
  58. package/src/utils/autonomous-job-ids.ts +7 -0
  59. package/src/workers/organization-learning.worker.ts +31 -0
  60. package/src/workers/regular-chat-memory-digest.runner.ts +9 -3
  61. package/src/workers/skill-extraction.runner.ts +2 -2
  62. package/src/queues/recent-activity-title-refinement.queue.ts +0 -30
  63. package/src/queues/regular-chat-memory-digest.config.ts +0 -12
  64. package/src/queues/regular-chat-memory-digest.queue.ts +0 -34
  65. package/src/queues/skill-extraction.config.ts +0 -9
  66. package/src/queues/skill-extraction.queue.ts +0 -27
  67. package/src/queues/workstream-title-generation.queue.ts +0 -33
  68. package/src/services/context-enrichment.service.ts +0 -33
  69. package/src/services/coordination-registry.service.ts +0 -117
  70. package/src/services/domain-agent-executor.service.ts +0 -71
  71. package/src/services/memory-assessment.service.ts +0 -44
  72. package/src/services/playbook-registry.service.ts +0 -67
  73. package/src/workers/regular-chat-memory-digest.worker.ts +0 -22
  74. package/src/workers/skill-extraction.worker.ts +0 -22
@@ -4,27 +4,19 @@ import {
4
4
  buildSlackSocialReplyMarkdown,
5
5
  CONSULT_SPECIALIST_TOOL_NAME,
6
6
  ConsultSpecialistArgsSchema,
7
- stripSlackToolExecutionNoticeMarkdown,
8
7
  } from '@lota-sdk/shared'
9
8
  import type { ChatMessage, ConsultSpecialistArgs } from '@lota-sdk/shared'
10
- import { stepCountIs, tool as createTool } from 'ai'
11
- import type { ToolLoopAgent, ToolSet } from 'ai'
9
+ import { tool as createTool } from 'ai'
12
10
  import { Chat, ConsoleLogger } from 'chat'
13
11
  import type { Message, Thread, WebhookOptions } from 'chat'
14
12
  import type IORedis from 'ioredis'
15
13
 
16
- import {
17
- agentDisplayNames,
18
- createAgent,
19
- getAgentRuntimeConfig,
20
- teamConsultParticipants,
21
- } from '../config/agent-defaults'
14
+ import { agentDisplayNames, teamConsultParticipants } from '../config/agent-defaults'
22
15
  import { aiLogger } from '../config/logger'
23
16
  import { recordIdToString } from '../db/record-id'
24
17
  import { TABLES } from '../db/tables'
18
+ import { enqueueRegularChatMemoryDigest, enqueueSkillExtraction } from '../queues/organization-learning.queue'
25
19
  import { enqueuePostChatMemory } from '../queues/post-chat-memory.queue'
26
- import { enqueueRegularChatMemoryDigest } from '../queues/regular-chat-memory-digest.queue'
27
- import { enqueueSkillExtraction } from '../queues/skill-extraction.queue'
28
20
  import type {
29
21
  BuildSocialChatAgentToolsParams,
30
22
  LotaRuntimeSocialChatConfig,
@@ -37,7 +29,26 @@ import { socialChatHistoryService } from '../services/social-chat-history.servic
37
29
  import { safeEnqueue } from '../utils/async'
38
30
  import { buildAgentPromptContext } from './agent-prompt-context'
39
31
  import { createServerRunAbortController } from './agent-stream-helpers'
40
- import { buildAgentHistoryMessages, extractMessageText, toHistoryMessages } from './workstream-chat-helpers'
32
+ import { runSocialAgentTurn, withLoggedSocialToolSet } from './social-chat-agent-runner'
33
+ import {
34
+ collectThreadMessages,
35
+ createSocialChatCursorId,
36
+ normalizeSocialHistoryMessage,
37
+ readSlackAuthorName,
38
+ buildSocialChatThreadTranscript,
39
+ } from './social-chat-history'
40
+ import {
41
+ buildLeadSocialChatPrompt,
42
+ buildSocialChatIdentitySection,
43
+ buildSpecialistSocialChatPrompt,
44
+ } from './social-chat-prompts'
45
+ import { runSpecialistSession } from './specialist-runner'
46
+ import {
47
+ buildAgentHistoryMessages,
48
+ extractMessageText,
49
+ toHistoryMessages,
50
+ toOptionalTrimmedString,
51
+ } from './workstream-chat-helpers'
41
52
 
42
53
  const DEFAULT_SOCIAL_CHAT_AGENT_ID = 'socialChat'
43
54
  const DEFAULT_SOCIAL_CHAT_AGENT_DISPLAY_NAME = 'Lota'
@@ -45,8 +56,6 @@ const DEFAULT_SOCIAL_CHAT_STATE_PREFIX = 'lota:social:chat-sdk'
45
56
  const DEFAULT_SOCIAL_CHAT_DEDUPE_TTL_MS = 15 * 60 * 1000
46
57
  const PRESEEDED_MEMORY_LOOKUP_LIMIT = 3
47
58
 
48
- type SocialChatAgent = ToolLoopAgent<never, ToolSet>
49
-
50
59
  export interface LotaRuntimeSocialChat {
51
60
  enabled: boolean
52
61
  initialize(): Promise<void>
@@ -76,96 +85,6 @@ function createDisabledSocialChatRuntime(): LotaRuntimeSocialChat {
76
85
  }
77
86
  }
78
87
 
79
- function toOptionalTrimmedString(value: unknown): string | undefined {
80
- if (typeof value !== 'string') return undefined
81
- const normalized = value.trim()
82
- return normalized.length > 0 ? normalized : undefined
83
- }
84
-
85
- function readSlackAuthorName(message: Message): string | undefined {
86
- return (
87
- toOptionalTrimmedString(message.author.fullName) ??
88
- toOptionalTrimmedString(message.author.userName) ??
89
- toOptionalTrimmedString(message.author.userId)
90
- )
91
- }
92
-
93
- function buildSocialChatIdentitySection(agentDisplayName: string): string {
94
- return [
95
- '<social-chat-agent>',
96
- `You are ${agentDisplayName}, the social chat agent for this workspace.`,
97
- '- You respond inside Slack threads.',
98
- '- Use the available tools, memories, learned skills, and workspace knowledge before guessing.',
99
- '- If the answer may depend on prior workspace decisions, preferences, promises, history, or relationship context, check memory with the available memory tools before answering.',
100
- '- If the user shares a URL or asks about a website/page, read it with fetchWebpage before answering. Do not summarize website contents from prior knowledge.',
101
- '- When another roster agent is better suited for a question, consult that specialist and then synthesize the final answer yourself.',
102
- '- Keep the final reply crisp and readable for Slack unless the user explicitly asks for depth.',
103
- '</social-chat-agent>',
104
- ].join('\n')
105
- }
106
-
107
- function buildThreadTranscript(
108
- messages: Array<{
109
- role: 'user' | 'assistant'
110
- parts: Array<Record<string, unknown>>
111
- metadata?: Record<string, unknown>
112
- }>,
113
- ): string {
114
- const historyMessages = toHistoryMessages(messages)
115
- if (historyMessages.length === 0) return 'No prior thread history.'
116
-
117
- return historyMessages
118
- .map((message) =>
119
- message.role === 'user' ? `User: ${message.content}` : `${message.agentName ?? 'Assistant'}: ${message.content}`,
120
- )
121
- .join('\n\n')
122
- }
123
-
124
- function buildLeadAgentPrompt(params: {
125
- agentDisplayName: string
126
- channelId: string
127
- threadId: string
128
- transcript: string
129
- latestUserMessage: string
130
- latestAuthorName?: string
131
- }): string {
132
- return [
133
- `Platform: Slack`,
134
- `Channel ID: ${params.channelId}`,
135
- `Thread ID: ${params.threadId}`,
136
- `Current Slack author: ${params.latestAuthorName ?? 'Unknown'}`,
137
- '',
138
- `You are replying as ${params.agentDisplayName}.`,
139
- '',
140
- '<thread-transcript>',
141
- params.transcript,
142
- '</thread-transcript>',
143
- '',
144
- 'Latest user message:',
145
- params.latestUserMessage,
146
- ].join('\n')
147
- }
148
-
149
- function buildSpecialistPrompt(params: {
150
- requesterName: string
151
- agentName: string
152
- task: string
153
- transcript: string
154
- }): string {
155
- return [
156
- `${params.requesterName} needs specialist help from ${params.agentName}.`,
157
- '',
158
- '<thread-transcript>',
159
- params.transcript,
160
- '</thread-transcript>',
161
- '',
162
- 'Specialist task:',
163
- params.task,
164
- '',
165
- 'Answer only with the specialist guidance that should be used in the final Slack reply.',
166
- ].join('\n')
167
- }
168
-
169
88
  function createAssistantMessage(params: { agentId: string; agentName: string; text: string }): ChatMessage {
170
89
  return {
171
90
  id: Bun.randomUUIDv7(),
@@ -175,10 +94,6 @@ function createAssistantMessage(params: { agentId: string; agentName: string; te
175
94
  }
176
95
  }
177
96
 
178
- function createCursorId(message: { workspaceId: string; threadId: string; messageId: string }): string {
179
- return `social:slack:${message.workspaceId}:${message.threadId}:${message.messageId}`
180
- }
181
-
182
97
  function toSafeJobIdSegment(value: string): string {
183
98
  return value.replace(/[^a-zA-Z0-9_-]/g, '_')
184
99
  }
@@ -192,132 +107,6 @@ function createSocialMemoryDedupeKey(params: { workspaceId: string; threadId: st
192
107
  ].join('-')
193
108
  }
194
109
 
195
- interface ExecutableToolDefinition {
196
- execute: (...args: unknown[]) => Promise<unknown>
197
- }
198
-
199
- function withLoggedToolSet(
200
- tools: ToolSet,
201
- params: { agentId: string; channelId: string; threadId: string; executedToolNames: string[] },
202
- ): ToolSet {
203
- return Object.fromEntries(
204
- Object.entries(tools).map(([toolName, toolDefinition]) => {
205
- if (
206
- typeof toolDefinition !== 'object' ||
207
- !('execute' in toolDefinition) ||
208
- typeof toolDefinition.execute !== 'function'
209
- ) {
210
- return [toolName, toolDefinition]
211
- }
212
-
213
- const executableTool = toolDefinition as typeof toolDefinition & ExecutableToolDefinition
214
-
215
- return [
216
- toolName,
217
- {
218
- ...toolDefinition,
219
- execute: async (...args: unknown[]): Promise<unknown> => {
220
- aiLogger.info`Slack social-chat tool start: agentId=${params.agentId}, tool=${toolName}, channelId=${params.channelId}, threadId=${params.threadId}`
221
- try {
222
- const result: unknown = await executableTool.execute(...args)
223
- params.executedToolNames.push(toolName)
224
- aiLogger.info`Slack social-chat tool finish: agentId=${params.agentId}, tool=${toolName}, channelId=${params.channelId}, threadId=${params.threadId}`
225
- return result
226
- } catch (error) {
227
- aiLogger.warn`Slack social-chat tool failed: agentId=${params.agentId}, tool=${toolName}, channelId=${params.channelId}, threadId=${params.threadId}, error=${error}`
228
- throw error
229
- }
230
- },
231
- },
232
- ]
233
- }),
234
- )
235
- }
236
-
237
- function normalizeSocialHistoryMessage(params: {
238
- workspaceId: string
239
- channelId: string
240
- agentId: string
241
- agentDisplayName: string
242
- message: Message
243
- textOverride?: string
244
- }): {
245
- source: 'social'
246
- sourceId: string
247
- platform: 'slack'
248
- workspaceId: string
249
- channelId: string
250
- threadId: string
251
- messageId: string
252
- role: 'user' | 'assistant'
253
- parts: Array<Record<string, unknown>>
254
- metadata?: Record<string, unknown>
255
- cursor: { createdAt: Date; id: string }
256
- } | null {
257
- const role: 'user' | 'assistant' = params.message.author.isMe ? 'assistant' : 'user'
258
- const text = toOptionalTrimmedString(
259
- role === 'assistant'
260
- ? stripSlackToolExecutionNoticeMarkdown(params.textOverride ?? params.message.text)
261
- : (params.textOverride ?? params.message.text),
262
- )
263
- const fileParts = params.message.attachments.map((attachment) => ({
264
- type: 'file',
265
- filename: attachment.name ?? 'attachment',
266
- mediaType: attachment.mimeType ?? 'application/octet-stream',
267
- sizeBytes: typeof attachment.size === 'number' ? attachment.size : null,
268
- storageKey: attachment.url ?? 'external',
269
- }))
270
- const parts = [...(text ? [{ type: 'text', text }] : []), ...fileParts]
271
- if (parts.length === 0) return null
272
-
273
- const createdAt = params.message.metadata.dateSent
274
- const metadata: Record<string, unknown> = {
275
- platform: 'slack',
276
- channelId: params.channelId,
277
- threadId: params.message.threadId,
278
- messageId: params.message.id,
279
- authorId: params.message.author.userId,
280
- authorName: readSlackAuthorName(params.message),
281
- }
282
- if (role === 'assistant') {
283
- metadata.agentId = params.agentId
284
- metadata.agentName = params.agentDisplayName
285
- }
286
-
287
- return {
288
- source: 'social',
289
- sourceId: `slack:${params.channelId}:${params.message.threadId}`,
290
- platform: 'slack',
291
- workspaceId: params.workspaceId,
292
- channelId: params.channelId,
293
- threadId: params.message.threadId,
294
- messageId: params.message.id,
295
- role,
296
- parts,
297
- metadata,
298
- cursor: {
299
- createdAt,
300
- id: createCursorId({
301
- workspaceId: params.workspaceId,
302
- threadId: params.message.threadId,
303
- messageId: params.message.id,
304
- }),
305
- },
306
- }
307
- }
308
-
309
- async function collectThreadMessages(thread: Thread, incomingMessage: Message): Promise<Message[]> {
310
- try {
311
- const messages: Message[] = []
312
- for await (const message of thread.allMessages) {
313
- messages.push(message)
314
- }
315
- return messages.length > 0 ? messages : [incomingMessage]
316
- } catch {
317
- return thread.recentMessages.length > 0 ? [...thread.recentMessages] : [incomingMessage]
318
- }
319
- }
320
-
321
110
  function buildBuildToolsParams(params: {
322
111
  agentId: string
323
112
  context: LotaSocialChatResolvedContext
@@ -395,7 +184,7 @@ export function createSocialChatRuntime(params: {
395
184
  threadId: thread.id,
396
185
  messageId: incomingMessage.id,
397
186
  text: incomingMessage.text.trim(),
398
- authorId: toOptionalTrimmedString(incomingMessage.author.userId),
187
+ authorId: toOptionalTrimmedString(incomingMessage.author.userId) ?? undefined,
399
188
  authorName: readSlackAuthorName(incomingMessage),
400
189
  }
401
190
  aiLogger.info`Slack social-chat message received: channelId=${messageContext.channelId}, threadId=${messageContext.threadId}, messageId=${messageContext.messageId}, author=${messageContext.authorName ?? 'unknown'}, textLength=${messageContext.text.length}`
@@ -437,7 +226,7 @@ export function createSocialChatRuntime(params: {
437
226
  ? historyBeforeReply.filter((message) => message.cursor.id !== currentUserMessage.cursor.id)
438
227
  : historyBeforeReply
439
228
 
440
- const workspaceProvider = getRuntimeAdapters().services?.workspaceProvider
229
+ const workspaceProvider = getRuntimeAdapters().workspaceProvider
441
230
  const workspace = workspaceProvider ? await workspaceProvider.getWorkspace(resolvedContext.workspaceId) : {}
442
231
  const lifecycleState = workspaceProvider ? await workspaceProvider.getLifecycleState?.(workspace) : undefined
443
232
  const workspaceProfileState = workspaceProvider
@@ -453,9 +242,10 @@ export function createSocialChatRuntime(params: {
453
242
 
454
243
  const promptContext = buildAgentPromptContext({
455
244
  workspaceName:
456
- workspaceProfileState?.workspaceName ?? toOptionalTrimmedString((workspace as { name?: unknown }).name),
245
+ workspaceProfileState?.workspaceName ??
246
+ toOptionalTrimmedString((workspace as { name?: unknown }).name) ??
247
+ undefined,
457
248
  summaryBlock: workspaceProfileState?.summaryBlock,
458
- structuredProfile: workspaceProfileState?.structuredProfile,
459
249
  promptSummary,
460
250
  userName: messageContext.authorName,
461
251
  recentDomainEvents,
@@ -492,23 +282,6 @@ export function createSocialChatRuntime(params: {
492
282
  ])
493
283
 
494
284
  let memoryBlock = ''
495
- const runtimeConfig = getAgentRuntimeConfig({
496
- agentId: socialAgentId,
497
- workstreamMode: 'group',
498
- mode: 'workstreamMode',
499
- onboardingActive: lifecycleState?.bootstrapActive ?? false,
500
- linearInstalled: false,
501
- systemWorkspaceDetails: promptContext.systemWorkspaceDetails,
502
- preSeededMemoriesSection,
503
- retrievedKnowledgeSection,
504
- learnedSkillsSection,
505
- additionalInstructionSections: [buildSocialChatIdentitySection(socialAgentDisplayName)],
506
- }) as Record<string, unknown>
507
- const agentFactory = createAgent[runtimeConfig.id as string]
508
- if (typeof agentFactory !== 'function') {
509
- throw new Error(`Social chat agent factory is not configured for ${String(runtimeConfig.id)}`)
510
- }
511
-
512
285
  const consultParticipants = [
513
286
  ...new Set(
514
287
  (await socialChatConfig.getConsultParticipants?.({
@@ -520,7 +293,7 @@ export function createSocialChatRuntime(params: {
520
293
  ].filter((agentId) => agentId !== socialAgentId)
521
294
  const executedToolNames: string[] = []
522
295
 
523
- const baseTools = withLoggedToolSet(
296
+ const baseTools = withLoggedSocialToolSet(
524
297
  await socialChatConfig.buildAgentTools(
525
298
  buildBuildToolsParams({
526
299
  agentId: socialAgentId,
@@ -542,7 +315,7 @@ export function createSocialChatRuntime(params: {
542
315
  },
543
316
  )
544
317
 
545
- const transcript = buildThreadTranscript(historyBeforeReply)
318
+ const transcript = buildSocialChatThreadTranscript(historyBeforeReply)
546
319
  const runAbort = createServerRunAbortController()
547
320
 
548
321
  const consultSpecialistTool = createTool({
@@ -553,77 +326,66 @@ export function createSocialChatRuntime(params: {
553
326
  throw new Error(`Agent "${agentId}" is not an allowed social-chat specialist.`)
554
327
  }
555
328
 
556
- let specialistMemoryBlock = ''
557
- const specialistTools = withLoggedToolSet(
558
- await socialChatConfig.buildAgentTools(
559
- buildBuildToolsParams({
560
- agentId,
561
- context: resolvedContext,
562
- workspaceIdString,
563
- userIdString,
564
- messageContext,
565
- memoryBlock: specialistMemoryBlock,
566
- onAppendMemoryBlock: (value: string) => {
567
- specialistMemoryBlock = value
568
- },
569
- }),
570
- ),
571
- { agentId, channelId: messageContext.channelId, threadId: messageContext.threadId, executedToolNames },
572
- )
573
- const [specialistPreSeededMemories, specialistLearnedSkills] = await Promise.all([
574
- memoryService.getTopMemories({
575
- orgId: workspaceIdString,
576
- agentName: agentId,
577
- limit: PRESEEDED_MEMORY_LOOKUP_LIMIT,
578
- }),
579
- lifecycleState?.bootstrapActive
580
- ? Promise.resolve(undefined)
581
- : learnedSkillService
582
- .retrieveForTurn({ orgId: workspaceIdString, agentId, query: task, limit: 3, minConfidence: 0.6 })
583
- .catch((error) => {
584
- aiLogger.warn`Failed to retrieve learned skills for ${agentId}: ${error}`
585
- return undefined
329
+ const { result: specialistRun } = await runSpecialistSession({
330
+ initialMemoryBlock: '',
331
+ buildTools: async ({ memoryBlock: currentMemoryBlock, onAppendMemoryBlock }) =>
332
+ withLoggedSocialToolSet(
333
+ await socialChatConfig.buildAgentTools(
334
+ buildBuildToolsParams({
335
+ agentId,
336
+ context: resolvedContext,
337
+ workspaceIdString,
338
+ userIdString,
339
+ messageContext,
340
+ memoryBlock: currentMemoryBlock,
341
+ onAppendMemoryBlock,
586
342
  }),
587
- ])
588
-
589
- const specialistRuntimeConfig = getAgentRuntimeConfig({
590
- agentId,
591
- workstreamMode: 'group',
592
- mode: 'fixedWorkstreamMode',
593
- onboardingActive: lifecycleState?.bootstrapActive ?? false,
594
- linearInstalled: false,
595
- systemWorkspaceDetails: promptContext.systemWorkspaceDetails,
596
- preSeededMemoriesSection: specialistPreSeededMemories,
597
- retrievedKnowledgeSection,
598
- learnedSkillsSection: specialistLearnedSkills,
599
- additionalInstructionSections: [
600
- `You are supporting ${socialAgentDisplayName} in a Slack social-chat thread. Stay within your role.`,
601
- ],
602
- }) as Record<string, unknown>
603
- const specialistFactory = createAgent[specialistRuntimeConfig.id as string]
604
- if (typeof specialistFactory !== 'function') {
605
- throw new Error(`Social specialist agent factory is not configured for ${String(specialistRuntimeConfig.id)}`)
606
- }
607
-
608
- const specialistAgent = specialistFactory({
609
- mode: 'fixedWorkstreamMode',
610
- tools: specialistTools,
611
- extraInstructions: specialistRuntimeConfig.extraInstructions,
612
- stopWhen: [stepCountIs(specialistRuntimeConfig.maxSteps as number)],
613
- }) as SocialChatAgent
614
- const specialistResponse = await specialistAgent.generate({
615
- prompt: buildSpecialistPrompt({
616
- requesterName: socialAgentDisplayName,
617
- agentName: agentDisplayNames[agentId] ?? agentId,
618
- task,
619
- transcript,
620
- }),
621
- abortSignal: runAbort.signal,
343
+ ),
344
+ { agentId, channelId: messageContext.channelId, threadId: messageContext.threadId, executedToolNames },
345
+ ),
346
+ run: async ({ tools }) => {
347
+ const [specialistPreSeededMemories, specialistLearnedSkills] = await Promise.all([
348
+ memoryService.getTopMemories({
349
+ orgId: workspaceIdString,
350
+ agentName: agentId,
351
+ limit: PRESEEDED_MEMORY_LOOKUP_LIMIT,
352
+ }),
353
+ lifecycleState?.bootstrapActive
354
+ ? Promise.resolve(undefined)
355
+ : learnedSkillService
356
+ .retrieveForTurn({ orgId: workspaceIdString, agentId, query: task, limit: 3, minConfidence: 0.6 })
357
+ .catch((error) => {
358
+ aiLogger.warn`Failed to retrieve learned skills for ${agentId}: ${error}`
359
+ return undefined
360
+ }),
361
+ ])
362
+
363
+ return await runSocialAgentTurn({
364
+ agentId,
365
+ mode: 'fixedWorkstreamMode',
366
+ workstreamMode: 'group',
367
+ onboardingActive: lifecycleState?.bootstrapActive ?? false,
368
+ linearInstalled: false,
369
+ systemWorkspaceDetails: promptContext.systemWorkspaceDetails,
370
+ preSeededMemoriesSection: specialistPreSeededMemories,
371
+ retrievedKnowledgeSection,
372
+ learnedSkillsSection: specialistLearnedSkills,
373
+ userMessageText: task,
374
+ additionalInstructionSections: [
375
+ `You are supporting ${socialAgentDisplayName} in a Slack social-chat thread. Stay within your role.`,
376
+ ],
377
+ tools,
378
+ prompt: buildSpecialistSocialChatPrompt({
379
+ requesterName: socialAgentDisplayName,
380
+ agentName: agentDisplayNames[agentId] ?? agentId,
381
+ task,
382
+ transcript,
383
+ }),
384
+ abortSignal: runAbort.signal,
385
+ })
386
+ },
622
387
  })
623
- const text = specialistResponse.text.trim()
624
- if (!text) {
625
- throw new Error(`Specialist ${agentId} returned an empty response.`)
626
- }
388
+ const text = specialistRun.text
627
389
 
628
390
  return createAssistantMessage({ agentId, agentName: agentDisplayNames[agentId] ?? agentId, text })
629
391
  },
@@ -641,17 +403,22 @@ export function createSocialChatRuntime(params: {
641
403
  },
642
404
  })
643
405
 
644
- const agent = agentFactory({
645
- mode: 'workstreamMode',
646
- tools: { ...baseTools, [CONSULT_SPECIALIST_TOOL_NAME]: consultSpecialistTool },
647
- extraInstructions: runtimeConfig.extraInstructions,
648
- stopWhen: [stepCountIs(runtimeConfig.maxSteps as number)],
649
- }) as SocialChatAgent
650
-
651
406
  await thread.startTyping('Thinking...').catch(() => undefined)
652
407
  aiLogger.info`Slack social-chat generating reply: channelId=${messageContext.channelId}, threadId=${messageContext.threadId}`
653
- const response = await agent.generate({
654
- prompt: buildLeadAgentPrompt({
408
+ const leadRun = await runSocialAgentTurn({
409
+ agentId: socialAgentId,
410
+ mode: 'workstreamMode',
411
+ workstreamMode: 'group',
412
+ onboardingActive: lifecycleState?.bootstrapActive ?? false,
413
+ linearInstalled: false,
414
+ systemWorkspaceDetails: promptContext.systemWorkspaceDetails,
415
+ preSeededMemoriesSection,
416
+ retrievedKnowledgeSection,
417
+ learnedSkillsSection,
418
+ userMessageText: messageContext.text,
419
+ additionalInstructionSections: [buildSocialChatIdentitySection(socialAgentDisplayName)],
420
+ tools: { ...baseTools, [CONSULT_SPECIALIST_TOOL_NAME]: consultSpecialistTool },
421
+ prompt: buildLeadSocialChatPrompt({
655
422
  agentDisplayName: socialAgentDisplayName,
656
423
  channelId: messageContext.channelId,
657
424
  threadId: messageContext.threadId,
@@ -661,10 +428,7 @@ export function createSocialChatRuntime(params: {
661
428
  }),
662
429
  abortSignal: runAbort.signal,
663
430
  })
664
- const responseText = response.text.trim()
665
- if (!responseText) {
666
- throw new Error('Social chat agent returned an empty response.')
667
- }
431
+ const responseText = leadRun.text
668
432
 
669
433
  const replyMarkdown = buildSlackSocialReplyMarkdown({ replyMarkdown: responseText, executedToolNames })
670
434
  const sentMessage = await thread.post({ markdown: replyMarkdown })
@@ -690,7 +454,7 @@ export function createSocialChatRuntime(params: {
690
454
  {
691
455
  orgId: workspaceIdString,
692
456
  workstreamId: `social:slack:${messageContext.threadId}`,
693
- sourceId: createCursorId({
457
+ sourceId: createSocialChatCursorId({
694
458
  workspaceId: workspaceIdString,
695
459
  threadId: messageContext.threadId,
696
460
  messageId: messageContext.messageId,
@@ -0,0 +1,18 @@
1
+ import type { ToolSet } from 'ai'
2
+
3
+ export async function runSpecialistSession<TResult>(params: {
4
+ initialMemoryBlock: string
5
+ buildTools: (options: { memoryBlock: string; onAppendMemoryBlock: (value: string) => void }) => Promise<ToolSet>
6
+ run: (options: { tools: ToolSet; memoryBlock: string }) => Promise<TResult>
7
+ }): Promise<{ result: TResult; memoryBlock: string }> {
8
+ let specialistMemoryBlock = params.initialMemoryBlock
9
+ const tools = await params.buildTools({
10
+ memoryBlock: specialistMemoryBlock,
11
+ onAppendMemoryBlock: (value) => {
12
+ specialistMemoryBlock = value
13
+ },
14
+ })
15
+
16
+ const result = await params.run({ tools, memoryBlock: specialistMemoryBlock })
17
+ return { result, memoryBlock: specialistMemoryBlock }
18
+ }
@@ -7,6 +7,25 @@ export interface WorkstreamHistoryMessage {
7
7
  agentName?: string
8
8
  }
9
9
 
10
+ export function asRecord(value: unknown): Record<string, unknown> | null {
11
+ return value && typeof value === 'object' ? (value as Record<string, unknown>) : null
12
+ }
13
+
14
+ export function readOptionalString(value: unknown): string | undefined {
15
+ return typeof value === 'string' && value.trim().length > 0 ? value : undefined
16
+ }
17
+
18
+ export function readInstructionSections(value: unknown): string[] {
19
+ if (!Array.isArray(value)) {
20
+ return []
21
+ }
22
+
23
+ return value
24
+ .filter((section): section is string => typeof section === 'string')
25
+ .map((section) => section.trim())
26
+ .filter((section) => section.length > 0)
27
+ }
28
+
10
29
  function getAgentName(message: ChatMessageLike): string | undefined {
11
30
  const metadata = message.metadata
12
31
  if (!metadata || typeof metadata !== 'object') return undefined