@lota-sdk/core 0.1.18 → 0.1.19

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lota-sdk/core",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -32,7 +32,7 @@
32
32
  "@chat-adapter/slack": "^4.23.0",
33
33
  "@chat-adapter/state-ioredis": "^4.23.0",
34
34
  "@logtape/logtape": "^2.0.5",
35
- "@lota-sdk/shared": "0.1.18",
35
+ "@lota-sdk/shared": "0.1.19",
36
36
  "@mendable/firecrawl-js": "^4.17.0",
37
37
  "@surrealdb/node": "^3.0.3",
38
38
  "ai": "^6.0.137",
@@ -5,6 +5,7 @@ import type { LanguageModelMiddleware } from 'ai'
5
5
 
6
6
  import { getRuntimeConfig } from '../runtime/runtime-config'
7
7
  import { isRecord, readString } from '../utils/string'
8
+ import { buildAiGatewayCacheHeaders } from './cache-headers'
8
9
 
9
10
  type AiGatewayLanguageModel = Parameters<typeof wrapLanguageModel>[0]['model']
10
11
  type AiGatewayExtraParams = Record<string, unknown>
@@ -17,6 +18,7 @@ type AiGatewayStreamResult = Awaited<ReturnType<WrapStreamOptions['doStream']>>
17
18
  type AiGatewayGeneratedContent = AiGatewayGenerateResult['content'][number]
18
19
  type AiGatewayStreamPart = AiGatewayStreamResult['stream'] extends ReadableStream<infer T> ? T : never
19
20
  type AiGatewayConfig = { apiKey: string; baseURL: string }
21
+ type AiGatewayProviderOptions = NonNullable<AiGatewayCallOptions['providerOptions']>
20
22
 
21
23
  const EXPECTED_GATEWAY_KEY_PREFIX = 'sk-bf-'
22
24
  const AI_GATEWAY_VIRTUAL_KEY_HEADER = 'x-bf-vk'
@@ -26,6 +28,38 @@ const OPENROUTER_RESPONSE_HEALING_EXTRA_PARAMS = {
26
28
  plugins: [{ id: 'response-healing' }],
27
29
  } as const satisfies AiGatewayExtraParams
28
30
 
