@lota-sdk/core 0.1.23 → 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 (78) 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 +5 -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/execution-plan.ts +2 -1
  20. package/src/runtime/memory-pipeline.ts +70 -1
  21. package/src/runtime/memory-prompts-fact.ts +16 -0
  22. package/src/runtime/plugin-resolution.ts +3 -2
  23. package/src/runtime/plugin-types.ts +1 -42
  24. package/src/runtime/post-turn-side-effects.ts +212 -0
  25. package/src/runtime/runtime-config.ts +0 -13
  26. package/src/runtime/runtime-extensions.ts +10 -16
  27. package/src/runtime/runtime-worker-registry.ts +8 -19
  28. package/src/runtime/social-chat-agent-runner.ts +119 -0
  29. package/src/runtime/social-chat-history.ts +110 -0
  30. package/src/runtime/social-chat-prompts.ts +58 -0
  31. package/src/runtime/social-chat.ts +104 -340
  32. package/src/runtime/specialist-runner.ts +18 -0
  33. package/src/runtime/workstream-chat-helpers.ts +19 -0
  34. package/src/runtime/workstream-plan-turn.ts +195 -0
  35. package/src/runtime/workstream-state.ts +11 -8
  36. package/src/runtime/workstream-turn-context.ts +183 -0
  37. package/src/services/agent-activity.service.ts +350 -0
  38. package/src/services/autonomous-job.service.ts +1 -8
  39. package/src/services/execution-plan.service.ts +205 -334
  40. package/src/services/index.ts +2 -4
  41. package/src/services/memory.service.ts +54 -44
  42. package/src/services/ownership-dispatcher.service.ts +2 -19
  43. package/src/services/plan-completion-side-effects.ts +80 -0
  44. package/src/services/plan-event-delivery.service.ts +1 -1
  45. package/src/services/plan-executor.service.ts +42 -190
  46. package/src/services/plan-node-spec.ts +60 -0
  47. package/src/services/plan-run-data.ts +88 -0
  48. package/src/services/plan-validator.service.ts +10 -8
  49. package/src/services/workstream-constants.ts +2 -0
  50. package/src/services/workstream-title.service.ts +1 -1
  51. package/src/services/workstream-turn-preparation.service.ts +208 -715
  52. package/src/services/workstream.service.ts +162 -192
  53. package/src/services/workstream.types.ts +12 -44
  54. package/src/system-agents/regular-chat-memory-digest.agent.ts +3 -0
  55. package/src/tools/execution-plan.tool.ts +11 -6
  56. package/src/tools/index.ts +1 -0
  57. package/src/tools/project-with-plan.tool.ts +87 -0
  58. package/src/tools/remember-memory.tool.ts +7 -10
  59. package/src/tools/research-topic.tool.ts +1 -1
  60. package/src/tools/team-think.tool.ts +1 -1
  61. package/src/tools/user-questions.tool.ts +1 -1
  62. package/src/utils/autonomous-job-ids.ts +7 -0
  63. package/src/workers/organization-learning.worker.ts +31 -0
  64. package/src/workers/regular-chat-memory-digest.runner.ts +9 -3
  65. package/src/workers/skill-extraction.runner.ts +2 -2
  66. package/src/queues/recent-activity-title-refinement.queue.ts +0 -30
  67. package/src/queues/regular-chat-memory-digest.config.ts +0 -12
  68. package/src/queues/regular-chat-memory-digest.queue.ts +0 -34
  69. package/src/queues/skill-extraction.config.ts +0 -9
  70. package/src/queues/skill-extraction.queue.ts +0 -27
  71. package/src/queues/workstream-title-generation.queue.ts +0 -33
  72. package/src/services/context-enrichment.service.ts +0 -33
  73. package/src/services/coordination-registry.service.ts +0 -117
  74. package/src/services/domain-agent-executor.service.ts +0 -71
  75. package/src/services/memory-assessment.service.ts +0 -44
  76. package/src/services/playbook-registry.service.ts +0 -67
  77. package/src/workers/regular-chat-memory-digest.worker.ts +0 -22
  78. package/src/workers/skill-extraction.worker.ts +0 -22
