@lota-sdk/core 0.4.8 → 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/thread-defaults.ts +33 -21
- package/src/create-runtime.ts +725 -387
- 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 -76
- package/src/index.ts +46 -1
- package/src/openrouter/direct-provider.ts +11 -35
- 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 +150 -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 -21
- 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 -914
- 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/rerank.service.ts +0 -156
- 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
|
@@ -18,32 +18,57 @@ import type {
|
|
|
18
18
|
ChatMessage,
|
|
19
19
|
CreateAutonomousJobInput,
|
|
20
20
|
QueueJobError,
|
|
21
|
+
SerializableExecutionPlan,
|
|
21
22
|
UpdateAutonomousJobInput,
|
|
22
23
|
} from '@lota-sdk/shared'
|
|
23
24
|
import type { Job } from 'bullmq'
|
|
24
|
-
import {
|
|
25
|
+
import { Context, Cron, Schema, Effect, Layer } from 'effect'
|
|
25
26
|
import { BoundQuery, RecordId } from 'surrealdb'
|
|
26
27
|
import { z } from 'zod'
|
|
27
28
|
|
|
28
29
|
import { ensureRecordId, recordIdToString } from '../db/record-id'
|
|
29
30
|
import type { RecordIdInput } from '../db/record-id'
|
|
30
|
-
import {
|
|
31
|
+
import type { SurrealDBService } from '../db/service'
|
|
31
32
|
import { TABLES } from '../db/tables'
|
|
33
|
+
import { makeEffectTryPromiseWithMessage } from '../effect/helpers'
|
|
34
|
+
import { DatabaseServiceTag, RuntimeConfigServiceTag } from '../effect/services'
|
|
32
35
|
import type { AutonomousJobQueuePayload } from '../queues/autonomous-job.queue'
|
|
36
|
+
import type { ResolvedLotaRuntimeConfig } from '../runtime/runtime-config'
|
|
33
37
|
import { extractMessageText } from '../runtime/thread-chat-helpers'
|
|
34
38
|
import { buildAutonomousAtJobId, encodeBullmqId } from '../utils/autonomous-job-ids'
|
|
35
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
nowDate,
|
|
41
|
+
nowEpochMillis,
|
|
42
|
+
toIsoDateTimeString,
|
|
43
|
+
toOptionalIsoDateTimeString,
|
|
44
|
+
unsafeDateFrom,
|
|
45
|
+
} from '../utils/date-time'
|
|
36
46
|
import { compactRecord, compactWhitespace, stringifyUnknown, truncateText } from '../utils/string'
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
41
|
-
import {
|
|
47
|
+
import type { makeExecutionPlanService } from './execution-plan/execution-plan.service'
|
|
48
|
+
import { ExecutionPlanServiceTag } from './execution-plan/execution-plan.service'
|
|
49
|
+
import type { makeQueueJobService } from './queue-job.service'
|
|
50
|
+
import { QueueJobServiceTag } from './queue-job.service'
|
|
51
|
+
import type { PreparedThreadTurnResult } from './thread/thread-turn'
|
|
52
|
+
import type { ThreadTurnParams } from './thread/thread-turn-preparation.service'
|
|
53
|
+
import type { makeThreadService } from './thread/thread.service'
|
|
54
|
+
import { ThreadServiceTag } from './thread/thread.service'
|
|
55
|
+
// runThreadTurnInBackground is imported dynamically below
|
|
56
|
+
|
|
57
|
+
type ThreadTurnModule = { runThreadTurnInBackground(params: ThreadTurnParams): Promise<PreparedThreadTurnResult> }
|
|
42
58
|
|
|
43
59
|
const AUTONOMOUS_JOB_QUEUE_NAME = 'autonomous-job'
|
|
44
60
|
|
|
61
|
+
class AutonomousJobServiceError extends Schema.TaggedErrorClass<AutonomousJobServiceError>()(
|
|
62
|
+
'AutonomousJobServiceError',
|
|
63
|
+
{ message: Schema.String, cause: Schema.optional(Schema.Defect) },
|
|
64
|
+
) {}
|
|
65
|
+
|
|
66
|
+
const effectTryPromise = makeEffectTryPromiseWithMessage(
|
|
67
|
+
(message, cause) => new AutonomousJobServiceError({ message, cause }),
|
|
68
|
+
)
|
|
69
|
+
|
|
45
70
|
function buildAutonomousManualJobId(autonomousJobId: string): string {
|
|
46
|
-
return `autonomous-manual-${encodeBullmqId(autonomousJobId)}-${
|
|
71
|
+
return `autonomous-manual-${encodeBullmqId(autonomousJobId)}-${nowEpochMillis()}`
|
|
47
72
|
}
|
|
48
73
|
|
|
49
74
|
const AutonomousJobRowSchema = z.object({
|
|
@@ -89,6 +114,20 @@ const AutonomousJobRunRowSchema = z.object({
|
|
|
89
114
|
|
|
90
115
|
type AutonomousJobRow = z.infer<typeof AutonomousJobRowSchema>
|
|
91
116
|
type AutonomousJobRunRow = z.infer<typeof AutonomousJobRunRowSchema>
|
|
117
|
+
type ActiveExecutionPlan = SerializableExecutionPlan | null
|
|
118
|
+
|
|
119
|
+
function computeNextCronDate(cronExpression: string, baseTime: Date): Date | null {
|
|
120
|
+
const parsedCron = Cron.parse(cronExpression)
|
|
121
|
+
if (parsedCron._tag === 'Failure') {
|
|
122
|
+
return null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
return Cron.next(parsedCron.success, baseTime)
|
|
127
|
+
} catch {
|
|
128
|
+
return null
|
|
129
|
+
}
|
|
130
|
+
}
|
|
92
131
|
|
|
93
132
|
function toQueueJobError(error: unknown): QueueJobError {
|
|
94
133
|
if (error instanceof Error) {
|
|
@@ -104,583 +143,941 @@ function toQueueJobError(error: unknown): QueueJobError {
|
|
|
104
143
|
return QueueJobErrorSchema.parse({ message: truncateText(stringifyUnknown(error) ?? 'Unknown error', 5_000) })
|
|
105
144
|
}
|
|
106
145
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
146
|
+
function toPublicJob(row: AutonomousJobRow): AutonomousJob {
|
|
147
|
+
return AutonomousJobSchema.parse({
|
|
148
|
+
id: recordIdToString(row.id, TABLES.AUTONOMOUS_JOB),
|
|
149
|
+
organizationId: recordIdToString(row.organizationId, TABLES.ORGANIZATION),
|
|
150
|
+
ownerUserId: recordIdToString(row.ownerUserId, TABLES.USER),
|
|
151
|
+
ownerUserName: row.ownerUserName,
|
|
152
|
+
threadId: recordIdToString(row.threadId, TABLES.THREAD),
|
|
153
|
+
agentId: row.agentId,
|
|
154
|
+
title: row.title,
|
|
155
|
+
prompt: row.prompt,
|
|
156
|
+
schedule: row.schedule,
|
|
157
|
+
status: row.status,
|
|
158
|
+
autoPauseThreshold: row.autoPauseThreshold,
|
|
159
|
+
consecutiveErrorCount: row.consecutiveErrorCount,
|
|
160
|
+
lastRunStatus: row.lastRunStatus,
|
|
161
|
+
lastRunAt: toOptionalIsoDateTimeString(row.lastRunAt),
|
|
162
|
+
nextRunAt: toOptionalIsoDateTimeString(row.nextRunAt),
|
|
163
|
+
linkedPlanSpecId: row.linkedPlanSpecId ? recordIdToString(row.linkedPlanSpecId, TABLES.PLAN_SPEC) : undefined,
|
|
164
|
+
linkedPlanRunId: row.linkedPlanRunId ? recordIdToString(row.linkedPlanRunId, TABLES.PLAN_RUN) : undefined,
|
|
165
|
+
lastError: row.lastError,
|
|
166
|
+
createdAt: toIsoDateTimeString(row.createdAt),
|
|
167
|
+
updatedAt: toIsoDateTimeString(row.updatedAt),
|
|
168
|
+
})
|
|
169
|
+
}
|
|
132
170
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
171
|
+
function toPublicRun(row: AutonomousJobRunRow): AutonomousJobRun {
|
|
172
|
+
return AutonomousJobRunSchema.parse({
|
|
173
|
+
id: recordIdToString(row.id, TABLES.AUTONOMOUS_JOB_RUN),
|
|
174
|
+
autonomousJobId: recordIdToString(row.autonomousJobId, TABLES.AUTONOMOUS_JOB),
|
|
175
|
+
threadId: recordIdToString(row.threadId, TABLES.THREAD),
|
|
176
|
+
queueJobId: row.queueJobId ? recordIdToString(row.queueJobId, TABLES.QUEUE_JOB) : undefined,
|
|
177
|
+
status: row.status,
|
|
178
|
+
inputMessageId: row.inputMessageId,
|
|
179
|
+
assistantMessageIds: row.assistantMessageIds,
|
|
180
|
+
summary: row.summary,
|
|
181
|
+
error: row.error,
|
|
182
|
+
linkedPlanSpecId: row.linkedPlanSpecId ? recordIdToString(row.linkedPlanSpecId, TABLES.PLAN_SPEC) : undefined,
|
|
183
|
+
linkedPlanRunId: row.linkedPlanRunId ? recordIdToString(row.linkedPlanRunId, TABLES.PLAN_RUN) : undefined,
|
|
184
|
+
startedAt: toOptionalIsoDateTimeString(row.startedAt),
|
|
185
|
+
completedAt: toOptionalIsoDateTimeString(row.completedAt),
|
|
186
|
+
createdAt: toIsoDateTimeString(row.createdAt),
|
|
187
|
+
updatedAt: toIsoDateTimeString(row.updatedAt),
|
|
188
|
+
})
|
|
189
|
+
}
|
|
152
190
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
case 'every':
|
|
164
|
-
return new Date(baseTime.getTime() + schedule.intervalMs)
|
|
165
|
-
case 'at': {
|
|
166
|
-
const at = new Date(schedule.at)
|
|
191
|
+
export function computeNextRunAt(schedule: AutonomousJobSchedule, baseTime: Date = nowDate()): Date | null {
|
|
192
|
+
switch (schedule.kind) {
|
|
193
|
+
case 'cron': {
|
|
194
|
+
return computeNextCronDate(schedule.cron, baseTime)
|
|
195
|
+
}
|
|
196
|
+
case 'every':
|
|
197
|
+
return unsafeDateFrom(baseTime.getTime() + schedule.intervalMs)
|
|
198
|
+
case 'at': {
|
|
199
|
+
try {
|
|
200
|
+
const at = unsafeDateFrom(schedule.at)
|
|
167
201
|
return Number.isNaN(at.getTime()) ? null : at
|
|
168
|
-
}
|
|
169
|
-
default:
|
|
202
|
+
} catch {
|
|
170
203
|
return null
|
|
204
|
+
}
|
|
171
205
|
}
|
|
206
|
+
default:
|
|
207
|
+
return null
|
|
172
208
|
}
|
|
209
|
+
}
|
|
173
210
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
} catch (error) {
|
|
189
|
-
if (error instanceof Error && error.message === 'Notification service is not configured.') {
|
|
190
|
-
return
|
|
191
|
-
}
|
|
192
|
-
throw error
|
|
193
|
-
}
|
|
211
|
+
function maybeNotifyEffect(
|
|
212
|
+
deps: AutonomousJobDeps,
|
|
213
|
+
params: {
|
|
214
|
+
organizationId: string
|
|
215
|
+
threadId: string
|
|
216
|
+
title: string
|
|
217
|
+
body: string
|
|
218
|
+
severity: 'info' | 'warning'
|
|
219
|
+
metadata?: Record<string, unknown>
|
|
220
|
+
},
|
|
221
|
+
): Effect.Effect<void, AutonomousJobServiceError> {
|
|
222
|
+
const notificationService = deps.config.notificationService
|
|
223
|
+
if (!notificationService) {
|
|
224
|
+
return Effect.void
|
|
194
225
|
}
|
|
195
226
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
227
|
+
return effectTryPromise(() => notificationService.notify(params), 'Failed to send autonomous job notification.')
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function buildSyntheticUserMessage(prompt: string): ChatMessage {
|
|
231
|
+
return {
|
|
232
|
+
id: Bun.randomUUIDv7(),
|
|
233
|
+
role: 'user',
|
|
234
|
+
parts: [{ type: 'text', text: prompt }],
|
|
235
|
+
metadata: { createdAt: nowEpochMillis() },
|
|
203
236
|
}
|
|
237
|
+
}
|
|
204
238
|
|
|
205
|
-
|
|
239
|
+
function createRunRowEffect(
|
|
240
|
+
deps: AutonomousJobDeps,
|
|
241
|
+
params: {
|
|
206
242
|
autonomousJobId: RecordIdInput
|
|
207
243
|
threadId: RecordIdInput
|
|
208
244
|
queueJobId?: RecordIdInput
|
|
209
245
|
status?: AutonomousJobRunStatus
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
246
|
+
},
|
|
247
|
+
): Effect.Effect<AutonomousJobRunRow, AutonomousJobServiceError> {
|
|
248
|
+
const runId = new RecordId(TABLES.AUTONOMOUS_JOB_RUN, Bun.randomUUIDv7())
|
|
249
|
+
return effectTryPromise(
|
|
250
|
+
() =>
|
|
251
|
+
deps.db.createWithId(
|
|
252
|
+
TABLES.AUTONOMOUS_JOB_RUN,
|
|
253
|
+
runId,
|
|
254
|
+
compactRecord({
|
|
255
|
+
autonomousJobId: ensureRecordId(params.autonomousJobId, TABLES.AUTONOMOUS_JOB),
|
|
256
|
+
threadId: ensureRecordId(params.threadId, TABLES.THREAD),
|
|
257
|
+
queueJobId: params.queueJobId ? ensureRecordId(params.queueJobId, TABLES.QUEUE_JOB) : undefined,
|
|
258
|
+
status: params.status ?? 'queued',
|
|
259
|
+
assistantMessageIds: [],
|
|
260
|
+
}),
|
|
261
|
+
AutonomousJobRunRowSchema,
|
|
262
|
+
),
|
|
263
|
+
'Failed to create autonomous job run.',
|
|
264
|
+
)
|
|
265
|
+
}
|
|
225
266
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
267
|
+
function getRowEffect(
|
|
268
|
+
deps: AutonomousJobDeps,
|
|
269
|
+
jobId: RecordIdInput,
|
|
270
|
+
): Effect.Effect<AutonomousJobRow, AutonomousJobServiceError> {
|
|
271
|
+
return Effect.gen(function* () {
|
|
272
|
+
yield* effectTryPromise(() => deps.db.connect(), 'Failed to connect to autonomous job database.')
|
|
273
|
+
const row = yield* effectTryPromise(
|
|
274
|
+
() =>
|
|
275
|
+
deps.db.findOne(
|
|
276
|
+
TABLES.AUTONOMOUS_JOB,
|
|
277
|
+
{ id: ensureRecordId(jobId, TABLES.AUTONOMOUS_JOB) },
|
|
278
|
+
AutonomousJobRowSchema,
|
|
279
|
+
),
|
|
280
|
+
'Failed to load autonomous job.',
|
|
232
281
|
)
|
|
233
282
|
if (!row) {
|
|
234
|
-
|
|
283
|
+
return yield* new AutonomousJobServiceError({
|
|
284
|
+
message: `Autonomous job not found: ${recordIdToString(jobId, TABLES.AUTONOMOUS_JOB)}`,
|
|
285
|
+
})
|
|
235
286
|
}
|
|
236
287
|
return row
|
|
237
|
-
}
|
|
288
|
+
})
|
|
289
|
+
}
|
|
238
290
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
291
|
+
function getRunRowEffect(
|
|
292
|
+
deps: AutonomousJobDeps,
|
|
293
|
+
runId: RecordIdInput,
|
|
294
|
+
): Effect.Effect<AutonomousJobRunRow, AutonomousJobServiceError> {
|
|
295
|
+
return Effect.gen(function* () {
|
|
296
|
+
yield* effectTryPromise(() => deps.db.connect(), 'Failed to connect to autonomous job database.')
|
|
297
|
+
const row = yield* effectTryPromise(
|
|
298
|
+
() =>
|
|
299
|
+
deps.db.findOne(
|
|
300
|
+
TABLES.AUTONOMOUS_JOB_RUN,
|
|
301
|
+
{ id: ensureRecordId(runId, TABLES.AUTONOMOUS_JOB_RUN) },
|
|
302
|
+
AutonomousJobRunRowSchema,
|
|
303
|
+
),
|
|
304
|
+
'Failed to load autonomous job run.',
|
|
245
305
|
)
|
|
246
306
|
if (!row) {
|
|
247
|
-
|
|
307
|
+
return yield* new AutonomousJobServiceError({
|
|
308
|
+
message: `Autonomous job run not found: ${recordIdToString(runId, TABLES.AUTONOMOUS_JOB_RUN)}`,
|
|
309
|
+
})
|
|
248
310
|
}
|
|
249
311
|
return row
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
private async unscheduleRow(row: AutonomousJobRow): Promise<void> {
|
|
253
|
-
if (row.schedule.kind === 'at') {
|
|
254
|
-
const { removeAutonomousAtJob } = await import('../queues/autonomous-job.queue')
|
|
255
|
-
await removeAutonomousAtJob(recordIdToString(row.id, TABLES.AUTONOMOUS_JOB))
|
|
256
|
-
return
|
|
257
|
-
}
|
|
312
|
+
})
|
|
313
|
+
}
|
|
258
314
|
|
|
259
|
-
|
|
260
|
-
|
|
315
|
+
function unscheduleRowEffect(row: AutonomousJobRow): Effect.Effect<void, AutonomousJobServiceError> {
|
|
316
|
+
if (row.schedule.kind === 'at') {
|
|
317
|
+
return Effect.gen(function* () {
|
|
318
|
+
const { removeAutonomousAtJob } = yield* effectTryPromise(
|
|
319
|
+
() => import('../queues/autonomous-job.queue'),
|
|
320
|
+
'Failed to load autonomous job queue helpers.',
|
|
321
|
+
)
|
|
322
|
+
yield* effectTryPromise(
|
|
323
|
+
() => removeAutonomousAtJob(recordIdToString(row.id, TABLES.AUTONOMOUS_JOB)),
|
|
324
|
+
'Failed to remove autonomous at-job.',
|
|
325
|
+
)
|
|
326
|
+
})
|
|
261
327
|
}
|
|
262
328
|
|
|
263
|
-
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
{ autonomousJobId: ensureRecordId(autonomousJobId, TABLES.AUTONOMOUS_JOB), statuses: ['queued', 'running'] },
|
|
272
|
-
),
|
|
273
|
-
AutonomousJobRunRowSchema,
|
|
329
|
+
return Effect.gen(function* () {
|
|
330
|
+
const { removeAutonomousJobScheduler } = yield* effectTryPromise(
|
|
331
|
+
() => import('../queues/autonomous-job.queue'),
|
|
332
|
+
'Failed to load autonomous job queue helpers.',
|
|
333
|
+
)
|
|
334
|
+
yield* effectTryPromise(
|
|
335
|
+
() => removeAutonomousJobScheduler(recordIdToString(row.id, TABLES.AUTONOMOUS_JOB)),
|
|
336
|
+
'Failed to remove autonomous job scheduler.',
|
|
274
337
|
)
|
|
338
|
+
})
|
|
339
|
+
}
|
|
275
340
|
|
|
341
|
+
function findRecoverableRunRowEffect(
|
|
342
|
+
deps: AutonomousJobDeps,
|
|
343
|
+
autonomousJobId: RecordIdInput,
|
|
344
|
+
): Effect.Effect<AutonomousJobRunRow | null, AutonomousJobServiceError> {
|
|
345
|
+
return Effect.gen(function* () {
|
|
346
|
+
const rows = yield* effectTryPromise(
|
|
347
|
+
() =>
|
|
348
|
+
deps.db.queryMany(
|
|
349
|
+
new BoundQuery(
|
|
350
|
+
`SELECT * FROM ${TABLES.AUTONOMOUS_JOB_RUN}
|
|
351
|
+
WHERE autonomousJobId = $autonomousJobId
|
|
352
|
+
AND status IN $statuses
|
|
353
|
+
ORDER BY createdAt DESC
|
|
354
|
+
LIMIT 1`,
|
|
355
|
+
{
|
|
356
|
+
autonomousJobId: ensureRecordId(autonomousJobId, TABLES.AUTONOMOUS_JOB),
|
|
357
|
+
statuses: ['queued', 'running'],
|
|
358
|
+
},
|
|
359
|
+
),
|
|
360
|
+
AutonomousJobRunRowSchema,
|
|
361
|
+
),
|
|
362
|
+
'Failed to find recoverable autonomous job run.',
|
|
363
|
+
)
|
|
276
364
|
return rows[0] ?? null
|
|
277
|
-
}
|
|
365
|
+
})
|
|
366
|
+
}
|
|
278
367
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
if (row.schedule.kind === 'at') {
|
|
291
|
-
const queuedRun = options.reusePendingAtRun
|
|
292
|
-
? ((await this.findRecoverableRunRow(row.id)) ??
|
|
293
|
-
(await this.createRunRow({ autonomousJobId: row.id, threadId: row.threadId })))
|
|
294
|
-
: await this.createRunRow({ autonomousJobId: row.id, threadId: row.threadId })
|
|
295
|
-
const { enqueueAutonomousJobRun } = await import('../queues/autonomous-job.queue')
|
|
296
|
-
const enqueueResult = await enqueueAutonomousJobRun({
|
|
297
|
-
payload: {
|
|
298
|
-
autonomousJobId: jobId,
|
|
299
|
-
autonomousJobRunId: recordIdToString(queuedRun.id, TABLES.AUTONOMOUS_JOB_RUN),
|
|
300
|
-
trigger: 'scheduled',
|
|
301
|
-
},
|
|
302
|
-
delayMs: Math.max(0, (nextRunAt ?? referenceTime).getTime() - referenceTime.getTime()),
|
|
303
|
-
jobId: buildAutonomousAtJobId(jobId),
|
|
304
|
-
})
|
|
368
|
+
function scheduleRowEffect(
|
|
369
|
+
deps: AutonomousJobDeps,
|
|
370
|
+
row: AutonomousJobRow,
|
|
371
|
+
options: { runImmediate?: boolean; referenceTime?: Date; reusePendingAtRun?: boolean } = {},
|
|
372
|
+
): Effect.Effect<void, AutonomousJobServiceError> {
|
|
373
|
+
const jobId = recordIdToString(row.id, TABLES.AUTONOMOUS_JOB)
|
|
374
|
+
const referenceTime = options.referenceTime ?? nowDate()
|
|
375
|
+
const nextRunAt =
|
|
376
|
+
row.schedule.kind === 'at'
|
|
377
|
+
? (row.nextRunAt ?? computeNextRunAt(row.schedule, referenceTime))
|
|
378
|
+
: computeNextRunAt(row.schedule, referenceTime)
|
|
305
379
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
380
|
+
if (row.schedule.kind === 'at') {
|
|
381
|
+
return Effect.gen(function* () {
|
|
382
|
+
const queuedRun = yield* options.reusePendingAtRun
|
|
383
|
+
? Effect.gen(function* () {
|
|
384
|
+
const recoverable = yield* findRecoverableRunRowEffect(deps, row.id)
|
|
385
|
+
return recoverable ?? (yield* createRunRowEffect(deps, { autonomousJobId: row.id, threadId: row.threadId }))
|
|
386
|
+
})
|
|
387
|
+
: createRunRowEffect(deps, { autonomousJobId: row.id, threadId: row.threadId })
|
|
388
|
+
|
|
389
|
+
const { enqueueAutonomousJobRun } = yield* effectTryPromise(
|
|
390
|
+
() => import('../queues/autonomous-job.queue'),
|
|
391
|
+
'Failed to load autonomous job queue helpers.',
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
const enqueueResult = yield* effectTryPromise(
|
|
395
|
+
() =>
|
|
396
|
+
enqueueAutonomousJobRun({
|
|
397
|
+
payload: {
|
|
398
|
+
autonomousJobId: jobId,
|
|
399
|
+
autonomousJobRunId: recordIdToString(queuedRun.id, TABLES.AUTONOMOUS_JOB_RUN),
|
|
400
|
+
trigger: 'scheduled',
|
|
401
|
+
},
|
|
402
|
+
delayMs: Math.max(0, (nextRunAt ?? referenceTime).getTime() - referenceTime.getTime()),
|
|
403
|
+
jobId: buildAutonomousAtJobId(jobId),
|
|
404
|
+
}),
|
|
405
|
+
'Failed to enqueue autonomous job run.',
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
const queueJobId = enqueueResult.queueJobId
|
|
409
|
+
if (queueJobId) {
|
|
410
|
+
yield* effectTryPromise(
|
|
411
|
+
() =>
|
|
412
|
+
deps.db.update(
|
|
413
|
+
TABLES.AUTONOMOUS_JOB_RUN,
|
|
414
|
+
queuedRun.id,
|
|
415
|
+
{ queueJobId: ensureRecordId(queueJobId, TABLES.QUEUE_JOB) },
|
|
416
|
+
AutonomousJobRunRowSchema,
|
|
417
|
+
),
|
|
418
|
+
'Failed to persist autonomous job run queue job id.',
|
|
312
419
|
)
|
|
313
420
|
}
|
|
314
|
-
} else {
|
|
315
|
-
const { upsertAutonomousJobScheduler } = await import('../queues/autonomous-job.queue')
|
|
316
|
-
await upsertAutonomousJobScheduler({ autonomousJobId: jobId, schedule: row.schedule })
|
|
317
421
|
|
|
318
|
-
|
|
319
|
-
|
|
422
|
+
yield* effectTryPromise(
|
|
423
|
+
() =>
|
|
424
|
+
deps.db.update(TABLES.AUTONOMOUS_JOB, row.id, { nextRunAt: nextRunAt ?? undefined }, AutonomousJobRowSchema),
|
|
425
|
+
'Failed to update autonomous job next run time.',
|
|
426
|
+
)
|
|
427
|
+
})
|
|
428
|
+
} else {
|
|
429
|
+
const recurringSchedule = row.schedule
|
|
430
|
+
return Effect.gen(function* () {
|
|
431
|
+
const { upsertAutonomousJobScheduler } = yield* effectTryPromise(
|
|
432
|
+
() => import('../queues/autonomous-job.queue'),
|
|
433
|
+
'Failed to load autonomous job queue helpers.',
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
yield* effectTryPromise(
|
|
437
|
+
() => upsertAutonomousJobScheduler({ autonomousJobId: jobId, schedule: recurringSchedule }),
|
|
438
|
+
'Failed to upsert autonomous job scheduler.',
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
if (options.runImmediate ?? recurringSchedule.immediately) {
|
|
442
|
+
yield* runNowEffect(deps, jobId).pipe(Effect.asVoid)
|
|
320
443
|
}
|
|
321
|
-
}
|
|
322
444
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
)
|
|
445
|
+
yield* effectTryPromise(
|
|
446
|
+
() =>
|
|
447
|
+
deps.db.update(TABLES.AUTONOMOUS_JOB, row.id, { nextRunAt: nextRunAt ?? undefined }, AutonomousJobRowSchema),
|
|
448
|
+
'Failed to update autonomous job next run time.',
|
|
449
|
+
)
|
|
450
|
+
})
|
|
329
451
|
}
|
|
452
|
+
}
|
|
330
453
|
|
|
331
|
-
|
|
332
|
-
|
|
454
|
+
function createEffect(
|
|
455
|
+
deps: AutonomousJobDeps,
|
|
456
|
+
input: CreateAutonomousJobInput,
|
|
457
|
+
): Effect.Effect<AutonomousJob, AutonomousJobServiceError> {
|
|
458
|
+
return Effect.gen(function* () {
|
|
459
|
+
yield* effectTryPromise(() => deps.db.connect(), 'Failed to connect to autonomous job database.')
|
|
333
460
|
const parsed = CreateAutonomousJobInputSchema.parse(input)
|
|
334
461
|
const organizationId = ensureRecordId(parsed.organizationId, TABLES.ORGANIZATION)
|
|
335
462
|
const ownerUserId = ensureRecordId(parsed.ownerUserId, TABLES.USER)
|
|
336
|
-
const thread =
|
|
337
|
-
userId: ownerUserId,
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
463
|
+
const thread = yield* deps.thread
|
|
464
|
+
.createThread({ userId: ownerUserId, organizationId, type: 'group', title: parsed.threadTitle ?? parsed.title })
|
|
465
|
+
.pipe(
|
|
466
|
+
Effect.mapError(
|
|
467
|
+
(cause) => new AutonomousJobServiceError({ message: 'Failed to create autonomous job thread.', cause }),
|
|
468
|
+
),
|
|
469
|
+
)
|
|
470
|
+
|
|
342
471
|
const jobId = new RecordId(TABLES.AUTONOMOUS_JOB, Bun.randomUUIDv7())
|
|
343
|
-
const nextRunAt =
|
|
344
|
-
const created =
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
472
|
+
const nextRunAt = computeNextRunAt(parsed.schedule)
|
|
473
|
+
const created = yield* effectTryPromise(
|
|
474
|
+
() =>
|
|
475
|
+
deps.db.createWithId(
|
|
476
|
+
TABLES.AUTONOMOUS_JOB,
|
|
477
|
+
jobId,
|
|
478
|
+
compactRecord({
|
|
479
|
+
organizationId,
|
|
480
|
+
ownerUserId,
|
|
481
|
+
ownerUserName: parsed.ownerUserName,
|
|
482
|
+
threadId: ensureRecordId(thread.id, TABLES.THREAD),
|
|
483
|
+
agentId: parsed.agentId,
|
|
484
|
+
title: parsed.title,
|
|
485
|
+
prompt: parsed.prompt,
|
|
486
|
+
schedule: parsed.schedule,
|
|
487
|
+
status: 'active',
|
|
488
|
+
autoPauseThreshold: parsed.autoPauseThreshold,
|
|
489
|
+
consecutiveErrorCount: 0,
|
|
490
|
+
nextRunAt: nextRunAt ?? undefined,
|
|
491
|
+
}),
|
|
492
|
+
AutonomousJobRowSchema,
|
|
493
|
+
),
|
|
494
|
+
'Failed to create autonomous job.',
|
|
362
495
|
)
|
|
363
496
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
497
|
+
yield* scheduleRowEffect(deps, created)
|
|
498
|
+
const row = yield* getRowEffect(deps, created.id)
|
|
499
|
+
return toPublicJob(row)
|
|
500
|
+
})
|
|
501
|
+
}
|
|
367
502
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
503
|
+
function recoverActiveJobsEffect(
|
|
504
|
+
deps: AutonomousJobDeps,
|
|
505
|
+
now = nowDate(),
|
|
506
|
+
): Effect.Effect<void, AutonomousJobServiceError> {
|
|
507
|
+
return Effect.gen(function* () {
|
|
508
|
+
yield* effectTryPromise(() => deps.db.connect(), 'Failed to connect to autonomous job database.')
|
|
509
|
+
const activeRows = yield* effectTryPromise(
|
|
510
|
+
() =>
|
|
511
|
+
deps.db.queryMany(
|
|
512
|
+
new BoundQuery(`SELECT * FROM ${TABLES.AUTONOMOUS_JOB} WHERE status = $status ORDER BY createdAt ASC`, {
|
|
513
|
+
status: 'active',
|
|
514
|
+
}),
|
|
515
|
+
AutonomousJobRowSchema,
|
|
516
|
+
),
|
|
517
|
+
'Failed to load active autonomous jobs.',
|
|
375
518
|
)
|
|
519
|
+
yield* Effect.forEach(activeRows, (row) =>
|
|
520
|
+
scheduleRowEffect(deps, row, { runImmediate: false, referenceTime: now, reusePendingAtRun: true }),
|
|
521
|
+
)
|
|
522
|
+
})
|
|
523
|
+
}
|
|
376
524
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
return
|
|
384
|
-
}
|
|
525
|
+
function getEffect(
|
|
526
|
+
deps: AutonomousJobDeps,
|
|
527
|
+
jobId: RecordIdInput,
|
|
528
|
+
): Effect.Effect<AutonomousJob, AutonomousJobServiceError> {
|
|
529
|
+
return Effect.gen(function* () {
|
|
530
|
+
const row = yield* getRowEffect(deps, jobId)
|
|
531
|
+
return toPublicJob(row)
|
|
532
|
+
})
|
|
533
|
+
}
|
|
385
534
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
const rows =
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
535
|
+
function listEffect(
|
|
536
|
+
deps: AutonomousJobDeps,
|
|
537
|
+
params: { organizationId: RecordIdInput; ownerUserId?: RecordIdInput; status?: AutonomousJobStatus },
|
|
538
|
+
): Effect.Effect<AutonomousJob[], AutonomousJobServiceError> {
|
|
539
|
+
return Effect.gen(function* () {
|
|
540
|
+
yield* effectTryPromise(() => deps.db.connect(), 'Failed to connect to autonomous job database.')
|
|
541
|
+
const rows = yield* effectTryPromise(
|
|
542
|
+
() =>
|
|
543
|
+
deps.db.findMany(
|
|
544
|
+
TABLES.AUTONOMOUS_JOB,
|
|
545
|
+
compactRecord({
|
|
546
|
+
organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
|
|
547
|
+
ownerUserId: params.ownerUserId ? ensureRecordId(params.ownerUserId, TABLES.USER) : undefined,
|
|
548
|
+
status: params.status,
|
|
549
|
+
}),
|
|
550
|
+
AutonomousJobRowSchema,
|
|
551
|
+
{ orderBy: 'createdAt', orderDir: 'DESC' },
|
|
552
|
+
),
|
|
553
|
+
'Failed to list autonomous jobs.',
|
|
401
554
|
)
|
|
402
|
-
return rows.map((row) =>
|
|
403
|
-
}
|
|
555
|
+
return rows.map((row) => toPublicJob(row))
|
|
556
|
+
})
|
|
557
|
+
}
|
|
404
558
|
|
|
405
|
-
|
|
559
|
+
function updateEffect(
|
|
560
|
+
deps: AutonomousJobDeps,
|
|
561
|
+
jobId: RecordIdInput,
|
|
562
|
+
input: UpdateAutonomousJobInput,
|
|
563
|
+
): Effect.Effect<AutonomousJob, AutonomousJobServiceError> {
|
|
564
|
+
return Effect.gen(function* () {
|
|
406
565
|
const parsed = UpdateAutonomousJobInputSchema.parse(input)
|
|
407
|
-
const existing =
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
if (
|
|
411
|
-
|
|
566
|
+
const existing = yield* getRowEffect(deps, jobId)
|
|
567
|
+
yield* unscheduleRowEffect(existing)
|
|
568
|
+
const nextTitle = parsed.title
|
|
569
|
+
if (typeof nextTitle === 'string' && compactWhitespace(nextTitle) !== compactWhitespace(existing.title)) {
|
|
570
|
+
const title = nextTitle
|
|
571
|
+
yield* deps.thread
|
|
572
|
+
.updateTitle(existing.threadId, title)
|
|
573
|
+
.pipe(
|
|
574
|
+
Effect.mapError(
|
|
575
|
+
(cause) =>
|
|
576
|
+
new AutonomousJobServiceError({ message: 'Failed to update autonomous job thread title.', cause }),
|
|
577
|
+
),
|
|
578
|
+
)
|
|
412
579
|
}
|
|
413
580
|
|
|
414
|
-
const nextRunAt =
|
|
415
|
-
const updated =
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
581
|
+
const nextRunAt = computeNextRunAt(parsed.schedule ?? existing.schedule)
|
|
582
|
+
const updated = yield* effectTryPromise(
|
|
583
|
+
() =>
|
|
584
|
+
deps.db.update(
|
|
585
|
+
TABLES.AUTONOMOUS_JOB,
|
|
586
|
+
existing.id,
|
|
587
|
+
compactRecord({
|
|
588
|
+
title: parsed.title,
|
|
589
|
+
prompt: parsed.prompt,
|
|
590
|
+
schedule: parsed.schedule,
|
|
591
|
+
autoPauseThreshold: parsed.autoPauseThreshold,
|
|
592
|
+
nextRunAt: existing.status === 'active' ? (nextRunAt ?? undefined) : undefined,
|
|
593
|
+
}),
|
|
594
|
+
AutonomousJobRowSchema,
|
|
595
|
+
),
|
|
596
|
+
'Failed to update autonomous job.',
|
|
426
597
|
)
|
|
427
598
|
if (!updated) {
|
|
428
|
-
|
|
599
|
+
return yield* new AutonomousJobServiceError({
|
|
600
|
+
message: `Failed to update autonomous job: ${recordIdToString(existing.id, TABLES.AUTONOMOUS_JOB)}`,
|
|
601
|
+
})
|
|
429
602
|
}
|
|
430
603
|
|
|
431
604
|
if (updated.status === 'active') {
|
|
432
|
-
|
|
605
|
+
yield* scheduleRowEffect(deps, updated)
|
|
433
606
|
}
|
|
434
|
-
return this.toPublicJob(await this.getRow(updated.id))
|
|
435
|
-
}
|
|
436
607
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
608
|
+
const row = yield* getRowEffect(deps, updated.id)
|
|
609
|
+
return toPublicJob(row)
|
|
610
|
+
})
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function pauseEffect(
|
|
614
|
+
deps: AutonomousJobDeps,
|
|
615
|
+
jobId: RecordIdInput,
|
|
616
|
+
): Effect.Effect<AutonomousJob, AutonomousJobServiceError> {
|
|
617
|
+
return Effect.gen(function* () {
|
|
618
|
+
const row = yield* getRowEffect(deps, jobId)
|
|
619
|
+
yield* unscheduleRowEffect(row)
|
|
620
|
+
yield* effectTryPromise(
|
|
621
|
+
() =>
|
|
622
|
+
deps.db.update(
|
|
623
|
+
TABLES.AUTONOMOUS_JOB,
|
|
624
|
+
row.id,
|
|
625
|
+
{ status: 'paused', nextRunAt: undefined },
|
|
626
|
+
AutonomousJobRowSchema,
|
|
627
|
+
),
|
|
628
|
+
'Failed to pause autonomous job.',
|
|
445
629
|
)
|
|
446
|
-
|
|
447
|
-
|
|
630
|
+
const updatedRow = yield* getRowEffect(deps, row.id)
|
|
631
|
+
return toPublicJob(updatedRow)
|
|
632
|
+
})
|
|
633
|
+
}
|
|
448
634
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
635
|
+
function resumeEffect(
|
|
636
|
+
deps: AutonomousJobDeps,
|
|
637
|
+
jobId: RecordIdInput,
|
|
638
|
+
): Effect.Effect<AutonomousJob, AutonomousJobServiceError> {
|
|
639
|
+
return Effect.gen(function* () {
|
|
640
|
+
const row = yield* getRowEffect(deps, jobId)
|
|
641
|
+
const nextRunAt = computeNextRunAt(row.schedule)
|
|
642
|
+
const resumed = yield* effectTryPromise(
|
|
643
|
+
() =>
|
|
644
|
+
deps.db.update(
|
|
645
|
+
TABLES.AUTONOMOUS_JOB,
|
|
646
|
+
row.id,
|
|
647
|
+
{ status: 'active', nextRunAt: nextRunAt ?? undefined },
|
|
648
|
+
AutonomousJobRowSchema,
|
|
649
|
+
),
|
|
650
|
+
'Failed to resume autonomous job.',
|
|
457
651
|
)
|
|
458
652
|
if (!resumed) {
|
|
459
|
-
|
|
653
|
+
return yield* new AutonomousJobServiceError({
|
|
654
|
+
message: `Failed to resume autonomous job: ${recordIdToString(row.id, TABLES.AUTONOMOUS_JOB)}`,
|
|
655
|
+
})
|
|
460
656
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
657
|
+
yield* scheduleRowEffect(deps, resumed)
|
|
658
|
+
const updatedRow = yield* getRowEffect(deps, resumed.id)
|
|
659
|
+
return toPublicJob(updatedRow)
|
|
660
|
+
})
|
|
661
|
+
}
|
|
464
662
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
})
|
|
663
|
+
function runNowEffect(
|
|
664
|
+
deps: AutonomousJobDeps,
|
|
665
|
+
jobId: RecordIdInput,
|
|
666
|
+
): Effect.Effect<AutonomousJobRun, AutonomousJobServiceError> {
|
|
667
|
+
return Effect.gen(function* () {
|
|
668
|
+
const row = yield* getRowEffect(deps, jobId)
|
|
669
|
+
const queuedRun = yield* createRunRowEffect(deps, { autonomousJobId: row.id, threadId: row.threadId })
|
|
670
|
+
const { enqueueAutonomousJobRun } = yield* effectTryPromise(
|
|
671
|
+
() => import('../queues/autonomous-job.queue'),
|
|
672
|
+
'Failed to load autonomous job queue helpers.',
|
|
673
|
+
)
|
|
477
674
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
675
|
+
const enqueueResult = yield* effectTryPromise(
|
|
676
|
+
() =>
|
|
677
|
+
enqueueAutonomousJobRun({
|
|
678
|
+
payload: {
|
|
679
|
+
autonomousJobId: recordIdToString(row.id, TABLES.AUTONOMOUS_JOB),
|
|
680
|
+
autonomousJobRunId: recordIdToString(queuedRun.id, TABLES.AUTONOMOUS_JOB_RUN),
|
|
681
|
+
trigger: 'manual',
|
|
682
|
+
},
|
|
683
|
+
jobId: buildAutonomousManualJobId(recordIdToString(row.id, TABLES.AUTONOMOUS_JOB)),
|
|
684
|
+
}),
|
|
685
|
+
'Failed to enqueue autonomous job run.',
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
const queueJobId = enqueueResult.queueJobId
|
|
689
|
+
if (queueJobId) {
|
|
690
|
+
yield* effectTryPromise(
|
|
691
|
+
() =>
|
|
692
|
+
deps.db.update(
|
|
693
|
+
TABLES.AUTONOMOUS_JOB_RUN,
|
|
694
|
+
queuedRun.id,
|
|
695
|
+
{ queueJobId: ensureRecordId(queueJobId, TABLES.QUEUE_JOB) },
|
|
696
|
+
AutonomousJobRunRowSchema,
|
|
697
|
+
),
|
|
698
|
+
'Failed to persist autonomous job run queue job id.',
|
|
484
699
|
)
|
|
485
700
|
}
|
|
486
701
|
|
|
487
|
-
|
|
488
|
-
|
|
702
|
+
const runRow = yield* getRunRowEffect(deps, queuedRun.id)
|
|
703
|
+
return toPublicRun(runRow)
|
|
704
|
+
})
|
|
705
|
+
}
|
|
489
706
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
707
|
+
function cancelEffect(
|
|
708
|
+
deps: AutonomousJobDeps,
|
|
709
|
+
jobId: RecordIdInput,
|
|
710
|
+
): Effect.Effect<AutonomousJob, AutonomousJobServiceError> {
|
|
711
|
+
return Effect.gen(function* () {
|
|
712
|
+
const row = yield* getRowEffect(deps, jobId)
|
|
713
|
+
yield* unscheduleRowEffect(row)
|
|
714
|
+
yield* effectTryPromise(
|
|
715
|
+
() =>
|
|
716
|
+
deps.db.update(
|
|
717
|
+
TABLES.AUTONOMOUS_JOB,
|
|
718
|
+
row.id,
|
|
719
|
+
{ status: 'cancelled', nextRunAt: undefined },
|
|
720
|
+
AutonomousJobRowSchema,
|
|
721
|
+
),
|
|
722
|
+
'Failed to cancel autonomous job.',
|
|
498
723
|
)
|
|
499
|
-
|
|
500
|
-
|
|
724
|
+
const updatedRow = yield* getRowEffect(deps, row.id)
|
|
725
|
+
return toPublicJob(updatedRow)
|
|
726
|
+
})
|
|
727
|
+
}
|
|
501
728
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
729
|
+
function deleteJobEffect(
|
|
730
|
+
deps: AutonomousJobDeps,
|
|
731
|
+
jobId: RecordIdInput,
|
|
732
|
+
): Effect.Effect<AutonomousJob, AutonomousJobServiceError> {
|
|
733
|
+
return Effect.gen(function* () {
|
|
734
|
+
const row = yield* getRowEffect(deps, jobId)
|
|
735
|
+
const cancelled = yield* cancelEffect(deps, row.id)
|
|
736
|
+
yield* deps.thread
|
|
737
|
+
.updateStatus(row.threadId, 'archived')
|
|
738
|
+
.pipe(
|
|
739
|
+
Effect.mapError(
|
|
740
|
+
(cause) => new AutonomousJobServiceError({ message: 'Failed to archive autonomous job thread.', cause }),
|
|
741
|
+
),
|
|
742
|
+
)
|
|
506
743
|
return cancelled
|
|
507
|
-
}
|
|
744
|
+
})
|
|
745
|
+
}
|
|
508
746
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
747
|
+
function listRunsEffect(
|
|
748
|
+
deps: AutonomousJobDeps,
|
|
749
|
+
jobId: RecordIdInput,
|
|
750
|
+
): Effect.Effect<AutonomousJobRun[], AutonomousJobServiceError> {
|
|
751
|
+
return Effect.gen(function* () {
|
|
752
|
+
yield* effectTryPromise(() => deps.db.connect(), 'Failed to connect to autonomous job database.')
|
|
753
|
+
const rows = yield* effectTryPromise(
|
|
754
|
+
() =>
|
|
755
|
+
deps.db.findMany(
|
|
756
|
+
TABLES.AUTONOMOUS_JOB_RUN,
|
|
757
|
+
{ autonomousJobId: ensureRecordId(jobId, TABLES.AUTONOMOUS_JOB) },
|
|
758
|
+
AutonomousJobRunRowSchema,
|
|
759
|
+
{ orderBy: 'createdAt', orderDir: 'DESC' },
|
|
760
|
+
),
|
|
761
|
+
'Failed to list autonomous job runs.',
|
|
516
762
|
)
|
|
517
|
-
return rows.map((row) =>
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
async executeQueuedRun(job: Job<AutonomousJobQueuePayload>): Promise<{ status: string; summary?: string }> {
|
|
521
|
-
await databaseService.connect()
|
|
763
|
+
return rows.map((row) => toPublicRun(row))
|
|
764
|
+
})
|
|
765
|
+
}
|
|
522
766
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
767
|
+
function getOrCreateQueuedRunRowEffect(
|
|
768
|
+
deps: AutonomousJobDeps,
|
|
769
|
+
job: Job<AutonomousJobQueuePayload>,
|
|
770
|
+
currentJobRow: AutonomousJobRow,
|
|
771
|
+
): Effect.Effect<{ queueJobId: string; runRow: AutonomousJobRunRow }, AutonomousJobServiceError> {
|
|
772
|
+
return Effect.gen(function* () {
|
|
773
|
+
const queueJobId = deps.queueJob.getQueueJobId(AUTONOMOUS_JOB_QUEUE_NAME, String(job.id))
|
|
774
|
+
const runRow =
|
|
526
775
|
job.data.autonomousJobRunId !== undefined
|
|
527
|
-
?
|
|
528
|
-
:
|
|
529
|
-
autonomousJobId:
|
|
530
|
-
threadId:
|
|
776
|
+
? yield* getRunRowEffect(deps, job.data.autonomousJobRunId)
|
|
777
|
+
: yield* createRunRowEffect(deps, {
|
|
778
|
+
autonomousJobId: currentJobRow.id,
|
|
779
|
+
threadId: currentJobRow.threadId,
|
|
531
780
|
queueJobId,
|
|
532
781
|
status: 'queued',
|
|
533
782
|
})
|
|
534
783
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
784
|
+
return { queueJobId, runRow }
|
|
785
|
+
})
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function markQueuedRunRunningEffect(
|
|
789
|
+
deps: AutonomousJobDeps,
|
|
790
|
+
runRow: AutonomousJobRunRow,
|
|
791
|
+
queueJobId: string,
|
|
792
|
+
): Effect.Effect<AutonomousJobRunRow, AutonomousJobServiceError> {
|
|
793
|
+
const startedAt = nowDate()
|
|
794
|
+
|
|
795
|
+
return Effect.gen(function* () {
|
|
796
|
+
const updatedRunRow = yield* effectTryPromise(
|
|
797
|
+
() =>
|
|
798
|
+
deps.db.update(
|
|
799
|
+
TABLES.AUTONOMOUS_JOB_RUN,
|
|
800
|
+
runRow.id,
|
|
801
|
+
{ queueJobId: ensureRecordId(queueJobId, TABLES.QUEUE_JOB), status: 'running', startedAt },
|
|
802
|
+
AutonomousJobRunRowSchema,
|
|
803
|
+
),
|
|
804
|
+
'Failed to mark autonomous job run as running.',
|
|
805
|
+
)
|
|
806
|
+
|
|
807
|
+
return updatedRunRow ?? runRow
|
|
808
|
+
})
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function completeQueuedRunEffect(
|
|
812
|
+
deps: AutonomousJobDeps,
|
|
813
|
+
params: {
|
|
814
|
+
job: Job<AutonomousJobQueuePayload>
|
|
815
|
+
currentJobRow: AutonomousJobRow
|
|
816
|
+
currentRunRow: AutonomousJobRunRow
|
|
817
|
+
turnResult: PreparedThreadTurnResult
|
|
818
|
+
activePlan: ActiveExecutionPlan
|
|
819
|
+
},
|
|
820
|
+
): Effect.Effect<{ status: AutonomousJobRunStatus; summary: string }, AutonomousJobServiceError> {
|
|
821
|
+
return Effect.gen(function* () {
|
|
822
|
+
const { job, currentJobRow, currentRunRow, turnResult, activePlan } = params
|
|
823
|
+
const runStatus: AutonomousJobRunStatus = activePlan?.status === 'awaiting-human' ? 'awaiting-human' : 'completed'
|
|
824
|
+
const summary = truncateText(
|
|
825
|
+
turnResult.assistantMessages
|
|
826
|
+
.map((message) => extractMessageText(message))
|
|
827
|
+
.filter(Boolean)
|
|
828
|
+
.join('\n\n')
|
|
829
|
+
.trim() || `${currentJobRow.title} completed.`,
|
|
830
|
+
2_000,
|
|
831
|
+
)
|
|
832
|
+
const completedAt = nowDate()
|
|
833
|
+
|
|
834
|
+
void (yield* effectTryPromise(
|
|
835
|
+
() =>
|
|
836
|
+
deps.db.update(
|
|
837
|
+
TABLES.AUTONOMOUS_JOB_RUN,
|
|
838
|
+
currentRunRow.id,
|
|
839
|
+
compactRecord({
|
|
840
|
+
status: runStatus,
|
|
841
|
+
inputMessageId: turnResult.inputMessageId,
|
|
842
|
+
assistantMessageIds: turnResult.assistantMessages.map((message) => message.id),
|
|
843
|
+
summary,
|
|
844
|
+
linkedPlanSpecId: activePlan?.specId ? ensureRecordId(activePlan.specId, TABLES.PLAN_SPEC) : undefined,
|
|
845
|
+
linkedPlanRunId: activePlan?.runId ? ensureRecordId(activePlan.runId, TABLES.PLAN_RUN) : undefined,
|
|
846
|
+
completedAt,
|
|
847
|
+
}),
|
|
848
|
+
AutonomousJobRunRowSchema,
|
|
849
|
+
),
|
|
850
|
+
'Failed to update autonomous job run completion.',
|
|
851
|
+
))
|
|
852
|
+
|
|
853
|
+
const nextRunAt =
|
|
854
|
+
job.data.trigger === 'scheduled' && currentJobRow.schedule.kind !== 'at'
|
|
855
|
+
? computeNextRunAt(currentJobRow.schedule, completedAt)
|
|
856
|
+
: currentJobRow.nextRunAt
|
|
857
|
+
const nextStatus =
|
|
858
|
+
currentJobRow.schedule.kind === 'at' && job.data.trigger === 'scheduled' ? 'completed' : currentJobRow.status
|
|
859
|
+
|
|
860
|
+
yield* effectTryPromise(
|
|
861
|
+
() =>
|
|
862
|
+
deps.db.update(
|
|
863
|
+
TABLES.AUTONOMOUS_JOB,
|
|
864
|
+
currentJobRow.id,
|
|
865
|
+
compactRecord({
|
|
866
|
+
status: nextStatus,
|
|
867
|
+
consecutiveErrorCount: 0,
|
|
868
|
+
lastRunStatus: runStatus,
|
|
869
|
+
lastRunAt: completedAt,
|
|
870
|
+
nextRunAt: nextStatus === 'active' ? (nextRunAt ?? undefined) : undefined,
|
|
871
|
+
linkedPlanSpecId: activePlan?.specId ? ensureRecordId(activePlan.specId, TABLES.PLAN_SPEC) : undefined,
|
|
872
|
+
linkedPlanRunId: activePlan?.runId ? ensureRecordId(activePlan.runId, TABLES.PLAN_RUN) : undefined,
|
|
873
|
+
lastError: undefined,
|
|
874
|
+
}),
|
|
875
|
+
AutonomousJobRowSchema,
|
|
876
|
+
),
|
|
877
|
+
'Failed to update autonomous job after successful run.',
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
yield* maybeNotifyEffect(deps, {
|
|
881
|
+
organizationId: recordIdToString(currentJobRow.organizationId, TABLES.ORGANIZATION),
|
|
882
|
+
threadId: recordIdToString(currentJobRow.threadId, TABLES.THREAD),
|
|
883
|
+
severity: 'info',
|
|
884
|
+
title: `${currentJobRow.title} completed`,
|
|
885
|
+
body: summary,
|
|
886
|
+
metadata: {
|
|
887
|
+
autonomousJobId: params.job.data.autonomousJobId,
|
|
888
|
+
runId: recordIdToString(currentRunRow.id, TABLES.AUTONOMOUS_JOB_RUN),
|
|
889
|
+
},
|
|
890
|
+
})
|
|
891
|
+
|
|
892
|
+
return { status: runStatus, summary }
|
|
893
|
+
})
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function failQueuedRunEffect(
|
|
897
|
+
deps: AutonomousJobDeps,
|
|
898
|
+
params: {
|
|
899
|
+
job: Job<AutonomousJobQueuePayload>
|
|
900
|
+
currentJobRow: AutonomousJobRow
|
|
901
|
+
currentRunRow: AutonomousJobRunRow
|
|
902
|
+
error: unknown
|
|
903
|
+
},
|
|
904
|
+
): Effect.Effect<never, AutonomousJobServiceError> {
|
|
905
|
+
return Effect.gen(function* () {
|
|
906
|
+
const { job, currentJobRow, currentRunRow, error } = params
|
|
907
|
+
const normalizedError = toQueueJobError(error)
|
|
908
|
+
const completedAt = nowDate()
|
|
909
|
+
const nextConsecutiveErrorCount = currentJobRow.consecutiveErrorCount + 1
|
|
910
|
+
const autoPause = nextConsecutiveErrorCount >= currentJobRow.autoPauseThreshold
|
|
911
|
+
const terminalOneShot = currentJobRow.schedule.kind === 'at' && job.data.trigger === 'scheduled'
|
|
912
|
+
const nextStatus: AutonomousJobStatus = terminalOneShot ? 'failed' : autoPause ? 'paused' : currentJobRow.status
|
|
913
|
+
|
|
914
|
+
yield* effectTryPromise(
|
|
915
|
+
() =>
|
|
916
|
+
deps.db.update(
|
|
917
|
+
TABLES.AUTONOMOUS_JOB_RUN,
|
|
918
|
+
currentRunRow.id,
|
|
919
|
+
{ status: 'failed', error: normalizedError, completedAt },
|
|
920
|
+
AutonomousJobRunRowSchema,
|
|
921
|
+
),
|
|
922
|
+
'Failed to persist failed autonomous job run.',
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
if (autoPause || terminalOneShot) {
|
|
926
|
+
yield* unscheduleRowEffect(currentJobRow)
|
|
538
927
|
}
|
|
539
928
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
})
|
|
561
|
-
const activePlan = await executionPlanService.getActivePlanForThread(autonomousJobRow.threadId)
|
|
562
|
-
const runStatus: AutonomousJobRunStatus = activePlan?.status === 'awaiting-human' ? 'awaiting-human' : 'completed'
|
|
563
|
-
const summary = truncateText(
|
|
564
|
-
turnResult.assistantMessages
|
|
565
|
-
.map((message) => extractMessageText(message))
|
|
566
|
-
.filter(Boolean)
|
|
567
|
-
.join('\n\n')
|
|
568
|
-
.trim() || `${autonomousJobRow.title} completed.`,
|
|
569
|
-
2_000,
|
|
570
|
-
)
|
|
571
|
-
const completedAt = new Date()
|
|
929
|
+
yield* effectTryPromise(
|
|
930
|
+
() =>
|
|
931
|
+
deps.db.update(
|
|
932
|
+
TABLES.AUTONOMOUS_JOB,
|
|
933
|
+
currentJobRow.id,
|
|
934
|
+
compactRecord({
|
|
935
|
+
status: nextStatus,
|
|
936
|
+
consecutiveErrorCount: nextConsecutiveErrorCount,
|
|
937
|
+
lastRunStatus: 'failed',
|
|
938
|
+
lastRunAt: completedAt,
|
|
939
|
+
nextRunAt:
|
|
940
|
+
nextStatus === 'active' && currentJobRow.schedule.kind !== 'at'
|
|
941
|
+
? (computeNextRunAt(currentJobRow.schedule, completedAt) ?? undefined)
|
|
942
|
+
: undefined,
|
|
943
|
+
lastError: normalizedError,
|
|
944
|
+
}),
|
|
945
|
+
AutonomousJobRowSchema,
|
|
946
|
+
),
|
|
947
|
+
'Failed to update autonomous job failure state.',
|
|
948
|
+
)
|
|
572
949
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
AutonomousJobRunRowSchema,
|
|
586
|
-
)
|
|
950
|
+
yield* maybeNotifyEffect(deps, {
|
|
951
|
+
organizationId: recordIdToString(currentJobRow.organizationId, TABLES.ORGANIZATION),
|
|
952
|
+
threadId: recordIdToString(currentJobRow.threadId, TABLES.THREAD),
|
|
953
|
+
severity: 'warning',
|
|
954
|
+
title: autoPause ? `${currentJobRow.title} paused after repeated failures` : `${currentJobRow.title} failed`,
|
|
955
|
+
body: normalizedError.message,
|
|
956
|
+
metadata: {
|
|
957
|
+
autonomousJobId: job.data.autonomousJobId,
|
|
958
|
+
runId: recordIdToString(currentRunRow.id, TABLES.AUTONOMOUS_JOB_RUN),
|
|
959
|
+
autoPaused: autoPause,
|
|
960
|
+
},
|
|
961
|
+
})
|
|
587
962
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
: autonomousJobRow.nextRunAt
|
|
592
|
-
const nextStatus =
|
|
593
|
-
autonomousJobRow.schedule.kind === 'at' && job.data.trigger === 'scheduled'
|
|
594
|
-
? 'completed'
|
|
595
|
-
: autonomousJobRow.status
|
|
596
|
-
|
|
597
|
-
await databaseService.update(
|
|
598
|
-
TABLES.AUTONOMOUS_JOB,
|
|
599
|
-
autonomousJobRow.id,
|
|
600
|
-
compactRecord({
|
|
601
|
-
status: nextStatus,
|
|
602
|
-
consecutiveErrorCount: 0,
|
|
603
|
-
lastRunStatus: runStatus,
|
|
604
|
-
lastRunAt: completedAt,
|
|
605
|
-
nextRunAt: nextStatus === 'active' ? (nextRunAt ?? undefined) : undefined,
|
|
606
|
-
linkedPlanSpecId: activePlan?.specId ? ensureRecordId(activePlan.specId, TABLES.PLAN_SPEC) : undefined,
|
|
607
|
-
linkedPlanRunId: activePlan?.runId ? ensureRecordId(activePlan.runId, TABLES.PLAN_RUN) : undefined,
|
|
608
|
-
lastError: undefined,
|
|
609
|
-
}),
|
|
610
|
-
AutonomousJobRowSchema,
|
|
611
|
-
)
|
|
963
|
+
return yield* new AutonomousJobServiceError({ message: normalizedError.message, cause: error })
|
|
964
|
+
})
|
|
965
|
+
}
|
|
612
966
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
metadata: {
|
|
620
|
-
autonomousJobId: job.data.autonomousJobId,
|
|
621
|
-
runId: recordIdToString(runRow.id, TABLES.AUTONOMOUS_JOB_RUN),
|
|
622
|
-
},
|
|
623
|
-
})
|
|
967
|
+
function executeQueuedRunEffect(
|
|
968
|
+
deps: AutonomousJobDeps,
|
|
969
|
+
job: Job<AutonomousJobQueuePayload>,
|
|
970
|
+
): Effect.Effect<{ status: string; summary?: string }, AutonomousJobServiceError> {
|
|
971
|
+
let autonomousJobRow: AutonomousJobRow | null = null
|
|
972
|
+
let runRow: AutonomousJobRunRow | null = null
|
|
624
973
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
const autoPause = nextConsecutiveErrorCount >= autonomousJobRow.autoPauseThreshold
|
|
631
|
-
const terminalOneShot = autonomousJobRow.schedule.kind === 'at' && job.data.trigger === 'scheduled'
|
|
632
|
-
const nextStatus: AutonomousJobStatus = terminalOneShot
|
|
633
|
-
? 'failed'
|
|
634
|
-
: autoPause
|
|
635
|
-
? 'paused'
|
|
636
|
-
: autonomousJobRow.status
|
|
637
|
-
|
|
638
|
-
await databaseService.update(
|
|
639
|
-
TABLES.AUTONOMOUS_JOB_RUN,
|
|
640
|
-
runRow.id,
|
|
641
|
-
{ status: 'failed', error: normalizedError, completedAt },
|
|
642
|
-
AutonomousJobRunRowSchema,
|
|
643
|
-
)
|
|
974
|
+
return Effect.gen(function* () {
|
|
975
|
+
yield* effectTryPromise(() => deps.db.connect(), 'Failed to connect to autonomous job database.')
|
|
976
|
+
autonomousJobRow = yield* getRowEffect(deps, job.data.autonomousJobId)
|
|
977
|
+
const currentJobRow = autonomousJobRow
|
|
978
|
+
const { queueJobId, runRow: initialRunRow } = yield* getOrCreateQueuedRunRowEffect(deps, job, currentJobRow)
|
|
644
979
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
}
|
|
980
|
+
runRow = initialRunRow
|
|
981
|
+
if (currentJobRow.status !== 'active' && job.data.trigger === 'scheduled') {
|
|
982
|
+
return { status: 'skipped' }
|
|
983
|
+
}
|
|
648
984
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
985
|
+
runRow = yield* markQueuedRunRunningEffect(deps, initialRunRow, queueJobId)
|
|
986
|
+
const currentRunRow = runRow
|
|
987
|
+
const inputMessage = buildSyntheticUserMessage(currentJobRow.prompt)
|
|
988
|
+
|
|
989
|
+
const [thread, turnModule] = yield* Effect.all([
|
|
990
|
+
deps.thread
|
|
991
|
+
.getThread(currentJobRow.threadId)
|
|
992
|
+
.pipe(
|
|
993
|
+
Effect.mapError(
|
|
994
|
+
(cause) => new AutonomousJobServiceError({ message: 'Failed to load autonomous job thread.', cause }),
|
|
995
|
+
),
|
|
996
|
+
),
|
|
997
|
+
effectTryPromise<ThreadTurnModule>(() => import('./thread/thread-turn'), 'Failed to load thread turn runtime.'),
|
|
998
|
+
])
|
|
999
|
+
|
|
1000
|
+
const turnResult = yield* effectTryPromise(
|
|
1001
|
+
() =>
|
|
1002
|
+
turnModule.runThreadTurnInBackground({
|
|
1003
|
+
thread,
|
|
1004
|
+
threadRef: ensureRecordId(currentJobRow.threadId, TABLES.THREAD),
|
|
1005
|
+
orgRef: ensureRecordId(currentJobRow.organizationId, TABLES.ORGANIZATION),
|
|
1006
|
+
userRef: ensureRecordId(currentJobRow.ownerUserId, TABLES.USER),
|
|
1007
|
+
userName: currentJobRow.ownerUserName,
|
|
1008
|
+
agentIdOverride: currentJobRow.agentId,
|
|
1009
|
+
inputMessage,
|
|
662
1010
|
}),
|
|
663
|
-
|
|
1011
|
+
'Failed to run autonomous job thread turn.',
|
|
1012
|
+
)
|
|
1013
|
+
|
|
1014
|
+
const activePlan = yield* deps.executionPlan
|
|
1015
|
+
.getActivePlanForThread(currentJobRow.threadId)
|
|
1016
|
+
.pipe(
|
|
1017
|
+
Effect.mapError(
|
|
1018
|
+
(cause) => new AutonomousJobServiceError({ message: 'Failed to load active execution plan.', cause }),
|
|
1019
|
+
),
|
|
664
1020
|
)
|
|
665
1021
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
1022
|
+
return yield* completeQueuedRunEffect(deps, { job, currentJobRow, currentRunRow, turnResult, activePlan })
|
|
1023
|
+
}).pipe(
|
|
1024
|
+
Effect.catch((error: unknown) =>
|
|
1025
|
+
Effect.gen(function* () {
|
|
1026
|
+
const currentJobRow = autonomousJobRow
|
|
1027
|
+
const currentRunRow = runRow
|
|
1028
|
+
if (!currentJobRow || !currentRunRow) {
|
|
1029
|
+
return yield* new AutonomousJobServiceError({ message: 'Failed to execute autonomous job.', cause: error })
|
|
1030
|
+
}
|
|
1031
|
+
return yield* failQueuedRunEffect(deps, { job, currentJobRow, currentRunRow, error })
|
|
1032
|
+
}),
|
|
1033
|
+
),
|
|
1034
|
+
)
|
|
1035
|
+
}
|
|
680
1036
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
1037
|
+
function createAutonomousJobService(deps: AutonomousJobDeps) {
|
|
1038
|
+
return {
|
|
1039
|
+
computeNextRunAt,
|
|
1040
|
+
create: (input: CreateAutonomousJobInput) => createEffect(deps, input),
|
|
1041
|
+
recoverActiveJobs: (now = nowDate()) => recoverActiveJobsEffect(deps, now),
|
|
1042
|
+
get: (jobId: RecordIdInput) => getEffect(deps, jobId),
|
|
1043
|
+
list: (params: { organizationId: RecordIdInput; ownerUserId?: RecordIdInput; status?: AutonomousJobStatus }) =>
|
|
1044
|
+
listEffect(deps, params),
|
|
1045
|
+
update: (jobId: RecordIdInput, input: UpdateAutonomousJobInput) => updateEffect(deps, jobId, input),
|
|
1046
|
+
pause: (jobId: RecordIdInput) => pauseEffect(deps, jobId),
|
|
1047
|
+
resume: (jobId: RecordIdInput) => resumeEffect(deps, jobId),
|
|
1048
|
+
runNow: (jobId: RecordIdInput) => runNowEffect(deps, jobId),
|
|
1049
|
+
cancel: (jobId: RecordIdInput) => cancelEffect(deps, jobId),
|
|
1050
|
+
delete: (jobId: RecordIdInput) => deleteJobEffect(deps, jobId),
|
|
1051
|
+
listRuns: (jobId: RecordIdInput) => listRunsEffect(deps, jobId),
|
|
1052
|
+
executeQueuedRun: (job: Job<AutonomousJobQueuePayload>) => executeQueuedRunEffect(deps, job),
|
|
1053
|
+
} as const
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
interface AutonomousJobDeps {
|
|
1057
|
+
db: SurrealDBService
|
|
1058
|
+
config: ResolvedLotaRuntimeConfig
|
|
1059
|
+
executionPlan: ReturnType<typeof makeExecutionPlanService>
|
|
1060
|
+
queueJob: ReturnType<typeof makeQueueJobService>
|
|
1061
|
+
thread: ReturnType<typeof makeThreadService>
|
|
684
1062
|
}
|
|
685
1063
|
|
|
686
|
-
export
|
|
1064
|
+
export function makeAutonomousJobService(deps: AutonomousJobDeps) {
|
|
1065
|
+
return createAutonomousJobService(deps)
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
export class AutonomousJobServiceTag extends Context.Service<
|
|
1069
|
+
AutonomousJobServiceTag,
|
|
1070
|
+
ReturnType<typeof makeAutonomousJobService>
|
|
1071
|
+
>()('AutonomousJobService') {}
|
|
1072
|
+
|
|
1073
|
+
export const AutonomousJobServiceLive = Layer.effect(
|
|
1074
|
+
AutonomousJobServiceTag,
|
|
1075
|
+
Effect.gen(function* () {
|
|
1076
|
+
const db = yield* DatabaseServiceTag
|
|
1077
|
+
const config = yield* RuntimeConfigServiceTag
|
|
1078
|
+
const executionPlan = yield* ExecutionPlanServiceTag
|
|
1079
|
+
const queueJob = yield* QueueJobServiceTag
|
|
1080
|
+
const thread = yield* ThreadServiceTag
|
|
1081
|
+
return makeAutonomousJobService({ db, config, executionPlan, queueJob, thread })
|
|
1082
|
+
}),
|
|
1083
|
+
)
|