@lota-sdk/core 0.1.24 → 0.1.25

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 (74) hide show
  1. package/package.json +2 -2
  2. package/src/ai/definitions.ts +5 -59
  3. package/src/ai-gateway/ai-gateway.ts +36 -28
  4. package/src/ai-gateway/cache-headers.ts +9 -0
  5. package/src/config/model-constants.ts +6 -2
  6. package/src/create-runtime.ts +1 -17
  7. package/src/db/memory-types.ts +13 -8
  8. package/src/db/memory.ts +74 -53
  9. package/src/queues/autonomous-job.queue.ts +1 -8
  10. package/src/queues/context-compaction.queue.ts +2 -2
  11. package/src/queues/index.ts +2 -6
  12. package/src/queues/organization-learning.queue.ts +78 -0
  13. package/src/queues/plan-agent-heartbeat.queue.ts +10 -16
  14. package/src/queues/title-generation.queue.ts +62 -0
  15. package/src/runtime/agent-prompt-context.ts +0 -18
  16. package/src/runtime/agent-runtime-policy.ts +9 -2
  17. package/src/runtime/context-compaction-constants.ts +4 -2
  18. package/src/runtime/context-compaction.ts +135 -118
  19. package/src/runtime/memory-pipeline.ts +70 -1
  20. package/src/runtime/memory-prompts-fact.ts +16 -0
  21. package/src/runtime/plugin-resolution.ts +3 -2
  22. package/src/runtime/plugin-types.ts +1 -42
  23. package/src/runtime/post-turn-side-effects.ts +212 -0
  24. package/src/runtime/runtime-config.ts +0 -13
  25. package/src/runtime/runtime-extensions.ts +10 -16
  26. package/src/runtime/runtime-worker-registry.ts +8 -19
  27. package/src/runtime/social-chat-agent-runner.ts +119 -0
  28. package/src/runtime/social-chat-history.ts +110 -0
  29. package/src/runtime/social-chat-prompts.ts +58 -0
  30. package/src/runtime/social-chat.ts +104 -340
  31. package/src/runtime/specialist-runner.ts +18 -0
  32. package/src/runtime/workstream-chat-helpers.ts +19 -0
  33. package/src/runtime/workstream-plan-turn.ts +195 -0
  34. package/src/runtime/workstream-state.ts +11 -8
  35. package/src/runtime/workstream-turn-context.ts +183 -0
  36. package/src/services/autonomous-job.service.ts +1 -8
  37. package/src/services/execution-plan.service.ts +205 -334
  38. package/src/services/index.ts +1 -4
  39. package/src/services/memory.service.ts +54 -44
  40. package/src/services/ownership-dispatcher.service.ts +2 -19
  41. package/src/services/plan-completion-side-effects.ts +80 -0
  42. package/src/services/plan-event-delivery.service.ts +1 -1
  43. package/src/services/plan-executor.service.ts +42 -190
  44. package/src/services/plan-node-spec.ts +60 -0
  45. package/src/services/plan-run-data.ts +88 -0
  46. package/src/services/plan-validator.service.ts +10 -8
  47. package/src/services/workstream-constants.ts +2 -0
  48. package/src/services/workstream-title.service.ts +1 -1
  49. package/src/services/workstream-turn-preparation.service.ts +208 -715
  50. package/src/services/workstream.service.ts +162 -192
  51. package/src/services/workstream.types.ts +12 -44
  52. package/src/system-agents/regular-chat-memory-digest.agent.ts +3 -0
  53. package/src/tools/execution-plan.tool.ts +7 -6
  54. package/src/tools/remember-memory.tool.ts +7 -10
  55. package/src/tools/research-topic.tool.ts +1 -1
  56. package/src/tools/team-think.tool.ts +1 -1
  57. package/src/tools/user-questions.tool.ts +1 -1
  58. package/src/utils/autonomous-job-ids.ts +7 -0
  59. package/src/workers/organization-learning.worker.ts +31 -0
  60. package/src/workers/regular-chat-memory-digest.runner.ts +9 -3
  61. package/src/workers/skill-extraction.runner.ts +2 -2
  62. package/src/queues/recent-activity-title-refinement.queue.ts +0 -30
  63. package/src/queues/regular-chat-memory-digest.config.ts +0 -12
  64. package/src/queues/regular-chat-memory-digest.queue.ts +0 -34
  65. package/src/queues/skill-extraction.config.ts +0 -9
  66. package/src/queues/skill-extraction.queue.ts +0 -27
  67. package/src/queues/workstream-title-generation.queue.ts +0 -33
  68. package/src/services/context-enrichment.service.ts +0 -33
  69. package/src/services/coordination-registry.service.ts +0 -117
  70. package/src/services/domain-agent-executor.service.ts +0 -71
  71. package/src/services/memory-assessment.service.ts +0 -44
  72. package/src/services/playbook-registry.service.ts +0 -67
  73. package/src/workers/regular-chat-memory-digest.worker.ts +0 -22
  74. package/src/workers/skill-extraction.worker.ts +0 -22