@@ -0,0 +1,350 @@
1
+ import type {
2
+ AgentActivityCounts,
3
+ AgentActivityEntry,
4
+ AgentActivityResponse,
5
+ AgentProjectEntry,
6
+ MyTasksResponse,
7
+ PlanBoardColumn,
8
+ PlanBoardResponse,
9
+ PlanNodeCard,
10
+ PlanViewNode,
11
+ PlanViewResponse,
12
+ SerializableExecutionPlan,
13
+ SerializablePlanNode,
14
+ } from '@lota-sdk/shared'
15
+
16
+ import { agentRoster } from '../config/agent-defaults'
17
+ import { serverLogger } from '../config/logger'
18
+ import { executionPlanService } from './execution-plan.service'
19
+ import { workstreamService } from './workstream.service'
20
+ import type { NormalizedWorkstream } from './workstream.types'
21
+
22
+ const BOARD_COLUMN_ORDER = ['ready', 'running', 'awaiting-human', 'completed', 'blocked', 'failed'] as const
23
+ type BoardColumnStatus = (typeof BOARD_COLUMN_ORDER)[number]
24
+
25
+ const COLUMN_LABELS: Record<BoardColumnStatus, string> = {
26
+ ready: 'Ready',
27
+ running: 'Running',
28
+ 'awaiting-human': 'Awaiting Human',
29
+ completed: 'Completed',
30
+ blocked: 'Blocked',
31
+ failed: 'Failed',
32
+ }
33
+
34
+ type ActivePlanEntry = {
35
+ plan: SerializableExecutionPlan
36
+ workstream: Pick<NormalizedWorkstream, 'id' | 'title' | 'isRunning' | 'updatedAt'>
37
+ }
38
+
39
+ type AgentActivityDeps = {
40
+ executionPlanService: Pick<typeof executionPlanService, 'getActivePlansForWorkstream'>
41
+ workstreamService: Pick<typeof workstreamService, 'listWorkstreams'>
42
+ }
43
+
44
+ function normalizeCardStatus(status: string): BoardColumnStatus {
45
+ if (status === 'pending' || status === 'scheduled') return 'ready'
46
+ if (status === 'partial') return 'completed'
47
+ if (status === 'running' || status === 'awaiting-human' || status === 'completed' || status === 'blocked') {
48
+ return status
49
+ }
50
+ if (status === 'failed') return 'failed'
51
+ return 'blocked'
52
+ }
53
+
54
+ function isCompletedStatus(status: string): boolean {
55
+ return status === 'completed' || status === 'partial' || status === 'skipped'
56
+ }
57
+
58
+ function createEmptyCounts(): AgentActivityCounts {
59
+ return { running: 0, ready: 0, pending: 0, awaitingHuman: 0, blocked: 0, completed: 0, failed: 0 }
60
+ }
61
+
62
+ function maxIsoDate(current: string | null, candidate: string): string {
63
+ if (!current) return candidate
64
+ return new Date(candidate).getTime() > new Date(current).getTime() ? candidate : current
65
+ }
66
+
67
+ function incrementCounts(counts: AgentActivityCounts, rawStatus: string): void {
68
+ if (rawStatus === 'running') {
69
+ counts.running += 1
70
+ return
71
+ }
72
+ if (rawStatus === 'ready') {
73
+ counts.ready += 1
74
+ return
75
+ }
76
+ if (rawStatus === 'pending' || rawStatus === 'scheduled') {
77
+ counts.pending += 1
78
+ return
79
+ }
80
+ if (rawStatus === 'awaiting-human') {
81
+ counts.awaitingHuman += 1
82
+ return
83
+ }
84
+ if (rawStatus === 'blocked') {
85
+ counts.blocked += 1
86
+ return
87
+ }
88
+ if (rawStatus === 'failed') {
89
+ counts.failed += 1
90
+ return
91
+ }
92
+ if (rawStatus === 'completed' || rawStatus === 'partial') {
93
+ counts.completed += 1
94
+ }
95
+ }
96
+
97
+ export function planNodeToCard(
98
+ node: SerializablePlanNode,
99
+ plan: SerializableExecutionPlan,
100
+ workstreamId: string,
101
+ workstreamTitle: string,
102
+ ): PlanNodeCard {
103
+ const approval = plan.approvals.find((candidate) => candidate.nodeId === node.id && candidate.status === 'pending')
104
+
105
+ return {
106
+ nodeId: node.id,
107
+ label: node.label,
108
+ objective: node.objective,
109
+ status: normalizeCardStatus(node.status),
110
+ ownerType: node.owner.executorType,
111
+ ownerRef: node.owner.ref,
112
+ planRunId: plan.runId,
113
+ planTitle: plan.title,
114
+ workstreamId,
115
+ workstreamTitle,
116
+ nodeType: node.type,
117
+ artifactCount: plan.artifacts.filter((artifact) => artifact.nodeId === node.id).length,
118
+ hasApproval: Boolean(approval),
119
+ approvalId: approval?.id ?? null,
120
+ approvalStatus: approval?.status ?? null,
121
+ blockedReason: node.blockedReason ?? null,
122
+ latestNotes: node.latestNotes ?? null,
123
+ startedAt: node.startedAt ?? null,
124
+ completedAt: node.completedAt ?? null,
125
+ readyAt: node.readyAt ?? null,
126
+ }
127
+ }
128
+
129
+ function buildPlanViewNode(
130
+ node: SerializablePlanNode,
131
+ plan: SerializableExecutionPlan,
132
+ workstreamId: string,
133
+ workstreamTitle: string,
134
+ ): PlanViewNode {
135
+ return {
136
+ ...planNodeToCard(node, plan, workstreamId, workstreamTitle),
137
+ deliverableNames: node.deliverables.map((deliverable) => deliverable.name),
138
+ upstreamNodeIds: node.upstreamNodeIds,
139
+ downstreamNodeIds: node.downstreamNodeIds,
140
+ }
141
+ }
142
+
143
+ export class AgentActivityService {
144
+ constructor(private readonly deps: AgentActivityDeps = { executionPlanService, workstreamService }) {}
145
+
146
+ async getBoard(userRef: string, orgRef: string): Promise<PlanBoardResponse> {
147
+ const activePlans = await this.getAllActivePlans(userRef, orgRef)
148
+ const cards = activePlans.flatMap(({ plan, workstream }) =>
149
+ plan.nodes.map((node) => planNodeToCard(node, plan, workstream.id, workstream.title)),
150
+ )
151
+
152
+ const columns: PlanBoardColumn[] = BOARD_COLUMN_ORDER.map((status) => ({
153
+ status,
154
+ label: COLUMN_LABELS[status],
155
+ nodes: cards.filter((card) => card.status === status),
156
+ }))
157
+
158
+ return {
159
+ columns,
160
+ summary: {
161
+ totalNodes: cards.length,
162
+ completedNodes: cards.filter((card) => card.status === 'completed').length,
163
+ activePlanCount: activePlans.length,
164
+ pendingApprovalCount: cards.filter((card) => card.hasApproval).length,
165
+ },
166
+ }
167
+ }
168
+
169
+ async getPlanView(orgRef: string, planRunId: string, userRef: string): Promise<PlanViewResponse | null> {
170
+ const activePlans = await this.getAllActivePlans(userRef, orgRef)
171
+ const match = activePlans.find(({ plan }) => plan.runId === planRunId)
172
+ if (!match) return null
173
+
174
+ const { plan, workstream } = match
175
+ return {
176
+ planRunId: plan.runId,
177
+ title: plan.title,
178
+ objective: plan.objective,
179
+ status: plan.status,
180
+ leadAgentId: plan.leadAgentId,
181
+ progress: { completed: plan.progress.completed + plan.progress.partial, total: plan.progress.total },
182
+ nodes: plan.nodes.map((node) => buildPlanViewNode(node, plan, workstream.id, workstream.title)),
183
+ edges: plan.edges.map((edge) => ({ from: edge.source, to: edge.target })),
184
+ }
185
+ }
186
+
187
+ async getMyTasks(userRef: string, orgRef: string): Promise<MyTasksResponse> {
188
+ const activePlans = await this.getAllActivePlans(userRef, orgRef)
189
+ const tasks: PlanNodeCard[] = []
190
+
191
+ for (const { plan, workstream } of activePlans) {
192
+ for (const node of plan.nodes) {
193
+ const humanOwned = node.owner.executorType === 'user'
194
+ const awaitingHuman = node.status === 'awaiting-human'
195
+ if (!humanOwned && !awaitingHuman) continue
196
+
197
+ tasks.push(planNodeToCard(node, plan, workstream.id, workstream.title))
198
+ }
199
+ }
200
+
201
+ return { tasks, pendingApprovalCount: tasks.filter((task) => task.hasApproval).length }
202
+ }
203
+
204
+ async getAgentActivity(userRef: string, orgRef: string): Promise<AgentActivityResponse> {
205
+ const activePlans = await this.getAllActivePlans(userRef, orgRef)
206
+ const activityByAgent = new Map<string, AgentActivityEntry>()
207
+
208
+ for (const agentId of agentRoster) {
209
+ activityByAgent.set(agentId, this.createEmptyEntry(agentId))
210
+ }
211
+
212
+ for (const { plan, workstream } of activePlans) {
213
+ const involvedAgents = new Set<string>()
214
+
215
+ for (const node of plan.nodes) {
216
+ if (node.owner.executorType !== 'agent') continue
217
+
218
+ const agentId = node.owner.ref
219
+ const entry = this.ensureEntry(activityByAgent, agentId)
220
+ involvedAgents.add(agentId)
221
+ incrementCounts(entry.counts, node.status)
222
+
223
+ if (!isCompletedStatus(node.status)) {
224
+ entry.tasks.push(planNodeToCard(node, plan, workstream.id, workstream.title))
225
+ }
226
+ }
227
+
228
+ if (plan.leadAgentId.trim()) {
229
+ const leadEntry = this.ensureEntry(activityByAgent, plan.leadAgentId)
230
+ leadEntry.isLeadingActivePlan = true
231
+ involvedAgents.add(plan.leadAgentId)
232
+ }
233
+
234
+ for (const agentId of involvedAgents) {
235
+ const entry = this.ensureEntry(activityByAgent, agentId)
236
+ this.ensureProjectEntry(entry.projects, {
237
+ workstreamId: workstream.id,
238
+ workstreamTitle: workstream.title,
239
+ planRunId: plan.runId,
240
+ planTitle: plan.title,
241
+ status: plan.status,
242
+ })
243
+
244
+ entry.isRunning = entry.isRunning || workstream.isRunning
245
+ entry.lastActiveAt = maxIsoDate(entry.lastActiveAt, workstream.updatedAt)
246
+ }
247
+ }
248
+
249
+ const userTasks = await this.getMyTasks(userRef, orgRef)
250
+ const agents = [...activityByAgent.values()].sort((left, right) => {
251
+ const leftIndex = agentRoster.indexOf(left.agentId)
252
+ const rightIndex = agentRoster.indexOf(right.agentId)
253
+ if (leftIndex !== -1 || rightIndex !== -1) {
254
+ if (leftIndex === -1) return 1
255
+ if (rightIndex === -1) return -1
256
+ return leftIndex - rightIndex
257
+ }
258
+ return left.agentId.localeCompare(right.agentId)
259
+ })
260
+
261
+ return {
262
+ agents,
263
+ userActivity: {
264
+ taskCount: userTasks.tasks.length,
265
+ pendingApprovalCount: userTasks.pendingApprovalCount,
266
+ awaitingHumanCount: userTasks.tasks.filter((task) => task.status === 'awaiting-human').length,
267
+ lastActiveAt: activePlans.reduce<string | null>(
268
+ (latest, entry) => maxIsoDate(latest, entry.workstream.updatedAt),
269
+ null,
270
+ ),
271
+ },
272
+ totalActivePlans: activePlans.length,
273
+ }
274
+ }
275
+
276
+ async getAllActivePlans(userRef: string, orgRef: string): Promise<ActivePlanEntry[]> {
277
+ const workstreams = await this.listRelevantWorkstreams(userRef, orgRef)
278
+ const planResults = await Promise.all(
279
+ workstreams.map(async (workstream) => {
280
+ try {
281
+ const plans = await this.deps.executionPlanService.getActivePlansForWorkstream(workstream.id)
282
+ return plans.map((plan) => ({ plan, workstream }))
283
+ } catch (error) {
284
+ serverLogger.error`Failed to load active plans for workstream ${workstream.id}: ${error}`
285
+ return []
286
+ }
287
+ }),
288
+ )
289
+
290
+ return planResults.flat()
291
+ }
292
+
293
+ private async listRelevantWorkstreams(userRef: string, orgRef: string): Promise<NormalizedWorkstream[]> {
294
+ const [direct, core, group] = await Promise.all([
295
+ this.deps.workstreamService.listWorkstreams(userRef, orgRef, { mode: 'direct', includeArchived: false }),
296
+ this.deps.workstreamService.listWorkstreams(userRef, orgRef, {
297
+ mode: 'group',
298
+ core: true,
299
+ includeArchived: false,
300
+ }),
301
+ this.deps.workstreamService.listWorkstreams(userRef, orgRef, {
302
+ mode: 'group',
303
+ core: false,
304
+ includeArchived: false,
305
+ take: 500,
306
+ page: 1,
307
+ }),
308
+ ])
309
+
310
+ const deduped = new Map<string, NormalizedWorkstream>()
311
+ for (const workstream of [...direct.workstreams, ...core.workstreams, ...group.workstreams]) {
312
+ deduped.set(workstream.id, workstream)
313
+ }
314
+
315
+ return [...deduped.values()]
316
+ }
317
+
318
+ private createEmptyEntry(agentId: string): AgentActivityEntry {
319
+ return {
320
+ agentId,
321
+ counts: createEmptyCounts(),
322
+ tasks: [],
323
+ projects: [],
324
+ isLeadingActivePlan: false,
325
+ isRunning: false,
326
+ lastActiveAt: null,
327
+ }
328
+ }
329
+
330
+ private ensureEntry(entries: Map<string, AgentActivityEntry>, agentId: string): AgentActivityEntry {
331
+ const existing = entries.get(agentId)
332
+ if (existing) {
333
+ return existing
334
+ }
335
+
336
+ const created = this.createEmptyEntry(agentId)
337
+ entries.set(agentId, created)
338
+ return created
339
+ }
340
+
341
+ private ensureProjectEntry(projects: AgentProjectEntry[], next: AgentProjectEntry): void {
342
+ if (projects.some((project) => project.planRunId === next.planRunId)) {
343
+ return
344
+ }
345
+
346
+ projects.push(next)
347
+ }
348
+ }
349
+
350
+ export const agentActivityService = new AgentActivityService()
@@ -31,6 +31,7 @@ import { databaseService } from '../db/service'
31
31
  import { TABLES } from '../db/tables'
