@lota-sdk/core 0.1.9 → 0.1.12

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 (105) hide show
  1. package/infrastructure/schema/00_workstream.surql +1 -0
  2. package/infrastructure/schema/02_execution_plan.surql +202 -52
  3. package/package.json +4 -87
  4. package/src/ai/index.ts +3 -0
  5. package/src/bifrost/bifrost.ts +94 -25
  6. package/src/bifrost/index.ts +1 -0
  7. package/src/config/agent-defaults.ts +30 -7
  8. package/src/config/constants.ts +0 -9
  9. package/src/config/debug-logger.ts +43 -0
  10. package/src/config/index.ts +5 -0
  11. package/src/config/model-constants.ts +8 -9
  12. package/src/config/workstream-defaults.ts +4 -0
  13. package/src/db/cursor-pagination.ts +2 -2
  14. package/src/db/index.ts +10 -0
  15. package/src/db/memory-store.ts +3 -71
  16. package/src/db/memory.ts +9 -15
  17. package/src/db/service.ts +42 -2
  18. package/src/db/tables.ts +9 -2
  19. package/src/document/index.ts +2 -0
  20. package/src/document/parsing.ts +0 -25
  21. package/src/embeddings/provider.ts +102 -22
  22. package/src/index.ts +15 -499
  23. package/src/queues/index.ts +10 -0
  24. package/src/redis/connection-accessor.ts +26 -0
  25. package/src/redis/connection.ts +1 -1
  26. package/src/redis/index.ts +9 -25
  27. package/src/redis/org-memory-lock.ts +1 -1
  28. package/src/redis/redis-lease-lock.ts +1 -1
  29. package/src/redis/stream-context.ts +54 -0
  30. package/src/runtime/agent-runtime-policy.ts +9 -5
  31. package/src/runtime/agent-stream-helpers.ts +6 -3
  32. package/src/runtime/agent-types.ts +1 -5
  33. package/src/runtime/approval-continuation.ts +68 -1
  34. package/src/runtime/chat-attachments.ts +1 -1
  35. package/src/runtime/chat-request-routing.ts +6 -2
  36. package/src/runtime/context-compaction-runtime.ts +2 -2
  37. package/src/runtime/context-compaction.ts +1 -1
  38. package/src/runtime/execution-plan.ts +22 -15
  39. package/src/runtime/index.ts +26 -0
  40. package/src/runtime/indexed-repositories-policy.ts +10 -10
  41. package/src/runtime/memory-pipeline.ts +0 -2
  42. package/src/runtime/runtime-config.ts +238 -0
  43. package/src/runtime/runtime-extensions.ts +3 -2
  44. package/src/runtime/runtime-worker-registry.ts +47 -0
  45. package/src/runtime/team-consultation-orchestrator.ts +9 -6
  46. package/src/runtime/team-consultation-prompts.ts +3 -2
  47. package/src/runtime/turn-lifecycle.ts +13 -5
  48. package/src/runtime/workstream-chat-helpers.ts +0 -54
  49. package/src/runtime/workstream-routing-policy.ts +3 -7
  50. package/src/runtime.ts +387 -0
  51. package/src/services/chat-attachments.service.ts +1 -1
  52. package/src/services/context-compaction.service.ts +1 -1
  53. package/src/services/document-chunk.service.ts +2 -2
  54. package/src/services/execution-plan.service.ts +584 -793
  55. package/src/services/index.ts +14 -0
  56. package/src/services/learned-skill.service.ts +82 -39
  57. package/src/services/memory.service.ts +5 -4
  58. package/src/services/mutating-approval.service.ts +1 -1
  59. package/src/services/organization-member.service.ts +1 -1
  60. package/src/services/organization.service.ts +1 -1
  61. package/src/services/plan-approval.service.ts +83 -0
  62. package/src/services/plan-artifact.service.ts +44 -0
  63. package/src/services/plan-builder.service.ts +61 -0
  64. package/src/services/plan-checkpoint.service.ts +53 -0
  65. package/src/services/plan-compiler.service.ts +81 -0
  66. package/src/services/plan-executor.service.ts +1624 -0
  67. package/src/services/plan-run.service.ts +422 -0
  68. package/src/services/plan-validator.service.ts +760 -0
  69. package/src/services/recent-activity-title.service.ts +1 -1
  70. package/src/services/recent-activity.service.ts +14 -16
  71. package/src/services/user.service.ts +2 -2
  72. package/src/services/workstream-message.service.ts +2 -3
  73. package/src/services/workstream-title.service.ts +1 -1
  74. package/src/services/workstream-turn-preparation.ts +156 -59
  75. package/src/services/workstream-turn.ts +26 -1
  76. package/src/services/workstream.service.ts +35 -9
  77. package/src/services/workstream.types.ts +1 -0
  78. package/src/storage/attachment-parser.ts +1 -1
  79. package/src/storage/attachment-storage.service.ts +11 -10
  80. package/src/storage/generated-document-storage.service.ts +7 -6
  81. package/src/storage/index.ts +10 -0
  82. package/src/system-agents/delegated-agent-factory.ts +78 -29
  83. package/src/system-agents/index.ts +4 -0
  84. package/src/system-agents/recent-activity-title-refiner.agent.ts +38 -3
  85. package/src/system-agents/regular-chat-memory-digest.agent.ts +1 -1
  86. package/src/system-agents/skill-extractor.agent.ts +1 -1
  87. package/src/system-agents/skill-manager.agent.ts +2 -4
  88. package/src/system-agents/title-generator.agent.ts +2 -2
  89. package/src/tools/execution-plan.tool.ts +22 -48
  90. package/src/tools/firecrawl-client.ts +2 -2
  91. package/src/tools/index.ts +12 -0
  92. package/src/tools/log-hello-world.tool.ts +17 -0
  93. package/src/tools/research-topic.tool.ts +1 -1
  94. package/src/tools/team-think.tool.ts +1 -1
  95. package/src/tools/user-questions.tool.ts +2 -2
  96. package/src/utils/index.ts +6 -0
  97. package/src/workers/bootstrap.ts +8 -16
  98. package/src/workers/index.ts +7 -0
  99. package/src/workers/regular-chat-memory-digest.runner.ts +1 -1
  100. package/src/workers/skill-extraction.runner.ts +3 -3
  101. package/src/workers/utils/{repo-indexer-chunker.ts → file-section-chunker.ts} +23 -52
  102. package/src/workers/utils/repo-structure-extractor.ts +2 -5
  103. package/src/workers/utils/repomix-file-sections.ts +42 -0
  104. package/src/config/env-shapes.ts +0 -121
  105. package/src/runtime/agent-contract.ts +0 -1
