@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.
- package/package.json +2 -2
- package/src/ai/definitions.ts +5 -59
- package/src/ai-gateway/ai-gateway.ts +36 -28
- package/src/ai-gateway/cache-headers.ts +9 -0
- package/src/config/model-constants.ts +6 -2
- package/src/create-runtime.ts +1 -17
- package/src/db/memory-types.ts +13 -8
- package/src/db/memory.ts +74 -53
- package/src/queues/autonomous-job.queue.ts +1 -8
- package/src/queues/context-compaction.queue.ts +2 -2
- package/src/queues/index.ts +2 -6
- package/src/queues/organization-learning.queue.ts +78 -0
- package/src/queues/plan-agent-heartbeat.queue.ts +10 -16
- package/src/queues/title-generation.queue.ts +62 -0
- package/src/runtime/agent-prompt-context.ts +0 -18
- package/src/runtime/agent-runtime-policy.ts +9 -2
- package/src/runtime/context-compaction-constants.ts +4 -2
- package/src/runtime/context-compaction.ts +135 -118
- package/src/runtime/memory-pipeline.ts +70 -1
- package/src/runtime/memory-prompts-fact.ts +16 -0
- package/src/runtime/plugin-resolution.ts +3 -2
- package/src/runtime/plugin-types.ts +1 -42
- package/src/runtime/post-turn-side-effects.ts +212 -0
- package/src/runtime/runtime-config.ts +0 -13
- package/src/runtime/runtime-extensions.ts +10 -16
- package/src/runtime/runtime-worker-registry.ts +8 -19
- package/src/runtime/social-chat-agent-runner.ts +119 -0
- package/src/runtime/social-chat-history.ts +110 -0
- package/src/runtime/social-chat-prompts.ts +58 -0
- package/src/runtime/social-chat.ts +104 -340
- package/src/runtime/specialist-runner.ts +18 -0
- package/src/runtime/workstream-chat-helpers.ts +19 -0
- package/src/runtime/workstream-plan-turn.ts +195 -0
- package/src/runtime/workstream-state.ts +11 -8
- package/src/runtime/workstream-turn-context.ts +183 -0
- package/src/services/autonomous-job.service.ts +1 -8
- package/src/services/execution-plan.service.ts +205 -334
- package/src/services/index.ts +1 -4
- package/src/services/memory.service.ts +54 -44
- package/src/services/ownership-dispatcher.service.ts +2 -19
- package/src/services/plan-completion-side-effects.ts +80 -0
- package/src/services/plan-event-delivery.service.ts +1 -1
- package/src/services/plan-executor.service.ts +42 -190
- package/src/services/plan-node-spec.ts +60 -0
- package/src/services/plan-run-data.ts +88 -0
- package/src/services/plan-validator.service.ts +10 -8
- package/src/services/workstream-constants.ts +2 -0
- package/src/services/workstream-title.service.ts +1 -1
- package/src/services/workstream-turn-preparation.service.ts +208 -715
- package/src/services/workstream.service.ts +162 -192
- package/src/services/workstream.types.ts +12 -44
- package/src/system-agents/regular-chat-memory-digest.agent.ts +3 -0
- package/src/tools/execution-plan.tool.ts +7 -6
- package/src/tools/remember-memory.tool.ts +7 -10
- package/src/tools/research-topic.tool.ts +1 -1
- package/src/tools/team-think.tool.ts +1 -1
- package/src/tools/user-questions.tool.ts +1 -1
- package/src/utils/autonomous-job-ids.ts +7 -0
- package/src/workers/organization-learning.worker.ts +31 -0
- package/src/workers/regular-chat-memory-digest.runner.ts +9 -3
- package/src/workers/skill-extraction.runner.ts +2 -2
- package/src/queues/recent-activity-title-refinement.queue.ts +0 -30
- package/src/queues/regular-chat-memory-digest.config.ts +0 -12
- package/src/queues/regular-chat-memory-digest.queue.ts +0 -34
- package/src/queues/skill-extraction.config.ts +0 -9
- package/src/queues/skill-extraction.queue.ts +0 -27
- package/src/queues/workstream-title-generation.queue.ts +0 -33
- package/src/services/context-enrichment.service.ts +0 -33
- package/src/services/coordination-registry.service.ts +0 -117
- package/src/services/domain-agent-executor.service.ts +0 -71
- package/src/services/memory-assessment.service.ts +0 -44
- package/src/services/playbook-registry.service.ts +0 -67
- package/src/workers/regular-chat-memory-digest.worker.ts +0 -22
- 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
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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(
|
|
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: '
|
|
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: '
|
|
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.
|
|
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().
|
|
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
|
-
'
|
|
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,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/
|
|
13
|
-
import type { RegularChatMemoryDigestJob } from '../queues/
|
|
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().
|
|
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/
|
|
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().
|
|
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()
|