@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 +2 -2
- package/src/config/constants.ts +1 -1
- package/src/queues/plan-agent-heartbeat.queue.ts +40 -24
- package/src/runtime/domain-layer.ts +6 -6
- package/src/services/plan/plan-agent-heartbeat.service.ts +7 -12
- package/src/services/thread/thread-turn-streaming.ts +1 -7
- package/src/services/thread/thread-turn.ts +10 -8
- package/src/system-agents/thread-router.agent.ts +1 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lota-sdk/core",
|
|
3
|
-
"version": "0.4.
|
|
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.
|
|
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",
|
package/src/config/constants.ts
CHANGED
|
@@ -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://
|
|
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
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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 ?? '
|
|
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,
|