@@ -1,4 +1,4 @@
1
- import { WORKSTREAM } from '@lota-sdk/shared/constants/workstream'
1
+ import { WORKSTREAM } from '@lota-sdk/shared'
2
2
  import { BoundQuery, RecordId, StringRecordId, surql } from 'surrealdb'
3
3
 
4
4
  import { agentDisplayNames, getCoreWorkstreamProfile, isAgentName } from '../config/agent-defaults'
@@ -345,19 +345,19 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
345
345
 
346
346
  const onboardingWelcome = bootstrapConfig.onboardingWelcome
347
347
  if (!onboardingCompleted && onboardingWelcome) {
348
- const createdChiefWorkstream = createdWorkstreams.find(
348
+ const createdOnboardingOwnerWorkstream = createdWorkstreams.find(
349
349
  (workstream) => workstream.mode === 'direct' && workstream.agentId === onboardingWelcome.directAgentId,
350
350
  )
351
- const existingChiefWorkstream = directWorkstreamsByAgent.get(onboardingWelcome.directAgentId)
351
+ const existingOnboardingOwnerWorkstream = directWorkstreamsByAgent.get(onboardingWelcome.directAgentId)
352
352
 
353
- const chiefWorkstreamId =
354
- createdChiefWorkstream?.id ??
355
- (existingChiefWorkstream ? this.normalizeWorkstreamId(existingChiefWorkstream.id) : null)
353
+ const onboardingOwnerWorkstreamId =
354
+ createdOnboardingOwnerWorkstream?.id ??
355
+ (existingOnboardingOwnerWorkstream ? this.normalizeWorkstreamId(existingOnboardingOwnerWorkstream.id) : null)
356
356
 
357
- if (chiefWorkstreamId) {
358
- const chiefWorkstreamRef = ensureRecordId(chiefWorkstreamId, TABLES.WORKSTREAM)
357
+ if (onboardingOwnerWorkstreamId) {
358
+ const onboardingOwnerWorkstreamRef = ensureRecordId(onboardingOwnerWorkstreamId, TABLES.WORKSTREAM)
359
359
  await workstreamMessageService.ensureBootstrapWelcomeMessage({
360
- workstreamId: chiefWorkstreamRef,
360
+ workstreamId: onboardingOwnerWorkstreamRef,
361
361
  agentId: onboardingWelcome.directAgentId,
362
362
  text: onboardingWelcome.buildMessageText({ userName: options?.userName }),
363
363
  })
@@ -504,6 +504,32 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
504
504
  await this.setActiveRunId(workstreamId, null)
505
505
  }
506
506
 
507
+ async setActiveStreamId(workstreamId: RecordIdRef, streamId: string): Promise<void> {
508
+ const workstreamRef = ensureRecordId(workstreamId, TABLES.WORKSTREAM)
509
+ await databaseService.query<unknown>(surql`
510
+ UPDATE ONLY ${workstreamRef}
511
+ SET activeStreamId = ${streamId}
512
+ `)
513
+ }
514
+
515
+ async getActiveStreamId(workstreamId: RecordIdRef): Promise<string | null> {
516
+ const workstream = await this.getById(workstreamId)
517
+ const activeStreamId = workstream.activeStreamId
518
+ if (typeof activeStreamId !== 'string') return null
519
+ const normalized = activeStreamId.trim()
520
+ return normalized.length > 0 ? normalized : null
521
+ }
522
+
523
+ async clearActiveStreamIdIfMatches(workstreamId: RecordIdRef, streamId: string): Promise<void> {
524
+ const activeStreamId = await this.getActiveStreamId(workstreamId)
525
+ if (activeStreamId !== streamId) return
526
+ const workstreamRef = ensureRecordId(workstreamId, TABLES.WORKSTREAM)
527
+ await databaseService.query<unknown>(surql`
528
+ UPDATE ONLY ${workstreamRef}
529
+ SET activeStreamId = NONE
530
+ `)
531
+ }
532
+
507
533
  async stopActiveRun(workstreamId: RecordIdRef): Promise<boolean> {
508
534
  const activeRunId = await this.getActiveRunId(workstreamId)
509
535
  if (!activeRunId) return false
@@ -42,6 +42,7 @@ export const WorkstreamSchema = z.object({
42
42
  memoryBlock: z.string().nullish(),
43
43
  memoryBlockSummary: z.string().nullish(),
44
44
  activeRunId: z.string().nullish(),
45
+ activeStreamId: z.string().nullish(),
45
46
  compactionSummary: z.string().nullish(),
46
47
  lastCompactedMessageId: z.string().nullish(),
47
48
  nameGenerated: z.boolean().optional().default(false),
@@ -1,4 +1,4 @@
1
- import { SUPPORTED_ATTACHMENT_MIME_TYPES, getAttachmentExtension } from '@lota-sdk/shared/constants/attachments'
1
+ import { SUPPORTED_ATTACHMENT_MIME_TYPES, getAttachmentExtension } from '@lota-sdk/shared'
2
2
  import mammoth from 'mammoth'
3
3
  import { PDFParse } from 'pdf-parse'
4
4
 
@@ -1,8 +1,8 @@
1
- import { inferContentType } from '@lota-sdk/shared/constants/attachments'
1
+ import { inferContentType } from '@lota-sdk/shared'
2
2
  import { S3Client } from 'bun'
3
3
 
4
- import { env } from '../config/env-shapes'
5
4
  import { serverLogger } from '../config/logger'
5
+ import { getRuntimeConfig } from '../runtime/runtime-config'
6
6
  import { readString } from '../utils/string'
7
7
  import {
8
8
  extractAttachmentText,
@@ -41,17 +41,18 @@ export class AttachmentStorageService {
41
41
  private readonly client: S3Client
42
42
 
43
43
  constructor() {
44
+ const config = getRuntimeConfig()
44
45
  this.client = new S3Client({
45
- accessKeyId: env.S3_ACCESS_KEY_ID,
46
- secretAccessKey: env.S3_SECRET_ACCESS_KEY,
47
- bucket: env.S3_BUCKET,
48
- endpoint: env.S3_ENDPOINT,
49
- region: env.S3_REGION,
46
+ accessKeyId: config.s3.accessKeyId,
47
+ secretAccessKey: config.s3.secretAccessKey,
48
+ bucket: config.s3.bucket,
49
+ endpoint: config.s3.endpoint,
50
+ region: config.s3.region,
50
51
  })
51
52
  }
52
53
 
53
54
  getAttachmentUrl(storageKey: string): string {
54
- return this.client.file(storageKey).presign({ expiresIn: env.ATTACHMENT_URL_EXPIRES_IN })
55
+ return this.client.file(storageKey).presign({ expiresIn: getRuntimeConfig().s3.attachmentUrlExpiresIn })
55
56
  }
56
57
 
57
58
  async writeOrganizationDocument({
@@ -102,7 +103,7 @@ export class AttachmentStorageService {
102
103
  sizeBytes: file.size,
103
104
  storageKey,
104
105
  url: this.getAttachmentUrl(storageKey),
105
- expiresInSeconds: env.ATTACHMENT_URL_EXPIRES_IN,
106
+ expiresInSeconds: getRuntimeConfig().s3.attachmentUrlExpiresIn,
106
107
  }
107
108
  }
108
109
 
@@ -128,7 +129,7 @@ export class AttachmentStorageService {
128
129
  sizeBytes,
129
130
  storageKey,
130
131
  url: this.getAttachmentUrl(storageKey),
131
- expiresInSeconds: env.ATTACHMENT_URL_EXPIRES_IN,
132
+ expiresInSeconds: getRuntimeConfig().s3.attachmentUrlExpiresIn,
132
133
  }
133
134
  }
134
135
 
@@ -1,6 +1,6 @@
1
1
  import { S3Client } from 'bun'
2
2
 
3
- import { env } from '../config/env-shapes'
3
+ import { getRuntimeConfig } from '../runtime/runtime-config'
4
4
 
5
5
  function toSafeSegment(value: string): string {
6
6
  return value
@@ -27,12 +27,13 @@ class GeneratedDocumentStorageService {
27
27
 
28
28
  private get client(): InstanceType<typeof S3Client> {
29
29
  if (!this._client) {
30
+ const config = getRuntimeConfig()
30
31
  this._client = new S3Client({
31
- accessKeyId: env.S3_ACCESS_KEY_ID,
32
- secretAccessKey: env.S3_SECRET_ACCESS_KEY,
33
- bucket: env.S3_BUCKET,
34
- endpoint: env.S3_ENDPOINT,
35
- region: env.S3_REGION,
32
+ accessKeyId: config.s3.accessKeyId,
33
+ secretAccessKey: config.s3.secretAccessKey,
34
+ bucket: config.s3.bucket,
35
+ endpoint: config.s3.endpoint,
36
+ region: config.s3.region,
36
37
  })
37
38
  }
38
39
  return this._client
@@ -0,0 +1,10 @@
1
+ export * from './attachment-parser'
2
+ export * from './attachment-storage.service'
3
+ export * from './generated-document-storage.service'
4
+ export type { MessagePartLike, ReadableUploadPageMode, ReadableUploadPagePart } from './attachments.types'
5
+ export {
6
+ buildOrganizationDocumentStorageKey,
7
+ buildUploadStorageKey,
8
+ buildUploadStoragePrefix,
9
+ readNonNegativeInteger,
10
+ } from './attachments.utils'
@@ -7,11 +7,12 @@ import { isRecord } from '../utils/string'
7
7
  import { assertSubstantiveAgentResult } from './agent-result'
8
8
 
9
9
  type AgentProviderOptions = ToolLoopAgentSettings['providerOptions']
10
+ type AgentModel = LanguageModel | (() => LanguageModel)
10
11
 
11
12
  interface DelegatedAgentDefinition {
12
13
  id: string
13
14
  description: string
14
- model: LanguageModel
15
+ model: AgentModel
15
16
  providerOptions?: AgentProviderOptions
16
17
  instructions: string
17
18
  tools?: ToolSet
@@ -24,6 +25,10 @@ interface DelegatedAgentDefinitionWithContext<TContext> extends Omit<DelegatedAg
24
25
  createTools: (context: TContext) => ToolSet
25
26
  }
26
27
 
28
+ function resolveAgentModel(model: AgentModel): LanguageModel {
29
+ return typeof model === 'function' ? model() : model
30
+ }
31
+
27
32
  function buildCurrentDateContext(now = new Date()): string {
28
33
  const isoDate = now.toISOString().slice(0, 10)
29
34
  const humanDate = new Intl.DateTimeFormat('en-US', {
@@ -75,6 +80,9 @@ When your analysis is complete, return your final answer directly as markdown te
75
80
  </termination>`
76
81
 
77
82
  const MAX_RETAINED_AGENT_MESSAGES = 10
83
+ const MAX_NON_SUBSTANTIVE_AGENT_RESULT_ATTEMPTS = 2
84
+ const NON_SUBSTANTIVE_AGENT_RESULT_RETRY_PROMPT =
85
+ 'Return a complete substantive markdown answer. Do not reply with an empty result, placeholder, or tool-only outcome.'
78
86
 
79
87
  export function retainCriticalAgentMessages(messages: ModelMessage[]): ModelMessage[] {
80
88
  const systemMessages = messages.filter((message) => message.role === 'system')
@@ -112,6 +120,31 @@ function resolveDelegatedAgentTemperature(definition: {
112
120
  return definition.temperature ?? 0.2
113
121
  }
114
122
 
123
+ async function generateSubstantiveDelegatedAgentResult(params: {
124
+ label: string
125
+ task: string
126
+ abortSignal?: AbortSignal
127
+ createAgent: () => ToolLoopAgent<never, ToolSet>
128
+ }): Promise<string> {
129
+ let lastError: unknown
130
+
131
+ for (let attempt = 0; attempt < MAX_NON_SUBSTANTIVE_AGENT_RESULT_ATTEMPTS; attempt += 1) {
132
+ const prompt = attempt === 0 ? params.task : `${params.task}\n\n${NON_SUBSTANTIVE_AGENT_RESULT_RETRY_PROMPT}`
133
+ const result = await params.createAgent().generate({ prompt, abortSignal: params.abortSignal })
134
+
135
+ try {
136
+ return assertSubstantiveAgentResult(result.text, params.label)
137
+ } catch (error) {
138
+ lastError = error
139
+ if (params.abortSignal?.aborted) {
140
+ throw error
141
+ }
142
+ }
143
+ }
144
+
145
+ throw lastError ?? new Error(`${params.label} must contain substantive text.`)
146
+ }
147
+
115
148
  export function createDelegatedAgentTool(definition: DelegatedAgentDefinition): ToolDefinition<void> {
116
149
  const maxSteps = definition.maxSteps ?? 10
117
150
  const temperature = resolveDelegatedAgentTemperature(definition)
@@ -124,20 +157,28 @@ export function createDelegatedAgentTool(definition: DelegatedAgentDefinition):
124
157
  inputSchema: z.object({ task: z.string().min(1) }),
125
158
  execute: async ({ task }: { task: string }, { abortSignal }) => {
126
159
  const agentTools = definition.tools
127
- const agent = new ToolLoopAgent({
128
- id: definition.id,
129
- model: definition.model,
130
- ...(definition.providerOptions ? { providerOptions: definition.providerOptions } : {}),
131
- instructions: buildDelegatedAgentInstructions(definition.instructions, agentTools),
132
- tools: agentTools,
133
- maxOutputTokens: definition.maxOutputTokens ?? 4096,
134
- ...(typeof temperature === 'number' ? { temperature } : {}),
135
- stopWhen: [stepCountIs(maxSteps)],
136
- prepareStep: async ({ messages }) => ({ messages: retainCriticalAgentMessages(messages) }),
137
- })
138
-
139
- const result = await agent.generate({ prompt: task, abortSignal })
140
- return { task, result: assertSubstantiveAgentResult(result.text, `${definition.id} result`) }
160
+ const createAgent = () =>
161
+ new ToolLoopAgent({
162
+ id: definition.id,
163
+ model: resolveAgentModel(definition.model),
164
+ ...(definition.providerOptions ? { providerOptions: definition.providerOptions } : {}),
165
+ instructions: buildDelegatedAgentInstructions(definition.instructions, agentTools),
166
+ tools: agentTools,
167
+ maxOutputTokens: definition.maxOutputTokens ?? 4096,
168
+ ...(typeof temperature === 'number' ? { temperature } : {}),
169
+ stopWhen: [stepCountIs(maxSteps)],
170
+ prepareStep: async ({ messages }) => ({ messages: retainCriticalAgentMessages(messages) }),
171
+ })
172
+
173
+ return {
174
+ task,
175
+ result: await generateSubstantiveDelegatedAgentResult({
176
+ label: `${definition.id} result`,
177
+ task,
178
+ abortSignal,
179
+ createAgent,
180
+ }),
181
+ }
141
182
  },
142
183
  }),
143
184
  } as const satisfies ToolDefinition<void>
@@ -157,20 +198,28 @@ export function createDelegatedAgentToolWithContext<TContext>(
157
198
  inputSchema: z.object({ task: z.string().min(1) }),
158
199
  execute: async ({ task }: { task: string }, { abortSignal }) => {
159
200
  const agentTools = definition.createTools(context)
160
- const agent = new ToolLoopAgent({
161
- id: definition.id,
162
- model: definition.model,
163
- ...(definition.providerOptions ? { providerOptions: definition.providerOptions } : {}),
164
- instructions: buildDelegatedAgentInstructions(definition.instructions, agentTools),
165
- tools: agentTools,
166
- maxOutputTokens: definition.maxOutputTokens ?? 4096,
167
- ...(typeof temperature === 'number' ? { temperature } : {}),
168
- stopWhen: [stepCountIs(maxSteps)],
169
- prepareStep: async ({ messages }) => ({ messages: retainCriticalAgentMessages(messages) }),
170
- })
171
-
172
- const result = await agent.generate({ prompt: task, abortSignal })
173
- return { task, result: assertSubstantiveAgentResult(result.text, `${definition.id} result`) }
201
+ const createAgent = () =>
202
+ new ToolLoopAgent({
203
+ id: definition.id,
204
+ model: resolveAgentModel(definition.model),
205
+ ...(definition.providerOptions ? { providerOptions: definition.providerOptions } : {}),
206
+ instructions: buildDelegatedAgentInstructions(definition.instructions, agentTools),
207
+ tools: agentTools,
208
+ maxOutputTokens: definition.maxOutputTokens ?? 4096,
209
+ ...(typeof temperature === 'number' ? { temperature } : {}),
210
+ stopWhen: [stepCountIs(maxSteps)],
211
+ prepareStep: async ({ messages }) => ({ messages: retainCriticalAgentMessages(messages) }),
212
+ })
213
+
214
+ return {
215
+ task,
216
+ result: await generateSubstantiveDelegatedAgentResult({
217
+ label: `${definition.id} result`,
218
+ task,
219
+ abortSignal,
220
+ createAgent,
221
+ }),
222
+ }
174
223
  },
175
224
  }),
176
225
  } as const satisfies ToolDefinition<TContext>
@@ -0,0 +1,4 @@
1
+ export * from './agent-result'
2
+ export * from './delegated-agent-factory'
3
+ export * from './helper-agent-options'
4
+ export * from './recent-activity-title-refiner.agent'
@@ -1,6 +1,7 @@
1
1
  import { ToolLoopAgent } from 'ai'
2
2
 
3
3
  import { bifrostModel } from '../bifrost/bifrost'
4
+ import { getLeadAgentDisplayName } from '../config/agent-defaults'
4
5
  import {
5
6
  OPENROUTER_STRUCTURED_HELPER_MODEL_ID,
6
7
  OPENROUTER_HIGH_REASONING_PROVIDER_OPTIONS,
@@ -10,8 +11,42 @@ import { resolveHelperAgentOptions } from './helper-agent-options'
10
11
 
11
12
  const RECENT_ACTIVITY_TITLE_MAX_TOKENS = 256
12
13
 
14
+ export function buildRecentActivityTitleRefinerPrompt(): string {
15
+ const leadAgentDisplayName = getLeadAgentDisplayName() || 'the lead agent'
16
+
17
+ return `<agent-instructions>
18
+ You are ${leadAgentDisplayName} writing the visible title for a recent activity item.
19
+
20
+ <goal>
21
+ Turn recent activity context into a short label that helps the user quickly recognize and reopen the task.
22
+ </goal>
23
+
24
+ <rules>
25
+ - Write 4-10 words when possible.
26
+ - Prefer concrete task language over generic labels.
27
+ - Mention the agent only when it adds useful context.
28
+ - Focus on what the user was trying to accomplish.
29
+ - Avoid generic phrases like "Chat", "Conversation", "Recent activity", "Task", or "Workstream update".
30
+ - Avoid punctuation at the end.
31
+ - Do not invent details that are not present in the input.
32
+ - If the input is too weak for improvement, return the current system title.
33
+ </rules>
34
+
35
+ <examples>
36
+ - "Review pending decisions with ${leadAgentDisplayName}"
37
+ - "Extract website intelligence findings"
38
+ - "Discuss onboarding repository indexing"
39
+ - "Review PRD from GitHub indexing"
40
+ </examples>
41
+
42
+ <output>
43
+ Return only the title text. No quotes, labels, JSON, markdown, or explanation.
44
+ </output>
45
+ </agent-instructions>`
46
+ }
47
+
13
48
  export const recentActivityTitleRefinerPrompt = `<agent-instructions>
14
- You are the Chief of Staff writing the visible title for a recent activity item.
49
+ You are the lead agent writing the visible title for a recent activity item.
15
50
 
16
51
  <goal>
17
52
  Turn recent activity context into a short label that helps the user quickly recognize and reopen the task.
@@ -29,7 +64,7 @@ Turn recent activity context into a short label that helps the user quickly reco
29
64
  </rules>
30
65
 
31
66
  <examples>
32
- - "Review pending decisions with Chief"
67
+ - "Review pending decisions with the lead agent"
33
68
  - "Extract website intelligence findings"
34
69
  - "Discuss onboarding repository indexing"
35
70
  - "Review PRD from GitHub indexing"
@@ -46,7 +81,7 @@ export function createRecentActivityTitleRefinerAgent(options: CreateHelperToolL
46
81
  model: bifrostModel(OPENROUTER_STRUCTURED_HELPER_MODEL_ID),
47
82
  providerOptions: OPENROUTER_HIGH_REASONING_PROVIDER_OPTIONS,
48
83
  ...resolveHelperAgentOptions(options, {
49
- instructions: recentActivityTitleRefinerPrompt,
84
+ instructions: buildRecentActivityTitleRefinerPrompt(),
50
85
  maxOutputTokens: RECENT_ACTIVITY_TITLE_MAX_TOKENS,
51
86
  }),
52
87
  })
@@ -10,7 +10,7 @@ import { resolveHelperAgentOptions } from './helper-agent-options'
10
10
 
11
11
  const REGULAR_CHAT_MEMORY_DIGEST_MAX_TOKENS = 8_192
12
12
 
13
- export const regularChatMemoryDigestPrompt = `<agent-instructions>
13
+ const regularChatMemoryDigestPrompt = `<agent-instructions>
14
14
  You are the regular-chat memory digest synthesizer.
15
15
 
16
16
  <goal>
@@ -11,7 +11,7 @@ import { resolveHelperAgentOptions } from './helper-agent-options'
11
11
 
12
12
  const SKILL_EXTRACTOR_MAX_TOKENS = 8_192
13
13
 
14
- export const skillExtractorPrompt = `<agent-instructions>
14
+ const skillExtractorPrompt = `<agent-instructions>
15
15
  You are the skill extractor.
16
16
 
17
17
  <goal>
@@ -11,7 +11,7 @@ import { resolveHelperAgentOptions } from './helper-agent-options'
11
11
 
12
12
  const SKILL_MANAGER_MAX_TOKENS = 4_096
13
13
 
14
- export const skillManagerPrompt = `<agent-instructions>
14
+ const skillManagerPrompt = `<agent-instructions>
15
15
  You are the skill manager.
16
16
 
17
17
  <goal>
@@ -49,7 +49,7 @@ Return a JSON object with:
49
49
  </output-contract>
50
50
  </agent-instructions>`
51
51
 
52
- export const MergedSkillSchema = z.object({
52
+ const MergedSkillSchema = z.object({
53
53
  name: z.string(),
54
54
  description: z.string(),
55
55
  instructions: z.string(),
@@ -65,8 +65,6 @@ export const SkillManagerOutputSchema = z.object({
65
65
  mergedSkill: MergedSkillSchema.optional(),
66
66
  })
67
67
 
68
- export type SkillManagerOutput = z.infer<typeof SkillManagerOutputSchema>
69
-
70
68
  export function createSkillManagerAgent(options: CreateHelperToolLoopAgentOptions) {
71
69
  return new ToolLoopAgent({
72
70
  id: 'skill-manager',
@@ -8,7 +8,7 @@ import {
8
8
  import type { CreateHelperToolLoopAgentOptions } from '../runtime/agent-types'
9
9
  import { resolveHelperAgentOptions } from './helper-agent-options'
10
10
 
11
- const WORKSTREAM_TITLE_MAX_TOKENS = 64
11
+ const WORKSTREAM_TITLE_MAX_TOKENS = 512
12
12
 
13
13
  export const WORKSTREAM_TITLE_GENERATOR_PROMPT = `<agent-instructions>
14
14
  You are a **Title Generator** that creates concise chat titles.
@@ -18,7 +18,7 @@ Generate a chat title based only on the user's message.
18
18
  </task>
19
19
 
20
20
  <constraints>
21
- - Maximum 4-5 words
21
+ - Maximum 3-4 words
22
22
  - Capture the core workstream or intent
23
23
  - Use natural, readable language
24
24
  - No punctuation at the end
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  CreateExecutionPlanArgsSchema,
3
3
  GetActiveExecutionPlanArgsSchema,
4
- RestartExecutionTaskArgsSchema,
4
+ ResumeExecutionPlanRunArgsSchema,
5
5
  ReplaceExecutionPlanArgsSchema,
6
- SetExecutionTaskStatusArgsSchema,
7
- } from '@lota-sdk/shared/schemas/tools'
8
- import type { ExecutionPlanToolResultData } from '@lota-sdk/shared/schemas/tools'
6
+ SubmitExecutionNodeResultArgsSchema,
7
+ } from '@lota-sdk/shared'
8
+ import type { ExecutionPlanToolResultData } from '@lota-sdk/shared'
9
9
  import { tool } from 'ai'
10
10
 
11
11
  import type { RecordIdRef } from '../db/record-id'
@@ -34,7 +34,7 @@ export function createCreateExecutionPlanTool(params: {
34
34
  }) {
35
35
  return tool({
36
36
  description:
37
- 'Create a structured execution plan for multi-step work in this workstream. Use this before starting multi-step execution.',
37
+ 'Create a contract-driven execution plan for this workstream. Nodes must define deliverables, success criteria, completion checks, and executor policy.',
38
38
  inputSchema: CreateExecutionPlanArgsSchema,
39
39
  execute: async (input) => {
40
40
  const result = await executionPlanService.createPlan({
@@ -56,7 +56,8 @@ export function createReplaceExecutionPlanTool(params: {
56
56
  onPlanChanged?: () => void
57
57
  }) {
58
58
  return tool({
59
- description: 'Replace the active execution plan when the ordered steps or approach have materially changed.',
59
+ description:
60
+ 'Replace the active execution run with a new contract-driven plan when the graph, contracts, or approach materially change.',
60
61
  inputSchema: ReplaceExecutionPlanArgsSchema,
61
62
  execute: async (input) => {
62
63
  const result = await executionPlanService.replacePlan({
@@ -71,48 +72,17 @@ export function createReplaceExecutionPlanTool(params: {
71
72
  })
72
73
  }
73
74
 
74
- export function createSetExecutionTaskStatusTool(params: {
75
+ export function createSubmitExecutionNodeResultTool(params: {
75
76
  workstreamId: RecordIdRef
76
77
  agentId: string
77
78
  onPlanChanged?: () => void
78
79
  }) {
79
80
  return tool({
80
81
  description:
81
- 'Update the active execution-plan task state, including in-progress, completed, blocked, failed, or skipped outcomes.',
82
- inputSchema: SetExecutionTaskStatusArgsSchema,
83
- execute: async function* (input) {
84
- const activePlan = await executionPlanService.getActivePlanToolResult({
85
- workstreamId: params.workstreamId,
86
- includeEvents: true,
87
- })
88
-
89
- if (activePlan.plan && activePlan.plan.planId === input.planId) {
90
- const optimisticPlan = structuredClone(activePlan.plan)
91
- const targetTask = optimisticPlan.tasks.find((task) => task.id === input.taskId)
92
-
93
- if (targetTask) {
94
- targetTask.status = input.status
95
- if (input.resultStatus !== undefined) targetTask.resultStatus = input.resultStatus
96
- if (input.outputSummary !== undefined) targetTask.outputSummary = input.outputSummary || undefined
97
- if (input.blockedReason !== undefined) targetTask.blockedReason = input.blockedReason || undefined
98
- if (input.externalRef !== undefined) targetTask.externalRef = input.externalRef || undefined
99
- if (input.status === 'in-progress') {
100
- optimisticPlan.status = 'executing'
101
- optimisticPlan.currentTaskId = targetTask.id
102
- }
103
-
104
- yield {
105
- action: 'task-status-updated',
106
- message: `Updating task "${targetTask.title}".`,
107
- changedTaskId: targetTask.id,
108
- plan: optimisticPlan,
109
- hasPlan: true,
110
- status: optimisticPlan.status,
111
- } satisfies ExecutionPlanToolResultData
112
- }
113
- }
114
-
115
- const result = await executionPlanService.setTaskStatus({
82
+ 'Submit the result for the currently running execution node. The executor validates outputs, artifacts, and completion checks before advancing the run.',
83
+ inputSchema: SubmitExecutionNodeResultArgsSchema,
84
+ execute: async (input) => {
85
+ const result = await executionPlanService.submitNodeResult({
116
86
  workstreamId: params.workstreamId,
117
87
  emittedBy: params.agentId,
118
88
  input,
@@ -123,7 +93,7 @@ export function createSetExecutionTaskStatusTool(params: {
123
93
  toModelOutput: ({ output }) => {
124
94
  const result = getLatestExecutionPlanToolResult(output)
125
95
  const summary = result?.message?.trim()
126
- return { type: 'text', value: summary && summary.length > 0 ? summary : 'Execution task status updated.' }
96
+ return { type: 'text', value: summary && summary.length > 0 ? summary : 'Execution node result submitted.' }
127
97
  },
128
98
  })
129
99
  }
@@ -131,27 +101,31 @@ export function createSetExecutionTaskStatusTool(params: {
131
101
  export function createGetActiveExecutionPlanTool(params: { workstreamId: RecordIdRef }) {
132
102
  return tool({
133
103
  description:
134
- 'Load the active execution plan for this workstream, including task state and recent events when needed.',
104
+ 'Load the active execution run for this workstream, including graph state, node contracts, recent events, approvals, artifacts, and checkpoints when requested.',
135
105
  inputSchema: GetActiveExecutionPlanArgsSchema,
136
106
  execute: async (input) =>
137
107
  await executionPlanService.getActivePlanToolResult({
138
108
  workstreamId: params.workstreamId,
139
109
  includeEvents: input.includeEvents,
110
+ includeArtifacts: input.includeArtifacts,
111
+ includeApprovals: input.includeApprovals,
112
+ includeCheckpoints: input.includeCheckpoints,
113
+ includeValidationIssues: input.includeValidationIssues,
140
114
  }),
141
115
  })
142
116
  }
143
117
 
144
- export function createRestartExecutionTaskTool(params: {
118
+ export function createResumeExecutionPlanRunTool(params: {
145
119
  workstreamId: RecordIdRef
146
120
  agentId: string
147
121
  onPlanChanged?: () => void
148
122
  }) {
149
123
  return tool({
150
124
  description:
151
- 'Restart a failed or blocked execution-plan task and optionally reset downstream tasks back to pending.',
152
- inputSchema: RestartExecutionTaskArgsSchema,
125
+ 'Resume the active execution run from its latest checkpoint after interruption. The executor reconciles state before re-activating nodes.',
126
+ inputSchema: ResumeExecutionPlanRunArgsSchema,
153
127
  execute: async (input) => {
154
- const result = await executionPlanService.restartTask({
128
+ const result = await executionPlanService.resumeRun({
155
129
  workstreamId: params.workstreamId,
156
130
  emittedBy: params.agentId,
157
131
  input,
@@ -1,12 +1,12 @@
1
1
  import Firecrawl from '@mendable/firecrawl-js'
2
2
 
3
- import { env } from '../config/env-shapes'
3
+ import { getRuntimeConfig } from '../runtime/runtime-config'
4
4
 
5
5
  let _firecrawlClient: Firecrawl | undefined
6
6
 
7
7
  export function getFirecrawlClient(): Firecrawl {
8
8
  if (!_firecrawlClient) {
9
- _firecrawlClient = new Firecrawl({ apiKey: env.FIRECRAWL_API_KEY })
9
+ _firecrawlClient = new Firecrawl({ apiKey: getRuntimeConfig().firecrawl.apiKey })
10
10
  }
11
11
  return _firecrawlClient
12
12
  }
@@ -0,0 +1,12 @@
1
+ export * from './execution-plan.tool'
2
+ export * from './fetch-webpage.tool'
3
+ export * from './log-hello-world.tool'
4
+ export * from './memory-block.tool'
5
+ export * from './read-file-parts.tool'
6
+ export * from './remember-memory.tool'
7
+ export * from './research-topic.tool'
8
+ export * from './search-tools'
9
+ export * from './search-web.tool'
10
+ export * from './team-think.tool'
11
+ export * from './tool-contract'
12
+ export * from './user-questions.tool'
@@ -0,0 +1,17 @@
1
+ import { tool } from 'ai'
2
+ import { z } from 'zod'
3
+
4
+ export function createLogHelloWorldTool() {
5
+ return tool({
6
+ description: 'Logs "Hello World" to the server console. Requires user approval before running.',
7
+ inputSchema: z.object({
8
+ message: z.string().optional().describe('Optional custom message to log alongside Hello World'),
9
+ }),
10
+ needsApproval: true,
11
+ execute: async ({ message }: { message?: string }) => {
12
+ const output = message ? `Hello World: ${message}` : 'Hello World'
13
+ console.log('[logHelloWorld]', output)
14
+ return { logged: output, timestamp: new Date().toISOString() }
15
+ },
16
+ })
17
+ }