@lota-sdk/core 0.4.14 → 0.4.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lota-sdk/core",
3
- "version": "0.4.14",
3
+ "version": "0.4.15",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -31,7 +31,7 @@
31
31
  "@ai-sdk/openai": "^3.0.53",
32
32
  "@chat-adapter/slack": "^4.26.0",
33
33
  "@chat-adapter/state-ioredis": "^4.26.0",
34
- "@lota-sdk/shared": "0.4.14",
34
+ "@lota-sdk/shared": "0.4.15",
35
35
  "@mendable/firecrawl-js": "^4.18.3",
36
36
  "@surrealdb/node": "^3.0.3",
37
37
  "ai": "^6.0.168",
@@ -6,7 +6,7 @@ export const MEMORY = {
6
6
  MAX_KNN_LIMIT: 100,
7
7
  } as const
8
8
 
9
- export const DEFAULT_AI_GATEWAY_URL = 'https://ai-gateway.gobrainy.ai' as const
9
+ export const DEFAULT_AI_GATEWAY_URL = 'https://aigateway.dev.ventur-os.com' as const
10
10
 
11
11
  /** Validates that a value is a safe integer for KNN queries. Throws if validation fails. */
12
12
  export function validateKnnLimit(limit: unknown): number {
@@ -1,4 +1,4 @@
1
- import type { Job } from 'bullmq'
1
+ import type { Job, JobsOptions } from 'bullmq'
2
2
  import { Effect, Schema } from 'effect'
3
3
  import type { Context } from 'effect'
4
4
  import type IORedis from 'ioredis'
@@ -44,6 +44,7 @@ export type PlanAgentHeartbeatJob = PlanAgentHeartbeatWakeJob | PlanAgentHeartbe
44
44
  export const PLAN_AGENT_HEARTBEAT_QUEUE = 'plan-agent-heartbeat'
45
45
  const PLAN_AGENT_HEARTBEAT_SWEEP_INTERVAL_MS = 30_000
46
46
  const PLAN_AGENT_HEARTBEAT_SWEEP_JOB_ID = 'plan-agent-heartbeat-sweep'
47
+ const REUSABLE_HEARTBEAT_JOB_STATES = new Set(['completed', 'failed'])
47
48
 
48
49
  export interface PlanAgentHeartbeatWorkerDeps {
49
50
  databaseService: Context.Service.Shape<typeof DatabaseServiceTag>
@@ -84,35 +85,50 @@ export function makePlanAgentHeartbeatQueueRuntime(
84
85
  ): PlanAgentHeartbeatQueueRuntime {
85
86
  const { connectionProvider, queueJobService } = params
86
87
 
88
+ const enqueueReusableHeartbeatJob = async (job: PlanAgentHeartbeatJob, options: JobsOptions): Promise<void> => {
89
+ const jobId = typeof options.jobId === 'string' ? options.jobId : null
90
+ if (jobId) {
91
+ const existing = await queue.getQueue().getJob(jobId)
92
+ if (existing) {
93
+ const state = await existing.getState()
94
+ if (REUSABLE_HEARTBEAT_JOB_STATES.has(state)) {
95
+ await existing.remove()
96
+ serverLogger.info`Removed terminal Plan agent heartbeat job before re-enqueue (${jobId}, state=${state})`
97
+ }
98
+ }
99
+ }
100
+
101
+ await queue.enqueue(job, options)
102
+ }
103
+
87
104
  const enqueueDelayedSweep = (delayMs = PLAN_AGENT_HEARTBEAT_SWEEP_INTERVAL_MS): Promise<void> =>
88
- queue.enqueue({ type: 'sweep' }, { delay: delayMs, jobId: PLAN_AGENT_HEARTBEAT_SWEEP_JOB_ID })
105
+ enqueueReusableHeartbeatJob({ type: 'sweep' }, { delay: delayMs, jobId: PLAN_AGENT_HEARTBEAT_SWEEP_JOB_ID })
89
106
 
90
107
  const processPlanAgentHeartbeatJob = (
91
108
  deps: PlanAgentHeartbeatWorkerDeps,
92
109
  job: Job<PlanAgentHeartbeatJob>,
93
110
  ): Promise<void> => {
94
111
  const { planAgentHeartbeatService: service } = deps
95
- return Effect.runPromise(
96
- Effect.gen(function* () {
97
- if (job.data.type === 'wake-node') {
98
- const wakeJob: PlanAgentHeartbeatWakeJob = job.data
99
- yield* service.wakeNode(wakeJob)
100
- return
101
- }
102
-
103
- yield* service.sweep({ organizationId: job.data.organizationId })
104
- if (!job.data.organizationId) {
105
- yield* Effect.tryPromise({
106
- try: () => enqueueDelayedSweep(),
107
- catch: (cause) =>
108
- new PlanAgentHeartbeatQueueError({
109
- message: 'Failed to enqueue delayed plan-agent heartbeat sweep.',
110
- cause,
111
- }),
112
- })
113
- }
114
- }),
115
- )
112
+ const program: Effect.Effect<void, unknown, never> = Effect.gen(function* () {
113
+ if (job.data.type === 'wake-node') {
114
+ const wakeJob: PlanAgentHeartbeatWakeJob = job.data
115
+ yield* service.wakeNode(wakeJob)
116
+ return
117
+ }
118
+
119
+ yield* service.sweep({ organizationId: job.data.organizationId })
120
+ if (!job.data.organizationId) {
121
+ yield* Effect.tryPromise({
122
+ try: () => enqueueDelayedSweep(),
123
+ catch: (cause) =>
124
+ new PlanAgentHeartbeatQueueError({
125
+ message: 'Failed to enqueue delayed plan-agent heartbeat sweep.',
126
+ cause,
127
+ }),
128
+ })
129
+ }
130
+ })
131
+ return Effect.runPromise(program)
116
132
  }
117
133
 
118
134
  const queue = createQueueFactoryWithDeps<PlanAgentHeartbeatJob, PlanAgentHeartbeatWorkerDeps>({
@@ -130,7 +146,7 @@ export function makePlanAgentHeartbeatQueueRuntime(
130
146
 
131
147
  return {
132
148
  enqueuePlanAgentHeartbeatWake: (wakeParams) =>
133
- queue.enqueue({ type: 'wake-node', ...wakeParams }, { jobId: buildWakeJobId(wakeParams) }),
149
+ enqueueReusableHeartbeatJob({ type: 'wake-node', ...wakeParams }, { jobId: buildWakeJobId(wakeParams) }),
134
150
  startWorker: (options) => {
135
151
  const handle = queue.startWorker({
136
152
  deps: options.deps,
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Builds the domain-service Layer tree for `createLotaRuntime`.
3
3
  *
4
- * The services form a 9-tier dependency graph on top of the infrastructure
4
+ * The services form a tiered dependency graph on top of the infrastructure
5
5
  * layer (config, logging, database, redis, agents, threads, extensions).
6
6
  * Each tier is provided with the accumulated context of earlier tiers so
7
7
  * every service resolves cleanly when the ManagedRuntime eagerly loads them.
@@ -184,10 +184,7 @@ export function buildDomainServiceLayer(infrastructureLayer: InfrastructureLayer
184
184
  const tier5 = provide(PlanExecutorServiceLive, ctx4)
185
185
  const ctx5 = Layer.mergeAll(ctx4, tier5)
186
186
 
187
- const tier6 = provide(
188
- Layer.mergeAll(OwnershipDispatcherServiceLive, PlanAgentHeartbeatServiceLive, PlanDeadlineServiceLive),
189
- ctx5,
190
- )
187
+ const tier6 = provide(Layer.mergeAll(OwnershipDispatcherServiceLive, PlanDeadlineServiceLive), ctx5)
191
188
  const ctx6 = Layer.mergeAll(ctx5, tier6)
192
189
 
193
190
  const tier7 = provide(Layer.mergeAll(ExecutionPlanServiceLive, GlobalOrchestratorServiceLive), ctx6)
@@ -205,5 +202,8 @@ export function buildDomainServiceLayer(infrastructureLayer: InfrastructureLayer
205
202
  const ctx8 = Layer.mergeAll(ctx7, tier8)
206
203
 
207
204
  const tier9 = provide(Layer.mergeAll(PlanCycleServiceLive, ThreadTurnServiceLive), ctx8)
208
- return Layer.mergeAll(ctx8, tier9)
205
+ const ctx9 = Layer.mergeAll(ctx8, tier9)
206
+
207
+ const tier10 = provide(PlanAgentHeartbeatServiceLive, ctx9)
208
+ return Layer.mergeAll(ctx9, tier10)
209
209
  }
@@ -11,6 +11,8 @@ import { LotaQueuesServiceTag } from '../../queues/queues.service'
11
11
  import type { RedisConnectionManager } from '../../redis/connection'
12
12
  import { withLeaseLock } from '../../redis/redis-lease-lock'
13
13
  import { resolvePlanNodeExecutionVisibility } from '../../runtime/execution-plan-visibility'
14
+ import type { makeThreadTurnService } from '../thread/thread-turn'
15
+ import { ThreadTurnServiceTag } from '../thread/thread-turn'
14
16
  import type { makeThreadService } from '../thread/thread.service'
15
17
  import { ThreadServiceTag } from '../thread/thread.service'
16
18
  import type { makePlanAgentQueryService } from './plan-agent-query.service'
@@ -51,24 +53,24 @@ function heartbeatServiceEffect<A, E, R = never>(
51
53
 
52
54
  interface PlanAgentHeartbeatDeps {
53
55
  agentConfig: ResolvedAgentConfig
54
- provideCurrentContext: <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, never>
55
56
  redis: RedisConnectionManager
56
57
  planAgentQueryService: ReturnType<typeof makePlanAgentQueryService>
57
58
  planExecutorService: ReturnType<typeof makePlanExecutorService>
58
59
  planRunService: ReturnType<typeof makePlanRunService>
59
60
  threadService: ReturnType<typeof makeThreadService>
61
+ threadTurnService: ReturnType<typeof makeThreadTurnService>
60
62
  planAgentHeartbeatQueue: PlanAgentHeartbeatQueueRuntime
61
63
  }
62
64
 
63
65
  export function makePlanAgentHeartbeatService(deps: PlanAgentHeartbeatDeps) {
64
66
  const {
65
67
  agentConfig,
66
- provideCurrentContext,
67
68
  planExecutorService,
68
69
  planRunService,
69
70
  redis,
70
71
  planAgentQueryService,
71
72
  threadService,
73
+ threadTurnService,
72
74
  planAgentHeartbeatQueue,
73
75
  } = deps
74
76
 
@@ -149,14 +151,9 @@ export function makePlanAgentHeartbeatService(deps: PlanAgentHeartbeatDeps) {
149
151
  )
150
152
  }
151
153
 
152
- const { triggerPlanNodeTurn } = yield* Effect.tryPromise({
153
- try: () => import('../thread/thread-turn'),
154
- catch: (cause) => new PlanAgentHeartbeatError({ operation: 'import-thread-turn', cause }),
155
- })
156
-
157
154
  yield* heartbeatServiceEffect(
158
155
  'trigger-plan-node-turn',
159
- provideCurrentContext(triggerPlanNodeTurn({ runId: params.runId, nodeId: params.nodeId })),
156
+ threadTurnService.triggerPlanNodeTurn({ runId: params.runId, nodeId: params.nodeId }),
160
157
  )
161
158
  return true
162
159
  }),
@@ -227,24 +224,22 @@ export class PlanAgentHeartbeatServiceTag extends Context.Service<
227
224
  export const PlanAgentHeartbeatServiceLive = Layer.effect(
228
225
  PlanAgentHeartbeatServiceTag,
229
226
  Effect.gen(function* () {
230
- const currentContext = yield* Effect.context()
231
- const provideCurrentContext = <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, never> =>
232
- effect.pipe(Effect.provide(currentContext)) as Effect.Effect<A, E, never>
233
227
  const agentConfig = yield* AgentConfigServiceTag
234
228
  const redis = yield* RedisServiceTag
235
229
  const planAgentQueryService = yield* PlanAgentQueryServiceTag
236
230
  const planRunService = yield* PlanRunServiceTag
237
231
  const planExecutor = yield* PlanExecutorServiceTag
238
232
  const threadSvc = yield* ThreadServiceTag
233
+ const threadTurnSvc = yield* ThreadTurnServiceTag
239
234
  const queues = yield* LotaQueuesServiceTag
240
235
  return makePlanAgentHeartbeatService({
241
236
  agentConfig,
242
- provideCurrentContext,
243
237
  redis,
244
238
  planAgentQueryService,
245
239
  planExecutorService: planExecutor,
246
240
  planRunService,
247
241
  threadService: threadSvc,
242
+ threadTurnService: threadTurnSvc,
248
243
  planAgentHeartbeatQueue: queues.planAgentHeartbeat,
249
244
  })
250
245
  }),
@@ -74,7 +74,7 @@ function isTextTokenChunkType(chunkType: string | undefined): boolean {
74
74
  return chunkType === 'text-delta'
75
75
  }
76
76
 
77
- function buildFallbackResponseMessage(
77
+ export function buildFallbackResponseMessage(
78
78
  result: ToolLoopGenerateResult,
79
79
  ): Effect.Effect<ChatMessage, ThreadTurnStreamingError> {
80
80
  const parts: ChatMessage['parts'] = []
@@ -96,12 +96,6 @@ function buildFallbackResponseMessage(
96
96
  parts.push({ type: 'text', text })
97
97
  }
98
98
 
99
- if (parts.length === 0) {
100
- return Effect.fail(
101
- new ThreadTurnStreamingError({ message: 'Agent generate fallback did not produce any response parts.' }),
102
- )
103
- }
104
-
105
99
  return Effect.succeed({ id: Bun.randomUUIDv7(), role: 'assistant', parts })
106
100
  }
107
101
 
@@ -428,14 +428,16 @@ function triggerPlanNodeTurnWith(
428
428
  deps: ThreadTurnDeps,
429
429
  params: { runId: string; nodeId: string; abortSignal?: AbortSignal; streamId?: string },
430
430
  ) {
431
- return triggerPlanNodeTurnEffect(deps, params).pipe(
432
- Effect.annotateSpans(
433
- compactSpanAttributes({
434
- turnKind: 'planTurn',
435
- streamId: params.streamId,
436
- planRunId: params.runId,
437
- planNodeId: params.nodeId,
438
- }),
431
+ return deps.provideCurrentContext(
432
+ triggerPlanNodeTurnEffect(deps, params).pipe(
433
+ Effect.annotateSpans(
434
+ compactSpanAttributes({
435
+ turnKind: 'planTurn',
436
+ streamId: params.streamId,
437
+ planRunId: params.runId,
438
+ planNodeId: params.nodeId,
439
+ }),
440
+ ),
439
441
  ),
440
442
  )
441
443
  }
@@ -176,14 +176,13 @@ function generateRouterObjectEffect<TSchema extends z.ZodTypeAny>(params: {
176
176
  prompt: string
177
177
  label: 'triage' | 'check'
178
178
  }): Effect.Effect<z.infer<TSchema> | null, never> {
179
- const modelId = params.agentConfig.routerModelId ?? 'openrouter/openai/gpt-5.4-nano'
179
+ const modelId = params.agentConfig.routerModelId ?? 'gpt-5.4-nano'
180
180
 
181
181
  return Effect.tryPromise({
182
182
  try: () =>
183
183
  generateObject({
184
184
  model: params.aiGatewayModels.chatModel(modelId),
185
185
  headers: buildAiGatewayDirectCacheHeaders('lota-sdk'),
186
- providerOptions: { openai: { reasoningEffort: 'low' } },
187
186
  schema: params.schema,
188
187
  system: params.system,
189
188
  prompt: params.prompt,