31
+ function toAiGatewayCacheKeyPart(value: string): string {
32
+ const normalized = value
33
+ .trim()
34
+ .toLowerCase()
35
+ .replace(/[^a-z0-9:_-]+/g, '-')
36
+ .replace(/-+/g, '-')
37
+ return normalized.replace(/^-+|-+$/g, '') || 'request'
38
+ }
39
+
40
+ function mergeAiGatewayHeaders(
41
+ existingHeaders: AiGatewayCallOptions['headers'] | undefined,
42
+ additionalHeaders: Record<string, string>,
43
+ ): Record<string, string> {
44
+ const merged = new Headers(existingHeaders as HeadersInit | undefined)
45
+ for (const [key, value] of Object.entries(additionalHeaders)) {
46
+ if (!merged.has(key)) {
47
+ merged.set(key, value)
48
+ }
49
+ }
50
+ return Object.fromEntries(merged.entries())
51
+ }
52
+
53
+ function withDefaultAiGatewayCacheHeaders(params: AiGatewayCallOptions, modelId: string): AiGatewayCallOptions {
54
+ return {
55
+ ...params,
56
+ headers: mergeAiGatewayHeaders(
57
+ params.headers,
58
+ buildAiGatewayCacheHeaders(`model:${toAiGatewayCacheKeyPart(modelId)}`),
59
+ ),
60
+ }
61
+ }
62
+
29
63
  function normalizeAiGatewayUrl(value: string): string {
30
64
  const trimmed = value.trim()
31
65
  if (!trimmed) {
@@ -37,10 +71,10 @@ function normalizeAiGatewayUrl(value: string): string {
37
71
  }
38
72
 
39
73
  function readDirectEnvAiGatewayConfig(): AiGatewayConfig {
40
- const apiKey = (process.env.LOTA_KEY ?? process.env.VENTUROS_KEY ?? '').trim()
74
+ const apiKey = (process.env.AI_GATEWAY_KEY ?? '').trim()
41
75
  if (!apiKey) {
42
76
  throw new Error(
43
- '[ai-gateway] Missing AI gateway key. Set LOTA_KEY or VENTUROS_KEY, or configure createLotaRuntime({ aiGateway: { key } }).',
77
+ '[ai-gateway] Missing AI gateway key. Set AI_GATEWAY_KEY, or configure createLotaRuntime({ aiGateway: { key } }).',
44
78
  )
45
79
  }
46
80
 
@@ -234,6 +268,30 @@ function addAiGatewayReasoningRawChunks(
234
268
  return { ...params, includeRawChunks: true }
235
269
  }
236
270
 
271
+ export function normalizeAiGatewayChatProviderOptions(params: AiGatewayCallOptions): AiGatewayCallOptions {
272
+ const providerOptions = isRecord(params.providerOptions)
273
+ ? ({ ...params.providerOptions } as AiGatewayProviderOptions)
274
+ : ({} as AiGatewayProviderOptions)
275
+ const openaiOptions = isRecord(providerOptions.openai)
276
+ ? { ...providerOptions.openai }
277
+ : ({} as Record<string, unknown>)
278
+
279
+ if (openaiOptions.systemMessageMode === 'system') {
280
+ return params
281
+ }
282
+
283
+ return {
284
+ ...params,
285
+ providerOptions: {
286
+ ...providerOptions,
287
+ openai: {
288
+ ...openaiOptions,
289
+ ...(openaiOptions.systemMessageMode === 'remove' ? {} : { systemMessageMode: 'system' }),
290
+ },
291
+ },
292
+ }
293
+ }
294
+
237
295
  export function injectAiGatewayExtraParamsRequestBody(
238
296
  body: BodyInit | null | undefined,
239
297
  extraParams: AiGatewayExtraParams,
@@ -309,7 +367,8 @@ export function aiGatewayModel(modelId: string) {
309
367
  model: getAiGatewayProvider()(modelId),
310
368
  middleware: {
311
369
  specificationVersion: 'v3',
312
- transformParams: async ({ params, type }) => addAiGatewayReasoningRawChunks(params, type),
370
+ transformParams: async ({ params, type }) =>
371
+ withDefaultAiGatewayCacheHeaders(addAiGatewayReasoningRawChunks(params, type), modelId),
313
372
  wrapStream: async ({ doStream, params }) => {
314
373
  const result = await doStream()
315
374
  if (!isReasoningEnabled(params)) return result
@@ -322,7 +381,15 @@ export function aiGatewayModel(modelId: string) {
322
381
  }
323
382
 
324
383
  export function aiGatewayOpenRouterResponseHealingModel(modelId: string) {
325
- return withAiGatewayDevTools(getAiGatewayOpenRouterResponseHealingProvider()(modelId))
384
+ return withAiGatewayDevTools(
385
+ wrapLanguageModel({
386
+ model: getAiGatewayOpenRouterResponseHealingProvider()(modelId),
387
+ middleware: {
388
+ specificationVersion: 'v3',
389
+ transformParams: async ({ params }) => withDefaultAiGatewayCacheHeaders(params, modelId),
390
+ },
391
+ }),
392
+ )
326
393
  }
327
394
 
328
395
  export function aiGatewayChatModel(modelId: string) {
@@ -331,7 +398,10 @@ export function aiGatewayChatModel(modelId: string) {
331
398
  model: getAiGatewayProvider().chat(modelId),
332
399
  middleware: {
333
400
  specificationVersion: 'v3',
334
- transformParams: async ({ params, type }) => addAiGatewayReasoningRawChunks(params, type),
401
+ transformParams: async ({ params, type }) =>
402
+ normalizeAiGatewayChatProviderOptions(
403
+ withDefaultAiGatewayCacheHeaders(addAiGatewayReasoningRawChunks(params, type), modelId),
404
+ ),
335
405
  wrapGenerate: async ({ doGenerate }) => {
336
406
  const result = await doGenerate()
337
407
 
@@ -1,8 +1,33 @@
1
1
  const AI_GATEWAY_CACHE_KEY_HEADER = 'x-bf-cache-key'
2
2
  const AI_GATEWAY_CACHE_TTL_HEADER = 'x-bf-cache-ttl'
3
+ const AI_GATEWAY_CACHE_THRESHOLD_HEADER = 'x-bf-cache-threshold'
4
+ const AI_GATEWAY_CACHE_TYPE_HEADER = 'x-bf-cache-type'
3
5
 
4
- export function buildAiGatewayCacheHeaders(cacheKey: string, ttl?: string): Record<string, string> {
6
+ export const AI_GATEWAY_STRICT_SEMANTIC_CACHE_THRESHOLD = 0.975
7
+
8
+ export type AiGatewayCacheType = 'direct' | 'semantic'
9
+
10
+ export function buildAiGatewayCacheHeaders(
11
+ cacheKey: string,
12
+ ttl?: string,
13
+ threshold?: number,
14
+ cacheType?: AiGatewayCacheType,
15
+ ): Record<string, string> {
5
16
  const headers: Record<string, string> = { [AI_GATEWAY_CACHE_KEY_HEADER]: cacheKey }
6
17
  if (ttl) headers[AI_GATEWAY_CACHE_TTL_HEADER] = ttl
18
+ if (typeof threshold === 'number') headers[AI_GATEWAY_CACHE_THRESHOLD_HEADER] = String(threshold)
19
+ if (cacheType) headers[AI_GATEWAY_CACHE_TYPE_HEADER] = cacheType
7
20
  return headers
8
21
  }
22
+
23
+ export function buildAiGatewayDirectCacheHeaders(cacheKey: string, ttl?: string): Record<string, string> {
24
+ return buildAiGatewayCacheHeaders(cacheKey, ttl, undefined, 'direct')
25
+ }
26
+
27
+ export function buildAiGatewayStrictSemanticCacheHeaders(
28
+ cacheKey: string,
29
+ ttl?: string,
30
+ threshold = AI_GATEWAY_STRICT_SEMANTIC_CACHE_THRESHOLD,
31
+ ): Record<string, string> {
32
+ return buildAiGatewayCacheHeaders(cacheKey, ttl, threshold, 'semantic')
33
+ }
@@ -1,6 +1,10 @@
1
1
  export class ChatRunRegistry {
2
2
  private controllers = new Map<string, AbortController>()
3
3
 
4
+ has(runId: string): boolean {
5
+ return this.controllers.has(runId)
6
+ }
7
+
4
8
  register(runId: string, controller: AbortController): void {
5
9
  this.controllers.set(runId, controller)
6
10
  }
@@ -101,6 +101,90 @@ function sanitizeStateText(value: string): string | null {
101
101
  return normalized
102
102
  }
103
103
 
104
+ function buildExistingWorkstreamStateForCompactionPrompt(state: WorkstreamState): Record<string, unknown> {
105
+ const currentPlanText = state.currentPlan ? sanitizeStateText(state.currentPlan.text) : null
106
+
107
+ const activeConstraints = state.activeConstraints
108
+ .map((constraint) => ({
109
+ id: constraint.id,
110
+ text: sanitizeStateText(constraint.text),
111
+ source: constraint.source,
112
+ approved: constraint.approved,
113
+ sourceMessageIds: constraint.sourceMessageIds,
114
+ }))
115
+ .filter((constraint): constraint is typeof constraint & { text: string } => Boolean(constraint.text))
116
+
117
+ const keyDecisions = state.keyDecisions
118
+ .map((decision) => ({
119
+ id: decision.id,
120
+ decision: sanitizeStateText(decision.decision),
121
+ rationale: sanitizeStateText(decision.rationale),
122
+ agent: decision.agent,
123
+ sourceMessageIds: decision.sourceMessageIds,
124
+ confidence: decision.confidence,
125
+ }))
126
+ .filter((decision): decision is typeof decision & { decision: string; rationale: string } =>
127
+ Boolean(decision.decision && decision.rationale),
128
+ )
129
+
130
+ const tasks = state.tasks
131
+ .map((task) => ({
132
+ id: task.id,
133
+ title: sanitizeStateText(task.title),
134
+ status: task.status,
135
+ owner: sanitizeStateText(task.owner),
136
+ externalId: task.externalId,
137
+ source: task.source,
138
+ sourceMessageIds: task.sourceMessageIds,
139
+ }))
140
+ .filter((task): task is typeof task & { title: string; owner: string } => Boolean(task.title && task.owner))
141
+
142
+ const openQuestions = state.openQuestions
143
+ .map((question) => ({
144
+ id: question.id,
145
+ text: sanitizeStateText(question.text),
146
+ source: question.source,
147
+ sourceMessageIds: question.sourceMessageIds,
148
+ }))
149
+ .filter((question): question is typeof question & { text: string } => Boolean(question.text))
150
+
151
+ const artifacts = state.artifacts
152
+ .map((artifact) => ({
153
+ id: artifact.id,
154
+ name: sanitizeStateText(artifact.name),
155
+ type: artifact.type,
156
+ pointer: sanitizeStateText(artifact.pointer),
157
+ }))
158
+ .filter((artifact): artifact is typeof artifact & { name: string; pointer: string } =>
159
+ Boolean(artifact.name && artifact.pointer),
160
+ )
161
+
162
+ const agentContributions = state.agentContributions
163
+ .map((note) => ({ id: note.id, agent: note.agent, summary: sanitizeStateText(note.summary) }))
164
+ .filter((note): note is typeof note & { summary: string } => Boolean(note.summary))
165
+
166
+ return {
167
+ currentPlan: currentPlanText
168
+ ? {
169
+ id: state.currentPlan?.id,
170
+ text: currentPlanText,
171
+ source: state.currentPlan?.source,
172
+ approved: state.currentPlan?.approved,
173
+ sourceMessageIds: state.currentPlan?.sourceMessageIds ?? [],
174
+ }
175
+ : null,
176
+ activeConstraints,
177
+ keyDecisions,
178
+ tasks,
179
+ openQuestions,
180
+ risks: state.risks.map((risk) => sanitizeStateText(risk)).filter((risk): risk is string => Boolean(risk)),
181
+ artifacts,
182
+ agentContributions,
183
+ approvedBy: state.approvedBy ? sanitizeStateText(state.approvedBy) : undefined,
184
+ approvalNote: state.approvalNote ? sanitizeStateText(state.approvalNote) : undefined,
185
+ }
186
+ }
187
+
104
188
  function createStableId(prefix: string, ...parts: Array<string | number | undefined>): string {
105
189
  const payload = parts
106
190
  .map((part) => (part === undefined ? '' : String(part)))
@@ -488,7 +572,7 @@ export function buildContextCompactionPrompt(params: ContextCompactionPromptPara
488
572
  params.previousSummary.trim() || 'None',
489
573
  '</previous-summary>',
490
574
  '<existing-workstream-state>',
491
- JSON.stringify(params.existingState),
575
+ JSON.stringify(buildExistingWorkstreamStateForCompactionPrompt(params.existingState)),
492
576
  '</existing-workstream-state>',
493
577
  '<new-messages>',
494
578
  params.transcript || 'None',
@@ -605,27 +689,34 @@ export function createContextCompactionRuntime(
605
689
  .filter((constraint): constraint is typeof constraint & { text: string } => Boolean(constraint.text))
606
690
 
607
691
  const openQuestions = state.openQuestions
608
- .map((question) => ({ ...question, text: sanitizeStateText(question.text) }))
609
- .filter((question): question is typeof question & { text: string } => Boolean(question.text))
692
+ .map((question) => sanitizeStateText(question.text))
693
+ .filter((question): question is string => Boolean(question))
610
694
 
611
695
  const decisions = state.keyDecisions
612
696
  .map((decision) => ({
613
- ...decision,
697
+ agent: decision.agent,
614
698
  decision: sanitizeStateText(decision.decision),
615
699
  rationale: sanitizeStateText(decision.rationale),
700
+ confidence: decision.confidence,
616
701
  }))
617
702
  .filter((decision): decision is typeof decision & { decision: string; rationale: string } =>
618
703
  Boolean(decision.decision && decision.rationale),
619
704
  )
620
705
 
621
706
  const tasks = state.tasks
622
- .map((task) => ({ ...task, title: sanitizeStateText(task.title), owner: sanitizeStateText(task.owner) }))
707
+ .map((task) => ({
708
+ title: sanitizeStateText(task.title),
709
+ status: task.status,
710
+ owner: sanitizeStateText(task.owner),
711
+ externalId: task.externalId,
712
+ source: task.source,
713
+ }))
623
714
  .filter((task): task is typeof task & { title: string; owner: string } => Boolean(task.title && task.owner))
624
715
 
625
716
  const artifacts = state.artifacts
626
717
  .map((artifact) => ({
627
- ...artifact,
628
718
  name: sanitizeStateText(artifact.name),
719
+ type: artifact.type,
629
720
  pointer: sanitizeStateText(artifact.pointer),
630
721
  }))
631
722
  .filter((artifact): artifact is typeof artifact & { name: string; pointer: string } =>
@@ -633,7 +724,7 @@ export function createContextCompactionRuntime(
633
724
  )
634
725
 
635
726
  const agentContributions = state.agentContributions
636
- .map((note) => ({ ...note, summary: sanitizeStateText(note.summary) }))
727
+ .map((note) => ({ agent: note.agent, summary: sanitizeStateText(note.summary) }))
637
728
  .filter((note): note is typeof note & { summary: string } => Boolean(note.summary))
638
729
 
639
730
  const payload = {
@@ -661,12 +752,9 @@ export function createContextCompactionRuntime(
661
752
  artifacts,
662
753
  agentContributions,
663
754
  advisory: {
664
- approvedBy: state.approvedBy ?? null,
665
- approvedAt: state.approvedAt ?? null,
666
- approvalMessageId: state.approvalMessageId ?? null,
667
- approvalNote: state.approvalNote ?? null,
755
+ approvedBy: state.approvedBy ? sanitizeStateText(state.approvedBy) : null,
756
+ approvalNote: state.approvalNote ? sanitizeStateText(state.approvalNote) : null,
668
757
  },
669
- lastUpdated: state.lastUpdated,
670
758
  }
671
759
 
672
760
  return ['<workstream-state>', JSON.stringify(payload, null, 2), '</workstream-state>'].join('\n')
@@ -1,3 +1,5 @@
1
+ import { formatUtcPromptDate } from '../utils/date-time'
2
+
1
3
  export function getFactRetrievalMessages(
2
4
  parsedMessages: string,
3
5
  customPrompt?: string,
@@ -38,7 +40,7 @@ Hard rules:
38
40
  - Prefer returning fewer items. If uncertain, return an empty list.
39
41
  - Max ${maxFacts} facts.
40
42
 
41
- Today's date is ${new Date().toISOString().split('T')[0]}.
43
+ Today's date is ${formatUtcPromptDate(new Date())}.
42
44
  ${baseInstructions}`
43
45
 
44
46
  const userPrompt = `Conversation:\n${parsedMessages}`
@@ -292,7 +292,7 @@ export const LOTA_RUNTIME_ENV_KEYS = Object.freeze([
292
292
  'SURREALDB_PASSWORD',
293
293
  'REDIS_URL',
294
294
  'AI_GATEWAY_URL',
295
- 'LOTA_KEY',
295
+ 'AI_GATEWAY_KEY',
296
296
  'AI_EMBEDDING_MODEL',
297
297
  'S3_ENDPOINT',
298
298
  'S3_BUCKET',
@@ -10,7 +10,7 @@ import {
10
10
  } from '../system-agents/title-generator.agent'
11
11
  import { workstreamService } from './workstream.service'
12
12
 
13
- const WORKSTREAM_TITLE_TIMEOUT_MS = 5_000
13
+ const WORKSTREAM_TITLE_TIMEOUT_MS = 30_000
14
14
 
15
15
  class WorkstreamTitleService {
16
16
  helperRuntime = createHelperModelRuntime()
@@ -632,7 +632,10 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
632
632
  const workstreamRecord = await waitForWorkstreamCompactionIfNeeded(workstreamRef)
633
633
  timer.step('compaction-gate')
634
634
  if (toOptionalTrimmedString(workstreamRecord.activeRunId)) {
635
- throw new WorkstreamTurnError('A chat run is already active.', 409)
635
+ const clearedStaleRun = await workstreamService.clearStaleActiveRunIfMissingFromRegistry(workstreamRef)
636
+ if (!clearedStaleRun) {
637
+ throw new WorkstreamTurnError('A chat run is already active.', 409)
638
+ }
636
639
  }
637
640
 
638
641
  if (params.kind === 'approvalContinuation' || params.kind === 'nativeToolApprovalTurn') {
@@ -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