@lota-sdk/core 0.1.18 → 0.1.20

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.
Files changed (40) hide show
  1. package/infrastructure/schema/09_queue_job.surql +38 -0
  2. package/infrastructure/schema/10_autonomous_job.surql +44 -0
  3. package/package.json +2 -2
  4. package/src/ai-gateway/ai-gateway.ts +130 -21
  5. package/src/ai-gateway/cache-headers.ts +26 -1
  6. package/src/create-runtime.ts +10 -1
  7. package/src/db/base.service.ts +6 -1
  8. package/src/db/tables.ts +4 -0
  9. package/src/queues/autonomous-job.queue.ts +134 -0
  10. package/src/queues/document-processor.queue.ts +13 -2
  11. package/src/queues/index.ts +1 -0
  12. package/src/queues/memory-consolidation.queue.ts +22 -3
  13. package/src/queues/queue-factory.ts +33 -4
  14. package/src/runtime/chat-run-registry.ts +4 -0
  15. package/src/runtime/context-compaction.ts +100 -12
  16. package/src/runtime/memory-prompts-fact.ts +3 -1
  17. package/src/runtime/runtime-config.ts +1 -1
  18. package/src/runtime/runtime-worker-registry.ts +3 -0
  19. package/src/services/autonomous-job.service.ts +692 -0
  20. package/src/services/index.ts +2 -0
  21. package/src/services/plan-deadline.service.ts +6 -4
  22. package/src/services/queue-job.service.ts +356 -0
  23. package/src/services/workstream-message.service.ts +25 -14
  24. package/src/services/workstream-title.service.ts +1 -1
  25. package/src/services/workstream-turn-preparation.service.ts +22 -6
  26. package/src/services/workstream-turn.ts +11 -3
  27. package/src/services/workstream.service.ts +19 -2
  28. package/src/system-agents/context-compaction.agent.ts +2 -2
  29. package/src/system-agents/delegated-agent-factory.ts +2 -9
  30. package/src/system-agents/memory-reranker.agent.ts +2 -2
  31. package/src/system-agents/memory.agent.ts +2 -2
  32. package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -2
  33. package/src/system-agents/regular-chat-memory-digest.agent.ts +2 -2
  34. package/src/system-agents/skill-extractor.agent.ts +2 -2
  35. package/src/system-agents/skill-manager.agent.ts +2 -2
  36. package/src/system-agents/title-generator.agent.ts +2 -2
  37. package/src/tools/research-topic.tool.ts +2 -2
  38. package/src/utils/date-time.ts +11 -0
  39. package/src/workers/utils/file-section-chunker.ts +1 -1
  40. package/src/workers/worker-utils.ts +35 -7
@@ -525,6 +525,22 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
525
525
  )
526
526
  }
527
527
 
528
+ async clearStaleActiveRunIfMissingFromRegistry(workstreamId: RecordIdRef): Promise<boolean> {
529
+ const activeRunId = await this.getActiveRunId(workstreamId)
530
+ if (!activeRunId || chatRunRegistry.has(activeRunId)) {
531
+ return false
532
+ }
533
+
534
+ const activeStreamId = await this.getActiveStreamId(workstreamId)
535
+ await Promise.all([
536
+ this.clearActiveRunIdIfMatches(workstreamId, activeRunId),
537
+ activeStreamId ? this.clearActiveStreamIdIfMatches(workstreamId, activeStreamId) : Promise.resolve(),
538
+ ])
539
+
540
+ serverLogger.warn`Cleared stale workstream run after process restart: workstream=${recordIdToString(ensureRecordId(workstreamId, TABLES.WORKSTREAM), TABLES.WORKSTREAM)} run=${activeRunId}`
541
+ return true
542
+ }
543
+
528
544
  async stopActiveRun(workstreamId: RecordIdRef): Promise<boolean> {
529
545
  const activeRunId = await this.getActiveRunId(workstreamId)
530
546
  if (!activeRunId) return false
@@ -534,7 +550,7 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
534
550
  return true
535
551
  }