@@ -4,7 +4,6 @@ import { z } from 'zod'
4
4
  import type { RecordIdRef } from '../db/record-id'
5
5
  import { recordIdToString } from '../db/record-id'
6
6
  import { TABLES } from '../db/tables'
7
- import { assessMemoryImportance, clampMemoryImportance } from '../services/memory-assessment.service'
8
7
  import { memoryService } from '../services/memory.service'
9
8
  import { safeEnqueue } from '../utils/async'
10
9
 
@@ -29,17 +28,15 @@ export function createRememberMemoryTool({ orgId, agentName }: { orgId: RecordId
29
28
  const orgIdString = recordIdToString(orgId, TABLES.ORGANIZATION)
30
29
  void safeEnqueue(
31
30
  async () => {
32
- const assessment = await assessMemoryImportance({
33
- content: trimmed,
34
- targetScope,
35
- tag: 'memory-importance-assessment',
36
- })
37
-
31
+ const assessment = await memoryService.assessMemoryCandidate({ orgId: orgIdString, content: trimmed })
32
+ if (!assessment) {
33
+ return
34
+ }
38
35
  if (assessment.classification === 'transient' && assessment.durability === 'ephemeral') {
39
36
  return
40
37
  }
41
38
 
42
- const importance = Math.round(clampMemoryImportance(assessment.importance) * 100) / 100
39
+ const importance = Math.round(assessment.importance * 100) / 100
43
40
 
44
41
  if (targetScope === 'global') {
45
42
  await memoryService.createOrganizationMemory({
@@ -51,7 +48,7 @@ export function createRememberMemoryTool({ orgId, agentName }: { orgId: RecordId
51
48
  source: 'agent_tool',
52
49
  agentName,
53
50
  memoryScope: 'global',
54
- importanceSource: 'model_assessed',
51
+ importanceSource: 'extraction_assessed',
55
52
  durability: assessment.durability,
56
53
  classification: assessment.classification,
57
54
  rationale: assessment.rationale,
@@ -69,7 +66,7 @@ export function createRememberMemoryTool({ orgId, agentName }: { orgId: RecordId
69
66
  metadata: {
70
67
  source: 'agent_tool',
71
68
  memoryScope: 'agent',
72
- importanceSource: 'model_assessed',
69
+ importanceSource: 'extraction_assessed',
73
70
  durability: assessment.durability,
74
71
  classification: assessment.classification,
75
72
  rationale: assessment.rationale,
@@ -12,7 +12,7 @@ import { searchWebTool } from './search-web.tool'
12
12
  export const researchTopicTool = createDelegatedAgentTool({
13
13
  id: 'researchTopic',
14
14
  description:
15
- 'Delegate a research task to a dedicated research agent that searches the web, fetches pages, and returns a synthesized markdown report. Call multiple instances in parallel for broad research across different topics.',
15
+ 'Delegate a research task to a dedicated research agent that searches the web, fetches pages, and returns a synthesized markdown report.',
16
16
  model: () => aiGatewayChatModel(OPENROUTER_WEB_RESEARCH_MODEL_ID),
17
17
  providerOptions: OPENROUTER_MEDIUM_REASONING_PROVIDER_OPTIONS,
18
18
  headers: buildAiGatewayStrictSemanticCacheHeaders('researchTopic'),
@@ -18,7 +18,7 @@ import type { ReadableUploadMetadata } from '../services/attachment.service'
18
18
  async function buildTeamThinkAgentTools(
19
19
  params: LotaRuntimeTeamThinkToolsParams,
20
20
  ): Promise<{ tools: Record<string, unknown> }> {
21
- const builder = getRuntimeAdapters().workstream?.buildTeamThinkAgentTools
21
+ const builder = getRuntimeAdapters().buildTeamThinkAgentTools
22
22
  if (!builder) {
23
23
  return { tools: {} }
24
24
  }
@@ -9,7 +9,7 @@ export const userQuestionsTool = {
9
9
  create: () =>
10
10
  tool({
11
11
  description:
12
- 'Present structured questions to the user and wait for their response. The chat UI already renders the questions, so do not repeat them as assistant text. Use this when you need clarification or input from the user. Execution stops after this tool is called.',
12
+ 'Ask the user structured questions. UI renders them; do not repeat them in text. Turn ends after this call.',
13
13
  inputSchema: UserQuestionsArgsSchema,
14
14
  execute: async (_args: UserQuestionsArgs) => {
15
15
  return 'Questions have been presented to the user in the chat UI. Do NOT restate them in text, and do NOT generate further text or tool calls. Your turn is complete.'
@@ -0,0 +1,7 @@
1
+ export function encodeBullmqId(raw: string): string {
2
+ return Buffer.from(raw).toString('base64url')
3
+ }
4
+
5
+ export function buildAutonomousAtJobId(autonomousJobId: string): string {
6
+ return `autonomous-at-${encodeBullmqId(autonomousJobId)}`
7
+ }
@@ -0,0 +1,31 @@
1
+ import type { SandboxedJob } from 'bullmq'
2
+
3
+ import { serverLogger } from '../config/logger'
4
+ import type { OrganizationLearningJob } from '../queues/organization-learning.queue'
5
+ import { initializeSandboxedWorkerRuntime } from './bootstrap'
6
+ import { runRegularChatMemoryDigest } from './regular-chat-memory-digest.runner'
7
+ import { runSkillExtraction } from './skill-extraction.runner'
8
+ import { toSandboxedWorkerError } from './utils/sandbox-error'
9
+ import { createTracedWorkerProcessor } from './worker-utils'
10
+
11
+ await initializeSandboxedWorkerRuntime()
12
+
13
+ // One sandboxed worker handles both organization-learning branches so the
14
+ // queue can dispatch digest and skill jobs without separate worker images.
15
+ const handler = async (job: SandboxedJob<OrganizationLearningJob>) => {
16
+ try {
17
+ if (job.data.kind === 'regular-chat-memory-digest') {
18
+ await runRegularChatMemoryDigest(job.data)
19
+ return
20
+ }
21
+ await runSkillExtraction(job.data)
22
+ } catch (error) {
23
+ const message =
24
+ job.data.kind === 'regular-chat-memory-digest' ? 'Regular chat memory digest failed' : 'Skill extraction failed'
25
+ const serialized = toSandboxedWorkerError(error, message)
26
+ serverLogger.error`${serialized.message}`
27
+ throw serialized
28
+ }
29
+ }
30
+
31
+ export default createTracedWorkerProcessor('organization-learning', handler)
@@ -9,8 +9,8 @@ import { TABLES } from '../db/tables'
9
9
  import {
10
10
  clearRegularChatMemoryDigestDeduplicationKey,
11
11
  enqueueRegularChatMemoryDigest,
12
- } from '../queues/regular-chat-memory-digest.queue'
13
- import type { RegularChatMemoryDigestJob } from '../queues/regular-chat-memory-digest.queue'
12
+ } from '../queues/organization-learning.queue'
13
+ import type { RegularChatMemoryDigestJob } from '../queues/organization-learning.queue'
14
14
  import { createHelperModelRuntime } from '../runtime/helper-model'
15
15
  import { getRuntimeAdapters, withConfiguredWorkspaceMemoryLock } from '../runtime/runtime-extensions'
16
16
  import { memoryService } from '../services/memory.service'
@@ -26,6 +26,9 @@ import {
26
26
  } from './utils/workstream-message-query'
27
27
  import type { DigestCursor, DigestMessage } from './utils/workstream-message-query'
28
28
 
29
+ // Onboarding extracts memory immediately inside the turn flow. This delayed
30
+ // runner handles the regular-chat path after onboarding so longer transcripts
31
+ // can be digested into durable memory and profile projections in the background.
29
32
  const StructuredProfilePatchSchema = z.record(z.string(), z.unknown()).default({})
30
33
 
31
34
  const REGULAR_CHAT_MEMORY_DIGEST_TIMEOUT_MS = 10 * 60 * 1000
@@ -36,6 +39,9 @@ const ExtractedFactSchema = z.object({
36
39
  type: z.enum(['fact', 'preference', 'decision']),
37
40
  confidence: z.number().min(0).max(1),
38
41
  durability: z.enum(['core', 'standard', 'ephemeral']),
42
+ importance: z.number().min(0).max(1),
43
+ classification: z.enum(['durable', 'transient', 'uncertain']),
44
+ rationale: z.string().trim().min(1),
39
45
  })
40
46
 
41
47
  const RegularChatMemoryDigestOutputSchema = z.object({
@@ -151,7 +157,7 @@ export async function runRegularChatMemoryDigest(
151
157
  ): Promise<RegularChatDigestRunResult> {
152
158
  const orgRef = ensureRecordId(data.orgId, TABLES.ORGANIZATION)
153
159
  const orgId = recordIdToString(orgRef, TABLES.ORGANIZATION)
154
- const workspaceProvider = getRuntimeAdapters().services?.workspaceProvider
160
+ const workspaceProvider = getRuntimeAdapters().workspaceProvider
155
161
  if (!workspaceProvider) {
156
162
  serverLogger.info`Skipping regular chat memory digest for ${orgId}: workspaceProvider is not configured`
157
163
  return { skipped: true, processedWorkstreamMessages: 0, processedSocialMessages: 0, followUpScheduled: false }
@@ -2,7 +2,7 @@ import { serverLogger } from '../config/logger'
2
2
  import { ensureRecordId, recordIdToString } from '../db/record-id'
3
3
  import { TABLES } from '../db/tables'
4
4
  import { getDefaultEmbeddings } from '../embeddings/provider'
5
- import type { SkillExtractionJob } from '../queues/skill-extraction.queue'
5
+ import type { SkillExtractionJob } from '../queues/organization-learning.queue'
6
6
  import { createHelperModelRuntime } from '../runtime/helper-model'
7
7
  import { getRuntimeAdapters, withConfiguredWorkspaceMemoryLock } from '../runtime/runtime-extensions'
8
8
  import { learnedSkillService } from '../services/learned-skill.service'
@@ -74,7 +74,7 @@ function buildManagerPrompt(params: {
74
74
  export async function runSkillExtraction(data: SkillExtractionJob): Promise<SkillExtractionRunResult> {
75
75
  const orgRef = ensureRecordId(data.orgId, TABLES.ORGANIZATION)
76
76
  const orgId = recordIdToString(orgRef, TABLES.ORGANIZATION)
77
- const workspaceProvider = getRuntimeAdapters().services?.workspaceProvider
77
+ const workspaceProvider = getRuntimeAdapters().workspaceProvider
78
78
  const cursorAwareWorkspaceProvider =
79
79
  workspaceProvider?.getBackgroundCursor && workspaceProvider.setBackgroundCursor
80
80
  ? (workspaceProvider as typeof workspaceProvider & {
@@ -1,30 +0,0 @@
1
- import type { Job } from 'bullmq'
2
-
3
- import { databaseService } from '../db/service'
4
- import { recentActivityTitleService } from '../services/recent-activity-title.service'
5
- import { createQueueFactory } from './queue-factory'
6
-
7
- interface RecentActivityTitleRefinementJob {
8
- activityId: string
9
- }
10
-
11
- async function processRecentActivityTitleRefinementJob(job: Job<RecentActivityTitleRefinementJob>): Promise<void> {
12
- await databaseService.connect()
13
- await recentActivityTitleService.refineRecentActivityTitle(job.data.activityId)
14
- }
15
-
16
- const recentActivityTitleRefinement = createQueueFactory<RecentActivityTitleRefinementJob>({
17
- name: 'recent-activity-title-refinement',
18
- displayName: 'Recent activity title refinement',
19
- jobName: 'refine-recent-activity-title',
20
- concurrency: 10,
21
- lockDuration: 300_000,
22
- defaultJobOptions: { attempts: 3, backoff: { type: 'exponential', delay: 2_000 } },
23
- processor: processRecentActivityTitleRefinementJob,
24
- })
25
-
26
- export function enqueueRecentActivityTitleRefinement(job: RecentActivityTitleRefinementJob) {
27
- return recentActivityTitleRefinement.enqueue(job, { jobId: `recent-activity-title:${job.activityId}` })
28
- }
29
-
30
- export const startRecentActivityTitleRefinementWorker = recentActivityTitleRefinement.startWorker
@@ -1,12 +0,0 @@
1
- const REGULAR_CHAT_MEMORY_DIGEST_DELAY_MS = 15 * 60 * 1000
2
-
3
- export function buildRegularChatMemoryDigestDeduplicationId(orgId: string): string {
4
- return `regular-chat-digest:${orgId}`
5
- }
6
-
7
- export function buildRegularChatMemoryDigestJobOptions(orgId: string) {
8
- return {
9
- delay: REGULAR_CHAT_MEMORY_DIGEST_DELAY_MS,
10
- deduplication: { id: buildRegularChatMemoryDigestDeduplicationId(orgId) },
11
- }
12
- }
@@ -1,34 +0,0 @@
1
- import { getWorkerPath, LONG_JOB_LOCK_DURATION_MS } from '../workers/worker-utils'
2
- import { createQueueFactory } from './queue-factory'
3
- import {
4
- buildRegularChatMemoryDigestDeduplicationId,
5
- buildRegularChatMemoryDigestJobOptions,
6
- } from './regular-chat-memory-digest.config'
7
-
8
- export interface RegularChatMemoryDigestJob {
9
- orgId: string
10
- }
11
-
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))
24
- }
25
-
26
- export async function clearRegularChatMemoryDigestDeduplicationKey(orgId: string): Promise<void> {
27
- await regularChatMemoryDigest.getQueue().removeDeduplicationKey(buildRegularChatMemoryDigestDeduplicationId(orgId))
28
- }
29
-
30
- export const startRegularChatMemoryDigestWorker = regularChatMemoryDigest.startWorker
31
-
32
- if (import.meta.main) {
33
- startRegularChatMemoryDigestWorker()
34
- }
@@ -1,9 +0,0 @@
1
- const SKILL_EXTRACTION_DELAY_MS = 15 * 60 * 1000
2
-
3
- export function buildSkillExtractionDeduplicationId(orgId: string): string {
4
- return `skill-extraction:${orgId}`
5
- }
6
-
7
- export function buildSkillExtractionJobOptions(orgId: string) {
8
- return { delay: SKILL_EXTRACTION_DELAY_MS, deduplication: { id: buildSkillExtractionDeduplicationId(orgId) } }
9
- }
@@ -1,27 +0,0 @@
1
- import { getWorkerPath } from '../workers/worker-utils'
2
- import { createQueueFactory } from './queue-factory'
3
- import { buildSkillExtractionJobOptions } from './skill-extraction.config'
4
-
5
- export interface SkillExtractionJob {
6
- orgId: string
7
- }
8
-
9
- const skillExtraction = createQueueFactory<SkillExtractionJob>({
10
- name: 'skill-extraction',
11
- displayName: 'Skill extraction',
12
- jobName: 'run-extraction',
13
- concurrency: 10,
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))
21
- }
22
-
23
- export const startSkillExtractionWorker = skillExtraction.startWorker
24
-
25
- if (import.meta.main) {
26
- startSkillExtractionWorker()
27
- }
@@ -1,33 +0,0 @@
1
- import type { Job } from 'bullmq'
2
-
3
- import { ensureRecordId } from '../db/record-id'
4
- import { databaseService } from '../db/service'
5
- import { workstreamTitleService } from '../services/workstream-title.service'
6
- import { createQueueFactory } from './queue-factory'
7
-
8
- interface WorkstreamTitleGenerationJob {
9
- workstreamId: string
10
- sourceText: string
11
- }
12
-
13
- async function processWorkstreamTitleGenerationJob(job: Job<WorkstreamTitleGenerationJob>): Promise<void> {
14
- await databaseService.connect()
15
- const workstreamRef = ensureRecordId(job.data.workstreamId)
16
- await workstreamTitleService.generateAndPersistTitle(workstreamRef, job.data.sourceText)
17
- }
18
-
19
- const workstreamTitleGeneration = createQueueFactory<WorkstreamTitleGenerationJob>({
20
- name: 'workstream-title-generation',
21
- displayName: 'Workstream title generation',
22
- jobName: 'generate-workstream-title',
23
- concurrency: 10,
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}` })
31
- }
32
-
33
- export const startWorkstreamTitleGenerationWorker = workstreamTitleGeneration.startWorker
@@ -1,33 +0,0 @@
1
- import type { ContextEnrichment } from '@lota-sdk/shared'
2
-
3
- import { serverLogger } from '../config/logger'
4
- import { getRuntimeConfig } from '../runtime/runtime-config'
5
-
6
- class ContextEnrichmentService {
7
- async enrichForPlanCreation(params: { objective: string; organizationId: string }): Promise<ContextEnrichment[]> {
8
- const pluginRuntime = getRuntimeConfig().pluginRuntime ?? {}
9
- const enrichers = Object.values(pluginRuntime).flatMap((plugin) => plugin.contextEnrichers ?? [])
10
-
11
- if (enrichers.length === 0) return []
12
-
13
- const results = await Promise.allSettled(
14
- enrichers.map((enricher) =>
15
- enricher.enrich({ objective: params.objective, organizationId: params.organizationId }),
16
- ),
17
- )
18
-
19
- const enrichments: ContextEnrichment[] = []
20
- for (let i = 0; i < results.length; i++) {
21
- const result = results[i]
22
- if (result.status === 'fulfilled') {
23
- enrichments.push({ domain: enrichers[i].domain, data: result.value.data, confidence: result.value.confidence })
24
- } else {
25
- serverLogger.warn`Context enricher "${enrichers[i].domain}" failed: ${result.reason}`
26
- }
27
- }
28
-
29
- return enrichments.sort((a, b) => b.confidence - a.confidence)
30
- }
31
- }
32
-
33
- export const contextEnrichmentService = new ContextEnrichmentService()
@@ -1,117 +0,0 @@
1
- import type { SignalDeclaration } from '@lota-sdk/shared'
2
-
3
- import { serverLogger } from '../config/logger'
4
- import type { LotaPlugin } from '../runtime/plugin-types'
5
- import { getRuntimeConfig } from '../runtime/runtime-config'
6
- import type { PlanValidationIssueInput } from './plan-validator.service'
7
-
8
- export interface EmittedSignal {
9
- signal: string
10
- payload: unknown
11
- sourcePlugin: string
12
- consumers: string[]
13
- emittedAt: Date
14
- }
15
-
16
- const MAX_EMITTED_SIGNALS = 1000
17
-
18
- class CoordinationRegistryService {
19
- private producers = new Map<string, string[]>()
20
- private consumers = new Map<string, string[]>()
21
- private _emittedSignals: EmittedSignal[] = []
22
-
23
- register(pluginRef: string, signals: SignalDeclaration[]): void {
24
- for (const signal of signals) {
25
- if (signal.direction === 'produces') {
26
- const existing = this.producers.get(signal.signalName) ?? []
27
- existing.push(pluginRef)
28
- this.producers.set(signal.signalName, existing)
29
- } else {
30
- const existing = this.consumers.get(signal.signalName) ?? []
31
- existing.push(pluginRef)
32
- this.consumers.set(signal.signalName, existing)
33
- }
34
- }
35
- }
36
-
37
- async emit(signal: string, payload: unknown, sourcePlugin: string): Promise<void> {
38
- const consumers = this.consumers.get(signal) ?? []
39
-
40
- this._emittedSignals.push({ signal, payload, sourcePlugin, consumers: [...consumers], emittedAt: new Date() })
41
- if (this._emittedSignals.length > MAX_EMITTED_SIGNALS) {
42
- this._emittedSignals.shift()
43
- }
44
-
45
- serverLogger.debug(
46
- `Signal "${signal}" emitted by "${sourcePlugin}" — ${consumers.length} consumer(s): ${consumers.join(', ') || 'none'}`,
47
- )
48
-
49
- if (consumers.length === 0) return
50
-
51
- let pluginRuntime: Record<string, unknown> = {}
52
- try {
53
- pluginRuntime = getRuntimeConfig().pluginRuntime ?? {}
54
- } catch {
55
- // Runtime not yet configured — skip plugin dispatch
56
- }
57
-
58
- for (const consumerRef of consumers) {
59
- const plugin = pluginRuntime[consumerRef] as LotaPlugin | undefined
60
- if (plugin?.onSignal) {
61
- try {
62
- await plugin.onSignal(signal, payload, sourcePlugin)
63
- } catch (error) {
64
- serverLogger.warn`Signal handler for "${consumerRef}" failed on signal "${signal}": ${error}`
65
- }
66
- }
67
- }
68
- }
69
-
70
- getEmittedSignals(signalName?: string): EmittedSignal[] {
71
- if (!signalName) return [...this._emittedSignals]
72
- return this._emittedSignals.filter((entry) => entry.signal === signalName)
73
- }
74
-
75
- getProducers(signalName: string): string[] {
76
- return this.producers.get(signalName) ?? []
77
- }
78
-
79
- getConsumers(signalName: string): string[] {
80
- return this.consumers.get(signalName) ?? []
81
- }
82
-
83
- validate(): PlanValidationIssueInput[] {
84
- const issues: PlanValidationIssueInput[] = []
85
-
86
- for (const [signalName] of this.producers) {
87
- if ((this.consumers.get(signalName) ?? []).length === 0) {
88
- issues.push({
89
- severity: 'warning',
90
- code: 'signal_no_consumer',
91
- message: `Signal "${signalName}" is produced but has no consumers.`,
92
- })
93
- }
94
- }
95
-
96
- for (const [signalName] of this.consumers) {
97
- if ((this.producers.get(signalName) ?? []).length === 0) {
98
- issues.push({
99
- severity: 'warning',
100
- code: 'signal_no_producer',
101
- message: `Signal "${signalName}" is consumed but has no producers.`,
102
- })
103
- }
104
- }
105
-
106
- return issues
107
- }
108
-
109
- /** Reset internal state. Intended for testing. */
110
- _reset(): void {
111
- this.producers.clear()
112
- this.consumers.clear()
113
- this._emittedSignals = []
114
- }
115
- }
116
-
117
- export const coordinationRegistryService = new CoordinationRegistryService()
@@ -1,71 +0,0 @@
1
- import type { OwnershipDispatchContext, PlanArtifactSubmission, PlanNodeResult, PlanNodeSpec } from '@lota-sdk/shared'
2
-
3
- import type { LotaPlugin, PluginDomainAgentDefinition } from '../runtime/plugin-types'
4
- import { getRuntimeConfig } from '../runtime/runtime-config'
5
- import type { PlanValidationIssueInput } from './plan-validator.service'
6
-
7
- class DomainAgentExecutorService {
8
- private registry = new Map<string, { pluginRef: string; definition: PluginDomainAgentDefinition }>()
9
-
10
- configure(pluginRuntime: Record<string, LotaPlugin>): void {
11
- this.registry.clear()
12
- for (const [pluginRef, plugin] of Object.entries(pluginRuntime)) {
13
- if (plugin.domainAgents) {
14
- for (const def of plugin.domainAgents) {
15
- this.registry.set(def.agentId, { pluginRef, definition: def })
16
- }
17
- }
18
- }
19
- }
20
-
21
- hasAgent(agentId: string): boolean {
22
- return this.registry.has(agentId)
23
- }
24
-
25
- validateOwner(agentId: string, nodeId: string): PlanValidationIssueInput[] {
26
- if (!this.registry.has(agentId)) {
27
- return [
28
- {
29
- severity: 'blocking',
30
- code: 'domain_agent_missing',
31
- message: `Node "${nodeId}" references unknown domain agent "${agentId}".`,
32
- nodeId,
33
- },
34
- ]
35
- }
36
- return []
37
- }
38
-
39
- async executeNode(params: {
40
- nodeSpec: PlanNodeSpec
41
- resolvedInput: Record<string, unknown>
42
- inputArtifacts: PlanArtifactSubmission[]
43
- context: OwnershipDispatchContext
44
- }): Promise<PlanNodeResult> {
45
- const entry = this.registry.get(params.nodeSpec.owner.ref)
46
- if (!entry) {
47
- throw new Error(`Domain agent "${params.nodeSpec.owner.ref}" not registered.`)
48
- }
49
-
50
- const plugin = (getRuntimeConfig().pluginRuntime as Record<string, LotaPlugin | undefined> | undefined)?.[
51
- entry.pluginRef
52
- ]
53
- if (!plugin?.nodeExecutor) {
54
- throw new Error(`Plugin "${entry.pluginRef}" does not have a nodeExecutor.`)
55
- }
56
-
57
- return plugin.nodeExecutor.executeNode({
58
- operation: `domain-agent:${entry.definition.agentId}`,
59
- nodeSpec: params.nodeSpec,
60
- inputs: params.resolvedInput,
61
- context: {
62
- organizationId: params.context.organizationId,
63
- workstreamId: params.context.workstreamId,
64
- planId: params.context.planId,
65
- nodeId: params.context.nodeId,
66
- },
67
- })
68
- }
69
- }
70
-
71
- export const domainAgentExecutorService = new DomainAgentExecutorService()
@@ -1,44 +0,0 @@
1
- import type { z } from 'zod'
2
-
3
- import { MemoryImportanceAssessmentSchema } from '../db/memory-types'
4
- import { createHelperModelRuntime } from '../runtime/helper-model'
5
- import { createOrgMemoryAgent } from '../system-agents/memory.agent'
6
- import { clampImportance } from '../utils/string'
7
-
8
- type MemoryImportanceAssessment = z.infer<typeof MemoryImportanceAssessmentSchema>
9
- const MEMORY_IMPORTANCE_ASSESSMENT_TIMEOUT_MS = 10 * 60 * 1000
10
- const helperModelRuntime = createHelperModelRuntime()
11
-
12
- export function clampMemoryImportance(value: number): number {
13
- return clampImportance(Math.max(0.2, Math.min(0.95, value)))
14
- }
15
-
16
- export async function assessMemoryImportance(params: {
17
- content: string
18
- targetScope: 'agent' | 'global'
19
- tag?: string
20
- }): Promise<MemoryImportanceAssessment> {
21
- const prompt = [
22
- 'Assess whether this memory should be persisted in long-term memory.',
23
- `targetScope: ${params.targetScope}`,
24
- `memory: ${params.content}`,
25
- '',
26
- 'Rules:',
27
- '- importance: number between 0 and 1 for long-term usefulness.',
28
- '- durability: core|standard|ephemeral.',
29
- '- classification: durable|transient|uncertain.',
30
- '- rationale: short and concrete.',
31
- '',
32
- 'The caller enforces a structured output schema.',
33
- 'Return only schema fields.',
34
- ].join('\n')
35
-
36
- return helperModelRuntime.generateHelperStructured({
37
- tag: params.tag ?? 'memory-importance-assessment',
38
- createAgent: createOrgMemoryAgent,
39
- systemPrompt: 'You are a strict long-term memory quality assessor for an AI agent.',
40
- messages: [{ role: 'user', content: prompt }],
41
- schema: MemoryImportanceAssessmentSchema,
42
- timeoutMs: MEMORY_IMPORTANCE_ASSESSMENT_TIMEOUT_MS,
43
- })
44
- }
@@ -1,67 +0,0 @@
1
- import type { PlanTemplateRecord } from '@lota-sdk/shared'
2
-
3
- import type { RecordIdInput } from '../db/record-id'
4
- import type { PlaybookContribution } from '../runtime/plugin-types'
5
- import { getRuntimeConfig } from '../runtime/runtime-config'
6
- import { planTemplateService } from './plan-template.service'
7
-
8
- class PlaybookRegistryService {
9
- collectPlaybooks(): PlaybookContribution[] {
10
- const plugins = getRuntimeConfig().pluginRuntime ?? {}
11
- const playbooks: PlaybookContribution[] = []
12
- for (const plugin of Object.values(plugins)) {
13
- if (plugin.playbookContributor?.playbooks) {
14
- playbooks.push(...plugin.playbookContributor.playbooks)
15
- }
16
- }
17
- return playbooks
18
- }
19
-
20
- async syncPlaybookTemplates(organizationId: RecordIdInput): Promise<PlanTemplateRecord[]> {
21
- const playbooks = this.collectPlaybooks()
22
- const templates: PlanTemplateRecord[] = []
23
- const existing = await planTemplateService.listTemplates(organizationId, { source: 'playbook' })
24
-
25
- for (const pb of playbooks) {
26
- const match = existing.find((t) => t.sourceRef === pb.name)
27
- if (match) {
28
- const updated = await planTemplateService.updateTemplate(match.id, { draft: pb.draft, tags: pb.tags })
29
- templates.push(updated)
30
- } else {
31
- const created = await planTemplateService.createTemplate({
32
- organizationId,
33
- name: pb.name,
34
- description: pb.description,
35
- draft: pb.draft,
36
- tags: pb.tags,
37
- source: 'playbook',
38
- sourceRef: pb.name,
39
- })
40
- templates.push(created)
41
- }
42
- }
43
-
44
- return templates
45
- }
46
-
47
- async instantiatePlaybook(params: {
48
- name: string
49
- organizationId: RecordIdInput
50
- workstreamId: RecordIdInput
51
- leadAgentId: string
52
- }): Promise<unknown> {
53
- const templates = await planTemplateService.listTemplates(params.organizationId, { source: 'playbook' })
54
- const template = templates.find((t) => t.sourceRef === params.name)
55
- if (!template) {
56
- throw new Error(`Playbook "${params.name}" not found.`)
57
- }
58
- return planTemplateService.instantiate({
59
- templateId: template.id,
60
- organizationId: params.organizationId,
61
- workstreamId: params.workstreamId,
62
- leadAgentId: params.leadAgentId,
63
- })
64
- }
65
- }
66
-
67
- export const playbookRegistryService = new PlaybookRegistryService()