@lota-sdk/core 0.1.8 → 0.1.11
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/infrastructure/schema/00_workstream.surql +2 -1
- package/infrastructure/schema/02_execution_plan.surql +202 -52
- package/package.json +4 -2
- package/src/bifrost/bifrost.ts +94 -25
- package/src/config/model-constants.ts +8 -6
- package/src/db/memory-store.ts +3 -71
- package/src/db/service.ts +42 -2
- package/src/db/tables.ts +9 -2
- package/src/embeddings/provider.ts +92 -21
- package/src/index.ts +6 -0
- package/src/redis/stream-context.ts +44 -0
- package/src/runtime/approval-continuation.ts +59 -0
- package/src/runtime/chat-request-routing.ts +5 -1
- package/src/runtime/execution-plan.ts +21 -14
- package/src/runtime/turn-lifecycle.ts +14 -6
- package/src/runtime/workstream-chat-helpers.ts +5 -5
- package/src/services/context-compaction.service.ts +6 -2
- package/src/services/document-chunk.service.ts +2 -2
- package/src/services/execution-plan.service.ts +579 -786
- package/src/services/learned-skill.service.ts +2 -2
- package/src/services/plan-approval.service.ts +83 -0
- package/src/services/plan-artifact.service.ts +45 -0
- package/src/services/plan-builder.service.ts +61 -0
- package/src/services/plan-checkpoint.service.ts +53 -0
- package/src/services/plan-compiler.service.ts +81 -0
- package/src/services/plan-executor.service.ts +1623 -0
- package/src/services/plan-run.service.ts +422 -0
- package/src/services/plan-validator.service.ts +760 -0
- package/src/services/workstream-turn-preparation.ts +70 -196
- package/src/services/workstream-turn.ts +12 -0
- package/src/services/workstream.service.ts +24 -182
- package/src/services/workstream.types.ts +2 -65
- package/src/system-agents/title-generator.agent.ts +2 -2
- package/src/tools/execution-plan.tool.ts +20 -46
- package/src/tools/log-hello-world.tool.ts +17 -0
- package/src/workers/skill-extraction.runner.ts +2 -2
- package/src/services/workstream-change-tracker.service.ts +0 -313
- package/src/system-agents/workstream-tracker.agent.ts +0 -58
package/src/db/service.ts
CHANGED
|
@@ -66,6 +66,8 @@ export type CreateMutationBuilder = {
|
|
|
66
66
|
export interface DatabaseTransaction {
|
|
67
67
|
query: (query: unknown) => Promise<unknown>
|
|
68
68
|
create: (target: unknown) => CreateMutationBuilder
|
|
69
|
+
update: (target: unknown) => MutationBuilder
|
|
70
|
+
delete: (target: unknown) => Promise<unknown>
|
|
69
71
|
relate: (from: unknown, edgeTable: unknown, to: unknown, data?: Values<Record<string, unknown>>) => Promise<unknown>
|
|
70
72
|
commit: () => Promise<void>
|
|
71
73
|
cancel: () => Promise<void>
|
|
@@ -458,6 +460,38 @@ export class SurrealDBService {
|
|
|
458
460
|
throw new SurrealDBError('Invalid table value')
|
|
459
461
|
}
|
|
460
462
|
|
|
463
|
+
private isRecordIdLike(value: unknown): boolean {
|
|
464
|
+
if (value instanceof RecordId || value instanceof StringRecordId) {
|
|
465
|
+
return true
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (typeof value === 'string') {
|
|
469
|
+
return /^[a-zA-Z][a-zA-Z0-9_]*:/.test(value)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (value && typeof value === 'object') {
|
|
473
|
+
const record = value as { tb?: unknown; id?: unknown }
|
|
474
|
+
if (typeof record.tb === 'string' && record.id !== undefined) {
|
|
475
|
+
return true
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const stringValue = toStringLikeValue(value)
|
|
479
|
+
if (stringValue) {
|
|
480
|
+
return /^[a-zA-Z][a-zA-Z0-9_]*:/.test(stringValue)
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return false
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
private normalizeCreateTarget(value: unknown): Table | RecordId {
|
|
488
|
+
if (this.isRecordIdLike(value)) {
|
|
489
|
+
return ensureRecordId(value as RecordIdInput)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return this.normalizeTableValue(value)
|
|
493
|
+
}
|
|
494
|
+
|
|
461
495
|
private wrapMutationBuilder(builder: MutationBuilder): MutationBuilder {
|
|
462
496
|
return {
|
|
463
497
|
content: (data) => this.wrapMutationBuilder(builder.content(this.normalizeMutationData(data))),
|
|
@@ -478,8 +512,14 @@ export class SurrealDBService {
|
|
|
478
512
|
private wrapTransaction(tx: SurrealTransaction): DatabaseTransaction {
|
|
479
513
|
return {
|
|
480
514
|
query: (query: unknown) => tx.query(this.normalizeBoundQuery(query as BoundQuery | BoundQueryLike)),
|
|
481
|
-
create: (target: unknown) =>
|
|
482
|
-
this.
|
|
515
|
+
create: (target: unknown) => {
|
|
516
|
+
const normalizedTarget = this.normalizeCreateTarget(target)
|
|
517
|
+
const builder = normalizedTarget instanceof Table ? tx.create(normalizedTarget) : tx.create(normalizedTarget)
|
|
518
|
+
return this.wrapCreateBuilder(builder as unknown as CreateMutationBuilder)
|
|
519
|
+
},
|
|
520
|
+
update: (target: unknown) =>
|
|
521
|
+
this.wrapMutationBuilder(tx.update(ensureRecordId(target as RecordIdInput)) as unknown as MutationBuilder),
|
|
522
|
+
delete: (target: unknown) => tx.delete(ensureRecordId(target as RecordIdInput)),
|
|
483
523
|
relate: (from: unknown, edgeTable: unknown, to: unknown, data?: Values<Record<string, unknown>>) =>
|
|
484
524
|
tx.relate(
|
|
485
525
|
ensureRecordId(from as RecordIdInput),
|
package/src/db/tables.ts
CHANGED
|
@@ -7,8 +7,15 @@ export const TABLES = {
|
|
|
7
7
|
MEMORY_RELATION: 'memoryRelation',
|
|
8
8
|
MEMORY_HISTORY: 'memoryHistory',
|
|
9
9
|
LEARNED_SKILL: 'learnedSkill',
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
PLAN_SPEC: 'planSpec',
|
|
11
|
+
PLAN_NODE_SPEC: 'planNodeSpec',
|
|
12
|
+
PLAN_RUN: 'planRun',
|
|
13
|
+
PLAN_NODE_RUN: 'planNodeRun',
|
|
14
|
+
PLAN_NODE_ATTEMPT: 'planNodeAttempt',
|
|
15
|
+
PLAN_ARTIFACT: 'planArtifact',
|
|
16
|
+
PLAN_APPROVAL: 'planApproval',
|
|
17
|
+
PLAN_CHECKPOINT: 'planCheckpoint',
|
|
18
|
+
PLAN_VALIDATION_ISSUE: 'planValidationIssue',
|
|
12
19
|
PLAN_EVENT: 'planEvent',
|
|
13
20
|
ORGANIZATION: 'organization',
|
|
14
21
|
ORGANIZATION_MEMBER: 'organizationMember',
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import { embed, embedMany } from 'ai'
|
|
2
2
|
|
|
3
|
+
import { getEmbeddingCache } from '../ai/embedding-cache'
|
|
3
4
|
import { bifrostEmbeddingModel } from '../bifrost/bifrost'
|
|
4
5
|
import { env } from '../config/env-shapes'
|
|
5
6
|
|
|
6
7
|
const SUPPORTED_EMBEDDING_PREFIXES = ['openai/', 'openrouter/'] as const
|
|
7
8
|
|
|
9
|
+
type SharedEmbeddingCache = {
|
|
10
|
+
get(model: string, text: string): Promise<number[] | null>
|
|
11
|
+
set(model: string, text: string, embedding: number[]): Promise<void>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type ProviderEmbeddingsOptions = {
|
|
15
|
+
embedFn?: typeof embed
|
|
16
|
+
embedManyFn?: typeof embedMany
|
|
17
|
+
getCache?: () => SharedEmbeddingCache | null
|
|
18
|
+
modelId?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
8
21
|
function resolveEmbeddingModel(modelId: string) {
|
|
9
22
|
const normalized = modelId.trim()
|
|
10
23
|
if (!normalized) {
|
|
@@ -20,23 +33,54 @@ function resolveEmbeddingModel(modelId: string) {
|
|
|
20
33
|
return bifrostEmbeddingModel(normalized)
|
|
21
34
|
}
|
|
22
35
|
|
|
23
|
-
|
|
36
|
+
function normalizeEmbedding(embedding: readonly number[]): number[] {
|
|
37
|
+
return embedding.map((value) => Number(value))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class ProviderEmbeddings {
|
|
41
|
+
private readonly embedFn: typeof embed
|
|
42
|
+
private readonly embedManyFn: typeof embedMany
|
|
43
|
+
private readonly getCache: () => SharedEmbeddingCache | null
|
|
44
|
+
private readonly modelId: string
|
|
24
45
|
private _model: ReturnType<typeof resolveEmbeddingModel> | null = null
|
|
25
46
|
|
|
47
|
+
constructor(options: ProviderEmbeddingsOptions = {}) {
|
|
48
|
+
this.embedFn = options.embedFn ?? embed
|
|
49
|
+
this.embedManyFn = options.embedManyFn ?? embedMany
|
|
50
|
+
this.getCache = options.getCache ?? getEmbeddingCache
|
|
51
|
+
this.modelId = options.modelId ?? env.AI_EMBEDDING_MODEL
|
|
52
|
+
}
|
|
53
|
+
|
|
26
54
|
private getModel() {
|
|
27
55
|
if (!this._model) {
|
|
28
|
-
this._model = resolveEmbeddingModel(
|
|
56
|
+
this._model = resolveEmbeddingModel(this.modelId)
|
|
29
57
|
}
|
|
30
58
|
return this._model
|
|
31
59
|
}
|
|
32
60
|
|
|
61
|
+
private async loadCachedEmbedding(text: string): Promise<number[] | null> {
|
|
62
|
+
const redisCache = this.getCache()
|
|
63
|
+
if (!redisCache) return null
|
|
64
|
+
|
|
65
|
+
return await redisCache.get(this.modelId, text)
|
|
66
|
+
}
|
|
67
|
+
|
|
33
68
|
async embedQuery(text: string): Promise<number[]> {
|
|
34
69
|
const input = text.trim()
|
|
35
70
|
if (!input) return []
|
|
36
71
|
|
|
37
|
-
const
|
|
72
|
+
const cached = await this.loadCachedEmbedding(input)
|
|
73
|
+
if (cached) return cached
|
|
74
|
+
|
|
75
|
+
const result = await this.embedFn({ model: this.getModel(), value: input, maxRetries: 2 })
|
|
76
|
+
const embedding = normalizeEmbedding(result.embedding)
|
|
77
|
+
|
|
78
|
+
const redisCache = this.getCache()
|
|
79
|
+
if (redisCache) {
|
|
80
|
+
void redisCache.set(this.modelId, input, embedding)
|
|
81
|
+
}
|
|
38
82
|
|
|
39
|
-
return
|
|
83
|
+
return embedding
|
|
40
84
|
}
|
|
41
85
|
|
|
42
86
|
async embedDocuments(values: string[]): Promise<number[][]> {
|
|
@@ -51,26 +95,53 @@ class ProviderEmbeddings {
|
|
|
51
95
|
return normalized.map(() => [])
|
|
52
96
|
}
|
|
53
97
|
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const entry = nonEmptyEntries.at(index)
|
|
63
|
-
if (!entry) return
|
|
64
|
-
embeddingsByIndex.set(
|
|
65
|
-
entry.index,
|
|
66
|
-
embedding.map((value) => Number(value)),
|
|
98
|
+
const uniqueTexts = [...new Set(nonEmptyEntries.map((entry) => entry.value))]
|
|
99
|
+
const embeddingsByText = new Map<string, number[]>()
|
|
100
|
+
let missingTexts = [...uniqueTexts]
|
|
101
|
+
|
|
102
|
+
const redisCache = this.getCache()
|
|
103
|
+
if (redisCache && missingTexts.length > 0) {
|
|
104
|
+
const redisResults = await Promise.all(
|
|
105
|
+
missingTexts.map(async (text) => ({ text, embedding: await redisCache.get(this.modelId, text) })),
|
|
67
106
|
)
|
|
68
|
-
})
|
|
69
107
|
|
|
70
|
-
|
|
108
|
+
missingTexts = []
|
|
109
|
+
for (const result of redisResults) {
|
|
110
|
+
if (!result.embedding) {
|
|
111
|
+
missingTexts.push(result.text)
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
embeddingsByText.set(result.text, result.embedding)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (missingTexts.length > 0) {
|
|
120
|
+
const result = await this.embedManyFn({ model: this.getModel(), values: missingTexts, maxRetries: 2 })
|
|
121
|
+
|
|
122
|
+
missingTexts.forEach((text, index) => {
|
|
123
|
+
const embedding = normalizeEmbedding(result.embeddings[index] ?? [])
|
|
124
|
+
embeddingsByText.set(text, embedding)
|
|
125
|
+
if (redisCache) {
|
|
126
|
+
void redisCache.set(this.modelId, text, embedding)
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return normalized.map((text) => (text ? (embeddingsByText.get(text) ?? []) : []))
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let defaultEmbeddings: ProviderEmbeddings | null = null
|
|
136
|
+
|
|
137
|
+
export function getDefaultEmbeddings(): ProviderEmbeddings {
|
|
138
|
+
if (!defaultEmbeddings) {
|
|
139
|
+
defaultEmbeddings = new ProviderEmbeddings()
|
|
71
140
|
}
|
|
141
|
+
|
|
142
|
+
return defaultEmbeddings
|
|
72
143
|
}
|
|
73
144
|
|
|
74
|
-
export function
|
|
75
|
-
|
|
145
|
+
export function resetDefaultEmbeddingsForTests(): void {
|
|
146
|
+
defaultEmbeddings = null
|
|
76
147
|
}
|
package/src/index.ts
CHANGED
|
@@ -32,6 +32,7 @@ import type { workstreamMessageService } from './services/workstream-message.ser
|
|
|
32
32
|
import type { workstreamTitleService } from './services/workstream-title.service'
|
|
33
33
|
import type {
|
|
34
34
|
createWorkstreamApprovalContinuationStream,
|
|
35
|
+
createWorkstreamNativeToolApprovalStream,
|
|
35
36
|
createWorkstreamTurnStream,
|
|
36
37
|
runWorkstreamTurnInBackground,
|
|
37
38
|
} from './services/workstream-turn'
|
|
@@ -124,6 +125,7 @@ export interface LotaRuntime {
|
|
|
124
125
|
workstreamService: typeof workstreamService
|
|
125
126
|
workstreamTitleService: typeof workstreamTitleService
|
|
126
127
|
createWorkstreamApprovalContinuationStream: typeof createWorkstreamApprovalContinuationStream
|
|
128
|
+
createWorkstreamNativeToolApprovalStream: typeof createWorkstreamNativeToolApprovalStream
|
|
127
129
|
createWorkstreamTurnStream: typeof createWorkstreamTurnStream
|
|
128
130
|
isApprovalContinuationRequest: typeof isApprovalContinuationRequest
|
|
129
131
|
runWorkstreamTurnInBackground: typeof runWorkstreamTurnInBackground
|
|
@@ -291,6 +293,7 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
|
|
|
291
293
|
const { workstreamTitleService } = await import('./services/workstream-title.service')
|
|
292
294
|
const {
|
|
293
295
|
createWorkstreamApprovalContinuationStream,
|
|
296
|
+
createWorkstreamNativeToolApprovalStream,
|
|
294
297
|
createWorkstreamTurnStream,
|
|
295
298
|
isApprovalContinuationRequest,
|
|
296
299
|
runWorkstreamTurnInBackground,
|
|
@@ -431,6 +434,7 @@ export async function createLotaRuntime(config: LotaRuntimeConfig): Promise<Lota
|
|
|
431
434
|
workstreamService,
|
|
432
435
|
workstreamTitleService,
|
|
433
436
|
createWorkstreamApprovalContinuationStream,
|
|
437
|
+
createWorkstreamNativeToolApprovalStream,
|
|
434
438
|
createWorkstreamTurnStream,
|
|
435
439
|
isApprovalContinuationRequest,
|
|
436
440
|
runWorkstreamTurnInBackground,
|
|
@@ -495,5 +499,7 @@ function createPluginDatabaseConnector(pluginRuntime: Record<string, LotaPlugin>
|
|
|
495
499
|
export type { CoreWorkstreamProfile } from './config/agent-defaults'
|
|
496
500
|
export type { SurrealDBService } from './db/service'
|
|
497
501
|
export type { RedisConnectionManager } from './redis/connection'
|
|
502
|
+
export { createWorkstreamResumableContext } from './redis/stream-context'
|
|
498
503
|
export type { LotaPlugin, LotaPluginContributions } from './runtime/plugin-types'
|
|
499
504
|
export type { LotaRuntimeAdapters, LotaRuntimeTurnHooks } from './runtime/runtime-extensions'
|
|
505
|
+
export { createLogHelloWorldTool } from './tools/log-hello-world.tool'
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Redis } from 'ioredis'
|
|
2
|
+
import type { Publisher, Subscriber } from 'resumable-stream/ioredis'
|
|
3
|
+
import { createResumableStreamContext } from 'resumable-stream/ioredis'
|
|
4
|
+
|
|
5
|
+
import { getRedisConnection } from './index'
|
|
6
|
+
|
|
7
|
+
function toSubscriber(client: Redis): Subscriber {
|
|
8
|
+
const handlers = new Map<string, (message: string) => void>()
|
|
9
|
+
const messageListener = (channel: string, message: string) => {
|
|
10
|
+
handlers.get(channel)?.(message)
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
connect: () => Promise.resolve(),
|
|
14
|
+
subscribe: async (channel, callback) => {
|
|
15
|
+
if (handlers.size === 0) client.on('message', messageListener)
|
|
16
|
+
handlers.set(channel, callback)
|
|
17
|
+
await client.subscribe(channel)
|
|
18
|
+
},
|
|
19
|
+
unsubscribe: async (channel) => {
|
|
20
|
+
handlers.delete(channel)
|
|
21
|
+
if (handlers.size === 0) client.removeListener('message', messageListener)
|
|
22
|
+
return client.unsubscribe(channel)
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function toPublisher(client: Redis): Publisher {
|
|
28
|
+
return {
|
|
29
|
+
connect: () => Promise.resolve(),
|
|
30
|
+
publish: (channel, message) => client.publish(channel, message),
|
|
31
|
+
set: (key, value, options) => (options?.EX ? client.set(key, value, 'EX', options.EX) : client.set(key, value)),
|
|
32
|
+
get: (key) => client.get(key),
|
|
33
|
+
incr: (key) => client.incr(key),
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createWorkstreamResumableContext() {
|
|
38
|
+
const redis = getRedisConnection()
|
|
39
|
+
return createResumableStreamContext({
|
|
40
|
+
waitUntil: null,
|
|
41
|
+
subscriber: toSubscriber(redis.duplicate()),
|
|
42
|
+
publisher: toPublisher(redis),
|
|
43
|
+
})
|
|
44
|
+
}
|
|
@@ -4,6 +4,44 @@ export function hasApprovalRespondedParts(message: ChatMessage): boolean {
|
|
|
4
4
|
return message.parts.some((part) => 'state' in part && (part as { state: string }).state === 'approval-responded')
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
export interface ApprovalContinuationResponse {
|
|
8
|
+
approvalId: string
|
|
9
|
+
approved: boolean
|
|
10
|
+
comments?: string
|
|
11
|
+
requiredEdits: string[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function readApprovalContinuationResponse(message: ChatMessage): ApprovalContinuationResponse | null {
|
|
15
|
+
for (const part of message.parts) {
|
|
16
|
+
if (!('state' in part) || (part as { state: string }).state !== 'approval-responded') continue
|
|
17
|
+
const approval = (part as { approval?: unknown }).approval
|
|
18
|
+
if (!approval || typeof approval !== 'object') continue
|
|
19
|
+
|
|
20
|
+
const record = approval as {
|
|
21
|
+
id?: unknown
|
|
22
|
+
approved?: unknown
|
|
23
|
+
reason?: unknown
|
|
24
|
+
comments?: unknown
|
|
25
|
+
requiredEdits?: unknown
|
|
26
|
+
}
|
|
27
|
+
if (typeof record.id !== 'string' || typeof record.approved !== 'boolean') continue
|
|
28
|
+
|
|
29
|
+
const requiredEdits = Array.isArray(record.requiredEdits)
|
|
30
|
+
? record.requiredEdits.filter((item): item is string => typeof item === 'string' && item.trim().length > 0)
|
|
31
|
+
: []
|
|
32
|
+
const comments =
|
|
33
|
+
typeof record.comments === 'string' && record.comments.trim().length > 0
|
|
34
|
+
? record.comments.trim()
|
|
35
|
+
: typeof record.reason === 'string' && record.reason.trim().length > 0
|
|
36
|
+
? record.reason.trim()
|
|
37
|
+
: undefined
|
|
38
|
+
|
|
39
|
+
return { approvalId: record.id, approved: record.approved, comments, requiredEdits }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
|
|
7
45
|
export function isApprovalContinuationRequest(messages: ChatMessage[]): boolean {
|
|
8
46
|
const lastAssistant = [...messages].reverse().find((message) => message.role === 'assistant')
|
|
9
47
|
if (!lastAssistant) return false
|
|
@@ -14,3 +52,24 @@ export function isApprovalContinuationRequest(messages: ChatMessage[]): boolean
|
|
|
14
52
|
|
|
15
53
|
return hasApprovalRespondedParts(lastAssistant)
|
|
16
54
|
}
|
|
55
|
+
|
|
56
|
+
const PLAN_TOOL_NAMES = new Set([
|
|
57
|
+
'createExecutionPlan',
|
|
58
|
+
'replaceExecutionPlan',
|
|
59
|
+
'submitExecutionNodeResult',
|
|
60
|
+
'getActiveExecutionPlan',
|
|
61
|
+
'resumeExecutionPlanRun',
|
|
62
|
+
])
|
|
63
|
+
|
|
64
|
+
export function isNativeToolApprovalRequest(messages: ChatMessage[]): boolean {
|
|
65
|
+
const lastAssistant = [...messages].reverse().find((m) => m.role === 'assistant')
|
|
66
|
+
if (!lastAssistant) return false
|
|
67
|
+
|
|
68
|
+
return lastAssistant.parts.some((part) => {
|
|
69
|
+
if (!('state' in part) || (part as { state: string }).state !== 'approval-responded') return false
|
|
70
|
+
const type = (part as { type?: string }).type
|
|
71
|
+
if (typeof type !== 'string' || !type.startsWith('tool-')) return false
|
|
72
|
+
const toolName = type.slice(5)
|
|
73
|
+
return !PLAN_TOOL_NAMES.has(toolName)
|
|
74
|
+
})
|
|
75
|
+
}
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import type { ChatMessage } from '@lota-sdk/shared/schemas/chat-message'
|
|
2
2
|
|
|
3
|
-
import { isApprovalContinuationRequest } from './approval-continuation'
|
|
3
|
+
import { isApprovalContinuationRequest, isNativeToolApprovalRequest } from './approval-continuation'
|
|
4
4
|
|
|
5
5
|
export type RoutedChatRequest =
|
|
6
6
|
| { kind: 'approval-continuation'; approvalMessages: ChatMessage[] }
|
|
7
|
+
| { kind: 'native-tool-approval'; messages: ChatMessage[] }
|
|
7
8
|
| { kind: 'turn'; inputMessage: ChatMessage }
|
|
8
9
|
| { kind: 'invalid'; message: string }
|
|
9
10
|
|
|
10
11
|
export function routeWorkstreamChatMessages(messages: ChatMessage[]): RoutedChatRequest {
|
|
11
12
|
if (isApprovalContinuationRequest(messages)) {
|
|
13
|
+
if (isNativeToolApprovalRequest(messages)) {
|
|
14
|
+
return { kind: 'native-tool-approval', messages }
|
|
15
|
+
}
|
|
12
16
|
return { kind: 'approval-continuation', approvalMessages: messages }
|
|
13
17
|
}
|
|
14
18
|
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import type { SerializableExecutionPlan } from '@lota-sdk/shared/schemas/execution-plan'
|
|
2
2
|
|
|
3
3
|
export const EXECUTION_PLAN_AGENT_PROTOCOL_PROMPT = `<execution-plan-protocol>
|
|
4
|
-
- Before doing multi-step work, create
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
- If the
|
|
11
|
-
- If the active plan is blocked, do not continue blindly. Replan, restart a task, ask for user input, or abort.
|
|
4
|
+
- Before doing multi-step work, create a contract-driven execution plan instead of tracking steps only in prose.
|
|
5
|
+
- Plans are graph-capable workflow contracts. Every execution node must define objective, instructions, deliverables, success criteria, completion checks, retry policy, failure policy, and tool/context policy.
|
|
6
|
+
- The runtime executor owns lifecycle truth. Do not claim that a node is complete until submitExecutionNodeResult succeeds.
|
|
7
|
+
- Use execution-plan tools to create, replace, inspect, submit node results, and resume runs.
|
|
8
|
+
- Treat the active execution run in <execution-plan-state> as authoritative. Do not mutate run or node status in prose.
|
|
9
|
+
- Work only on nodes that are active or explicitly ready for your executor. If a node is awaiting human input or approval, stop and let the runtime resume it.
|
|
10
|
+
- If the graph, contracts, or success criteria materially change, replace the plan instead of silently drifting.
|
|
12
11
|
</execution-plan-protocol>`
|
|
13
12
|
|
|
14
13
|
export function formatExecutionPlanForPrompt(plan: SerializableExecutionPlan | null | undefined): string | undefined {
|
|
@@ -16,11 +15,11 @@ export function formatExecutionPlanForPrompt(plan: SerializableExecutionPlan | n
|
|
|
16
15
|
|
|
17
16
|
const payload = {
|
|
18
17
|
policy: {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
executorOwnsLifecycleTruth: true,
|
|
19
|
+
contractDrivenExecution: true,
|
|
20
|
+
humanGatesAreDurable: true,
|
|
21
|
+
artifactsAreFirstClassOutputs: true,
|
|
22
|
+
checkpointRecoveryEnabled: true,
|
|
24
23
|
},
|
|
25
24
|
plan,
|
|
26
25
|
}
|
|
@@ -43,16 +42,24 @@ export function createExecutionPlanInstructionSectionCache(params: {
|
|
|
43
42
|
disabled?: boolean
|
|
44
43
|
loadPlan: () => Promise<SerializableExecutionPlan | null | undefined>
|
|
45
44
|
}) {
|
|
45
|
+
let planPromise: Promise<SerializableExecutionPlan | null | undefined> | null = null
|
|
46
46
|
let sectionsPromise: Promise<string[] | undefined> | null = null
|
|
47
47
|
|
|
48
48
|
return {
|
|
49
49
|
invalidate() {
|
|
50
|
+
planPromise = null
|
|
50
51
|
sectionsPromise = null
|
|
51
52
|
},
|
|
53
|
+
async getPlan(): Promise<SerializableExecutionPlan | null | undefined> {
|
|
54
|
+
if (params.disabled) return undefined
|
|
55
|
+
|
|
56
|
+
planPromise ??= params.loadPlan()
|
|
57
|
+
return await planPromise
|
|
58
|
+
},
|
|
52
59
|
async getSections(): Promise<string[] | undefined> {
|
|
53
60
|
if (params.disabled) return undefined
|
|
54
61
|
|
|
55
|
-
sectionsPromise ??=
|
|
62
|
+
sectionsPromise ??= this.getPlan().then((plan) => buildExecutionPlanInstructionSections(plan))
|
|
56
63
|
return await sectionsPromise
|
|
57
64
|
},
|
|
58
65
|
}
|
|
@@ -2,27 +2,35 @@ import type { ChatMessage } from '@lota-sdk/shared/schemas/chat-message'
|
|
|
2
2
|
|
|
3
3
|
export async function finalizeTurnRun(params: {
|
|
4
4
|
serverRunId: string
|
|
5
|
-
getEntity: () => Promise<{ lastCompactedMessageId?: string | null;
|
|
5
|
+
getEntity: () => Promise<{ lastCompactedMessageId?: string | null; compactionSummary?: string | null }>
|
|
6
6
|
getUncompactedMessages: (cursor?: string) => Promise<ChatMessage[]>
|
|
7
7
|
assessCompaction: (summaryText: string, messages: ChatMessage[]) => { shouldCompact: boolean }
|
|
8
8
|
enqueueCompaction: () => Promise<void>
|
|
9
9
|
unregisterRun: (runId: string) => void
|
|
10
10
|
clearActiveRunId: (runId: string) => Promise<void>
|
|
11
11
|
disposeAbort: () => void
|
|
12
|
+
activeStreamId?: string
|
|
13
|
+
clearActiveStreamId?: (streamId: string) => Promise<void>
|
|
12
14
|
}): Promise<void> {
|
|
15
|
+
// Clear the active run immediately so new messages are not blocked
|
|
16
|
+
params.unregisterRun(params.serverRunId)
|
|
17
|
+
await params.clearActiveRunId(params.serverRunId)
|
|
18
|
+
if (params.activeStreamId && params.clearActiveStreamId) {
|
|
19
|
+
await params.clearActiveStreamId(params.activeStreamId)
|
|
20
|
+
}
|
|
21
|
+
params.disposeAbort()
|
|
22
|
+
|
|
13
23
|
try {
|
|
14
24
|
const entity = await params.getEntity()
|
|
15
25
|
const cursor = typeof entity.lastCompactedMessageId === 'string' ? entity.lastCompactedMessageId : undefined
|
|
16
26
|
const uncompactedMessages = await params.getUncompactedMessages(cursor)
|
|
17
|
-
const summaryText = typeof entity.
|
|
27
|
+
const summaryText = typeof entity.compactionSummary === 'string' ? entity.compactionSummary : ''
|
|
18
28
|
const { shouldCompact } = params.assessCompaction(summaryText, uncompactedMessages)
|
|
19
29
|
|
|
20
30
|
if (shouldCompact) {
|
|
21
31
|
await params.enqueueCompaction()
|
|
22
32
|
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
await params.clearActiveRunId(params.serverRunId)
|
|
26
|
-
params.disposeAbort()
|
|
33
|
+
} catch {
|
|
34
|
+
// compaction assessment errors should not surface to callers
|
|
27
35
|
}
|
|
28
36
|
}
|
|
@@ -121,14 +121,14 @@ export function buildAgentHistoryMessages(messages: ChatMessageLike[]): Array<{
|
|
|
121
121
|
.map((message) => ({ content: message.content, ...(message.agentName ? { agentName: message.agentName } : {}) }))
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
export function
|
|
124
|
+
export function appendPersistedWorkstreamContextToHistoryMessages(
|
|
125
125
|
historyMessages: WorkstreamHistoryMessage[],
|
|
126
|
-
params: {
|
|
126
|
+
params: { compactionSummary?: string | null; persistedState?: unknown },
|
|
127
127
|
): WorkstreamHistoryMessage[] {
|
|
128
128
|
const nextHistoryMessages = [...historyMessages]
|
|
129
|
-
const
|
|
130
|
-
if (
|
|
131
|
-
nextHistoryMessages.push({ role: 'agent', content: `Compacted chat summary:\n${
|
|
129
|
+
const compactionSummary = typeof params.compactionSummary === 'string' ? params.compactionSummary.trim() : ''
|
|
130
|
+
if (compactionSummary) {
|
|
131
|
+
nextHistoryMessages.push({ role: 'agent', content: `Compacted chat summary:\n${compactionSummary}` })
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
if (params.persistedState !== undefined && params.persistedState !== null) {
|
|
@@ -61,7 +61,7 @@ class ContextCompactionService {
|
|
|
61
61
|
)
|
|
62
62
|
|
|
63
63
|
const result = await contextCompactionRuntime.compactHistory({
|
|
64
|
-
summaryText: typeof workstream.
|
|
64
|
+
summaryText: typeof workstream.compactionSummary === 'string' ? workstream.compactionSummary : '',
|
|
65
65
|
liveMessages,
|
|
66
66
|
tailMessageCount: WORKSTREAM_RAW_TAIL_MESSAGES,
|
|
67
67
|
contextSize: params.contextSize,
|
|
@@ -82,7 +82,11 @@ class ContextCompactionService {
|
|
|
82
82
|
await databaseService.update(
|
|
83
83
|
TABLES.WORKSTREAM,
|
|
84
84
|
params.workstreamId,
|
|
85
|
-
{
|
|
85
|
+
{
|
|
86
|
+
compactionSummary: result.summaryText,
|
|
87
|
+
lastCompactedMessageId: result.lastCompactedMessageId,
|
|
88
|
+
state: result.state,
|
|
89
|
+
},
|
|
86
90
|
WorkstreamSchema,
|
|
87
91
|
)
|
|
88
92
|
|
|
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto'
|
|
|
2
2
|
|
|
3
3
|
import { chunkMarkdownDocument, chunkPagedDocument, chunkPlainTextDocument } from '../document/org-document-chunking'
|
|
4
4
|
import type { ParsedDocumentChunk } from '../document/org-document-chunking'
|
|
5
|
-
import {
|
|
5
|
+
import { getDefaultEmbeddings } from '../embeddings/provider'
|
|
6
6
|
|
|
7
7
|
type DocumentChunkEmbeddings = {
|
|
8
8
|
embedDocuments(documents: string[]): Promise<number[][]>
|
|
@@ -10,7 +10,7 @@ type DocumentChunkEmbeddings = {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
function createDocumentChunkEmbeddings(): DocumentChunkEmbeddings {
|
|
13
|
-
const embeddings =
|
|
13
|
+
const embeddings = getDefaultEmbeddings()
|
|
14
14
|
|
|
15
15
|
return {
|
|
16
16
|
embedDocuments: async (documents) => await embeddings.embedDocuments(documents),
|