@lota-sdk/core 0.1.14 → 0.1.16

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.
Files changed (174) hide show
  1. package/infrastructure/schema/00_identity.surql +0 -2
  2. package/infrastructure/schema/01_memory.surql +1 -1
  3. package/infrastructure/schema/02_execution_plan.surql +62 -1
  4. package/infrastructure/schema/03_learned_skill.surql +1 -1
  5. package/infrastructure/schema/06_playbook.surql +25 -0
  6. package/infrastructure/schema/07_institutional_memory.surql +13 -0
  7. package/infrastructure/schema/08_quality_metrics.surql +17 -0
  8. package/package.json +9 -8
  9. package/src/ai/definitions.ts +80 -2
  10. package/src/ai/embedding-cache.ts +7 -6
  11. package/src/ai/index.ts +0 -1
  12. package/src/bifrost/bifrost.ts +14 -14
  13. package/src/config/agent-defaults.ts +32 -22
  14. package/src/config/agent-types.ts +11 -0
  15. package/src/config/constants.ts +2 -14
  16. package/src/config/debug-logger.ts +5 -1
  17. package/src/config/index.ts +3 -0
  18. package/src/config/logger.ts +7 -9
  19. package/src/config/model-constants.ts +16 -34
  20. package/src/config/search.ts +1 -15
  21. package/src/create-runtime.ts +453 -0
  22. package/src/db/cursor-pagination.ts +3 -6
  23. package/src/db/index.ts +2 -0
  24. package/src/db/memory-store.rows.ts +7 -7
  25. package/src/db/memory-store.ts +24 -24
  26. package/src/db/memory.ts +18 -16
  27. package/src/db/schema-fingerprint.ts +1 -0
  28. package/src/db/service.ts +193 -122
  29. package/src/db/startup.ts +9 -13
  30. package/src/db/surreal-mutation.ts +43 -0
  31. package/src/db/tables.ts +7 -0
  32. package/src/db/workstream-message-row.ts +15 -0
  33. package/src/embeddings/provider.ts +1 -1
  34. package/src/index.ts +1 -1
  35. package/src/queues/context-compaction.queue.ts +17 -52
  36. package/src/queues/delayed-node-promotion.queue.ts +41 -0
  37. package/src/queues/document-processor.queue.ts +7 -7
  38. package/src/queues/index.ts +3 -0
  39. package/src/queues/memory-consolidation.queue.ts +18 -54
  40. package/src/queues/plan-scheduler.queue.ts +97 -0
  41. package/src/queues/post-chat-memory.queue.ts +15 -60
  42. package/src/queues/queue-factory.ts +100 -0
  43. package/src/queues/recent-activity-title-refinement.queue.ts +15 -54
  44. package/src/queues/regular-chat-memory-digest.queue.ts +16 -55
  45. package/src/queues/skill-extraction.queue.ts +15 -50
  46. package/src/queues/workstream-title-generation.queue.ts +15 -51
  47. package/src/redis/connection.ts +12 -3
  48. package/src/redis/index.ts +2 -1
  49. package/src/redis/org-memory-lock.ts +1 -1
  50. package/src/redis/redis-lease-lock.ts +41 -8
  51. package/src/redis/stream-context.ts +11 -0
  52. package/src/runtime/agent-runtime-policy.ts +106 -21
  53. package/src/runtime/agent-stream-helpers.ts +2 -1
  54. package/src/runtime/approval-continuation.ts +12 -6
  55. package/src/runtime/context-compaction-constants.ts +1 -1
  56. package/src/runtime/context-compaction-runtime.ts +7 -5
  57. package/src/runtime/context-compaction.ts +40 -97
  58. package/src/runtime/execution-plan.ts +23 -19
  59. package/src/runtime/graph-designer.ts +15 -0
  60. package/src/runtime/helper-model.ts +10 -196
  61. package/src/runtime/index.ts +14 -1
  62. package/src/runtime/llm-content.ts +1 -1
  63. package/src/runtime/memory-block.ts +11 -12
  64. package/src/runtime/memory-pipeline.ts +26 -10
  65. package/src/runtime/plugin-resolution.ts +35 -0
  66. package/src/runtime/plugin-types.ts +73 -1
  67. package/src/runtime/retrieval-adapters.ts +1 -1
  68. package/src/runtime/runtime-config.ts +25 -12
  69. package/src/runtime/runtime-extensions.ts +91 -15
  70. package/src/runtime/runtime-worker-registry.ts +6 -0
  71. package/src/runtime/team-consultation-orchestrator.ts +45 -28
  72. package/src/runtime/team-consultation-prompts.ts +11 -2
  73. package/src/runtime/title-helpers.ts +11 -4
  74. package/src/runtime/workstream-chat-helpers.ts +6 -7
  75. package/src/runtime/workstream-routing-policy.ts +0 -30
  76. package/src/runtime/workstream-state.ts +17 -7
  77. package/src/services/adaptive-playbook.service.ts +152 -0
  78. package/src/services/agent-executor.service.ts +293 -0
  79. package/src/services/artifact-provenance.service.ts +172 -0
  80. package/src/services/attachment.service.ts +7 -12
  81. package/src/services/context-compaction.service.ts +75 -58
  82. package/src/services/context-enrichment.service.ts +33 -0
  83. package/src/services/coordination-registry.service.ts +117 -0
  84. package/src/services/document-chunk.service.ts +38 -33
  85. package/src/services/domain-agent-executor.service.ts +71 -0
  86. package/src/services/execution-plan.service.ts +271 -50
  87. package/src/services/feedback-loop.service.ts +96 -0
  88. package/src/services/global-orchestrator.service.ts +148 -0
  89. package/src/services/index.ts +26 -0
  90. package/src/services/institutional-memory.service.ts +145 -0
  91. package/src/services/learned-skill.service.ts +30 -15
  92. package/src/services/memory-assessment.service.ts +3 -2
  93. package/src/services/{memory.utils.ts → memory-utils.ts} +4 -13
  94. package/src/services/memory.service.ts +55 -69
  95. package/src/services/monitoring-window.service.ts +86 -0
  96. package/src/services/mutating-approval.service.ts +1 -1
  97. package/src/services/node-workspace.service.ts +155 -0
  98. package/src/services/notification.service.ts +39 -0
  99. package/src/services/organization-member.service.ts +12 -5
  100. package/src/services/organization.service.ts +5 -5
  101. package/src/services/ownership-dispatcher.service.ts +403 -0
  102. package/src/services/plan-approval.service.ts +1 -1
  103. package/src/services/plan-artifact.service.ts +1 -0
  104. package/src/services/plan-builder.service.ts +1 -0
  105. package/src/services/plan-checkpoint.service.ts +30 -2
  106. package/src/services/plan-compiler.service.ts +5 -0
  107. package/src/services/plan-coordination.service.ts +152 -0
  108. package/src/services/plan-cycle.service.ts +284 -0
  109. package/src/services/plan-deadline.service.ts +287 -0
  110. package/src/services/plan-executor.service.ts +386 -58
  111. package/src/services/plan-helpers.ts +15 -0
  112. package/src/services/plan-run.service.ts +41 -7
  113. package/src/services/plan-scheduler.service.ts +240 -0
  114. package/src/services/plan-template.service.ts +117 -0
  115. package/src/services/plan-validator.service.ts +87 -20
  116. package/src/services/plan-workspace.service.ts +83 -0
  117. package/src/services/playbook-registry.service.ts +67 -0
  118. package/src/services/plugin-executor.service.ts +103 -0
  119. package/src/services/quality-metrics.service.ts +132 -0
  120. package/src/services/recent-activity-title.service.ts +3 -10
  121. package/src/services/recent-activity.service.ts +33 -43
  122. package/src/services/skill-resolver.service.ts +19 -0
  123. package/src/services/system-executor.service.ts +105 -0
  124. package/src/services/workstream-message.service.ts +29 -41
  125. package/src/services/workstream-plan-registry.service.ts +22 -0
  126. package/src/services/workstream-title.service.ts +3 -9
  127. package/src/services/{workstream-turn-preparation.ts → workstream-turn-preparation.service.ts} +428 -373
  128. package/src/services/workstream-turn.ts +2 -2
  129. package/src/services/workstream.service.ts +55 -65
  130. package/src/services/workstream.types.ts +10 -19
  131. package/src/services/write-intent-validator.service.ts +81 -0
  132. package/src/storage/attachment-parser.ts +1 -1
  133. package/src/storage/attachment-storage.service.ts +4 -4
  134. package/src/storage/{attachments.utils.ts → attachment-utils.ts} +2 -5
  135. package/src/storage/generated-document-storage.service.ts +3 -2
  136. package/src/storage/index.ts +2 -2
  137. package/src/system-agents/{context-compacter.agent.ts → context-compaction.agent.ts} +4 -4
  138. package/src/system-agents/delegated-agent-factory.ts +5 -2
  139. package/src/system-agents/index.ts +8 -0
  140. package/src/system-agents/memory-reranker.agent.ts +1 -1
  141. package/src/system-agents/memory.agent.ts +1 -1
  142. package/src/system-agents/recent-activity-title-refiner.agent.ts +1 -1
  143. package/src/tools/execution-plan.tool.ts +17 -19
  144. package/src/tools/fetch-webpage.tool.ts +20 -18
  145. package/src/tools/index.ts +2 -3
  146. package/src/tools/read-file-parts.tool.ts +1 -1
  147. package/src/tools/search-web.tool.ts +18 -15
  148. package/src/tools/{search-tools.ts → search.tool.ts} +1 -1
  149. package/src/tools/team-think.tool.ts +14 -8
  150. package/src/tools/{tool-contract.ts → tool-contracts.ts} +9 -2
  151. package/src/utils/async.ts +3 -2
  152. package/src/utils/date-time.ts +4 -32
  153. package/src/utils/env.ts +8 -0
  154. package/src/utils/errors.ts +47 -0
  155. package/src/utils/hono-error-handler.ts +1 -2
  156. package/src/utils/index.ts +19 -2
  157. package/src/utils/string.ts +128 -1
  158. package/src/workers/bootstrap.ts +2 -2
  159. package/src/workers/index.ts +1 -0
  160. package/src/workers/memory-consolidation.worker.ts +12 -12
  161. package/src/workers/regular-chat-memory-digest.helpers.ts +2 -7
  162. package/src/workers/regular-chat-memory-digest.runner.ts +11 -105
  163. package/src/workers/skill-extraction.runner.ts +8 -102
  164. package/src/workers/utils/file-section-chunker.ts +6 -3
  165. package/src/workers/utils/repomix-file-sections.ts +2 -2
  166. package/src/workers/utils/sandbox-error.ts +11 -2
  167. package/src/workers/utils/workstream-message-query.ts +97 -0
  168. package/src/workers/worker-utils.ts +6 -2
  169. package/src/runtime/retrieval-pipeline.ts +0 -3
  170. package/src/runtime.ts +0 -387
  171. package/src/tools/log-hello-world.tool.ts +0 -17
  172. package/src/utils/error.ts +0 -10
  173. /package/src/services/{context-compaction-runtime.ts → context-compaction-runtime.singleton.ts} +0 -0
  174. /package/src/storage/{attachments.types.ts → attachment-types.ts} +0 -0
