@lota-sdk/core 0.1.47 → 0.1.48

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.47",
3
+ "version": "0.1.48",
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.47",
35
+ "@lota-sdk/shared": "0.1.48",
36
36
  "@mendable/firecrawl-js": "^4.18.0",
37
37
  "@surrealdb/node": "^3.0.3",
38
38
  "ai": "^6.0.141",
@@ -50,22 +50,38 @@ export function createAgentMessageMetadata(params: {
50
50
  }
51
51
  }
52
52
 
53
- export function createServerRunAbortController(externalAbortSignal?: AbortSignal) {
53
+ function toExternalAbortSignals(externalAbortSignal?: AbortSignal | readonly AbortSignal[]): AbortSignal[] {
54
+ if (!externalAbortSignal) return []
55
+
56
+ const signals = Array.isArray(externalAbortSignal) ? externalAbortSignal : [externalAbortSignal]
57
+ return [...new Set<AbortSignal>(signals)]
58
+ }
59
+
60
+ export function createServerRunAbortController(externalAbortSignal?: AbortSignal | readonly AbortSignal[]) {
54
61
  const controller = new AbortController()
55
62
  const abort = (reason?: unknown) => {
56
63
  if (controller.signal.aborted) return
57
64
  controller.abort(reason ?? new DOMException('Run stopped by user.', 'AbortError'))
58
65
  }
59
66
 
60
- const abortFromExternal = () => {
61
- abort((externalAbortSignal as (AbortSignal & { reason?: unknown }) | undefined)?.reason)
62
- }
67
+ const externalSignals = toExternalAbortSignals(externalAbortSignal)
68
+ const listeners = externalSignals.map((signal) => {
69
+ const abortFromExternal = () => {
70
+ abort((signal as AbortSignal & { reason?: unknown }).reason)
71
+ }
63
72
 
64
- if (externalAbortSignal) {
65
- if (externalAbortSignal.aborted) {
73
+ if (signal.aborted) {
66
74
  abortFromExternal()
67
75
  } else {
68
- externalAbortSignal.addEventListener('abort', abortFromExternal, { once: true })
76
+ signal.addEventListener('abort', abortFromExternal, { once: true })
77
+ }
78
+
79
+ return { signal, abortFromExternal }
80
+ })
81
+
82
+ if (controller.signal.aborted) {
83
+ for (const { signal, abortFromExternal } of listeners) {
84
+ signal.removeEventListener('abort', abortFromExternal)
69
85
  }
70
86
  }
71
87
 
@@ -74,8 +90,9 @@ export function createServerRunAbortController(externalAbortSignal?: AbortSignal
74
90
  signal: controller.signal,
75
91
  abort,
76
92
  dispose: () => {
77
- if (!externalAbortSignal) return
78
- externalAbortSignal.removeEventListener('abort', abortFromExternal)
93
+ for (const { signal, abortFromExternal } of listeners) {
94
+ signal.removeEventListener('abort', abortFromExternal)
95
+ }
79
96
  },
80
97
  }
81
98
  }
@@ -734,10 +734,13 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
734
734
  return {
735
735
  originalMessages,
736
736
  run: async (writer?: UIMessageStreamWriter<ChatMessage>) => {
737
- const executeRun = async (): Promise<PreparedWorkstreamTurnResult | void> => {
737
+ const executeRun = async (leaseAbortSignal?: AbortSignal): Promise<PreparedWorkstreamTurnResult | void> => {
738
738
  const runTimer = lotaDebugLogger.timer('run')
739
739
  const serverRunId = Bun.randomUUIDv7()
740
- const runAbort = createServerRunAbortController(params.abortSignal)
740
+ const runAbortSignals = leaseAbortSignal ? [params.abortSignal, leaseAbortSignal] : [params.abortSignal]
741
+ const runAbort = createServerRunAbortController(
742
+ runAbortSignals.filter((signal): signal is AbortSignal => Boolean(signal)),
743
+ )
741
744
  // Plan turns run without the chat lease — don't claim the active run slot.
742
745
  if (params.kind !== 'planTurn') {
743
746
  await workstreamService.setActiveTurn(workstreamRef, serverRunId, params.streamId ?? null)
@@ -1093,8 +1096,8 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
1093
1096
  }
1094
1097
 
1095
1098
  try {
1096
- return await workstreamService.withActiveRunLease(workstreamRef, async () => {
1097
- const runResult = await executeRun()
1099
+ return await workstreamService.withActiveRunLease(workstreamRef, async (leaseAbortSignal) => {
1100
+ const runResult = await executeRun(leaseAbortSignal)
1098
1101
  if (runResult) {
1099
1102
  return runResult
1100
1103
  }
@@ -3,7 +3,13 @@ import { z } from 'zod'
3
3
 
4
4
  import { aiGatewayChatModel } from '../ai-gateway/ai-gateway'
5
5
  import { buildAiGatewayDirectCacheHeaders } from '../ai-gateway/cache-headers'
6
- import { agentDescriptions, agentDisplayNames, routerModelId } from '../config/agent-defaults'
6
+ import {
7
+ agentDescriptions,
8
+ agentDisplayNames,
9
+ agentShortDisplayNames,
10
+ resolveAgentNameAlias,
11
+ routerModelId,
12
+ } from '../config/agent-defaults'
7
13
 
8
14
  // ---------------------------------------------------------------------------
9
15
  // Schemas
@@ -34,6 +40,60 @@ function buildMembersDescription(members: readonly string[]): string {
34
40
  .join('\n')
35
41
  }
36
42
 
43
+ function escapeRegex(value: string): string {
44
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
45
+ }
46
+
47
+ function buildExplicitAgentRoutingContext(agentId: string): string {
48
+ const displayName = agentDisplayNames[agentId] ?? agentId
49
+ return `Respond directly to the part of the user's request explicitly addressed to ${displayName}.`
50
+ }
51
+
52
+ function extractExplicitAgentTargets(messageText: string, members: readonly string[]): string[] {
53
+ const normalizedMessage = messageText.trim()
54
+ if (!normalizedMessage) return []
55
+
56
+ const memberSet = new Set(members)
57
+ const aliases = new Map<string, string>()
58
+
59
+ for (const member of members) {
60
+ for (const rawAlias of [member, agentDisplayNames[member], agentShortDisplayNames[member]]) {
61
+ if (typeof rawAlias !== 'string') continue
62
+ const alias = rawAlias.trim().toLowerCase()
63
+ if (!alias) continue
64
+ aliases.set(alias, member)
65
+ }
66
+ }
67
+
68
+ if (aliases.size === 0) return []
69
+
70
+ const orderedAliases = [...aliases.keys()].sort((left, right) => right.length - left.length)
71
+ const matches: Array<{ agentId: string; index: number }> = []
72
+ const directAddressRegex = new RegExp(
73
+ `(^|[^\\w@])(?<alias>${orderedAliases.map(escapeRegex).join('|')})(?=\\s*(?::|\\-|—))`,
74
+ 'gi',
75
+ )
76
+
77
+ for (const match of normalizedMessage.matchAll(directAddressRegex)) {
78
+ const alias = match.groups?.alias
79
+ const agentId = alias ? resolveAgentNameAlias(alias) : undefined
80
+ if (!agentId || !memberSet.has(agentId)) continue
81
+
82
+ const prefix = match[1]
83
+ const index = match.index + prefix.length
84
+ matches.push({ agentId, index })
85
+ }
86
+
87
+ const seenAgents = new Set<string>()
88
+ return matches
89
+ .sort((left, right) => left.index - right.index)
90
+ .flatMap(({ agentId }) => {
91
+ if (seenAgents.has(agentId)) return []
92
+ seenAgents.add(agentId)
93
+ return [agentId]
94
+ })
95
+ }
96
+
37
97
  function extractJson(text: string): unknown {
38
98
  const match = text.match(/\{[\s\S]*\}/)
39
99
  if (!match) return null
@@ -53,7 +113,13 @@ function extractResultText(result: { text?: string; reasoning?: unknown }): stri
53
113
  if (typeof reasoning === 'string') return reasoning
54
114
  if (Array.isArray(reasoning)) {
55
115
  return reasoning
56
- .map((r) => (typeof r === 'string' ? r : typeof r === 'object' && r && 'text' in r ? String(r.text) : ''))
116
+ .map((r) => {
117
+ if (typeof r === 'string') return r
118
+ if (typeof r !== 'object' || r === null || !('text' in r)) return ''
119
+
120
+ const text = (r as { text?: unknown }).text
121
+ return typeof text === 'string' ? text : ''
122
+ })
57
123
  .join('')
58
124
  }
59
125
  return ''
@@ -112,6 +178,12 @@ export async function triageWorkstreamMessage(params: {
112
178
  messageText: string
113
179
  recentContext?: string
114
180
  }): Promise<RouterTriageResult | null> {
181
+ const explicitTargets = extractExplicitAgentTargets(params.messageText, params.members)
182
+ const firstExplicitTarget = explicitTargets[0]
183
+ if (firstExplicitTarget) {
184
+ return { agentId: firstExplicitTarget, routingContext: buildExplicitAgentRoutingContext(firstExplicitTarget) }
185
+ }
186
+
115
187
  const membersDesc = buildMembersDescription(params.members)
116
188
  const prompt = [
117
189
  `Workstream: "${params.workstreamTitle}"`,
@@ -159,6 +231,16 @@ export async function checkForNextAgent(params: {
159
231
  respondedAgents: string[]
160
232
  lastResponseSummary: string
161
233
  }): Promise<RouterCheckResult> {
234
+ const explicitTargets = extractExplicitAgentTargets(params.messageText, params.members)
235
+ const nextExplicitTarget = explicitTargets.find((agentId) => !params.respondedAgents.includes(agentId))
236
+ if (nextExplicitTarget) {
237
+ return {
238
+ done: false,
239
+ agentId: nextExplicitTarget,
240
+ routingContext: buildExplicitAgentRoutingContext(nextExplicitTarget),
241
+ }
242
+ }
243
+
162
244
  const remainingMembers = params.members.filter((id) => !params.respondedAgents.includes(id))
163
245
  if (remainingMembers.length === 0) return { done: true }
164
246