@lota-sdk/core 0.4.3 → 0.4.5

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.
@@ -86,7 +86,9 @@ DEFINE TABLE IF NOT EXISTS planRun SCHEMAFULL;
86
86
  DEFINE FIELD IF NOT EXISTS planSpecId ON TABLE planRun TYPE record<planSpec> REFERENCE ON DELETE CASCADE;
87
87
  DEFINE FIELD IF NOT EXISTS organizationId ON TABLE planRun TYPE record<organization>;
88
88
  DEFINE FIELD IF NOT EXISTS threadId ON TABLE planRun TYPE record<thread> REFERENCE ON DELETE CASCADE;
89
+ DEFINE FIELD IF NOT EXISTS sourceThreadId ON TABLE planRun TYPE option<record<thread>>;
89
90
  DEFINE FIELD IF NOT EXISTS leadAgentId ON TABLE planRun TYPE string;
91
+ DEFINE FIELD IF NOT EXISTS createdByAgentId ON TABLE planRun TYPE option<string>;
90
92
  DEFINE FIELD IF NOT EXISTS status ON TABLE planRun TYPE string;
91
93
  DEFINE FIELD IF NOT EXISTS currentNodeId ON TABLE planRun TYPE option<string>;
92
94
  DEFINE FIELD IF NOT EXISTS waitingNodeId ON TABLE planRun TYPE option<string>;
@@ -103,7 +105,9 @@ DEFINE FIELD IF NOT EXISTS completedAt ON TABLE planRun TYPE option<datetime>;
103
105
 
104
106
  DEFINE INDEX IF NOT EXISTS planRunOrgIdx ON TABLE planRun COLUMNS organizationId;
105
107
  DEFINE INDEX IF NOT EXISTS planRunThreadIdx ON TABLE planRun COLUMNS threadId;
108
+ DEFINE INDEX IF NOT EXISTS planRunSourceThreadIdx ON TABLE planRun COLUMNS sourceThreadId;
106
109
  DEFINE INDEX IF NOT EXISTS planRunThreadStatusIdx ON TABLE planRun COLUMNS threadId, status;
110
+ DEFINE INDEX IF NOT EXISTS planRunSourceThreadStatusIdx ON TABLE planRun COLUMNS sourceThreadId, status;
107
111
  DEFINE INDEX IF NOT EXISTS planRunSpecIdx ON TABLE planRun COLUMNS planSpecId;
108
112
 
109
113
  DEFINE TABLE IF NOT EXISTS planNodeRun SCHEMAFULL;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lota-sdk/core",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -32,7 +32,7 @@
32
32
  "@chat-adapter/slack": "^4.23.0",
33
33
  "@chat-adapter/state-ioredis": "^4.23.0",
34
34
  "@logtape/logtape": "^2.0.5",
35
- "@lota-sdk/shared": "0.4.3",
35
+ "@lota-sdk/shared": "0.4.5",
36
36
  "@mendable/firecrawl-js": "^4.18.1",
37
37
  "@surrealdb/node": "^3.0.3",
38
38
  "ai": "^6.0.145",
@@ -305,17 +305,6 @@ export function injectAiGatewayExtraParamsRequestBody(
305
305
  return JSON.stringify({ ...parsed, extra_params: mergedExtraParams })
306
306
  }
307
307
 
