@lota-sdk/core 0.1.15 → 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 (138) 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 +8 -7
  9. package/src/ai/definitions.ts +80 -2
  10. package/src/ai/index.ts +0 -2
  11. package/src/bifrost/bifrost.ts +2 -7
  12. package/src/config/agent-defaults.ts +31 -21
  13. package/src/config/agent-types.ts +11 -0
  14. package/src/config/constants.ts +2 -14
  15. package/src/config/debug-logger.ts +5 -1
  16. package/src/config/index.ts +3 -0
  17. package/src/config/model-constants.ts +16 -34
  18. package/src/config/search.ts +1 -15
  19. package/src/create-runtime.ts +244 -178
  20. package/src/db/cursor-pagination.ts +3 -6
  21. package/src/db/index.ts +2 -0
  22. package/src/db/memory-store.rows.ts +7 -7
  23. package/src/db/memory-store.ts +14 -18
  24. package/src/db/memory.ts +13 -13
  25. package/src/db/service.ts +153 -79
  26. package/src/db/startup.ts +6 -10
  27. package/src/db/surreal-mutation.ts +43 -0
  28. package/src/db/tables.ts +7 -0
  29. package/src/db/workstream-message-row.ts +15 -0
  30. package/src/embeddings/provider.ts +1 -1
  31. package/src/queues/context-compaction.queue.ts +15 -46
  32. package/src/queues/delayed-node-promotion.queue.ts +41 -0
  33. package/src/queues/index.ts +3 -0
  34. package/src/queues/memory-consolidation.queue.ts +16 -51
  35. package/src/queues/plan-scheduler.queue.ts +97 -0
  36. package/src/queues/post-chat-memory.queue.ts +15 -56
  37. package/src/queues/queue-factory.ts +100 -0
  38. package/src/queues/recent-activity-title-refinement.queue.ts +15 -50
  39. package/src/queues/regular-chat-memory-digest.queue.ts +16 -52
  40. package/src/queues/skill-extraction.queue.ts +15 -47
  41. package/src/queues/workstream-title-generation.queue.ts +15 -47
  42. package/src/redis/connection.ts +6 -0
  43. package/src/redis/index.ts +1 -1
  44. package/src/redis/stream-context.ts +11 -0
  45. package/src/runtime/agent-runtime-policy.ts +106 -21
  46. package/src/runtime/approval-continuation.ts +12 -6
  47. package/src/runtime/context-compaction-runtime.ts +1 -1
  48. package/src/runtime/context-compaction.ts +22 -60
  49. package/src/runtime/execution-plan.ts +22 -18
  50. package/src/runtime/graph-designer.ts +15 -0
  51. package/src/runtime/helper-model.ts +9 -197
  52. package/src/runtime/index.ts +2 -0
  53. package/src/runtime/llm-content.ts +1 -1
  54. package/src/runtime/memory-block.ts +9 -11
  55. package/src/runtime/memory-pipeline.ts +6 -9
  56. package/src/runtime/plugin-resolution.ts +35 -0
  57. package/src/runtime/plugin-types.ts +72 -0
  58. package/src/runtime/retrieval-adapters.ts +1 -1
  59. package/src/runtime/runtime-config.ts +25 -12
  60. package/src/runtime/runtime-extensions.ts +2 -2
  61. package/src/runtime/runtime-worker-registry.ts +6 -0
  62. package/src/runtime/team-consultation-orchestrator.ts +45 -28
  63. package/src/runtime/team-consultation-prompts.ts +11 -2
  64. package/src/runtime/title-helpers.ts +2 -4
  65. package/src/runtime/workstream-chat-helpers.ts +1 -1
  66. package/src/services/adaptive-playbook.service.ts +152 -0
  67. package/src/services/agent-executor.service.ts +293 -0
  68. package/src/services/artifact-provenance.service.ts +172 -0
  69. package/src/services/attachment.service.ts +6 -11
  70. package/src/services/context-compaction.service.ts +72 -55
  71. package/src/services/context-enrichment.service.ts +33 -0
  72. package/src/services/coordination-registry.service.ts +117 -0
  73. package/src/services/document-chunk.service.ts +1 -1
  74. package/src/services/domain-agent-executor.service.ts +71 -0
  75. package/src/services/execution-plan.service.ts +269 -50
  76. package/src/services/feedback-loop.service.ts +96 -0
  77. package/src/services/global-orchestrator.service.ts +148 -0
  78. package/src/services/index.ts +26 -0
  79. package/src/services/institutional-memory.service.ts +145 -0
  80. package/src/services/learned-skill.service.ts +24 -5
  81. package/src/services/memory-assessment.service.ts +3 -2
  82. package/src/services/memory-utils.ts +3 -8
  83. package/src/services/memory.service.ts +42 -59
  84. package/src/services/monitoring-window.service.ts +86 -0
  85. package/src/services/mutating-approval.service.ts +1 -1
  86. package/src/services/node-workspace.service.ts +155 -0
  87. package/src/services/notification.service.ts +39 -0
  88. package/src/services/organization-member.service.ts +11 -4
  89. package/src/services/organization.service.ts +5 -5
  90. package/src/services/ownership-dispatcher.service.ts +403 -0
  91. package/src/services/plan-approval.service.ts +1 -1
  92. package/src/services/plan-builder.service.ts +1 -0
  93. package/src/services/plan-checkpoint.service.ts +30 -2
  94. package/src/services/plan-compiler.service.ts +5 -0
  95. package/src/services/plan-coordination.service.ts +152 -0
  96. package/src/services/plan-cycle.service.ts +284 -0
  97. package/src/services/plan-deadline.service.ts +287 -0
  98. package/src/services/plan-executor.service.ts +384 -40
  99. package/src/services/plan-run.service.ts +41 -7
  100. package/src/services/plan-scheduler.service.ts +240 -0
  101. package/src/services/plan-template.service.ts +117 -0
  102. package/src/services/plan-validator.service.ts +84 -2
  103. package/src/services/plan-workspace.service.ts +83 -0
  104. package/src/services/playbook-registry.service.ts +67 -0
  105. package/src/services/plugin-executor.service.ts +103 -0
  106. package/src/services/quality-metrics.service.ts +132 -0
  107. package/src/services/recent-activity.service.ts +27 -31
  108. package/src/services/skill-resolver.service.ts +19 -0
  109. package/src/services/system-executor.service.ts +105 -0
  110. package/src/services/workstream-message.service.ts +12 -34
  111. package/src/services/workstream-plan-registry.service.ts +22 -0
  112. package/src/services/workstream-title.service.ts +3 -1
  113. package/src/services/workstream-turn-preparation.service.ts +34 -66
  114. package/src/services/workstream.service.ts +33 -55
  115. package/src/services/workstream.types.ts +9 -9
  116. package/src/services/write-intent-validator.service.ts +81 -0
  117. package/src/storage/attachment-parser.ts +1 -1
  118. package/src/storage/attachment-utils.ts +1 -1
  119. package/src/storage/generated-document-storage.service.ts +3 -2
  120. package/src/system-agents/delegated-agent-factory.ts +2 -0
  121. package/src/tools/execution-plan.tool.ts +17 -23
  122. package/src/tools/index.ts +0 -1
  123. package/src/tools/team-think.tool.ts +6 -4
  124. package/src/utils/async.ts +2 -1
  125. package/src/utils/date-time.ts +4 -32
  126. package/src/utils/env.ts +8 -0
  127. package/src/utils/errors.ts +42 -10
  128. package/src/utils/index.ts +9 -0
  129. package/src/utils/string.ts +114 -1
  130. package/src/workers/index.ts +1 -0
  131. package/src/workers/regular-chat-memory-digest.runner.ts +2 -2
  132. package/src/workers/skill-extraction.runner.ts +1 -1
  133. package/src/workers/utils/file-section-chunker.ts +2 -1
  134. package/src/workers/utils/repomix-file-sections.ts +2 -2
  135. package/src/workers/utils/sandbox-error.ts +11 -2
  136. package/src/workers/utils/workstream-message-query.ts +11 -20
  137. package/src/workers/worker-utils.ts +2 -2
  138. package/src/tools/log-hello-world.tool.ts +0 -17
