@lota-sdk/core 0.1.30 → 0.1.32
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 +2 -2
- package/src/ai/definitions.ts +13 -102
- package/src/runtime/agent-runtime-policy.ts +5 -16
- package/src/runtime/context-compaction.ts +12 -0
- package/src/runtime/execution-plan.ts +9 -28
- package/src/runtime/workstream-plan-turn.ts +10 -69
- package/src/runtime/workstream-turn-context.ts +7 -8
- package/src/services/plan-run.service.ts +31 -5
- package/src/services/workstream-turn-preparation.service.ts +32 -17
- package/src/system-agents/delegated-agent-factory.ts +1 -2
- package/src/system-agents/regular-chat-memory-digest.agent.ts +10 -52
- package/src/system-agents/skill-extractor.agent.ts +9 -43
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lota-sdk/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.32",
|
|
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.32",
|
|
36
36
|
"@mendable/firecrawl-js": "^4.18.0",
|
|
37
37
|
"@surrealdb/node": "^3.0.3",
|
|
38
38
|
"ai": "^6.0.141",
|
package/src/ai/definitions.ts
CHANGED
|
@@ -81,60 +81,14 @@ export const askingUserQuestionsSkill = defineSkill({
|
|
|
81
81
|
tools: askingUserQuestionsSkillTools,
|
|
82
82
|
instructions: `# asking-user-questions
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
Use this skill when an answer or action requires missing user input that cannot be inferred reliably.
|
|
84
|
+
Use \`userQuestions\` when an answer requires missing user input that cannot be inferred.
|
|
87
85
|
|
|
88
|
-
|
|
86
|
+
Types: \`single-select\` (pick one), \`multi-select\` (pick many), \`input\` (free text).
|
|
87
|
+
Pass questions as \`{ questions: [{ question, type, options? }] }\`.
|
|
89
88
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
- The user must choose between materially different options.
|
|
94
|
-
- You need explicit consent before a mutation or irreversible action.
|
|
95
|
-
- You need specific business context only the user can provide.
|
|
96
|
-
- The request is ambiguous and proceeding would produce low-quality output.
|
|
97
|
-
- You need approval or confirmation before taking a significant action.
|
|
98
|
-
</when-to-use>
|
|
99
|
-
|
|
100
|
-
<how-to-use>
|
|
101
|
-
Use the \`userQuestions\` tool to present structured questions. This renders an interactive form in the chat UI and
|
|
102
|
-
terminates the agent chain until the user responds. The user already sees this form, so do **not** restate the same
|
|
103
|
-
questions in plain text. Do **not** call any additional tools in the same response.
|
|
104
|
-
|
|
105
|
-
### Question types
|
|
106
|
-
|
|
107
|
-
- \`single-select\` — user picks one option from a list (with optional custom answer field)
|
|
108
|
-
- \`multi-select\` — user picks multiple options from a list (with optional custom answer field)
|
|
109
|
-
- \`input\` — user types a free-text answer
|
|
110
|
-
|
|
111
|
-
### Schema
|
|
112
|
-
|
|
113
|
-
\`\`\`json
|
|
114
|
-
{
|
|
115
|
-
"questions": [
|
|
116
|
-
{ "question": "What is your target market?", "type": "single-select", "options": ["B2B", "B2C", "Both"] },
|
|
117
|
-
{
|
|
118
|
-
"question": "Which features do you need?",
|
|
119
|
-
"type": "multi-select",
|
|
120
|
-
"options": ["Authentication", "REST API", "Admin Panel", "Analytics"]
|
|
121
|
-
},
|
|
122
|
-
{ "question": "What is your budget range?", "type": "input" }
|
|
123
|
-
]
|
|
124
|
-
}
|
|
125
|
-
\`\`\`
|
|
126
|
-
|
|
127
|
-
- \`options\` is a plain string array (no IDs needed).
|
|
128
|
-
- \`allowCustomAnswer\` (optional, defaults to \`true\` for select types) adds a free-text field alongside options.
|
|
129
|
-
- Do **not** generate additional text or tool calls after calling \`userQuestions\` (including repeating the questions as
|
|
130
|
-
plain text). Your turn is complete.
|
|
131
|
-
</how-to-use>
|
|
132
|
-
|
|
133
|
-
<do-not-use>
|
|
134
|
-
- Do not ask when reasonable defaults are already explicit in the user request.
|
|
135
|
-
- Do not ask for information that can be retrieved using available read tools.
|
|
136
|
-
- Do not ask broad discovery questions when one targeted question will unblock progress.
|
|
137
|
-
</do-not-use>`,
|
|
89
|
+
Do not restate questions in text after calling the tool — the UI renders them.
|
|
90
|
+
Do not call additional tools in the same response.
|
|
91
|
+
Do not ask when reasonable defaults exist or information is retrievable.`,
|
|
138
92
|
})
|
|
139
93
|
|
|
140
94
|
const researchSkillTools = ['researchTopic', 'fetchWebpage', 'inspectWebsite'] as const
|
|
@@ -146,58 +100,15 @@ export const researchSkill = defineSkill({
|
|
|
146
100
|
tools: researchSkillTools,
|
|
147
101
|
instructions: `# research
|
|
148
102
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
Use for any task requiring external information gathering: market research, competitive analysis, technical evaluation,
|
|
152
|
-
fact verification, or source-backed answers. Delegates web research to a dedicated research agent.
|
|
103
|
+
Use for external information: market research, competitive analysis, fact verification.
|
|
153
104
|
|
|
154
|
-
##
|
|
105
|
+
## Tools
|
|
106
|
+
- \`researchTopic\` — delegate to research agent. For broad research, call 2-3 in parallel with different focused tasks.
|
|
107
|
+
- \`fetchWebpage\` — only when user shares a specific URL. Do not use for general research.
|
|
108
|
+
- \`inspectWebsite\` — structured analysis of a website. Pass URL in \`url\` field. Use \`forceRefresh: true\` to overwrite.
|
|
155
109
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
- Market sizing, competitor analysis, or industry trend research.
|
|
159
|
-
- Technical evaluation of tools, frameworks, vendors, or platforms.
|
|
160
|
-
- Fact-checking claims, statistics, or benchmarks before using them in deliverables.
|
|
161
|
-
- Gathering evidence to support or challenge a hypothesis from another skill.
|
|
162
|
-
|
|
163
|
-
Do NOT use when:
|
|
164
|
-
|
|
165
|
-
- The question can be answered from conversation context or organizational memory alone.
|
|
166
|
-
- The user is asking for coaching, strategy, or opinion (use a domain skill instead).
|
|
167
|
-
- The task is purely internal (roadmap prioritization, team dynamics, financial modeling from known data).
|
|
168
|
-
</when-to-use>
|
|
169
|
-
|
|
170
|
-
<how-to-research>
|
|
171
|
-
- Call \`researchTopic\` with a clear, specific research task description.
|
|
172
|
-
- For broad research, call 2-3 \`researchTopic\` instances **in parallel** with different focused tasks (e.g., one for market sizing, one for competitor analysis, one for pricing benchmarks).
|
|
173
|
-
- Each \`researchTopic\` call returns a synthesized markdown report with sources — you do not need to search or fetch pages yourself.
|
|
174
|
-
- Use \`fetchWebpage\` **only** when the user shares a specific URL they want you to read. Do not use it for general research.
|
|
175
|
-
- Use \`inspectWebsite\` when the user wants a structured analysis of a specific website or landing page. Pass the exact URL in the tool's \`url\` field and the learning goal in \`task\`.
|
|
176
|
-
- When the user asks to refresh, re-run, or overwrite existing website extraction, call \`inspectWebsite\` with \`forceRefresh: true\`.
|
|
177
|
-
</how-to-research>
|
|
178
|
-
|
|
179
|
-
<startup-context>
|
|
180
|
-
- Time is the scarcest resource; research must be fast and decisive, not exhaustive.
|
|
181
|
-
- Early-stage decisions tolerate lower confidence thresholds; a directionally correct answer now beats a perfect answer next week.
|
|
182
|
-
- Prioritize sources that reveal market reality: pricing pages, job postings, SEC filings, G2/Capterra reviews, GitHub activity, and community forums over polished marketing content.
|
|
183
|
-
- Always contextualize findings to the startup's stage, segment, and constraints.
|
|
184
|
-
</startup-context>
|
|
185
|
-
|
|
186
|
-
<answering-style>
|
|
187
|
-
- Deliver findings directly; do not narrate the research process to the user.
|
|
188
|
-
- Lead with the answer or recommendation, then provide supporting evidence.
|
|
189
|
-
- State confidence level (high / medium / low) for each key claim based on source quality and corroboration.
|
|
190
|
-
- When sources conflict, present both sides and explain the discrepancy.
|
|
191
|
-
- Never mention internal tool names (researchTopic, fetchWebpage, inspectWebsite, skillDetails, memorySearch).
|
|
192
|
-
</answering-style>
|
|
193
|
-
|
|
194
|
-
<output-structure>
|
|
195
|
-
- Summary: 2-4 sentence bottom-line answer to the research question.
|
|
196
|
-
- Key Findings: Bulleted findings with inline source citations [URL] and confidence tags (high/medium/low).
|
|
197
|
-
- Comparison Table: (when applicable) structured comparison of alternatives, competitors, or options.
|
|
198
|
-
- Confidence Assessment: Overall confidence level with explanation of what drives it up or down.
|
|
199
|
-
- Gaps and Follow-ups: What remains unknown and what specific research would resolve it.
|
|
200
|
-
</output-structure>`,
|
|
110
|
+
## Output
|
|
111
|
+
Lead with the answer. State confidence (high/medium/low). Cite sources inline [URL]. Never mention tool names to the user.`,
|
|
201
112
|
})
|
|
202
113
|
|
|
203
114
|
const surrealDbSkillTools = [] as const
|
|
@@ -103,20 +103,9 @@ export function resolveActiveAgentSkills<TAgent extends string, TSkill extends P
|
|
|
103
103
|
getAgentSkills: (agentId: TAgent, mode: ChatMode) => TSkill[]
|
|
104
104
|
}): TSkill[] {
|
|
105
105
|
const mode = params.mode ?? toChatMode(params.workstreamMode)
|
|
106
|
-
|
|
107
|
-
const baseSkills = params
|
|
106
|
+
return params
|
|
108
107
|
.getAgentSkills(params.agentId, mode)
|
|
109
108
|
.filter((skill) => (params.linearInstalled ? true : skill !== ('linear' as TSkill)))
|
|
110
|
-
|
|
111
|
-
if (!params.onboardingActive) {
|
|
112
|
-
return baseSkills
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (params.agentId !== onboardingOwnerAgentId) {
|
|
116
|
-
return []
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return baseSkills.filter((skill) => skill === ('asking-user-questions' as TSkill))
|
|
120
109
|
}
|
|
121
110
|
|
|
122
111
|
export function buildAgentRuntimeConfig<TAgent extends string, TSkill extends PropertyKey>(params: {
|
|
@@ -195,10 +184,10 @@ export function buildWorkstreamAgentToolPolicy<TAgent extends string, TSkill ext
|
|
|
195
184
|
return {
|
|
196
185
|
resolvedMode,
|
|
197
186
|
skills,
|
|
198
|
-
includeMemorySearch:
|
|
199
|
-
includeConversationSearch:
|
|
200
|
-
includeMemoryRemember:
|
|
201
|
-
includeOrgActionSearch:
|
|
187
|
+
includeMemorySearch: true,
|
|
188
|
+
includeConversationSearch: true,
|
|
189
|
+
includeMemoryRemember: true,
|
|
190
|
+
includeOrgActionSearch: true,
|
|
202
191
|
includeMemoryBlockAppend: true,
|
|
203
192
|
includeReadFileParts: true,
|
|
204
193
|
includeInspectWebsite: params.onboardingActive && params.agentId === onboardingOwnerAgentId,
|
|
@@ -690,6 +690,18 @@ export function createContextCompactionRuntime(
|
|
|
690
690
|
const formatWorkstreamStateForPrompt = (state: WorkstreamState | null | undefined): string | undefined => {
|
|
691
691
|
if (!state) return undefined
|
|
692
692
|
|
|
693
|
+
// Skip serialization when all fields are empty
|
|
694
|
+
const hasContent =
|
|
695
|
+
(state.currentPlan !== null && state.currentPlan !== undefined) ||
|
|
696
|
+
state.activeConstraints.length > 0 ||
|
|
697
|
+
state.keyDecisions.length > 0 ||
|
|
698
|
+
state.openQuestions.length > 0 ||
|
|
699
|
+
state.risks.length > 0 ||
|
|
700
|
+
state.tasks.length > 0 ||
|
|
701
|
+
state.artifacts.length > 0 ||
|
|
702
|
+
state.agentContributions.length > 0
|
|
703
|
+
if (!hasContent) return undefined
|
|
704
|
+
|
|
693
705
|
const approvedPlan =
|
|
694
706
|
state.currentPlan && state.currentPlan.approved ? sanitizeStateText(state.currentPlan.text) : null
|
|
695
707
|
const candidatePlan =
|
|
@@ -1,46 +1,27 @@
|
|
|
1
|
-
import { PROJECT_PLAN_ROUTING_PROMPT } from '@lota-sdk/shared'
|
|
2
1
|
import type { SerializableExecutionPlan } from '@lota-sdk/shared'
|
|
3
2
|
|
|
4
3
|
const EXECUTION_PLAN_AGENT_PROTOCOL_PROMPT = `<execution-plan-protocol>
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
- Use execution-plan tools to create, replace, inspect, and resume runs.
|
|
10
|
-
- Visible workstream agents do not manually submit node results; dispatched execution nodes are completed by the runtime executor.
|
|
11
|
-
- When the runtime starts a plan-triggered visible execution turn, use the dedicated result-submission tool for that turn and include durable handoffContext for downstream nodes.
|
|
4
|
+
- Create execution plans for multi-step work. Review existing plans before creating new ones.
|
|
5
|
+
- The runtime executor owns lifecycle truth. Do not claim node completion until the executor confirms.
|
|
6
|
+
- Work only on active/ready nodes assigned to you. Stop at human gates.
|
|
7
|
+
- During plan-triggered turns, use the dedicated result-submission tool. Include handoffContext.
|
|
12
8
|
- Treat the active execution runs in <execution-plan-state> as authoritative. Do not mutate run or node status in prose.
|
|
13
|
-
-
|
|
14
|
-
- If the graph, contracts, or success criteria materially change, replace the plan instead of silently drifting.
|
|
9
|
+
- If contracts or criteria materially change, replace the plan.
|
|
15
10
|
</execution-plan-protocol>`
|
|
16
11
|
|
|
17
12
|
function formatExecutionPlansForPrompt(plans: SerializableExecutionPlan[]): string | undefined {
|
|
18
13
|
if (plans.length === 0) return undefined
|
|
19
14
|
|
|
20
|
-
const payload = {
|
|
21
|
-
policy: {
|
|
22
|
-
executorOwnsLifecycleTruth: true,
|
|
23
|
-
contractDrivenExecution: true,
|
|
24
|
-
humanGatesAreDurable: true,
|
|
25
|
-
artifactsAreFirstClassOutputs: true,
|
|
26
|
-
checkpointRecoveryEnabled: true,
|
|
27
|
-
},
|
|
28
|
-
activePlans: plans,
|
|
29
|
-
planCount: plans.length,
|
|
30
|
-
}
|
|
15
|
+
const payload = { activePlans: plans, planCount: plans.length }
|
|
31
16
|
|
|
32
17
|
return ['<execution-plan-state>', JSON.stringify(payload, null, 2), '</execution-plan-state>'].join('\n')
|
|
33
18
|
}
|
|
34
19
|
|
|
35
|
-
export function buildExecutionPlanInstructionSections(
|
|
36
|
-
plans: SerializableExecutionPlan[] | null | undefined,
|
|
37
|
-
): string[] | undefined {
|
|
20
|
+
export function buildExecutionPlanInstructionSections(plans: SerializableExecutionPlan[] | null | undefined): string[] {
|
|
38
21
|
const normalized = plans ?? []
|
|
39
|
-
const sections = [EXECUTION_PLAN_AGENT_PROTOCOL_PROMPT
|
|
22
|
+
const sections = [EXECUTION_PLAN_AGENT_PROTOCOL_PROMPT]
|
|
40
23
|
const stateSection = formatExecutionPlansForPrompt(normalized)
|
|
41
|
-
if (stateSection)
|
|
42
|
-
sections.push(stateSection)
|
|
43
|
-
}
|
|
24
|
+
if (stateSection) sections.push(stateSection)
|
|
44
25
|
return sections
|
|
45
26
|
}
|
|
46
27
|
|
|
@@ -29,30 +29,9 @@ export interface WorkstreamPlanTurnContext {
|
|
|
29
29
|
upstreamHandoffs: PlanTurnUpstreamHandoff[]
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
function describePlanTurnDeliverable(deliverable: PlanNodeSpecRecord['deliverables'][number]): string {
|
|
33
|
-
return [
|
|
34
|
-
`- ${deliverable.name}`,
|
|
35
|
-
`kind=${deliverable.kind}`,
|
|
36
|
-
deliverable.required ? 'required' : 'optional',
|
|
37
|
-
deliverable.schemaRef ? `schemaRef=${deliverable.schemaRef}` : undefined,
|
|
38
|
-
deliverable.description ? `description=${deliverable.description}` : undefined,
|
|
39
|
-
]
|
|
40
|
-
.filter(Boolean)
|
|
41
|
-
.join(' | ')
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function describePlanTurnCompletionCheck(check: PlanNodeSpecRecord['completionChecks'][number]): string {
|
|
45
|
-
return [
|
|
46
|
-
`- ${check.description}`,
|
|
47
|
-
`type=${check.type}`,
|
|
48
|
-
check.blocking ? 'blocking' : 'warning',
|
|
49
|
-
Object.keys(check.config).length > 0 ? `config=${JSON.stringify(check.config)}` : undefined,
|
|
50
|
-
]
|
|
51
|
-
.filter(Boolean)
|
|
52
|
-
.join(' | ')
|
|
53
|
-
}
|
|
54
|
-
|
|
55
32
|
function buildPlanTurnExecutionSection(planTurn: WorkstreamPlanTurnContext): string {
|
|
33
|
+
const requiredDeliverables = planTurn.nodeSpec.deliverables.filter((deliverable) => deliverable.required)
|
|
34
|
+
const completionCheckOutputHints = buildCompletionCheckStructuredOutputHints(planTurn.nodeSpec)
|
|
56
35
|
const payload = {
|
|
57
36
|
runId: planTurn.runId,
|
|
58
37
|
planTitle: planTurn.planTitle,
|
|
@@ -75,55 +54,17 @@ function buildPlanTurnExecutionSection(planTurn: WorkstreamPlanTurnContext): str
|
|
|
75
54
|
|
|
76
55
|
return [
|
|
77
56
|
'<plan-turn-execution>',
|
|
78
|
-
'The runtime has activated a visible execution-plan node inside this workstream.',
|
|
79
57
|
`Complete node "${planTurn.nodeSpec.label}" for plan "${planTurn.planTitle}".`,
|
|
80
|
-
'Use only the node contract, resolved input, input artifacts, and upstream handoff context
|
|
81
|
-
'
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
'
|
|
85
|
-
'If a deliverable declares schemaRef, include the same schemaRef and a payload that satisfies that schema.',
|
|
86
|
-
'If outputSchemaRef is declared, structuredOutput must satisfy that schema before you submit.',
|
|
87
|
-
`When finished, call ${SUBMIT_PLAN_TURN_RESULT_TOOL_NAME} exactly once.`,
|
|
88
|
-
'Always include durable handoffContext for downstream nodes when you submit the final result.',
|
|
89
|
-
'Do not ask the user for confirmation and do not create or replace execution plans in this turn.',
|
|
90
|
-
JSON.stringify(payload, null, 2),
|
|
91
|
-
'</plan-turn-execution>',
|
|
92
|
-
].join('\n')
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function buildPlanTurnResultContractSection(planTurn: WorkstreamPlanTurnContext): string {
|
|
96
|
-
const requiredDeliverables = planTurn.nodeSpec.deliverables.filter((deliverable) => deliverable.required)
|
|
97
|
-
const completionCheckOutputHints = buildCompletionCheckStructuredOutputHints(planTurn.nodeSpec)
|
|
98
|
-
const deliverableLines =
|
|
99
|
-
planTurn.nodeSpec.deliverables.length > 0
|
|
100
|
-
? planTurn.nodeSpec.deliverables.map(describePlanTurnDeliverable)
|
|
101
|
-
: ['- none']
|
|
102
|
-
const completionCheckLines =
|
|
103
|
-
planTurn.nodeSpec.completionChecks.length > 0
|
|
104
|
-
? planTurn.nodeSpec.completionChecks.map(describePlanTurnCompletionCheck)
|
|
105
|
-
: ['- none']
|
|
106
|
-
|
|
107
|
-
return [
|
|
108
|
-
'<plan-turn-result-contract>',
|
|
109
|
-
`Call ${SUBMIT_PLAN_TURN_RESULT_TOOL_NAME} exactly once with a result object that passes node validation.`,
|
|
110
|
-
'Validation is strict. Missing required artifacts, schema mismatches, or failed completion checks will fail the node run.',
|
|
111
|
-
`Required artifacts: ${requiredDeliverables.length > 0 ? requiredDeliverables.map((deliverable) => deliverable.name).join(', ') : 'none'}`,
|
|
112
|
-
`Structured output: ${
|
|
113
|
-
planTurn.nodeSpec.outputSchemaRef
|
|
114
|
-
? `required and must match schema "${planTurn.nodeSpec.outputSchemaRef}"`
|
|
115
|
-
: 'optional unless needed by a completion check'
|
|
116
|
-
}`,
|
|
117
|
-
'Deliverables:',
|
|
118
|
-
...deliverableLines,
|
|
119
|
-
'Completion checks:',
|
|
120
|
-
...completionCheckLines,
|
|
58
|
+
'Use only the node contract, resolved input, input artifacts, and upstream handoff context.',
|
|
59
|
+
'Before submitting, satisfy every required deliverable, success criterion, and completion check.',
|
|
60
|
+
`Call ${SUBMIT_PLAN_TURN_RESULT_TOOL_NAME} exactly once when done.`,
|
|
61
|
+
`Required artifacts: ${requiredDeliverables.length > 0 ? requiredDeliverables.map((d) => d.name).join(', ') : 'none'}.`,
|
|
62
|
+
'Include notes with a concise completion summary. Include handoffContext with summary, key decisions, open questions, risks, and recommendations for downstream nodes.',
|
|
121
63
|
...(completionCheckOutputHints.length > 0
|
|
122
64
|
? ['Structured output fields required by completion checks:', ...completionCheckOutputHints]
|
|
123
65
|
: []),
|
|
124
|
-
|
|
125
|
-
'
|
|
126
|
-
'</plan-turn-result-contract>',
|
|
66
|
+
JSON.stringify(payload, null, 2),
|
|
67
|
+
'</plan-turn-execution>',
|
|
127
68
|
].join('\n')
|
|
128
69
|
}
|
|
129
70
|
|
|
@@ -153,7 +94,7 @@ export function buildPlanTurnInstructionSections(planTurn: WorkstreamPlanTurnCon
|
|
|
153
94
|
const upstreamHandoffSection = buildUpstreamHandoffSection(planTurn.upstreamHandoffs)
|
|
154
95
|
return (
|
|
155
96
|
mergeInstructionSections(
|
|
156
|
-
[buildPlanTurnExecutionSection(planTurn)
|
|
97
|
+
[buildPlanTurnExecutionSection(planTurn)],
|
|
157
98
|
upstreamHandoffSection ? [upstreamHandoffSection] : undefined,
|
|
158
99
|
) ?? []
|
|
159
100
|
)
|
|
@@ -93,14 +93,13 @@ export async function assembleWorkstreamTurnContext(params: {
|
|
|
93
93
|
userName: params.userName ?? undefined,
|
|
94
94
|
recentDomainEvents,
|
|
95
95
|
})
|
|
96
|
-
let retrievedKnowledgeSection: string | undefined =
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
})
|
|
96
|
+
let retrievedKnowledgeSection: string | undefined = !params.messageText
|
|
97
|
+
? undefined
|
|
98
|
+
: await params.workspaceProvider?.buildRetrievedKnowledgeSection?.({
|
|
99
|
+
workspaceId: params.orgIdString,
|
|
100
|
+
userId: params.userIdString,
|
|
101
|
+
query: params.messageText,
|
|
102
|
+
})
|
|
104
103
|
params.onStep?.('rag-knowledge-retrieval')
|
|
105
104
|
|
|
106
105
|
const buildContextResult = asRecord(
|
|
@@ -334,21 +334,27 @@ class PlanRunService {
|
|
|
334
334
|
includeCheckpoints?: boolean
|
|
335
335
|
includeEvents?: boolean
|
|
336
336
|
includeValidationIssues?: boolean
|
|
337
|
+
/** When true, non-active/ready nodes are summarized (id, label, status, owner, objective, edges). Reduces prompt tokens. */
|
|
338
|
+
slim?: boolean
|
|
337
339
|
},
|
|
338
340
|
): Promise<SerializableExecutionPlan> {
|
|
341
|
+
const slim = options?.slim === true
|
|
339
342
|
const spec = await this.getPlanSpecById(run.planSpecId)
|
|
340
343
|
const nodeSpecs = await this.listNodeSpecs(spec.id)
|
|
341
344
|
const nodeRuns = await this.listNodeRuns(run.id)
|
|
342
345
|
const artifacts = options?.includeArtifacts === false ? [] : await this.listArtifacts(run.id)
|
|
343
|
-
const lineageArtifacts = options?.includeArtifacts === false ? [] : await this.collectLineageArtifacts(run)
|
|
344
|
-
const approvals = options?.includeApprovals === false ? [] : await this.listApprovals(run.id)
|
|
346
|
+
const lineageArtifacts = options?.includeArtifacts === false || slim ? [] : await this.collectLineageArtifacts(run)
|
|
347
|
+
const approvals = options?.includeApprovals === false || slim ? [] : await this.listApprovals(run.id)
|
|
345
348
|
const validationIssues =
|
|
346
|
-
options?.includeValidationIssues === false
|
|
349
|
+
options?.includeValidationIssues === false || slim
|
|
347
350
|
? []
|
|
348
351
|
: await this.listValidationIssues({ runId: run.id, planSpecId: spec.id })
|
|
349
352
|
const latestCheckpoint = options?.includeCheckpoints ? await this.getLatestCheckpoint(run.id) : null
|
|
350
|
-
const
|
|
353
|
+
const eventLimit = slim ? 5 : 20
|
|
354
|
+
const recentEvents = options?.includeEvents === false ? [] : await this.listEvents(run.id, eventLimit)
|
|
351
355
|
const nodeRunsById = new Map(nodeRuns.map((nodeRun) => [nodeRun.nodeId, nodeRun]))
|
|
356
|
+
const activeNodeIds = new Set(run.currentNodeId ? [run.currentNodeId] : [])
|
|
357
|
+
const readyNodeIds = new Set(run.readyNodeIds)
|
|
352
358
|
|
|
353
359
|
const nodes: SerializablePlanNode[] = nodeSpecs.map((nodeSpec) => {
|
|
354
360
|
const nodeRun = nodeRunsById.get(nodeSpec.nodeId)
|
|
@@ -358,6 +364,26 @@ class PlanRunService {
|
|
|
358
364
|
)
|
|
359
365
|
}
|
|
360
366
|
|
|
367
|
+
const isActiveOrReady = activeNodeIds.has(nodeSpec.nodeId) || readyNodeIds.has(nodeSpec.nodeId)
|
|
368
|
+
|
|
369
|
+
// Slim mode: non-active/ready nodes get summary only (used for prompt injection via JSON.stringify).
|
|
370
|
+
// The cast is safe — this data is only consumed by formatExecutionPlansForPrompt, not by Zod validation.
|
|
371
|
+
// Plan introspection tools (getExecutionPlanDetails) call toSerializablePlan without slim=true.
|
|
372
|
+
if (slim && !isActiveOrReady) {
|
|
373
|
+
return {
|
|
374
|
+
id: nodeSpec.nodeId,
|
|
375
|
+
type: nodeSpec.type,
|
|
376
|
+
label: nodeSpec.label,
|
|
377
|
+
owner: { executorType: nodeSpec.owner.executorType, ref: nodeSpec.owner.ref },
|
|
378
|
+
objective: nodeSpec.objective,
|
|
379
|
+
status: nodeRun.status,
|
|
380
|
+
upstreamNodeIds: [...nodeSpec.upstreamNodeIds],
|
|
381
|
+
downstreamNodeIds: [...nodeSpec.downstreamNodeIds],
|
|
382
|
+
...(nodeRun.handoffContext ? { handoffContext: nodeRun.handoffContext } : {}),
|
|
383
|
+
...(nodeRun.completedAt ? { completedAt: toOptionalIsoDateTimeString(nodeRun.completedAt) } : {}),
|
|
384
|
+
} as SerializablePlanNode
|
|
385
|
+
}
|
|
386
|
+
|
|
361
387
|
return {
|
|
362
388
|
id: nodeSpec.nodeId,
|
|
363
389
|
type: nodeSpec.type,
|
|
@@ -415,7 +441,7 @@ class PlanRunService {
|
|
|
415
441
|
leadAgentId: run.leadAgentId,
|
|
416
442
|
defaultExecutionVisibility: spec.defaultExecutionVisibility,
|
|
417
443
|
executionMode: spec.executionMode,
|
|
418
|
-
schemaRegistry: structuredClone(spec.schemaRegistry),
|
|
444
|
+
schemaRegistry: slim ? {} : structuredClone(spec.schemaRegistry),
|
|
419
445
|
entryNodeIds: [...spec.entryNodeIds],
|
|
420
446
|
edges: [...spec.edges],
|
|
421
447
|
schedule: spec.schedule,
|
|
@@ -284,7 +284,9 @@ async function streamAgentResponse(
|
|
|
284
284
|
streamParams: StreamAgentResponseParams,
|
|
285
285
|
): Promise<ChatMessage> {
|
|
286
286
|
const agentTimer = lotaDebugLogger.timer(`agent:${streamParams.agentId}`)
|
|
287
|
-
|
|
287
|
+
// Skip full plan state during plan turns — the plan-turn sections already have the active node contract
|
|
288
|
+
const executionPlanInstructionSections =
|
|
289
|
+
streamParams.includeExecutionPlanTools === false ? undefined : await ctx.getExecutionPlanInstructionSections()
|
|
288
290
|
agentTimer.step('get-execution-plan')
|
|
289
291
|
const agentResolution = asRecord(
|
|
290
292
|
await ctx.turnHooks.resolveAgent?.({
|
|
@@ -361,6 +363,7 @@ async function streamAgentResponse(
|
|
|
361
363
|
mode: streamParams.mode,
|
|
362
364
|
tools: streamParams.tools,
|
|
363
365
|
extraInstructions: config.extraInstructions,
|
|
366
|
+
maxRetries: 3,
|
|
364
367
|
stopWhen: (agentResolution?.stopWhen as StopCondition<ToolSet> | Array<StopCondition<ToolSet>> | undefined) ??
|
|
365
368
|
streamParams.stopWhen ?? [stepCountIs(config.maxSteps as number)],
|
|
366
369
|
prepareStep: (agentResolution?.prepareStep as PrepareStepFunction<ToolSet> | undefined) ?? streamParams.prepareStep,
|
|
@@ -368,19 +371,32 @@ async function streamAgentResponse(
|
|
|
368
371
|
const agentAbortSignal = streamParams.abortSignal ?? ctx.runAbortSignal
|
|
369
372
|
agentTimer.step('agent-construction')
|
|
370
373
|
|
|
374
|
+
const MAX_STREAM_RETRIES = 3
|
|
371
375
|
let result: unknown
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
376
|
+
for (let attempt = 0; ; attempt++) {
|
|
377
|
+
try {
|
|
378
|
+
result = await streamParams.observer.run(() =>
|
|
379
|
+
agent.stream({ messages: modelMessages, abortSignal: agentAbortSignal }),
|
|
380
|
+
)
|
|
381
|
+
agentTimer.step('agent.stream()-resolved')
|
|
382
|
+
break
|
|
383
|
+
} catch (error) {
|
|
384
|
+
if (agentAbortSignal.aborted) {
|
|
385
|
+
streamParams.observer.recordAbort(error)
|
|
386
|
+
throw error
|
|
387
|
+
}
|
|
388
|
+
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
389
|
+
const isTransient =
|
|
390
|
+
errorMessage.includes('client disconnected') ||
|
|
391
|
+
errorMessage.includes('ECONNRESET') ||
|
|
392
|
+
errorMessage.includes('socket hang up') ||
|
|
393
|
+
errorMessage.includes('fetch failed')
|
|
394
|
+
if (!isTransient || attempt >= MAX_STREAM_RETRIES - 1) {
|
|
395
|
+
streamParams.observer.recordError(error)
|
|
396
|
+
throw error
|
|
397
|
+
}
|
|
398
|
+
aiLogger.warn`Transient stream error (attempt ${attempt + 1}/${MAX_STREAM_RETRIES}): ${errorMessage} — retrying`
|
|
382
399
|
}
|
|
383
|
-
throw error
|
|
384
400
|
}
|
|
385
401
|
if (!hasUIMessageStream(result)) {
|
|
386
402
|
throw new Error(`Agent run for ${resolvedAgentId} did not expose a UI message stream.`)
|
|
@@ -642,10 +658,10 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
642
658
|
let memoryBlock = workstreamService.formatMemoryBlockForPrompt(workstreamRecord)
|
|
643
659
|
let workstreamState = initialWorkstreamState
|
|
644
660
|
const executionPlanInstructionSectionCache = createExecutionPlanInstructionSectionCache({
|
|
645
|
-
disabled:
|
|
661
|
+
disabled: false,
|
|
646
662
|
loadPlans: async () => {
|
|
647
663
|
const runs = await planRunService.getActiveRunRecords(workstreamRef)
|
|
648
|
-
return Promise.all(runs.map((run) => planRunService.toSerializablePlan(run)))
|
|
664
|
+
return Promise.all(runs.map((run) => planRunService.toSerializablePlan(run, { slim: true })))
|
|
649
665
|
},
|
|
650
666
|
})
|
|
651
667
|
const getExecutionPlans = async () => await executionPlanInstructionSectionCache.getPlans()
|
|
@@ -689,7 +705,6 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
689
705
|
|
|
690
706
|
const learnedSkillsByAgent = new Map<string, string | undefined>()
|
|
691
707
|
const getLearnedSkillsSection = async (agentId: string, queryText = messageText): Promise<string | undefined> => {
|
|
692
|
-
if (onboardingActive) return undefined
|
|
693
708
|
const cacheKey = `${agentId}::${queryText}`
|
|
694
709
|
if (learnedSkillsByAgent.has(cacheKey)) return learnedSkillsByAgent.get(cacheKey)
|
|
695
710
|
|
|
@@ -843,7 +858,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
843
858
|
const visibleTimer = lotaDebugLogger.timer(`visible:${runParams.agentId}`)
|
|
844
859
|
let runMemoryBlock = memoryBlock
|
|
845
860
|
const includeExecutionPlanTools =
|
|
846
|
-
runParams.includeExecutionPlanTools ??
|
|
861
|
+
runParams.includeExecutionPlanTools ?? runParams.mode !== 'fixedWorkstreamMode'
|
|
847
862
|
const rawTools: ToolSet = {
|
|
848
863
|
...((await buildAgentTools(
|
|
849
864
|
buildTurnToolParams({
|
|
@@ -970,7 +985,7 @@ export async function prepareWorkstreamRunCore(params: WorkstreamRunCoreParams):
|
|
|
970
985
|
})
|
|
971
986
|
|
|
972
987
|
const teamThinkTool =
|
|
973
|
-
workstream.mode === 'group'
|
|
988
|
+
workstream.mode === 'group'
|
|
974
989
|
? createTeamThinkTool({
|
|
975
990
|
historyMessages: currentMessages,
|
|
976
991
|
latestUserMessageId: referenceUserMessageId,
|
|
@@ -76,7 +76,6 @@ When your analysis is complete, return your final answer directly as markdown te
|
|
|
76
76
|
|
|
77
77
|
const DEFAULT_DELEGATED_AGENT_MAX_OUTPUT_TOKENS = 4096
|
|
78
78
|
const MAX_RETAINED_AGENT_MESSAGES = 10
|
|
79
|
-
const MAX_NON_SUBSTANTIVE_AGENT_RESULT_ATTEMPTS = 2
|
|
80
79
|
const NON_SUBSTANTIVE_AGENT_RESULT_RETRY_PROMPT =
|
|
81
80
|
'Return a complete substantive markdown answer. Do not reply with an empty result, placeholder, or tool-only outcome.'
|
|
82
81
|
|
|
@@ -154,7 +153,7 @@ async function generateSubstantiveDelegatedAgentResult(params: {
|
|
|
154
153
|
}
|
|
155
154
|
|
|
156
155
|
// Try a follow-up: feed the agent's tool results back as context and ask for synthesis
|
|
157
|
-
const toolContext = extractToolResultText(result.messages ?? [])
|
|
156
|
+
const toolContext = extractToolResultText('messages' in result ? ((result.messages ?? []) as ModelMessage[]) : [])
|
|
158
157
|
if (toolContext.length > 100) {
|
|
159
158
|
const followUpPrompt = [
|
|
160
159
|
params.task,
|
|
@@ -12,58 +12,16 @@ import { resolveHelperAgentOptions } from './helper-agent-options'
|
|
|
12
12
|
const REGULAR_CHAT_MEMORY_DIGEST_MAX_TOKENS = 8_192
|
|
13
13
|
|
|
14
14
|
const regularChatMemoryDigestPrompt = `<agent-instructions>
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
- Use only evidence from provided transcript and existing memory context.
|
|
26
|
-
</scope>
|
|
27
|
-
|
|
28
|
-
<quality-bar>
|
|
29
|
-
- Stay evidence-grounded. Do not invent details.
|
|
30
|
-
- Keep terminology consistent with existing profile wording unless new evidence contradicts it.
|
|
31
|
-
- Prefer concrete, reusable facts about the workspace, product, users, strategy, decisions, execution, and constraints.
|
|
32
|
-
- Exclude routing chatter, tool chatter, and purely stylistic language.
|
|
33
|
-
- If there are no durable updates, return the current summary block unchanged and an empty facts list.
|
|
34
|
-
</quality-bar>
|
|
35
|
-
|
|
36
|
-
<profile-format>
|
|
37
|
-
- Return a single summaryBlock string.
|
|
38
|
-
- Preserve the existing labeled-section format when present.
|
|
39
|
-
- Merge corrections from new evidence; remove stale claims only when clearly contradicted.
|
|
40
|
-
</profile-format>
|
|
41
|
-
|
|
42
|
-
<structured-profile-patch>
|
|
43
|
-
- Also return a structuredProfilePatch object that contains only evidence-grounded host-specific updates.
|
|
44
|
-
- Keep the patch additive when possible.
|
|
45
|
-
- If there are no structured updates, return an empty object.
|
|
46
|
-
</structured-profile-patch>
|
|
47
|
-
|
|
48
|
-
<facts-format>
|
|
49
|
-
- Return facts as durable, standalone statements.
|
|
50
|
-
- Each fact must be understandable without transcript context.
|
|
51
|
-
- Prefer one concrete claim per fact.
|
|
52
|
-
- Set type to one of: fact, preference, decision.
|
|
53
|
-
- Set confidence between 0 and 1.
|
|
54
|
-
- Set durability to core, standard, or ephemeral based on expected longevity.
|
|
55
|
-
- Set importance between 0 and 1 for long-term usefulness.
|
|
56
|
-
- Set classification to durable, transient, or uncertain.
|
|
57
|
-
- Set rationale to one short evidence-grounded sentence.
|
|
58
|
-
</facts-format>
|
|
59
|
-
|
|
60
|
-
<output-contract>
|
|
61
|
-
The caller enforces a structured output schema with:
|
|
62
|
-
- summaryBlock: non-empty string
|
|
63
|
-
- structuredProfilePatch: partial structured host-specific updates
|
|
64
|
-
- facts: array of extracted fact objects
|
|
65
|
-
Return only schema fields.
|
|
66
|
-
</output-contract>
|
|
15
|
+
Synthesize an updated workspace profile summary and durable memory facts from conversation transcripts.
|
|
16
|
+
|
|
17
|
+
<rules>
|
|
18
|
+
- Evidence-grounded only. Do not invent details. Exclude routing/tool chatter.
|
|
19
|
+
- Treat [workstream:...] prefixes as thread context only.
|
|
20
|
+
- Preserve existing profile format. Merge corrections; remove stale claims only when contradicted.
|
|
21
|
+
- Facts must be standalone, one concrete claim each, understandable without transcript context.
|
|
22
|
+
- If no durable updates exist, return current summary unchanged and empty facts.
|
|
23
|
+
- Return structuredProfilePatch with evidence-grounded host-specific updates only; empty object if none.
|
|
24
|
+
</rules>
|
|
67
25
|
</agent-instructions>`
|
|
68
26
|
|
|
69
27
|
export function createRegularChatMemoryDigestAgent(options: CreateHelperToolLoopAgentOptions) {
|
|
@@ -13,51 +13,17 @@ import { resolveHelperAgentOptions } from './helper-agent-options'
|
|
|
13
13
|
const SKILL_EXTRACTOR_MAX_TOKENS = 8_192
|
|
14
14
|
|
|
15
15
|
const skillExtractorPrompt = `<agent-instructions>
|
|
16
|
-
|
|
16
|
+
Extract reusable procedural patterns from conversation transcripts.
|
|
17
17
|
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
</goal>
|
|
18
|
+
<what-to-extract>
|
|
19
|
+
Skills are repeatable workflows, reasoning frameworks, or domain-specific protocols. Each needs a clear trigger and procedural steps.
|
|
20
|
+
Extract from successful agent patterns and user corrections. If no genuine procedures exist, return empty candidates.
|
|
21
|
+
</what-to-extract>
|
|
23
22
|
|
|
24
|
-
<what-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
-
|
|
28
|
-
- "When creating Linear issues for bugs, always include reproduction steps, severity, and affected area..."
|
|
29
|
-
- "When discussing fundraising, use this evaluation checklist..."
|
|
30
|
-
</what-is-a-skill>
|
|
31
|
-
|
|
32
|
-
<what-is-NOT-a-skill>
|
|
33
|
-
- One-off facts about the company (→ memory fact)
|
|
34
|
-
- User preferences for tone or formatting (→ memory preference)
|
|
35
|
-
- Transient requests with no reusable pattern
|
|
36
|
-
- Highly context-specific decisions that won't recur
|
|
37
|
-
</what-is-NOT-a-skill>
|
|
38
|
-
|
|
39
|
-
<extraction-rules>
|
|
40
|
-
- Extract from user requests and successful agent execution patterns
|
|
41
|
-
- Learn from user corrections (what the agent did wrong → what it should do instead)
|
|
42
|
-
- Require at least a clear trigger condition and procedural steps
|
|
43
|
-
- Keep instructions concise and actionable
|
|
44
|
-
- Do NOT generate skills from hallucinated or speculative patterns
|
|
45
|
-
- If no genuine procedural patterns exist in the transcript, return empty candidates
|
|
46
|
-
</extraction-rules>
|
|
47
|
-
|
|
48
|
-
<output-contract>
|
|
49
|
-
Return a JSON object with:
|
|
50
|
-
- candidates: array of skill candidates, each with:
|
|
51
|
-
- name: kebab-case identifier
|
|
52
|
-
- description: 1-2 sentence summary for retrieval
|
|
53
|
-
- instructions: full procedural prompt
|
|
54
|
-
- triggers: when to use this skill (array of trigger descriptions)
|
|
55
|
-
- tags: semantic tags
|
|
56
|
-
- examples: 1-2 example queries that would trigger this
|
|
57
|
-
- classification: 'skill' | 'fact' | 'preference' | 'discard'
|
|
58
|
-
- confidence: 0-1
|
|
59
|
-
- agentId: which agent this is most relevant for (null = all)
|
|
60
|
-
</output-contract>
|
|
23
|
+
<what-to-skip>
|
|
24
|
+
One-off facts (→ memory), tone preferences (→ memory), transient requests, context-specific decisions.
|
|
25
|
+
Do not hallucinate patterns. Keep instructions concise and actionable.
|
|
26
|
+
</what-to-skip>
|
|
61
27
|
</agent-instructions>`
|
|
62
28
|
|
|
63
29
|
export const SkillCandidateSchema = z.object({
|