@lota-sdk/core 0.2.3 → 0.3.1
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_identity.surql +2 -2
- package/infrastructure/schema/00_thread.surql +73 -0
- package/infrastructure/schema/02_execution_plan.surql +10 -11
- package/infrastructure/schema/04_runtime_bootstrap.surql +1 -0
- package/infrastructure/schema/10_autonomous_job.surql +3 -3
- package/package.json +2 -2
- package/src/ai/definitions.ts +1 -1
- package/src/config/agent-defaults.ts +5 -5
- package/src/config/index.ts +1 -1
- package/src/config/thread-defaults.ts +72 -0
- package/src/create-runtime.ts +90 -94
- package/src/db/record-id.ts +21 -21
- package/src/db/service.ts +44 -40
- package/src/db/tables.ts +3 -3
- package/src/db/{workstream-message-row.ts → thread-message-row.ts} +3 -3
- package/src/queues/context-compaction.queue.ts +6 -6
- package/src/queues/plan-agent-heartbeat.queue.ts +3 -3
- package/src/queues/post-chat-memory.queue.ts +1 -1
- package/src/queues/title-generation.queue.ts +10 -13
- package/src/redis/index.ts +1 -1
- package/src/redis/stream-context.ts +1 -1
- package/src/runtime/agent-identity-overrides.ts +1 -1
- package/src/runtime/agent-runtime-policy.ts +19 -21
- package/src/runtime/chat-request-routing.ts +1 -1
- package/src/runtime/context-compaction-constants.ts +1 -1
- package/src/runtime/context-compaction.ts +1 -1
- package/src/runtime/execution-plan.ts +1 -1
- package/src/runtime/index.ts +1 -1
- package/src/runtime/memory-digest-policy.ts +1 -1
- package/src/runtime/plugin-types.ts +1 -1
- package/src/runtime/post-turn-side-effects.ts +35 -35
- package/src/runtime/runtime-config.ts +24 -21
- package/src/runtime/runtime-extensions.ts +11 -11
- package/src/runtime/social-chat-agent-runner.ts +3 -3
- package/src/runtime/social-chat-history.ts +1 -1
- package/src/runtime/social-chat.ts +6 -6
- package/src/runtime/team-consultation-orchestrator.ts +1 -1
- package/src/runtime/{workstream-chat-helpers.ts → thread-chat-helpers.ts} +7 -7
- package/src/runtime/{workstream-plan-turn.ts → thread-plan-turn.ts} +11 -17
- package/src/runtime/{workstream-turn-context.ts → thread-turn-context.ts} +10 -10
- package/src/services/agent-activity.service.ts +39 -44
- package/src/services/agent-executor.service.ts +17 -19
- package/src/services/attachment.service.ts +4 -8
- package/src/services/autonomous-job.service.ts +29 -28
- package/src/services/context-compaction.service.ts +19 -29
- package/src/services/execution-plan.service.ts +58 -70
- package/src/services/global-orchestrator.service.ts +5 -5
- package/src/services/index.ts +6 -6
- package/src/services/memory.service.ts +1 -1
- package/src/services/monitoring-window.service.ts +2 -2
- package/src/services/mutating-approval.service.ts +7 -10
- package/src/services/node-workspace.service.ts +8 -7
- package/src/services/notification.service.ts +1 -1
- package/src/services/organization.service.ts +9 -9
- package/src/services/ownership-dispatcher.service.ts +13 -19
- package/src/services/plan-agent-heartbeat.service.ts +13 -13
- package/src/services/plan-agent-query.service.ts +7 -7
- package/src/services/plan-artifact.service.ts +1 -2
- package/src/services/plan-coordination.service.ts +4 -4
- package/src/services/plan-cycle.service.ts +7 -7
- package/src/services/plan-deadline.service.ts +4 -4
- package/src/services/plan-event-delivery.service.ts +8 -12
- package/src/services/plan-executor.service.ts +25 -39
- package/src/services/plan-run-data.ts +27 -8
- package/src/services/plan-run.service.ts +7 -9
- package/src/services/plan-scheduler.service.ts +4 -4
- package/src/services/plan-template.service.ts +2 -2
- package/src/services/plan-validator.service.ts +0 -11
- package/src/services/plugin-executor.service.ts +1 -1
- package/src/services/queue-job.service.ts +1 -1
- package/src/services/recent-activity-title.service.ts +1 -1
- package/src/services/recent-activity.service.ts +4 -4
- package/src/services/system-executor.service.ts +2 -2
- package/src/services/{workstream-message.service.ts → thread-message.service.ts} +72 -76
- package/src/services/thread-plan-registry.service.ts +22 -0
- package/src/services/thread-title.service.ts +39 -0
- package/src/services/{workstream-turn-preparation.service.ts → thread-turn-preparation.service.ts} +148 -171
- package/src/services/{workstream-turn.ts → thread-turn.ts} +27 -31
- package/src/services/thread.service.ts +853 -0
- package/src/services/thread.types.ts +17 -0
- package/src/storage/attachment-storage.service.ts +4 -4
- package/src/system-agents/index.ts +1 -1
- package/src/system-agents/memory.agent.ts +1 -1
- package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -2
- package/src/system-agents/regular-chat-memory-digest.agent.ts +1 -1
- package/src/system-agents/researcher.agent.ts +3 -3
- package/src/system-agents/{workstream-router.agent.ts → thread-router.agent.ts} +68 -135
- package/src/system-agents/title-generator.agent.ts +8 -8
- package/src/tools/execution-plan.tool.ts +39 -40
- package/src/tools/memory-block.tool.ts +4 -4
- package/src/tools/research-topic.tool.ts +1 -0
- package/src/tools/search-web.tool.ts +1 -1
- package/src/tools/search.tool.ts +4 -4
- package/src/tools/team-think.tool.ts +9 -9
- package/src/utils/async.ts +6 -7
- package/src/workers/regular-chat-memory-digest.helpers.ts +1 -1
- package/src/workers/regular-chat-memory-digest.runner.ts +43 -43
- package/src/workers/skill-extraction.runner.ts +9 -13
- package/src/workers/utils/{workstream-message-query.ts → thread-message-query.ts} +21 -21
- package/infrastructure/schema/00_workstream.surql +0 -64
- package/src/config/workstream-defaults.ts +0 -72
- package/src/services/workstream-plan-registry.service.ts +0 -22
- package/src/services/workstream-title.service.ts +0 -42
- package/src/services/workstream.service.ts +0 -803
- package/src/services/workstream.types.ts +0 -17
- /package/src/services/{workstream-constants.ts → thread-constants.ts} +0 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { sdkPublicThreadSchema, sdkThreadRecordSchema, sdkThreadSchema } from '@lota-sdk/shared'
|
|
2
|
+
import type { SdkPublicThread, SdkThreadRecord } from '@lota-sdk/shared'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
|
|
5
|
+
export const ThreadSchema = sdkThreadRecordSchema
|
|
6
|
+
export type ThreadRecord = SdkThreadRecord
|
|
7
|
+
|
|
8
|
+
export const NormalizedThreadSchema = sdkThreadSchema.extend({
|
|
9
|
+
agentId: z.string().optional(),
|
|
10
|
+
threadType: z.string().optional(),
|
|
11
|
+
memoryBlock: z.string(),
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
export type NormalizedThread = z.infer<typeof NormalizedThreadSchema>
|
|
15
|
+
export type PublicThread = SdkPublicThread
|
|
16
|
+
|
|
17
|
+
export { sdkPublicThreadSchema as PublicThreadSchema }
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
|
|
27
27
|
const READ_FILE_PARTS_PAGES_PER_PART = 25
|
|
28
28
|
|
|
29
|
-
export type
|
|
29
|
+
export type UploadedThreadAttachment = {
|
|
30
30
|
filename: string
|
|
31
31
|
mediaType: string
|
|
32
32
|
sizeBytes: number
|
|
@@ -86,7 +86,7 @@ export class AttachmentStorageService {
|
|
|
86
86
|
orgId: string
|
|
87
87
|
namespace: string
|
|
88
88
|
relativePath: string
|
|
89
|
-
}): Promise<
|
|
89
|
+
}): Promise<UploadedThreadAttachment> {
|
|
90
90
|
const filename = file.name || 'document'
|
|
91
91
|
const mediaType = file.type || inferContentType(filename)
|
|
92
92
|
const storageKey = buildOrganizationDocumentStorageKey({
|
|
@@ -107,7 +107,7 @@ export class AttachmentStorageService {
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
async
|
|
110
|
+
async uploadThreadAttachment({
|
|
111
111
|
file,
|
|
112
112
|
orgId,
|
|
113
113
|
userId,
|
|
@@ -115,7 +115,7 @@ export class AttachmentStorageService {
|
|
|
115
115
|
file: File
|
|
116
116
|
orgId: string
|
|
117
117
|
userId: string
|
|
118
|
-
}): Promise<
|
|
118
|
+
}): Promise<UploadedThreadAttachment> {
|
|
119
119
|
const filename = file.name || 'attachment'
|
|
120
120
|
const mediaType = file.type || inferContentType(filename)
|
|
121
121
|
const sizeBytes = file.size
|
|
@@ -27,7 +27,7 @@ Turn recent activity context into a short label that helps the user quickly reco
|
|
|
27
27
|
- Prefer concrete task language over generic labels.
|
|
28
28
|
- Mention the agent only when it adds useful context.
|
|
29
29
|
- Focus on what the user was trying to accomplish.
|
|
30
|
-
- Avoid generic phrases like "Chat", "Conversation", "Recent activity", "Task", or "
|
|
30
|
+
- Avoid generic phrases like "Chat", "Conversation", "Recent activity", "Task", or "Thread update".
|
|
31
31
|
- Avoid punctuation at the end.
|
|
32
32
|
- Do not invent details that are not present in the input.
|
|
33
33
|
- If the input is too weak for improvement, return the current system title.
|
|
@@ -58,7 +58,7 @@ Turn recent activity context into a short label that helps the user quickly reco
|
|
|
58
58
|
- Prefer concrete task language over generic labels.
|
|
59
59
|
- Mention the agent only when it adds useful context.
|
|
60
60
|
- Focus on what the user was trying to accomplish.
|
|
61
|
-
- Avoid generic phrases like "Chat", "Conversation", "Recent activity", "Task", or "
|
|
61
|
+
- Avoid generic phrases like "Chat", "Conversation", "Recent activity", "Task", or "Thread update".
|
|
62
62
|
- Avoid punctuation at the end.
|
|
63
63
|
- Do not invent details that are not present in the input.
|
|
64
64
|
- If the input is too weak for improvement, return the current system title.
|
|
@@ -16,7 +16,7 @@ Synthesize an updated workspace profile summary and durable memory facts from co
|
|
|
16
16
|
|
|
17
17
|
<rules>
|
|
18
18
|
- Evidence-grounded only. Do not invent details. Exclude routing/tool chatter.
|
|
19
|
-
- Treat [
|
|
19
|
+
- Treat [thread:...] prefixes as thread context only.
|
|
20
20
|
- Preserve existing profile format. Merge corrections; remove stale claims only when contradicted.
|
|
21
21
|
- Facts must be standalone, one concrete claim each, understandable without transcript context.
|
|
22
22
|
- If no durable updates exist, return current summary unchanged and empty facts.
|
|
@@ -2,12 +2,12 @@ export const RESEARCHER_PROMPT = `<agent-instructions>
|
|
|
2
2
|
You are a **Research Agent** that gathers accurate, up-to-date information from the web and synthesizes it into a clear markdown report.
|
|
3
3
|
|
|
4
4
|
<workflow>
|
|
5
|
-
1. Break the research task into 2-
|
|
5
|
+
1. Break the research task into 2-3 focused search queries.
|
|
6
6
|
2. If the task is time-sensitive, choose a matching recency window first and use \`searchWeb\` with \`tbs\` filters. Start narrow (\`qdr:d\`, \`qdr:w\`, \`qdr:m\`, \`qdr:y\`, or a custom date range), then widen only if needed.
|
|
7
7
|
3. Run all searchWeb calls in parallel.
|
|
8
|
-
4. Review results and identify the 3
|
|
8
|
+
4. Review results and identify the 2-3 most authoritative/relevant URLs.
|
|
9
9
|
5. Fetch those pages in parallel using fetchWebpage.
|
|
10
|
-
6. If initial results are insufficient, reformulate queries and
|
|
10
|
+
6. If initial results are insufficient, reformulate 1-2 queries and retry once.
|
|
11
11
|
7. Synthesize findings into a structured markdown report.
|
|
12
12
|
</workflow>
|
|
13
13
|
|
|
@@ -1,30 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { generateObject } from 'ai'
|
|
2
2
|
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
6
|
import { agentDescriptions, agentDisplayNames, agentShortDisplayNames, routerModelId } from '../config/agent-defaults'
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
// Schemas
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
7
|
+
import { chatLogger } from '../config/logger'
|
|
8
|
+
import { withTimeout } from '../utils/async'
|
|
11
9
|
|
|
12
10
|
const TriageResultSchema = z.object({ agentId: z.string(), routingContext: z.string() })
|
|
13
11
|
|
|
14
|
-
const
|
|
12
|
+
const CheckResultObjectSchema = z.object({
|
|
15
13
|
done: z.boolean(),
|
|
16
|
-
agentId: z.string().
|
|
17
|
-
routingContext: z.string().
|
|
14
|
+
agentId: z.string().nullable(),
|
|
15
|
+
routingContext: z.string().nullable(),
|
|
18
16
|
})
|
|
19
|
-
|
|
20
|
-
const ROUTER_OUTPUT_PREVIEW_CHARS = 300
|
|
17
|
+
type RouterCheckContinueResult = { done: false; agentId: string; routingContext: string | null }
|
|
21
18
|
|
|
22
19
|
export type RouterTriageResult = z.infer<typeof TriageResultSchema>
|
|
23
|
-
export type RouterCheckResult =
|
|
24
|
-
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
// Helpers
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
20
|
+
export type RouterCheckResult = { done: true } | RouterCheckContinueResult
|
|
28
21
|
|
|
29
22
|
interface RouterDisplayOptions {
|
|
30
23
|
displayNamesById?: Partial<Record<string, string>>
|
|
@@ -136,56 +129,13 @@ function extractExplicitAgentTargets(
|
|
|
136
129
|
})
|
|
137
130
|
}
|
|
138
131
|
|
|
139
|
-
|
|
140
|
-
const match = text.match(/\{[\s\S]*\}/)
|
|
141
|
-
if (!match) return null
|
|
142
|
-
try {
|
|
143
|
-
return JSON.parse(match[0])
|
|
144
|
-
} catch {
|
|
145
|
-
return null
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/** Extract usable text from agent result — reasoning-only models put output in reasoning tokens */
|
|
150
|
-
function extractResultText(result: { text?: string; reasoning?: unknown }): string {
|
|
151
|
-
const text = typeof result.text === 'string' ? result.text : ''
|
|
152
|
-
if (text.trim()) return text
|
|
153
|
-
// Reasoning can be a string or an array of { type, text } objects
|
|
154
|
-
const reasoning = result.reasoning
|
|
155
|
-
if (typeof reasoning === 'string') return reasoning
|
|
156
|
-
if (Array.isArray(reasoning)) {
|
|
157
|
-
return reasoning
|
|
158
|
-
.map((r) => {
|
|
159
|
-
if (typeof r === 'string') return r
|
|
160
|
-
if (typeof r !== 'object' || r === null || !('text' in r)) return ''
|
|
161
|
-
|
|
162
|
-
const text = (r as { text?: unknown }).text
|
|
163
|
-
return typeof text === 'string' ? text : ''
|
|
164
|
-
})
|
|
165
|
-
.join('')
|
|
166
|
-
}
|
|
167
|
-
return ''
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function logRouterRaw(label: 'triage' | 'check', text: string): void {
|
|
171
|
-
const preview = text.trim().slice(0, ROUTER_OUTPUT_PREVIEW_CHARS)
|
|
172
|
-
if (!preview) return
|
|
173
|
-
console.log(`[workstream-router] ${label} raw:`, preview)
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// ---------------------------------------------------------------------------
|
|
177
|
-
// Prompts
|
|
178
|
-
// ---------------------------------------------------------------------------
|
|
179
|
-
|
|
180
|
-
const TRIAGE_SYSTEM_PROMPT = `You are a workstream message router. Decide which team member should respond FIRST to the user message.
|
|
132
|
+
const TRIAGE_SYSTEM_PROMPT = `You are a thread message router. Decide which team member should respond FIRST to the user message.
|
|
181
133
|
|
|
182
134
|
Rules:
|
|
183
135
|
- Pick the single best-fit agent from the members list based on domain expertise.
|
|
184
136
|
- If the user explicitly addresses an agent by name or role (e.g. "CTO: ..." or "CMO: ..."), route to that agent.
|
|
185
137
|
- If no specialist clearly matches (general chat, greetings, coordination), respond with agentId "".
|
|
186
|
-
- Be decisive
|
|
187
|
-
|
|
188
|
-
Format: {"agentId":"<id>","routingContext":"<1-sentence instruction>"}`
|
|
138
|
+
- Be decisive.`
|
|
189
139
|
|
|
190
140
|
const CHECK_SYSTEM_PROMPT = `You decide if another team member should ALSO respond after the previous agent's response.
|
|
191
141
|
|
|
@@ -199,29 +149,41 @@ Rules:
|
|
|
199
149
|
- If the user explicitly addressed multiple agents (e.g. "CTO: ... CMO: ...") and one hasn't responded yet, they MUST respond. Return done:false.
|
|
200
150
|
- If the last agent's response explicitly defers to or recommends another specialist, that specialist SHOULD respond. Return done:false.
|
|
201
151
|
- 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.
|
|
202
|
-
- Do NOT add agents just for agreement, acknowledgement, or minor additions
|
|
203
|
-
- Reply with ONLY a JSON object, no other text.
|
|
152
|
+
- Do NOT add agents just for agreement, acknowledgement, or minor additions.`
|
|
204
153
|
|
|
205
|
-
|
|
154
|
+
const THREAD_ROUTER_TIMEOUT_MS = 30_000
|
|
206
155
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
156
|
+
async function generateRouterObject<TSchema extends z.ZodTypeAny>(params: {
|
|
157
|
+
schema: TSchema
|
|
158
|
+
system: string
|
|
159
|
+
prompt: string
|
|
160
|
+
label: 'triage' | 'check'
|
|
161
|
+
}): Promise<z.infer<TSchema> | null> {
|
|
212
162
|
const modelId = routerModelId ?? 'openai/gpt-5.4-nano'
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const { object } = await withTimeout(
|
|
166
|
+
generateObject({
|
|
167
|
+
model: aiGatewayChatModel(modelId),
|
|
168
|
+
headers: buildAiGatewayDirectCacheHeaders('thread-router'),
|
|
169
|
+
providerOptions: { openai: { reasoningEffort: 'low' } },
|
|
170
|
+
schema: params.schema,
|
|
171
|
+
system: params.system,
|
|
172
|
+
prompt: params.prompt,
|
|
173
|
+
maxOutputTokens: 256,
|
|
174
|
+
}),
|
|
175
|
+
THREAD_ROUTER_TIMEOUT_MS,
|
|
176
|
+
`thread-router ${params.label}`,
|
|
177
|
+
)
|
|
178
|
+
return params.schema.parse(object)
|
|
179
|
+
} catch (error) {
|
|
180
|
+
chatLogger.error`[thread-router] ${params.label} failed: ${error instanceof Error ? error.message : String(error)}`
|
|
181
|
+
return null
|
|
182
|
+
}
|
|
221
183
|
}
|
|
222
184
|
|
|
223
|
-
export async function
|
|
224
|
-
|
|
185
|
+
export async function triageThreadMessage(params: {
|
|
186
|
+
threadTitle: string
|
|
225
187
|
members: readonly string[]
|
|
226
188
|
messageText: string
|
|
227
189
|
recentContext?: string
|
|
@@ -245,7 +207,7 @@ export async function triageWorkstreamMessage(params: {
|
|
|
245
207
|
|
|
246
208
|
const membersDesc = buildMembersDescription(params.members, displayOptions)
|
|
247
209
|
const prompt = [
|
|
248
|
-
`
|
|
210
|
+
`Thread: "${params.threadTitle}"`,
|
|
249
211
|
`Members:\n${membersDesc}`,
|
|
250
212
|
params.recentContext ? `Recent context:\n${params.recentContext}` : '',
|
|
251
213
|
`User message: "${params.messageText}"`,
|
|
@@ -253,44 +215,31 @@ export async function triageWorkstreamMessage(params: {
|
|
|
253
215
|
.filter(Boolean)
|
|
254
216
|
.join('\n\n')
|
|
255
217
|
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
return null
|
|
263
|
-
}
|
|
218
|
+
const parsed = await generateRouterObject({
|
|
219
|
+
schema: TriageResultSchema,
|
|
220
|
+
system: TRIAGE_SYSTEM_PROMPT,
|
|
221
|
+
prompt,
|
|
222
|
+
label: 'triage',
|
|
223
|
+
})
|
|
264
224
|
|
|
265
|
-
|
|
266
|
-
logRouterRaw('triage', effectiveText)
|
|
267
|
-
const json = extractJson(effectiveText)
|
|
268
|
-
if (json === null) {
|
|
269
|
-
if (effectiveText.trim()) {
|
|
270
|
-
console.log('[workstream-router] triage ignored non-json output')
|
|
271
|
-
}
|
|
272
|
-
return null
|
|
273
|
-
}
|
|
274
|
-
const parsed = TriageResultSchema.safeParse(json)
|
|
275
|
-
if (!parsed.success) {
|
|
276
|
-
console.log('[workstream-router] triage parse failed:', JSON.stringify(parsed.error.issues))
|
|
225
|
+
if (!parsed) {
|
|
277
226
|
return null
|
|
278
227
|
}
|
|
279
|
-
if (!parsed.
|
|
280
|
-
|
|
228
|
+
if (!parsed.agentId) {
|
|
229
|
+
chatLogger.debug`[thread-router] triage returned empty agentId`
|
|
281
230
|
return null
|
|
282
231
|
}
|
|
283
|
-
if (!params.members.includes(parsed.
|
|
284
|
-
|
|
232
|
+
if (!params.members.includes(parsed.agentId)) {
|
|
233
|
+
chatLogger.warn`[thread-router] triage returned unknown agent: ${parsed.agentId}`
|
|
285
234
|
return null
|
|
286
235
|
}
|
|
287
236
|
|
|
288
|
-
|
|
289
|
-
return parsed
|
|
237
|
+
chatLogger.debug`[thread-router] triage routed to ${parsed.agentId}`
|
|
238
|
+
return parsed
|
|
290
239
|
}
|
|
291
240
|
|
|
292
241
|
export async function checkForNextAgent(params: {
|
|
293
|
-
|
|
242
|
+
threadTitle: string
|
|
294
243
|
members: readonly string[]
|
|
295
244
|
messageText: string
|
|
296
245
|
respondedAgents: string[]
|
|
@@ -321,45 +270,29 @@ export async function checkForNextAgent(params: {
|
|
|
321
270
|
const respondedList = params.respondedAgents.map((id) => readDisplayName(id, displayOptions)).join(', ')
|
|
322
271
|
|
|
323
272
|
const prompt = [
|
|
324
|
-
`
|
|
273
|
+
`Thread: "${params.threadTitle}"`,
|
|
325
274
|
`Remaining members:\n${membersDesc}`,
|
|
326
275
|
`Already responded: ${respondedList}`,
|
|
327
276
|
`User message: "${params.messageText}"`,
|
|
328
277
|
`Last agent response:\n"${params.lastResponseSummary}"`,
|
|
329
278
|
].join('\n\n')
|
|
330
279
|
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
return { done: true }
|
|
338
|
-
}
|
|
280
|
+
const parsed = await generateRouterObject({
|
|
281
|
+
schema: CheckResultObjectSchema,
|
|
282
|
+
system: CHECK_SYSTEM_PROMPT,
|
|
283
|
+
prompt,
|
|
284
|
+
label: 'check',
|
|
285
|
+
})
|
|
339
286
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
const json = extractJson(effectiveText)
|
|
343
|
-
if (json === null) {
|
|
344
|
-
if (effectiveText.trim()) {
|
|
345
|
-
console.log('[workstream-router] check ignored non-json output')
|
|
346
|
-
}
|
|
347
|
-
return { done: true }
|
|
348
|
-
}
|
|
349
|
-
const parsed = CheckResultSchema.safeParse(json)
|
|
350
|
-
if (!parsed.success) {
|
|
351
|
-
console.log('[workstream-router] check parse failed:', JSON.stringify(parsed.error.issues))
|
|
352
|
-
return { done: true }
|
|
353
|
-
}
|
|
354
|
-
if (parsed.data.done) {
|
|
355
|
-
console.log('[workstream-router] check: done, no more agents needed')
|
|
287
|
+
if (!parsed || parsed.done) {
|
|
288
|
+
chatLogger.debug`[thread-router] check finished without another agent`
|
|
356
289
|
return { done: true }
|
|
357
290
|
}
|
|
358
|
-
if (!parsed.
|
|
359
|
-
|
|
291
|
+
if (!parsed.agentId || !remainingMembers.includes(parsed.agentId)) {
|
|
292
|
+
chatLogger.warn`[thread-router] check returned invalid agent: ${parsed.agentId ?? 'missing'}`
|
|
360
293
|
return { done: true }
|
|
361
294
|
}
|
|
362
295
|
|
|
363
|
-
|
|
364
|
-
return parsed.
|
|
296
|
+
chatLogger.debug`[thread-router] check selected ${parsed.agentId}`
|
|
297
|
+
return { done: false, agentId: parsed.agentId, routingContext: parsed.routingContext ?? null }
|
|
365
298
|
}
|
|
@@ -9,9 +9,9 @@ import {
|
|
|
9
9
|
import type { CreateHelperToolLoopAgentOptions } from '../runtime/agent-types'
|
|
10
10
|
import { resolveHelperAgentOptions } from './helper-agent-options'
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const THREAD_TITLE_MAX_TOKENS = 512
|
|
13
13
|
|
|
14
|
-
export const
|
|
14
|
+
export const THREAD_TITLE_GENERATOR_PROMPT = `<agent-instructions>
|
|
15
15
|
You are a **Title Generator** that creates concise chat titles.
|
|
16
16
|
|
|
17
17
|
<task>
|
|
@@ -20,7 +20,7 @@ Generate a chat title based only on the user's message.
|
|
|
20
20
|
|
|
21
21
|
<constraints>
|
|
22
22
|
- Maximum 3-4 words
|
|
23
|
-
- Capture the core
|
|
23
|
+
- Capture the core thread or intent
|
|
24
24
|
- Use natural, readable language
|
|
25
25
|
- No punctuation at the end
|
|
26
26
|
</constraints>
|
|
@@ -30,15 +30,15 @@ Return only the title text. No quotes, no labels, no explanation.
|
|
|
30
30
|
</output-format>
|
|
31
31
|
</agent-instructions>`
|
|
32
32
|
|
|
33
|
-
export function
|
|
33
|
+
export function createThreadTitleGeneratorAgent(options: CreateHelperToolLoopAgentOptions) {
|
|
34
34
|
return new ToolLoopAgent({
|
|
35
|
-
id: '
|
|
35
|
+
id: 'thread-title-generator',
|
|
36
36
|
model: aiGatewayModel(OPENROUTER_FAST_REASONING_MODEL_ID),
|
|
37
|
-
headers: buildAiGatewayDirectCacheHeaders('
|
|
37
|
+
headers: buildAiGatewayDirectCacheHeaders('thread-title-generator'),
|
|
38
38
|
providerOptions: OPENROUTER_MINIMAL_REASONING_PROVIDER_OPTIONS,
|
|
39
39
|
...resolveHelperAgentOptions(options, {
|
|
40
|
-
instructions:
|
|
41
|
-
maxOutputTokens:
|
|
40
|
+
instructions: THREAD_TITLE_GENERATOR_PROMPT,
|
|
41
|
+
maxOutputTokens: THREAD_TITLE_MAX_TOKENS,
|
|
42
42
|
}),
|
|
43
43
|
})
|
|
44
44
|
}
|
|
@@ -5,19 +5,16 @@ import {
|
|
|
5
5
|
expandAgentPlanDraft,
|
|
6
6
|
getLatestExecutionPlanResult,
|
|
7
7
|
} from '@lota-sdk/shared'
|
|
8
|
-
import type { ExecutionPlanAction, ExecutionPlanArgs
|
|
8
|
+
import type { CreateProjectWithPlanResultData, ExecutionPlanAction, ExecutionPlanArgs } from '@lota-sdk/shared'
|
|
9
9
|
import { tool } from 'ai'
|
|
10
10
|
|
|
11
11
|
import type { RecordIdRef } from '../db/record-id'
|
|
12
12
|
import { recordIdToString } from '../db/record-id'
|
|
13
13
|
import { TABLES } from '../db/tables'
|
|
14
14
|
import { executionPlanService } from '../services/execution-plan.service'
|
|
15
|
-
import {
|
|
15
|
+
import { threadService } from '../services/thread.service'
|
|
16
16
|
|
|
17
|
-
type
|
|
18
|
-
typeof workstreamService,
|
|
19
|
-
'createWorkstream' | 'deleteWorkstream' | 'getWorkstream'
|
|
20
|
-
>
|
|
17
|
+
type ExecutionPlanThreadService = Pick<typeof threadService, 'createThread' | 'deleteThread' | 'getThread'>
|
|
21
18
|
|
|
22
19
|
type ExecutionPlanExecutionPlanService = Pick<
|
|
23
20
|
typeof executionPlanService,
|
|
@@ -26,90 +23,92 @@ type ExecutionPlanExecutionPlanService = Pick<
|
|
|
26
23
|
| 'resumeRun'
|
|
27
24
|
| 'listActivePlanSummaries'
|
|
28
25
|
| 'getActivePlanToolResult'
|
|
29
|
-
| '
|
|
26
|
+
| 'getActivePlansForThread'
|
|
30
27
|
>
|
|
31
28
|
|
|
32
29
|
function extractDraft(input: ExecutionPlanArgs) {
|
|
33
|
-
const { action, projectTitle,
|
|
34
|
-
return { action, projectTitle,
|
|
30
|
+
const { action, projectTitle, targetThreadId, runId, reason, ...draftInput } = input
|
|
31
|
+
return { action, projectTitle, targetThreadId, runId, reason, draft: expandAgentPlanDraft(draftInput) }
|
|
35
32
|
}
|
|
36
33
|
|
|
37
34
|
export function createExecutionPlanTool(params: {
|
|
38
35
|
orgId: RecordIdRef
|
|
39
36
|
userId: RecordIdRef
|
|
40
|
-
|
|
37
|
+
threadId: RecordIdRef
|
|
41
38
|
agentId: string
|
|
42
39
|
executionPlanService?: ExecutionPlanExecutionPlanService
|
|
43
|
-
|
|
40
|
+
threadService?: ExecutionPlanThreadService
|
|
44
41
|
onPlanChanged?: () => void
|
|
45
42
|
validateInlinePlan?: (draft: ReturnType<typeof expandAgentPlanDraft>) => void
|
|
46
43
|
}) {
|
|
47
44
|
const resolvedEpService = params.executionPlanService ?? executionPlanService
|
|
48
|
-
const resolvedWsService = params.
|
|
45
|
+
const resolvedWsService = params.threadService ?? threadService
|
|
49
46
|
|
|
50
47
|
return tool({
|
|
51
48
|
description:
|
|
52
|
-
'Manage execution plans. Actions: create (inline, 1-2 nodes), create-project (dedicated project
|
|
49
|
+
'Manage execution plans. Actions: create (inline, 1-2 nodes), create-project (dedicated project thread, 3+ nodes), replace (swap active plan), resume (resume interrupted plan).',
|
|
53
50
|
inputSchema: ExecutionPlanArgsSchema,
|
|
54
51
|
execute: async (input) => {
|
|
55
|
-
const { action, projectTitle,
|
|
52
|
+
const { action, projectTitle, targetThreadId, runId, reason, draft } = extractDraft(input)
|
|
56
53
|
|
|
57
54
|
const handler: Record<ExecutionPlanAction, () => Promise<unknown>> = {
|
|
58
55
|
create: async () => {
|
|
59
56
|
params.validateInlinePlan?.(draft)
|
|
60
57
|
return await resolvedEpService.createPlan({
|
|
61
58
|
organizationId: params.orgId,
|
|
62
|
-
|
|
59
|
+
threadId: targetThreadId ?? params.threadId,
|
|
63
60
|
leadAgentId: params.agentId,
|
|
64
61
|
input: draft,
|
|
65
62
|
})
|
|
66
63
|
},
|
|
67
64
|
|
|
68
65
|
'create-project': async () => {
|
|
69
|
-
const
|
|
70
|
-
? await resolvedWsService.
|
|
66
|
+
const targetThread = targetThreadId
|
|
67
|
+
? await resolvedWsService.getThread(targetThreadId)
|
|
71
68
|
: await (() => {
|
|
72
69
|
if (!projectTitle) {
|
|
73
70
|
throw new Error('projectTitle is required when action is "create-project".')
|
|
74
71
|
}
|
|
75
72
|
|
|
76
|
-
return resolvedWsService.
|
|
73
|
+
return resolvedWsService.createThread({
|
|
74
|
+
userId: params.userId,
|
|
75
|
+
organizationId: params.orgId,
|
|
77
76
|
title: projectTitle,
|
|
78
|
-
|
|
77
|
+
type: 'group',
|
|
79
78
|
})
|
|
80
79
|
})()
|
|
81
80
|
|
|
82
|
-
if (
|
|
83
|
-
throw new Error('Target
|
|
81
|
+
if (targetThread.organizationId !== recordIdToString(params.orgId, TABLES.ORGANIZATION)) {
|
|
82
|
+
throw new Error('Target thread belongs to a different organization.')
|
|
84
83
|
}
|
|
85
|
-
if (
|
|
86
|
-
throw new Error('Target
|
|
84
|
+
if (targetThread.userId !== recordIdToString(params.userId, TABLES.USER)) {
|
|
85
|
+
throw new Error('Target thread belongs to a different user.')
|
|
87
86
|
}
|
|
88
87
|
|
|
89
|
-
const existingPlans = await resolvedEpService.
|
|
90
|
-
if (
|
|
88
|
+
const existingPlans = await resolvedEpService.getActivePlansForThread(targetThread.id)
|
|
89
|
+
if (targetThread.type !== 'thread' && existingPlans.length > 0) {
|
|
91
90
|
throw new Error(
|
|
92
|
-
'This
|
|
91
|
+
'This thread already has an active execution plan. Use action "replace" or target a core thread.',
|
|
93
92
|
)
|
|
94
93
|
}
|
|
95
94
|
|
|
96
|
-
const
|
|
95
|
+
const createdThread = !targetThreadId
|
|
97
96
|
try {
|
|
98
97
|
const result = await resolvedEpService.createPlan({
|
|
99
98
|
organizationId: params.orgId,
|
|
100
|
-
|
|
99
|
+
threadId: targetThread.id,
|
|
101
100
|
leadAgentId: params.agentId,
|
|
102
101
|
input: draft,
|
|
103
102
|
})
|
|
104
103
|
return {
|
|
105
104
|
...result,
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
threadId: targetThread.id,
|
|
106
|
+
threadTitle: targetThread.title,
|
|
107
|
+
createdThread,
|
|
109
108
|
} satisfies CreateProjectWithPlanResultData
|
|
110
109
|
} catch (error) {
|
|
111
|
-
if (
|
|
112
|
-
await resolvedWsService.
|
|
110
|
+
if (createdThread) {
|
|
111
|
+
await resolvedWsService.deleteThread(targetThread.id).catch(() => {})
|
|
113
112
|
}
|
|
114
113
|
throw error
|
|
115
114
|
}
|
|
@@ -122,7 +121,7 @@ export function createExecutionPlanTool(params: {
|
|
|
122
121
|
|
|
123
122
|
return await resolvedEpService.replacePlan({
|
|
124
123
|
organizationId: params.orgId,
|
|
125
|
-
|
|
124
|
+
threadId: params.threadId,
|
|
126
125
|
leadAgentId: params.agentId,
|
|
127
126
|
input: { runId, reason, ...draft },
|
|
128
127
|
})
|
|
@@ -134,7 +133,7 @@ export function createExecutionPlanTool(params: {
|
|
|
134
133
|
}
|
|
135
134
|
|
|
136
135
|
return await resolvedEpService.resumeRun({
|
|
137
|
-
|
|
136
|
+
threadId: params.threadId,
|
|
138
137
|
emittedBy: params.agentId,
|
|
139
138
|
input: { runId },
|
|
140
139
|
})
|
|
@@ -148,17 +147,17 @@ export function createExecutionPlanTool(params: {
|
|
|
148
147
|
})
|
|
149
148
|
}
|
|
150
149
|
|
|
151
|
-
export function createExecutionPlanQueryTool(params: {
|
|
150
|
+
export function createExecutionPlanQueryTool(params: { threadId: RecordIdRef }) {
|
|
152
151
|
return tool({
|
|
153
152
|
description:
|
|
154
153
|
'Query execution plans. Omit runId to list all active plans. Provide runId to load a specific plan run.',
|
|
155
154
|
inputSchema: ExecutionPlanQueryArgsSchema,
|
|
156
155
|
execute: async (input) => {
|
|
157
156
|
if (!input.runId) {
|
|
158
|
-
return await executionPlanService.listActivePlanSummaries(params.
|
|
157
|
+
return await executionPlanService.listActivePlanSummaries(params.threadId)
|
|
159
158
|
}
|
|
160
159
|
return await executionPlanService.getActivePlanToolResult({
|
|
161
|
-
|
|
160
|
+
threadId: params.threadId,
|
|
162
161
|
runId: input.runId,
|
|
163
162
|
includeEvents: input.includeEvents,
|
|
164
163
|
includeArtifacts: input.includeArtifacts,
|
|
@@ -171,7 +170,7 @@ export function createExecutionPlanQueryTool(params: { workstreamId: RecordIdRef
|
|
|
171
170
|
}
|
|
172
171
|
|
|
173
172
|
export function createSubmitExecutionNodeResultTool(params: {
|
|
174
|
-
|
|
173
|
+
threadId: RecordIdRef
|
|
175
174
|
agentId: string
|
|
176
175
|
onPlanChanged?: () => void
|
|
177
176
|
}) {
|
|
@@ -181,7 +180,7 @@ export function createSubmitExecutionNodeResultTool(params: {
|
|
|
181
180
|
inputSchema: SubmitExecutionNodeResultArgsSchema,
|
|
182
181
|
execute: async (input) => {
|
|
183
182
|
const result = await executionPlanService.submitNodeResult({
|
|
184
|
-
|
|
183
|
+
threadId: params.threadId,
|
|
185
184
|
emittedBy: params.agentId,
|
|
186
185
|
input,
|
|
187
186
|
})
|