308
- export function injectAiGatewayOpenAIPromptCacheRetentionRequestBody(
309
- body: BodyInit | null | undefined,
310
- ): BodyInit | null | undefined {
311
- const parsed = parseAiGatewayJsonRequestBody(body)
312
- if (!parsed) return body
313
- if (!readString(parsed.model)?.startsWith('openai/')) return body
314
- if (readString(parsed.prompt_cache_retention) !== null) return body
315
-
316
- return JSON.stringify({ ...parsed, prompt_cache_retention: OPENAI_PROMPT_CACHE_RETENTION })
317
- }
318
-
319
308
  function createAiGatewayFetch(extraParams?: AiGatewayExtraParams): typeof fetch {
320
309
  const fetchWithMutations = (input: RequestInfo | URL, init?: RequestInit | BunFetchRequestInit) => {
321
310
  const parsedBody = parseAiGatewayJsonRequestBody(init?.body)
@@ -14,15 +14,6 @@ const DEFAULT_CONFIG: BackgroundProcessingConfig = {
14
14
 
15
15
  let resolvedConfig: BackgroundProcessingConfig = { ...DEFAULT_CONFIG }
16
16
 
17
- export function configureBackgroundProcessing(config?: Partial<BackgroundProcessingConfig>): void {
18
- resolvedConfig = {
19
- memoryExtractionFrequency: config?.memoryExtractionFrequency ?? DEFAULT_CONFIG.memoryExtractionFrequency,
20
- skillExtractionFrequency: config?.skillExtractionFrequency ?? DEFAULT_CONFIG.skillExtractionFrequency,
21
- memoryDigestFrequency: config?.memoryDigestFrequency ?? DEFAULT_CONFIG.memoryDigestFrequency,
22
- memoryConsolidationFrequency: config?.memoryConsolidationFrequency ?? DEFAULT_CONFIG.memoryConsolidationFrequency,
23
- }
24
- }
25
-
26
17
  export function getBackgroundProcessingConfig(): BackgroundProcessingConfig {
27
18
  return resolvedConfig
28
19
  }
@@ -8,7 +8,6 @@ export {
8
8
  OPENROUTER_LOW_REASONING_PROVIDER_OPTIONS,
9
9
  OPENROUTER_MEDIUM_REASONING_PROVIDER_OPTIONS,
10
10
  OPENROUTER_MINIMAL_REASONING_PROVIDER_OPTIONS,
11
- OPENROUTER_STRUCTURED_REASONING_MODEL_ID,
12
11
  OPENROUTER_TEAM_AGENT_MODEL_ID,
13
12
  OPENROUTER_WEB_RESEARCH_MODEL_ID,
14
13
  OPENROUTER_XHIGH_REASONING_PROVIDER_OPTIONS,
@@ -2,7 +2,6 @@ import type { ChatMessage } from '@lota-sdk/shared'
2
2
 
3
3
  import { configureEmbeddingCache } from './ai/embedding-cache'
4
4
  import { configureAgentFactory, configureAgents } from './config/agent-defaults'
5
- import { configureBackgroundProcessing } from './config/background-processing'
6
5
  import { configureLotaLogger } from './config/logger'
7
6
  import { configureThreads } from './config/thread-defaults'
8
7
  import { ensureRecordId } from './db/record-id'
@@ -251,7 +250,6 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
251
250
  const redisManager = createRedisConnectionManager({ url: runtimeConfig.redis.url })
252
251
  setRedisConnectionManager(redisManager)
253
252
  configureEmbeddingCache(redisManager.getConnection(), runtimeConfig.memory.embeddingCacheTtlSeconds)
254
- configureBackgroundProcessing()
255
253
  configureSocialChatHistory({ keyPrefix: runtimeConfig.socialChat?.historyRedisKeyPrefix })
256
254
 
257
255
  const socialChatAgentId = runtimeConfig.socialChat?.agentId?.trim() || 'socialChat'
package/src/db/service.ts CHANGED
@@ -44,8 +44,6 @@ export interface SurrealDatabaseLogger {
44
44
  error?: (message: string) => void
45
45
  }
46
46
 
47
- export type BoundQueryLike = { query: string; bindings?: Record<string, unknown> }
48
-
49
47
  interface FindManyOptions {
50
48
  limit?: number
51
49
  offset?: number
@@ -91,18 +89,6 @@ function configureMutation(
91
89
  return builder.merge(data)
92
90
  }
93
91
 
94
- function isBoundQueryLike(value: unknown): value is BoundQueryLike {
95
- if (!isRecord(value)) {
96
- return false
97
- }
98
-
99
- if (typeof value.query !== 'string') {
100
- return false
101
- }
102
-
103
- return value.bindings === undefined || isRecord(value.bindings)
104
- }
105
-
106
92
  const CONNECT_MAX_ATTEMPTS = 5
107
93
  const CONNECT_RETRY_BASE_DELAY_MS = 100
108
94
  const CONNECT_RETRY_JITTER_MS = 50
@@ -396,11 +382,7 @@ export class SurrealDBService {
396
382
  return { clause: clauses.join(' AND '), bindings }
397
383
  }
398
384
 
399
- private normalizeBoundQuery(query: BoundQuery | BoundQueryLike): BoundQuery {
400
- if (!(query instanceof BoundQuery) && !isBoundQueryLike(query)) {
401
- throw new SurrealDBError('Invalid query object: expected a BoundQuery-like value')
402
- }
403
-
385
+ private normalizeBoundQuery(query: BoundQuery): BoundQuery {
404
386
  return new BoundQuery(query.query, this.normalizeBindings(query.bindings))
405
387
  }
406
388
 
@@ -533,7 +515,7 @@ export class SurrealDBService {
533
515
  private wrapTransaction(tx: SurrealTransaction): DatabaseTransaction {
534
516
  return {
535
517
  query: async (query: unknown) => {
536
- const boundQuery = this.normalizeBoundQuery(query as BoundQuery | BoundQueryLike)
518
+ const boundQuery = this.normalizeBoundQuery(query as BoundQuery)
537
519
  const queryText = this.resolveQueryText(boundQuery)
538
520
 
539
521
  try {
@@ -578,16 +560,16 @@ export class SurrealDBService {
578
560
  }
579
561
  }
580
562
 
581
- private resolveQueryText(query: BoundQuery | BoundQueryLike): string {
563
+ private resolveQueryText(query: BoundQuery): string {
582
564
  return query.query
583
565
  }
584
566
 
585
- async query<T>(query: BoundQuery | BoundQueryLike): Promise<T[]> {
567
+ async query<T>(query: BoundQuery): Promise<T[]> {
586
568
  const statements = await this.queryAll<T>(query)
587
569
  return statements.at(0) ?? []
588
570
  }
589
571
 
590
- async queryAll<T>(query: BoundQuery | BoundQueryLike, schema?: z.ZodTypeAny): Promise<T[][]> {
572
+ async queryAll<T>(query: BoundQuery, schema?: z.ZodTypeAny): Promise<T[][]> {
591
573
  const client = await this.ensureConnected()
592
574
  const boundQuery = this.normalizeBoundQuery(query)
593
575
  const queryText = this.resolveQueryText(boundQuery)
@@ -607,13 +589,13 @@ export class SurrealDBService {
607
589
  }
608
590
  }
609
591
 
610
- async queryOne<T extends z.ZodTypeAny>(query: BoundQuery | BoundQueryLike, schema: T): Promise<z.infer<T> | null> {
592
+ async queryOne<T extends z.ZodTypeAny>(query: BoundQuery, schema: T): Promise<z.infer<T> | null> {
611
593
  const results = await this.query<unknown>(query)
612
594
  const first = results.at(0)
613
595
  return first ? this.parseSchema(schema, first) : null
614
596
  }
615
597
 
616
- async queryMany<T extends z.ZodTypeAny>(query: BoundQuery | BoundQueryLike, schema: T): Promise<z.infer<T>[]> {
598
+ async queryMany<T extends z.ZodTypeAny>(query: BoundQuery, schema: T): Promise<z.infer<T>[]> {
617
599
  const results = await this.query<unknown>(query)
618
600
  return results.map((row) => this.parseSchema(schema, row))
619
601
  }
@@ -129,10 +129,6 @@ export function startAutonomousJobWorker(options: AutonomousJobWorkerOptions = {
129
129
  return handle
130
130
  }
131
131
 
132
- export function getAutonomousJobQueueHandle(): WorkerHandle {
133
- return startAutonomousJobWorker()
134
- }
135
-
136
132
  if (import.meta.main) {
137
133
  startAutonomousJobWorker()
138
134
  }
@@ -16,9 +16,7 @@ function toExecutionPlanPromptSummaries(plans: SerializableExecutionPlan[]): Exe
16
16
  return plans.map(({ runId, title }) => ({ runId, title }))
17
17
  }
18
18
 
19
- function formatExecutionPlansForPrompt(plans: SerializableExecutionPlan[]): string | undefined {
20
- if (plans.length === 0) return undefined
21
-
19
+ function formatExecutionPlansForPrompt(plans: SerializableExecutionPlan[]): string {
22
20
  const payload = { activePlans: toExecutionPlanPromptSummaries(plans), planCount: plans.length }
23
21
 
24
22
  return ['<execution-plan-state>', JSON.stringify(payload, null, 2), '</execution-plan-state>'].join('\n')
@@ -26,10 +24,7 @@ function formatExecutionPlansForPrompt(plans: SerializableExecutionPlan[]): stri
26
24
 
27
25
  export function buildExecutionPlanInstructionSections(plans: SerializableExecutionPlan[] | null | undefined): string[] {
28
26
  const normalized = plans ?? []
29
- const sections = [EXECUTION_PLAN_AGENT_PROTOCOL_PROMPT]
30
- const stateSection = formatExecutionPlansForPrompt(normalized)
31
- if (stateSection) sections.push(stateSection)
32
- return sections
27
+ return [EXECUTION_PLAN_AGENT_PROTOCOL_PROMPT, formatExecutionPlansForPrompt(normalized)]
33
28
  }
34
29
 
35
30
  export function createExecutionPlanInstructionSectionCache(params: {
@@ -41,6 +41,14 @@ type AgentActivityDeps = {
41
41
  threadService: Pick<typeof threadService, 'listThreads'>
42
42
  }
43
43
 
44
+ function isPendingPlanApproval(plan: SerializableExecutionPlan): boolean {
45
+ return plan.status === 'pending-approval'
46
+ }
47
+
48
+ function countPendingPlanApprovals(activePlans: ActivePlanEntry[]): number {
49
+ return activePlans.filter(({ plan }) => isPendingPlanApproval(plan)).length
50
+ }
51
+
44
52
  function normalizeCardStatus(status: string): BoardColumnStatus {
45
53
  if (status === 'pending' || status === 'scheduled') return 'ready'
46
54
  if (status === 'partial') return 'completed'
@@ -145,9 +153,9 @@ export class AgentActivityService {
145
153
 
146
154
  async getBoard(userRef: string, orgRef: string): Promise<PlanBoardResponse> {
147
155
  const activePlans = await this.getAllActivePlans(userRef, orgRef)
148
- const cards = activePlans.flatMap(({ plan, thread }) =>
149
- plan.nodes.map((node) => planNodeToCard(node, plan, thread.id, thread.title)),
150
- )
156
+ const cards = activePlans
157
+ .filter(({ plan }) => !isPendingPlanApproval(plan))
158
+ .flatMap(({ plan, thread }) => plan.nodes.map((node) => planNodeToCard(node, plan, thread.id, thread.title)))
151
159
 
152
160
  const columns: PlanBoardColumn[] = BOARD_COLUMN_ORDER.map((status) => ({
153
161
  status,
@@ -161,7 +169,7 @@ export class AgentActivityService {
161
169
  totalNodes: cards.length,
162
170
  completedNodes: cards.filter((card) => card.status === 'completed').length,
163
171
  activePlanCount: activePlans.length,
164
- pendingApprovalCount: cards.filter((card) => card.hasApproval).length,
172
+ pendingApprovalCount: countPendingPlanApprovals(activePlans) + cards.filter((card) => card.hasApproval).length,
165
173
  },
166
174
  }
167
175
  }
@@ -187,8 +195,13 @@ export class AgentActivityService {
187
195
  async getMyTasks(userRef: string, orgRef: string): Promise<MyTasksResponse> {
188
196
  const activePlans = await this.getAllActivePlans(userRef, orgRef)
189
197
  const tasks: PlanNodeCard[] = []
198
+ const pendingPlanApprovalCount = countPendingPlanApprovals(activePlans)
190
199
 
191
200
  for (const { plan, thread } of activePlans) {
201
+ if (isPendingPlanApproval(plan)) {
202
+ continue
203
+ }
204
+
192
205
  for (const node of plan.nodes) {
193
206
  const humanOwned = node.owner.executorType === 'user'
194
207
  const awaitingHuman = node.status === 'awaiting-human'
@@ -198,7 +211,7 @@ export class AgentActivityService {
198
211
  }
199
212
  }
200
213
 
201
- return { tasks, pendingApprovalCount: tasks.filter((task) => task.hasApproval).length }
214
+ return { tasks, pendingApprovalCount: pendingPlanApprovalCount + tasks.filter((task) => task.hasApproval).length }
202
215
  }
203
216
 
204
217
  async getAgentActivity(userRef: string, orgRef: string): Promise<AgentActivityResponse> {
@@ -210,6 +223,23 @@ export class AgentActivityService {
210
223
  }
211
224
 
212
225
  for (const { plan, thread } of activePlans) {
226
+ if (isPendingPlanApproval(plan)) {
227
+ if (plan.leadAgentId.trim()) {
228
+ const leadEntry = this.ensureEntry(activityByAgent, plan.leadAgentId)
229
+ leadEntry.isLeadingActivePlan = true
230
+ this.ensureProjectEntry(leadEntry.projects, {
231
+ threadId: thread.id,
232
+ threadTitle: thread.title,
233
+ planRunId: plan.runId,
234
+ planTitle: plan.title,
235
+ status: plan.status,
236
+ })
237
+ leadEntry.isRunning = leadEntry.isRunning || thread.isRunning
238
+ leadEntry.lastActiveAt = maxIsoDate(leadEntry.lastActiveAt, thread.updatedAt)
239
+ }
240
+ continue
241
+ }
242
+
213
243
  const involvedAgents = new Set<string>()
214
244
 
215
245
  for (const node of plan.nodes) {
@@ -7,6 +7,7 @@ import type {
7
7
  GetArtifactResult,
8
8
  PublishArtifactArgs,
9
9
  } from '@lota-sdk/shared'
10
+ import { BoundQuery } from 'surrealdb'
10
11
 
11
12
  import type { RecordIdInput } from '../db/record-id'
12
13
  import { ensureRecordId, recordIdToString } from '../db/record-id'
@@ -270,25 +271,25 @@ class ArtifactService {
270
271
 
271
272
  let lastError: unknown = null
272
273
  for (let attempt = 1; attempt <= ARTIFACT_PUBLISH_MAX_ATTEMPTS; attempt += 1) {
273
- let pendingStorageKey: string | null = null
274
+ const publishAttemptState: { pendingStorageKey?: string } = {}
274
275
  try {
275
276
  return await databaseService.withTransaction(
276
277
  async (tx) =>
277
278
  await this.publishArtifactInTransaction(params, tx, {
278
279
  onStorageWrite: (storageKey) => {
279
- pendingStorageKey = storageKey
280
+ publishAttemptState.pendingStorageKey = storageKey
280
281
  },
281
282
  onStorageCleanup: (storageKey) => {
282
- if (pendingStorageKey === storageKey) {
283
- pendingStorageKey = null
283
+ if (publishAttemptState.pendingStorageKey === storageKey) {
284
+ publishAttemptState.pendingStorageKey = undefined
284
285
  }
285
286
  },
286
287
  }),
287
288
  )
288
289
  } catch (error) {
289
- const storageKeyToCleanup = pendingStorageKey
290
- pendingStorageKey = null
291
- if (storageKeyToCleanup !== null) {
290
+ const storageKeyToCleanup = publishAttemptState.pendingStorageKey
291
+ publishAttemptState.pendingStorageKey = undefined
292
+ if (typeof storageKeyToCleanup === 'string') {
292
293
  await generatedDocumentStorageService.deleteTextArtifact(storageKeyToCleanup).catch(() => undefined)
293
294
  }
294
295
  lastError = error
@@ -361,13 +362,10 @@ class ArtifactService {
361
362
  async listBacklinks(artifact: Pick<ArtifactRecord, 'id' | 'organizationId'>): Promise<ArtifactRecord[]> {
362
363
  const artifactId = recordIdToString(artifact.id, TABLES.ARTIFACT)
363
364
  const records = await databaseService.queryMany(
364
- {
365
- query: `SELECT * FROM ${TABLES.ARTIFACT} WHERE organizationId = $organizationId AND references[*].targetId CONTAINS $targetId`,
366
- bindings: {
367
- organizationId: ensureRecordId(artifact.organizationId, TABLES.ORGANIZATION),
368
- targetId: artifactId,
369
- },
370
- },
365
+ new BoundQuery(
366
+ `SELECT * FROM ${TABLES.ARTIFACT} WHERE organizationId = $organizationId AND references[*].targetId CONTAINS $targetId`,
367
+ { organizationId: ensureRecordId(artifact.organizationId, TABLES.ORGANIZATION), targetId: artifactId },
368
+ ),
371
369
  ArtifactRecordSchema,
372
370
  )
373
371
  return records
@@ -22,7 +22,7 @@ import type {
22
22
  } from '@lota-sdk/shared'
23
23
  import type { Job } from 'bullmq'
24
24
  import { CronExpressionParser } from 'cron-parser'
25
- import { RecordId } from 'surrealdb'
25
+ import { BoundQuery, RecordId } from 'surrealdb'
26
26
  import { z } from 'zod'
27
27
 
28
28
  import { ensureRecordId, recordIdToString } from '../db/record-id'
@@ -262,17 +262,14 @@ class AutonomousJobService {
262
262
 
263
263
  private async findRecoverableRunRow(autonomousJobId: RecordIdInput): Promise<AutonomousJobRunRow | null> {
264
264
  const rows = await databaseService.queryMany(
265
- {
266
- query: `SELECT * FROM ${TABLES.AUTONOMOUS_JOB_RUN}
265
+ new BoundQuery(
266
+ `SELECT * FROM ${TABLES.AUTONOMOUS_JOB_RUN}
267
267
  WHERE autonomousJobId = $autonomousJobId
268
268
  AND status IN $statuses
269
269
  ORDER BY createdAt DESC
270
270
  LIMIT 1`,
271
- bindings: {
272
- autonomousJobId: ensureRecordId(autonomousJobId, TABLES.AUTONOMOUS_JOB),
273
- statuses: ['queued', 'running'],
274
- },
275
- },
271
+ { autonomousJobId: ensureRecordId(autonomousJobId, TABLES.AUTONOMOUS_JOB), statuses: ['queued', 'running'] },
272
+ ),
276
273
  AutonomousJobRunRowSchema,
277
274
  )
278
275
 
@@ -371,10 +368,9 @@ class AutonomousJobService {
371
368
  async recoverActiveJobs(now = new Date()): Promise<void> {
372
369
  await databaseService.connect()
373
370
  const activeRows = await databaseService.queryMany(
374
- {
375
- query: `SELECT * FROM ${TABLES.AUTONOMOUS_JOB} WHERE status = $status ORDER BY createdAt ASC`,
376
- bindings: { status: 'active' },
377
- },
371
+ new BoundQuery(`SELECT * FROM ${TABLES.AUTONOMOUS_JOB} WHERE status = $status ORDER BY createdAt ASC`, {
372
+ status: 'active',
373
+ }),
378
374
  AutonomousJobRowSchema,
379
375
  )
380
376