32
32
  import type { AutonomousJobQueuePayload } from '../queues/autonomous-job.queue'
33
33
  import { extractMessageText } from '../runtime/workstream-chat-helpers'
34
+ import { buildAutonomousAtJobId, encodeBullmqId } from '../utils/autonomous-job-ids'
34
35
  import { toIsoDateTimeString, toOptionalIsoDateTimeString } from '../utils/date-time'
35
36
  import { compactRecord, compactWhitespace, stringifyUnknown, truncateText } from '../utils/string'
36
37
  import { executionPlanService } from './execution-plan.service'
@@ -41,14 +42,6 @@ import { workstreamService } from './workstream.service'
41
42
 
42
43
  const AUTONOMOUS_JOB_QUEUE_NAME = 'autonomous-job'
43
44
 
44
- function encodeBullmqId(raw: string): string {
45
- return Buffer.from(raw).toString('base64url')
46
- }
47
-
48
- function buildAutonomousAtJobId(autonomousJobId: string): string {
49
- return `autonomous-at-${encodeBullmqId(autonomousJobId)}`
50
- }
51
-
52
45
  function buildAutonomousManualJobId(autonomousJobId: string): string {
53
46
  return `autonomous-manual-${encodeBullmqId(autonomousJobId)}-${Date.now()}`
54
47
  }