@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lota-sdk/core",
3
- "version": "0.1.30",
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.30",
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",
@@ -81,60 +81,14 @@ export const askingUserQuestionsSkill = defineSkill({
81
81
  tools: askingUserQuestionsSkillTools,
82
82
  instructions: `# asking-user-questions
83
83
 
84
- ## Description
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
- ## Details
86
+ Types: \`single-select\` (pick one), \`multi-select\` (pick many), \`input\` (free text).
87
+ Pass questions as \`{ questions: [{ question, type, options? }] }\`.
89
88
 
90
- <when-to-use>
91
- - Requirements are ambiguous and different interpretations would change the outcome.
92
- - A required identifier is missing (for example project/team/owner/issue reference).
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
- ## Description
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
- ## Details
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
- <when-to-use>
157
- - The user asks a question that requires current, external information beyond the agent's training data.
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
- const onboardingOwnerAgentId = resolveOnboardingOwnerAgentId(getLeadAgentId()) as TAgent
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: !params.onboardingActive,
199
- includeConversationSearch: !params.onboardingActive,
200
- includeMemoryRemember: !params.onboardingActive,
201
- includeOrgActionSearch: !params.onboardingActive,
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
- - Before doing multi-step work, create a contract-driven execution plan instead of tracking steps only in prose.
6
- - A workstream may have multiple active execution plans. Review all plans before creating new ones.
7
- - Plans are graph-capable workflow contracts. Every execution node must define objective, instructions, deliverables, success criteria, completion checks, retry policy, failure policy, and tool/context policy.
8
- - The runtime executor owns lifecycle truth. Do not claim that a node is complete until submitExecutionNodeResult succeeds.
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
- - Work only on nodes that are active or explicitly ready for your executor. If a node is awaiting human input or approval, stop and let the runtime resume it.
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, PROJECT_PLAN_ROUTING_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 provided here.',
81
- 'Do not ask the user for more input and do not rely on unstated external context.',
82
- 'Do not submit placeholders, partial work, or speculative outputs.',
83
- 'Before submitting, satisfy every required deliverable, success criterion, and completion check for this node.',
84
- 'Deliverables must use the exact artifact names and kinds declared in the node contract.',
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
- 'Include notes with a concise completion summary grounded in the submitted artifacts and structuredOutput.',
125
- 'Always include handoffContext for downstream execution with a durable summary, key decisions, open questions, risks, recommendations, and references when relevant.',
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), buildPlanTurnResultContractSection(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
- onboardingActive || !params.messageText
98
- ? undefined
99
- : await params.workspaceProvider?.buildRetrievedKnowledgeSection?.({
100
- workspaceId: params.orgIdString,
101
- userId: params.userIdString,
102
- query: params.messageText,
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 recentEvents = options?.includeEvents === false ? [] : await this.listEvents(run.id, 20)
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
- const executionPlanInstructionSections = await ctx.getExecutionPlanInstructionSections()
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
- try {
373
- result = await streamParams.observer.run(() =>
374
- agent.stream({ messages: modelMessages, abortSignal: agentAbortSignal }),
375
- )
376
- agentTimer.step('agent.stream()-resolved')
377
- } catch (error) {
378
- if (agentAbortSignal.aborted) {
379
- streamParams.observer.recordAbort(error)
380
- } else {
381
- streamParams.observer.recordError(error)
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: onboardingActive,
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 ?? (runParams.mode !== 'fixedWorkstreamMode' && !onboardingActive)
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' && !onboardingActive
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
- You are the regular-chat memory digest synthesizer.
16
-
17
- <goal>
18
- Given the current workspace profile summary, existing durable memories, and newly added regular-chat transcript lines,
19
- produce one updated workspace profile summary plus durable memory facts.
20
- </goal>
21
-
22
- <scope>
23
- - Input transcript contains workstream messages with source prefixes like [workstream:...].
24
- - Treat source prefixes as thread context only.
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
- You are the skill extractor.
16
+ Extract reusable procedural patterns from conversation transcripts.
17
17
 
18
- <goal>
19
- Given recent conversation transcripts, identify reusable procedural patterns that would help agents
20
- handle similar requests better in the future. Extract only genuine procedures not facts, preferences,
21
- or one-off requests.
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-is-a-skill>
25
- A skill is a reusable procedure: a repeatable workflow, reasoning framework, or domain-specific protocol
26
- that an agent can follow when encountering similar situations. Examples:
27
- - "When user asks for competitive analysis, follow this 5-step framework..."
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({