@lota-sdk/core 0.1.43 → 0.1.45

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.43",
3
+ "version": "0.1.45",
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.43",
35
+ "@lota-sdk/shared": "0.1.45",
36
36
  "@mendable/firecrawl-js": "^4.18.0",
37
37
  "@surrealdb/node": "^3.0.3",
38
38
  "ai": "^6.0.141",
@@ -4,7 +4,6 @@ import { z } from 'zod'
4
4
  import { aiGatewayChatModel } from '../ai-gateway/ai-gateway'
5
5
  import { buildAiGatewayDirectCacheHeaders } from '../ai-gateway/cache-headers'
6
6
  import { agentDescriptions, agentDisplayNames, routerModelId } from '../config/agent-defaults'
7
- import { OPENROUTER_FAST_REASONING_MODEL_ID } from '../config/model-constants'
8
7
 
9
8
  // ---------------------------------------------------------------------------
10
9
  // Schemas
@@ -45,26 +44,48 @@ function extractJson(text: string): unknown {
45
44
  }
46
45
  }
47
46
 
47
+ /** Extract usable text from agent result — reasoning-only models put output in reasoning tokens */
48
+ function extractResultText(result: { text?: string; reasoning?: unknown }): string {
49
+ const text = typeof result.text === 'string' ? result.text : ''
50
+ if (text.trim()) return text
51
+ // Reasoning can be a string or an array of { type, text } objects
52
+ const reasoning = result.reasoning
53
+ if (typeof reasoning === 'string') return reasoning
54
+ if (Array.isArray(reasoning)) {
55
+ return reasoning
56
+ .map((r) => (typeof r === 'string' ? r : typeof r === 'object' && r && 'text' in r ? String(r.text) : ''))
57
+ .join('')
58
+ }
59
+ return ''
60
+ }
61
+
48
62
  // ---------------------------------------------------------------------------
49
63
  // Prompts
50
64
  // ---------------------------------------------------------------------------
51
65
 
52
- const TRIAGE_SYSTEM_PROMPT = `You are a workstream message router. Decide which team member should respond to the user message.
66
+ const TRIAGE_SYSTEM_PROMPT = `You are a workstream message router. Decide which team member should respond FIRST to the user message.
53
67
 
54
68
  Rules:
55
69
  - Pick the single best-fit agent from the members list based on domain expertise.
70
+ - If the user explicitly addresses an agent by name or role (e.g. "CTO: ..." or "CMO: ..."), route to that agent.
56
71
  - If no specialist clearly matches (general chat, greetings, coordination), respond with agentId "".
57
72
  - Be decisive. Reply with ONLY a JSON object, no other text.
58
73
 
59
74
  Format: {"agentId":"<id>","routingContext":"<1-sentence instruction>"}`
60
75
 
61
- const CHECK_SYSTEM_PROMPT = `You decide if another team member should also respond after the previous agent.
76
+ const CHECK_SYSTEM_PROMPT = `You decide if another team member should ALSO respond after the previous agent's response.
77
+
78
+ You will receive:
79
+ - The original user message
80
+ - Which agents already responded
81
+ - The last agent's actual response
82
+ - The remaining available agents
62
83
 
63
84
  Rules:
64
- - Add another agent if the user's message has a clearly separate dimension not yet covered.
65
- - If the user explicitly addressed multiple agents (e.g. "CTO: ... CMO: ..."), each addressed agent MUST respond.
66
- - If the previous agent deferred to another specialist, that specialist SHOULD respond.
67
- - Do NOT add agents for agreement or acknowledgement only.
85
+ - If the user explicitly addressed multiple agents (e.g. "CTO: ... CMO: ...") and one hasn't responded yet, they MUST respond. Return done:false.
86
+ - If the last agent's response explicitly defers to or recommends another specialist, that specialist SHOULD respond. Return done:false.
87
+ - If there is a clearly separate dimension of the user's question not yet covered by any responded agent, add the best-fit remaining agent.
88
+ - Do NOT add agents just for agreement, acknowledgement, or minor additions.
68
89
  - Reply with ONLY a JSON object, no other text.
69
90
 
70
91
  Format: {"done":true} or {"done":false,"agentId":"<id>","routingContext":"<1-sentence>"}`
