@lota-sdk/core 0.1.20 → 0.1.22
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/infrastructure/schema/02_execution_plan.surql +4 -0
- package/package.json +6 -6
- package/src/ai-gateway/ai-gateway.ts +2 -4
- package/src/create-runtime.ts +8 -0
- package/src/queues/document-processor.queue.ts +11 -8
- package/src/queues/index.ts +1 -0
- package/src/queues/plan-agent-heartbeat.queue.ts +100 -0
- package/src/queues/queue-factory.ts +12 -11
- package/src/redis/redis-lease-lock.ts +1 -1
- package/src/runtime/agent-runtime-policy.ts +41 -4
- package/src/runtime/execution-plan-visibility.ts +23 -0
- package/src/runtime/execution-plan.ts +1 -0
- package/src/runtime/runtime-extensions.ts +26 -0
- package/src/runtime/runtime-worker-registry.ts +9 -1
- package/src/services/agent-executor.service.ts +6 -0
- package/src/services/execution-plan.service.ts +51 -36
- package/src/services/index.ts +3 -0
- package/src/services/ownership-dispatcher.service.ts +50 -8
- package/src/services/plan-agent-heartbeat.service.ts +136 -0
- package/src/services/plan-agent-query.service.ts +238 -0
- package/src/services/plan-builder.service.ts +11 -1
- package/src/services/plan-compiler.service.ts +2 -0
- package/src/services/plan-deadline.service.ts +186 -44
- package/src/services/plan-event-delivery.service.ts +170 -0
- package/src/services/plan-executor.service.ts +107 -3
- package/src/services/plan-helpers.ts +13 -0
- package/src/services/plan-run.service.ts +4 -0
- package/src/services/plan-template.service.ts +0 -1
- package/src/services/workstream-turn-preparation.service.ts +452 -176
- package/src/services/workstream-turn.ts +101 -1
- package/src/services/workstream.service.ts +76 -16
- package/src/tools/execution-plan.tool.ts +0 -2
|
@@ -17,6 +17,7 @@ DEFINE FIELD IF NOT EXISTS edges.*.target ON TABLE planSpec TYPE string;
|
|
|
17
17
|
DEFINE FIELD IF NOT EXISTS edges.*.when ON TABLE planSpec TYPE option<string>;
|
|
18
18
|
DEFINE FIELD IF NOT EXISTS edges.*.map ON TABLE planSpec TYPE object FLEXIBLE DEFAULT {};
|
|
19
19
|
DEFINE FIELD IF NOT EXISTS entryNodeIds ON TABLE planSpec TYPE array<string>;
|
|
20
|
+
DEFINE FIELD IF NOT EXISTS defaultExecutionVisibility ON TABLE planSpec TYPE string DEFAULT 'auto';
|
|
20
21
|
DEFINE FIELD IF NOT EXISTS executionMode ON TABLE planSpec TYPE string DEFAULT 'linear';
|
|
21
22
|
DEFINE FIELD IF NOT EXISTS contextEnrichments ON TABLE planSpec TYPE option<array<object>> FLEXIBLE;
|
|
22
23
|
DEFINE FIELD OVERWRITE contextEnrichments.* ON TABLE planSpec TYPE object FLEXIBLE;
|
|
@@ -65,8 +66,10 @@ DEFINE FIELD IF NOT EXISTS failurePolicy.*.note ON TABLE planNodeSpec TYPE strin
|
|
|
65
66
|
DEFINE FIELD IF NOT EXISTS timeoutMs ON TABLE planNodeSpec TYPE option<int>;
|
|
66
67
|
DEFINE FIELD IF NOT EXISTS toolPolicy ON TABLE planNodeSpec TYPE object FLEXIBLE;
|
|
67
68
|
DEFINE FIELD IF NOT EXISTS contextPolicy ON TABLE planNodeSpec TYPE object FLEXIBLE;
|
|
69
|
+
DEFINE FIELD IF NOT EXISTS executionVisibility ON TABLE planNodeSpec TYPE string DEFAULT 'auto';
|
|
68
70
|
DEFINE FIELD IF NOT EXISTS schedule ON TABLE planNodeSpec TYPE option<object> FLEXIBLE;
|
|
69
71
|
DEFINE FIELD IF NOT EXISTS deadline ON TABLE planNodeSpec TYPE option<object> FLEXIBLE;
|
|
72
|
+
DEFINE FIELD IF NOT EXISTS escalation ON TABLE planNodeSpec TYPE option<object> FLEXIBLE;
|
|
70
73
|
DEFINE FIELD IF NOT EXISTS monitoringConfig ON TABLE planNodeSpec TYPE option<object> FLEXIBLE;
|
|
71
74
|
DEFINE FIELD IF NOT EXISTS delayAfterPredecessorMs ON TABLE planNodeSpec TYPE option<int>;
|
|
72
75
|
DEFINE FIELD IF NOT EXISTS deliberationConfig ON TABLE planNodeSpec TYPE option<object> FLEXIBLE;
|
|
@@ -113,6 +116,7 @@ DEFINE FIELD IF NOT EXISTS retryCount ON TABLE planNodeRun TYPE int DEFAULT 0;
|
|
|
113
116
|
DEFINE FIELD IF NOT EXISTS resolvedInput ON TABLE planNodeRun TYPE option<object> FLEXIBLE;
|
|
114
117
|
DEFINE FIELD IF NOT EXISTS latestStructuredOutput ON TABLE planNodeRun TYPE option<object> FLEXIBLE;
|
|
115
118
|
DEFINE FIELD IF NOT EXISTS latestNotes ON TABLE planNodeRun TYPE option<string>;
|
|
119
|
+
DEFINE FIELD IF NOT EXISTS handoffContext ON TABLE planNodeRun TYPE option<object> FLEXIBLE;
|
|
116
120
|
DEFINE FIELD IF NOT EXISTS latestAttemptId ON TABLE planNodeRun TYPE option<record<planNodeAttempt>>;
|
|
117
121
|
DEFINE FIELD IF NOT EXISTS blockedReason ON TABLE planNodeRun TYPE option<string>;
|
|
118
122
|
DEFINE FIELD IF NOT EXISTS failureClass ON TABLE planNodeRun TYPE option<string>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lota-sdk/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"lint": "bunx oxlint --fix -c ../oxlint.config.ts src",
|
|
20
20
|
"format": "bunx oxfmt src",
|
|
21
21
|
"typecheck": "bunx tsgo --noEmit",
|
|
22
|
-
"test:unit": "bun test ../tests/unit/core",
|
|
22
|
+
"test:unit": "bun test --max-concurrency=1 ../tests/unit/core",
|
|
23
23
|
"test:coverage": "bun test --coverage ../tests/unit/core"
|
|
24
24
|
},
|
|
25
25
|
"publishConfig": {
|
|
@@ -32,11 +32,11 @@
|
|
|
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.1.
|
|
36
|
-
"@mendable/firecrawl-js": "^4.
|
|
35
|
+
"@lota-sdk/shared": "0.1.22",
|
|
36
|
+
"@mendable/firecrawl-js": "^4.18.0",
|
|
37
37
|
"@surrealdb/node": "^3.0.3",
|
|
38
|
-
"ai": "^6.0.
|
|
39
|
-
"bullmq": "^5.71.
|
|
38
|
+
"ai": "^6.0.141",
|
|
39
|
+
"bullmq": "^5.71.1",
|
|
40
40
|
"chat": "^4.23.0",
|
|
41
41
|
"cron-parser": "^5.5.0",
|
|
42
42
|
"hono": "^4.12.9",
|
|
@@ -349,7 +349,7 @@ function createAiGatewayFetch(extraParams?: AiGatewayExtraParams): typeof fetch
|
|
|
349
349
|
? injectAiGatewayExtraParamsRequestBody(bodyWithPromptCacheRetention, extraParams)
|
|
350
350
|
: bodyWithPromptCacheRetention
|
|
351
351
|
|
|
352
|
-
const headers = new Headers(init?.headers
|
|
352
|
+
const headers = new Headers(init?.headers)
|
|
353
353
|
if (extraParams !== undefined || (isAiGatewayOpenAIModelRequest(body) && hasAiGatewayPromptCacheRetention(body))) {
|
|
354
354
|
// Bifrost only forwards provider-specific extra params when passthrough is enabled.
|
|
355
355
|
headers.set(AI_GATEWAY_EXTRA_PARAMS_HEADER, 'true')
|
|
@@ -370,9 +370,7 @@ function createAiGatewayProvider(extraParams?: AiGatewayExtraParams) {
|
|
|
370
370
|
return createOpenAI({
|
|
371
371
|
baseURL,
|
|
372
372
|
apiKey,
|
|
373
|
-
headers: {
|
|
374
|
-
[AI_GATEWAY_VIRTUAL_KEY_HEADER]: apiKey,
|
|
375
|
-
},
|
|
373
|
+
headers: { [AI_GATEWAY_VIRTUAL_KEY_HEADER]: apiKey },
|
|
376
374
|
fetch: createAiGatewayFetch(extraParams),
|
|
377
375
|
})
|
|
378
376
|
}
|
package/src/create-runtime.ts
CHANGED
|
@@ -46,6 +46,8 @@ import type { organizationMemberService } from './services/organization-member.s
|
|
|
46
46
|
import { organizationMemberService as organizationMemberServiceSingleton } from './services/organization-member.service'
|
|
47
47
|
import type { organizationService } from './services/organization.service'
|
|
48
48
|
import { organizationService as organizationServiceSingleton } from './services/organization.service'
|
|
49
|
+
import type { planAgentQueryService } from './services/plan-agent-query.service'
|
|
50
|
+
import { planAgentQueryService as planAgentQueryServiceSingleton } from './services/plan-agent-query.service'
|
|
49
51
|
import { playbookRegistryService } from './services/playbook-registry.service'
|
|
50
52
|
import type { recentActivityTitleService } from './services/recent-activity-title.service'
|
|
51
53
|
import { recentActivityTitleService as recentActivityTitleServiceSingleton } from './services/recent-activity-title.service'
|
|
@@ -67,6 +69,7 @@ import type {
|
|
|
67
69
|
createWorkstreamNativeToolApprovalStream,
|
|
68
70
|
createWorkstreamTurnStream,
|
|
69
71
|
runWorkstreamTurnInBackground,
|
|
72
|
+
triggerPlanNodeTurn,
|
|
70
73
|
} from './services/workstream-turn'
|
|
71
74
|
import {
|
|
72
75
|
createWorkstreamApprovalContinuationStream as createWorkstreamApprovalContinuationStreamSingleton,
|
|
@@ -74,6 +77,7 @@ import {
|
|
|
74
77
|
createWorkstreamTurnStream as createWorkstreamTurnStreamSingleton,
|
|
75
78
|
isApprovalContinuationRequest as isApprovalContinuationRequestSingleton,
|
|
76
79
|
runWorkstreamTurnInBackground as runWorkstreamTurnInBackgroundSingleton,
|
|
80
|
+
triggerPlanNodeTurn as triggerPlanNodeTurnSingleton,
|
|
77
81
|
} from './services/workstream-turn'
|
|
78
82
|
import type { workstreamService } from './services/workstream.service'
|
|
79
83
|
import { workstreamService as workstreamServiceSingleton } from './services/workstream.service'
|
|
@@ -126,6 +130,7 @@ export interface LotaRuntime {
|
|
|
126
130
|
recentActivityTitleService: typeof recentActivityTitleService
|
|
127
131
|
socialChatHistoryService: typeof socialChatHistoryServiceSingleton
|
|
128
132
|
executionPlanService: typeof executionPlanService
|
|
133
|
+
planAgentQueryService: typeof planAgentQueryService
|
|
129
134
|
workstreamMessageService: typeof workstreamMessageService
|
|
130
135
|
workstreamService: typeof workstreamService
|
|
131
136
|
workstreamTitleService: typeof workstreamTitleService
|
|
@@ -134,6 +139,7 @@ export interface LotaRuntime {
|
|
|
134
139
|
createWorkstreamTurnStream: typeof createWorkstreamTurnStream
|
|
135
140
|
isApprovalContinuationRequest: typeof isApprovalContinuationRequest
|
|
136
141
|
runWorkstreamTurnInBackground: typeof runWorkstreamTurnInBackground
|
|
142
|
+
triggerPlanNodeTurn: typeof triggerPlanNodeTurn
|
|
137
143
|
syncPlaybookTemplates: typeof playbookRegistryService.syncPlaybookTemplates
|
|
138
144
|
}
|
|
139
145
|
lota: {
|
|
@@ -397,6 +403,7 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
|
|
|
397
403
|
recentActivityTitleService: recentActivityTitleServiceSingleton,
|
|
398
404
|
socialChatHistoryService: socialChatHistoryServiceSingleton,
|
|
399
405
|
executionPlanService: executionPlanServiceSingleton,
|
|
406
|
+
planAgentQueryService: planAgentQueryServiceSingleton,
|
|
400
407
|
workstreamMessageService: workstreamMessageServiceSingleton,
|
|
401
408
|
workstreamService: workstreamServiceSingleton,
|
|
402
409
|
workstreamTitleService: workstreamTitleServiceSingleton,
|
|
@@ -405,6 +412,7 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
|
|
|
405
412
|
createWorkstreamTurnStream: createWorkstreamTurnStreamSingleton,
|
|
406
413
|
isApprovalContinuationRequest: isApprovalContinuationRequestSingleton,
|
|
407
414
|
runWorkstreamTurnInBackground: runWorkstreamTurnInBackgroundSingleton,
|
|
415
|
+
triggerPlanNodeTurn: triggerPlanNodeTurnSingleton,
|
|
408
416
|
syncPlaybookTemplates: playbookRegistryService.syncPlaybookTemplates.bind(playbookRegistryService),
|
|
409
417
|
},
|
|
410
418
|
lota,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Queue, Worker } from 'bullmq'
|
|
2
|
+
import type { QueueOptions } from 'bullmq'
|
|
2
3
|
import type IORedis from 'ioredis'
|
|
3
4
|
|
|
4
5
|
import type { chatLogger } from '../config/logger'
|
|
@@ -70,22 +71,24 @@ export function createDocumentProcessorQueueRuntime<TJob extends DocumentProcess
|
|
|
70
71
|
enqueue: (job: TJob) => Promise<unknown>
|
|
71
72
|
startWorker: (options?: { registerSignals?: boolean }) => WorkerHandle
|
|
72
73
|
} {
|
|
74
|
+
type QueueShape = Queue<TJob, unknown, string, TJob, unknown, string>
|
|
75
|
+
|
|
73
76
|
const queueName = params.queueName ?? DEFAULT_DOCUMENT_PROCESSOR_QUEUE
|
|
74
77
|
const workerName = params.workerName ?? DEFAULT_WORKER_NAME
|
|
75
78
|
const concurrency = params.concurrency ?? 10
|
|
76
79
|
const lockDuration = params.lockDuration ?? 300_000
|
|
77
|
-
const jobName = 'process-document' as Parameters<
|
|
78
|
-
const toQueueData = (job: TJob): Parameters<
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
const jobName = 'process-document' as Parameters<QueueShape['add']>[0]
|
|
81
|
+
const toQueueData = (job: TJob): Parameters<QueueShape['add']>[1] => job
|
|
82
|
+
let queue: QueueShape | null = null
|
|
83
|
+
const getConnection = (): IORedis => params.getConnectionForBullMQ()
|
|
81
84
|
|
|
82
|
-
const getQueue = ():
|
|
85
|
+
const getQueue = (): QueueShape => {
|
|
83
86
|
if (queue) {
|
|
84
87
|
return queue
|
|
85
88
|
}
|
|
86
89
|
|
|
87
|
-
queue = new Queue<TJob, unknown, string>(queueName, {
|
|
88
|
-
connection:
|
|
90
|
+
queue = new Queue<TJob, unknown, string, TJob, unknown, string>(queueName, {
|
|
91
|
+
connection: getConnection() as QueueOptions['connection'],
|
|
89
92
|
defaultJobOptions: { ...DEFAULT_JOB_RETENTION, attempts: 3, backoff: { type: 'exponential', delay: 1000 } },
|
|
90
93
|
})
|
|
91
94
|
|
|
@@ -108,7 +111,7 @@ export function createDocumentProcessorQueueRuntime<TJob extends DocumentProcess
|
|
|
108
111
|
startWorker: (options = {}) => {
|
|
109
112
|
const { registerSignals = import.meta.main } = options
|
|
110
113
|
const worker = new Worker(queueName, params.getWorkerPath(), {
|
|
111
|
-
connection:
|
|
114
|
+
connection: getConnection() as QueueOptions['connection'],
|
|
112
115
|
concurrency,
|
|
113
116
|
lockDuration,
|
|
114
117
|
})
|
package/src/queues/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ export * from './context-compaction.queue'
|
|
|
4
4
|
export * from './delayed-node-promotion.queue'
|
|
5
5
|
export * from './document-processor.queue'
|
|
6
6
|
export * from './memory-consolidation.queue'
|
|
7
|
+
export * from './plan-agent-heartbeat.queue'
|
|
7
8
|
export * from './plan-scheduler.queue'
|
|
8
9
|
export * from './post-chat-memory.queue'
|
|
9
10
|
export * from './recent-activity-title-refinement.queue'
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { Job } from 'bullmq'
|
|
2
|
+
|
|
3
|
+
import { serverLogger } from '../config/logger'
|
|
4
|
+
import { databaseService } from '../db/service'
|
|
5
|
+
import { planAgentHeartbeatService } from '../services/plan-agent-heartbeat.service'
|
|
6
|
+
import type { WorkerHandle } from '../workers/worker-utils'
|
|
7
|
+
import { DEFAULT_JOB_RETENTION, LONG_JOB_LOCK_DURATION_MS } from '../workers/worker-utils'
|
|
8
|
+
import { createQueueFactory } from './queue-factory'
|
|
9
|
+
|
|
10
|
+
export interface PlanAgentHeartbeatWakeJob {
|
|
11
|
+
type: 'wake-node'
|
|
12
|
+
organizationId: string
|
|
13
|
+
workstreamId: string
|
|
14
|
+
runId: string
|
|
15
|
+
nodeId: string
|
|
16
|
+
agentId: string
|
|
17
|
+
reason: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PlanAgentHeartbeatSweepJob {
|
|
21
|
+
type: 'sweep'
|
|
22
|
+
organizationId?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type PlanAgentHeartbeatJob = PlanAgentHeartbeatWakeJob | PlanAgentHeartbeatSweepJob
|
|
26
|
+
|
|
27
|
+
export const PLAN_AGENT_HEARTBEAT_QUEUE = 'plan-agent-heartbeat'
|
|
28
|
+
|
|
29
|
+
async function processPlanAgentHeartbeatJob(job: Job<PlanAgentHeartbeatJob>): Promise<void> {
|
|
30
|
+
await databaseService.connect()
|
|
31
|
+
|
|
32
|
+
if (job.data.type === 'wake-node') {
|
|
33
|
+
await planAgentHeartbeatService.wakeNode(job.data)
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
await planAgentHeartbeatService.sweep({ organizationId: job.data.organizationId })
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const planAgentHeartbeatQueue = createQueueFactory<PlanAgentHeartbeatJob>({
|
|
41
|
+
name: PLAN_AGENT_HEARTBEAT_QUEUE,
|
|
42
|
+
displayName: 'Plan agent heartbeat',
|
|
43
|
+
jobName: 'plan-agent-heartbeat-job',
|
|
44
|
+
concurrency: 2,
|
|
45
|
+
lockDuration: LONG_JOB_LOCK_DURATION_MS,
|
|
46
|
+
stalledInterval: 120_000,
|
|
47
|
+
maxStalledCount: 5,
|
|
48
|
+
defaultJobOptions: { ...DEFAULT_JOB_RETENTION, attempts: 3, backoff: { type: 'exponential', delay: 5_000 } },
|
|
49
|
+
processor: processPlanAgentHeartbeatJob,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
function buildWakeJobId(params: {
|
|
53
|
+
organizationId: string
|
|
54
|
+
workstreamId: string
|
|
55
|
+
runId: string
|
|
56
|
+
nodeId: string
|
|
57
|
+
agentId: string
|
|
58
|
+
reason: string
|
|
59
|
+
}): string {
|
|
60
|
+
const encode = (value: string) => Buffer.from(value).toString('base64url')
|
|
61
|
+
return `plan-agent-wake__${encode(params.runId)}__${encode(params.nodeId)}__${encode(params.agentId)}`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function enqueuePlanAgentHeartbeatWake(params: {
|
|
65
|
+
organizationId: string
|
|
66
|
+
workstreamId: string
|
|
67
|
+
runId: string
|
|
68
|
+
nodeId: string
|
|
69
|
+
agentId: string
|
|
70
|
+
reason: string
|
|
71
|
+
}): Promise<void> {
|
|
72
|
+
await planAgentHeartbeatQueue.enqueue({ type: 'wake-node', ...params }, { jobId: buildWakeJobId(params) })
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const PLAN_AGENT_HEARTBEAT_SCHEDULER_ID = 'plan-agent-heartbeat-sweep'
|
|
76
|
+
|
|
77
|
+
export async function schedulePlanAgentHeartbeatSweep(params?: { everyMs?: number }): Promise<void> {
|
|
78
|
+
const everyMs = params?.everyMs ?? 30_000
|
|
79
|
+
await planAgentHeartbeatQueue
|
|
80
|
+
.getQueue()
|
|
81
|
+
.upsertJobScheduler(
|
|
82
|
+
PLAN_AGENT_HEARTBEAT_SCHEDULER_ID,
|
|
83
|
+
{ every: everyMs },
|
|
84
|
+
{ name: 'plan-agent-heartbeat-job', data: { type: 'sweep' }, opts: { ...DEFAULT_JOB_RETENTION } },
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function startPlanAgentHeartbeatWorker(options: { registerSignals?: boolean } = {}): WorkerHandle {
|
|
89
|
+
const handle = planAgentHeartbeatQueue.startWorker(options)
|
|
90
|
+
|
|
91
|
+
schedulePlanAgentHeartbeatSweep().catch((error: unknown) => {
|
|
92
|
+
serverLogger.error`Plan agent heartbeat scheduler setup failed: ${error}`
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
return handle
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (import.meta.main) {
|
|
99
|
+
startPlanAgentHeartbeatWorker()
|
|
100
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Queue, Worker } from 'bullmq'
|
|
2
|
-
import type { Job, JobsOptions, WorkerOptions } from 'bullmq'
|
|
2
|
+
import type { Job, JobsOptions, QueueOptions, WorkerOptions } from 'bullmq'
|
|
3
3
|
import type IORedis from 'ioredis'
|
|
4
4
|
|
|
5
5
|
import { serverLogger } from '../config/logger'
|
|
@@ -39,18 +39,20 @@ interface QueueFactoryConfigFile extends QueueFactoryConfigBase {
|
|
|
39
39
|
export type QueueFactoryConfig<TJob> = QueueFactoryConfigInline<TJob> | QueueFactoryConfigFile
|
|
40
40
|
|
|
41
41
|
export interface QueueFactory<TJob> {
|
|
42
|
-
getQueue: () => Queue<TJob, unknown, string>
|
|
42
|
+
getQueue: () => Queue<TJob, unknown, string, TJob, unknown, string>
|
|
43
43
|
enqueue: (job: TJob, options?: JobsOptions) => Promise<void>
|
|
44
44
|
startWorker: (options?: { registerSignals?: boolean }) => WorkerHandle
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export function createQueueFactory<TJob>(config: QueueFactoryConfig<TJob>): QueueFactory<TJob> {
|
|
48
|
-
|
|
48
|
+
type QueueShape = Queue<TJob, unknown, string, TJob, unknown, string>
|
|
49
|
+
|
|
50
|
+
let _queue: QueueShape | null = null
|
|
49
51
|
let _queueConnection: IORedis | null = null
|
|
50
52
|
|
|
51
|
-
const getConnection = () => config.connectionProvider?.() ?? getRedisConnectionForBullMQ()
|
|
53
|
+
const getConnection = (): IORedis => config.connectionProvider?.() ?? getRedisConnectionForBullMQ()
|
|
52
54
|
|
|
53
|
-
const getQueue = ():
|
|
55
|
+
const getQueue = (): QueueShape => {
|
|
54
56
|
const connection = getConnection()
|
|
55
57
|
const shouldRecreateQueue =
|
|
56
58
|
_queue === null ||
|
|
@@ -66,8 +68,8 @@ export function createQueueFactory<TJob>(config: QueueFactoryConfig<TJob>): Queu
|
|
|
66
68
|
})
|
|
67
69
|
}
|
|
68
70
|
|
|
69
|
-
_queue = new Queue<TJob, unknown, string>(config.name, {
|
|
70
|
-
connection,
|
|
71
|
+
_queue = new Queue<TJob, unknown, string, TJob, unknown, string>(config.name, {
|
|
72
|
+
connection: connection as QueueOptions['connection'],
|
|
71
73
|
defaultJobOptions: { ...DEFAULT_JOB_RETENTION, ...config.defaultJobOptions },
|
|
72
74
|
})
|
|
73
75
|
_queueConnection = connection
|
|
@@ -78,9 +80,8 @@ export function createQueueFactory<TJob>(config: QueueFactoryConfig<TJob>): Queu
|
|
|
78
80
|
return _queue
|
|
79
81
|
}
|
|
80
82
|
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
const toData = (job: TJob) => job as Parameters<QueueAdd>[1]
|
|
83
|
+
const jobName = config.jobName
|
|
84
|
+
const toData = (job: TJob) => job
|
|
84
85
|
|
|
85
86
|
const enqueue = async (job: TJob, options?: JobsOptions): Promise<void> => {
|
|
86
87
|
const queuedJob = await getQueue().add(jobName, toData(job), options)
|
|
@@ -99,7 +100,7 @@ export function createQueueFactory<TJob>(config: QueueFactoryConfig<TJob>): Queu
|
|
|
99
100
|
const { registerSignals = import.meta.main } = options
|
|
100
101
|
|
|
101
102
|
const workerOptions: WorkerOptions = {
|
|
102
|
-
connection: getConnection(),
|
|
103
|
+
connection: getConnection() as QueueOptions['connection'],
|
|
103
104
|
concurrency: config.concurrency,
|
|
104
105
|
...(config.lockDuration !== undefined ? { lockDuration: config.lockDuration } : {}),
|
|
105
106
|
...(config.stalledInterval !== undefined ? { stalledInterval: config.stalledInterval } : {}),
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ExecutionMode,
|
|
3
|
+
OwnershipDispatchContext,
|
|
4
|
+
PlanArtifactSubmission,
|
|
5
|
+
PlanNodeSpec,
|
|
6
|
+
PlanNodeSpecRecord,
|
|
7
|
+
} from '@lota-sdk/shared'
|
|
2
8
|
|
|
3
9
|
import { getLeadAgentId } from '../config/agent-defaults'
|
|
4
10
|
import { resolveOnboardingOwnerAgentId } from '../config/workstream-defaults'
|
|
@@ -40,6 +46,27 @@ export const OWNERSHIP_DISPATCH_BLOCKED_TOOL_NAMES = Object.freeze([
|
|
|
40
46
|
'teamThink',
|
|
41
47
|
])
|
|
42
48
|
|
|
49
|
+
export function buildCompletionCheckStructuredOutputHints(node: PlanNodeSpec | PlanNodeSpecRecord): string[] {
|
|
50
|
+
const hints: string[] = []
|
|
51
|
+
|
|
52
|
+
for (const check of node.completionChecks) {
|
|
53
|
+
if (check.type === 'llm-judge') {
|
|
54
|
+
const resultField = typeof check.config.resultField === 'string' ? check.config.resultField : 'passed'
|
|
55
|
+
hints.push(`Set structuredOutput.${resultField} = true only when this check is satisfied: ${check.description}`)
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (check.type === 'human-approval') {
|
|
60
|
+
const approvedField = typeof check.config.approvedField === 'string' ? check.config.approvedField : 'approved'
|
|
61
|
+
hints.push(
|
|
62
|
+
`Set structuredOutput.${approvedField} = true only when this human approval check is satisfied: ${check.description}`,
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return hints
|
|
68
|
+
}
|
|
69
|
+
|
|
43
70
|
function buildOwnershipDispatchArtifactPayload(artifacts: PlanArtifactSubmission[]) {
|
|
44
71
|
return artifacts.map((artifact) => ({
|
|
45
72
|
name: artifact.name,
|
|
@@ -206,6 +233,7 @@ export function buildOwnershipDispatchContextSection(params: {
|
|
|
206
233
|
node: PlanNodeSpec
|
|
207
234
|
resolvedInput: Record<string, unknown>
|
|
208
235
|
inputArtifacts: PlanArtifactSubmission[]
|
|
236
|
+
upstreamHandoffs?: OwnershipDispatchContext['upstreamHandoffs']
|
|
209
237
|
}): string {
|
|
210
238
|
const payload = {
|
|
211
239
|
node: {
|
|
@@ -223,14 +251,15 @@ export function buildOwnershipDispatchContextSection(params: {
|
|
|
223
251
|
},
|
|
224
252
|
resolvedInput: params.resolvedInput,
|
|
225
253
|
inputArtifacts: buildOwnershipDispatchArtifactPayload(params.inputArtifacts),
|
|
254
|
+
upstreamHandoffs: params.upstreamHandoffs ?? [],
|
|
226
255
|
}
|
|
227
256
|
|
|
228
257
|
return [
|
|
229
258
|
'<ownership-dispatch-execution>',
|
|
230
259
|
'You are executing a single isolated execution-plan node.',
|
|
231
260
|
'Do not ask the user questions. Do not reference any hidden or prior workstream chat history.',
|
|
232
|
-
'Use only the provided node context, resolved input, and
|
|
233
|
-
'Return only the final structured node result that satisfies the required output contract.',
|
|
261
|
+
'Use only the provided node context, resolved input, input artifacts, and upstream handoff context.',
|
|
262
|
+
'Return only the final structured node result that satisfies the required output contract, including durable handoffContext for downstream nodes.',
|
|
234
263
|
JSON.stringify(payload, null, 2),
|
|
235
264
|
'</ownership-dispatch-execution>',
|
|
236
265
|
].join('\n')
|
|
@@ -241,15 +270,20 @@ export function buildOwnershipDispatchResponseGuard(params: {
|
|
|
241
270
|
executionMode?: ExecutionMode
|
|
242
271
|
}): string {
|
|
243
272
|
const mode = params.executionMode ?? 'linear'
|
|
273
|
+
const completionCheckHints = buildCompletionCheckStructuredOutputHints(params.node)
|
|
244
274
|
|
|
245
275
|
if (mode === 'linear') {
|
|
246
276
|
return [
|
|
247
277
|
'<ownership-dispatch-result-contract>',
|
|
248
278
|
'Return a single JSON object with this exact shape:',
|
|
249
|
-
'{"structuredOutput"?: object, "artifacts": Array<{ "name": string, "kind": "json"|"markdown"|"file"|"external-ref"|"record", "pointer": string, "schemaRef"?: string, "description"?: string, "payload"?: object|array }>, "notes"?: string}',
|
|
279
|
+
'{"structuredOutput"?: object, "artifacts": Array<{ "name": string, "kind": "json"|"markdown"|"file"|"external-ref"|"record", "pointer": string, "schemaRef"?: string, "description"?: string, "payload"?: object|array }>, "notes"?: string, "handoffContext"?: { "summary": string, "keyDecisions"?: string[], "openQuestions"?: string[], "risks"?: string[], "recommendations"?: string[], "references"?: string[] }}',
|
|
250
280
|
'Do not wrap the JSON in markdown or code fences.',
|
|
251
281
|
`Node label: ${params.node.label}`,
|
|
252
282
|
`Required deliverables: ${params.node.deliverables.length > 0 ? params.node.deliverables.map((item) => item.name).join(', ') : 'none'}`,
|
|
283
|
+
...(completionCheckHints.length > 0
|
|
284
|
+
? ['Structured output fields required by completion checks:', ...completionCheckHints]
|
|
285
|
+
: []),
|
|
286
|
+
'If downstream nodes depend on this work, include handoffContext.',
|
|
253
287
|
'</ownership-dispatch-result-contract>',
|
|
254
288
|
].join('\n')
|
|
255
289
|
}
|
|
@@ -263,6 +297,9 @@ export function buildOwnershipDispatchResponseGuard(params: {
|
|
|
263
297
|
.map((d) => d.name)
|
|
264
298
|
.join(', ') || 'none'
|
|
265
299
|
}`,
|
|
300
|
+
...(completionCheckHints.length > 0
|
|
301
|
+
? ['Write these structuredOutput fields when the checks are satisfied:', ...completionCheckHints]
|
|
302
|
+
: []),
|
|
266
303
|
'If writeIntent returns validation_failed, correct and re-call.',
|
|
267
304
|
'After all writes, return a brief summary.',
|
|
268
305
|
'</ownership-dispatch-result-contract>',
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { PlanExecutionVisibility, PlanNodeSpecRecord, PlanSpecRecord } from '@lota-sdk/shared'
|
|
2
|
+
|
|
3
|
+
import { agentRoster } from '../config/agent-defaults'
|
|
4
|
+
|
|
5
|
+
export function resolvePlanNodeExecutionVisibility(
|
|
6
|
+
plan: Pick<PlanSpecRecord, 'defaultExecutionVisibility'>,
|
|
7
|
+
node: Pick<PlanNodeSpecRecord, 'executionVisibility' | 'owner'>,
|
|
8
|
+
): PlanExecutionVisibility {
|
|
9
|
+
const configuredVisibility =
|
|
10
|
+
node.executionVisibility === 'auto' ? plan.defaultExecutionVisibility : node.executionVisibility
|
|
11
|
+
if (configuredVisibility === 'visible' || configuredVisibility === 'silent') {
|
|
12
|
+
return configuredVisibility
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return node.owner.executorType === 'agent' && agentRoster.includes(node.owner.ref) ? 'visible' : 'silent'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function shouldPlanNodeUseVisibleTurn(
|
|
19
|
+
plan: Pick<PlanSpecRecord, 'defaultExecutionVisibility'>,
|
|
20
|
+
node: Pick<PlanNodeSpecRecord, 'executionVisibility' | 'owner'>,
|
|
21
|
+
): boolean {
|
|
22
|
+
return resolvePlanNodeExecutionVisibility(plan, node) === 'visible'
|
|
23
|
+
}
|
|
@@ -7,6 +7,7 @@ const EXECUTION_PLAN_AGENT_PROTOCOL_PROMPT = `<execution-plan-protocol>
|
|
|
7
7
|
- The runtime executor owns lifecycle truth. Do not claim that a node is complete until submitExecutionNodeResult succeeds.
|
|
8
8
|
- Use execution-plan tools to create, replace, inspect, and resume runs.
|
|
9
9
|
- Visible workstream agents do not manually submit node results; dispatched execution nodes are completed by the runtime executor.
|
|
10
|
+
- When the runtime starts a plan-triggered visible execution turn, use the dedicated result-submission tool for that turn and include durable handoffContext for downstream nodes.
|
|
10
11
|
- Treat the active execution runs in <execution-plan-state> as authoritative. Do not mutate run or node status in prose.
|
|
11
12
|
- Work only on nodes that are active or explicitly ready for your executor. If a node is awaiting human input or approval, stop and let the runtime resume it.
|
|
12
13
|
- If the graph, contracts, or success criteria materially change, replace the plan instead of silently drifting.
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PlanEventRecord,
|
|
3
|
+
PlanNodeRunRecord,
|
|
4
|
+
PlanNodeSpecRecord,
|
|
5
|
+
PlanRunRecord,
|
|
6
|
+
PlanSpecRecord,
|
|
7
|
+
} from '@lota-sdk/shared'
|
|
1
8
|
import type { ToolSet } from 'ai'
|
|
2
9
|
|
|
3
10
|
import type { RecordIdRef } from '../db/record-id'
|
|
@@ -76,6 +83,24 @@ export interface LotaRuntimeTeamThinkToolsParams {
|
|
|
76
83
|
toolProviders?: ToolSet
|
|
77
84
|
}
|
|
78
85
|
|
|
86
|
+
export interface LotaRuntimePlanEventEnvelope {
|
|
87
|
+
event: PlanEventRecord
|
|
88
|
+
spec: PlanSpecRecord
|
|
89
|
+
run: PlanRunRecord
|
|
90
|
+
nodeSpec?: PlanNodeSpecRecord
|
|
91
|
+
nodeRun?: PlanNodeRunRecord
|
|
92
|
+
organizationId: string
|
|
93
|
+
workstreamId: string
|
|
94
|
+
runId: string
|
|
95
|
+
planSpecId: string
|
|
96
|
+
userId?: string
|
|
97
|
+
userName?: string
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface LotaRuntimePlanEventAdapter {
|
|
101
|
+
onPlanEvent(event: LotaRuntimePlanEventEnvelope): Promise<void>
|
|
102
|
+
}
|
|
103
|
+
|
|
79
104
|
export interface BuildContextParams {
|
|
80
105
|
workstream: unknown
|
|
81
106
|
workstreamRef: RecordIdRef
|
|
@@ -160,6 +185,7 @@ export interface LotaRuntimeTurnHooks {
|
|
|
160
185
|
|
|
161
186
|
export interface LotaRuntimeAdapters {
|
|
162
187
|
services?: { workspaceProvider?: LotaRuntimeWorkspaceProvider }
|
|
188
|
+
events?: { planEventAdapter?: LotaRuntimePlanEventAdapter }
|
|
163
189
|
workstream?: {
|
|
164
190
|
buildIndexedRepositoriesContext?: (workspaceId: string) => Promise<LotaRuntimeIndexedRepositoriesContext>
|
|
165
191
|
buildTeamThinkAgentTools?: (params: LotaRuntimeTeamThinkToolsParams) => Promise<{ tools: ToolSet }>
|
|
@@ -2,6 +2,7 @@ import { startAutonomousJobWorker } from '../queues/autonomous-job.queue'
|
|
|
2
2
|
import { startContextCompactionWorker } from '../queues/context-compaction.queue'
|
|
3
3
|
import { startDelayedNodePromotionWorker } from '../queues/delayed-node-promotion.queue'
|
|
4
4
|
import { scheduleRecurringConsolidation, startMemoryConsolidationWorker } from '../queues/memory-consolidation.queue'
|
|
5
|
+
import { schedulePlanAgentHeartbeatSweep, startPlanAgentHeartbeatWorker } from '../queues/plan-agent-heartbeat.queue'
|
|
5
6
|
import { startPlanSchedulerWorker } from '../queues/plan-scheduler.queue'
|
|
6
7
|
import { startPostChatMemoryWorker } from '../queues/post-chat-memory.queue'
|
|
7
8
|
import { startRecentActivityTitleRefinementWorker } from '../queues/recent-activity-title-refinement.queue'
|
|
@@ -14,6 +15,7 @@ export interface LotaRuntimeWorkerStartRegistry {
|
|
|
14
15
|
contextCompaction: typeof startContextCompactionWorker
|
|
15
16
|
delayedNodePromotion: typeof startDelayedNodePromotionWorker
|
|
16
17
|
memoryConsolidation: typeof startMemoryConsolidationWorker
|
|
18
|
+
planAgentHeartbeat: typeof startPlanAgentHeartbeatWorker
|
|
17
19
|
planScheduler: typeof startPlanSchedulerWorker
|
|
18
20
|
postChatMemory: typeof startPostChatMemoryWorker
|
|
19
21
|
regularChatMemoryDigest: typeof startRegularChatMemoryDigestWorker
|
|
@@ -24,6 +26,7 @@ export interface LotaRuntimeWorkerStartRegistry {
|
|
|
24
26
|
|
|
25
27
|
export interface LotaRuntimeWorkerScheduleRegistry {
|
|
26
28
|
recurringConsolidation: typeof scheduleRecurringConsolidation
|
|
29
|
+
planAgentHeartbeatSweep: typeof schedulePlanAgentHeartbeatSweep
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
export interface LotaRuntimeWorkers {
|
|
@@ -43,6 +46,7 @@ export function buildRuntimeWorkerRegistry(extraWorkers?: LotaRuntimeWorkerExten
|
|
|
43
46
|
contextCompaction: startContextCompactionWorker,
|
|
44
47
|
delayedNodePromotion: startDelayedNodePromotionWorker,
|
|
45
48
|
memoryConsolidation: startMemoryConsolidationWorker,
|
|
49
|
+
planAgentHeartbeat: startPlanAgentHeartbeatWorker,
|
|
46
50
|
planScheduler: startPlanSchedulerWorker,
|
|
47
51
|
postChatMemory: startPostChatMemoryWorker,
|
|
48
52
|
regularChatMemoryDigest: startRegularChatMemoryDigestWorker,
|
|
@@ -51,6 +55,10 @@ export function buildRuntimeWorkerRegistry(extraWorkers?: LotaRuntimeWorkerExten
|
|
|
51
55
|
recentActivityTitleRefinement: startRecentActivityTitleRefinementWorker,
|
|
52
56
|
...extraWorkers?.start,
|
|
53
57
|
},
|
|
54
|
-
schedule: {
|
|
58
|
+
schedule: {
|
|
59
|
+
recurringConsolidation: scheduleRecurringConsolidation,
|
|
60
|
+
planAgentHeartbeatSweep: schedulePlanAgentHeartbeatSweep,
|
|
61
|
+
...extraWorkers?.schedule,
|
|
62
|
+
},
|
|
55
63
|
}
|
|
56
64
|
}
|
|
@@ -17,6 +17,7 @@ import { databaseService } from '../db/service'
|
|
|
17
17
|
import { TABLES } from '../db/tables'
|
|
18
18
|
import {
|
|
19
19
|
OWNERSHIP_DISPATCH_BLOCKED_TOOL_NAMES,
|
|
20
|
+
buildCompletionCheckStructuredOutputHints,
|
|
20
21
|
buildOwnershipDispatchContextSection,
|
|
21
22
|
buildOwnershipDispatchResponseGuard,
|
|
22
23
|
} from '../runtime/agent-runtime-policy'
|
|
@@ -50,6 +51,7 @@ export function buildWriteIntentDispatchPrompt(nodeSpec: PlanNodeSpec): string {
|
|
|
50
51
|
const deliverables = nodeSpec.deliverables
|
|
51
52
|
.map((d) => `- ${d.name} (${d.kind}${d.required ? ', required' : ''})`)
|
|
52
53
|
.join('\n')
|
|
54
|
+
const completionCheckHints = buildCompletionCheckStructuredOutputHints(nodeSpec)
|
|
53
55
|
return [
|
|
54
56
|
`Execute the execution-plan node "${nodeSpec.label}".`,
|
|
55
57
|
`Objective: ${nodeSpec.objective}`,
|
|
@@ -59,6 +61,9 @@ export function buildWriteIntentDispatchPrompt(nodeSpec: PlanNodeSpec): string {
|
|
|
59
61
|
deliverables,
|
|
60
62
|
'',
|
|
61
63
|
'For each, call writeIntent with targetPath matching the deliverable name.',
|
|
64
|
+
...(completionCheckHints.length > 0
|
|
65
|
+
? ['Also write these structuredOutput fields when the checks are satisfied:', ...completionCheckHints, '']
|
|
66
|
+
: []),
|
|
62
67
|
'If writeIntent returns validation_failed, correct the payload and try again.',
|
|
63
68
|
'When all deliverables are written, end with a brief completion summary.',
|
|
64
69
|
].join('\n')
|
|
@@ -151,6 +156,7 @@ class AgentExecutorService {
|
|
|
151
156
|
node: params.nodeSpec,
|
|
152
157
|
resolvedInput: params.resolvedInput,
|
|
153
158
|
inputArtifacts: params.inputArtifacts,
|
|
159
|
+
upstreamHandoffs: params.context.upstreamHandoffs,
|
|
154
160
|
}),
|
|
155
161
|
],
|
|
156
162
|
responseGuardSection: buildOwnershipDispatchResponseGuard({ node: params.nodeSpec, executionMode: mode }),
|