@lota-sdk/core 0.1.34 → 0.1.36
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/infrastructure/schema/00_workstream.surql +1 -0
- package/package.json +2 -2
- package/src/config/agent-defaults.ts +5 -0
- package/src/config/model-constants.ts +1 -0
- package/src/create-runtime.ts +1 -0
- package/src/runtime/runtime-config.ts +3 -0
- package/src/services/execution-plan.service.ts +4 -9
- package/src/services/plan-executor.service.ts +0 -10
- package/src/services/workstream-turn-preparation.service.ts +81 -130
- package/src/services/workstream.service.ts +12 -2
- package/src/system-agents/index.ts +1 -0
- package/src/system-agents/workstream-manager.agent.ts +153 -0
|
@@ -19,6 +19,7 @@ DEFINE FIELD IF NOT EXISTS lastCompactedMessageId ON TABLE workstream TYPE optio
|
|
|
19
19
|
DEFINE FIELD IF NOT EXISTS nameGenerated ON TABLE workstream TYPE bool DEFAULT false;
|
|
20
20
|
DEFINE FIELD IF NOT EXISTS isCompacting ON TABLE workstream TYPE bool DEFAULT false;
|
|
21
21
|
DEFINE FIELD IF NOT EXISTS state ON TABLE workstream TYPE option<object> FLEXIBLE;
|
|
22
|
+
DEFINE FIELD IF NOT EXISTS members ON TABLE workstream TYPE option<array<string>> DEFAULT [];
|
|
22
23
|
DEFINE FIELD IF NOT EXISTS turnCount ON TABLE workstream TYPE int DEFAULT 0;
|
|
23
24
|
|
|
24
25
|
DEFINE INDEX IF NOT EXISTS workstreamOrgIdx ON TABLE workstream COLUMNS organizationId;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lota-sdk/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.36",
|
|
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.
|
|
35
|
+
"@lota-sdk/shared": "0.1.35",
|
|
36
36
|
"@mendable/firecrawl-js": "^4.18.0",
|
|
37
37
|
"@surrealdb/node": "^3.0.3",
|
|
38
38
|
"ai": "^6.0.141",
|
|
@@ -11,12 +11,14 @@ function defaultGetAgentRuntimeConfig(): Record<string, never> {
|
|
|
11
11
|
// Agent configuration — these are defaults that consumers override via createLotaRuntime config
|
|
12
12
|
export let agentDisplayNames: Record<string, string> = {}
|
|
13
13
|
export let agentShortDisplayNames: Record<string, string> = {}
|
|
14
|
+
export let agentDescriptions: Record<string, string> = {}
|
|
14
15
|
export let agentRoster: readonly string[] = []
|
|
15
16
|
export let leadAgentId = ''
|
|
16
17
|
export let teamConsultParticipants: readonly string[] = []
|
|
17
18
|
|
|
18
19
|
export interface CoreWorkstreamProfile {
|
|
19
20
|
config: { coreType: string; agentId: string; title: string }
|
|
21
|
+
members: readonly string[]
|
|
20
22
|
tools: readonly string[]
|
|
21
23
|
skills: readonly string[]
|
|
22
24
|
instructions: string
|
|
@@ -24,6 +26,7 @@ export interface CoreWorkstreamProfile {
|
|
|
24
26
|
|
|
25
27
|
export let getCoreWorkstreamProfile: (coreType: string) => CoreWorkstreamProfile = (_coreType) => ({
|
|
26
28
|
config: { coreType: _coreType, agentId: '', title: '' },
|
|
29
|
+
members: [],
|
|
27
30
|
tools: [],
|
|
28
31
|
skills: [],
|
|
29
32
|
instructions: '',
|
|
@@ -34,6 +37,7 @@ export function configureAgents(config: {
|
|
|
34
37
|
leadAgentId: string
|
|
35
38
|
displayNames: Record<string, string>
|
|
36
39
|
shortDisplayNames?: Record<string, string>
|
|
40
|
+
descriptions?: Record<string, string>
|
|
37
41
|
teamConsultParticipants: readonly string[]
|
|
38
42
|
getCoreWorkstreamProfile?: (coreType: string) => CoreWorkstreamProfile
|
|
39
43
|
}): void {
|
|
@@ -45,6 +49,7 @@ export function configureAgents(config: {
|
|
|
45
49
|
leadAgentId = config.leadAgentId
|
|
46
50
|
agentDisplayNames = config.displayNames
|
|
47
51
|
agentShortDisplayNames = config.shortDisplayNames ?? {}
|
|
52
|
+
agentDescriptions = config.descriptions ?? {}
|
|
48
53
|
teamConsultParticipants = config.teamConsultParticipants
|
|
49
54
|
if (config.getCoreWorkstreamProfile) {
|
|
50
55
|
getCoreWorkstreamProfile = config.getCoreWorkstreamProfile
|
|
@@ -12,6 +12,7 @@ export {
|
|
|
12
12
|
OPENROUTER_TEAM_AGENT_MODEL_ID,
|
|
13
13
|
OPENROUTER_WEB_RESEARCH_MODEL_ID,
|
|
14
14
|
OPENROUTER_XHIGH_REASONING_PROVIDER_OPTIONS,
|
|
15
|
+
OPENROUTER_MANAGER_MODEL_ID,
|
|
15
16
|
} from '@lota-sdk/shared'
|
|
16
17
|
|
|
17
18
|
// Both aliases point to the same underlying model. Keep the names separate so
|
package/src/create-runtime.ts
CHANGED
|
@@ -250,6 +250,7 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
|
|
|
250
250
|
leadAgentId: runtimeConfig.agents.leadAgentId,
|
|
251
251
|
displayNames: agentDisplayNames,
|
|
252
252
|
shortDisplayNames: runtimeConfig.agents.shortDisplayNames,
|
|
253
|
+
descriptions: runtimeConfig.agents.descriptions,
|
|
253
254
|
teamConsultParticipants: runtimeConfig.agents.teamConsultParticipants,
|
|
254
255
|
getCoreWorkstreamProfile: runtimeConfig.agents.getCoreWorkstreamProfile,
|
|
255
256
|
})
|
|
@@ -173,6 +173,9 @@ const agentsConfigSchema = z
|
|
|
173
173
|
shortDisplayNames: z
|
|
174
174
|
.custom<Record<string, string>>(isStringRecord, { error: 'agents.shortDisplayNames must be a string record' })
|
|
175
175
|
.optional(),
|
|
176
|
+
descriptions: z
|
|
177
|
+
.custom<Record<string, string>>(isStringRecord, { error: 'agents.descriptions must be a string record' })
|
|
178
|
+
.optional(),
|
|
176
179
|
teamConsultParticipants: z.array(z.string().trim().min(1)),
|
|
177
180
|
getCoreWorkstreamProfile: z
|
|
178
181
|
.custom<(coreType: string) => CoreWorkstreamProfile>(isFunction, {
|
|
@@ -336,14 +336,9 @@ class ExecutionPlanService {
|
|
|
336
336
|
input: PlanDraft & { runId: string; reason: string }
|
|
337
337
|
}): Promise<ExecutionPlanToolResultData> {
|
|
338
338
|
const activeRun = await planRunService.getRunById(params.input.runId)
|
|
339
|
-
|
|
340
|
-
recordIdToString(activeRun.workstreamId, TABLES.WORKSTREAM) !==
|
|
341
|
-
recordIdToString(params.workstreamId, TABLES.WORKSTREAM)
|
|
342
|
-
) {
|
|
343
|
-
throw new Error('Execution run belongs to a different workstream.')
|
|
344
|
-
}
|
|
339
|
+
const resolvedWorkstreamId = activeRun.workstreamId
|
|
345
340
|
|
|
346
|
-
const activeRuns = await planRunService.getActiveRunRecords(
|
|
341
|
+
const activeRuns = await planRunService.getActiveRunRecords(resolvedWorkstreamId)
|
|
347
342
|
if (activeRuns.length === 0) {
|
|
348
343
|
throw new Error('No active execution run exists for this workstream.')
|
|
349
344
|
}
|
|
@@ -404,7 +399,7 @@ class ExecutionPlanService {
|
|
|
404
399
|
.content(
|
|
405
400
|
buildCompiledSpecCreateData({
|
|
406
401
|
organizationId: params.organizationId,
|
|
407
|
-
workstreamId:
|
|
402
|
+
workstreamId: resolvedWorkstreamId,
|
|
408
403
|
leadAgentId: params.leadAgentId,
|
|
409
404
|
compiled,
|
|
410
405
|
version: supersededSpec.version + 1,
|
|
@@ -418,7 +413,7 @@ class ExecutionPlanService {
|
|
|
418
413
|
runId,
|
|
419
414
|
spec,
|
|
420
415
|
organizationId: params.organizationId,
|
|
421
|
-
workstreamId:
|
|
416
|
+
workstreamId: resolvedWorkstreamId,
|
|
422
417
|
leadAgentId: params.leadAgentId,
|
|
423
418
|
nodes: compiled.nodes,
|
|
424
419
|
emittedEvents,
|
|
@@ -986,11 +986,6 @@ class PlanExecutorService {
|
|
|
986
986
|
emittedBy: string
|
|
987
987
|
}): Promise<ExecutionPlanToolResultData> {
|
|
988
988
|
const run = await planRunService.getRunById(params.runId)
|
|
989
|
-
if (
|
|
990
|
-
recordIdToString(run.workstreamId, TABLES.WORKSTREAM) !== recordIdToString(params.workstreamId, TABLES.WORKSTREAM)
|
|
991
|
-
) {
|
|
992
|
-
throw new Error('Execution run belongs to a different workstream.')
|
|
993
|
-
}
|
|
994
989
|
|
|
995
990
|
const spec = await planRunService.getPlanSpecById(run.planSpecId)
|
|
996
991
|
const nodeSpecs = await planRunService.listNodeSpecs(spec.id)
|
|
@@ -1119,11 +1114,6 @@ class PlanExecutorService {
|
|
|
1119
1114
|
failureClass: PlanFailureClass
|
|
1120
1115
|
}): Promise<SerializableExecutionPlan> {
|
|
1121
1116
|
const run = await planRunService.getRunById(params.runId)
|
|
1122
|
-
if (
|
|
1123
|
-
recordIdToString(run.workstreamId, TABLES.WORKSTREAM) !== recordIdToString(params.workstreamId, TABLES.WORKSTREAM)
|
|
1124
|
-
) {
|
|
1125
|
-
throw new Error('Execution run belongs to a different workstream.')
|
|
1126
|
-
}
|
|
1127
1117
|
|
|
1128
1118
|
const spec = await planRunService.getPlanSpecById(run.planSpecId)
|
|
1129
1119
|
const nodeSpec = await planRunService.getNodeSpecByNodeId(spec.id, params.nodeId)
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
WORKSTREAM,
|
|
3
|
-
baseChatMessageSchema,
|
|
4
|
-
CONSULT_SPECIALIST_TOOL_NAME,
|
|
5
|
-
CONSULT_TEAM_TOOL_NAME,
|
|
6
|
-
ConsultSpecialistArgsSchema,
|
|
7
3
|
dataPartsSchemas,
|
|
8
4
|
messageMetadataSchema,
|
|
9
5
|
PlanNodeResultSubmissionSchema,
|
|
@@ -11,7 +7,7 @@ import {
|
|
|
11
7
|
toTimestamp,
|
|
12
8
|
withMessageCreatedAt,
|
|
13
9
|
} from '@lota-sdk/shared'
|
|
14
|
-
import type { ChatMessage,
|
|
10
|
+
import type { ChatMessage, MessageMetadata, PlanNodeSpecRecord } from '@lota-sdk/shared'
|
|
15
11
|
import { convertToModelMessages, stepCountIs, tool as createTool, validateUIMessages } from 'ai'
|
|
16
12
|
import type { PrepareStepFunction, StopCondition, ToolLoopAgent, ToolSet, UIMessageStreamWriter } from 'ai'
|
|
17
13
|
|
|
@@ -32,11 +28,7 @@ import { TABLES } from '../db/tables'
|
|
|
32
28
|
import { enqueueContextCompaction } from '../queues/context-compaction.queue'
|
|
33
29
|
import { enqueueWorkstreamTitleGeneration } from '../queues/title-generation.queue'
|
|
34
30
|
import { OWNERSHIP_DISPATCH_BLOCKED_TOOL_NAMES } from '../runtime/agent-runtime-policy'
|
|
35
|
-
import {
|
|
36
|
-
buildSpecialistTaskMessage,
|
|
37
|
-
createAgentMessageMetadata,
|
|
38
|
-
createServerRunAbortController,
|
|
39
|
-
} from '../runtime/agent-stream-helpers'
|
|
31
|
+
import { createAgentMessageMetadata, createServerRunAbortController } from '../runtime/agent-stream-helpers'
|
|
40
32
|
import { hasApprovalRespondedParts } from '../runtime/approval-continuation'
|
|
41
33
|
import { buildModelInputMessagesWithUploadMetadata, buildReadableUploadMetadataText } from '../runtime/chat-attachments'
|
|
42
34
|
import { hasMessageContent } from '../runtime/chat-message'
|
|
@@ -47,7 +39,6 @@ import { createExecutionPlanInstructionSectionCache } from '../runtime/execution
|
|
|
47
39
|
import { mergeInstructionSections } from '../runtime/instruction-sections'
|
|
48
40
|
import { runPostTurnSideEffects } from '../runtime/post-turn-side-effects'
|
|
49
41
|
import { getRuntimeAdapters, getToolProviders, getTurnHooks } from '../runtime/runtime-extensions'
|
|
50
|
-
import { runSpecialistSession } from '../runtime/specialist-runner'
|
|
51
42
|
import { finalizeTurnRun } from '../runtime/turn-lifecycle'
|
|
52
43
|
import {
|
|
53
44
|
asRecord,
|
|
@@ -67,7 +58,7 @@ import type { WorkstreamState } from '../runtime/workstream-state'
|
|
|
67
58
|
import { assembleWorkstreamTurnContext } from '../runtime/workstream-turn-context'
|
|
68
59
|
import { chatRunRegistry } from '../services/chat-run-registry.service'
|
|
69
60
|
import type { NormalizedWorkstream, WorkstreamRecord } from '../services/workstream.types'
|
|
70
|
-
import {
|
|
61
|
+
import { triageWorkstreamMessage, checkForNextAgent } from '../system-agents/workstream-manager.agent'
|
|
71
62
|
import { safeEnqueue } from '../utils/async'
|
|
72
63
|
import { AppError } from '../utils/errors'
|
|
73
64
|
import { attachmentService } from './attachment.service'
|
|
@@ -114,30 +105,6 @@ async function waitForWorkstreamCompactionIfNeeded(workstreamId: RecordIdRef): P
|
|
|
114
105
|
})
|
|
115
106
|
}
|
|
116
107
|
|
|
117
|
-
function parseChatMessageCandidate(value: unknown): ChatMessage | undefined {
|
|
118
|
-
const parsed = baseChatMessageSchema.safeParse(value)
|
|
119
|
-
if (!parsed.success) return undefined
|
|
120
|
-
return parsed.data as ChatMessage
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function getChatMessageFromToolOutput(output: unknown): ChatMessage | undefined {
|
|
124
|
-
const directCandidate = parseChatMessageCandidate(output)
|
|
125
|
-
if (directCandidate) return directCandidate
|
|
126
|
-
|
|
127
|
-
if (Array.isArray(output)) {
|
|
128
|
-
for (let index = output.length - 1; index >= 0; index -= 1) {
|
|
129
|
-
const candidate = getChatMessageFromToolOutput(output[index])
|
|
130
|
-
if (candidate) return candidate
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (output && typeof output === 'object' && 'message' in output) {
|
|
135
|
-
return getChatMessageFromToolOutput((output as { message?: unknown }).message)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return undefined
|
|
139
|
-
}
|
|
140
|
-
|
|
141
108
|
class WorkstreamTurnError extends AppError {
|
|
142
109
|
constructor(
|
|
143
110
|
message: string,
|
|
@@ -924,107 +891,91 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
924
891
|
metadataPatch: { trigger: 'plan-turn', planRunId: planTurn.runId, planNodeId: planTurn.nodeId },
|
|
925
892
|
})
|
|
926
893
|
} else {
|
|
927
|
-
const consultSpecialistTool = createTool({
|
|
928
|
-
description: 'Consult one specialist teammate for domain-specific guidance before replying to the user.',
|
|
929
|
-
inputSchema: ConsultSpecialistArgsSchema,
|
|
930
|
-
execute: async function* (
|
|
931
|
-
{ agentId, task }: ConsultSpecialistArgs,
|
|
932
|
-
{ abortSignal: toolAbortSignal }: { abortSignal?: AbortSignal },
|
|
933
|
-
) {
|
|
934
|
-
const specialistTaskMessage = buildSpecialistTaskMessage({ agentId, task })
|
|
935
|
-
const specialistAbortSignal = toolAbortSignal ?? runAbort.signal
|
|
936
|
-
const { result: finalMessage, memoryBlock: nextMemoryBlock } = await runSpecialistSession({
|
|
937
|
-
initialMemoryBlock: memoryBlock,
|
|
938
|
-
buildTools: async ({ memoryBlock: currentMemoryBlock, onAppendMemoryBlock }) =>
|
|
939
|
-
(await buildAgentTools(
|
|
940
|
-
buildTurnToolParams({
|
|
941
|
-
agentId,
|
|
942
|
-
mode: 'fixedWorkstreamMode',
|
|
943
|
-
memoryBlock: currentMemoryBlock,
|
|
944
|
-
onAppendMemoryBlock,
|
|
945
|
-
extraMessages: [specialistTaskMessage],
|
|
946
|
-
includeExecutionPlanTools: false,
|
|
947
|
-
}),
|
|
948
|
-
)) as ToolSet,
|
|
949
|
-
run: async ({ tools, memoryBlock: currentMemoryBlock }) =>
|
|
950
|
-
await streamAgentResponse(
|
|
951
|
-
{ ...streamCtx, memoryBlock: currentMemoryBlock },
|
|
952
|
-
{
|
|
953
|
-
agentId,
|
|
954
|
-
mode: 'fixedWorkstreamMode',
|
|
955
|
-
messages: buildRunInputMessages([specialistTaskMessage]),
|
|
956
|
-
tools: { ...tools, ...toolProviders },
|
|
957
|
-
observer: createObserver(agentId),
|
|
958
|
-
additionalInstructionSections: coreInstructionSections,
|
|
959
|
-
includeExecutionPlanTools: false,
|
|
960
|
-
abortSignal: specialistAbortSignal,
|
|
961
|
-
},
|
|
962
|
-
),
|
|
963
|
-
})
|
|
964
|
-
const committedFinalMessage = withMessageCreatedAt(finalMessage, Date.now())
|
|
965
|
-
memoryBlock = nextMemoryBlock
|
|
966
|
-
yield committedFinalMessage
|
|
967
|
-
return committedFinalMessage
|
|
968
|
-
},
|
|
969
|
-
toModelOutput: ({ output }) => {
|
|
970
|
-
const result = getChatMessageFromToolOutput(output)
|
|
971
|
-
const agentName =
|
|
972
|
-
typeof result?.metadata?.agentName === 'string' && result.metadata.agentName.trim().length > 0
|
|
973
|
-
? result.metadata.agentName.trim()
|
|
974
|
-
: 'Specialist'
|
|
975
|
-
const summary = result ? extractMessageText(result).trim() : ''
|
|
976
|
-
return {
|
|
977
|
-
type: 'text',
|
|
978
|
-
value: summary ? `${agentName}: ${summary}` : `${agentName} completed the requested task.`,
|
|
979
|
-
}
|
|
980
|
-
},
|
|
981
|
-
})
|
|
982
|
-
|
|
983
|
-
const teamThinkTool =
|
|
984
|
-
workstream.mode === 'group'
|
|
985
|
-
? createTeamThinkTool({
|
|
986
|
-
historyMessages: currentMessages,
|
|
987
|
-
latestUserMessageId: referenceUserMessageId,
|
|
988
|
-
orgId: orgRef,
|
|
989
|
-
userId: userRef,
|
|
990
|
-
workstreamId: workstreamRef,
|
|
991
|
-
githubInstalled,
|
|
992
|
-
availableUploads: listReadableUploads(),
|
|
993
|
-
provideRepoTool: indexedRepoContext.provideRepoTool,
|
|
994
|
-
defaultRepoSectionsByAgent: indexedRepoContext.defaultSectionsByAgent as never,
|
|
995
|
-
systemWorkspaceDetails: promptContext.systemWorkspaceDetails,
|
|
996
|
-
getPreSeededMemoriesSection,
|
|
997
|
-
retrievedKnowledgeSection,
|
|
998
|
-
additionalInstructionSections: mergeInstructionSections(
|
|
999
|
-
coreInstructionSections,
|
|
1000
|
-
hookInstructionSections,
|
|
1001
|
-
),
|
|
1002
|
-
getAdditionalInstructionSections: getExecutionPlanInstructionSections,
|
|
1003
|
-
context: buildContextResult,
|
|
1004
|
-
toolProviders,
|
|
1005
|
-
abortSignal: runAbort.signal,
|
|
1006
|
-
})
|
|
1007
|
-
: null
|
|
1008
|
-
|
|
1009
894
|
if (workstream.mode === 'direct') {
|
|
1010
895
|
if (!workstream.agentId) {
|
|
1011
896
|
throw new WorkstreamTurnError('Direct workstreams require an assigned agent.', 400)
|
|
1012
897
|
}
|
|
1013
898
|
await runVisibleAgent({ agentId: workstream.agentId, mode: 'direct' })
|
|
1014
899
|
} else {
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
),
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
900
|
+
// Multi-agent orchestration for group workstreams
|
|
901
|
+
const members =
|
|
902
|
+
workstream.members.length > 0 ? workstream.members : [visibleWorkstreamAgentId ?? defaultLeadAgentId]
|
|
903
|
+
const fallbackAgentId = coreWorkstreamProfile?.config.agentId ?? defaultLeadAgentId
|
|
904
|
+
|
|
905
|
+
const recentContext = currentMessages
|
|
906
|
+
.slice(-6)
|
|
907
|
+
.map((m) => `${m.role}: ${extractMessageText(m).slice(0, 200)}`)
|
|
908
|
+
.join('\n')
|
|
909
|
+
|
|
910
|
+
const triageResult = await triageWorkstreamMessage({
|
|
911
|
+
workstreamTitle: workstream.title,
|
|
912
|
+
members,
|
|
913
|
+
messageText,
|
|
914
|
+
recentContext,
|
|
1027
915
|
})
|
|
916
|
+
|
|
917
|
+
const runGroupAgent = async (agentId: string, routingContext?: string) => {
|
|
918
|
+
const additionalSections = [...(coreInstructionSections ?? []), ...hookInstructionSections]
|
|
919
|
+
if (routingContext) {
|
|
920
|
+
additionalSections.push(`<routing-context>\n${routingContext}\n</routing-context>`)
|
|
921
|
+
}
|
|
922
|
+
// Multi-agent member protocol: be direct, focus on domain
|
|
923
|
+
additionalSections.push(
|
|
924
|
+
'<multi-agent-protocol>\nYou are responding as part of a multi-agent workstream. Focus on your domain expertise. Be direct and concise — another agent may follow up on different aspects.\n</multi-agent-protocol>',
|
|
925
|
+
)
|
|
926
|
+
|
|
927
|
+
return await runVisibleAgent({
|
|
928
|
+
agentId,
|
|
929
|
+
mode: 'workstreamMode',
|
|
930
|
+
skills: coreWorkstreamProfile?.skills ? [...coreWorkstreamProfile.skills] : undefined,
|
|
931
|
+
additionalInstructionSections: additionalSections,
|
|
932
|
+
})
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
if (!triageResult) {
|
|
936
|
+
// No specialist match — fallback to owner (core) or chief (non-core)
|
|
937
|
+
await runGroupAgent(fallbackAgentId)
|
|
938
|
+
} else {
|
|
939
|
+
// Run first routed agent
|
|
940
|
+
const respondedAgents: string[] = []
|
|
941
|
+
let lastResponse = await runGroupAgent(triageResult.agentId, triageResult.routingContext)
|
|
942
|
+
respondedAgents.push(triageResult.agentId)
|
|
943
|
+
|
|
944
|
+
// Check if more agents should respond (max 3 total)
|
|
945
|
+
while (respondedAgents.length < 3) {
|
|
946
|
+
const lastResponseText = extractMessageText(lastResponse).slice(0, 500)
|
|
947
|
+
const checkResult = await checkForNextAgent({
|
|
948
|
+
workstreamTitle: workstream.title,
|
|
949
|
+
members,
|
|
950
|
+
messageText,
|
|
951
|
+
respondedAgents,
|
|
952
|
+
lastResponseSummary: lastResponseText,
|
|
953
|
+
})
|
|
954
|
+
|
|
955
|
+
if (checkResult.done || !checkResult.agentId) break
|
|
956
|
+
|
|
957
|
+
// Insert hidden bridge message between agent turns
|
|
958
|
+
const bridgeMessage: ChatMessage = {
|
|
959
|
+
id: Bun.randomUUIDv7(),
|
|
960
|
+
role: 'user',
|
|
961
|
+
parts: [
|
|
962
|
+
{
|
|
963
|
+
type: 'text',
|
|
964
|
+
text: checkResult.routingContext ?? 'Please also provide your perspective on this topic.',
|
|
965
|
+
},
|
|
966
|
+
],
|
|
967
|
+
metadata: { hidden: true, createdAt: Date.now() },
|
|
968
|
+
}
|
|
969
|
+
await workstreamMessageService.upsertMessages({
|
|
970
|
+
workstreamId: workstreamRef,
|
|
971
|
+
messages: [bridgeMessage],
|
|
972
|
+
})
|
|
973
|
+
currentMessages = upsertChatHistoryMessage(currentMessages, bridgeMessage)
|
|
974
|
+
|
|
975
|
+
lastResponse = await runGroupAgent(checkResult.agentId, checkResult.routingContext)
|
|
976
|
+
respondedAgents.push(checkResult.agentId)
|
|
977
|
+
}
|
|
978
|
+
}
|
|
1028
979
|
}
|
|
1029
980
|
}
|
|
1030
981
|
} finally {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { WORKSTREAM, sdkWorkstreamStatusSchema } from '@lota-sdk/shared'
|
|
2
2
|
import { BoundQuery, RecordId, StringRecordId, surql } from 'surrealdb'
|
|
3
3
|
|
|
4
|
-
import { agentDisplayNames, getCoreWorkstreamProfile, isAgentName } from '../config/agent-defaults'
|
|
4
|
+
import { agentDisplayNames, agentRoster, getCoreWorkstreamProfile, isAgentName } from '../config/agent-defaults'
|
|
5
5
|
import { serverLogger } from '../config/logger'
|
|
6
6
|
import { getWorkstreamBootstrapConfig } from '../config/workstream-defaults'
|
|
7
7
|
import { BaseService } from '../db/base.service'
|
|
@@ -152,7 +152,14 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
|
|
|
152
152
|
async createWorkstream(
|
|
153
153
|
userId: RecordIdRef,
|
|
154
154
|
orgId: RecordIdRef,
|
|
155
|
-
options?: {
|
|
155
|
+
options?: {
|
|
156
|
+
title?: string
|
|
157
|
+
mode?: string
|
|
158
|
+
agentId?: string
|
|
159
|
+
core?: boolean
|
|
160
|
+
coreType?: string
|
|
161
|
+
members?: string[]
|
|
162
|
+
},
|
|
156
163
|
): Promise<NormalizedWorkstream> {
|
|
157
164
|
const mode = options?.mode ?? 'group'
|
|
158
165
|
const directAgentId = options?.agentId
|
|
@@ -199,6 +206,7 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
|
|
|
199
206
|
mode,
|
|
200
207
|
core: false,
|
|
201
208
|
agentId,
|
|
209
|
+
members: [agentId],
|
|
202
210
|
title,
|
|
203
211
|
status: 'regular',
|
|
204
212
|
nameGenerated: true,
|
|
@@ -217,6 +225,7 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
|
|
|
217
225
|
core: true,
|
|
218
226
|
coreType: resolvedCoreType,
|
|
219
227
|
agentId: coreProfile.config.agentId,
|
|
228
|
+
members: [...coreProfile.members],
|
|
220
229
|
title,
|
|
221
230
|
status: 'regular',
|
|
222
231
|
nameGenerated: true,
|
|
@@ -229,6 +238,7 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
|
|
|
229
238
|
organizationId: orgId,
|
|
230
239
|
mode,
|
|
231
240
|
core: false,
|
|
241
|
+
members: options?.members ?? [...agentRoster],
|
|
232
242
|
title,
|
|
233
243
|
status: 'regular',
|
|
234
244
|
nameGenerated: options?.title !== undefined && options.title !== WORKSTREAM.DEFAULT_TITLE,
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { Output, ToolLoopAgent } from 'ai'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
import { aiGatewayChatModel } from '../ai-gateway/ai-gateway'
|
|
5
|
+
import { buildAiGatewayDirectCacheHeaders } from '../ai-gateway/cache-headers'
|
|
6
|
+
import { agentDescriptions, agentDisplayNames } from '../config/agent-defaults'
|
|
7
|
+
import { OPENROUTER_MANAGER_MODEL_ID, OPENROUTER_XHIGH_REASONING_PROVIDER_OPTIONS } from '../config/model-constants'
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Schemas
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
const TriageResultSchema = z.object({
|
|
14
|
+
agentId: z.string().describe('The agent ID that should respond next'),
|
|
15
|
+
routingContext: z.string().describe('Brief instruction for the selected agent about what to focus on'),
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const CheckResultSchema = z.object({
|
|
19
|
+
done: z.boolean().describe('true if no more agents need to respond'),
|
|
20
|
+
agentId: z.string().optional().describe('Next agent ID if done is false'),
|
|
21
|
+
routingContext: z.string().optional().describe('Brief instruction for the next agent if done is false'),
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export type ManagerTriageResult = z.infer<typeof TriageResultSchema>
|
|
25
|
+
export type ManagerCheckResult = z.infer<typeof CheckResultSchema>
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Prompt builders
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
function buildMembersDescription(members: readonly string[]): string {
|
|
32
|
+
return members
|
|
33
|
+
.map((id) => {
|
|
34
|
+
const display = agentDisplayNames[id] ?? id
|
|
35
|
+
const desc = agentDescriptions[id] ?? ''
|
|
36
|
+
return `- **${display}** (id: \`${id}\`): ${desc}`
|
|
37
|
+
})
|
|
38
|
+
.join('\n')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const TRIAGE_SYSTEM_PROMPT = `You are a workstream message router. Your job is to decide which team member should respond to a user message.
|
|
42
|
+
|
|
43
|
+
<rules>
|
|
44
|
+
- Pick the single best-fit agent from the members list.
|
|
45
|
+
- Consider the agent's domain expertise against the user's question.
|
|
46
|
+
- If the message is clearly about a specific domain (marketing, tech, product, finance, etc.), route to the domain specialist.
|
|
47
|
+
- If multiple domains are equally relevant, pick the most relevant one — a follow-up check will decide if another agent should also respond.
|
|
48
|
+
- If no specialist clearly matches (general chat, greetings, coordination), respond with an empty agentId to signal fallback.
|
|
49
|
+
- Be decisive. Do not overthink.
|
|
50
|
+
</rules>
|
|
51
|
+
|
|
52
|
+
<output-format>
|
|
53
|
+
Respond with JSON only: { "agentId": "<agent-id>", "routingContext": "<1-sentence instruction>" }
|
|
54
|
+
If no specialist matches, respond: { "agentId": "", "routingContext": "" }
|
|
55
|
+
</output-format>`
|
|
56
|
+
|
|
57
|
+
const CHECK_SYSTEM_PROMPT = `You are a workstream conversation reviewer. After an agent has responded to a user message, you decide if another team member should also respond.
|
|
58
|
+
|
|
59
|
+
<rules>
|
|
60
|
+
- Only add another agent if the user's question has a clearly separate dimension that the previous agent(s) did not cover.
|
|
61
|
+
- Do NOT add agents just for acknowledgement or agreement.
|
|
62
|
+
- Do NOT repeat the same domain. If the CTO already covered tech, don't add another tech perspective.
|
|
63
|
+
- Most messages need only one agent. Multi-agent responses should be the exception, not the rule.
|
|
64
|
+
- Maximum 3 total agent responses per user message.
|
|
65
|
+
</rules>
|
|
66
|
+
|
|
67
|
+
<output-format>
|
|
68
|
+
Respond with JSON only: { "done": true } or { "done": false, "agentId": "<agent-id>", "routingContext": "<1-sentence instruction>" }
|
|
69
|
+
</output-format>`
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Agent functions
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
function createManagerAgent(systemPrompt: string, schema: z.ZodSchema) {
|
|
76
|
+
return new ToolLoopAgent({
|
|
77
|
+
id: 'workstream-manager',
|
|
78
|
+
model: aiGatewayChatModel(OPENROUTER_MANAGER_MODEL_ID),
|
|
79
|
+
headers: buildAiGatewayDirectCacheHeaders('workstream-manager'),
|
|
80
|
+
providerOptions: OPENROUTER_XHIGH_REASONING_PROVIDER_OPTIONS,
|
|
81
|
+
instructions: systemPrompt,
|
|
82
|
+
output: Output.object({ schema }),
|
|
83
|
+
maxOutputTokens: 256,
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function triageWorkstreamMessage(params: {
|
|
88
|
+
workstreamTitle: string
|
|
89
|
+
members: readonly string[]
|
|
90
|
+
messageText: string
|
|
91
|
+
recentContext?: string
|
|
92
|
+
}): Promise<ManagerTriageResult | null> {
|
|
93
|
+
const membersDesc = buildMembersDescription(params.members)
|
|
94
|
+
const prompt = [
|
|
95
|
+
`Workstream: "${params.workstreamTitle}"`,
|
|
96
|
+
`\nMembers:\n${membersDesc}`,
|
|
97
|
+
params.recentContext ? `\nRecent context:\n${params.recentContext}` : '',
|
|
98
|
+
`\nUser message: "${params.messageText}"`,
|
|
99
|
+
]
|
|
100
|
+
.filter(Boolean)
|
|
101
|
+
.join('\n')
|
|
102
|
+
|
|
103
|
+
const agent = createManagerAgent(TRIAGE_SYSTEM_PROMPT, TriageResultSchema)
|
|
104
|
+
const result = await agent.generate({ messages: [{ role: 'user', content: prompt }], timeout: { totalMs: 15_000 } })
|
|
105
|
+
|
|
106
|
+
const parsed = TriageResultSchema.safeParse(result.output)
|
|
107
|
+
if (!parsed.success) return null
|
|
108
|
+
|
|
109
|
+
// Empty agentId = no specialist match, fallback to owner/chief
|
|
110
|
+
if (!parsed.data.agentId) return null
|
|
111
|
+
|
|
112
|
+
// Validate agent is in members list
|
|
113
|
+
if (!params.members.includes(parsed.data.agentId)) return null
|
|
114
|
+
|
|
115
|
+
return parsed.data
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function checkForNextAgent(params: {
|
|
119
|
+
workstreamTitle: string
|
|
120
|
+
members: readonly string[]
|
|
121
|
+
messageText: string
|
|
122
|
+
respondedAgents: string[]
|
|
123
|
+
lastResponseSummary: string
|
|
124
|
+
}): Promise<ManagerCheckResult> {
|
|
125
|
+
const remainingMembers = params.members.filter((id) => !params.respondedAgents.includes(id))
|
|
126
|
+
if (remainingMembers.length === 0) return { done: true }
|
|
127
|
+
|
|
128
|
+
const membersDesc = buildMembersDescription(remainingMembers)
|
|
129
|
+
const respondedList = params.respondedAgents.map((id) => agentDisplayNames[id] ?? id).join(', ')
|
|
130
|
+
|
|
131
|
+
const prompt = [
|
|
132
|
+
`Workstream: "${params.workstreamTitle}"`,
|
|
133
|
+
`\nRemaining members:\n${membersDesc}`,
|
|
134
|
+
`\nAlready responded: ${respondedList}`,
|
|
135
|
+
`\nUser message: "${params.messageText}"`,
|
|
136
|
+
`\nLast response summary: "${params.lastResponseSummary}"`,
|
|
137
|
+
].join('\n')
|
|
138
|
+
|
|
139
|
+
const agent = createManagerAgent(CHECK_SYSTEM_PROMPT, CheckResultSchema)
|
|
140
|
+
const result = await agent.generate({ messages: [{ role: 'user', content: prompt }], timeout: { totalMs: 15_000 } })
|
|
141
|
+
|
|
142
|
+
const parsed = CheckResultSchema.safeParse(result.output)
|
|
143
|
+
if (!parsed.success) return { done: true }
|
|
144
|
+
|
|
145
|
+
if (parsed.data.done) return { done: true }
|
|
146
|
+
|
|
147
|
+
// Validate next agent is in remaining members
|
|
148
|
+
if (!parsed.data.agentId || !remainingMembers.includes(parsed.data.agentId)) {
|
|
149
|
+
return { done: true }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return parsed.data
|
|
153
|
+
}
|