@@ -74,16 +95,12 @@ Format: {"done":true} or {"done":false,"agentId":"<id>","routingContext":"<1-sen
74
95
  // ---------------------------------------------------------------------------
75
96
 
76
97
  function createRouterAgent(systemPrompt: string) {
77
- const modelId = routerModelId ?? OPENROUTER_FAST_REASONING_MODEL_ID
78
- // Router needs plain JSON output, not reasoning tokens
79
- const providerOptions = routerModelId
80
- ? { openai: { provider: { order: ['groq'], allow_fallbacks: true } } }
81
- : undefined
98
+ const modelId = routerModelId ?? 'openai/gpt-5.4-nano'
82
99
  return new ToolLoopAgent({
83
100
  id: 'workstream-router',
84
101
  model: aiGatewayChatModel(modelId),
85
102
  headers: buildAiGatewayDirectCacheHeaders('workstream-router'),
86
- providerOptions,
103
+ providerOptions: { openai: { reasoningEffort: 'high' } },
87
104
  instructions: systemPrompt,
88
105
  maxOutputTokens: 256,
89
106
  })
@@ -114,12 +131,8 @@ export async function triageWorkstreamMessage(params: {
114
131
  return null
115
132
  }
116
133
 
117
- const rawText = typeof result.text === 'string' ? result.text : ''
118
- const reasoning = (result as { reasoning?: string }).reasoning ?? ''
119
- console.log('[workstream-router] triage raw text:', rawText.slice(0, 300))
120
- console.log('[workstream-router] triage reasoning:', reasoning.slice(0, 300))
121
- // Use reasoning as fallback if text is empty (reasoning-only models like gpt-oss-120b)
122
- const effectiveText = rawText || reasoning
134
+ const effectiveText = extractResultText(result as { text?: string; reasoning?: unknown })
135
+ console.log('[workstream-router] triage raw:', effectiveText.slice(0, 300))
123
136
  const json = extractJson(effectiveText)
124
137
  const parsed = TriageResultSchema.safeParse(json)
125
138
  if (!parsed.success) {
@@ -157,17 +170,35 @@ export async function checkForNextAgent(params: {
157
170
  `Remaining members:\n${membersDesc}`,
158
171
  `Already responded: ${respondedList}`,
159
172
  `User message: "${params.messageText}"`,
160
- `Last response summary: "${params.lastResponseSummary}"`,
173
+ `Last agent response:\n"${params.lastResponseSummary}"`,
161
174
  ].join('\n\n')
162
175
 
163
176
  const agent = createRouterAgent(CHECK_SYSTEM_PROMPT)
164
- const result = await agent.generate({ messages: [{ role: 'user', content: prompt }], timeout: { totalMs: 30_000 } })
177
+ let result: Awaited<ReturnType<typeof agent.generate>>
178
+ try {
179
+ result = await agent.generate({ messages: [{ role: 'user', content: prompt }], timeout: { totalMs: 30_000 } })
180
+ } catch (error) {
181
+ console.error('[workstream-router] check failed:', error instanceof Error ? error.message : error)
182
+ return { done: true }
183
+ }
165
184
 
166
- const json = extractJson(typeof result.text === 'string' ? result.text : '')
185
+ const effectiveText = extractResultText(result as { text?: string; reasoning?: unknown })
186
+ console.log('[workstream-router] check raw:', effectiveText.slice(0, 300))
187
+ const json = extractJson(effectiveText)
167
188
  const parsed = CheckResultSchema.safeParse(json)
168
- if (!parsed.success) return { done: true }
169
- if (parsed.data.done) return { done: true }
170
- if (!parsed.data.agentId || !remainingMembers.includes(parsed.data.agentId)) return { done: true }
189
+ if (!parsed.success) {
190
+ console.log('[workstream-router] check parse failed:', JSON.stringify(parsed.error.issues))
191
+ return { done: true }
192
+ }
193
+ if (parsed.data.done) {
194
+ console.log('[workstream-router] check: done, no more agents needed')
195
+ return { done: true }
196
+ }
197
+ if (!parsed.data.agentId || !remainingMembers.includes(parsed.data.agentId)) {
198
+ console.log('[workstream-router] check: invalid agentId:', parsed.data.agentId)
199
+ return { done: true }
200
+ }
171
201
 
202
+ console.log('[workstream-router] check: next agent:', parsed.data.agentId)
172
203
  return parsed.data
173
204
  }