@lota-sdk/core 0.1.47 → 0.2.0
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.
|
|
3
|
+
"version": "0.2.0",
|
|
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.
|
|
35
|
+
"@lota-sdk/shared": "0.2.0",
|
|
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
|
-
|
|
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
|
|
61
|
-
|
|
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
|
-
|
|
65
|
-
if (externalAbortSignal.aborted) {
|
|
73
|
+
if (signal.aborted) {
|
|
66
74
|
abortFromExternal()
|
|
67
75
|
} else {
|
|
68
|
-
|
|
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
|
-
|
|
78
|
-
|
|
93
|
+
for (const { signal, abortFromExternal } of listeners) {
|
|
94
|
+
signal.removeEventListener('abort', abortFromExternal)
|
|
95
|
+
}
|
|
79
96
|
},
|
|
80
97
|
}
|
|
81
98
|
}
|
|
@@ -4,10 +4,11 @@ type ExecutionPlanPromptSummary = Pick<SerializableExecutionPlan, 'runId' | 'tit
|
|
|
4
4
|
|
|
5
5
|
const EXECUTION_PLAN_AGENT_PROTOCOL_PROMPT = `<execution-plan-protocol>
|
|
6
6
|
- Create execution plans for multi-step work. Review existing plans before creating new ones.
|
|
7
|
+
- The active execution runs in <execution-plan-state> are a summary inventory only. They list runId and title, not node-level state.
|
|
7
8
|
- The runtime executor owns lifecycle truth. Do not claim node completion until the executor confirms.
|
|
8
|
-
-
|
|
9
|
+
- Do not invent or restate run/node lifecycle details that are not present in the prompt or tool results.
|
|
9
10
|
- During plan-triggered turns, use the dedicated result-submission tool. Include handoffContext.
|
|
10
|
-
- Treat the active execution runs in <execution-plan-state> as authoritative
|
|
11
|
+
- Treat the active execution runs in <execution-plan-state> as authoritative for whether a plan already exists.
|
|
11
12
|
- If contracts or criteria materially change, replace the plan.
|
|
12
13
|
</execution-plan-protocol>`
|
|
13
14
|
|
|
@@ -734,10 +734,16 @@ 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
|
|
740
|
+
const runAbortSignals = leaseAbortSignal ? [params.abortSignal, leaseAbortSignal] : [params.abortSignal]
|
|
741
|
+
const runAbort = createServerRunAbortController(
|
|
742
|
+
runAbortSignals.filter((signal): signal is AbortSignal => Boolean(signal)),
|
|
743
|
+
)
|
|
744
|
+
if (runAbort.signal.aborted) {
|
|
745
|
+
throw runAbort.signal.reason ?? new DOMException('The operation was aborted.', 'AbortError')
|
|
746
|
+
}
|
|
741
747
|
// Plan turns run without the chat lease — don't claim the active run slot.
|
|
742
748
|
if (params.kind !== 'planTurn') {
|
|
743
749
|
await workstreamService.setActiveTurn(workstreamRef, serverRunId, params.streamId ?? null)
|
|
@@ -746,6 +752,11 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
746
752
|
runTimer.step('set-active-run+stream')
|
|
747
753
|
|
|
748
754
|
try {
|
|
755
|
+
const throwIfRunAborted = () => {
|
|
756
|
+
if (!runAbort.signal.aborted) return
|
|
757
|
+
throw runAbort.signal.reason ?? new DOMException('The operation was aborted.', 'AbortError')
|
|
758
|
+
}
|
|
759
|
+
|
|
749
760
|
const buildAgentMetadataPatch = (agentId: string, agentName: string): NonNullable<MessageMetadata> => ({
|
|
750
761
|
agentId,
|
|
751
762
|
agentName,
|
|
@@ -768,6 +779,8 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
768
779
|
agentName: string,
|
|
769
780
|
metadataPatch?: NonNullable<MessageMetadata>,
|
|
770
781
|
) => {
|
|
782
|
+
throwIfRunAborted()
|
|
783
|
+
|
|
771
784
|
const committed = withMessageCreatedAt(
|
|
772
785
|
{
|
|
773
786
|
...response,
|
|
@@ -779,6 +792,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
779
792
|
await workstreamMessageService.upsertMessages({ workstreamId: workstreamRef, messages: [committed] })
|
|
780
793
|
currentMessages = upsertChatHistoryMessage(currentMessages, committed)
|
|
781
794
|
allAssistantMessages = upsertChatHistoryMessage(allAssistantMessages, committed)
|
|
795
|
+
throwIfRunAborted()
|
|
782
796
|
|
|
783
797
|
return committed
|
|
784
798
|
}
|
|
@@ -840,6 +854,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
840
854
|
const tools = runParams.filterTools ? runParams.filterTools(rawTools) : rawTools
|
|
841
855
|
visibleTimer.step('build-agent-tools')
|
|
842
856
|
streamCtx.memoryBlock = memoryBlock
|
|
857
|
+
throwIfRunAborted()
|
|
843
858
|
const responseMessage = await streamAgentResponse(streamCtx, {
|
|
844
859
|
agentId: runParams.agentId,
|
|
845
860
|
mode: runParams.mode,
|
|
@@ -899,6 +914,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
899
914
|
const wsMembers = (workstream as { members?: string[] }).members ?? []
|
|
900
915
|
const members = wsMembers.length > 0 ? wsMembers : [...agentRoster]
|
|
901
916
|
const fallbackAgentId = coreWorkstreamProfile?.config.agentId ?? defaultLeadAgentId
|
|
917
|
+
throwIfRunAborted()
|
|
902
918
|
writeMultiAgentEvent(writer, { phase: 'routing', note: 'Routing this turn to the right agent.' })
|
|
903
919
|
|
|
904
920
|
const recentContext = currentMessages
|
|
@@ -912,6 +928,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
912
928
|
messageText,
|
|
913
929
|
recentContext,
|
|
914
930
|
})
|
|
931
|
+
throwIfRunAborted()
|
|
915
932
|
|
|
916
933
|
const runGroupAgent = async (
|
|
917
934
|
agentId: string,
|
|
@@ -938,6 +955,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
938
955
|
if (!triageResult) {
|
|
939
956
|
// No specialist match — fallback to owner (core) or chief (non-core), single visible turn.
|
|
940
957
|
await runGroupAgent(fallbackAgentId)
|
|
958
|
+
throwIfRunAborted()
|
|
941
959
|
writeMultiAgentEvent(writer, { phase: 'complete' })
|
|
942
960
|
} else {
|
|
943
961
|
const respondedAgents: string[] = []
|
|
@@ -945,6 +963,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
945
963
|
routingContext: triageResult.routingContext,
|
|
946
964
|
})
|
|
947
965
|
respondedAgents.push(triageResult.agentId)
|
|
966
|
+
throwIfRunAborted()
|
|
948
967
|
|
|
949
968
|
// Follow-up specialists run headless, persist their own messages,
|
|
950
969
|
// and are surfaced by transient events plus cache refresh.
|
|
@@ -957,6 +976,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
957
976
|
respondedAgents,
|
|
958
977
|
lastResponseSummary: lastResponseText,
|
|
959
978
|
})
|
|
979
|
+
throwIfRunAborted()
|
|
960
980
|
|
|
961
981
|
if (checkResult.done || !checkResult.agentId) break
|
|
962
982
|
|
|
@@ -979,17 +999,20 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
979
999
|
],
|
|
980
1000
|
metadata: { hidden: true, createdAt: Date.now() } as MessageMetadata,
|
|
981
1001
|
}
|
|
1002
|
+
throwIfRunAborted()
|
|
982
1003
|
await workstreamMessageService.upsertMessages({
|
|
983
1004
|
workstreamId: workstreamRef,
|
|
984
1005
|
messages: [bridgeMessage],
|
|
985
1006
|
})
|
|
986
1007
|
currentMessages = upsertChatHistoryMessage(currentMessages, bridgeMessage)
|
|
1008
|
+
throwIfRunAborted()
|
|
987
1009
|
|
|
988
1010
|
lastResponse = await runGroupAgent(checkResult.agentId, {
|
|
989
1011
|
routingContext: checkResult.routingContext,
|
|
990
1012
|
headless: true,
|
|
991
1013
|
})
|
|
992
1014
|
respondedAgents.push(checkResult.agentId)
|
|
1015
|
+
throwIfRunAborted()
|
|
993
1016
|
writeMultiAgentEvent(writer, {
|
|
994
1017
|
phase: 'agent-message-persisted',
|
|
995
1018
|
agentId: checkResult.agentId,
|
|
@@ -998,6 +1021,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
998
1021
|
})
|
|
999
1022
|
}
|
|
1000
1023
|
|
|
1024
|
+
throwIfRunAborted()
|
|
1001
1025
|
writeMultiAgentEvent(writer, { phase: 'complete' })
|
|
1002
1026
|
}
|
|
1003
1027
|
}
|
|
@@ -1093,8 +1117,8 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
1093
1117
|
}
|
|
1094
1118
|
|
|
1095
1119
|
try {
|
|
1096
|
-
return await workstreamService.withActiveRunLease(workstreamRef, async () => {
|
|
1097
|
-
const runResult = await executeRun()
|
|
1120
|
+
return await workstreamService.withActiveRunLease(workstreamRef, async (leaseAbortSignal) => {
|
|
1121
|
+
const runResult = await executeRun(leaseAbortSignal)
|
|
1098
1122
|
if (runResult) {
|
|
1099
1123
|
return runResult
|
|
1100
1124
|
}
|
|
@@ -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 {
|
|
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) =>
|
|
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
|
|