@lota-sdk/core 0.4.7 → 0.4.9
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 +11 -12
- package/src/ai/embedding-cache.ts +94 -22
- package/src/ai-gateway/ai-gateway.ts +738 -223
- package/src/config/agent-defaults.ts +176 -75
- package/src/config/agent-types.ts +54 -4
- package/src/config/constants.ts +8 -2
- package/src/config/logger.ts +286 -19
- package/src/config/model-constants.ts +1 -0
- package/src/config/thread-defaults.ts +33 -21
- package/src/create-runtime.ts +725 -383
- package/src/db/base.service.ts +52 -28
- package/src/db/cursor-pagination.ts +71 -30
- package/src/db/memory-store.helpers.ts +4 -7
- package/src/db/memory-store.ts +856 -598
- package/src/db/memory.ts +398 -275
- package/src/db/record-id.ts +32 -10
- package/src/db/schema-fingerprint.ts +30 -12
- package/src/db/service-normalization.ts +255 -0
- package/src/db/service.ts +726 -761
- package/src/db/startup.ts +140 -66
- package/src/db/transaction-conflict.ts +15 -0
- package/src/effect/awaitable-effect.ts +87 -0
- package/src/effect/errors.ts +121 -0
- package/src/effect/helpers.ts +98 -0
- package/src/effect/index.ts +22 -0
- package/src/effect/layers.ts +228 -0
- package/src/effect/runtime-ref.ts +25 -0
- package/src/effect/runtime.ts +31 -0
- package/src/effect/services.ts +57 -0
- package/src/effect/zod.ts +43 -0
- package/src/embeddings/provider.ts +122 -71
- package/src/index.ts +46 -1
- package/src/openrouter/direct-provider.ts +29 -0
- package/src/queues/autonomous-job.queue.ts +130 -74
- package/src/queues/context-compaction.queue.ts +60 -15
- package/src/queues/delayed-node-promotion.queue.ts +52 -15
- package/src/queues/document-processor.queue.ts +52 -77
- package/src/queues/memory-consolidation.queue.ts +47 -32
- package/src/queues/organization-learning.queue.ts +13 -4
- package/src/queues/plan-agent-heartbeat.queue.ts +65 -21
- package/src/queues/plan-scheduler.queue.ts +107 -31
- package/src/queues/post-chat-memory.queue.ts +66 -24
- package/src/queues/queue-factory.ts +142 -52
- package/src/queues/standalone-worker.ts +39 -0
- package/src/queues/title-generation.queue.ts +54 -9
- package/src/redis/connection.ts +84 -32
- package/src/redis/index.ts +6 -8
- package/src/redis/org-memory-lock.ts +60 -27
- package/src/redis/redis-lease-lock.ts +200 -121
- package/src/redis/runtime-connection.ts +10 -0
- package/src/redis/stream-context.ts +84 -46
- package/src/runtime/agent-identity-overrides.ts +2 -2
- package/src/runtime/agent-runtime-policy.ts +4 -1
- package/src/runtime/agent-stream-helpers.ts +20 -9
- package/src/runtime/chat-run-orchestration.ts +102 -19
- package/src/runtime/chat-run-registry.ts +36 -2
- package/src/runtime/context-compaction/context-compaction-runtime.ts +107 -0
- package/src/runtime/{context-compaction.ts → context-compaction/context-compaction.ts} +114 -91
- package/src/runtime/execution-plan-visibility.ts +2 -2
- package/src/runtime/execution-plan.ts +42 -15
- package/src/runtime/graph-designer.ts +11 -7
- package/src/runtime/helper-model.ts +135 -48
- package/src/runtime/index.ts +7 -7
- package/src/runtime/indexed-repositories-policy.ts +3 -3
- package/src/runtime/{memory-block.ts → memory/memory-block.ts} +40 -36
- package/src/runtime/{memory-digest-policy.ts → memory/memory-digest-policy.ts} +1 -1
- package/src/runtime/{memory-pipeline.ts → memory/memory-pipeline.ts} +1 -1
- package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
- package/src/runtime/{memory-scope.ts → memory/memory-scope.ts} +12 -6
- package/src/runtime/plugin-resolution.ts +144 -24
- package/src/runtime/plugin-types.ts +9 -1
- package/src/runtime/post-turn-side-effects.ts +197 -130
- package/src/runtime/retrieval-adapters.ts +38 -4
- package/src/runtime/runtime-config.ts +165 -61
- package/src/runtime/runtime-extensions.ts +21 -34
- package/src/runtime/social-chat/social-chat-agent-runner.ts +157 -0
- package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +42 -20
- package/src/runtime/social-chat/social-chat.ts +594 -0
- package/src/runtime/specialist-runner.ts +36 -10
- package/src/runtime/team-consultation/team-consultation-orchestrator.ts +427 -0
- package/src/runtime/{team-consultation-prompts.ts → team-consultation/team-consultation-prompts.ts} +6 -2
- package/src/runtime/thread-chat-helpers.ts +2 -2
- package/src/runtime/thread-plan-turn.ts +2 -1
- package/src/runtime/thread-turn-context.ts +172 -94
- package/src/runtime/turn-lifecycle.ts +93 -27
- package/src/services/agent-activity.service.ts +287 -203
- package/src/services/agent-executor.service.ts +329 -217
- package/src/services/artifact.service.ts +225 -148
- package/src/services/attachment.service.ts +137 -115
- package/src/services/autonomous-job.service.ts +888 -491
- package/src/services/chat-run-registry.service.ts +11 -1
- package/src/services/context-compaction.service.ts +136 -86
- package/src/services/document-chunk.service.ts +162 -90
- package/src/services/execution-plan/execution-plan-approval.ts +26 -0
- package/src/services/execution-plan/execution-plan-context.ts +29 -0
- package/src/services/execution-plan/execution-plan-graph.ts +256 -0
- package/src/services/execution-plan/execution-plan-schedule.ts +84 -0
- package/src/services/execution-plan/execution-plan-spec.ts +75 -0
- package/src/services/execution-plan/execution-plan.service.ts +1041 -0
- package/src/services/feedback-loop.service.ts +132 -76
- package/src/services/global-orchestrator.service.ts +80 -170
- package/src/services/graph-full-routing.ts +182 -0
- package/src/services/index.ts +18 -20
- package/src/services/institutional-memory.service.ts +220 -123
- package/src/services/learned-skill.service.ts +364 -259
- package/src/services/memory/memory-conversation.ts +95 -0
- package/src/services/memory/memory-org-memory.ts +39 -0
- package/src/services/memory/memory-preseeded.ts +80 -0
- package/src/services/memory/memory-rerank.ts +297 -0
- package/src/services/{memory-utils.ts → memory/memory-utils.ts} +5 -5
- package/src/services/memory/memory.service.ts +692 -0
- package/src/services/memory/rerank.service.ts +209 -0
- package/src/services/monitoring-window.service.ts +92 -70
- package/src/services/mutating-approval.service.ts +62 -53
- package/src/services/node-workspace.service.ts +141 -98
- package/src/services/notification.service.ts +17 -16
- package/src/services/organization-member.service.ts +120 -66
- package/src/services/organization.service.ts +144 -51
- package/src/services/ownership-dispatcher.service.ts +415 -264
- package/src/services/plan/plan-agent-heartbeat.service.ts +234 -0
- package/src/services/plan/plan-agent-query.service.ts +322 -0
- package/src/services/plan/plan-approval.service.ts +102 -0
- package/src/services/plan/plan-artifact.service.ts +60 -0
- package/src/services/plan/plan-builder.service.ts +76 -0
- package/src/services/plan/plan-checkpoint.service.ts +103 -0
- package/src/services/{plan-compiler.service.ts → plan/plan-compiler.service.ts} +26 -9
- package/src/services/plan/plan-completion-side-effects.ts +175 -0
- package/src/services/plan/plan-coordination.service.ts +181 -0
- package/src/services/plan/plan-cycle.service.ts +398 -0
- package/src/services/plan/plan-deadline.service.ts +547 -0
- package/src/services/plan/plan-event-delivery.service.ts +261 -0
- package/src/services/plan/plan-executor-context.ts +35 -0
- package/src/services/plan/plan-executor-graph.ts +475 -0
- package/src/services/plan/plan-executor-helpers.ts +322 -0
- package/src/services/plan/plan-executor-persistence.ts +209 -0
- package/src/services/plan/plan-executor.service.ts +1654 -0
- package/src/services/{plan-helpers.ts → plan/plan-helpers.ts} +1 -1
- package/src/services/{plan-run-data.ts → plan/plan-run-data.ts} +4 -4
- package/src/services/plan/plan-run-serialization.ts +15 -0
- package/src/services/plan/plan-run.service.ts +644 -0
- package/src/services/plan/plan-scheduler.service.ts +385 -0
- package/src/services/plan/plan-template.service.ts +224 -0
- package/src/services/plan/plan-transaction-events.ts +33 -0
- package/src/services/plan/plan-validator.service.ts +907 -0
- package/src/services/plan/plan-workspace.service.ts +125 -0
- package/src/services/plugin-executor.service.ts +97 -68
- package/src/services/quality-metrics.service.ts +112 -94
- package/src/services/queue-job.service.ts +296 -230
- package/src/services/recent-activity-title.service.ts +65 -36
- package/src/services/recent-activity.service.ts +274 -259
- package/src/services/skill-resolver.service.ts +38 -12
- package/src/services/social-chat-history.service.ts +176 -125
- package/src/services/system-executor.service.ts +91 -61
- package/src/services/thread/thread-active-run.ts +203 -0
- package/src/services/thread/thread-bootstrap.ts +369 -0
- package/src/services/thread/thread-listing.ts +198 -0
- package/src/services/thread/thread-memory-block.ts +117 -0
- package/src/services/thread/thread-message.service.ts +363 -0
- package/src/services/thread/thread-record-store.ts +155 -0
- package/src/services/thread/thread-title.service.ts +74 -0
- package/src/services/thread/thread-turn-execution.ts +280 -0
- package/src/services/thread/thread-turn-message-context.ts +73 -0
- package/src/services/thread/thread-turn-preparation.service.ts +1146 -0
- package/src/services/thread/thread-turn-streaming.ts +402 -0
- package/src/services/thread/thread-turn-tracing.ts +35 -0
- package/src/services/thread/thread-turn.ts +343 -0
- package/src/services/thread/thread.service.ts +335 -0
- package/src/services/user.service.ts +82 -32
- package/src/services/write-intent-validator.service.ts +63 -51
- package/src/storage/attachment-parser.ts +69 -27
- package/src/storage/attachment-storage.service.ts +331 -275
- package/src/storage/generated-document-storage.service.ts +66 -34
- package/src/system-agents/agent-result.ts +3 -1
- package/src/system-agents/context-compaction.agent.ts +2 -2
- package/src/system-agents/delegated-agent-factory.ts +159 -90
- package/src/system-agents/memory-reranker.agent.ts +2 -2
- package/src/system-agents/memory.agent.ts +2 -2
- package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -2
- package/src/system-agents/regular-chat-memory-digest.agent.ts +2 -2
- package/src/system-agents/skill-extractor.agent.ts +2 -2
- package/src/system-agents/skill-manager.agent.ts +2 -2
- package/src/system-agents/thread-router.agent.ts +157 -113
- package/src/system-agents/title-generator.agent.ts +2 -2
- package/src/tools/execution-plan.tool.ts +220 -161
- package/src/tools/fetch-webpage.tool.ts +21 -17
- package/src/tools/firecrawl-client.ts +16 -6
- package/src/tools/index.ts +1 -0
- package/src/tools/memory-block.tool.ts +14 -6
- package/src/tools/plan-approval.tool.ts +49 -47
- package/src/tools/read-file-parts.tool.ts +44 -33
- package/src/tools/remember-memory.tool.ts +65 -45
- package/src/tools/search-web.tool.ts +26 -22
- package/src/tools/search.tool.ts +41 -29
- package/src/tools/team-think.tool.ts +124 -83
- package/src/tools/user-questions.tool.ts +4 -3
- package/src/tools/web-tool-shared.ts +6 -0
- package/src/utils/async.ts +17 -23
- package/src/utils/crypto.ts +21 -0
- package/src/utils/date-time.ts +40 -1
- package/src/utils/errors.ts +95 -16
- package/src/utils/hono-error-handler.ts +24 -39
- package/src/utils/index.ts +2 -1
- package/src/utils/null-proto-record.ts +41 -0
- package/src/utils/sse-keepalive.ts +124 -21
- package/src/workers/bootstrap.ts +186 -51
- package/src/workers/memory-consolidation.worker.ts +325 -237
- package/src/workers/organization-learning.worker.ts +50 -16
- package/src/workers/regular-chat-memory-digest.helpers.ts +28 -27
- package/src/workers/regular-chat-memory-digest.runner.ts +175 -114
- package/src/workers/skill-extraction.runner.ts +176 -93
- package/src/workers/utils/file-section-chunker.ts +8 -10
- package/src/workers/utils/repo-structure-extractor.ts +349 -260
- package/src/workers/utils/repomix-file-sections.ts +2 -2
- package/src/workers/utils/thread-message-query.ts +97 -38
- package/src/workers/worker-utils.ts +56 -31
- package/src/config/debug-logger.ts +0 -47
- package/src/redis/connection-accessor.ts +0 -26
- package/src/runtime/context-compaction-runtime.ts +0 -87
- package/src/runtime/social-chat-agent-runner.ts +0 -118
- package/src/runtime/social-chat.ts +0 -516
- package/src/runtime/team-consultation-orchestrator.ts +0 -272
- package/src/services/adaptive-playbook.service.ts +0 -152
- package/src/services/artifact-provenance.service.ts +0 -172
- package/src/services/chat-attachments.service.ts +0 -17
- package/src/services/context-compaction-runtime.singleton.ts +0 -13
- package/src/services/execution-plan.service.ts +0 -1118
- package/src/services/memory.service.ts +0 -844
- package/src/services/plan-agent-heartbeat.service.ts +0 -136
- package/src/services/plan-agent-query.service.ts +0 -267
- package/src/services/plan-approval.service.ts +0 -83
- package/src/services/plan-artifact.service.ts +0 -50
- package/src/services/plan-builder.service.ts +0 -67
- package/src/services/plan-checkpoint.service.ts +0 -81
- package/src/services/plan-completion-side-effects.ts +0 -80
- package/src/services/plan-coordination.service.ts +0 -157
- package/src/services/plan-cycle.service.ts +0 -284
- package/src/services/plan-deadline.service.ts +0 -430
- package/src/services/plan-event-delivery.service.ts +0 -166
- package/src/services/plan-executor.service.ts +0 -1950
- package/src/services/plan-run.service.ts +0 -515
- package/src/services/plan-scheduler.service.ts +0 -240
- package/src/services/plan-template.service.ts +0 -177
- package/src/services/plan-validator.service.ts +0 -818
- package/src/services/plan-workspace.service.ts +0 -83
- package/src/services/thread-message.service.ts +0 -275
- package/src/services/thread-plan-registry.service.ts +0 -22
- package/src/services/thread-title.service.ts +0 -39
- package/src/services/thread-turn-preparation.service.ts +0 -1147
- package/src/services/thread-turn.ts +0 -172
- package/src/services/thread.service.ts +0 -869
- package/src/utils/env.ts +0 -8
- /package/src/runtime/{context-compaction-constants.ts → context-compaction/context-compaction-constants.ts} +0 -0
- /package/src/runtime/{memory-format.ts → memory/memory-format.ts} +0 -0
- /package/src/runtime/{memory-prompts-parse.ts → memory/memory-prompts-parse.ts} +0 -0
- /package/src/runtime/{memory-prompts-update.ts → memory/memory-prompts-update.ts} +0 -0
- /package/src/runtime/{social-chat-prompts.ts → social-chat/social-chat-prompts.ts} +0 -0
- /package/src/services/{plan-node-spec.ts → plan/plan-node-spec.ts} +0 -0
- /package/src/services/{thread-constants.ts → thread/thread-constants.ts} +0 -0
- /package/src/services/{thread.types.ts → thread/thread.types.ts} +0 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { PlanArtifactSchema } from '@lota-sdk/shared'
|
|
2
|
+
import type { PlanArtifactRecord, PlanArtifactSubmission } from '@lota-sdk/shared'
|
|
3
|
+
import { Context, Effect, Layer } from 'effect'
|
|
4
|
+
import { RecordId } from 'surrealdb'
|
|
5
|
+
|
|
6
|
+
import type { RecordIdInput } from '../../db/record-id'
|
|
7
|
+
import { ensureRecordId } from '../../db/record-id'
|
|
8
|
+
import type { DatabaseTransaction } from '../../db/service'
|
|
9
|
+
import { TABLES } from '../../db/tables'
|
|
10
|
+
|
|
11
|
+
export function makePlanArtifactService() {
|
|
12
|
+
return {
|
|
13
|
+
persistArtifacts(params: {
|
|
14
|
+
tx: DatabaseTransaction
|
|
15
|
+
runId: RecordIdInput
|
|
16
|
+
attemptId: RecordIdInput
|
|
17
|
+
nodeId: string
|
|
18
|
+
artifacts: PlanArtifactSubmission[]
|
|
19
|
+
}) {
|
|
20
|
+
return Effect.gen(function* () {
|
|
21
|
+
const records: PlanArtifactRecord[] = []
|
|
22
|
+
|
|
23
|
+
// Sequential: SurrealDB transactions require ordered operations
|
|
24
|
+
for (const artifact of params.artifacts) {
|
|
25
|
+
const artifactId = new RecordId(TABLES.PLAN_ARTIFACT, Bun.randomUUIDv7())
|
|
26
|
+
const created = yield* params.tx
|
|
27
|
+
.create(artifactId)
|
|
28
|
+
.content({
|
|
29
|
+
runId: ensureRecordId(params.runId, TABLES.PLAN_RUN),
|
|
30
|
+
attemptId: ensureRecordId(params.attemptId, TABLES.PLAN_NODE_ATTEMPT),
|
|
31
|
+
nodeId: params.nodeId,
|
|
32
|
+
name: artifact.name,
|
|
33
|
+
kind: artifact.kind,
|
|
34
|
+
pointer: artifact.publishedArtifactId
|
|
35
|
+
? `venturos://artifact/${artifact.publishedArtifactId}`
|
|
36
|
+
: `artifact://${params.nodeId}/${artifact.name}`,
|
|
37
|
+
...(artifact.description ? { description: artifact.description } : {}),
|
|
38
|
+
...(artifact.content ? { content: artifact.content } : {}),
|
|
39
|
+
...(artifact.payload ? { payload: artifact.payload } : {}),
|
|
40
|
+
...(artifact.publishedArtifactId
|
|
41
|
+
? { publishedArtifactId: ensureRecordId(artifact.publishedArtifactId, TABLES.ARTIFACT) }
|
|
42
|
+
: {}),
|
|
43
|
+
})
|
|
44
|
+
.output('after')
|
|
45
|
+
|
|
46
|
+
records.push(PlanArtifactSchema.parse(created))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return records
|
|
50
|
+
})
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class PlanArtifactServiceTag extends Context.Service<
|
|
56
|
+
PlanArtifactServiceTag,
|
|
57
|
+
ReturnType<typeof makePlanArtifactService>
|
|
58
|
+
>()('PlanArtifactService') {}
|
|
59
|
+
|
|
60
|
+
export const PlanArtifactServiceLive = Layer.succeed(PlanArtifactServiceTag, makePlanArtifactService())
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { PlanDraft } from '@lota-sdk/shared'
|
|
2
|
+
import { Context, Layer } from 'effect'
|
|
3
|
+
|
|
4
|
+
import { isExecutableConditionExpression } from './plan-helpers'
|
|
5
|
+
|
|
6
|
+
function buildImplicitLinearEdges(draft: PlanDraft) {
|
|
7
|
+
if (draft.edges.length > 0 || draft.nodes.length <= 1) {
|
|
8
|
+
return draft.edges
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return draft.nodes
|
|
12
|
+
.slice(0, -1)
|
|
13
|
+
.map((node, index) => ({
|
|
14
|
+
id: `edge_${node.id}_to_${draft.nodes[index + 1]?.id ?? index + 1}`,
|
|
15
|
+
source: node.id,
|
|
16
|
+
target: draft.nodes[index + 1].id,
|
|
17
|
+
map: {},
|
|
18
|
+
}))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function structureDesign(draft: PlanDraft): PlanDraft {
|
|
22
|
+
return {
|
|
23
|
+
...draft,
|
|
24
|
+
executionMode: draft.executionMode ?? 'linear',
|
|
25
|
+
edges: buildImplicitLinearEdges(draft),
|
|
26
|
+
entryNodeIds: draft.entryNodeIds && draft.entryNodeIds.length > 0 ? draft.entryNodeIds : [draft.nodes[0].id],
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function semanticCompletion(draft: PlanDraft): PlanDraft {
|
|
31
|
+
return {
|
|
32
|
+
...draft,
|
|
33
|
+
nodes: draft.nodes.map((node) => ({
|
|
34
|
+
...node,
|
|
35
|
+
deliverables: [...node.deliverables],
|
|
36
|
+
successCriteria: [...node.successCriteria],
|
|
37
|
+
completionChecks: [...node.completionChecks],
|
|
38
|
+
failurePolicy: [...node.failurePolicy],
|
|
39
|
+
retryPolicy: { ...node.retryPolicy, retryOn: [...node.retryPolicy.retryOn] },
|
|
40
|
+
toolPolicy: { allow: [...node.toolPolicy.allow], deny: [...node.toolPolicy.deny] },
|
|
41
|
+
contextPolicy: {
|
|
42
|
+
retrievalScopes: [...node.contextPolicy.retrievalScopes],
|
|
43
|
+
attachmentPolicy: node.contextPolicy.attachmentPolicy,
|
|
44
|
+
webPolicy: node.contextPolicy.webPolicy,
|
|
45
|
+
},
|
|
46
|
+
})),
|
|
47
|
+
edges: draft.edges.map((edge) => {
|
|
48
|
+
const normalizedWhen = edge.when?.trim()
|
|
49
|
+
const { when: _when, ...edgeWithoutWhen } = edge
|
|
50
|
+
return {
|
|
51
|
+
...edgeWithoutWhen,
|
|
52
|
+
...(normalizedWhen && isExecutableConditionExpression(normalizedWhen) ? { when: normalizedWhen } : {}),
|
|
53
|
+
map: { ...edge.map },
|
|
54
|
+
}
|
|
55
|
+
}),
|
|
56
|
+
schemas: structuredClone(draft.schemas),
|
|
57
|
+
entryNodeIds: [...(draft.entryNodeIds ?? [])],
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function makePlanBuilderService() {
|
|
62
|
+
return {
|
|
63
|
+
structureDesign,
|
|
64
|
+
semanticCompletion,
|
|
65
|
+
prepareDraft(draft: PlanDraft): PlanDraft {
|
|
66
|
+
return semanticCompletion(structureDesign(draft))
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class PlanBuilderServiceTag extends Context.Service<
|
|
72
|
+
PlanBuilderServiceTag,
|
|
73
|
+
ReturnType<typeof makePlanBuilderService>
|
|
74
|
+
>()('PlanBuilderService') {}
|
|
75
|
+
|
|
76
|
+
export const PlanBuilderServiceLive = Layer.succeed(PlanBuilderServiceTag, makePlanBuilderService())
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { PlanCheckpointSchema } from '@lota-sdk/shared'
|
|
2
|
+
import type { PlanRunStatus } from '@lota-sdk/shared'
|
|
3
|
+
import { Context, Effect, Layer } from 'effect'
|
|
4
|
+
import { RecordId } from 'surrealdb'
|
|
5
|
+
|
|
6
|
+
import { serverLogger } from '../../config/logger'
|
|
7
|
+
import type { RecordIdInput } from '../../db/record-id'
|
|
8
|
+
import { ensureRecordId, recordIdToString } from '../../db/record-id'
|
|
9
|
+
import type { SurrealDBService, DatabaseTransaction } from '../../db/service'
|
|
10
|
+
import { TABLES } from '../../db/tables'
|
|
11
|
+
import { DatabaseServiceTag } from '../../effect/services'
|
|
12
|
+
import type { makePlanWorkspaceService } from './plan-workspace.service'
|
|
13
|
+
import { PlanWorkspaceServiceTag } from './plan-workspace.service'
|
|
14
|
+
|
|
15
|
+
interface PlanCheckpointDeps {
|
|
16
|
+
db: SurrealDBService
|
|
17
|
+
planWorkspaceService: ReturnType<typeof makePlanWorkspaceService>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function makePlanCheckpointService(deps: PlanCheckpointDeps) {
|
|
21
|
+
const { db, planWorkspaceService } = deps
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
createCheckpoint: (params: {
|
|
25
|
+
tx: DatabaseTransaction
|
|
26
|
+
runId: RecordIdInput
|
|
27
|
+
sequence: number
|
|
28
|
+
runStatus: PlanRunStatus
|
|
29
|
+
readyNodeIds: string[]
|
|
30
|
+
activeNodeIds: string[]
|
|
31
|
+
artifactIds: RecordIdInput[]
|
|
32
|
+
lastCompletedNodeIds: string[]
|
|
33
|
+
snapshot: Record<string, unknown>
|
|
34
|
+
includeWorkspace?: boolean
|
|
35
|
+
}) =>
|
|
36
|
+
Effect.gen(function* () {
|
|
37
|
+
const snapshotData = { ...params.snapshot }
|
|
38
|
+
|
|
39
|
+
if (params.includeWorkspace) {
|
|
40
|
+
const runIdStr = recordIdToString(params.runId, TABLES.PLAN_RUN)
|
|
41
|
+
const workspaceSnapshotEffect = planWorkspaceService.currentRead({ runId: runIdStr }).pipe(
|
|
42
|
+
Effect.map((all: Record<string, { writeSequence: number }>) => {
|
|
43
|
+
const filtered: Record<string, unknown> = {}
|
|
44
|
+
for (const [key, entry] of Object.entries(all)) {
|
|
45
|
+
if (entry.writeSequence <= params.sequence) {
|
|
46
|
+
filtered[key] = entry
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return filtered
|
|
50
|
+
}),
|
|
51
|
+
Effect.orElseSucceed(() => {
|
|
52
|
+
serverLogger.warn`Workspace snapshot failed for run ${runIdStr}`
|
|
53
|
+
return {} as Record<string, unknown>
|
|
54
|
+
}),
|
|
55
|
+
)
|
|
56
|
+
const workspaceSnapshot: Record<string, unknown> = yield* workspaceSnapshotEffect
|
|
57
|
+
if (Object.keys(workspaceSnapshot).length > 0) {
|
|
58
|
+
snapshotData.workspaceSnapshot = workspaceSnapshot
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const checkpointId = new RecordId(TABLES.PLAN_CHECKPOINT, Bun.randomUUIDv7())
|
|
63
|
+
const created = yield* params.tx
|
|
64
|
+
.create(checkpointId)
|
|
65
|
+
.content({
|
|
66
|
+
runId: ensureRecordId(params.runId, TABLES.PLAN_RUN),
|
|
67
|
+
sequence: params.sequence,
|
|
68
|
+
runStatus: params.runStatus,
|
|
69
|
+
readyNodeIds: [...params.readyNodeIds],
|
|
70
|
+
activeNodeIds: [...params.activeNodeIds],
|
|
71
|
+
artifactIds: params.artifactIds.map((artifactId) => ensureRecordId(artifactId, TABLES.PLAN_ARTIFACT)),
|
|
72
|
+
lastCompletedNodeIds: [...params.lastCompletedNodeIds],
|
|
73
|
+
snapshot: snapshotData,
|
|
74
|
+
})
|
|
75
|
+
.output('after')
|
|
76
|
+
|
|
77
|
+
return PlanCheckpointSchema.parse(created)
|
|
78
|
+
}),
|
|
79
|
+
|
|
80
|
+
loadLatestForRun: (runId: RecordIdInput) =>
|
|
81
|
+
db
|
|
82
|
+
.findMany(TABLES.PLAN_CHECKPOINT, { runId: ensureRecordId(runId, TABLES.PLAN_RUN) }, PlanCheckpointSchema, {
|
|
83
|
+
orderBy: 'sequence',
|
|
84
|
+
orderDir: 'DESC',
|
|
85
|
+
limit: 1,
|
|
86
|
+
})
|
|
87
|
+
.pipe(Effect.map((checkpoints) => checkpoints.at(0) ?? null)),
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export class PlanCheckpointServiceTag extends Context.Service<
|
|
92
|
+
PlanCheckpointServiceTag,
|
|
93
|
+
ReturnType<typeof makePlanCheckpointService>
|
|
94
|
+
>()('PlanCheckpointService') {}
|
|
95
|
+
|
|
96
|
+
export const PlanCheckpointServiceLive = Layer.effect(
|
|
97
|
+
PlanCheckpointServiceTag,
|
|
98
|
+
Effect.gen(function* () {
|
|
99
|
+
const db = yield* DatabaseServiceTag
|
|
100
|
+
const planWorkspaceService = yield* PlanWorkspaceServiceTag
|
|
101
|
+
return makePlanCheckpointService({ db, planWorkspaceService })
|
|
102
|
+
}),
|
|
103
|
+
)
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { PlanDraft, PlanNodeSpec, PlanNodeSpecRecord } from '@lota-sdk/shared'
|
|
2
|
+
import { Context, Effect, Layer } from 'effect'
|
|
2
3
|
|
|
3
|
-
import {
|
|
4
|
+
import { ValidationError } from '../../effect/errors'
|
|
5
|
+
import type { makePlanValidatorService } from './plan-validator.service'
|
|
6
|
+
import { PlanValidatorServiceTag } from './plan-validator.service'
|
|
4
7
|
|
|
5
8
|
export interface CompiledPlanNode {
|
|
6
9
|
node: PlanNodeSpec
|
|
@@ -14,12 +17,14 @@ interface CompiledPlanDraft {
|
|
|
14
17
|
nodes: CompiledPlanNode[]
|
|
15
18
|
}
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
compile(draft: PlanDraft): CompiledPlanDraft {
|
|
20
|
+
export function makePlanCompilerService(planValidatorService: ReturnType<typeof makePlanValidatorService>) {
|
|
21
|
+
function compile(draft: PlanDraft): Effect.Effect<CompiledPlanDraft, ValidationError> {
|
|
19
22
|
const validation = planValidatorService.validateDraft(draft)
|
|
20
23
|
if (validation.blocking.length > 0) {
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
return Effect.fail(
|
|
25
|
+
new ValidationError({
|
|
26
|
+
message: `Plan draft failed validation: ${validation.blocking.map((issue) => `${issue.code}: ${issue.message}`).join(' | ')}`,
|
|
27
|
+
}),
|
|
23
28
|
)
|
|
24
29
|
}
|
|
25
30
|
|
|
@@ -36,7 +41,7 @@ class PlanCompilerService {
|
|
|
36
41
|
downstreamByNodeId.get(edge.source)?.push(edge.target)
|
|
37
42
|
}
|
|
38
43
|
|
|
39
|
-
return {
|
|
44
|
+
return Effect.succeed({
|
|
40
45
|
draft,
|
|
41
46
|
nodes: draft.nodes.map((node, index) => ({
|
|
42
47
|
node,
|
|
@@ -44,10 +49,10 @@ class PlanCompilerService {
|
|
|
44
49
|
upstreamNodeIds: [...(upstreamByNodeId.get(node.id) ?? [])],
|
|
45
50
|
downstreamNodeIds: [...(downstreamByNodeId.get(node.id) ?? [])],
|
|
46
51
|
})),
|
|
47
|
-
}
|
|
52
|
+
})
|
|
48
53
|
}
|
|
49
54
|
|
|
50
|
-
toNodeSpecRecords(
|
|
55
|
+
function toNodeSpecRecords(
|
|
51
56
|
compiled: CompiledPlanDraft,
|
|
52
57
|
): Array<Omit<PlanNodeSpecRecord, 'id' | 'planSpecId' | 'createdAt' | 'updatedAt'>> {
|
|
53
58
|
return compiled.nodes.map(({ node, position, upstreamNodeIds, downstreamNodeIds }) => ({
|
|
@@ -83,6 +88,18 @@ class PlanCompilerService {
|
|
|
83
88
|
downstreamNodeIds,
|
|
84
89
|
}))
|
|
85
90
|
}
|
|
91
|
+
|
|
92
|
+
return { compile, toNodeSpecRecords }
|
|
86
93
|
}
|
|
87
94
|
|
|
88
|
-
export
|
|
95
|
+
export class PlanCompilerServiceTag extends Context.Service<
|
|
96
|
+
PlanCompilerServiceTag,
|
|
97
|
+
ReturnType<typeof makePlanCompilerService>
|
|
98
|
+
>()('PlanCompilerService') {}
|
|
99
|
+
|
|
100
|
+
export const PlanCompilerServiceLive = Layer.effect(
|
|
101
|
+
PlanCompilerServiceTag,
|
|
102
|
+
Effect.map(PlanValidatorServiceTag.asEffect(), (planValidatorService) =>
|
|
103
|
+
makePlanCompilerService(planValidatorService),
|
|
104
|
+
),
|
|
105
|
+
)
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import type { PlanNodeOwner, PlanNodeType } from '@lota-sdk/shared'
|
|
2
|
+
import { PlanEventSchema } from '@lota-sdk/shared'
|
|
3
|
+
import { Schema, Effect, Metric } from 'effect'
|
|
4
|
+
|
|
5
|
+
import { aiLogger } from '../../config/logger'
|
|
6
|
+
import { ensureRecordId } from '../../db/record-id'
|
|
7
|
+
import type { SurrealDBService } from '../../db/service'
|
|
8
|
+
import { TABLES } from '../../db/tables'
|
|
9
|
+
import { runPromise } from '../../effect/runtime'
|
|
10
|
+
import { nowEpochMillis, unsafeDateFrom } from '../../utils/date-time'
|
|
11
|
+
import type { makeFeedbackLoopService } from '../feedback-loop.service'
|
|
12
|
+
import type { makeInstitutionalMemoryService } from '../institutional-memory.service'
|
|
13
|
+
import type { makeQualityMetricsService } from '../quality-metrics.service'
|
|
14
|
+
import type { makePlanEventDeliveryService } from './plan-event-delivery.service'
|
|
15
|
+
import type { makePlanRunService } from './plan-run.service'
|
|
16
|
+
import type { PlanValidationIssueInput } from './plan-validator.service'
|
|
17
|
+
|
|
18
|
+
const planNodeExecutionDuration = Metric.histogram('plan_node_execution_duration_ms', {
|
|
19
|
+
boundaries: Metric.boundariesFromIterable([100, 500, 1000, 5000, 10_000, 30_000, 60_000]),
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
interface PlanCompletionSideEffectsDeps {
|
|
23
|
+
databaseService: SurrealDBService
|
|
24
|
+
feedbackLoopService: ReturnType<typeof makeFeedbackLoopService>
|
|
25
|
+
institutionalMemoryService: ReturnType<typeof makeInstitutionalMemoryService>
|
|
26
|
+
planEventDeliveryService: ReturnType<typeof makePlanEventDeliveryService>
|
|
27
|
+
planRunService: ReturnType<typeof makePlanRunService>
|
|
28
|
+
qualityMetricsService: ReturnType<typeof makeQualityMetricsService>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class PlanCompletionSideEffectsError extends Schema.TaggedErrorClass<PlanCompletionSideEffectsError>()(
|
|
32
|
+
'PlanCompletionSideEffectsError',
|
|
33
|
+
{ message: Schema.String, cause: Schema.Defect },
|
|
34
|
+
) {}
|
|
35
|
+
|
|
36
|
+
function tryPlanCompletionPromise<A>(
|
|
37
|
+
message: string,
|
|
38
|
+
thunk: () => PromiseLike<A> | Effect.Effect<A, unknown>,
|
|
39
|
+
): Effect.Effect<A, PlanCompletionSideEffectsError> {
|
|
40
|
+
return Effect.suspend(() => {
|
|
41
|
+
try {
|
|
42
|
+
const value = thunk()
|
|
43
|
+
if (Effect.isEffect(value)) {
|
|
44
|
+
return value.pipe(Effect.mapError((cause) => new PlanCompletionSideEffectsError({ message, cause })))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return Effect.tryPromise({
|
|
48
|
+
try: () => Promise.resolve(value),
|
|
49
|
+
catch: (cause) => new PlanCompletionSideEffectsError({ message, cause }),
|
|
50
|
+
})
|
|
51
|
+
} catch (cause) {
|
|
52
|
+
return Effect.fail(new PlanCompletionSideEffectsError({ message, cause }))
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function makePlanCompletionSideEffects({
|
|
58
|
+
databaseService,
|
|
59
|
+
feedbackLoopService,
|
|
60
|
+
institutionalMemoryService,
|
|
61
|
+
planEventDeliveryService,
|
|
62
|
+
planRunService,
|
|
63
|
+
qualityMetricsService,
|
|
64
|
+
}: PlanCompletionSideEffectsDeps) {
|
|
65
|
+
function runPlanNodeCompletionSideEffects(params: {
|
|
66
|
+
runId: string
|
|
67
|
+
organizationId: string
|
|
68
|
+
nodeId: string
|
|
69
|
+
nodeLabel: string
|
|
70
|
+
nodeOwnerRef: string
|
|
71
|
+
nodeOwnerType: PlanNodeOwner['executorType']
|
|
72
|
+
nodeType: PlanNodeType
|
|
73
|
+
nodeStartedAt?: string | Date | null
|
|
74
|
+
nodeAttemptCount: number
|
|
75
|
+
artifactCount: number
|
|
76
|
+
validationIssues: PlanValidationIssueInput[]
|
|
77
|
+
}): Promise<void> {
|
|
78
|
+
return runPromise(
|
|
79
|
+
Effect.gen(function* () {
|
|
80
|
+
const executionTimeMs = params.nodeStartedAt
|
|
81
|
+
? nowEpochMillis() - unsafeDateFrom(params.nodeStartedAt).getTime()
|
|
82
|
+
: 0
|
|
83
|
+
if (executionTimeMs > 0) {
|
|
84
|
+
yield* Metric.update(
|
|
85
|
+
Metric.withAttributes(planNodeExecutionDuration, { nodeType: params.nodeType }),
|
|
86
|
+
executionTimeMs,
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
yield* qualityMetricsService.recordNodeMetrics({
|
|
90
|
+
organizationId: params.organizationId,
|
|
91
|
+
runId: params.runId,
|
|
92
|
+
nodeId: params.nodeId,
|
|
93
|
+
metrics: {
|
|
94
|
+
executionTimeMs: Math.max(0, executionTimeMs),
|
|
95
|
+
attemptCount: params.nodeAttemptCount,
|
|
96
|
+
artifactCount: params.artifactCount,
|
|
97
|
+
validationIssueCount: params.validationIssues.length,
|
|
98
|
+
ownerRef: params.nodeOwnerRef,
|
|
99
|
+
ownerType: params.nodeOwnerType,
|
|
100
|
+
nodeType: params.nodeType,
|
|
101
|
+
},
|
|
102
|
+
})
|
|
103
|
+
}),
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function runPlanCompletionSideEffects(params: { runId: string; organizationId: string }): Promise<void> {
|
|
108
|
+
return runPromise(
|
|
109
|
+
Effect.gen(function* () {
|
|
110
|
+
yield* qualityMetricsService.recordCycleMetrics({ organizationId: params.organizationId, runId: params.runId })
|
|
111
|
+
|
|
112
|
+
const recommendations = yield* feedbackLoopService
|
|
113
|
+
.analyzeOutcomes({ runId: params.runId, organizationId: params.organizationId })
|
|
114
|
+
.pipe(
|
|
115
|
+
Effect.mapError(
|
|
116
|
+
(cause) =>
|
|
117
|
+
new PlanCompletionSideEffectsError({ message: 'Failed to analyze plan feedback outcomes.', cause }),
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
if (recommendations.length > 0) {
|
|
121
|
+
const run = yield* planRunService.getRunById(params.runId)
|
|
122
|
+
const specRecord = yield* planRunService.getPlanSpecById(run.planSpecId)
|
|
123
|
+
const event = yield* tryPlanCompletionPromise('Failed to create feedback analyzed plan event.', () =>
|
|
124
|
+
databaseService.create(
|
|
125
|
+
TABLES.PLAN_EVENT,
|
|
126
|
+
{
|
|
127
|
+
planSpecId: ensureRecordId(specRecord.id, TABLES.PLAN_SPEC),
|
|
128
|
+
runId: ensureRecordId(run.id, TABLES.PLAN_RUN),
|
|
129
|
+
eventType: 'feedback-analyzed',
|
|
130
|
+
message: `Feedback analysis produced ${recommendations.length} recommendation(s).`,
|
|
131
|
+
detail: { recommendations },
|
|
132
|
+
emittedBy: 'system',
|
|
133
|
+
},
|
|
134
|
+
PlanEventSchema,
|
|
135
|
+
),
|
|
136
|
+
)
|
|
137
|
+
yield* Effect.tryPromise({
|
|
138
|
+
try: () => planEventDeliveryService.dispatchEvent(event),
|
|
139
|
+
catch: (cause) =>
|
|
140
|
+
new PlanCompletionSideEffectsError({
|
|
141
|
+
message: 'Failed to dispatch feedback analyzed plan event.',
|
|
142
|
+
cause,
|
|
143
|
+
}),
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
yield* institutionalMemoryService
|
|
148
|
+
.extractPatterns({ organizationId: params.organizationId, runId: params.runId })
|
|
149
|
+
.pipe(
|
|
150
|
+
Effect.mapError(
|
|
151
|
+
(cause) =>
|
|
152
|
+
new PlanCompletionSideEffectsError({
|
|
153
|
+
message: 'Failed to extract institutional memory patterns.',
|
|
154
|
+
cause,
|
|
155
|
+
}),
|
|
156
|
+
),
|
|
157
|
+
)
|
|
158
|
+
}),
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function runPlanCompletionSideEffectsSafely(params: { runId: string; organizationId: string }): Promise<void> {
|
|
163
|
+
return runPromise(
|
|
164
|
+
Effect.catch(
|
|
165
|
+
tryPlanCompletionPromise('Plan completion side effects failed.', () => runPlanCompletionSideEffects(params)),
|
|
166
|
+
(error: unknown) =>
|
|
167
|
+
Effect.sync(() => {
|
|
168
|
+
aiLogger.warn`Plan completion side effects failed for run ${params.runId}: ${error instanceof Error ? error.message : String(error)}`
|
|
169
|
+
}),
|
|
170
|
+
),
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return { runPlanNodeCompletionSideEffects, runPlanCompletionSideEffectsSafely }
|
|
175
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import type { PlanArtifactRecord, PlanDependency } from '@lota-sdk/shared'
|
|
2
|
+
import { Context, Schema, Effect, Layer } from 'effect'
|
|
3
|
+
|
|
4
|
+
import { serverLogger } from '../../config/logger'
|
|
5
|
+
import { recordIdToString } from '../../db/record-id'
|
|
6
|
+
import { TABLES } from '../../db/tables'
|
|
7
|
+
import { nowEpochMillis } from '../../utils/date-time'
|
|
8
|
+
import type { makePlanRunService } from './plan-run.service'
|
|
9
|
+
import { PlanRunServiceTag } from './plan-run.service'
|
|
10
|
+
import type { PlanValidationIssueInput } from './plan-validator.service'
|
|
11
|
+
|
|
12
|
+
export interface DependencyResolutionResult {
|
|
13
|
+
resolved: Map<string, PlanArtifactRecord>
|
|
14
|
+
unresolved: PlanDependency[]
|
|
15
|
+
notifications: Array<{ dependency: PlanDependency; reason: string }>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
class PlanCoordinationError extends Schema.TaggedErrorClass<PlanCoordinationError>()('PlanCoordinationError', {
|
|
19
|
+
message: Schema.String,
|
|
20
|
+
cause: Schema.optional(Schema.Defect),
|
|
21
|
+
}) {}
|
|
22
|
+
|
|
23
|
+
function isStale(artifact: Pick<PlanArtifactRecord, 'createdAt'>, maxStalenessMs: number): boolean {
|
|
24
|
+
if (!maxStalenessMs) return false
|
|
25
|
+
return nowEpochMillis() - artifact.createdAt.getTime() > maxStalenessMs
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function validateNoCycles(
|
|
29
|
+
specs: Array<{ id: string; title?: string; dependencies?: PlanDependency[] }>,
|
|
30
|
+
): PlanValidationIssueInput[] {
|
|
31
|
+
const adj = new Map<string, Set<string>>()
|
|
32
|
+
const inDegree = new Map<string, number>()
|
|
33
|
+
const labels = new Map(specs.map((spec) => [spec.id, spec.title ?? spec.id]))
|
|
34
|
+
|
|
35
|
+
for (const spec of specs) {
|
|
36
|
+
if (!adj.has(spec.id)) adj.set(spec.id, new Set())
|
|
37
|
+
if (!inDegree.has(spec.id)) inDegree.set(spec.id, 0)
|
|
38
|
+
|
|
39
|
+
for (const dep of spec.dependencies ?? []) {
|
|
40
|
+
if (!adj.has(dep.sourcePlanSpecId)) adj.set(dep.sourcePlanSpecId, new Set())
|
|
41
|
+
if (!inDegree.has(dep.sourcePlanSpecId)) inDegree.set(dep.sourcePlanSpecId, 0)
|
|
42
|
+
|
|
43
|
+
adj.get(dep.sourcePlanSpecId)?.add(spec.id)
|
|
44
|
+
inDegree.set(spec.id, (inDegree.get(spec.id) ?? 0) + 1)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const queue = [...inDegree.entries()].filter(([, d]) => d === 0).map(([t]) => t)
|
|
49
|
+
const visited = new Set<string>()
|
|
50
|
+
|
|
51
|
+
while (queue.length > 0) {
|
|
52
|
+
const node = queue.shift()
|
|
53
|
+
if (!node) break
|
|
54
|
+
visited.add(node)
|
|
55
|
+
for (const dep of adj.get(node) ?? []) {
|
|
56
|
+
const d = (inDegree.get(dep) ?? 0) - 1
|
|
57
|
+
inDegree.set(dep, d)
|
|
58
|
+
if (d === 0) queue.push(dep)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const unvisited = specs.filter((s) => !visited.has(s.id))
|
|
63
|
+
if (unvisited.length === 0) return []
|
|
64
|
+
|
|
65
|
+
return [
|
|
66
|
+
{
|
|
67
|
+
severity: 'blocking',
|
|
68
|
+
code: 'circular_dependency',
|
|
69
|
+
message: `Circular plan dependencies detected involving: ${unvisited.map((s) => labels.get(s.id) ?? s.id).join(', ')}`,
|
|
70
|
+
},
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function makePlanCoordinationService(planRunService: ReturnType<typeof makePlanRunService>) {
|
|
75
|
+
const resolveDependencies = (params: { dependencies: PlanDependency[]; threadId: string }) => {
|
|
76
|
+
const resolved = new Map<string, PlanArtifactRecord>()
|
|
77
|
+
const unresolved: PlanDependency[] = []
|
|
78
|
+
const notifications: DependencyResolutionResult['notifications'] = []
|
|
79
|
+
|
|
80
|
+
return Effect.gen(function* () {
|
|
81
|
+
const specs = yield* planRunService
|
|
82
|
+
.listPlanSpecsByThread(params.threadId)
|
|
83
|
+
.pipe(
|
|
84
|
+
Effect.mapError(
|
|
85
|
+
(cause) => new PlanCoordinationError({ message: 'Failed to list plan specs by thread.', cause }),
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
for (const dep of params.dependencies) {
|
|
90
|
+
const depKey = `${dep.sourcePlanSpecId}:${dep.sourceNodeId}:${dep.artifactName}`
|
|
91
|
+
const sourceSpec = specs.find((spec) => recordIdToString(spec.id, TABLES.PLAN_SPEC) === dep.sourcePlanSpecId)
|
|
92
|
+
if (!sourceSpec) {
|
|
93
|
+
const reason = `Source plan "${dep.sourcePlanSpecId}" not found in thread.`
|
|
94
|
+
if (dep.triggerMode === 'block') {
|
|
95
|
+
unresolved.push(dep)
|
|
96
|
+
} else if (dep.triggerMode === 'notify') {
|
|
97
|
+
notifications.push({ dependency: dep, reason })
|
|
98
|
+
serverLogger.warn`Dependency unmet (notify): ${reason}`
|
|
99
|
+
}
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const runs = yield* planRunService
|
|
104
|
+
.listRunsBySpec(sourceSpec.id)
|
|
105
|
+
.pipe(
|
|
106
|
+
Effect.mapError(
|
|
107
|
+
(cause) =>
|
|
108
|
+
new PlanCoordinationError({ message: 'Failed to list plan runs for dependency resolution.', cause }),
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
const activeRun = runs.find((run) => run.status === 'completed' || run.status === 'running')
|
|
112
|
+
if (!activeRun) {
|
|
113
|
+
const reason = `No active run found for plan "${sourceSpec.title}".`
|
|
114
|
+
if (dep.triggerMode === 'block') {
|
|
115
|
+
unresolved.push(dep)
|
|
116
|
+
} else if (dep.triggerMode === 'notify') {
|
|
117
|
+
notifications.push({ dependency: dep, reason })
|
|
118
|
+
serverLogger.warn`Dependency unmet (notify): ${reason}`
|
|
119
|
+
}
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const artifacts = yield* planRunService
|
|
124
|
+
.listArtifacts(activeRun.id)
|
|
125
|
+
.pipe(
|
|
126
|
+
Effect.mapError(
|
|
127
|
+
(cause) =>
|
|
128
|
+
new PlanCoordinationError({
|
|
129
|
+
message: 'Failed to list plan artifacts for dependency resolution.',
|
|
130
|
+
cause,
|
|
131
|
+
}),
|
|
132
|
+
),
|
|
133
|
+
)
|
|
134
|
+
const artifact = artifacts.find(
|
|
135
|
+
(candidate) => candidate.nodeId === dep.sourceNodeId && candidate.name === dep.artifactName,
|
|
136
|
+
)
|
|
137
|
+
if (!artifact) {
|
|
138
|
+
const reason = `Artifact "${dep.artifactName}" not found on node "${dep.sourceNodeId}" in plan "${sourceSpec.title}".`
|
|
139
|
+
if (dep.triggerMode === 'block') {
|
|
140
|
+
unresolved.push(dep)
|
|
141
|
+
} else if (dep.triggerMode === 'notify') {
|
|
142
|
+
notifications.push({ dependency: dep, reason })
|
|
143
|
+
serverLogger.warn`Dependency unmet (notify): ${reason}`
|
|
144
|
+
}
|
|
145
|
+
continue
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (dep.maxStalenessMs && isStale(artifact, dep.maxStalenessMs)) {
|
|
149
|
+
const reason = `Artifact "${dep.artifactName}" from plan "${sourceSpec.title}" is stale.`
|
|
150
|
+
if (dep.triggerMode === 'block') {
|
|
151
|
+
unresolved.push(dep)
|
|
152
|
+
continue
|
|
153
|
+
}
|
|
154
|
+
if (dep.triggerMode === 'notify') {
|
|
155
|
+
notifications.push({ dependency: dep, reason })
|
|
156
|
+
serverLogger.warn`Dependency stale (notify): ${reason}`
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
resolved.set(depKey, artifact)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return { resolved, unresolved, notifications }
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return { resolveDependencies, isStale, validateNoCycles }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export class PlanCoordinationServiceTag extends Context.Service<
|
|
171
|
+
PlanCoordinationServiceTag,
|
|
172
|
+
ReturnType<typeof makePlanCoordinationService>
|
|
173
|
+
>()('PlanCoordinationService') {}
|
|
174
|
+
|
|
175
|
+
export const PlanCoordinationServiceLive = Layer.effect(
|
|
176
|
+
PlanCoordinationServiceTag,
|
|
177
|
+
Effect.gen(function* () {
|
|
178
|
+
const planRunService = yield* PlanRunServiceTag
|
|
179
|
+
return makePlanCoordinationService(planRunService)
|
|
180
|
+
}),
|
|
181
|
+
)
|