@@ -1,3 +1,5 @@
1
+ import type { ExecutionMode, PlanArtifactSubmission, PlanNodeSpec } from '@lota-sdk/shared'
2
+
1
3
  import { getLeadAgentId } from '../config/agent-defaults'
2
4
  import { resolveOnboardingOwnerAgentId } from '../config/workstream-defaults'
3
5
  import type { ChatMode } from './agent-types'
@@ -31,6 +33,30 @@ export interface AgentToolPolicy<TSkill extends PropertyKey> {
31
33
  includeIndexedRepository: boolean
32
34
  }
33
35
 
36
+ export const OWNERSHIP_DISPATCH_BLOCKED_TOOL_NAMES = Object.freeze([
37
+ 'conversationSearch',
38
+ 'createExecutionPlan',
39
+ 'replaceExecutionPlan',
40
+ 'submitExecutionNodeResult',
41
+ 'listExecutionPlans',
42
+ 'getExecutionPlanDetails',
43
+ 'resumeExecutionPlanRun',
44
+ 'consultSpecialist',
45
+ 'consultTeam',
46
+ 'teamThink',
47
+ ])
48
+
49
+ function buildOwnershipDispatchArtifactPayload(artifacts: PlanArtifactSubmission[]) {
50
+ return artifacts.map((artifact) => ({
51
+ name: artifact.name,
52
+ kind: artifact.kind,
53
+ pointer: artifact.pointer,
54
+ ...(artifact.schemaRef ? { schemaRef: artifact.schemaRef } : {}),
55
+ ...(artifact.description ? { description: artifact.description } : {}),
56
+ ...(artifact.payload !== undefined ? { payload: artifact.payload } : {}),
57
+ }))
58
+ }
59
+
34
60
  export function toChatMode(workstreamMode: 'direct' | 'group'): ChatMode {
35
61
  return workstreamMode === 'direct' ? 'fixedWorkstreamMode' : 'workstreamMode'
36
62
  }
