@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
@@ -11,10 +11,6 @@ import { databaseService } from '../db/service'
11
11
  import type { DatabaseTable } from '../db/tables'
12
12
  import { TABLES } from '../db/tables'
13
13
  import { getRedisConnection, withRedisLeaseLock } from '../redis'
14
- import {
15
- MEMORY_BLOCK_COMPACTION_CHUNK_ENTRIES,
16
- MEMORY_BLOCK_COMPACTION_TRIGGER_ENTRIES,
17
- } from '../runtime/context-compaction-constants'
18
14
  import {
19
15
  appendToMemoryBlock,
20
16
  compactMemoryBlockEntries,
@@ -25,44 +21,13 @@ import {
25
21
  import { toIsoDateTimeString } from '../utils/date-time'
26
22
  import { chatRunRegistry } from './chat-run-registry.service'
27
23
  import { contextCompactionService } from './context-compaction.service'
24
+ import { MEMORY_BLOCK_COMPACTION_CHUNK_ENTRIES, MEMORY_BLOCK_COMPACTION_TRIGGER_ENTRIES } from './workstream-constants'
28
25
  import { workstreamMessageService } from './workstream-message.service'
29
- import { WorkstreamSchema } from './workstream.types'
30
- import type { NormalizedWorkstream, WorkstreamRecord } from './workstream.types'
26
+ import { NormalizedWorkstreamSchema, PublicWorkstreamSchema, WorkstreamSchema } from './workstream.types'
27
+ import type { NormalizedWorkstream, PublicWorkstream, WorkstreamRecord } from './workstream.types'
31
28
 
32
29
  // Uses SurrealQL directly to keep pagination/order logic close to queries.
33
30
 
34
- const LIST_WORKSTREAMS_QUERY = `SELECT * FROM ${TABLES.WORKSTREAM}
35
- WHERE userId = $userId
36
- AND organizationId = $orgId
37
- AND mode = $mode
38
- AND core = $core
39
- ORDER BY updatedAt DESC
40
- LIMIT $limit START $offset`
41
-
42
- const LIST_WORKSTREAMS_EXCLUDE_ARCHIVED_QUERY = `SELECT * FROM ${TABLES.WORKSTREAM}
43
- WHERE userId = $userId
44
- AND organizationId = $orgId
45
- AND mode = $mode
46
- AND core = $core
47
- AND status = "regular"
48
- ORDER BY updatedAt DESC
49
- LIMIT $limit START $offset`
50
-
51
- const LIST_ALL_WORKSTREAMS_BY_MODE_QUERY = `SELECT * FROM ${TABLES.WORKSTREAM}
52
- WHERE userId = $userId
53
- AND organizationId = $orgId
54
- AND mode = $mode
55
- AND core = $core
56
- ORDER BY updatedAt DESC`
57
-
58
- const LIST_ALL_WORKSTREAMS_BY_MODE_EXCLUDE_ARCHIVED_QUERY = `SELECT * FROM ${TABLES.WORKSTREAM}
59
- WHERE userId = $userId
60
- AND organizationId = $orgId
61
- AND mode = $mode
62
- AND core = $core
63
- AND status = "regular"
64
- ORDER BY updatedAt DESC`
65
-
66
31
  const WORKSTREAM_ACTIVE_RUN_LOCK_TTL_MS = 90_000
67
32
  const WORKSTREAM_ACTIVE_RUN_LOCK_MAX_WAIT_MS = 750
68
33
  const WORKSTREAM_ACTIVE_RUN_LOCK_RETRY_DELAY_MS = 75
@@ -116,6 +81,33 @@ function buildActiveRunLockKey(workstreamId: RecordIdRef): string {
116
81
  return `workstream-active-run:${recordIdToString(ensureRecordId(workstreamId, TABLES.WORKSTREAM), TABLES.WORKSTREAM)}`
117
82
  }
118
83
 
84
+ function buildListWorkstreamsQuery(options: { includeArchived: boolean; paginate: boolean }): string {
85
+ const clauses = [
86
+ `SELECT * FROM ${TABLES.WORKSTREAM}`,
87
+ 'WHERE userId = $userId',
88
+ ' AND organizationId = $orgId',
89
+ ' AND mode = $mode',
90
+ ' AND core = $core',
91
+ ]
92
+ if (!options.includeArchived) {
93
+ clauses.push(' AND status = "regular"')
94
+ }
95
+ clauses.push('ORDER BY updatedAt DESC')
96
+ if (options.paginate) {
97
+ clauses.push('LIMIT $limit START $offset')
98
+ }
99
+ return clauses.join('\n')
100
+ }
101
+
102
+ function normalizeActiveTurnValue(value: unknown): string | null {
103
+ if (typeof value !== 'string') {
104
+ return null
105
+ }
106
+
107
+ const normalized = value.trim()
108
+ return normalized.length > 0 ? normalized : null
109
+ }
110
+
119
111
  export class ActiveWorkstreamRunConflictError extends Error {
120
112
  constructor() {
121
113
  super('A chat run is already active.')
@@ -201,37 +193,16 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
201
193
  if (mode === 'direct') {
202
194
  const agentId = requireDirectAgentId(directAgentId)
203
195
  const directWorkstreamId = buildDirectWorkstreamId({ userId, orgId, agentId })
204
-
205
- const existing = await this.findById(directWorkstreamId)
206
- if (existing) {
207
- return await this.toNormalizedWorkstream(existing)
208
- }
209
-
210
- let createError: unknown = null
211
-
212
- let workstream = await databaseService
213
- .createWithId(
214
- TABLES.WORKSTREAM,
215
- directWorkstreamId,
216
- { userId, organizationId: orgId, mode, core: false, agentId, title, status: 'regular', nameGenerated: true },
217
- WorkstreamSchema,
218
- )
219
- .catch((error) => {
220
- createError = error
221
- return null
222
- })
223
-
224
- if (!workstream) {
225
- workstream = await this.findById(directWorkstreamId)
226
- }
227
-
228
- if (!workstream) {
229
- if (createError instanceof Error) {
230
- throw createError
231
- }
232
- throw new Error('Failed to create or load direct workstream')
233
- }
234
-
196
+ const workstream = await this.upsertDeterministicWorkstream(directWorkstreamId, {
197
+ userId,
198
+ organizationId: orgId,
199
+ mode,
200
+ core: false,
201
+ agentId,
202
+ title,
203
+ status: 'regular',
204
+ nameGenerated: true,
205
+ })
235
206
  return await this.toNormalizedWorkstream(workstream)
236
207
  }
237
208
 
@@ -239,46 +210,17 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
239
210
  const resolvedCoreType = requireString(coreType)
240
211
  const coreProfile = getCoreWorkstreamProfile(resolvedCoreType)
241
212
  const coreWorkstreamId = buildCoreWorkstreamId({ userId, orgId, coreType: resolvedCoreType })
242
- const existing = await this.findById(coreWorkstreamId)
243
- if (existing) {
244
- return await this.toNormalizedWorkstream(existing)
245
- }
246
-
247
- let createError: unknown = null
248
-
249
- let workstream = await databaseService
250
- .createWithId(
251
- TABLES.WORKSTREAM,
252
- coreWorkstreamId,
253
- {
254
- userId,
255
- organizationId: orgId,
256
- mode,
257
- core: true,
258
- coreType: resolvedCoreType,
259
- agentId: coreProfile.config.agentId,
260
- title,
261
- status: 'regular',
262
- nameGenerated: true,
263
- },
264
- WorkstreamSchema,
265
- )
266
- .catch((error) => {
267
- createError = error
268
- return null
269
- })
270
-
271
- if (!workstream) {
272
- workstream = await this.findById(coreWorkstreamId)
273
- }
274
-
275
- if (!workstream) {
276
- if (createError instanceof Error) {
277
- throw createError
278
- }
279
- throw new Error('Failed to create or load core workstream')
280
- }
281
-
213
+ const workstream = await this.upsertDeterministicWorkstream(coreWorkstreamId, {
214
+ userId,
215
+ organizationId: orgId,
216
+ mode,
217
+ core: true,
218
+ coreType: resolvedCoreType,
219
+ agentId: coreProfile.config.agentId,
220
+ title,
221
+ status: 'regular',
222
+ nameGenerated: true,
223
+ })
282
224
  return await this.toNormalizedWorkstream(workstream)
283
225
  }
284
226
 
@@ -395,22 +337,23 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
395
337
  }
396
338
 
397
339
  if (options.mode === 'direct' || core) {
398
- const query = includeArchived
399
- ? LIST_ALL_WORKSTREAMS_BY_MODE_QUERY
400
- : LIST_ALL_WORKSTREAMS_BY_MODE_EXCLUDE_ARCHIVED_QUERY
401
340
  const workstreams = await databaseService.queryMany<typeof WorkstreamSchema>(
402
- new BoundQuery(query, { userId, orgId, mode: options.mode, core }),
341
+ new BoundQuery(buildListWorkstreamsQuery({ includeArchived, paginate: false }), {
342
+ userId,
343
+ orgId,
344
+ mode: options.mode,
345
+ core,
346
+ }),
403
347
  WorkstreamSchema,
404
348
  )
405
349
 
406
- return { workstreams: await this.toNormalizedWorkstreams(workstreams), hasMore: false }
350
+ return { workstreams: await this.toNormalizedWorkstreams(workstreams, { checkLease: false }), hasMore: false }
407
351
  }
408
352
 
409
353
  const take = options.take ?? WORKSTREAM.DEFAULT_PAGE_LIMIT
410
354
  const page = options.page ?? 1
411
- const query = includeArchived ? LIST_WORKSTREAMS_QUERY : LIST_WORKSTREAMS_EXCLUDE_ARCHIVED_QUERY
412
355
  const workstreams = await databaseService.queryMany<typeof WorkstreamSchema>(
413
- new BoundQuery(query, {
356
+ new BoundQuery(buildListWorkstreamsQuery({ includeArchived, paginate: true }), {
414
357
  userId,
415
358
  orgId,
416
359
  mode: options.mode,
@@ -424,7 +367,7 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
424
367
  const hasMore = workstreams.length > take
425
368
  const sliced = hasMore ? workstreams.slice(0, take) : workstreams
426
369
 
427
- return { workstreams: await this.toNormalizedWorkstreams(sliced), hasMore }
370
+ return { workstreams: await this.toNormalizedWorkstreams(sliced, { checkLease: false }), hasMore }
428
371
  }
429
372
 
430
373
  async listOrganizationWorkstreams(params: {
@@ -466,7 +409,7 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
466
409
  WorkstreamSchema,
467
410
  )
468
411
 
469
- return await this.toNormalizedWorkstreams(workstreams)
412
+ return await this.toNormalizedWorkstreams(workstreams, { checkLease: false })
470
413
  }
471
414
 
472
415
  async getWorkstream(workstreamId: RecordIdRef): Promise<NormalizedWorkstream> {
@@ -489,28 +432,35 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
489
432
  return await this.toNormalizedWorkstream(workstream)
490
433
  }
491
434
 
492
- async setActiveRunId(workstreamId: RecordIdRef, runId: string | null): Promise<void> {
435
+ async setActiveTurn(workstreamId: RecordIdRef, runId: string, streamId?: string | null): Promise<void> {
493
436
  const workstreamRef = ensureRecordId(workstreamId, TABLES.WORKSTREAM)
494
- if (runId === null) {
437
+ if (streamId === null || streamId === undefined) {
495
438
  await databaseService.query<unknown>(surql`
496
439
  UPDATE ONLY ${workstreamRef}
497
- SET activeRunId = NONE
440
+ SET activeRunId = ${runId},
441
+ activeStreamId = NONE
498
442
  `)
499
443
  return
500
444
  }
501
445
 
502
446
  await databaseService.query<unknown>(surql`
503
447
  UPDATE ONLY ${workstreamRef}
504
- SET activeRunId = ${runId}
448
+ SET activeRunId = ${runId},
449
+ activeStreamId = ${streamId}
505
450
  `)
506
451
  }
507
452
 
508
- async getActiveRunId(workstreamId: RecordIdRef): Promise<string | null> {
453
+ async getActiveTurn(workstreamId: RecordIdRef): Promise<{ runId: string | null; streamId: string | null }> {
509
454
  const workstream = await this.getById(workstreamId)
510
- const activeRunId = workstream.activeRunId
511
- if (typeof activeRunId !== 'string') return null
512
- const normalized = activeRunId.trim()
513
- return normalized.length > 0 ? normalized : null
455
+ return {
456
+ runId: normalizeActiveTurnValue(workstream.activeRunId),
457
+ streamId: normalizeActiveTurnValue(workstream.activeStreamId),
458
+ }
459
+ }
460
+
461
+ async getActiveRunId(workstreamId: RecordIdRef): Promise<string | null> {
462
+ const { runId } = await this.getActiveTurn(workstreamId)
463
+ return runId
514
464
  }
515
465
 
516
466
  async hasActiveRunLease(workstreamId: RecordIdRef): Promise<boolean> {
@@ -540,52 +490,43 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
540
490
  }
541
491
  }
542
492
 
543
- async clearActiveRunIdIfMatches(workstreamId: RecordIdRef, runId: string): Promise<void> {
544
- const workstreamRef = ensureRecordId(workstreamId, TABLES.WORKSTREAM)
545
- await databaseService.query(surql`UPDATE ONLY ${workstreamRef} SET activeRunId = NONE WHERE activeRunId = ${runId}`)
493
+ async getActiveStreamId(workstreamId: RecordIdRef): Promise<string | null> {
494
+ const { streamId } = await this.getActiveTurn(workstreamId)
495
+ return streamId
546
496
  }
547
497
 
548
- async setActiveStreamId(workstreamId: RecordIdRef, streamId: string): Promise<void> {
498
+ async clearActiveTurn(workstreamId: RecordIdRef, params: { runId: string; streamId?: string | null }): Promise<void> {
549
499
  const workstreamRef = ensureRecordId(workstreamId, TABLES.WORKSTREAM)
550
- await databaseService.query<unknown>(surql`
500
+ const currentStreamId = params.streamId ?? null
501
+ if (currentStreamId === null) {
502
+ await databaseService.query(
503
+ surql`UPDATE ONLY ${workstreamRef} SET activeRunId = NONE, activeStreamId = NONE WHERE activeRunId = ${params.runId}`,
504
+ )
505
+ return
506
+ }
507
+
508
+ await databaseService.query(surql`
551
509
  UPDATE ONLY ${workstreamRef}
552
- SET activeStreamId = ${streamId}
510
+ SET activeRunId = NONE,
511
+ activeStreamId = NONE
512
+ WHERE activeRunId = ${params.runId} AND activeStreamId = ${currentStreamId}
553
513
  `)
554
514
  }
555
515
 
556
- async getActiveStreamId(workstreamId: RecordIdRef): Promise<string | null> {
557
- const workstream = await this.getById(workstreamId)
558
- const activeStreamId = workstream.activeStreamId
559
- if (typeof activeStreamId !== 'string') return null
560
- const normalized = activeStreamId.trim()
561
- return normalized.length > 0 ? normalized : null
562
- }
563
-
564
- async clearActiveStreamIdIfMatches(workstreamId: RecordIdRef, streamId: string): Promise<void> {
565
- const workstreamRef = ensureRecordId(workstreamId, TABLES.WORKSTREAM)
566
- await databaseService.query(
567
- surql`UPDATE ONLY ${workstreamRef} SET activeStreamId = NONE WHERE activeStreamId = ${streamId}`,
568
- )
569
- }
570
-
571
516
  async clearStaleActiveRunIfMissingFromRegistry(workstreamId: RecordIdRef): Promise<boolean> {
572
- const activeRunId = await this.getActiveRunId(workstreamId)
517
+ const { runId: activeRunId, streamId: activeStreamId } = await this.getActiveTurn(workstreamId)
573
518
  if (!activeRunId || (await this.hasActiveRunLease(workstreamId))) {
574
519
  return false
575
520
  }
576
521
 
577
- const activeStreamId = await this.getActiveStreamId(workstreamId)
578
- await Promise.all([
579
- this.clearActiveRunIdIfMatches(workstreamId, activeRunId),
580
- activeStreamId ? this.clearActiveStreamIdIfMatches(workstreamId, activeStreamId) : Promise.resolve(),
581
- ])
522
+ await this.clearActiveTurn(workstreamId, { runId: activeRunId, streamId: activeStreamId })
582
523
 
583
524
  serverLogger.warn`Cleared stale workstream run after lease expired: workstream=${recordIdToString(ensureRecordId(workstreamId, TABLES.WORKSTREAM), TABLES.WORKSTREAM)} run=${activeRunId}`
584
525
  return true
585
526
  }
586
527
 
587
528
  async stopActiveRun(workstreamId: RecordIdRef): Promise<boolean> {
588
- const activeRunId = await this.getActiveRunId(workstreamId)
529
+ const { runId: activeRunId } = await this.getActiveTurn(workstreamId)
589
530
  if (!activeRunId) return false
590
531
 
591
532
  const stopped = chatRunRegistry.stop(activeRunId, new DOMException('Run stopped by user.', 'AbortError'))
@@ -597,19 +538,11 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
597
538
  return false
598
539
  }
599
540
 
600
- async markCompacting(workstreamId: RecordIdRef): Promise<void> {
541
+ async setCompacting(workstreamId: RecordIdRef, value: boolean): Promise<void> {
601
542
  const workstreamRef = ensureRecordId(workstreamId, TABLES.WORKSTREAM)
602
543
  await databaseService.query<unknown>(surql`
603
544
  UPDATE ONLY ${workstreamRef}
604
- SET isCompacting = ${true}
605
- `)
606
- }
607
-
608
- async clearCompacting(workstreamId: RecordIdRef): Promise<void> {
609
- const workstreamRef = ensureRecordId(workstreamId, TABLES.WORKSTREAM)
610
- await databaseService.query<unknown>(surql`
611
- UPDATE ONLY ${workstreamRef}
612
- SET isCompacting = ${false}
545
+ SET isCompacting = ${value}
613
546
  `)
614
547
  }
615
548
 
@@ -699,7 +632,7 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
699
632
  WorkstreamSchema,
700
633
  )
701
634
 
702
- return await this.toNormalizedWorkstreams(workstreams)
635
+ return await this.toNormalizedWorkstreams(workstreams, { checkLease: false })
703
636
  }
704
637
 
705
638
  private normalizeWorkstreamId(id: unknown): string {
@@ -729,7 +662,10 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
729
662
  return WORKSTREAM.DEFAULT_TITLE
730
663
  }
731
664
 
732
- private async computeIsRunning(workstream: Pick<WorkstreamRecord, 'id' | 'activeRunId'>): Promise<boolean> {
665
+ private async computeIsRunning(
666
+ workstream: Pick<WorkstreamRecord, 'id' | 'activeRunId'>,
667
+ options: { checkLease: boolean },
668
+ ): Promise<boolean> {
733
669
  const activeRunId =
734
670
  typeof workstream.activeRunId === 'string' && workstream.activeRunId.trim().length > 0
735
671
  ? workstream.activeRunId
@@ -743,17 +679,24 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
743
679
  return true
744
680
  }
745
681
 
682
+ if (!options.checkLease) {
683
+ return true
684
+ }
685
+
746
686
  return await this.hasActiveRunLease(ensureRecordId(workstream.id, TABLES.WORKSTREAM))
747
687
  }
748
688
 
749
- private async toNormalizedWorkstream(workstream: WorkstreamRecord): Promise<NormalizedWorkstream> {
750
- const isRunning = await this.computeIsRunning(workstream)
689
+ private async toNormalizedWorkstream(
690
+ workstream: WorkstreamRecord,
691
+ options: { checkLease?: boolean } = {},
692
+ ): Promise<NormalizedWorkstream> {
693
+ const isRunning = await this.computeIsRunning(workstream, { checkLease: options.checkLease ?? true })
751
694
  const isCompacting = workstream.isCompacting === true
752
695
  const mode = workstream.mode
753
696
  const core = workstream.core
754
697
  const coreType = core && typeof workstream.coreType === 'string' ? workstream.coreType : undefined
755
698
  const status = workstream.status
756
- return {
699
+ return NormalizedWorkstreamSchema.parse({
757
700
  id: this.normalizeWorkstreamId(workstream.id),
758
701
  userId: this.normalizeRecordIdString(workstream.userId, TABLES.USER),
759
702
  organizationId: this.normalizeRecordIdString(workstream.organizationId, TABLES.ORGANIZATION),
@@ -769,28 +712,26 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
769
712
  memoryBlock: this.formatMemoryBlockForPrompt(workstream),
770
713
  createdAt: toIsoDateTimeString(workstream.createdAt),
771
714
  updatedAt: toIsoDateTimeString(workstream.updatedAt),
772
- }
715
+ })
773
716
  }
774
717
 
775
- private async toNormalizedWorkstreams(workstreams: WorkstreamRecord[]): Promise<NormalizedWorkstream[]> {
776
- return await Promise.all(workstreams.map(async (workstream) => await this.toNormalizedWorkstream(workstream)))
718
+ private async toNormalizedWorkstreams(
719
+ workstreams: WorkstreamRecord[],
720
+ options: { checkLease?: boolean } = {},
721
+ ): Promise<NormalizedWorkstream[]> {
722
+ return await Promise.all(
723
+ workstreams.map(async (workstream) => await this.toNormalizedWorkstream(workstream, options)),
724
+ )
777
725
  }
778
726
 
779
- toPublicWorkstream(workstream: NormalizedWorkstream) {
780
- return {
781
- id: workstream.id,
782
- mode: workstream.mode,
783
- core: workstream.core,
784
- ...(workstream.coreType ? { coreType: workstream.coreType } : {}),
785
- ...(workstream.agentId ? { agentId: workstream.agentId } : {}),
786
- title: workstream.title,
787
- status: workstream.status,
788
- nameGenerated: workstream.nameGenerated,
789
- isRunning: workstream.isRunning,
790
- isCompacting: workstream.isCompacting,
791
- createdAt: workstream.createdAt,
792
- updatedAt: workstream.updatedAt,
793
- }
727
+ toPublicWorkstream(workstream: NormalizedWorkstream): PublicWorkstream {
728
+ const {
729
+ organizationId: _organizationId,
730
+ userId: _userId,
731
+ memoryBlock: _memoryBlock,
732
+ ...publicWorkstream
733
+ } = workstream
734
+ return PublicWorkstreamSchema.parse(publicWorkstream)
794
735
  }
795
736
 
796
737
  async incrementTurnCount(workstreamId: RecordIdRef): Promise<number> {
@@ -803,10 +744,6 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
803
744
  return result[0].turnCount
804
745
  }
805
746
 
806
- async persistGeneratedTitle(workstreamId: RecordIdRef, title: string): Promise<void> {
807
- await this.update(workstreamId, { title, nameGenerated: true })
808
- }
809
-
810
747
  private assertMutableWorkstream(
811
748
  workstream: WorkstreamRecord,
812
749
  action: 'rename' | 'archive' | 'unarchive' | 'delete',
@@ -818,6 +755,39 @@ class WorkstreamService extends BaseService<typeof WorkstreamSchema> {
818
755
  throw new Error(`Core workstreams cannot be ${action}d`)
819
756
  }
820
757
  }
758
+
759
+ private async upsertDeterministicWorkstream(
760
+ workstreamId: RecordIdRef,
761
+ data: Record<string, unknown>,
762
+ ): Promise<WorkstreamRecord> {
763
+ const workstreamRef = ensureRecordId(workstreamId, TABLES.WORKSTREAM)
764
+ const existing = await this.findById(workstreamRef)
765
+ if (existing) {
766
+ return existing
767
+ }
768
+
769
+ let createError: unknown = null
770
+ let workstream = await databaseService
771
+ .createWithId(TABLES.WORKSTREAM, workstreamRef, data, WorkstreamSchema)
772
+ .catch((error) => {
773
+ createError = error
774
+ return null
775
+ })
776
+
777
+ if (!workstream) {
778
+ workstream = await this.findById(workstreamRef)
779
+ }
780
+
781
+ if (workstream) {
782
+ return workstream
783
+ }
784
+
785
+ if (createError instanceof Error) {
786
+ throw createError
787
+ }
788
+
789
+ throw new Error('Failed to create or load deterministic workstream')
790
+ }
821
791
  }
822
792
 
823
793
  export const workstreamService = new WorkstreamService()
@@ -1,49 +1,17 @@
1
- import { recordIdSchema, sdkWorkstreamStatusSchema } from '@lota-sdk/shared'
1
+ import { sdkPublicWorkstreamSchema, sdkWorkstreamRecordSchema, sdkWorkstreamSchema } from '@lota-sdk/shared'
2
+ import type { SdkPublicWorkstream, SdkWorkstreamRecord } from '@lota-sdk/shared'
2
3
  import { z } from 'zod'
3
4
 
4
- const WorkstreamModeSchema = z.enum(['direct', 'group'])
5
- const CoreWorkstreamTypeSchema = z.string()
5
+ export const WorkstreamSchema = sdkWorkstreamRecordSchema
6
+ export type WorkstreamRecord = SdkWorkstreamRecord
6
7
 
7
- export interface NormalizedWorkstream {
8
- id: string
9
- title: string
10
- status: 'regular' | 'archived'
11
- mode: 'direct' | 'group'
12
- core: boolean
13
- coreType?: string
14
- nameGenerated: boolean // Ideally `isNameGenerated`, but maps directly to SurrealDB column `nameGenerated`
15
- isRunning: boolean
16
- isCompacting: boolean
17
- agentId?: string | null
18
- memoryBlock: string
19
- createdAt: string
20
- updatedAt: string
21
- userId: string
22
- organizationId: string
23
- }
24
-
25
- export const WorkstreamSchema = z.object({
26
- id: recordIdSchema,
27
- mode: WorkstreamModeSchema,
28
- core: z.boolean(),
29
- coreType: CoreWorkstreamTypeSchema.nullish(),
30
- agentId: z.string().nullish(),
31
- title: z.string().nullish(),
32
- status: sdkWorkstreamStatusSchema,
33
- memoryBlock: z.string().nullish(),
34
- memoryBlockSummary: z.string().nullish(),
35
- activeRunId: z.string().nullish(),
36
- activeStreamId: z.string().nullish(),
37
- compactionSummary: z.string().nullish(),
38
- lastCompactedMessageId: z.string().nullish(),
39
- nameGenerated: z.boolean(), // Ideally `isNameGenerated`, but maps directly to SurrealDB column `nameGenerated`
40
- isCompacting: z.boolean().optional(),
41
- state: z.unknown().optional(),
42
- turnCount: z.number().int(),
43
- createdAt: z.coerce.date(),
44
- updatedAt: z.coerce.date(),
45
- userId: recordIdSchema,
46
- organizationId: recordIdSchema,
8
+ export const NormalizedWorkstreamSchema = sdkWorkstreamSchema.extend({
9
+ agentId: z.string().optional(),
10
+ coreType: z.string().optional(),
11
+ memoryBlock: z.string(),
47
12
  })
48
13
 
49
- export type WorkstreamRecord = z.infer<typeof WorkstreamSchema>
14
+ export type NormalizedWorkstream = z.infer<typeof NormalizedWorkstreamSchema>
15
+ export type PublicWorkstream = SdkPublicWorkstream
16
+
17
+ export { sdkPublicWorkstreamSchema as PublicWorkstreamSchema }
@@ -52,6 +52,9 @@ produce one updated workspace profile summary plus durable memory facts.
52
52
  - Set type to one of: fact, preference, decision.
53
53
  - Set confidence between 0 and 1.
54
54
  - Set durability to core, standard, or ephemeral based on expected longevity.
55
+ - Set importance between 0 and 1 for long-term usefulness.
56
+ - Set classification to durable, transient, or uncertain.
57
+ - Set rationale to one short evidence-grounded sentence.
55
58
  </facts-format>
56
59
 
57
60
  <output-contract>
@@ -5,6 +5,7 @@ import {
5
5
  ResumeExecutionPlanRunArgsSchema,
6
6
  ReplaceExecutionPlanArgsSchema,
7
7
  SubmitExecutionNodeResultArgsSchema,
8
+ expandAgentPlanDraft,
8
9
  getLatestExecutionPlanResult,
9
10
  } from '@lota-sdk/shared'
10
11
  import { tool } from 'ai'
@@ -17,17 +18,21 @@ export function createCreateExecutionPlanTool(params: {
17
18
  workstreamId: RecordIdRef
18
19
  agentId: string
19
20
  onPlanChanged?: () => void
21
+ validateInlinePlan?: (draft: ReturnType<typeof expandAgentPlanDraft>) => void
20
22
  }) {
21
23
  return tool({
22
24
  description:
23
- 'Create a contract-driven execution plan for this workstream. Nodes must define deliverables, success criteria, completion checks, and executor policy.',
25
+ 'Create a contract-driven execution plan for this workstream. Use createProjectWithPlan instead when the work needs a dedicated project workstream or a larger 3+ node plan.',
24
26
  inputSchema: CreateExecutionPlanArgsSchema,
25
27
  execute: async (input) => {
28
+ const { targetWorkstreamId, ...draftInput } = input
29
+ const draft = expandAgentPlanDraft(draftInput)
30
+ params.validateInlinePlan?.(draft)
26
31
  const result = await executionPlanService.createPlan({
27
32
  organizationId: params.orgId,
28
- workstreamId: params.workstreamId,
33
+ workstreamId: targetWorkstreamId ?? params.workstreamId,
29
34
  leadAgentId: params.agentId,
30
- input,
35
+ input: draft,
31
36
  })
32
37
  params.onPlanChanged?.()
33
38
  return result
@@ -46,11 +51,12 @@ export function createReplaceExecutionPlanTool(params: {
46
51
  'Replace the active execution run with a new contract-driven plan when the graph, contracts, or approach materially change.',
47
52
  inputSchema: ReplaceExecutionPlanArgsSchema,
48
53
  execute: async (input) => {
54
+ const { runId, reason, ...draftInput } = input
49
55
  const result = await executionPlanService.replacePlan({
50
56
  organizationId: params.orgId,
51
57
  workstreamId: params.workstreamId,
52
58
  leadAgentId: params.agentId,
53
- input,
59
+ input: { runId, reason, ...expandAgentPlanDraft(draftInput) },
54
60
  })
55
61
  params.onPlanChanged?.()
56
62
  return result
@@ -95,8 +101,7 @@ export function createListExecutionPlansTool(params: { workstreamId: RecordIdRef
95
101
 
96
102
  export function createGetExecutionPlanDetailsTool(params: { workstreamId: RecordIdRef }) {
97
103
  return tool({
98
- description:
99
- 'Load a specific execution run by runId, or the most recent active run if runId is omitted. Returns full graph state, node contracts, events, approvals, artifacts, and checkpoints.',
104
+ description: 'Load a plan run and its current state.',
100
105
  inputSchema: GetActiveExecutionPlanArgsSchema,
101
106
  execute: async (input) =>
102
107
  await executionPlanService.getActivePlanToolResult({
@@ -1,6 +1,7 @@
1
1
  export * from './execution-plan.tool'
2
2
  export * from './fetch-webpage.tool'
3
3
  export * from './memory-block.tool'
4
+ export * from './project-with-plan.tool'
4
5
  export * from './read-file-parts.tool'
5
6
  export * from './remember-memory.tool'
6
7
  export * from './research-topic.tool'