@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.
- package/infrastructure/schema/02_execution_plan.surql +4 -0
- package/package.json +2 -2
- package/src/ai-gateway/ai-gateway.ts +0 -11
- package/src/config/background-processing.ts +0 -9
- package/src/config/model-constants.ts +0 -1
- package/src/create-runtime.ts +0 -2
- package/src/db/service.ts +7 -25
- package/src/queues/autonomous-job.queue.ts +0 -4
- package/src/runtime/execution-plan.ts +2 -7
- package/src/services/agent-activity.service.ts +35 -5
- package/src/services/artifact.service.ts +12 -14
- package/src/services/autonomous-job.service.ts +8 -12
- package/src/services/execution-plan.service.ts +397 -41
- package/src/services/global-orchestrator.service.ts +1 -1
- package/src/services/ownership-dispatcher.service.ts +1 -1
- package/src/services/plan-agent-query.service.ts +4 -3
- package/src/services/plan-builder.service.ts +1 -6
- package/src/services/plan-deadline.service.ts +8 -9
- package/src/services/plan-run-data.ts +70 -4
- package/src/services/plan-run.service.ts +28 -1
- package/src/services/plan-scheduler.service.ts +4 -4
- package/src/services/plan-template.service.ts +6 -0
- package/src/tools/execution-plan.tool.ts +13 -2
- package/src/tools/index.ts +1 -0
- package/src/tools/plan-approval.tool.ts +66 -0
- package/src/workers/worker-utils.ts +0 -68
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ExecutionPlanToolPlanSummary,
|
|
3
|
+
ExecutionPlanToolResultData,
|
|
4
|
+
PlanRunRecord,
|
|
5
|
+
SerializableExecutionPlan,
|
|
6
|
+
} from '@lota-sdk/shared'
|
|
2
7
|
|
|
3
8
|
import type { RecordIdInput } from '../db/record-id'
|
|
4
9
|
import { ensureRecordId } from '../db/record-id'
|
|
@@ -7,12 +12,25 @@ import { toDatabaseDateTime } from '../utils/date-time'
|
|
|
7
12
|
|
|
8
13
|
export type PlanRunUpdate = Omit<
|
|
9
14
|
Partial<PlanRunRecord>,
|
|
10
|
-
|
|
15
|
+
| 'sourceThreadId'
|
|
16
|
+
| 'createdByAgentId'
|
|
17
|
+
| 'currentNodeId'
|
|
18
|
+
| 'waitingNodeId'
|
|
19
|
+
| 'replacedRunId'
|
|
20
|
+
| 'lastCheckpointId'
|
|
21
|
+
| 'scheduledAt'
|
|
22
|
+
| 'scheduleId'
|
|
23
|
+
| 'startedAt'
|
|
24
|
+
| 'completedAt'
|
|
11
25
|
> & {
|
|
26
|
+
sourceThreadId?: RecordIdInput | null
|
|
27
|
+
createdByAgentId?: string | null
|
|
12
28
|
currentNodeId?: string | null
|
|
13
29
|
waitingNodeId?: string | null
|
|
14
30
|
replacedRunId?: RecordIdInput | null
|
|
15
31
|
lastCheckpointId?: RecordIdInput | null
|
|
32
|
+
scheduledAt?: string | Date | null
|
|
33
|
+
scheduleId?: RecordIdInput | null
|
|
16
34
|
startedAt?: string | Date | null
|
|
17
35
|
completedAt?: string | Date | null
|
|
18
36
|
}
|
|
@@ -22,7 +40,21 @@ export function toRunData(run: PlanRunRecord, patch: PlanRunUpdate) {
|
|
|
22
40
|
planSpecId: ensureRecordId(run.planSpecId, TABLES.PLAN_SPEC),
|
|
23
41
|
organizationId: ensureRecordId(run.organizationId, TABLES.ORGANIZATION),
|
|
24
42
|
threadId: ensureRecordId(run.threadId, TABLES.THREAD),
|
|
43
|
+
...(patch.sourceThreadId === null
|
|
44
|
+
? {}
|
|
45
|
+
: patch.sourceThreadId !== undefined
|
|
46
|
+
? { sourceThreadId: ensureRecordId(patch.sourceThreadId, TABLES.THREAD) }
|
|
47
|
+
: run.sourceThreadId
|
|
48
|
+
? { sourceThreadId: ensureRecordId(run.sourceThreadId, TABLES.THREAD) }
|
|
49
|
+
: {}),
|
|
25
50
|
leadAgentId: patch.leadAgentId ?? run.leadAgentId,
|
|
51
|
+
...(patch.createdByAgentId === null
|
|
52
|
+
? {}
|
|
53
|
+
: patch.createdByAgentId !== undefined
|
|
54
|
+
? { createdByAgentId: patch.createdByAgentId }
|
|
55
|
+
: run.createdByAgentId
|
|
56
|
+
? { createdByAgentId: run.createdByAgentId }
|
|
57
|
+
: {}),
|
|
26
58
|
status: patch.status ?? run.status,
|
|
27
59
|
...(patch.currentNodeId === null
|
|
28
60
|
? {}
|
|
@@ -54,6 +86,20 @@ export function toRunData(run: PlanRunRecord, patch: PlanRunUpdate) {
|
|
|
54
86
|
: run.lastCheckpointId
|
|
55
87
|
? { lastCheckpointId: ensureRecordId(run.lastCheckpointId, TABLES.PLAN_CHECKPOINT) }
|
|
56
88
|
: {}),
|
|
89
|
+
...(patch.scheduledAt === null
|
|
90
|
+
? {}
|
|
91
|
+
: patch.scheduledAt !== undefined
|
|
92
|
+
? { scheduledAt: toDatabaseDateTime(patch.scheduledAt) }
|
|
93
|
+
: run.scheduledAt
|
|
94
|
+
? { scheduledAt: toDatabaseDateTime(run.scheduledAt) }
|
|
95
|
+
: {}),
|
|
96
|
+
...(patch.scheduleId === null
|
|
97
|
+
? {}
|
|
98
|
+
: patch.scheduleId
|
|
99
|
+
? { scheduleId: ensureRecordId(patch.scheduleId, TABLES.PLAN_SCHEDULE) }
|
|
100
|
+
: run.scheduleId
|
|
101
|
+
? { scheduleId: ensureRecordId(run.scheduleId, TABLES.PLAN_SCHEDULE) }
|
|
102
|
+
: {}),
|
|
57
103
|
...(patch.startedAt === null
|
|
58
104
|
? {}
|
|
59
105
|
: patch.startedAt !== undefined
|
|
@@ -71,13 +117,17 @@ export function toRunData(run: PlanRunRecord, patch: PlanRunUpdate) {
|
|
|
71
117
|
}
|
|
72
118
|
}
|
|
73
119
|
|
|
74
|
-
function
|
|
120
|
+
export function toToolPlanSummary(plan: SerializableExecutionPlan): ExecutionPlanToolPlanSummary {
|
|
75
121
|
const completed = plan.progress.completed + plan.progress.partial
|
|
76
122
|
return {
|
|
77
123
|
runId: plan.runId,
|
|
124
|
+
threadId: plan.threadId,
|
|
78
125
|
title: plan.title,
|
|
79
126
|
objective: plan.objective,
|
|
80
127
|
status: plan.status,
|
|
128
|
+
leadAgentId: plan.leadAgentId,
|
|
129
|
+
...(plan.sourceThreadId ? { sourceThreadId: plan.sourceThreadId } : {}),
|
|
130
|
+
...(plan.createdByAgentId ? { createdByAgentId: plan.createdByAgentId } : {}),
|
|
81
131
|
progress: { completed, total: plan.progress.total },
|
|
82
132
|
nodes: plan.nodes.map((node) => ({
|
|
83
133
|
id: node.id,
|
|
@@ -85,7 +135,23 @@ function toSlimPlanSummary(plan: SerializableExecutionPlan): NonNullable<Executi
|
|
|
85
135
|
type: node.type,
|
|
86
136
|
status: node.status,
|
|
87
137
|
ownerRef: node.owner.ref,
|
|
138
|
+
objective: node.objective,
|
|
139
|
+
ownerType: node.owner.executorType,
|
|
140
|
+
artifactCount: plan.artifacts.filter((artifact) => artifact.nodeId === node.id).length,
|
|
141
|
+
approvalId:
|
|
142
|
+
plan.approvals.find((approval) => approval.nodeId === node.id && approval.status === 'pending')?.id ?? null,
|
|
143
|
+
approvalStatus:
|
|
144
|
+
plan.approvals.find((approval) => approval.nodeId === node.id && approval.status === 'pending')?.status ?? null,
|
|
145
|
+
blockedReason: node.blockedReason ?? null,
|
|
146
|
+
latestNotes: node.latestNotes ?? null,
|
|
147
|
+
startedAt: node.startedAt ?? null,
|
|
148
|
+
completedAt: node.completedAt ?? null,
|
|
149
|
+
readyAt: node.readyAt ?? null,
|
|
150
|
+
deliverableNames: node.deliverables.map((deliverable) => deliverable.name),
|
|
151
|
+
upstreamNodeIds: [...node.upstreamNodeIds],
|
|
152
|
+
downstreamNodeIds: [...node.downstreamNodeIds],
|
|
88
153
|
})),
|
|
154
|
+
edges: [...plan.edges],
|
|
89
155
|
activeNodeIds: plan.activeNodeIds,
|
|
90
156
|
readyNodeIds: plan.readyNodeIds,
|
|
91
157
|
}
|
|
@@ -96,7 +162,7 @@ export function buildExecutionPlanToolResult(params: {
|
|
|
96
162
|
plan: SerializableExecutionPlan | null
|
|
97
163
|
message: string
|
|
98
164
|
}): ExecutionPlanToolResultData {
|
|
99
|
-
const slim = params.plan ?
|
|
165
|
+
const slim = params.plan ? toToolPlanSummary(params.plan) : null
|
|
100
166
|
return {
|
|
101
167
|
action: params.action,
|
|
102
168
|
message: params.message,
|
|
@@ -37,7 +37,7 @@ import { databaseService } from '../db/service'
|
|
|
37
37
|
import { TABLES } from '../db/tables'
|
|
38
38
|
import { toIsoDateTimeString, toOptionalIsoDateTimeString } from '../utils/date-time'
|
|
39
39
|
|
|
40
|
-
const ACTIVE_RUN_STATUSES = new Set(['running', 'awaiting-human', 'blocked'])
|
|
40
|
+
const ACTIVE_RUN_STATUSES = new Set(['pending-approval', 'running', 'awaiting-human', 'blocked'])
|
|
41
41
|
|
|
42
42
|
function buildProgress(nodeRuns: PlanNodeRunRecord[]) {
|
|
43
43
|
const counts = nodeRuns.reduce(
|
|
@@ -239,6 +239,31 @@ class PlanRunService {
|
|
|
239
239
|
return runs.filter((run) => ACTIVE_RUN_STATUSES.has(run.status))
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
+
async getRunsCreatedInContext(params: {
|
|
243
|
+
organizationId: RecordIdInput
|
|
244
|
+
sourceThreadId?: RecordIdInput
|
|
245
|
+
createdByAgentId?: string
|
|
246
|
+
statuses?: ReadonlyArray<PlanRunRecord['status']>
|
|
247
|
+
}): Promise<PlanRunRecord[]> {
|
|
248
|
+
const filter: Record<string, unknown> = {
|
|
249
|
+
organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
|
|
250
|
+
}
|
|
251
|
+
if (params.sourceThreadId) {
|
|
252
|
+
filter.sourceThreadId = ensureRecordId(params.sourceThreadId, TABLES.THREAD)
|
|
253
|
+
}
|
|
254
|
+
if (params.createdByAgentId) {
|
|
255
|
+
filter.createdByAgentId = params.createdByAgentId
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const runs = await databaseService.findMany(TABLES.PLAN_RUN, filter, PlanRunSchema, {
|
|
259
|
+
orderBy: 'updatedAt',
|
|
260
|
+
orderDir: 'DESC',
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
const statuses = params.statuses ? new Set(params.statuses) : ACTIVE_RUN_STATUSES
|
|
264
|
+
return runs.filter((run) => statuses.has(run.status))
|
|
265
|
+
}
|
|
266
|
+
|
|
242
267
|
async listNodeRuns(runId: RecordIdInput): Promise<PlanNodeRunRecord[]> {
|
|
243
268
|
return databaseService.findMany(
|
|
244
269
|
TABLES.PLAN_NODE_RUN,
|
|
@@ -435,12 +460,14 @@ class PlanRunService {
|
|
|
435
460
|
specId: recordIdToString(spec.id, TABLES.PLAN_SPEC),
|
|
436
461
|
runId: recordIdToString(run.id, TABLES.PLAN_RUN),
|
|
437
462
|
threadId: recordIdToString(run.threadId, TABLES.THREAD),
|
|
463
|
+
sourceThreadId: run.sourceThreadId ? recordIdToString(run.sourceThreadId, TABLES.THREAD) : undefined,
|
|
438
464
|
organizationId: recordIdToString(run.organizationId, TABLES.ORGANIZATION),
|
|
439
465
|
title: spec.title,
|
|
440
466
|
objective: spec.objective,
|
|
441
467
|
version: spec.version,
|
|
442
468
|
status: run.status,
|
|
443
469
|
leadAgentId: run.leadAgentId,
|
|
470
|
+
createdByAgentId: run.createdByAgentId,
|
|
444
471
|
defaultExecutionVisibility: spec.defaultExecutionVisibility,
|
|
445
472
|
executionMode: spec.executionMode,
|
|
446
473
|
schemaRegistry: slim ? {} : structuredClone(spec.schemaRegistry),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PlanCycleRecordSchema, PlanScheduleRecordSchema } from '@lota-sdk/shared'
|
|
2
2
|
import type { PlanScheduleRecord, PlanScheduleSpec } from '@lota-sdk/shared'
|
|
3
3
|
import { CronExpressionParser } from 'cron-parser'
|
|
4
|
+
import { BoundQuery } from 'surrealdb'
|
|
4
5
|
|
|
5
6
|
import type { RecordIdInput } from '../db/record-id'
|
|
6
7
|
import { ensureRecordId, recordIdToString } from '../db/record-id'
|
|
@@ -156,10 +157,9 @@ class PlanSchedulerService {
|
|
|
156
157
|
/** Re-enqueue BullMQ jobs for all active schedules. Called once at worker startup. */
|
|
157
158
|
async recoverActiveSchedules(): Promise<void> {
|
|
158
159
|
const activeSchedules = await databaseService.queryMany(
|
|
159
|
-
{
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
},
|
|
160
|
+
new BoundQuery(`SELECT * FROM ${TABLES.PLAN_SCHEDULE} WHERE status = $status ORDER BY nextFireAt ASC`, {
|
|
161
|
+
status: 'active',
|
|
162
|
+
}),
|
|
163
163
|
PlanScheduleRecordSchema,
|
|
164
164
|
)
|
|
165
165
|
|
|
@@ -143,7 +143,10 @@ class PlanTemplateService {
|
|
|
143
143
|
templateId: RecordIdInput
|
|
144
144
|
organizationId: RecordIdInput
|
|
145
145
|
threadId: RecordIdInput
|
|
146
|
+
sourceThreadId?: RecordIdInput
|
|
146
147
|
leadAgentId: string
|
|
148
|
+
createdByAgentId?: string
|
|
149
|
+
requireApproval?: boolean
|
|
147
150
|
overrides?: Partial<PlanDraft>
|
|
148
151
|
carryForwardArtifacts?: PlanArtifactRecord[]
|
|
149
152
|
}): Promise<ExecutionPlanToolResultData> {
|
|
@@ -162,7 +165,10 @@ class PlanTemplateService {
|
|
|
162
165
|
return executionPlanService.createPlan({
|
|
163
166
|
organizationId: params.organizationId,
|
|
164
167
|
threadId: params.threadId,
|
|
168
|
+
sourceThreadId: params.sourceThreadId,
|
|
165
169
|
leadAgentId: params.leadAgentId,
|
|
170
|
+
createdByAgentId: params.createdByAgentId,
|
|
171
|
+
requireApproval: params.requireApproval,
|
|
166
172
|
input: draft,
|
|
167
173
|
})
|
|
168
174
|
}
|
|
@@ -56,10 +56,16 @@ export function createExecutionPlanTool(params: {
|
|
|
56
56
|
case 'create': {
|
|
57
57
|
const draft = extractAgentPlanDraft(parsed)
|
|
58
58
|
params.validateInlinePlan?.(draft)
|
|
59
|
+
const targetThreadId = parsed.targetThreadId ?? params.threadId
|
|
60
|
+
const isCrossThreadTarget =
|
|
61
|
+
recordIdToString(targetThreadId, TABLES.THREAD) !== recordIdToString(params.threadId, TABLES.THREAD)
|
|
59
62
|
result = await resolvedEpService.createPlan({
|
|
60
63
|
organizationId: params.orgId,
|
|
61
|
-
threadId:
|
|
64
|
+
threadId: targetThreadId,
|
|
65
|
+
...(isCrossThreadTarget ? { sourceThreadId: params.threadId } : {}),
|
|
62
66
|
leadAgentId: params.agentId,
|
|
67
|
+
createdByAgentId: params.agentId,
|
|
68
|
+
requireApproval: parsed.requireApproval ?? isCrossThreadTarget,
|
|
63
69
|
input: draft,
|
|
64
70
|
})
|
|
65
71
|
break
|
|
@@ -101,11 +107,15 @@ export function createExecutionPlanTool(params: {
|
|
|
101
107
|
const created = await resolvedEpService.createPlan({
|
|
102
108
|
organizationId: params.orgId,
|
|
103
109
|
threadId: targetThread.id,
|
|
110
|
+
sourceThreadId: params.threadId,
|
|
104
111
|
leadAgentId: params.agentId,
|
|
112
|
+
createdByAgentId: params.agentId,
|
|
113
|
+
requireApproval: parsed.requireApproval ?? true,
|
|
105
114
|
input: draft,
|
|
106
115
|
})
|
|
107
116
|
result = {
|
|
108
117
|
...created,
|
|
118
|
+
runId: created.plan?.runId ?? '',
|
|
109
119
|
threadId: targetThread.id,
|
|
110
120
|
threadTitle: targetThread.title,
|
|
111
121
|
createdThread,
|
|
@@ -125,7 +135,8 @@ export function createExecutionPlanTool(params: {
|
|
|
125
135
|
organizationId: params.orgId,
|
|
126
136
|
threadId: params.threadId,
|
|
127
137
|
leadAgentId: params.agentId,
|
|
128
|
-
|
|
138
|
+
createdByAgentId: params.agentId,
|
|
139
|
+
input: { runId: parsed.runId, reason: parsed.reason, requireApproval: parsed.requireApproval, ...draft },
|
|
129
140
|
})
|
|
130
141
|
break
|
|
131
142
|
}
|
package/src/tools/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from './execution-plan.tool'
|
|
2
2
|
export * from './fetch-webpage.tool'
|
|
3
3
|
export * from './memory-block.tool'
|
|
4
|
+
export * from './plan-approval.tool'
|
|
4
5
|
export * from './read-file-parts.tool'
|
|
5
6
|
export * from './remember-memory.tool'
|
|
6
7
|
export * from './research-topic.tool'
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { PlanApprovalArgsSchema, PlanApprovalToolResultDataSchema } from '@lota-sdk/shared'
|
|
2
|
+
import { tool } from 'ai'
|
|
3
|
+
|
|
4
|
+
import type { RecordIdRef } from '../db/record-id'
|
|
5
|
+
import { executionPlanService } from '../services/execution-plan.service'
|
|
6
|
+
import { toToolPlanSummary } from '../services/plan-run-data'
|
|
7
|
+
|
|
8
|
+
type PlanApprovalExecutionPlanService = Pick<typeof executionPlanService, 'approvePlan' | 'rejectPlan'>
|
|
9
|
+
|
|
10
|
+
export function createPlanApprovalTool(params: {
|
|
11
|
+
orgId: RecordIdRef
|
|
12
|
+
threadId: RecordIdRef
|
|
13
|
+
actorId: string
|
|
14
|
+
executionPlanService?: PlanApprovalExecutionPlanService
|
|
15
|
+
}) {
|
|
16
|
+
const resolvedExecutionPlanService = params.executionPlanService ?? executionPlanService
|
|
17
|
+
|
|
18
|
+
return tool({
|
|
19
|
+
description:
|
|
20
|
+
'Approve, reject, or request changes to a plan that is pending approval. Use approve to start execution, reject to abort it, or modify to request a revised plan.',
|
|
21
|
+
inputSchema: PlanApprovalArgsSchema,
|
|
22
|
+
outputSchema: PlanApprovalToolResultDataSchema,
|
|
23
|
+
execute: async (input) => {
|
|
24
|
+
switch (input.action) {
|
|
25
|
+
case 'approve': {
|
|
26
|
+
const plan = await resolvedExecutionPlanService.approvePlan({
|
|
27
|
+
organizationId: params.orgId,
|
|
28
|
+
threadId: params.threadId,
|
|
29
|
+
runId: input.runId,
|
|
30
|
+
emittedBy: params.actorId,
|
|
31
|
+
})
|
|
32
|
+
const toolPlan = toToolPlanSummary(plan)
|
|
33
|
+
return {
|
|
34
|
+
action: 'approved',
|
|
35
|
+
message: `Approved execution plan "${plan.title}" and started it.`,
|
|
36
|
+
hasPlan: true,
|
|
37
|
+
status: toolPlan.status,
|
|
38
|
+
plan: toolPlan,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
case 'reject':
|
|
42
|
+
case 'modify': {
|
|
43
|
+
const plan = await resolvedExecutionPlanService.rejectPlan({
|
|
44
|
+
organizationId: params.orgId,
|
|
45
|
+
threadId: params.threadId,
|
|
46
|
+
runId: input.runId,
|
|
47
|
+
emittedBy: params.actorId,
|
|
48
|
+
reason: input.reason,
|
|
49
|
+
resolution: input.action === 'modify' ? 'changes-requested' : 'rejected',
|
|
50
|
+
})
|
|
51
|
+
const toolPlan = toToolPlanSummary(plan)
|
|
52
|
+
return {
|
|
53
|
+
action: input.action === 'modify' ? 'changes-requested' : 'rejected',
|
|
54
|
+
message:
|
|
55
|
+
input.action === 'modify'
|
|
56
|
+
? `Requested changes for execution plan "${plan.title}".`
|
|
57
|
+
: `Rejected execution plan "${plan.title}".`,
|
|
58
|
+
hasPlan: true,
|
|
59
|
+
status: toolPlan.status,
|
|
60
|
+
plan: toolPlan,
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
}
|
|
@@ -5,17 +5,11 @@ import type { Job, Worker } from 'bullmq'
|
|
|
5
5
|
|
|
6
6
|
import { chatLogger } from '../config/logger'
|
|
7
7
|
import { queueJobService } from '../services/queue-job.service'
|
|
8
|
-
import { truncateText } from '../utils/string'
|
|
9
|
-
|
|
10
8
|
export const DEFAULT_JOB_RETENTION = { removeOnComplete: true, removeOnFail: { age: 24 * 60 * 60, count: 200 } }
|
|
11
9
|
export const LOW_JOB_RETENTION = { removeOnComplete: true, removeOnFail: { age: 6 * 60 * 60, count: 50 } }
|
|
12
10
|
export const LONG_JOB_LOCK_DURATION_MS = 600_000
|
|
13
11
|
|
|
14
12
|
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 10_000
|
|
15
|
-
const MAX_TRACE_STRING_CHARS = 2_000
|
|
16
|
-
const MAX_TRACE_ARRAY_ITEMS = 12
|
|
17
|
-
const MAX_TRACE_OBJECT_KEYS = 24
|
|
18
|
-
const MAX_TRACE_DEPTH = 4
|
|
19
13
|
|
|
20
14
|
export function getWorkerPath(workerName: string): string {
|
|
21
15
|
return fileURLToPath(new URL(path.join('.', workerName), import.meta.url))
|
|
@@ -35,68 +29,6 @@ interface TracedWorkerJobLike {
|
|
|
35
29
|
timestamp?: number
|
|
36
30
|
}
|
|
37
31
|
|
|
38
|
-
function truncateTraceString(value: string, maxChars = MAX_TRACE_STRING_CHARS): string {
|
|
39
|
-
return truncateText(value, maxChars)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function normalizeTraceValue(value: unknown, depth = 0): unknown {
|
|
43
|
-
if (value === null || value === undefined) return value
|
|
44
|
-
if (typeof value === 'string') return truncateTraceString(value)
|
|
45
|
-
if (typeof value === 'number' || typeof value === 'boolean') return value
|
|
46
|
-
if (typeof value === 'bigint') return value.toString()
|
|
47
|
-
if (typeof value === 'symbol') return value.description ? `Symbol(${value.description})` : 'Symbol()'
|
|
48
|
-
if (typeof value === 'function') return value.name ? `[function ${value.name}]` : '[function anonymous]'
|
|
49
|
-
if (value instanceof Date) return value.toISOString()
|
|
50
|
-
|
|
51
|
-
if (depth >= MAX_TRACE_DEPTH) {
|
|
52
|
-
if (Array.isArray(value)) return `[array(${value.length})]`
|
|
53
|
-
return '[object]'
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (Array.isArray(value)) {
|
|
57
|
-
return value.slice(0, MAX_TRACE_ARRAY_ITEMS).map((item) => normalizeTraceValue(item, depth + 1))
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (!(value instanceof Date) && typeof value === 'object') {
|
|
61
|
-
const record = value as Record<string, unknown>
|
|
62
|
-
return Object.fromEntries(
|
|
63
|
-
Object.entries(record)
|
|
64
|
-
.slice(0, MAX_TRACE_OBJECT_KEYS)
|
|
65
|
-
.map(([key, entryValue]) => [key, normalizeTraceValue(entryValue, depth + 1)]),
|
|
66
|
-
)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return '[unknown]'
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function serializeTraceValue(value: unknown): string {
|
|
73
|
-
const serialized = JSON.stringify(normalizeTraceValue(value))
|
|
74
|
-
return truncateTraceString(serialized || 'null')
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function traceTextValue(value: unknown): string {
|
|
78
|
-
if (typeof value === 'string') return truncateTraceString(value)
|
|
79
|
-
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
|
|
80
|
-
return truncateTraceString(String(value))
|
|
81
|
-
}
|
|
82
|
-
if (value instanceof Date) return value.toISOString()
|
|
83
|
-
return serializeTraceValue(value)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function buildWorkerObservationMetadata(
|
|
87
|
-
queueName: string,
|
|
88
|
-
job: { id?: unknown; name: string; attemptsMade: number | null | undefined },
|
|
89
|
-
): Record<string, string> {
|
|
90
|
-
return {
|
|
91
|
-
queue: traceTextValue(queueName),
|
|
92
|
-
job_name: traceTextValue(job.name),
|
|
93
|
-
...(job.id !== undefined ? { job_id: traceTextValue(job.id) } : {}),
|
|
94
|
-
...(job.attemptsMade !== null && job.attemptsMade !== undefined
|
|
95
|
-
? { attempts_made: traceTextValue(job.attemptsMade) }
|
|
96
|
-
: {}),
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
32
|
export const attachWorkerEvents = (worker: Worker, name: string, logger: typeof chatLogger = chatLogger) => {
|
|
101
33
|
worker.on('ready', () => {
|
|
102
34
|
logger.info`${name} worker ready`
|