536
552
 
537
- await this.clearActiveRunIdIfMatches(workstreamId, activeRunId)
553
+ await this.clearStaleActiveRunIfMissingFromRegistry(workstreamId)
538
554
  return false
539
555
  }
540
556
 
@@ -675,6 +691,7 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
675
691
  typeof workstream.activeRunId === 'string' && workstream.activeRunId.trim().length > 0
676
692
  ? workstream.activeRunId
677
693
  : null
694
+ const isRunning = activeRunId !== null && chatRunRegistry.has(activeRunId)
678
695
  const isCompacting = workstream.isCompacting === true
679
696
  const mode = workstream.mode
680
697
  const core = workstream.core
@@ -688,7 +705,7 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
688
705
  core,
689
706
  ...(coreType ? { coreType } : {}),
690
707
  nameGenerated: workstream.nameGenerated,
691
- isRunning: activeRunId !== null,
708
+ isRunning,
692
709
  isCompacting,
693
710
  ...(isAgentName(workstream.agentId) ? { agentId: workstream.agentId } : {}),
694
711
  title: workstream.title ?? this.getDefaultTitle(workstream),
@@ -1,7 +1,7 @@
1
1
  import { ToolLoopAgent } from 'ai'
2
2
 
3
3
  import { aiGatewayOpenRouterResponseHealingModel } from '../ai-gateway/ai-gateway'
