@lota-sdk/core 0.4.10 → 0.4.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.
- package/package.json +3 -3
- package/src/ai-gateway/ai-gateway.ts +214 -98
- package/src/ai-gateway/index.ts +16 -1
- package/src/config/agent-defaults.ts +4 -120
- package/src/config/logger.ts +18 -34
- package/src/config/model-constants.ts +1 -0
- package/src/config/thread-defaults.ts +1 -18
- package/src/create-runtime.ts +90 -28
- package/src/db/base.service.ts +30 -38
- package/src/db/service.ts +489 -545
- package/src/effect/index.ts +0 -2
- package/src/effect/layers.ts +6 -13
- package/src/embeddings/provider.ts +2 -7
- package/src/index.ts +4 -5
- package/src/queues/autonomous-job.queue.ts +159 -113
- package/src/queues/context-compaction.queue.ts +39 -25
- package/src/queues/delayed-node-promotion.queue.ts +56 -29
- package/src/queues/document-processor.queue.ts +5 -3
- package/src/queues/index.ts +1 -0
- package/src/queues/memory-consolidation.queue.ts +79 -53
- package/src/queues/organization-learning.queue.ts +63 -39
- package/src/queues/plan-agent-heartbeat.queue.ts +104 -79
- package/src/queues/plan-scheduler.queue.ts +100 -84
- package/src/queues/post-chat-memory.queue.ts +55 -33
- package/src/queues/queue-factory.ts +40 -41
- package/src/queues/queues.service.ts +61 -0
- package/src/queues/title-generation.queue.ts +42 -31
- package/src/redis/org-memory-lock.ts +24 -9
- package/src/redis/redis-lease-lock.ts +8 -1
- package/src/runtime/agent-identity-overrides.ts +7 -3
- package/src/runtime/agent-runtime-policy.ts +9 -4
- package/src/runtime/agent-stream-helpers.ts +9 -4
- package/src/runtime/context-compaction/context-compaction-runtime.ts +28 -32
- package/src/runtime/context-compaction/context-compaction.ts +9 -7
- package/src/runtime/domain-layer.ts +15 -4
- package/src/runtime/execution-plan-visibility.ts +5 -2
- package/src/runtime/graph-designer.ts +0 -22
- package/src/runtime/index.ts +2 -0
- package/src/runtime/indexed-repositories-policy.ts +2 -6
- package/src/runtime/live-turn-trace.ts +344 -0
- package/src/runtime/plugin-resolution.ts +29 -12
- package/src/runtime/post-turn-side-effects.ts +139 -141
- package/src/runtime/runtime-config.ts +0 -6
- package/src/runtime/runtime-extensions.ts +0 -54
- package/src/runtime/runtime-lifecycle.ts +4 -4
- package/src/runtime/runtime-services.ts +125 -53
- package/src/runtime/runtime-worker-registry.ts +113 -30
- package/src/runtime/social-chat/social-chat-agent-runner.ts +6 -3
- package/src/runtime/social-chat/social-chat-history.ts +3 -1
- package/src/runtime/social-chat/social-chat.ts +35 -20
- package/src/runtime/team-consultation/team-consultation-orchestrator.ts +6 -5
- package/src/runtime/team-consultation/team-consultation-prompts.ts +11 -6
- package/src/runtime/thread-chat-helpers.ts +18 -9
- package/src/runtime/thread-turn-context.ts +7 -47
- package/src/runtime/turn-lifecycle.ts +6 -14
- package/src/services/agent-activity.service.ts +168 -175
- package/src/services/agent-executor.service.ts +35 -16
- package/src/services/attachment.service.ts +4 -70
- package/src/services/autonomous-job.service.ts +53 -61
- package/src/services/context-compaction.service.ts +7 -9
- package/src/services/execution-plan/execution-plan-graph.ts +106 -115
- package/src/services/execution-plan/execution-plan-schedule.ts +1 -15
- package/src/services/execution-plan/execution-plan.service.ts +67 -50
- package/src/services/global-orchestrator.service.ts +18 -7
- package/src/services/graph-full-routing.ts +7 -6
- package/src/services/memory/memory-conversation.ts +10 -5
- package/src/services/memory/memory.service.ts +11 -8
- package/src/services/ownership-dispatcher.service.ts +16 -5
- package/src/services/plan/plan-agent-heartbeat.service.ts +29 -15
- package/src/services/plan/plan-agent-query.service.ts +12 -8
- package/src/services/plan/plan-completion-side-effects.ts +93 -101
- package/src/services/plan/plan-cycle.service.ts +7 -45
- package/src/services/plan/plan-deadline.service.ts +28 -17
- package/src/services/plan/plan-event-delivery.service.ts +47 -40
- package/src/services/plan/plan-executor-context.ts +2 -0
- package/src/services/plan/plan-executor-graph.ts +366 -391
- package/src/services/plan/plan-executor.service.ts +13 -91
- package/src/services/plan/plan-scheduler.service.ts +62 -49
- package/src/services/plan/plan-transaction-events.ts +1 -1
- package/src/services/recent-activity-title.service.ts +6 -2
- package/src/services/thread/thread-bootstrap.ts +11 -9
- package/src/services/thread/thread-message.service.ts +6 -5
- package/src/services/thread/thread-turn-execution.ts +86 -82
- package/src/services/thread/thread-turn-preparation.service.ts +92 -45
- package/src/services/thread/thread-turn-streaming.ts +60 -28
- package/src/services/thread/thread-turn.ts +212 -46
- package/src/services/thread/thread.service.ts +21 -6
- package/src/system-agents/recent-activity-title-refiner.agent.ts +8 -5
- package/src/system-agents/thread-router.agent.ts +23 -20
- package/src/tools/execution-plan.tool.ts +8 -3
- package/src/tools/fetch-webpage.tool.ts +10 -9
- package/src/tools/firecrawl-client.ts +0 -15
- package/src/tools/remember-memory.tool.ts +3 -6
- package/src/tools/research-topic.tool.ts +12 -3
- package/src/tools/search-web.tool.ts +10 -9
- package/src/tools/search.tool.ts +4 -5
- package/src/tools/team-think.tool.ts +139 -121
- package/src/workers/bootstrap.ts +9 -10
- package/src/workers/memory-consolidation.worker.ts +4 -1
- package/src/workers/organization-learning.worker.ts +15 -2
- package/src/workers/regular-chat-memory-digest.helpers.ts +3 -4
- package/src/workers/regular-chat-memory-digest.runner.ts +21 -14
- package/src/workers/skill-extraction.runner.ts +13 -15
- package/src/workers/worker-utils.ts +6 -18
- package/src/effect/awaitable-effect.ts +0 -96
- package/src/effect/runtime-ref.ts +0 -25
- package/src/effect/runtime.ts +0 -46
- package/src/redis/runtime-connection.ts +0 -20
- package/src/runtime/runtime-accessors.ts +0 -92
- package/src/runtime/runtime-token.ts +0 -47
|
@@ -3,8 +3,8 @@ import type IORedis from 'ioredis'
|
|
|
3
3
|
|
|
4
4
|
import type { chatLogger } from '../config/logger'
|
|
5
5
|
import { sha256Hex } from '../utils/crypto'
|
|
6
|
-
import { DEFAULT_JOB_RETENTION
|
|
7
|
-
import type { WorkerHandle } from '../workers/worker-utils'
|
|
6
|
+
import { DEFAULT_JOB_RETENTION } from '../workers/worker-utils'
|
|
7
|
+
import type { QueueJobService, WorkerHandle } from '../workers/worker-utils'
|
|
8
8
|
import { createQueueFactory } from './queue-factory'
|
|
9
9
|
|
|
10
10
|
export type DocumentSourceChannel = string
|
|
@@ -56,6 +56,7 @@ export function createDocumentProcessorQueueRuntime<TJob extends DocumentProcess
|
|
|
56
56
|
getConnectionForBullMQ: () => IORedis
|
|
57
57
|
getWorkerPath: () => string
|
|
58
58
|
logger: typeof chatLogger
|
|
59
|
+
queueJobService: QueueJobService
|
|
59
60
|
queueName?: string
|
|
60
61
|
workerName?: string
|
|
61
62
|
concurrency?: number
|
|
@@ -75,6 +76,7 @@ export function createDocumentProcessorQueueRuntime<TJob extends DocumentProcess
|
|
|
75
76
|
connectionProvider: params.getConnectionForBullMQ,
|
|
76
77
|
defaultJobOptions: { ...DEFAULT_JOB_RETENTION, attempts: 3, backoff: { type: 'exponential', delay: 1000 } },
|
|
77
78
|
processorPath: params.getWorkerPath(),
|
|
79
|
+
queueJobService: params.queueJobService,
|
|
78
80
|
})
|
|
79
81
|
|
|
80
82
|
return {
|
|
@@ -86,7 +88,7 @@ export function createDocumentProcessorQueueRuntime<TJob extends DocumentProcess
|
|
|
86
88
|
)
|
|
87
89
|
|
|
88
90
|
yield* Effect.catch(
|
|
89
|
-
|
|
91
|
+
params.queueJobService.recordEnqueued({
|
|
90
92
|
queueName,
|
|
91
93
|
id: queuedJob.id,
|
|
92
94
|
name: queuedJob.name,
|
package/src/queues/index.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { Effect } from 'effect'
|
|
2
|
+
import type { Context } from 'effect'
|
|
3
|
+
import type IORedis from 'ioredis'
|
|
2
4
|
|
|
3
5
|
import { serverLogger } from '../config/logger'
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
LOW_JOB_RETENTION,
|
|
9
|
-
} from '../workers/worker-utils'
|
|
6
|
+
import { RedisServiceTag } from '../effect/services'
|
|
7
|
+
import { QueueJobServiceTag } from '../services/queue-job.service'
|
|
8
|
+
import type { QueueJobService, WorkerHandle } from '../workers/worker-utils'
|
|
9
|
+
import { getWorkerPath, LONG_JOB_LOCK_DURATION_MS, LOW_JOB_RETENTION } from '../workers/worker-utils'
|
|
10
10
|
import { createQueueFactory } from './queue-factory'
|
|
11
11
|
import { runStandaloneQueueWorker } from './standalone-worker'
|
|
12
12
|
|
|
@@ -17,58 +17,84 @@ export interface MemoryConsolidationJob {
|
|
|
17
17
|
const MEMORY_CONSOLIDATION_INTERVAL_MS = 24 * 60 * 60 * 1000
|
|
18
18
|
const MEMORY_CONSOLIDATION_SCHEDULER_ID = 'memory-consolidation-recurring'
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
lockDuration: LONG_JOB_LOCK_DURATION_MS,
|
|
26
|
-
defaultJobOptions: { ...LOW_JOB_RETENTION, attempts: 2, backoff: { type: 'exponential', delay: 5000 } },
|
|
27
|
-
processorPath: getWorkerPath('memory-consolidation.worker.ts'),
|
|
28
|
-
})
|
|
20
|
+
export interface MemoryConsolidationQueueRuntime {
|
|
21
|
+
enqueueMemoryConsolidation(job?: MemoryConsolidationJob): Promise<void>
|
|
22
|
+
scheduleRecurringConsolidation(): Promise<void>
|
|
23
|
+
startWorker(options?: { registerSignals?: boolean }): WorkerHandle
|
|
24
|
+
}
|
|
29
25
|
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
interface MakeMemoryConsolidationQueueRuntimeParams {
|
|
27
|
+
connectionProvider: () => IORedis
|
|
28
|
+
queueJobService: QueueJobService
|
|
32
29
|
}
|
|
33
30
|
|
|
34
|
-
export function
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
31
|
+
export function makeMemoryConsolidationQueueRuntime(
|
|
32
|
+
params: MakeMemoryConsolidationQueueRuntimeParams,
|
|
33
|
+
): MemoryConsolidationQueueRuntime {
|
|
34
|
+
const { connectionProvider, queueJobService } = params
|
|
35
|
+
const queue = createQueueFactory<MemoryConsolidationJob>({
|
|
36
|
+
name: 'memory-consolidation',
|
|
37
|
+
displayName: 'Memory consolidation',
|
|
38
|
+
jobName: 'consolidate-turn',
|
|
39
|
+
concurrency: 1,
|
|
40
|
+
lockDuration: LONG_JOB_LOCK_DURATION_MS,
|
|
41
|
+
defaultJobOptions: { ...LOW_JOB_RETENTION, attempts: 2, backoff: { type: 'exponential', delay: 5000 } },
|
|
42
|
+
processorPath: getWorkerPath('memory-consolidation.worker.ts'),
|
|
43
|
+
connectionProvider,
|
|
44
|
+
queueJobService,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const enqueueMemoryConsolidation: MemoryConsolidationQueueRuntime['enqueueMemoryConsolidation'] = (job = {}) =>
|
|
48
|
+
queue.enqueue(job, { jobId: job.scopeId ? `consolidate-turn:${job.scopeId}` : undefined })
|
|
50
49
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
50
|
+
const scheduleRecurringConsolidation: MemoryConsolidationQueueRuntime['scheduleRecurringConsolidation'] = () =>
|
|
51
|
+
Effect.runPromise(
|
|
52
|
+
Effect.gen(function* () {
|
|
53
|
+
const queuedJob = yield* Effect.tryPromise(() =>
|
|
54
|
+
queue
|
|
55
|
+
.getQueue()
|
|
56
|
+
.upsertJobScheduler(
|
|
57
|
+
MEMORY_CONSOLIDATION_SCHEDULER_ID,
|
|
58
|
+
{ every: MEMORY_CONSOLIDATION_INTERVAL_MS },
|
|
59
|
+
{
|
|
60
|
+
name: 'consolidate',
|
|
61
|
+
data: {},
|
|
62
|
+
opts: { ...LOW_JOB_RETENTION, attempts: 2, backoff: { type: 'exponential', delay: 5000 } },
|
|
63
|
+
},
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
yield* Effect.catch(
|
|
68
|
+
queueJobService.recordEnqueued({
|
|
69
|
+
queueName: 'memory-consolidation',
|
|
70
|
+
id: queuedJob.id,
|
|
71
|
+
name: queuedJob.name,
|
|
72
|
+
data: queuedJob.data,
|
|
73
|
+
opts: queuedJob.opts,
|
|
74
|
+
attemptsMade: queuedJob.attemptsMade,
|
|
75
|
+
timestamp: queuedJob.timestamp,
|
|
64
76
|
}),
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
77
|
+
(error) =>
|
|
78
|
+
Effect.sync(() => {
|
|
79
|
+
serverLogger.error`Failed to persist queued job metadata (queue=memory-consolidation, job=${queuedJob.id}): ${error}`
|
|
80
|
+
}),
|
|
81
|
+
)
|
|
82
|
+
}),
|
|
83
|
+
)
|
|
69
84
|
|
|
70
|
-
|
|
85
|
+
return {
|
|
86
|
+
enqueueMemoryConsolidation,
|
|
87
|
+
scheduleRecurringConsolidation,
|
|
88
|
+
startWorker: (options = {}) => queue.startWorker({ registerSignals: options.registerSignals, connectionProvider }),
|
|
89
|
+
}
|
|
90
|
+
}
|
|
71
91
|
|
|
72
|
-
runStandaloneQueueWorker(() => {
|
|
73
|
-
|
|
92
|
+
runStandaloneQueueWorker((runtime) => {
|
|
93
|
+
const resolve = <I, T>(tag: Context.Key<I, T>): T => runtime.runSync(Effect.service(tag))
|
|
94
|
+
const redis = resolve(RedisServiceTag)
|
|
95
|
+
const memoryConsolidationQueue = makeMemoryConsolidationQueueRuntime({
|
|
96
|
+
connectionProvider: () => redis.getConnectionForBullMQ(),
|
|
97
|
+
queueJobService: resolve(QueueJobServiceTag),
|
|
98
|
+
})
|
|
99
|
+
memoryConsolidationQueue.startWorker()
|
|
74
100
|
})
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { Effect, Schema } from 'effect'
|
|
2
|
+
import type { Context } from 'effect'
|
|
3
|
+
import type IORedis from 'ioredis'
|
|
2
4
|
|
|
5
|
+
import { RedisServiceTag } from '../effect/services'
|
|
6
|
+
import { QueueJobServiceTag } from '../services/queue-job.service'
|
|
7
|
+
import type { QueueJobService, WorkerHandle } from '../workers/worker-utils'
|
|
3
8
|
import { getWorkerPath, LONG_JOB_LOCK_DURATION_MS } from '../workers/worker-utils'
|
|
4
9
|
import { createQueueFactory } from './queue-factory'
|
|
5
10
|
import { runStandaloneQueueWorker } from './standalone-worker'
|
|
@@ -51,50 +56,69 @@ export function buildSkillExtractionJobOptions(orgId: string) {
|
|
|
51
56
|
return { delay: ORGANIZATION_LEARNING_DELAY_MS, deduplication: { id: buildSkillExtractionDeduplicationId(orgId) } }
|
|
52
57
|
}
|
|
53
58
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
lockDuration: LONG_JOB_LOCK_DURATION_MS,
|
|
60
|
-
defaultJobOptions: { attempts: 2, backoff: { type: 'exponential', delay: 5_000 } },
|
|
61
|
-
processorPath: getWorkerPath('organization-learning.worker.ts'),
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
export function enqueueRegularChatMemoryDigest(job: Omit<RegularChatMemoryDigestJob, 'kind'>) {
|
|
65
|
-
return organizationLearningQueue.enqueue(
|
|
66
|
-
{ kind: 'regular-chat-memory-digest', ...job },
|
|
67
|
-
buildRegularChatMemoryDigestJobOptions(job.orgId),
|
|
68
|
-
)
|
|
59
|
+
export interface OrganizationLearningQueueRuntime {
|
|
60
|
+
enqueueRegularChatMemoryDigest(job: Omit<RegularChatMemoryDigestJob, 'kind'>): Promise<void>
|
|
61
|
+
enqueueSkillExtraction(job: Omit<SkillExtractionJob, 'kind'>): Promise<void>
|
|
62
|
+
clearRegularChatMemoryDigestDeduplicationKey(orgId: string): Promise<void>
|
|
63
|
+
startWorker(options?: { registerSignals?: boolean }): WorkerHandle
|
|
69
64
|
}
|
|
70
65
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
buildSkillExtractionJobOptions(job.orgId),
|
|
75
|
-
)
|
|
66
|
+
interface MakeOrganizationLearningQueueRuntimeParams {
|
|
67
|
+
connectionProvider: () => IORedis
|
|
68
|
+
queueJobService: QueueJobService
|
|
76
69
|
}
|
|
77
70
|
|
|
78
|
-
export function
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
71
|
+
export function makeOrganizationLearningQueueRuntime(
|
|
72
|
+
params: MakeOrganizationLearningQueueRuntimeParams,
|
|
73
|
+
): OrganizationLearningQueueRuntime {
|
|
74
|
+
const { connectionProvider, queueJobService } = params
|
|
75
|
+
const queue = createQueueFactory<OrganizationLearningJob>({
|
|
76
|
+
name: ORGANIZATION_LEARNING_QUEUE,
|
|
77
|
+
displayName: 'Organization learning',
|
|
78
|
+
jobName: 'organization-learning',
|
|
79
|
+
concurrency: 4,
|
|
80
|
+
lockDuration: LONG_JOB_LOCK_DURATION_MS,
|
|
81
|
+
defaultJobOptions: { attempts: 2, backoff: { type: 'exponential', delay: 5_000 } },
|
|
82
|
+
processorPath: getWorkerPath('organization-learning.worker.ts'),
|
|
83
|
+
connectionProvider,
|
|
84
|
+
queueJobService,
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const enqueueRegularChatMemoryDigest: OrganizationLearningQueueRuntime['enqueueRegularChatMemoryDigest'] = (job) =>
|
|
88
|
+
queue.enqueue({ kind: 'regular-chat-memory-digest', ...job }, buildRegularChatMemoryDigestJobOptions(job.orgId))
|
|
89
|
+
|
|
90
|
+
const enqueueSkillExtraction: OrganizationLearningQueueRuntime['enqueueSkillExtraction'] = (job) =>
|
|
91
|
+
queue.enqueue({ kind: 'skill-extraction', ...job }, buildSkillExtractionJobOptions(job.orgId))
|
|
92
|
+
|
|
93
|
+
const clearRegularChatMemoryDigestDeduplicationKey: OrganizationLearningQueueRuntime['clearRegularChatMemoryDigestDeduplicationKey'] =
|
|
94
|
+
(orgId) =>
|
|
95
|
+
Effect.runPromise(
|
|
96
|
+
Effect.asVoid(
|
|
97
|
+
Effect.tryPromise({
|
|
98
|
+
try: () => queue.getQueue().removeDeduplicationKey(buildRegularChatMemoryDigestDeduplicationId(orgId)),
|
|
99
|
+
catch: (cause) =>
|
|
100
|
+
new OrganizationLearningQueueError({
|
|
101
|
+
message: `Failed to clear regular-chat memory digest deduplication key for ${orgId}.`,
|
|
102
|
+
cause,
|
|
103
|
+
}),
|
|
90
104
|
}),
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
)
|
|
94
|
-
}
|
|
105
|
+
),
|
|
106
|
+
)
|
|
95
107
|
|
|
96
|
-
|
|
108
|
+
return {
|
|
109
|
+
enqueueRegularChatMemoryDigest,
|
|
110
|
+
enqueueSkillExtraction,
|
|
111
|
+
clearRegularChatMemoryDigestDeduplicationKey,
|
|
112
|
+
startWorker: (options = {}) => queue.startWorker({ registerSignals: options.registerSignals, connectionProvider }),
|
|
113
|
+
}
|
|
114
|
+
}
|
|
97
115
|
|
|
98
|
-
runStandaloneQueueWorker(() => {
|
|
99
|
-
|
|
116
|
+
runStandaloneQueueWorker((runtime) => {
|
|
117
|
+
const resolve = <I, T>(tag: Context.Key<I, T>): T => runtime.runSync(Effect.service(tag))
|
|
118
|
+
const redis = resolve(RedisServiceTag)
|
|
119
|
+
const organizationLearningQueue = makeOrganizationLearningQueueRuntime({
|
|
120
|
+
connectionProvider: () => redis.getConnectionForBullMQ(),
|
|
121
|
+
queueJobService: resolve(QueueJobServiceTag),
|
|
122
|
+
})
|
|
123
|
+
organizationLearningQueue.startWorker()
|
|
100
124
|
})
|
|
@@ -5,12 +5,20 @@ import type IORedis from 'ioredis'
|
|
|
5
5
|
|
|
6
6
|
import { serverLogger } from '../config/logger'
|
|
7
7
|
import { DatabaseServiceTag, RedisServiceTag } from '../effect/services'
|
|
8
|
-
import {
|
|
9
|
-
import type { WorkerHandle } from '../workers/worker-utils'
|
|
8
|
+
import { QueueJobServiceTag } from '../services/queue-job.service'
|
|
9
|
+
import type { QueueJobService, WorkerHandle } from '../workers/worker-utils'
|
|
10
10
|
import { DEFAULT_JOB_RETENTION, LONG_JOB_LOCK_DURATION_MS } from '../workers/worker-utils'
|
|
11
11
|
import { createQueueFactoryWithDeps } from './queue-factory'
|
|
12
12
|
import { runStandaloneQueueWorker } from './standalone-worker'
|
|
13
13
|
|
|
14
|
+
// Minimal service shape used by the worker processor. Declared structurally to
|
|
15
|
+
// avoid importing the service tag — which would form a dependency cycle since
|
|
16
|
+
// PlanAgentHeartbeatServiceLive depends on LotaQueuesServiceTag.
|
|
17
|
+
interface PlanAgentHeartbeatWorkerServiceShape {
|
|
18
|
+
wakeNode(params: PlanAgentHeartbeatWakeJob): Effect.Effect<unknown, unknown, unknown>
|
|
19
|
+
sweep(params: { organizationId?: string }): Effect.Effect<void, unknown, unknown>
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
class PlanAgentHeartbeatQueueError extends Schema.TaggedErrorClass<PlanAgentHeartbeatQueueError>()(
|
|
15
23
|
'@lota-sdk/core/PlanAgentHeartbeatQueueError',
|
|
16
24
|
{ message: Schema.String, cause: Schema.optional(Schema.Defect) },
|
|
@@ -37,57 +45,11 @@ export const PLAN_AGENT_HEARTBEAT_QUEUE = 'plan-agent-heartbeat'
|
|
|
37
45
|
const PLAN_AGENT_HEARTBEAT_SWEEP_INTERVAL_MS = 30_000
|
|
38
46
|
const PLAN_AGENT_HEARTBEAT_SWEEP_JOB_ID = 'plan-agent-heartbeat-sweep'
|
|
39
47
|
|
|
40
|
-
interface
|
|
48
|
+
export interface PlanAgentHeartbeatWorkerDeps {
|
|
41
49
|
databaseService: Context.Service.Shape<typeof DatabaseServiceTag>
|
|
42
|
-
planAgentHeartbeatService:
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function enqueueDelayedPlanAgentHeartbeatSweep(delayMs = PLAN_AGENT_HEARTBEAT_SWEEP_INTERVAL_MS): Promise<void> {
|
|
46
|
-
return planAgentHeartbeatQueue.enqueue(
|
|
47
|
-
{ type: 'sweep' },
|
|
48
|
-
{ delay: delayMs, jobId: PLAN_AGENT_HEARTBEAT_SWEEP_JOB_ID },
|
|
49
|
-
)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function processPlanAgentHeartbeatJob(
|
|
53
|
-
deps: PlanAgentHeartbeatQueueDeps,
|
|
54
|
-
job: Job<PlanAgentHeartbeatJob>,
|
|
55
|
-
): Promise<void> {
|
|
56
|
-
const { planAgentHeartbeatService } = deps
|
|
57
|
-
return Effect.runPromise(
|
|
58
|
-
Effect.gen(function* () {
|
|
59
|
-
if (job.data.type === 'wake-node') {
|
|
60
|
-
const wakeJob = job.data
|
|
61
|
-
yield* planAgentHeartbeatService.wakeNode(wakeJob)
|
|
62
|
-
return
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
yield* planAgentHeartbeatService.sweep({ organizationId: job.data.organizationId })
|
|
66
|
-
if (!job.data.organizationId) {
|
|
67
|
-
yield* Effect.tryPromise({
|
|
68
|
-
try: () => enqueueDelayedPlanAgentHeartbeatSweep(),
|
|
69
|
-
catch: (cause) =>
|
|
70
|
-
new PlanAgentHeartbeatQueueError({
|
|
71
|
-
message: 'Failed to enqueue delayed plan-agent heartbeat sweep.',
|
|
72
|
-
cause,
|
|
73
|
-
}),
|
|
74
|
-
})
|
|
75
|
-
}
|
|
76
|
-
}),
|
|
77
|
-
)
|
|
50
|
+
planAgentHeartbeatService: PlanAgentHeartbeatWorkerServiceShape
|
|
78
51
|
}
|
|
79
52
|
|
|
80
|
-
const planAgentHeartbeatQueue = createQueueFactoryWithDeps<PlanAgentHeartbeatJob, PlanAgentHeartbeatQueueDeps>({
|
|
81
|
-
name: PLAN_AGENT_HEARTBEAT_QUEUE,
|
|
82
|
-
displayName: 'Plan agent heartbeat',
|
|
83
|
-
jobName: 'plan-agent-heartbeat-job',
|
|
84
|
-
concurrency: 2,
|
|
85
|
-
lockDuration: LONG_JOB_LOCK_DURATION_MS,
|
|
86
|
-
defaultJobOptions: { ...DEFAULT_JOB_RETENTION, attempts: 3, backoff: { type: 'exponential', delay: 5_000 } },
|
|
87
|
-
prepare: ({ databaseService }) => databaseService.connect(),
|
|
88
|
-
processor: processPlanAgentHeartbeatJob,
|
|
89
|
-
})
|
|
90
|
-
|
|
91
53
|
function buildWakeJobId(params: {
|
|
92
54
|
organizationId: string
|
|
93
55
|
threadId: string
|
|
@@ -100,42 +62,105 @@ function buildWakeJobId(params: {
|
|
|
100
62
|
return `plan-agent-wake__${encode(params.runId)}__${encode(params.nodeId)}__${encode(params.agentId)}`
|
|
101
63
|
}
|
|
102
64
|
|
|
103
|
-
export
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
65
|
+
export interface PlanAgentHeartbeatQueueRuntime {
|
|
66
|
+
enqueuePlanAgentHeartbeatWake(params: {
|
|
67
|
+
organizationId: string
|
|
68
|
+
threadId: string
|
|
69
|
+
runId: string
|
|
70
|
+
nodeId: string
|
|
71
|
+
agentId: string
|
|
72
|
+
reason: string
|
|
73
|
+
}): Promise<void>
|
|
74
|
+
startWorker(options: { registerSignals?: boolean; deps: PlanAgentHeartbeatWorkerDeps }): WorkerHandle
|
|
112
75
|
}
|
|
113
76
|
|
|
114
|
-
|
|
115
|
-
registerSignals?: boolean
|
|
77
|
+
interface MakePlanAgentHeartbeatQueueRuntimeParams {
|
|
116
78
|
connectionProvider: () => IORedis
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
const handle = planAgentHeartbeatQueue.startWorker({
|
|
120
|
-
deps: options.deps,
|
|
121
|
-
registerSignals: options.registerSignals,
|
|
122
|
-
connectionProvider: options.connectionProvider,
|
|
123
|
-
})
|
|
79
|
+
queueJobService: QueueJobService
|
|
80
|
+
}
|
|
124
81
|
|
|
125
|
-
|
|
126
|
-
|
|
82
|
+
export function makePlanAgentHeartbeatQueueRuntime(
|
|
83
|
+
params: MakePlanAgentHeartbeatQueueRuntimeParams,
|
|
84
|
+
): PlanAgentHeartbeatQueueRuntime {
|
|
85
|
+
const { connectionProvider, queueJobService } = params
|
|
86
|
+
|
|
87
|
+
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 })
|
|
89
|
+
|
|
90
|
+
const processPlanAgentHeartbeatJob = (
|
|
91
|
+
deps: PlanAgentHeartbeatWorkerDeps,
|
|
92
|
+
job: Job<PlanAgentHeartbeatJob>,
|
|
93
|
+
): Promise<void> => {
|
|
94
|
+
const { planAgentHeartbeatService: service } = deps
|
|
95
|
+
return Effect.runPromise(
|
|
96
|
+
Effect.gen(function* () {
|
|
97
|
+
if (job.data.type === 'wake-node') {
|
|
98
|
+
const wakeJob = 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
|
+
}) as Effect.Effect<void, never, never>,
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const queue = createQueueFactoryWithDeps<PlanAgentHeartbeatJob, PlanAgentHeartbeatWorkerDeps>({
|
|
119
|
+
name: PLAN_AGENT_HEARTBEAT_QUEUE,
|
|
120
|
+
displayName: 'Plan agent heartbeat',
|
|
121
|
+
jobName: 'plan-agent-heartbeat-job',
|
|
122
|
+
concurrency: 2,
|
|
123
|
+
lockDuration: LONG_JOB_LOCK_DURATION_MS,
|
|
124
|
+
defaultJobOptions: { ...DEFAULT_JOB_RETENTION, attempts: 3, backoff: { type: 'exponential', delay: 5_000 } },
|
|
125
|
+
connectionProvider,
|
|
126
|
+
queueJobService,
|
|
127
|
+
prepare: ({ databaseService }) => Effect.runPromise(databaseService.connect()),
|
|
128
|
+
processor: processPlanAgentHeartbeatJob,
|
|
127
129
|
})
|
|
128
130
|
|
|
129
|
-
return
|
|
131
|
+
return {
|
|
132
|
+
enqueuePlanAgentHeartbeatWake: (wakeParams) =>
|
|
133
|
+
queue.enqueue({ type: 'wake-node', ...wakeParams }, { jobId: buildWakeJobId(wakeParams) }),
|
|
134
|
+
startWorker: (options) => {
|
|
135
|
+
const handle = queue.startWorker({
|
|
136
|
+
deps: options.deps,
|
|
137
|
+
registerSignals: options.registerSignals,
|
|
138
|
+
connectionProvider,
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
enqueueDelayedSweep().catch((error: unknown) => {
|
|
142
|
+
serverLogger.error`Plan agent heartbeat scheduler setup failed: ${error}`
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
return handle
|
|
146
|
+
},
|
|
147
|
+
}
|
|
130
148
|
}
|
|
131
149
|
|
|
132
150
|
runStandaloneQueueWorker((runtime) => {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
151
|
+
void (async () => {
|
|
152
|
+
const { PlanAgentHeartbeatServiceTag } = await import('../services/plan/plan-agent-heartbeat.service')
|
|
153
|
+
const resolve = <I, T>(tag: Context.Key<I, T>): T => runtime.runSync(Effect.service(tag))
|
|
154
|
+
const redis = resolve(RedisServiceTag)
|
|
155
|
+
const planAgentHeartbeatQueue = makePlanAgentHeartbeatQueueRuntime({
|
|
156
|
+
connectionProvider: () => redis.getConnectionForBullMQ(),
|
|
157
|
+
queueJobService: resolve(QueueJobServiceTag),
|
|
158
|
+
})
|
|
159
|
+
planAgentHeartbeatQueue.startWorker({
|
|
160
|
+
deps: {
|
|
161
|
+
databaseService: resolve(DatabaseServiceTag),
|
|
162
|
+
planAgentHeartbeatService: resolve(PlanAgentHeartbeatServiceTag),
|
|
163
|
+
},
|
|
164
|
+
})
|
|
165
|
+
})()
|
|
141
166
|
})
|