@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,385 @@
|
|
|
1
|
+
import { PlanCycleRecordSchema, PlanScheduleRecordSchema } from '@lota-sdk/shared'
|
|
2
|
+
import type { PlanScheduleRecord, PlanScheduleSpec } from '@lota-sdk/shared'
|
|
3
|
+
import { Context, Cron, Schema, Effect, Layer, Result } from 'effect'
|
|
4
|
+
import { BoundQuery } from 'surrealdb'
|
|
5
|
+
|
|
6
|
+
import type { RecordIdInput } from '../../db/record-id'
|
|
7
|
+
import { ensureRecordId, recordIdToString } from '../../db/record-id'
|
|
8
|
+
import type { SurrealDBService } from '../../db/service'
|
|
9
|
+
import { TABLES } from '../../db/tables'
|
|
10
|
+
import { NotFoundError } from '../../effect/errors'
|
|
11
|
+
import { effectTryPromise as effectTryPromiseShared } from '../../effect/helpers'
|
|
12
|
+
import { DatabaseServiceTag } from '../../effect/services'
|
|
13
|
+
import { nowDate, nowEpochMillis, toDatabaseDateTime, unsafeDateFrom } from '../../utils/date-time'
|
|
14
|
+
|
|
15
|
+
interface PlanSchedulerRuntimeDeps {
|
|
16
|
+
promoteDelayedNode(params: { runId: string; nodeId: string; emittedBy: string }): Promise<void>
|
|
17
|
+
advanceCycle(cycleId: RecordIdInput): Promise<void>
|
|
18
|
+
recoverDeadlineChecks(): Promise<void>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class PlanSchedulerError extends Schema.TaggedErrorClass<PlanSchedulerError>()('PlanSchedulerError', {
|
|
22
|
+
message: Schema.String,
|
|
23
|
+
cause: Schema.optional(Schema.Defect),
|
|
24
|
+
}) {}
|
|
25
|
+
|
|
26
|
+
function effectTryPromise<A>(
|
|
27
|
+
evaluate: () => PromiseLike<A> | Effect.Effect<A, unknown>,
|
|
28
|
+
message: string,
|
|
29
|
+
): Effect.Effect<A, PlanSchedulerError> {
|
|
30
|
+
return effectTryPromiseShared(evaluate, (cause) => new PlanSchedulerError({ message, cause }))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function failPlanScheduler(message: string, cause?: unknown): Effect.Effect<never, PlanSchedulerError> {
|
|
34
|
+
return Effect.fail(new PlanSchedulerError({ message, ...(cause === undefined ? {} : { cause }) }))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function requireScheduleField<A>(value: A | undefined, message: string): Effect.Effect<A, PlanSchedulerError> {
|
|
38
|
+
return value === undefined ? failPlanScheduler(message) : Effect.succeed(value)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const computeNextCronDateEffect: (cronExpression: string, baseTime: Date) => Effect.Effect<Date, PlanSchedulerError> =
|
|
42
|
+
Effect.fn('PlanScheduler.computeNextCronDate')(function* (cronExpression: string, baseTime: Date) {
|
|
43
|
+
const parsedCron = Cron.parse(cronExpression)
|
|
44
|
+
const cron = Result.isSuccess(parsedCron)
|
|
45
|
+
? parsedCron.success
|
|
46
|
+
: yield* failPlanScheduler(`Invalid cron expression: "${cronExpression}".`)
|
|
47
|
+
|
|
48
|
+
return yield* Effect.try({
|
|
49
|
+
try: () => Cron.next(cron, baseTime),
|
|
50
|
+
catch: (cause) =>
|
|
51
|
+
new PlanSchedulerError({
|
|
52
|
+
message: `Failed to compute the next fire time for cron expression "${cronExpression}".`,
|
|
53
|
+
cause,
|
|
54
|
+
}),
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const computeNextFireAtEffect: (spec: PlanScheduleSpec, baseTime?: Date) => Effect.Effect<Date, PlanSchedulerError> =
|
|
59
|
+
Effect.fn('PlanScheduler.computeNextFireAt')(function* (spec: PlanScheduleSpec, baseTime: Date = nowDate()) {
|
|
60
|
+
switch (spec.type) {
|
|
61
|
+
case 'immediate':
|
|
62
|
+
return baseTime
|
|
63
|
+
|
|
64
|
+
case 'absolute': {
|
|
65
|
+
const at = yield* requireScheduleField(spec.at, 'Absolute schedules require an "at" timestamp.')
|
|
66
|
+
return unsafeDateFrom(at)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
case 'relative': {
|
|
70
|
+
const delayMs = yield* requireScheduleField(spec.delayMs, 'Relative schedules require "delayMs".')
|
|
71
|
+
return unsafeDateFrom(baseTime.getTime() + delayMs)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
case 'cron': {
|
|
75
|
+
const cronExpression = yield* requireScheduleField(spec.cron, 'Cron schedules require a "cron" expression.')
|
|
76
|
+
return yield* computeNextCronDateEffect(cronExpression, baseTime)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
case 'monitoring': {
|
|
80
|
+
const intervalMs = yield* requireScheduleField(spec.intervalMs, 'Monitoring schedules require "intervalMs".')
|
|
81
|
+
return unsafeDateFrom(baseTime.getTime() + intervalMs)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
default:
|
|
85
|
+
return yield* failPlanScheduler('Unsupported schedule type in schedule specification.')
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
export function makePlanSchedulerService(db: SurrealDBService) {
|
|
90
|
+
const loadPlanSchedulerQueue = () =>
|
|
91
|
+
effectTryPromise(() => import('../../queues/plan-scheduler.queue'), 'Failed to load plan scheduler queue module.')
|
|
92
|
+
|
|
93
|
+
const createScheduleEffect = (params: {
|
|
94
|
+
organizationId: RecordIdInput
|
|
95
|
+
threadId: RecordIdInput
|
|
96
|
+
planSpecId?: RecordIdInput
|
|
97
|
+
runId?: RecordIdInput
|
|
98
|
+
nodeId?: string
|
|
99
|
+
scheduleSpec: PlanScheduleSpec
|
|
100
|
+
}): Effect.Effect<PlanScheduleRecord, PlanSchedulerError> =>
|
|
101
|
+
Effect.gen(function* () {
|
|
102
|
+
const nextFireAt = yield* computeNextFireAtEffect(params.scheduleSpec)
|
|
103
|
+
const now = nowDate()
|
|
104
|
+
const record = yield* effectTryPromise(
|
|
105
|
+
() =>
|
|
106
|
+
db.create(
|
|
107
|
+
TABLES.PLAN_SCHEDULE,
|
|
108
|
+
{
|
|
109
|
+
organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
|
|
110
|
+
threadId: ensureRecordId(params.threadId, TABLES.THREAD),
|
|
111
|
+
planSpecId: params.planSpecId ? ensureRecordId(params.planSpecId, TABLES.PLAN_SPEC) : undefined,
|
|
112
|
+
runId: params.runId ? ensureRecordId(params.runId, TABLES.PLAN_RUN) : undefined,
|
|
113
|
+
nodeId: params.nodeId,
|
|
114
|
+
scheduleSpec: params.scheduleSpec,
|
|
115
|
+
status: 'active',
|
|
116
|
+
fireCount: 0,
|
|
117
|
+
nextFireAt: toDatabaseDateTime(nextFireAt),
|
|
118
|
+
createdAt: now,
|
|
119
|
+
},
|
|
120
|
+
PlanScheduleRecordSchema,
|
|
121
|
+
),
|
|
122
|
+
'Failed to create plan schedule.',
|
|
123
|
+
)
|
|
124
|
+
const { enqueueScheduleFire } = yield* loadPlanSchedulerQueue()
|
|
125
|
+
yield* effectTryPromise(
|
|
126
|
+
() =>
|
|
127
|
+
enqueueScheduleFire(
|
|
128
|
+
recordIdToString(record.id, TABLES.PLAN_SCHEDULE),
|
|
129
|
+
Math.max(0, nextFireAt.getTime() - nowEpochMillis()),
|
|
130
|
+
),
|
|
131
|
+
'Failed to enqueue schedule fire job.',
|
|
132
|
+
)
|
|
133
|
+
return record
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
const fireScheduleEffect = (
|
|
137
|
+
schedule: PlanScheduleRecord,
|
|
138
|
+
runtimeDeps: PlanSchedulerRuntimeDeps,
|
|
139
|
+
): Effect.Effect<void, PlanSchedulerError> =>
|
|
140
|
+
Effect.gen(function* () {
|
|
141
|
+
const now = nowDate()
|
|
142
|
+
const newFireCount = schedule.fireCount + 1
|
|
143
|
+
const isRecurring = schedule.scheduleSpec.type === 'cron' || schedule.scheduleSpec.type === 'monitoring'
|
|
144
|
+
const maxReached = schedule.scheduleSpec.maxFires !== undefined && newFireCount >= schedule.scheduleSpec.maxFires
|
|
145
|
+
|
|
146
|
+
let nextFireAt: Date | null = null
|
|
147
|
+
let newStatus: 'active' | 'completed' = 'completed'
|
|
148
|
+
|
|
149
|
+
if (isRecurring && !maxReached) {
|
|
150
|
+
nextFireAt = yield* computeNextFireAtEffect(schedule.scheduleSpec, now)
|
|
151
|
+
newStatus = 'active'
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
yield* effectTryPromise(
|
|
155
|
+
() =>
|
|
156
|
+
db.update(
|
|
157
|
+
TABLES.PLAN_SCHEDULE,
|
|
158
|
+
schedule.id,
|
|
159
|
+
{
|
|
160
|
+
fireCount: newFireCount,
|
|
161
|
+
lastFiredAt: now,
|
|
162
|
+
nextFireAt: toDatabaseDateTime(nextFireAt),
|
|
163
|
+
status: newStatus,
|
|
164
|
+
},
|
|
165
|
+
PlanScheduleRecordSchema,
|
|
166
|
+
),
|
|
167
|
+
'Failed to update fired schedule.',
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
let shouldRecoverDeadlineChecks = false
|
|
171
|
+
if (newStatus === 'active') {
|
|
172
|
+
if (!nextFireAt) {
|
|
173
|
+
return yield* new PlanSchedulerError({ message: 'Recurring schedules must resolve a next fire time.' })
|
|
174
|
+
}
|
|
175
|
+
const { enqueueScheduleFire } = yield* loadPlanSchedulerQueue()
|
|
176
|
+
yield* effectTryPromise(
|
|
177
|
+
() =>
|
|
178
|
+
enqueueScheduleFire(
|
|
179
|
+
recordIdToString(schedule.id, TABLES.PLAN_SCHEDULE),
|
|
180
|
+
Math.max(0, nextFireAt.getTime() - nowEpochMillis()),
|
|
181
|
+
),
|
|
182
|
+
'Failed to enqueue next schedule fire job.',
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const runId = schedule.runId
|
|
187
|
+
const nodeId = schedule.nodeId
|
|
188
|
+
if (runId && nodeId) {
|
|
189
|
+
yield* effectTryPromise(
|
|
190
|
+
() =>
|
|
191
|
+
runtimeDeps.promoteDelayedNode({
|
|
192
|
+
runId: recordIdToString(runId, TABLES.PLAN_RUN),
|
|
193
|
+
nodeId,
|
|
194
|
+
emittedBy: 'plan-scheduler',
|
|
195
|
+
}),
|
|
196
|
+
'Failed to promote delayed plan node.',
|
|
197
|
+
)
|
|
198
|
+
shouldRecoverDeadlineChecks = true
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (schedule.planSpecId && !schedule.runId) {
|
|
202
|
+
const cycle = yield* effectTryPromise(
|
|
203
|
+
() =>
|
|
204
|
+
db.findOne(
|
|
205
|
+
TABLES.PLAN_CYCLE,
|
|
206
|
+
{ scheduleId: ensureRecordId(schedule.id, TABLES.PLAN_SCHEDULE) },
|
|
207
|
+
PlanCycleRecordSchema,
|
|
208
|
+
),
|
|
209
|
+
'Failed to load plan cycle for schedule.',
|
|
210
|
+
)
|
|
211
|
+
if (cycle) {
|
|
212
|
+
yield* effectTryPromise(() => runtimeDeps.advanceCycle(cycle.id), 'Failed to advance plan cycle.')
|
|
213
|
+
shouldRecoverDeadlineChecks = true
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (shouldRecoverDeadlineChecks) {
|
|
218
|
+
yield* effectTryPromise(() => runtimeDeps.recoverDeadlineChecks(), 'Failed to recover deadline checks.')
|
|
219
|
+
}
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
const fireScheduleByIdEffect = (scheduleId: string, runtimeDeps: PlanSchedulerRuntimeDeps) =>
|
|
223
|
+
Effect.gen(function* () {
|
|
224
|
+
const schedule = yield* effectTryPromise(
|
|
225
|
+
() =>
|
|
226
|
+
db.findOne(
|
|
227
|
+
TABLES.PLAN_SCHEDULE,
|
|
228
|
+
{ id: ensureRecordId(scheduleId, TABLES.PLAN_SCHEDULE) },
|
|
229
|
+
PlanScheduleRecordSchema,
|
|
230
|
+
),
|
|
231
|
+
'Failed to load schedule by id.',
|
|
232
|
+
)
|
|
233
|
+
if (!schedule || schedule.status !== 'active') return
|
|
234
|
+
|
|
235
|
+
yield* fireScheduleEffect(schedule, runtimeDeps)
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
const recoverActiveSchedulesEffect = () =>
|
|
239
|
+
Effect.gen(function* () {
|
|
240
|
+
const activeSchedules = yield* effectTryPromise(
|
|
241
|
+
() =>
|
|
242
|
+
db.queryMany(
|
|
243
|
+
new BoundQuery(`SELECT * FROM ${TABLES.PLAN_SCHEDULE} WHERE status = $status ORDER BY nextFireAt ASC`, {
|
|
244
|
+
status: 'active',
|
|
245
|
+
}),
|
|
246
|
+
PlanScheduleRecordSchema,
|
|
247
|
+
),
|
|
248
|
+
'Failed to load active schedules.',
|
|
249
|
+
)
|
|
250
|
+
const { enqueueScheduleFire } = yield* loadPlanSchedulerQueue()
|
|
251
|
+
yield* Effect.forEach(
|
|
252
|
+
activeSchedules.filter(
|
|
253
|
+
(schedule): schedule is typeof schedule & { nextFireAt: Date } => schedule.nextFireAt !== undefined,
|
|
254
|
+
),
|
|
255
|
+
(schedule) =>
|
|
256
|
+
effectTryPromise(
|
|
257
|
+
() =>
|
|
258
|
+
enqueueScheduleFire(
|
|
259
|
+
recordIdToString(schedule.id, TABLES.PLAN_SCHEDULE),
|
|
260
|
+
Math.max(0, unsafeDateFrom(schedule.nextFireAt).getTime() - nowEpochMillis()),
|
|
261
|
+
),
|
|
262
|
+
`Failed to re-enqueue schedule ${recordIdToString(schedule.id, TABLES.PLAN_SCHEDULE)}.`,
|
|
263
|
+
),
|
|
264
|
+
{ concurrency: 5, discard: true },
|
|
265
|
+
)
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
const cancelScheduleEffect = (scheduleId: RecordIdInput) => {
|
|
269
|
+
const idStr = recordIdToString(scheduleId, TABLES.PLAN_SCHEDULE)
|
|
270
|
+
return Effect.gen(function* () {
|
|
271
|
+
const { removeScheduleFireJob } = yield* loadPlanSchedulerQueue()
|
|
272
|
+
yield* effectTryPromise(() => removeScheduleFireJob(idStr), 'Failed to remove schedule fire job.')
|
|
273
|
+
yield* effectTryPromise(
|
|
274
|
+
() =>
|
|
275
|
+
db.update(
|
|
276
|
+
TABLES.PLAN_SCHEDULE,
|
|
277
|
+
ensureRecordId(scheduleId, TABLES.PLAN_SCHEDULE),
|
|
278
|
+
{ status: 'cancelled' },
|
|
279
|
+
PlanScheduleRecordSchema,
|
|
280
|
+
),
|
|
281
|
+
'Failed to cancel schedule.',
|
|
282
|
+
)
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const pauseScheduleEffect = (scheduleId: RecordIdInput) => {
|
|
287
|
+
const idStr = recordIdToString(scheduleId, TABLES.PLAN_SCHEDULE)
|
|
288
|
+
return Effect.gen(function* () {
|
|
289
|
+
const { removeScheduleFireJob } = yield* loadPlanSchedulerQueue()
|
|
290
|
+
yield* effectTryPromise(() => removeScheduleFireJob(idStr), 'Failed to remove schedule fire job.')
|
|
291
|
+
yield* effectTryPromise(
|
|
292
|
+
() =>
|
|
293
|
+
db.update(
|
|
294
|
+
TABLES.PLAN_SCHEDULE,
|
|
295
|
+
ensureRecordId(scheduleId, TABLES.PLAN_SCHEDULE),
|
|
296
|
+
{ status: 'paused' },
|
|
297
|
+
PlanScheduleRecordSchema,
|
|
298
|
+
),
|
|
299
|
+
'Failed to pause schedule.',
|
|
300
|
+
)
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const resumeScheduleEffect = (scheduleId: RecordIdInput) =>
|
|
305
|
+
Effect.gen(function* () {
|
|
306
|
+
const schedule = yield* effectTryPromise(
|
|
307
|
+
() =>
|
|
308
|
+
db.findOne(
|
|
309
|
+
TABLES.PLAN_SCHEDULE,
|
|
310
|
+
{ id: ensureRecordId(scheduleId, TABLES.PLAN_SCHEDULE) },
|
|
311
|
+
PlanScheduleRecordSchema,
|
|
312
|
+
),
|
|
313
|
+
'Failed to load schedule for resume.',
|
|
314
|
+
)
|
|
315
|
+
if (!schedule) {
|
|
316
|
+
return yield* new NotFoundError({
|
|
317
|
+
resource: TABLES.PLAN_SCHEDULE,
|
|
318
|
+
id: recordIdToString(scheduleId, TABLES.PLAN_SCHEDULE),
|
|
319
|
+
message: `Schedule not found: ${recordIdToString(scheduleId, TABLES.PLAN_SCHEDULE)}`,
|
|
320
|
+
})
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const nextFireAt = yield* computeNextFireAtEffect(schedule.scheduleSpec)
|
|
324
|
+
yield* effectTryPromise(
|
|
325
|
+
() =>
|
|
326
|
+
db.update(
|
|
327
|
+
TABLES.PLAN_SCHEDULE,
|
|
328
|
+
ensureRecordId(scheduleId, TABLES.PLAN_SCHEDULE),
|
|
329
|
+
{ status: 'active', nextFireAt: toDatabaseDateTime(nextFireAt) },
|
|
330
|
+
PlanScheduleRecordSchema,
|
|
331
|
+
),
|
|
332
|
+
'Failed to resume schedule.',
|
|
333
|
+
)
|
|
334
|
+
const { enqueueScheduleFire } = yield* loadPlanSchedulerQueue()
|
|
335
|
+
yield* effectTryPromise(
|
|
336
|
+
() =>
|
|
337
|
+
enqueueScheduleFire(
|
|
338
|
+
recordIdToString(scheduleId, TABLES.PLAN_SCHEDULE),
|
|
339
|
+
Math.max(0, nextFireAt.getTime() - nowEpochMillis()),
|
|
340
|
+
),
|
|
341
|
+
'Failed to enqueue resumed schedule.',
|
|
342
|
+
)
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
const listSchedulesEffect = (threadId: RecordIdInput) =>
|
|
346
|
+
effectTryPromise(
|
|
347
|
+
() =>
|
|
348
|
+
db.findMany(
|
|
349
|
+
TABLES.PLAN_SCHEDULE,
|
|
350
|
+
{ threadId: ensureRecordId(threadId, TABLES.THREAD) },
|
|
351
|
+
PlanScheduleRecordSchema,
|
|
352
|
+
{ orderBy: 'createdAt', orderDir: 'ASC' },
|
|
353
|
+
),
|
|
354
|
+
'Failed to list schedules.',
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
createSchedule: createScheduleEffect,
|
|
359
|
+
computeNextFireAt(spec: PlanScheduleSpec, baseTime: Date = nowDate()): Date {
|
|
360
|
+
return Effect.runSync(computeNextFireAtEffect(spec, baseTime))
|
|
361
|
+
},
|
|
362
|
+
/** Called by the BullMQ worker when a fire-schedule job executes. */
|
|
363
|
+
fireScheduleById: fireScheduleByIdEffect,
|
|
364
|
+
fireSchedule: fireScheduleEffect,
|
|
365
|
+
/** Re-enqueue BullMQ jobs for all active schedules. Called once at worker startup. */
|
|
366
|
+
recoverActiveSchedules: recoverActiveSchedulesEffect,
|
|
367
|
+
cancelSchedule: cancelScheduleEffect,
|
|
368
|
+
pauseSchedule: pauseScheduleEffect,
|
|
369
|
+
resumeSchedule: resumeScheduleEffect,
|
|
370
|
+
listSchedules: listSchedulesEffect,
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export class PlanSchedulerServiceTag extends Context.Service<
|
|
375
|
+
PlanSchedulerServiceTag,
|
|
376
|
+
ReturnType<typeof makePlanSchedulerService>
|
|
377
|
+
>()('PlanSchedulerService') {}
|
|
378
|
+
|
|
379
|
+
export const PlanSchedulerServiceLive = Layer.effect(
|
|
380
|
+
PlanSchedulerServiceTag,
|
|
381
|
+
Effect.gen(function* () {
|
|
382
|
+
const db = yield* DatabaseServiceTag
|
|
383
|
+
return makePlanSchedulerService(db)
|
|
384
|
+
}),
|
|
385
|
+
)
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { PlanTemplateRecordSchema } from '@lota-sdk/shared'
|
|
2
|
+
import type { PlanArtifactRecord, PlanDraft } from '@lota-sdk/shared'
|
|
3
|
+
import { Context, Schema, Effect, Layer } from 'effect'
|
|
4
|
+
|
|
5
|
+
import type { RecordIdInput } from '../../db/record-id'
|
|
6
|
+
import { ensureRecordId, recordIdToString } from '../../db/record-id'
|
|
7
|
+
import type { SurrealDBService } from '../../db/service'
|
|
8
|
+
import { TABLES } from '../../db/tables'
|
|
9
|
+
import { ValidationError } from '../../effect/errors'
|
|
10
|
+
import { DatabaseServiceTag } from '../../effect/services'
|
|
11
|
+
import { nowDate } from '../../utils/date-time'
|
|
12
|
+
import type { makeExecutionPlanService } from '../execution-plan/execution-plan.service'
|
|
13
|
+
import { ExecutionPlanServiceTag } from '../execution-plan/execution-plan.service'
|
|
14
|
+
|
|
15
|
+
interface PlanTemplateDeps {
|
|
16
|
+
db: SurrealDBService
|
|
17
|
+
executionPlanService: Pick<ReturnType<typeof makeExecutionPlanService>, 'createPlan'>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type CreateTemplateParams = {
|
|
21
|
+
organizationId: RecordIdInput
|
|
22
|
+
name: string
|
|
23
|
+
description?: string
|
|
24
|
+
draft: PlanDraft
|
|
25
|
+
tags?: string[]
|
|
26
|
+
source?: 'user' | 'playbook' | 'system'
|
|
27
|
+
sourceRef?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type GetTemplateBySourceRefParams = {
|
|
31
|
+
organizationId: RecordIdInput
|
|
32
|
+
source: 'user' | 'playbook' | 'system'
|
|
33
|
+
sourceRef: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type ListTemplatesParams = { tags?: string[]; source?: string }
|
|
37
|
+
|
|
38
|
+
type UpdateTemplatePatch = Partial<{ name: string; description: string; draft: PlanDraft; tags: string[] }>
|
|
39
|
+
|
|
40
|
+
type InstantiateTemplateParams = {
|
|
41
|
+
templateId: RecordIdInput
|
|
42
|
+
organizationId: RecordIdInput
|
|
43
|
+
threadId: RecordIdInput
|
|
44
|
+
sourceThreadId?: RecordIdInput
|
|
45
|
+
leadAgentId: string
|
|
46
|
+
createdByAgentId?: string
|
|
47
|
+
requireApproval?: boolean
|
|
48
|
+
overrides?: Partial<PlanDraft>
|
|
49
|
+
carryForwardArtifacts?: PlanArtifactRecord[]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class PlanTemplateNotFoundError extends Schema.TaggedErrorClass<PlanTemplateNotFoundError>()(
|
|
53
|
+
'PlanTemplateNotFoundError',
|
|
54
|
+
{ templateId: Schema.String, message: Schema.String },
|
|
55
|
+
) {}
|
|
56
|
+
|
|
57
|
+
function resolveSourceIdentityEffect(params: {
|
|
58
|
+
source?: 'user' | 'playbook' | 'system'
|
|
59
|
+
sourceRef?: string
|
|
60
|
+
}): Effect.Effect<{ source: 'user' | 'playbook' | 'system'; sourceRef?: string }, ValidationError> {
|
|
61
|
+
const source = params.source ?? 'user'
|
|
62
|
+
const sourceRef = params.sourceRef?.trim()
|
|
63
|
+
|
|
64
|
+
if (source !== 'user' && !sourceRef) {
|
|
65
|
+
return Effect.fail(new ValidationError({ message: `sourceRef is required when source is "${source}".` }))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return Effect.succeed({ source, sourceRef })
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function makePlanTemplateService(deps: PlanTemplateDeps) {
|
|
72
|
+
const { db } = deps
|
|
73
|
+
|
|
74
|
+
const createTemplateEffect = (params: CreateTemplateParams) =>
|
|
75
|
+
Effect.gen(function* () {
|
|
76
|
+
const now = nowDate()
|
|
77
|
+
const identity = yield* resolveSourceIdentityEffect({ source: params.source, sourceRef: params.sourceRef })
|
|
78
|
+
return yield* db.create(
|
|
79
|
+
TABLES.PLAN_TEMPLATE,
|
|
80
|
+
{
|
|
81
|
+
organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
|
|
82
|
+
name: params.name,
|
|
83
|
+
...(params.description ? { description: params.description } : {}),
|
|
84
|
+
draft: params.draft,
|
|
85
|
+
tags: params.tags ?? [],
|
|
86
|
+
source: identity.source,
|
|
87
|
+
...(identity.sourceRef ? { sourceRef: identity.sourceRef } : {}),
|
|
88
|
+
createdAt: now,
|
|
89
|
+
},
|
|
90
|
+
PlanTemplateRecordSchema,
|
|
91
|
+
)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const getTemplateEffect = (templateId: RecordIdInput) =>
|
|
95
|
+
db.findOne(TABLES.PLAN_TEMPLATE, { id: ensureRecordId(templateId, TABLES.PLAN_TEMPLATE) }, PlanTemplateRecordSchema)
|
|
96
|
+
|
|
97
|
+
const getTemplateBySourceRefEffect = (params: GetTemplateBySourceRefParams) =>
|
|
98
|
+
db.findOne(
|
|
99
|
+
TABLES.PLAN_TEMPLATE,
|
|
100
|
+
{
|
|
101
|
+
organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
|
|
102
|
+
source: params.source,
|
|
103
|
+
sourceRef: params.sourceRef,
|
|
104
|
+
},
|
|
105
|
+
PlanTemplateRecordSchema,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
const listTemplatesEffect = (organizationId: RecordIdInput, params?: ListTemplatesParams) => {
|
|
109
|
+
const filter: Record<string, unknown> = { organizationId: ensureRecordId(organizationId, TABLES.ORGANIZATION) }
|
|
110
|
+
if (params?.source) {
|
|
111
|
+
filter.source = params.source
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return Effect.gen(function* () {
|
|
115
|
+
const templates = yield* db.findMany(TABLES.PLAN_TEMPLATE, filter, PlanTemplateRecordSchema, {
|
|
116
|
+
orderBy: 'createdAt',
|
|
117
|
+
orderDir: 'ASC',
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
if (params?.tags && params.tags.length > 0) {
|
|
121
|
+
const tagSet = new Set(params.tags)
|
|
122
|
+
return templates.filter((t) => t.tags.some((tag) => tagSet.has(tag)))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return templates
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const updateTemplateEffect = (templateId: RecordIdInput, patch: UpdateTemplatePatch) =>
|
|
130
|
+
Effect.gen(function* () {
|
|
131
|
+
const updated = yield* db.update(
|
|
132
|
+
TABLES.PLAN_TEMPLATE,
|
|
133
|
+
ensureRecordId(templateId, TABLES.PLAN_TEMPLATE),
|
|
134
|
+
patch,
|
|
135
|
+
PlanTemplateRecordSchema,
|
|
136
|
+
)
|
|
137
|
+
if (!updated) {
|
|
138
|
+
return yield* new PlanTemplateNotFoundError({
|
|
139
|
+
templateId: recordIdToString(templateId, TABLES.PLAN_TEMPLATE),
|
|
140
|
+
message: `Template not found: ${recordIdToString(templateId, TABLES.PLAN_TEMPLATE)}`,
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return updated
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
const upsertTemplateBySourceRefEffect = (
|
|
148
|
+
params: CreateTemplateParams & { source: 'user' | 'playbook' | 'system'; sourceRef: string },
|
|
149
|
+
) =>
|
|
150
|
+
Effect.gen(function* () {
|
|
151
|
+
const existing = yield* getTemplateBySourceRefEffect({
|
|
152
|
+
organizationId: params.organizationId,
|
|
153
|
+
source: params.source,
|
|
154
|
+
sourceRef: params.sourceRef,
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
if (!existing) {
|
|
158
|
+
return yield* createTemplateEffect(params)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return yield* updateTemplateEffect(existing.id, {
|
|
162
|
+
name: params.name,
|
|
163
|
+
description: params.description,
|
|
164
|
+
draft: params.draft,
|
|
165
|
+
tags: params.tags ?? [],
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
const deleteTemplateEffect = (templateId: RecordIdInput) =>
|
|
170
|
+
db.deleteById(TABLES.PLAN_TEMPLATE, ensureRecordId(templateId, TABLES.PLAN_TEMPLATE)).pipe(Effect.asVoid)
|
|
171
|
+
|
|
172
|
+
const instantiateEffect = (params: InstantiateTemplateParams) =>
|
|
173
|
+
Effect.gen(function* () {
|
|
174
|
+
const template = yield* getTemplateEffect(params.templateId)
|
|
175
|
+
if (!template) {
|
|
176
|
+
return yield* new PlanTemplateNotFoundError({
|
|
177
|
+
templateId: recordIdToString(params.templateId, TABLES.PLAN_TEMPLATE),
|
|
178
|
+
message: `Template not found: ${recordIdToString(params.templateId, TABLES.PLAN_TEMPLATE)}`,
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const draft: PlanDraft = { ...template.draft, ...params.overrides }
|
|
183
|
+
|
|
184
|
+
if (params.carryForwardArtifacts && params.carryForwardArtifacts.length > 0) {
|
|
185
|
+
const carryContext = params.carryForwardArtifacts.map((a) => `[carry-forward] ${a.name}: ${a.pointer}`)
|
|
186
|
+
draft.objective = `${draft.objective}\n\nCarry-forward context:\n${carryContext.join('\n')}`
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return yield* deps.executionPlanService.createPlan({
|
|
190
|
+
organizationId: params.organizationId,
|
|
191
|
+
threadId: params.threadId,
|
|
192
|
+
sourceThreadId: params.sourceThreadId,
|
|
193
|
+
leadAgentId: params.leadAgentId,
|
|
194
|
+
createdByAgentId: params.createdByAgentId,
|
|
195
|
+
requireApproval: params.requireApproval,
|
|
196
|
+
input: draft,
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
createTemplate: createTemplateEffect,
|
|
202
|
+
getTemplate: getTemplateEffect,
|
|
203
|
+
getTemplateBySourceRef: getTemplateBySourceRefEffect,
|
|
204
|
+
listTemplates: listTemplatesEffect,
|
|
205
|
+
updateTemplate: updateTemplateEffect,
|
|
206
|
+
upsertTemplateBySourceRef: upsertTemplateBySourceRefEffect,
|
|
207
|
+
deleteTemplate: deleteTemplateEffect,
|
|
208
|
+
instantiate: instantiateEffect,
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export class PlanTemplateServiceTag extends Context.Service<
|
|
213
|
+
PlanTemplateServiceTag,
|
|
214
|
+
ReturnType<typeof makePlanTemplateService>
|
|
215
|
+
>()('PlanTemplateService') {}
|
|
216
|
+
|
|
217
|
+
export const PlanTemplateServiceLive = Layer.effect(
|
|
218
|
+
PlanTemplateServiceTag,
|
|
219
|
+
Effect.gen(function* () {
|
|
220
|
+
const db = yield* DatabaseServiceTag
|
|
221
|
+
const executionPlanService = yield* ExecutionPlanServiceTag
|
|
222
|
+
return makePlanTemplateService({ db, executionPlanService })
|
|
223
|
+
}),
|
|
224
|
+
)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { PlanEventRecord } from '@lota-sdk/shared'
|
|
2
|
+
import { Schema, Effect } from 'effect'
|
|
3
|
+
|
|
4
|
+
import type { DatabaseTransaction, SurrealDBService } from '../../db/service'
|
|
5
|
+
import type { makePlanEventDeliveryService } from './plan-event-delivery.service'
|
|
6
|
+
|
|
7
|
+
class PlanTransactionEventsError extends Schema.TaggedErrorClass<PlanTransactionEventsError>()(
|
|
8
|
+
'PlanTransactionEventsError',
|
|
9
|
+
{ message: Schema.String, cause: Schema.optional(Schema.Defect) },
|
|
10
|
+
) {}
|
|
11
|
+
|
|
12
|
+
export function withTransactionAndEventsEffect<T, E, R>(params: {
|
|
13
|
+
db: SurrealDBService
|
|
14
|
+
planEventDeliveryService: ReturnType<typeof makePlanEventDeliveryService>
|
|
15
|
+
run: (tx: DatabaseTransaction, emittedEvents: PlanEventRecord[]) => Effect.Effect<T, E, R>
|
|
16
|
+
}) {
|
|
17
|
+
return Effect.gen(function* () {
|
|
18
|
+
const emittedEvents: PlanEventRecord[] = []
|
|
19
|
+
const result = yield* params.db
|
|
20
|
+
.withTransaction((tx) => params.run(tx, emittedEvents))
|
|
21
|
+
.pipe(
|
|
22
|
+
Effect.mapError(
|
|
23
|
+
(error) => new PlanTransactionEventsError({ message: 'Failed to run plan transaction.', cause: error }),
|
|
24
|
+
),
|
|
25
|
+
)
|
|
26
|
+
yield* Effect.tryPromise({
|
|
27
|
+
try: () => params.planEventDeliveryService.dispatchEvents(emittedEvents),
|
|
28
|
+
catch: (error) =>
|
|
29
|
+
new PlanTransactionEventsError({ message: 'Failed to dispatch plan transaction events.', cause: error }),
|
|
30
|
+
})
|
|
31
|
+
return result
|
|
32
|
+
})
|
|
33
|
+
}
|