@@ -1,16 +1,5 @@
1
- import { Queue, Worker } from 'bullmq'
2
-
3
- import { serverLogger } from '../config/logger'
4
- import { getRedisConnectionForBullMQ } from '../redis'
5
- import {
6
- attachWorkerEvents,
7
- DEFAULT_JOB_RETENTION,
8
- getWorkerPath,
9
- LONG_JOB_LOCK_DURATION_MS,
10
- createWorkerShutdown,
11
- registerShutdownSignals,
12
- } from '../workers/worker-utils'
13
- import type { WorkerHandle } from '../workers/worker-utils'
1
+ import { getWorkerPath, LONG_JOB_LOCK_DURATION_MS } from '../workers/worker-utils'
2
+ import { createQueueFactory } from './queue-factory'
14
3
  import {
15
4
  buildRegularChatMemoryDigestDeduplicationId,
16
5
  buildRegularChatMemoryDigestJobOptions,
@@ -20,50 +9,25 @@ export interface RegularChatMemoryDigestJob {
20
9
  orgId: string
21
10
  }
22
11
 
23
- const REGULAR_CHAT_MEMORY_DIGEST_QUEUE = 'regular-chat-memory-digest'
24
-
25
- let _regularChatMemoryDigestQueue: Queue<RegularChatMemoryDigestJob> | null = null
26
- function getRegularChatMemoryDigestQueue(): Queue<RegularChatMemoryDigestJob> {
27
- if (!_regularChatMemoryDigestQueue) {
28
- _regularChatMemoryDigestQueue = new Queue<RegularChatMemoryDigestJob>(REGULAR_CHAT_MEMORY_DIGEST_QUEUE, {
29
- connection: getRedisConnectionForBullMQ(),
30
- defaultJobOptions: { ...DEFAULT_JOB_RETENTION, attempts: 2, backoff: { type: 'exponential', delay: 5000 } },
31
- })
32
- }
33
- return _regularChatMemoryDigestQueue
34
- }
35
-
36
- export async function enqueueRegularChatMemoryDigest(job: RegularChatMemoryDigestJob) {
37
- return await getRegularChatMemoryDigestQueue().add(
38
- 'run-digest',
39
- job,
40
- buildRegularChatMemoryDigestJobOptions(job.orgId),
41
- )
12
+ const regularChatMemoryDigest = createQueueFactory<RegularChatMemoryDigestJob>({
13
+ name: 'regular-chat-memory-digest',
14
+ displayName: 'Regular chat memory digest',
15
+ jobName: 'run-digest',
16
+ concurrency: 1,
17
+ lockDuration: LONG_JOB_LOCK_DURATION_MS,
18
+ defaultJobOptions: { attempts: 2, backoff: { type: 'exponential', delay: 5000 } },
19
+ processorPath: getWorkerPath('regular-chat-memory-digest.worker.ts'),
20
+ })
21
+
22
+ export function enqueueRegularChatMemoryDigest(job: RegularChatMemoryDigestJob) {
23
+ return regularChatMemoryDigest.enqueue(job, buildRegularChatMemoryDigestJobOptions(job.orgId))
42
24
  }
43
25
 
44
26
  export async function clearRegularChatMemoryDigestDeduplicationKey(orgId: string): Promise<void> {
45
- await getRegularChatMemoryDigestQueue().removeDeduplicationKey(buildRegularChatMemoryDigestDeduplicationId(orgId))
27
+ await regularChatMemoryDigest.getQueue().removeDeduplicationKey(buildRegularChatMemoryDigestDeduplicationId(orgId))
46
28
  }
47
29
 
48
- export function startRegularChatMemoryDigestWorker(options: { registerSignals?: boolean } = {}): WorkerHandle {
49
- const { registerSignals = import.meta.main } = options
50
- const processorPath = getWorkerPath('regular-chat-memory-digest.worker.ts')
51
- const worker = new Worker(REGULAR_CHAT_MEMORY_DIGEST_QUEUE, processorPath, {
52
- connection: getRedisConnectionForBullMQ(),
53
- concurrency: 1,
54
- lockDuration: LONG_JOB_LOCK_DURATION_MS,
55
- })
56
-
57
- attachWorkerEvents(worker, 'Regular chat memory digest', serverLogger)
58
-
59
- const shutdown = createWorkerShutdown(worker, 'Regular chat memory digest', serverLogger)
60
-
61
- if (registerSignals) {
62
- registerShutdownSignals({ name: 'Regular chat memory digest', shutdown, logger: serverLogger })
63
- }
64
-
65
- return { worker, shutdown }
66
- }
30
+ export const startRegularChatMemoryDigestWorker = regularChatMemoryDigest.startWorker
67
31
 
68
32
  if (import.meta.main) {
69
33
  startRegularChatMemoryDigestWorker()
@@ -1,58 +1,26 @@
1
- import { Queue, Worker } from 'bullmq'
2
-
3
- import { serverLogger } from '../config/logger'
4
- import { getRedisConnectionForBullMQ } from '../redis'
5
- import {
6
- attachWorkerEvents,
7
- DEFAULT_JOB_RETENTION,
8
- getWorkerPath,
9
- LONG_JOB_LOCK_DURATION_MS,
10
- createWorkerShutdown,
11
- registerShutdownSignals,
12
- } from '../workers/worker-utils'
13
- import type { WorkerHandle } from '../workers/worker-utils'
1
+ import { getWorkerPath } from '../workers/worker-utils'
2
+ import { createQueueFactory } from './queue-factory'
14
3
  import { buildSkillExtractionJobOptions } from './skill-extraction.config'
15
4
 
16
5
  export interface SkillExtractionJob {
17
6
  orgId: string
18
7
  }
19
8
 
20
- const SKILL_EXTRACTION_QUEUE = 'skill-extraction'
21
-
22
- let _skillExtractionQueue: Queue<SkillExtractionJob> | null = null
23
- function getSkillExtractionQueue(): Queue<SkillExtractionJob> {
24
- if (!_skillExtractionQueue) {
25
- _skillExtractionQueue = new Queue<SkillExtractionJob>(SKILL_EXTRACTION_QUEUE, {
26
- connection: getRedisConnectionForBullMQ(),
27
- defaultJobOptions: { ...DEFAULT_JOB_RETENTION, attempts: 2, backoff: { type: 'exponential', delay: 5000 } },
28
- })
29
- }
30
- return _skillExtractionQueue
9
+ const skillExtraction = createQueueFactory<SkillExtractionJob>({
10
+ name: 'skill-extraction',
11
+ displayName: 'Skill extraction',
12
+ jobName: 'run-extraction',
13
+ concurrency: 1,
14
+ lockDuration: 600_000,
15
+ defaultJobOptions: { attempts: 2, backoff: { type: 'exponential', delay: 5000 } },
16
+ processorPath: getWorkerPath('skill-extraction.worker.ts'),
17
+ })
18
+
19
+ export function enqueueSkillExtraction(job: SkillExtractionJob) {
20
+ return skillExtraction.enqueue(job, buildSkillExtractionJobOptions(job.orgId))
31
21
  }
32
22
 
33
- export async function enqueueSkillExtraction(job: SkillExtractionJob) {
34
- return await getSkillExtractionQueue().add('run-extraction', job, buildSkillExtractionJobOptions(job.orgId))
35
- }
36
-
37
- export function startSkillExtractionWorker(options: { registerSignals?: boolean } = {}): WorkerHandle {
38
- const { registerSignals = import.meta.main } = options
39
- const processorPath = getWorkerPath('skill-extraction.worker.ts')
40
- const worker = new Worker(SKILL_EXTRACTION_QUEUE, processorPath, {
41
- connection: getRedisConnectionForBullMQ(),
42
- concurrency: 1,
43
- lockDuration: LONG_JOB_LOCK_DURATION_MS,
44
- })
45
-
46
- attachWorkerEvents(worker, 'Skill extraction', serverLogger)
47
-
48
- const shutdown = createWorkerShutdown(worker, 'Skill extraction', serverLogger)
49
-
50
- if (registerSignals) {
51
- registerShutdownSignals({ name: 'Skill extraction', shutdown, logger: serverLogger })
52
- }
53
-
54
- return { worker, shutdown }
55
- }
23
+ export const startSkillExtractionWorker = skillExtraction.startWorker
56
24
 
57
25
  if (import.meta.main) {
58
26
  startSkillExtractionWorker()
@@ -1,65 +1,33 @@
1
- import { Queue, Worker } from 'bullmq'
2
1
  import type { Job } from 'bullmq'
3
2
 
4
- import { serverLogger } from '../config/logger'
5
3
  import { ensureRecordId } from '../db/record-id'
6
4
  import { databaseService } from '../db/service'
7
- import { getRedisConnectionForBullMQ } from '../redis'
8
5
  import { workstreamTitleService } from '../services/workstream-title.service'
9
- import {
10
- attachWorkerEvents,
11
- createTracedWorkerProcessor,
12
- createWorkerShutdown,
13
- DEFAULT_JOB_RETENTION,
14
- registerShutdownSignals,
15
- } from '../workers/worker-utils'
16
- import type { WorkerHandle } from '../workers/worker-utils'
6
+ import { createQueueFactory } from './queue-factory'
17
7
 
18
8
  interface WorkstreamTitleGenerationJob {
19
9
  workstreamId: string
20
10
  sourceText: string
21
11
  }
22
12
 
23
- const WORKSTREAM_TITLE_GENERATION_QUEUE = 'workstream-title-generation'
24
-
25
- let _workstreamTitleGenerationQueue: Queue<WorkstreamTitleGenerationJob> | null = null
26
- function getWorkstreamTitleGenerationQueue(): Queue<WorkstreamTitleGenerationJob> {
27
- if (!_workstreamTitleGenerationQueue) {
28
- _workstreamTitleGenerationQueue = new Queue<WorkstreamTitleGenerationJob>(WORKSTREAM_TITLE_GENERATION_QUEUE, {
29
- connection: getRedisConnectionForBullMQ(),
30
- defaultJobOptions: { ...DEFAULT_JOB_RETENTION, attempts: 2, backoff: { type: 'exponential', delay: 2_000 } },
31
- })
32
- }
33
- return _workstreamTitleGenerationQueue
34
- }
35
-
36
- export async function enqueueWorkstreamTitleGeneration(job: WorkstreamTitleGenerationJob) {
37
- return await getWorkstreamTitleGenerationQueue().add('generate-workstream-title', job, {
38
- jobId: `workstream-title:${job.workstreamId}`,
39
- })
40
- }
41
-
42
13
  async function processWorkstreamTitleGenerationJob(job: Job<WorkstreamTitleGenerationJob>): Promise<void> {
43
14
  await databaseService.connect()
44
15
  const workstreamRef = ensureRecordId(job.data.workstreamId)
45
16
  await workstreamTitleService.generateAndPersistTitle(workstreamRef, job.data.sourceText)
46
17
  }
47
18
 
48
- export function startWorkstreamTitleGenerationWorker(options: { registerSignals?: boolean } = {}): WorkerHandle {
49
- const { registerSignals = import.meta.main } = options
50
- const worker = new Worker(
51
- WORKSTREAM_TITLE_GENERATION_QUEUE,
52
- createTracedWorkerProcessor(WORKSTREAM_TITLE_GENERATION_QUEUE, processWorkstreamTitleGenerationJob),
53
- { connection: getRedisConnectionForBullMQ(), concurrency: 2, lockDuration: 60_000 },
54
- )
55
-
56
- attachWorkerEvents(worker, 'Workstream title generation', serverLogger)
57
-
58
- const shutdown = createWorkerShutdown(worker, 'Workstream title generation', serverLogger)
59
-
60
- if (registerSignals) {
61
- registerShutdownSignals({ name: 'Workstream title generation', shutdown, logger: serverLogger })
62
- }
63
-
64
- return { worker, shutdown }
19
+ const workstreamTitleGeneration = createQueueFactory<WorkstreamTitleGenerationJob>({
20
+ name: 'workstream-title-generation',
21
+ displayName: 'Workstream title generation',
22
+ jobName: 'generate-workstream-title',
23
+ concurrency: 2,
24
+ lockDuration: 60_000,
25
+ defaultJobOptions: { attempts: 2, backoff: { type: 'exponential', delay: 2_000 } },
26
+ processor: processWorkstreamTitleGenerationJob,
27
+ })
28
+
29
+ export function enqueueWorkstreamTitleGeneration(job: WorkstreamTitleGenerationJob) {
30
+ return workstreamTitleGeneration.enqueue(job, { jobId: `workstream-title:${job.workstreamId}` })
65
31
  }
32
+
33
+ export const startWorkstreamTitleGenerationWorker = workstreamTitleGeneration.startWorker
@@ -58,6 +58,7 @@ class RedisConnectionManagerImpl implements RedisConnectionManager {
58
58
  private healthCheckInterval: ReturnType<typeof setInterval> | null = null
59
59
  private isInitialized = false
60
60
  private isClosing = false
61
+ private isHealthCheckRunning = false
61
62
 
62
63
  constructor(private readonly options: CreateRedisConnectionManagerOptions) {
63
64
  this.initializeConnection()
@@ -128,6 +129,8 @@ class RedisConnectionManagerImpl implements RedisConnectionManager {
128
129
 
129
130
  const intervalMs = this.options.healthCheckIntervalMs ?? DEFAULT_HEALTH_CHECK_INTERVAL_MS
130
131
  this.healthCheckInterval = setInterval(async () => {
132
+ if (this.isHealthCheckRunning) return
133
+ this.isHealthCheckRunning = true
131
134
  try {
132
135
  if (this.redis && this.redis.status === 'ready') {
133
136
  await this.redis.ping()
@@ -136,8 +139,11 @@ class RedisConnectionManagerImpl implements RedisConnectionManager {
136
139
  } catch (error) {
137
140
  log(this.options.logger, 'warn', `Redis health check failed: ${getErrorMessage(error)}`)
138
141
  this.isHealthy = false
142
+ } finally {
143
+ this.isHealthCheckRunning = false
139
144
  }
140
145
  }, intervalMs)
146
+ this.healthCheckInterval.unref()
141
147
  }
142
148
 
143
149
  getConnection(): IORedis {
@@ -9,7 +9,7 @@ export {
9
9
  } from './connection-accessor'
10
10
  export { withOrgMemoryLock } from './org-memory-lock'
11
11
  export { LeaseLockLostError, withRedisLeaseLock } from './redis-lease-lock'
12
- export { createWorkstreamResumableContext } from './stream-context'
12
+ export { closeSharedSubscriber, createWorkstreamResumableContext } from './stream-context'
13
13
 
14
14
  export { createRedisConnectionManager }
15
15
  export type { RedisConnectionManager }
@@ -44,6 +44,17 @@ function getSharedSubscriber(): Subscriber {
44
44
  return sharedSubscriber.subscriber
45
45
  }
46
46
 
47
+ export async function closeSharedSubscriber(): Promise<void> {
48
+ if (!sharedSubscriber) return
49
+ const { client } = sharedSubscriber
50
+ sharedSubscriber = undefined
51
+ try {
52
+ await client.quit()
53
+ } catch {
54
+ client.disconnect()
55
+ }
56
+ }
57
+
47
58
  export function createWorkstreamResumableContext() {
48
59
  const redis = getRedisConnection()
49
60
  return createResumableStreamContext({
@@ -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
  }
@@ -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
 
@@ -76,7 +76,7 @@ export function createWiredContextCompactionRuntime(deps: CreateContextCompactio
76
76
  const newEntriesText = params.newEntriesText.trim()
77
77
  if (!previousSummary && !newEntriesText) return ''
78
78
 
79
- return await helperModelRuntime.generateHelperText({
79
+ return helperModelRuntime.generateHelperText({
80
80
  tag: 'memory-block-compaction',
81
81
  createAgent: createContextCompactionAgent,
82
82
  messages: [{ role: 'user', content: buildMemoryBlockCompactionPrompt({ previousSummary, newEntriesText }) }],
@@ -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 { CHARS_PER_TOKEN_ESTIMATE, compactWhitespace, readRecord, 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,
@@ -112,20 +112,6 @@ function createStableId(prefix: string, ...parts: Array<string | number | undefi
112
112
  return `${prefix}_${hash}`
113
113
  }
114
114
 
115
- function stringifyUnknown(value: unknown): string | null {
116
- if (value === undefined) return null
117
- if (typeof value === 'string') {
118
- const normalized = value.trim()
119
- return normalized.length > 0 ? normalized : null
120
- }
121
-
122
- try {
123
- return JSON.stringify(value)
124
- } catch {
125
- return null
126
- }
127
- }
128
-
129
115
  function appendUnique(values: string[], nextValues: string[]): string[] {
130
116
  const seen = new Set(values.map((value) => compactWhitespace(value).toLowerCase()))
131
117
  const merged = [...values]
@@ -766,6 +752,24 @@ export function createContextCompactionRuntime(
766
752
  const initialPayload = JSON.stringify([...(summaryPayload ? [summaryPayload] : []), ...remainingMessages])
767
753
  const inputChars = initialPayload.length
768
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
+
769
773
  for (;;) {
770
774
  const assessment = shouldCompactHistory({
771
775
  summaryText,
@@ -774,40 +778,12 @@ export function createContextCompactionRuntime(
774
778
  })
775
779
 
776
780
  if (!assessment.shouldCompact) {
777
- const summaryPayload = buildSyntheticSummaryPayload(summaryText)
778
- const outputPayload = JSON.stringify([...(summaryPayload ? [summaryPayload] : []), ...remainingMessages])
779
- return {
780
- compacted: compactedMessages.length > 0,
781
- summaryText,
782
- ...(lastCompactedMessageId ? { lastCompactedMessageId } : {}),
783
- compactedMessages,
784
- compactedMessageCount: compactedMessages.length,
785
- remainingMessageCount: remainingMessages.length,
786
- estimatedTokens: assessment.estimatedTokens,
787
- inputChars,
788
- outputChars: outputPayload.length,
789
- state,
790
- stateDelta: mergedDelta,
791
- }
781
+ return buildEarlyExitResult(assessment.estimatedTokens)
792
782
  }
793
783
 
794
784
  const boundary = Math.max(0, remainingMessages.length - params.tailMessageCount)
795
785
  if (boundary <= 0) {
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
- }
786
+ return buildEarlyExitResult(assessment.estimatedTokens)
811
787
  }
812
788
 
813
789
  const candidatePrefix = remainingMessages.slice(0, boundary)
@@ -816,21 +792,7 @@ export function createContextCompactionRuntime(
816
792
  const sourceText = toCompactionTranscript(contextMessages)
817
793
 
818
794
  if (!compactWhitespace(sourceText)) {
819
- const summaryPayload = buildSyntheticSummaryPayload(summaryText)
820
- const outputPayload = JSON.stringify([...(summaryPayload ? [summaryPayload] : []), ...remainingMessages])
821
- return {
822
- compacted: compactedMessages.length > 0,
823
- summaryText,
824
- ...(lastCompactedMessageId ? { lastCompactedMessageId } : {}),
825
- compactedMessages,
826
- compactedMessageCount: compactedMessages.length,
827
- remainingMessageCount: remainingMessages.length,
828
- estimatedTokens: assessment.estimatedTokens,
829
- inputChars,
830
- outputChars: outputPayload.length,
831
- state,
832
- stateDelta: mergedDelta,
833
- }
795
+ return buildEarlyExitResult(assessment.estimatedTokens)
834
796
  }
835
797
 
836
798
  let compactionOutput = await compactContextMessages({