@@ -160,37 +186,96 @@ export function buildWorkstreamAgentToolPolicy<TAgent extends string, TSkill ext
160
186
  }
161
187
  }
162
188
 
163
- export function buildTeamConsultationAgentToolPolicy<TAgent extends string, TSkill extends PropertyKey>(params: {
164
- agentId: TAgent
165
- blockedSkills: readonly TSkill[]
189
+ export function buildTeamConsultationAgentToolPolicy({
190
+ githubInstalled,
191
+ provideRepoTool,
192
+ blockedToolNames,
193
+ }: {
166
194
  blockedToolNames: readonly string[]
167
195
  githubInstalled: boolean
168
196
  provideRepoTool: boolean
169
- getAgentSkills: (agentId: TAgent, mode: ChatMode) => TSkill[]
170
- }): AgentToolPolicy<TSkill> & { blockedToolNames: Set<string> } {
171
- const skills = resolveActiveAgentSkills({
172
- agentId: params.agentId,
173
- workstreamMode: 'group',
174
- mode: 'fixedWorkstreamMode',
175
- onboardingActive: false,
176
- linearInstalled: false,
177
- getAgentSkills: params.getAgentSkills,
178
- }).filter((skill) => !params.blockedSkills.includes(skill))
179
-
197
+ }): AgentToolPolicy<string> & { blockedToolNames: Set<string> } {
180
198
  return {
181
199
  resolvedMode: 'fixedWorkstreamMode',
182
- skills,
183
- includeMemorySearch: true,
184
- includeConversationSearch: true,
200
+ skills: [],
201
+ includeMemorySearch: false,
202
+ includeConversationSearch: false,
185
203
  includeMemoryRemember: false,
186
- includeOrgActionSearch: true,
204
+ includeOrgActionSearch: false,
187
205
  includeMemoryBlockAppend: false,
188
- includeReadFileParts: true,
206
+ includeReadFileParts: false,
189
207
  includeInspectWebsite: false,
190
208
  includeProceedInOnboarding: false,
191
209
  includeGithubIntegration: false,
192
210
  includeIndexRepositoryByURL: false,
193
- includeIndexedRepository: params.githubInstalled && params.provideRepoTool,
194
- blockedToolNames: new Set(params.blockedToolNames),
211
+ includeIndexedRepository: githubInstalled && provideRepoTool,
212
+ blockedToolNames: new Set(blockedToolNames),
213
+ }
214
+ }
215
+
216
+ export function buildOwnershipDispatchContextSection(params: {
217
+ node: PlanNodeSpec
218
+ resolvedInput: Record<string, unknown>
219
+ inputArtifacts: PlanArtifactSubmission[]
220
+ }): string {
221
+ const payload = {
222
+ node: {
223
+ id: params.node.id,
224
+ label: params.node.label,
225
+ owner: params.node.owner,
226
+ objective: params.node.objective,
227
+ instructions: params.node.instructions,
228
+ outputSchemaRef: params.node.outputSchemaRef ?? null,
229
+ deliverables: params.node.deliverables,
230
+ successCriteria: params.node.successCriteria,
231
+ completionChecks: params.node.completionChecks,
232
+ toolPolicy: params.node.toolPolicy,
233
+ contextPolicy: params.node.contextPolicy,
234
+ },
235
+ resolvedInput: params.resolvedInput,
236
+ inputArtifacts: buildOwnershipDispatchArtifactPayload(params.inputArtifacts),
195
237
  }
238
+
239
+ return [
240
+ '<ownership-dispatch-execution>',
241
+ 'You are executing a single isolated execution-plan node.',
242
+ 'Do not ask the user questions. Do not reference any hidden or prior workstream chat history.',
243
+ 'Use only the provided node context, resolved input, and input artifacts.',
244
+ 'Return only the final structured node result that satisfies the required output contract.',
245
+ JSON.stringify(payload, null, 2),
246
+ '</ownership-dispatch-execution>',
247
+ ].join('\n')
248
+ }
249
+
250
+ export function buildOwnershipDispatchResponseGuard(params: {
251
+ node: PlanNodeSpec
252
+ executionMode?: ExecutionMode
253
+ }): string {
254
+ const mode = params.executionMode ?? 'linear'
255
+
256
+ if (mode === 'linear') {
257
+ return [
258
+ '<ownership-dispatch-result-contract>',
259
+ 'Return a single JSON object with this exact shape:',
260
+ '{"structuredOutput"?: object, "artifacts": Array<{ "name": string, "kind": "json"|"markdown"|"file"|"external-ref"|"record", "pointer": string, "schemaRef"?: string, "description"?: string, "payload"?: object|array }>, "notes"?: string}',
261
+ 'Do not wrap the JSON in markdown or code fences.',
262
+ `Node label: ${params.node.label}`,
263
+ `Required deliverables: ${params.node.deliverables.length > 0 ? params.node.deliverables.map((item) => item.name).join(', ') : 'none'}`,
264
+ '</ownership-dispatch-result-contract>',
265
+ ].join('\n')
266
+ }
267
+
268
+ return [
269
+ '<ownership-dispatch-result-contract>',
270
+ 'Produce outputs by calling the writeIntent tool for each deliverable.',
271
+ `Required deliverables: ${
272
+ params.node.deliverables
273
+ .filter((d) => d.required)
274
+ .map((d) => d.name)
275
+ .join(', ') || 'none'
276
+ }`,
277
+ 'If writeIntent returns validation_failed, correct and re-call.',
278
+ 'After all writes, return a brief summary.',
279
+ '</ownership-dispatch-result-contract>',
280
+ ].join('\n')
196
281
  }
@@ -2,13 +2,14 @@ import type { ChatMessage } from '@lota-sdk/shared'
2
2
  import type { LanguageModelUsage, UIMessageStreamOptions } from 'ai'
3
3
 
4
4
  import { agentDisplayNames, getLeadAgentDisplayName } from '../config/agent-defaults'
5
+ import { readRecord as _readRecord } from '../utils/string'
5
6
 
6
7
  export function readFiniteNumber(value: unknown): number | undefined {
7
8
  return typeof value === 'number' && Number.isFinite(value) ? value : undefined
8
9
  }
9
10
 
10
11
  export function readRecord(value: unknown): Record<string, unknown> | undefined {
11
- return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined
12
+ return _readRecord(value) ?? undefined
12
13
  }
13
14
 
14
15
  export function readOpenRouterUsageCost(usage: LanguageModelUsage): { cost?: number; upstreamInferenceCost?: number } {
@@ -45,21 +45,27 @@ export function readApprovalContinuationResponse(message: ChatMessage): Approval
45
45
  }
46
46
 
47
47
  export function isApprovalContinuationRequest(messages: ChatMessage[]): boolean {
48
- const lastAssistant = [...messages].reverse().find((message) => message.role === 'assistant')
49
- if (!lastAssistant) return false
48
+ let lastAssistantIndex = -1
49
+ for (let i = messages.length - 1; i >= 0; i--) {
50
+ if (messages[i].role === 'assistant') {
51
+ lastAssistantIndex = i
52
+ break
53
+ }
54
+ }
55
+ if (lastAssistantIndex < 0) return false
50
56
 
51
- const lastMessageIndex = messages.lastIndexOf(lastAssistant)
52
- const hasUserAfter = messages.slice(lastMessageIndex + 1).some((message) => message.role === 'user')
57
+ const hasUserAfter = messages.slice(lastAssistantIndex + 1).some((message) => message.role === 'user')
53
58
  if (hasUserAfter) return false
54
59
 
55
- return hasApprovalRespondedParts(lastAssistant)
60
+ return hasApprovalRespondedParts(messages[lastAssistantIndex])
56
61
  }
57
62
 
58
63
  const PLAN_TOOL_NAMES = new Set([
59
64
  'createExecutionPlan',
60
65
  'replaceExecutionPlan',
61
66
  'submitExecutionNodeResult',
62
- 'getActiveExecutionPlan',
67
+ 'listExecutionPlans',
68
+ 'getExecutionPlanDetails',
63
69
  'resumeExecutionPlanRun',
64
70
  ])
65
71
 
@@ -7,5 +7,5 @@ export const CONTEXT_COMPACTION_INCLUDED_TOOL_NAMES = ['userQuestions', 'proceed
7
7
  export const CONTEXT_COMPACTION_INCLUDED_TOOL_PREFIXES = ['linear'] as const
8
8
  export const MEMORY_BLOCK_COMPACTION_TRIGGER_ENTRIES = 15
9
9
  export const MEMORY_BLOCK_COMPACTION_CHUNK_ENTRIES = 10
10
- export const CONTEXT_SIZE = 200_000
10
+ export const CONTEXT_WINDOW_TOKENS = 200_000
11
11
  export const WORKSTREAM_RAW_TAIL_MESSAGES = 6
@@ -1,4 +1,4 @@
1
- import { createContextCompacterAgent } from '../system-agents/context-compacter.agent'
1
+ import { createContextCompactionAgent } from '../system-agents/context-compaction.agent'
2
2
  import {
3
3
  buildContextCompactionPrompt,
4
4
  buildMemoryBlockCompactionPrompt,
@@ -18,6 +18,8 @@ import {
18
18
  import type { GenerateHelperStructuredParams, GenerateHelperTextParams } from './helper-model'
19
19
  import { StructuredCompactionOutputSchema } from './workstream-state'
20
20
 
21
+ const CONTEXT_COMPACTION_MAX_OUTPUT_TOKENS = 512
22
+
21
23
  interface HelperModelRuntime {
22
24
  generateHelperStructured<T>(params: GenerateHelperStructuredParams<T>): Promise<T>
23
25
  generateHelperText(params: GenerateHelperTextParams): Promise<string>
@@ -32,7 +34,7 @@ interface CreateContextCompactionRuntimeDeps {
32
34
  async function runContextCompacter(helperModelRuntime: HelperModelRuntime, params: ContextCompactionRunnerParams) {
33
35
  const output = await helperModelRuntime.generateHelperStructured({
34
36
  tag: 'context-compaction',
35
- createAgent: createContextCompacterAgent,
37
+ createAgent: createContextCompactionAgent,
36
38
  messages: [
37
39
  {
38
40
  role: 'user',
@@ -74,11 +76,11 @@ export function createWiredContextCompactionRuntime(deps: CreateContextCompactio
74
76
  const newEntriesText = params.newEntriesText.trim()
75
77
  if (!previousSummary && !newEntriesText) return ''
76
78
 
77
- return await helperModelRuntime.generateHelperText({
79
+ return helperModelRuntime.generateHelperText({
78
80
  tag: 'memory-block-compaction',
79
- createAgent: createContextCompacterAgent,
81
+ createAgent: createContextCompactionAgent,
80
82
  messages: [{ role: 'user', content: buildMemoryBlockCompactionPrompt({ previousSummary, newEntriesText }) }],
81
- maxOutputTokens: 512,
83
+ maxOutputTokens: CONTEXT_COMPACTION_MAX_OUTPUT_TOKENS,
82
84
  })
83
85
  }
84
86
 
@@ -2,7 +2,7 @@ import { createHash, randomUUID } from 'node:crypto'
2
2
 
3
3
  import type { ChatMessage } from '@lota-sdk/shared'
4
4
 
5
- import { readString } from '../utils/string'
5
+ import { CHARS_PER_TOKEN_ESTIMATE, compactWhitespace, readRecord, readString, stringifyUnknown } from '../utils/string'
6
6
  import {
7
7
  COMPACTION_CHUNK_MAX_CHARS,
8
8
  CONTEXT_COMPACTION_INCLUDED_TOOL_NAMES,
@@ -15,14 +15,8 @@ import {
15
15
  import {
16
16
  StructuredCompactionOutputSchema,
17
17
  WorkstreamStateDeltaSchema,
18
- WORKSTREAM_STATE_MAX_ACTIVE_CONSTRAINTS,
19
- WORKSTREAM_STATE_MAX_AGENT_CONTRIBUTIONS,
20
- WORKSTREAM_STATE_MAX_ARTIFACTS,
21
- WORKSTREAM_STATE_MAX_KEY_DECISIONS,
22
- WORKSTREAM_STATE_MAX_OPEN_QUESTIONS,
23
- WORKSTREAM_STATE_MAX_RISKS,
24
- WORKSTREAM_STATE_MAX_TASKS,
25
18
  WorkstreamStateSchema,
19
+ applyWorkstreamStateCaps,
26
20
  createEmptyWorkstreamState,
27
21
  parseStructuredWorkstreamStateDelta,
28
22
  } from './workstream-state'
@@ -99,15 +93,11 @@ const PROMPT_INJECTION_PATTERN =
99
93
  /\b(ignore (all )?(previous|prior|system|developer)? instructions?|system prompt|developer prompt|tool override|jailbreak|role ?override|do not follow|bypass)\b/i
100
94
 
101
95
  function estimateTokens(text: string): number {
102
- return Math.ceil(text.length / 3)
103
- }
104
-
105
- function normalizeWhitespace(value: string): string {
106
- return value.replace(/\s+/g, ' ').trim()
96
+ return Math.ceil(text.length / CHARS_PER_TOKEN_ESTIMATE)
107
97
  }
108
98
 
109
99
  function sanitizeStateText(value: string): string | null {
110
- const normalized = normalizeWhitespace(value)
100
+ const normalized = compactWhitespace(value)
111
101
  if (!normalized) return null
112
102
  if (PROMPT_INJECTION_PATTERN.test(normalized)) return null
113
103
  return normalized
@@ -116,36 +106,18 @@ function sanitizeStateText(value: string): string | null {
116
106
  function createStableId(prefix: string, ...parts: Array<string | number | undefined>): string {
117
107
  const payload = parts
118
108
  .map((part) => (part === undefined ? '' : String(part)))
119
- .map((part) => normalizeWhitespace(part))
109
+ .map((part) => compactWhitespace(part))
120
110
  .join('|')
121
111
  const hash = createHash('sha1').update(`${prefix}|${payload}`).digest('hex').slice(0, 20)
122
112
  return `${prefix}_${hash}`
123
113
  }
124
114
 
125
- function readRecord(value: unknown): Record<string, unknown> | null {
126
- return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : null
127
- }
128
-
129
- function stringifyUnknown(value: unknown): string | null {
130
- if (value === undefined) return null
131
- if (typeof value === 'string') {
132
- const normalized = value.trim()
133
- return normalized.length > 0 ? normalized : null
134
- }
135
-
136
- try {
137
- return JSON.stringify(value)
138
- } catch {
139
- return null
140
- }
141
- }
142
-
143
115
  function appendUnique(values: string[], nextValues: string[]): string[] {
144
- const seen = new Set(values.map((value) => normalizeWhitespace(value).toLowerCase()))
116
+ const seen = new Set(values.map((value) => compactWhitespace(value).toLowerCase()))
145
117
  const merged = [...values]
146
118
 
147
119
  for (const value of nextValues) {
148
- const normalized = normalizeWhitespace(value)
120
+ const normalized = compactWhitespace(value)
149
121
  if (!normalized) continue
150
122
  const key = normalized.toLowerCase()
151
123
  if (seen.has(key)) continue
@@ -395,7 +367,7 @@ export function mergeStateDelta(
395
367
  const normalizedRationale = sanitizeStateText(decision.rationale)
396
368
  if (!normalizedDecision || !normalizedRationale) continue
397
369
 
398
- const sourceIds = [...new Set(decision.sourceMessageIds.map((id) => normalizeWhitespace(id)).filter(Boolean))]
370
+ const sourceIds = [...new Set(decision.sourceMessageIds.map((id) => compactWhitespace(id)).filter(Boolean))]
399
371
  const decisionId = createStableId('decision', normalizedDecision, normalizedRationale, sourceIds.sort().join('|'))
400
372
  const alreadyExists = state.keyDecisions.some((item) => item.id === decisionId)
401
373
  if (alreadyExists) continue
@@ -403,7 +375,7 @@ export function mergeStateDelta(
403
375
  id: decisionId,
404
376
  decision: normalizedDecision,
405
377
  rationale: normalizedRationale,
406
- agent: normalizeWhitespace(decision.agent),
378
+ agent: compactWhitespace(decision.agent),
407
379
  sourceMessageIds: sourceIds,
408
380
  confidence: decision.confidence,
409
381
  timestamp,
@@ -417,11 +389,9 @@ export function mergeStateDelta(
417
389
  if (!title) continue
418
390
 
419
391
  const externalId = sanitizeStateText(update.externalId ?? '')
420
- const owner = normalizeWhitespace(update.owner)
392
+ const owner = compactWhitespace(update.owner)
421
393
  const taskId = externalId ? createStableId('task-external', externalId) : createStableId('task', title, owner)
422
- const sourceMessageIds = [
423
- ...new Set(update.sourceMessageIds.map((id) => normalizeWhitespace(id)).filter(Boolean)),
424
- ]
394
+ const sourceMessageIds = [...new Set(update.sourceMessageIds.map((id) => compactWhitespace(id)).filter(Boolean))]
425
395
  const existingIndex = state.tasks.findIndex((task) => task.id === taskId)
426
396
  const nextTask = {
427
397
  id: taskId,
@@ -455,7 +425,7 @@ export function mergeStateDelta(
455
425
  const artifactId = createStableId('artifact', name, pointer)
456
426
  const exists = state.artifacts.some((item) => item.id === artifactId)
457
427
  if (exists) continue
458
- state.artifacts.push({ id: artifactId, name, type: normalizeWhitespace(artifact.type), pointer, timestamp })
428
+ state.artifacts.push({ id: artifactId, name, type: compactWhitespace(artifact.type), pointer, timestamp })
459
429
  }
460
430
  }
461
431
 
@@ -495,13 +465,7 @@ export function mergeStateDelta(
495
465
  state.approvalNote = sanitizeStateText(delta.approvalNote) ?? undefined
496
466
  }
497
467
 
498
- state.keyDecisions = state.keyDecisions.slice(-WORKSTREAM_STATE_MAX_KEY_DECISIONS)
499
- state.activeConstraints = state.activeConstraints.slice(-WORKSTREAM_STATE_MAX_ACTIVE_CONSTRAINTS)
500
- state.tasks = state.tasks.slice(-WORKSTREAM_STATE_MAX_TASKS)
501
- state.openQuestions = state.openQuestions.slice(-WORKSTREAM_STATE_MAX_OPEN_QUESTIONS)
502
- state.risks = state.risks.slice(-WORKSTREAM_STATE_MAX_RISKS)
503
- state.artifacts = state.artifacts.slice(-WORKSTREAM_STATE_MAX_ARTIFACTS)
504
- state.agentContributions = state.agentContributions.slice(-WORKSTREAM_STATE_MAX_AGENT_CONTRIBUTIONS)
468
+ applyWorkstreamStateCaps(state)
505
469
 
506
470
  return WorkstreamStateSchema.parse(state)
507
471
  }
@@ -727,9 +691,12 @@ export function createContextCompactionRuntime(
727
691
  return summaryMessage ? [summaryMessage, ...liveMessages] : [...liveMessages]
728
692
  }
729
693
 
694
+ const CONTEXT_OUTPUT_RESERVE_MAX_RATIO = 0.35
695
+ const CONTEXT_SAFETY_MARGIN_MAX_RATIO = 0.1
696
+
730
697
  const estimateThreshold = (contextSize = 256_000): number => {
731
- const reservedOutput = Math.min(outputReserveTokens, Math.floor(contextSize * 0.35))
732
- const safetyMargin = Math.min(safetyMarginTokens, Math.floor(contextSize * 0.1))
698
+ const reservedOutput = Math.min(outputReserveTokens, Math.floor(contextSize * CONTEXT_OUTPUT_RESERVE_MAX_RATIO))
699
+ const safetyMargin = Math.min(safetyMarginTokens, Math.floor(contextSize * CONTEXT_SAFETY_MARGIN_MAX_RATIO))
733
700
  const reservedThreshold = contextSize - (reservedOutput + safetyMargin)
734
701
  const ratioThreshold = Math.floor(contextSize * thresholdRatio)
735
702
  return Math.max(1_000, Math.min(contextSize - 1, Math.min(reservedThreshold, ratioThreshold)))
@@ -785,6 +752,24 @@ export function createContextCompactionRuntime(
785
752
  const initialPayload = JSON.stringify([...(summaryPayload ? [summaryPayload] : []), ...remainingMessages])
786
753
  const inputChars = initialPayload.length
787
754
 
755
+ const buildEarlyExitResult = (estimatedTokens: number): CompactHistoryResult => {
756
+ const exitSummaryPayload = buildSyntheticSummaryPayload(summaryText)
757
+ const outputPayload = JSON.stringify([...(exitSummaryPayload ? [exitSummaryPayload] : []), ...remainingMessages])
758
+ return {
759
+ compacted: compactedMessages.length > 0,
760
+ summaryText,
761
+ ...(lastCompactedMessageId ? { lastCompactedMessageId } : {}),
762
+ compactedMessages,
763
+ compactedMessageCount: compactedMessages.length,
764
+ remainingMessageCount: remainingMessages.length,
765
+ estimatedTokens,
766
+ inputChars,
767
+ outputChars: outputPayload.length,
768
+ state,
769
+ stateDelta: mergedDelta,
770
+ }
771
+ }
772
+
788
773
  for (;;) {
789
774
  const assessment = shouldCompactHistory({
790
775
  summaryText,
@@ -793,40 +778,12 @@ export function createContextCompactionRuntime(
793
778
  })
794
779
 
795
780
  if (!assessment.shouldCompact) {
796
- const summaryPayload = buildSyntheticSummaryPayload(summaryText)
797
- const outputPayload = JSON.stringify([...(summaryPayload ? [summaryPayload] : []), ...remainingMessages])
798
- return {
799
- compacted: compactedMessages.length > 0,
800
- summaryText,
801
- ...(lastCompactedMessageId ? { lastCompactedMessageId } : {}),
802
- compactedMessages,
803
- compactedMessageCount: compactedMessages.length,
804
- remainingMessageCount: remainingMessages.length,
805
- estimatedTokens: assessment.estimatedTokens,
806
- inputChars,
807
- outputChars: outputPayload.length,
808
- state,
809
- stateDelta: mergedDelta,
810
- }
781
+ return buildEarlyExitResult(assessment.estimatedTokens)
811
782
  }
812
783
 
813
784
  const boundary = Math.max(0, remainingMessages.length - params.tailMessageCount)
814
785
  if (boundary <= 0) {
815
- const summaryPayload = buildSyntheticSummaryPayload(summaryText)
816
- const outputPayload = JSON.stringify([...(summaryPayload ? [summaryPayload] : []), ...remainingMessages])
817
- return {
818
- compacted: compactedMessages.length > 0,
819
- summaryText,
820
- ...(lastCompactedMessageId ? { lastCompactedMessageId } : {}),
821
- compactedMessages,
822
- compactedMessageCount: compactedMessages.length,
823
- remainingMessageCount: remainingMessages.length,
824
- estimatedTokens: assessment.estimatedTokens,
825
- inputChars,
826
- outputChars: outputPayload.length,
827
- state,
828
- stateDelta: mergedDelta,
829
- }
786
+ return buildEarlyExitResult(assessment.estimatedTokens)
830
787
  }
831
788
 
832
789
  const candidatePrefix = remainingMessages.slice(0, boundary)
@@ -834,22 +791,8 @@ export function createContextCompactionRuntime(
834
791
  const contextMessages = messagesToCompact.map(toContextMessageFromChatMessage)
835
792
  const sourceText = toCompactionTranscript(contextMessages)
836
793
 
837
- if (!normalizeWhitespace(sourceText)) {
838
- const summaryPayload = buildSyntheticSummaryPayload(summaryText)
839
- const outputPayload = JSON.stringify([...(summaryPayload ? [summaryPayload] : []), ...remainingMessages])
840
- return {
841
- compacted: compactedMessages.length > 0,
842
- summaryText,
843
- ...(lastCompactedMessageId ? { lastCompactedMessageId } : {}),
844
- compactedMessages,
845
- compactedMessageCount: compactedMessages.length,
846
- remainingMessageCount: remainingMessages.length,
847
- estimatedTokens: assessment.estimatedTokens,
848
- inputChars,
849
- outputChars: outputPayload.length,
850
- state,
851
- stateDelta: mergedDelta,
852
- }
794
+ if (!compactWhitespace(sourceText)) {
795
+ return buildEarlyExitResult(assessment.estimatedTokens)
853
796
  }
854
797
 
855
798
  let compactionOutput = await compactContextMessages({
@@ -1,17 +1,19 @@
1
1
  import type { SerializableExecutionPlan } from '@lota-sdk/shared'
2
2
 
3
- export const EXECUTION_PLAN_AGENT_PROTOCOL_PROMPT = `<execution-plan-protocol>
3
+ const EXECUTION_PLAN_AGENT_PROTOCOL_PROMPT = `<execution-plan-protocol>
4
4
  - Before doing multi-step work, create a contract-driven execution plan instead of tracking steps only in prose.
5
+ - A workstream may have multiple active execution plans. Review all plans before creating new ones.
5
6
  - 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.
6
7
  - The runtime executor owns lifecycle truth. Do not claim that a node is complete until submitExecutionNodeResult succeeds.
7
- - Use execution-plan tools to create, replace, inspect, submit node results, and resume runs.
8
- - Treat the active execution run in <execution-plan-state> as authoritative. Do not mutate run or node status in prose.
8
+ - Use execution-plan tools to create, replace, inspect, and resume runs.
9
+ - Visible workstream agents do not manually submit node results; dispatched execution nodes are completed by the runtime executor.
10
+ - Treat the active execution runs in <execution-plan-state> as authoritative. Do not mutate run or node status in prose.
9
11
  - 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.
10
12
  - If the graph, contracts, or success criteria materially change, replace the plan instead of silently drifting.
11
13
  </execution-plan-protocol>`
12
14
 
13
- export function formatExecutionPlanForPrompt(plan: SerializableExecutionPlan | null | undefined): string | undefined {
14
- if (!plan) return undefined
15
+ function formatExecutionPlansForPrompt(plans: SerializableExecutionPlan[]): string | undefined {
16
+ if (plans.length === 0) return undefined
15
17
 
16
18
  const payload = {
17
19
  policy: {
@@ -21,46 +23,48 @@ export function formatExecutionPlanForPrompt(plan: SerializableExecutionPlan | n
21
23
  artifactsAreFirstClassOutputs: true,
22
24
  checkpointRecoveryEnabled: true,
23
25
  },
24
- plan,
26
+ activePlans: plans,
27
+ planCount: plans.length,
25
28
  }
26
29
 
27
30
  return ['<execution-plan-state>', JSON.stringify(payload, null, 2), '</execution-plan-state>'].join('\n')
28
31
  }
29
32
 
30
33
  export function buildExecutionPlanInstructionSections(
31
- plan: SerializableExecutionPlan | null | undefined,
34
+ plans: SerializableExecutionPlan[] | null | undefined,
32
35
  ): string[] | undefined {
36
+ const normalized = plans ?? []
33
37
  const sections = [EXECUTION_PLAN_AGENT_PROTOCOL_PROMPT]
34
- const executionPlanStateSection = formatExecutionPlanForPrompt(plan)
35
- if (executionPlanStateSection) {
36
- sections.push(executionPlanStateSection)
38
+ const stateSection = formatExecutionPlansForPrompt(normalized)
39
+ if (stateSection) {
40
+ sections.push(stateSection)
37
41
  }
38
42
  return sections
39
43
  }
40
44
 
41
45
  export function createExecutionPlanInstructionSectionCache(params: {
42
46
  disabled?: boolean
43
- loadPlan: () => Promise<SerializableExecutionPlan | null | undefined>
47
+ loadPlans: () => Promise<SerializableExecutionPlan[]>
44
48
  }) {
45
- let planPromise: Promise<SerializableExecutionPlan | null | undefined> | null = null
49
+ let plansPromise: Promise<SerializableExecutionPlan[]> | null = null
46
50
  let sectionsPromise: Promise<string[] | undefined> | null = null
47
51
 
48
52
  return {
49
53
  invalidate() {
50
- planPromise = null
54
+ plansPromise = null
51
55
  sectionsPromise = null
52
56
  },
53
- async getPlan(): Promise<SerializableExecutionPlan | null | undefined> {
54
- if (params.disabled) return undefined
57
+ async getPlans(): Promise<SerializableExecutionPlan[]> {
58
+ if (params.disabled) return []
55
59
 
56
- planPromise ??= params.loadPlan()
57
- return await planPromise
60
+ plansPromise ??= params.loadPlans()
61
+ return plansPromise
58
62
  },
59
63
  async getSections(): Promise<string[] | undefined> {
60
64
  if (params.disabled) return undefined
61
65
 
62
- sectionsPromise ??= this.getPlan().then((plan) => buildExecutionPlanInstructionSections(plan))
63
- return await sectionsPromise
66
+ sectionsPromise ??= this.getPlans().then((plans) => buildExecutionPlanInstructionSections(plans))
67
+ return sectionsPromise
64
68
  },
65
69
  }
66
70
  }
@@ -0,0 +1,15 @@
1
+ import type { GraphDesignRequest, GraphDesignResponse } from '@lota-sdk/shared'
2
+
3
+ export interface GraphDesigner {
4
+ designGraph(request: GraphDesignRequest): Promise<GraphDesignResponse>
5
+ }
6
+
7
+ let _graphDesigner: GraphDesigner | null = null
8
+
9
+ export function configureGraphDesigner(designer: GraphDesigner): void {
10
+ _graphDesigner = designer
11
+ }
12
+
13
+ export function getGraphDesigner(): GraphDesigner | null {
14
+ return _graphDesigner
15
+ }