4
- import { buildAiGatewayCacheHeaders } from '../ai-gateway/cache-headers'
4
+ import { buildAiGatewayDirectCacheHeaders } from '../ai-gateway/cache-headers'
5
5
  import {
6
6
  OPENROUTER_STRUCTURED_HELPER_MODEL_ID,
7
7
  OPENROUTER_HIGH_REASONING_PROVIDER_OPTIONS,
@@ -41,7 +41,7 @@ export function createContextCompactionAgent(options: CreateHelperToolLoopAgentO
41
41
  return new ToolLoopAgent({
42
42
  id: 'context-compaction',
43
43
  model: aiGatewayOpenRouterResponseHealingModel(OPENROUTER_STRUCTURED_HELPER_MODEL_ID),
44
- headers: buildAiGatewayCacheHeaders('context-compaction'),
44
+ headers: buildAiGatewayDirectCacheHeaders('context-compaction'),
45
45
  providerOptions: OPENROUTER_HIGH_REASONING_PROVIDER_OPTIONS,
46
46
  ...resolveHelperAgentOptions(options, { instructions: CONTEXT_COMPACTION_PROMPT }),
47
47
  })
@@ -4,6 +4,7 @@ import { z } from 'zod'
4
4
 
5
5
  import type { ToolDefinition } from '../ai/definitions'
6
6
  import { aiLogger } from '../config/logger'
7
+ import { formatUtcPromptDate } from '../utils/date-time'
7
8
  import { isRecord } from '../utils/string'
8
9
  import { assertSubstantiveAgentResult } from './agent-result'
9
10
 
@@ -32,15 +33,7 @@ function resolveAgentModel(model: AgentModel): LanguageModel {
32
33
  }
33
34
 
34
35
  function buildCurrentDateContext(now = new Date()): string {
35
- const isoDate = now.toISOString().slice(0, 10)
36
- const humanDate = new Intl.DateTimeFormat('en-US', {
37
- timeZone: 'UTC',
38
- year: 'numeric',
39
- month: 'long',
40
- day: 'numeric',
41
- }).format(now)
42
-
43
- return [`Today is ${isoDate} (${humanDate}, UTC).`, 'Use this exact date for any recency reasoning.'].join(' ')
36
+ return [`Today is ${formatUtcPromptDate(now)}.`, 'Use this exact date for any recency reasoning.'].join(' ')
44
37
  }
45
38
 
46
39
  export function buildRecencyInstructions(tools?: ToolSet): string {
@@ -1,7 +1,7 @@
1
1
  import { ToolLoopAgent } from 'ai'
2
2
 
3
3
  import { aiGatewayOpenRouterResponseHealingModel } from '../ai-gateway/ai-gateway'
4
- import { buildAiGatewayCacheHeaders } from '../ai-gateway/cache-headers'
4
+ import { buildAiGatewayDirectCacheHeaders } from '../ai-gateway/cache-headers'
5
5
  import {
6
6
  OPENROUTER_LOW_REASONING_PROVIDER_OPTIONS,
7
7
  OPENROUTER_STRUCTURED_HELPER_MODEL_ID,
@@ -33,7 +33,7 @@ export function createMemoryRerankerAgent(options: CreateHelperToolLoopAgentOpti
33
33
  return new ToolLoopAgent({
34
34
  id: 'memory-reranker',
35
35
  model: aiGatewayOpenRouterResponseHealingModel(OPENROUTER_STRUCTURED_HELPER_MODEL_ID),
36
- headers: buildAiGatewayCacheHeaders('memory-reranker'),
36
+ headers: buildAiGatewayDirectCacheHeaders('memory-reranker'),
37
37
  providerOptions: OPENROUTER_LOW_REASONING_PROVIDER_OPTIONS,
38
38
  ...resolveHelperAgentOptions(options),
39
39
  })
@@ -1,7 +1,7 @@
1
1
  import { ToolLoopAgent } from 'ai'
2
2
 
3
3
  import { aiGatewayOpenRouterResponseHealingModel } from '../ai-gateway/ai-gateway'
4
- import { buildAiGatewayCacheHeaders } from '../ai-gateway/cache-headers'
4
+ import { buildAiGatewayDirectCacheHeaders } from '../ai-gateway/cache-headers'
5
5
  import {
6
6
  OPENROUTER_STRUCTURED_HELPER_MODEL_ID,
7
7
  OPENROUTER_HIGH_REASONING_PROVIDER_OPTIONS,
@@ -53,7 +53,7 @@ export function createOrgMemoryAgent(options: CreateHelperToolLoopAgentOptions)
53
53
  return new ToolLoopAgent({
54
54
  id: 'org-memory',
55
55
  model: aiGatewayOpenRouterResponseHealingModel(OPENROUTER_STRUCTURED_HELPER_MODEL_ID),
56
- headers: buildAiGatewayCacheHeaders('org-memory'),
56
+ headers: buildAiGatewayDirectCacheHeaders('org-memory'),
57
57
  providerOptions: OPENROUTER_HIGH_REASONING_PROVIDER_OPTIONS,
58
58
  ...resolveHelperAgentOptions(options),
59
59
  })
@@ -1,7 +1,7 @@
1
1
  import { ToolLoopAgent } from 'ai'
2
2
 
3
3
  import { aiGatewayModel } from '../ai-gateway/ai-gateway'
4
- import { buildAiGatewayCacheHeaders } from '../ai-gateway/cache-headers'
4
+ import { buildAiGatewayDirectCacheHeaders } from '../ai-gateway/cache-headers'
5
5
  import { getLeadAgentDisplayName } from '../config/agent-defaults'
6
6
  import {
7
7
  OPENROUTER_STRUCTURED_HELPER_MODEL_ID,
@@ -80,7 +80,7 @@ export function createRecentActivityTitleRefinerAgent(options: CreateHelperToolL
80
80
  return new ToolLoopAgent({
81
81
  id: 'recent-activity-title-refiner',
82
82
  model: aiGatewayModel(OPENROUTER_STRUCTURED_HELPER_MODEL_ID),
83
- headers: buildAiGatewayCacheHeaders('recent-activity-title-refiner'),
83
+ headers: buildAiGatewayDirectCacheHeaders('recent-activity-title-refiner'),
84
84
  providerOptions: OPENROUTER_HIGH_REASONING_PROVIDER_OPTIONS,
85
85
  ...resolveHelperAgentOptions(options, {
86
86
  instructions: buildRecentActivityTitleRefinerPrompt(),
@@ -1,7 +1,7 @@
1
1
  import { ToolLoopAgent } from 'ai'
2
2
 
3
3
  import { aiGatewayOpenRouterResponseHealingModel } from '../ai-gateway/ai-gateway'
4
- import { buildAiGatewayCacheHeaders } from '../ai-gateway/cache-headers'
4
+ import { buildAiGatewayDirectCacheHeaders } from '../ai-gateway/cache-headers'
5
5
  import {
6
6
  OPENROUTER_STRUCTURED_HELPER_MODEL_ID,
7
7
  OPENROUTER_HIGH_REASONING_PROVIDER_OPTIONS,
@@ -67,7 +67,7 @@ export function createRegularChatMemoryDigestAgent(options: CreateHelperToolLoop
67
67
  return new ToolLoopAgent({
68
68
  id: 'regular-chat-memory-digest',
69
69
  model: aiGatewayOpenRouterResponseHealingModel(OPENROUTER_STRUCTURED_HELPER_MODEL_ID),
70
- headers: buildAiGatewayCacheHeaders('regular-chat-memory-digest'),
70
+ headers: buildAiGatewayDirectCacheHeaders('regular-chat-memory-digest'),
71
71
  providerOptions: OPENROUTER_HIGH_REASONING_PROVIDER_OPTIONS,
72
72
  ...resolveHelperAgentOptions(options, {
73
73
  instructions: regularChatMemoryDigestPrompt,
@@ -2,7 +2,7 @@ import { ToolLoopAgent } from 'ai'
2
2
  import { z } from 'zod'
3
3
 
4
4
  import { aiGatewayOpenRouterResponseHealingModel } from '../ai-gateway/ai-gateway'
5
- import { buildAiGatewayCacheHeaders } from '../ai-gateway/cache-headers'
5
+ import { buildAiGatewayDirectCacheHeaders } from '../ai-gateway/cache-headers'
6
6
  import {
7
7
  OPENROUTER_STRUCTURED_HELPER_MODEL_ID,
8
8
  OPENROUTER_HIGH_REASONING_PROVIDER_OPTIONS,
@@ -80,7 +80,7 @@ export function createSkillExtractorAgent(options: CreateHelperToolLoopAgentOpti
80
80
  return new ToolLoopAgent({
81
81
  id: 'skill-extractor',
82
82
  model: aiGatewayOpenRouterResponseHealingModel(OPENROUTER_STRUCTURED_HELPER_MODEL_ID),
83
- headers: buildAiGatewayCacheHeaders('skill-extractor'),
83
+ headers: buildAiGatewayDirectCacheHeaders('skill-extractor'),
84
84
  providerOptions: OPENROUTER_HIGH_REASONING_PROVIDER_OPTIONS,
85
85
  ...resolveHelperAgentOptions(options, {
86
86
  instructions: skillExtractorPrompt,
@@ -2,7 +2,7 @@ import { ToolLoopAgent } from 'ai'
2
2
  import { z } from 'zod'
3
3
 
4
4
  import { aiGatewayOpenRouterResponseHealingModel } from '../ai-gateway/ai-gateway'
5
- import { buildAiGatewayCacheHeaders } from '../ai-gateway/cache-headers'
5
+ import { buildAiGatewayDirectCacheHeaders } from '../ai-gateway/cache-headers'
6
6
  import {
7
7
  OPENROUTER_STRUCTURED_HELPER_MODEL_ID,
8
8
  OPENROUTER_HIGH_REASONING_PROVIDER_OPTIONS,
@@ -70,7 +70,7 @@ export function createSkillManagerAgent(options: CreateHelperToolLoopAgentOption
70
70
  return new ToolLoopAgent({
71
71
  id: 'skill-manager',
72
72
  model: aiGatewayOpenRouterResponseHealingModel(OPENROUTER_STRUCTURED_HELPER_MODEL_ID),
73
- headers: buildAiGatewayCacheHeaders('skill-manager'),
73
+ headers: buildAiGatewayDirectCacheHeaders('skill-manager'),
74
74
  providerOptions: OPENROUTER_HIGH_REASONING_PROVIDER_OPTIONS,
75
75
  ...resolveHelperAgentOptions(options, {
76
76
  instructions: skillManagerPrompt,
@@ -1,7 +1,7 @@
1
1
  import { ToolLoopAgent } from 'ai'
2
2
 
3
3
  import { aiGatewayModel } from '../ai-gateway/ai-gateway'
4
- import { buildAiGatewayCacheHeaders } from '../ai-gateway/cache-headers'
4
+ import { buildAiGatewayDirectCacheHeaders } from '../ai-gateway/cache-headers'
5
5
  import {
6
6
  OPENROUTER_FAST_REASONING_MODEL_ID,
7
7
  OPENROUTER_MINIMAL_REASONING_PROVIDER_OPTIONS,
@@ -34,7 +34,7 @@ export function createWorkstreamTitleGeneratorAgent(options: CreateHelperToolLoo
34
34
  return new ToolLoopAgent({
35
35
  id: 'workstream-title-generator',
36
36
  model: aiGatewayModel(OPENROUTER_FAST_REASONING_MODEL_ID),
37
- headers: buildAiGatewayCacheHeaders('workstream-title-generator'),
37
+ headers: buildAiGatewayDirectCacheHeaders('workstream-title-generator'),
38
38
  providerOptions: OPENROUTER_MINIMAL_REASONING_PROVIDER_OPTIONS,
39
39
  ...resolveHelperAgentOptions(options, {
40
40
  instructions: WORKSTREAM_TITLE_GENERATOR_PROMPT,
@@ -1,5 +1,5 @@
1
1
  import { aiGatewayChatModel } from '../ai-gateway/ai-gateway'
2
- import { buildAiGatewayCacheHeaders } from '../ai-gateway/cache-headers'
2
+ import { buildAiGatewayStrictSemanticCacheHeaders } from '../ai-gateway/cache-headers'
3
3
  import {
4
4
  OPENROUTER_MEDIUM_REASONING_PROVIDER_OPTIONS,
5
5
  OPENROUTER_WEB_RESEARCH_MODEL_ID,
@@ -15,7 +15,7 @@ export const researchTopicTool = createDelegatedAgentTool({
15
15
  'Delegate a research task to a dedicated research agent that searches the web, fetches pages, and returns a synthesized markdown report. Call multiple instances in parallel for broad research across different topics.',
16
16
  model: () => aiGatewayChatModel(OPENROUTER_WEB_RESEARCH_MODEL_ID),
17
17
  providerOptions: OPENROUTER_MEDIUM_REASONING_PROVIDER_OPTIONS,
18
- headers: buildAiGatewayCacheHeaders('researchTopic'),
18
+ headers: buildAiGatewayStrictSemanticCacheHeaders('researchTopic'),
19
19
  instructions: RESEARCHER_PROMPT,
20
20
  tools: { searchWeb: searchWebTool.create(), fetchWebpage: fetchWebpageTool.create() },
21
21
  })
@@ -1,6 +1,17 @@
1
1
  export { toIsoDateTimeString, toOptionalIsoDateTimeString } from '@lota-sdk/shared'
2
2
 
3
+ const PROMPT_DATE_FORMATTER = new Intl.DateTimeFormat('en-US', {
4
+ timeZone: 'UTC',
5
+ year: 'numeric',
6
+ month: 'long',
7
+ day: 'numeric',
8
+ })
9
+
3
10
  export function toDatabaseDateTime(value: string | Date | null | undefined): Date | undefined {
4
11
  if (value === null || value === undefined) return undefined
5
12
  return value instanceof Date ? value : new Date(value)
6
13
  }
14
+
15
+ export function formatUtcPromptDate(value: Date): string {
16
+ return PROMPT_DATE_FORMATTER.format(value)
17
+ }
@@ -1,6 +1,6 @@
1
1
  import { CHARS_PER_TOKEN_ESTIMATE } from '../../utils/string'
2
2
 
3
- export const DEFAULT_FILE_SECTION_CHUNK_MAX_CHARS = 250_000
3
+ export const DEFAULT_FILE_SECTION_CHUNK_MAX_CHARS = 1_500_000
4
4
  export const MIN_FILE_SECTION_CHUNK_MAX_CHARS = 4_000
5
5
  export const DEFAULT_FILE_SECTION_CHUNK_MIN_CHARS = 10_000
6
6
  const SECTION_SEPARATOR_LENGTH = 2
@@ -4,10 +4,11 @@ import { fileURLToPath } from 'node:url'
4
4
  import type { Job, Worker } from 'bullmq'
5
5
 
6
6
  import { chatLogger } from '../config/logger'
7
+ import { queueJobService } from '../services/queue-job.service'
7
8
  import { truncateText } from '../utils/string'
8
9
 
9
- export const DEFAULT_JOB_RETENTION = { removeOnComplete: 200, removeOnFail: 200 }
10
- export const LOW_JOB_RETENTION = { removeOnComplete: 50, removeOnFail: 50 }
10
+ export const DEFAULT_JOB_RETENTION = { removeOnComplete: true, removeOnFail: { age: 24 * 60 * 60, count: 200 } }
11
+ export const LOW_JOB_RETENTION = { removeOnComplete: true, removeOnFail: { age: 6 * 60 * 60, count: 50 } }
11
12
  export const LONG_JOB_LOCK_DURATION_MS = 600_000
12
13
 
13
14
  const DEFAULT_SHUTDOWN_TIMEOUT_MS = 10_000
@@ -30,6 +31,8 @@ interface TracedWorkerJobLike {
30
31
  name: string
31
32
  attemptsMade: number | null | undefined
32
33
  data?: unknown
34
+ opts?: unknown
35
+ timestamp?: number
33
36
  }
34
37
 
35
38
  function truncateTraceString(value: string, maxChars = MAX_TRACE_STRING_CHARS): string {
@@ -143,11 +146,36 @@ export const createWorkerShutdown = (worker: Worker, name: string, logger: typeo
143
146
  }
144
147
  }
145
148
 
146
- export function createTracedWorkerProcessor<TJob extends TracedWorkerJobLike>(
147
- _queueName: string,
148
- processor: (job: TJob) => Promise<void>,
149
- ): (job: TJob) => Promise<void> {
150
- return async (job: TJob) => processor(job)
149
+ export function createTracedWorkerProcessor<TJob extends TracedWorkerJobLike, TResult = void>(
150
+ queueName: string,
151
+ processor: (job: TJob) => Promise<TResult>,
152
+ ): (job: TJob) => Promise<TResult> {
153
+ return async (job: TJob) => {
154
+ const trackedJob = {
155
+ queueName,
156
+ id: typeof job.id === 'string' || typeof job.id === 'number' ? job.id : undefined,
157
+ name: job.name,
158
+ attemptsMade: job.attemptsMade,
159
+ data: job.data,
160
+ opts: job.opts,
161
+ timestamp: job.timestamp,
162
+ }
163
+
164
+ await queueJobService.markAttemptStarted(trackedJob)
165
+
166
+ try {
167
+ const result = await processor(job)
168
+ await queueJobService.markAttemptCompleted(trackedJob, result)
169
+ return result
170
+ } catch (error) {
171
+ try {
172
+ await queueJobService.markAttemptFailed(trackedJob, error)
173
+ } catch (persistenceError) {
174
+ chatLogger.error`Failed to persist queue job failure (queue=${queueName}, job=${job.id}): ${persistenceError}`
175
+ }
176
+ throw error
177
+ }
178
+ }
151
179
  }
152
180
 
153
181
  export const registerShutdownSignals = ({