@lensagent/core 0.1.0 → 0.1.2
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/README.md +63 -4
- package/config/lens.example.jsonc +36 -13
- package/dist/adapters/executor-router.d.ts +20 -0
- package/dist/adapters/executor-router.js +86 -0
- package/dist/adapters/executor.d.ts +9 -3
- package/dist/adapters/executor.js +196 -61
- package/dist/adapters/lsp.d.ts +2 -1
- package/dist/adapters/lsp.js +98 -41
- package/dist/adapters/mcp.js +52 -17
- package/dist/adapters/revert.d.ts +5 -4
- package/dist/adapters/revert.js +6 -64
- package/dist/adapters/web.d.ts +9 -0
- package/dist/adapters/web.js +244 -0
- package/dist/config/files.d.ts +14 -0
- package/dist/config/files.js +57 -0
- package/dist/config/runtime.d.ts +3 -4
- package/dist/config/runtime.js +116 -97
- package/dist/config/schema.d.ts +56 -10
- package/dist/config/schema.js +48 -8
- package/dist/contracts/capability.d.ts +27 -1
- package/dist/contracts/domain.d.ts +1 -0
- package/dist/contracts/model.d.ts +33 -6
- package/dist/contracts/review.d.ts +17 -0
- package/dist/contracts/review.js +1 -0
- package/dist/core/access-policy.d.ts +40 -0
- package/dist/core/access-policy.js +68 -0
- package/dist/core/access.d.ts +30 -2
- package/dist/core/access.js +159 -26
- package/dist/core/context-boundaries.d.ts +25 -0
- package/dist/core/context-boundaries.js +106 -0
- package/dist/core/context.d.ts +3 -0
- package/dist/core/context.js +11 -0
- package/dist/core/host.js +261 -173
- package/dist/entry/pair.d.ts +1 -0
- package/dist/entry/pair.js +14 -0
- package/dist/entry/run.js +1 -1
- package/dist/execution/lsp-process.d.ts +3 -2
- package/dist/execution/lsp-process.js +46 -28
- package/dist/http/server.d.ts +5 -0
- package/dist/http/server.js +262 -80
- package/dist/runtime/agent.d.ts +9 -4
- package/dist/runtime/agent.js +361 -286
- package/dist/runtime/artifacts.d.ts +13 -1
- package/dist/runtime/artifacts.js +137 -10
- package/dist/runtime/cache.d.ts +59 -16
- package/dist/runtime/cache.js +124 -43
- package/dist/runtime/calls.d.ts +1 -1
- package/dist/runtime/calls.js +164 -48
- package/dist/runtime/capabilities.d.ts +2 -2
- package/dist/runtime/capabilities.js +4 -5
- package/dist/runtime/cleanup.d.ts +5 -2
- package/dist/runtime/cleanup.js +48 -17
- package/dist/runtime/compaction.d.ts +9 -0
- package/dist/runtime/compaction.js +52 -0
- package/dist/runtime/context.d.ts +8 -4
- package/dist/runtime/context.js +115 -128
- package/dist/runtime/events.d.ts +7 -0
- package/dist/runtime/events.js +83 -0
- package/dist/runtime/images.d.ts +14 -0
- package/dist/runtime/images.js +82 -0
- package/dist/runtime/kernel-capabilities.d.ts +7 -1
- package/dist/runtime/kernel-capabilities.js +146 -68
- package/dist/runtime/model-audit.d.ts +9 -0
- package/dist/runtime/model-audit.js +39 -0
- package/dist/runtime/model.d.ts +109 -4
- package/dist/runtime/model.js +368 -143
- package/dist/runtime/observability.d.ts +1 -0
- package/dist/runtime/observability.js +60 -15
- package/dist/runtime/policy.d.ts +1 -1
- package/dist/runtime/policy.js +2 -2
- package/dist/runtime/profile.d.ts +2 -1
- package/dist/runtime/profile.js +12 -15
- package/dist/runtime/projects.d.ts +3 -1
- package/dist/runtime/projects.js +38 -17
- package/dist/runtime/ptc-worker.d.ts +1 -0
- package/dist/runtime/ptc-worker.js +97 -0
- package/dist/runtime/ptc.d.ts +16 -0
- package/dist/runtime/ptc.js +128 -0
- package/dist/runtime/results.d.ts +5 -4
- package/dist/runtime/results.js +29 -51
- package/dist/runtime/review.d.ts +4 -1
- package/dist/runtime/review.js +101 -22
- package/dist/runtime/scheduler.js +1 -0
- package/dist/runtime/skills.js +34 -5
- package/dist/runtime/task-audit.d.ts +21 -0
- package/dist/runtime/task-audit.js +122 -0
- package/dist/runtime/time.d.ts +9 -0
- package/dist/runtime/time.js +18 -0
- package/dist/runtime/tokenizer.d.ts +2 -0
- package/dist/runtime/tokenizer.js +26 -1
- package/dist/store/database.js +26 -1
- package/dist/store/event-log.d.ts +1 -0
- package/dist/store/event-log.js +22 -5
- package/dist/store/schema.d.ts +1 -1
- package/dist/store/schema.js +50 -1
- package/dist/store/store.d.ts +65 -19
- package/dist/store/store.js +986 -188
- package/package.json +18 -8
- package/proto/executor.proto +69 -55
package/dist/store/store.js
CHANGED
|
@@ -1,18 +1,57 @@
|
|
|
1
|
+
import { assertContextBoundaries, assertContextTransition, boundaryMessages, contextToolCalls } from '../core/context-boundaries.js';
|
|
1
2
|
import { digest, id, json, now, parseJson } from '../core/codec.js';
|
|
2
|
-
import { fail } from '../core/domain.js';
|
|
3
|
+
import { fail, LensError } from '../core/domain.js';
|
|
4
|
+
import { estimateTokens } from '../runtime/tokenizer.js';
|
|
3
5
|
import { policyPrompt } from '../runtime/policy.js';
|
|
6
|
+
import { activeContextItems } from '../core/context.js';
|
|
4
7
|
export class Store {
|
|
8
|
+
observability;
|
|
5
9
|
#database;
|
|
6
10
|
#events;
|
|
7
11
|
#eventLog;
|
|
8
|
-
|
|
12
|
+
#batchChanges;
|
|
13
|
+
/** Persist a model tool batch before reviews or execution can yield control. */
|
|
14
|
+
commitToolBatch(projectId, command) {
|
|
15
|
+
const changes = [];
|
|
16
|
+
return this.#write(projectId, changes, () => {
|
|
17
|
+
this.#batchChanges = changes;
|
|
18
|
+
try {
|
|
19
|
+
return command();
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
this.#batchChanges = undefined;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
constructor(database, events, eventLog, observability) {
|
|
27
|
+
this.observability = observability;
|
|
9
28
|
this.#database = database;
|
|
10
29
|
this.#events = events;
|
|
11
30
|
this.#eventLog = eventLog;
|
|
31
|
+
this.#database.transaction(() => {
|
|
32
|
+
const migrated = new Set(this.#all("SELECT DISTINCT agent_id FROM context_items WHERE kind = 'compaction' AND role != 'assistant'").map((row) => String(row.agent_id)));
|
|
33
|
+
if (migrated.size > 0) {
|
|
34
|
+
const timestamp = now();
|
|
35
|
+
this.#database.prepare(`UPDATE messages SET role = 'assistant' WHERE id IN (SELECT source_id FROM context_items WHERE kind = 'compaction')`).run();
|
|
36
|
+
this.#database.prepare(`UPDATE context_items SET role = 'assistant', revision = revision + 1, updated_at = ? WHERE kind = 'compaction' AND role != 'assistant'`).run(timestamp);
|
|
37
|
+
this.#database.prepare('UPDATE instance SET state_revision = state_revision + 1 WHERE id = 1').run();
|
|
38
|
+
}
|
|
39
|
+
for (const row of this.#all('SELECT id FROM agents')) {
|
|
40
|
+
const agentId = String(row.id);
|
|
41
|
+
if (migrated.has(agentId))
|
|
42
|
+
this.#touchContext(agentId);
|
|
43
|
+
else
|
|
44
|
+
this.#syncWorkingContext(agentId);
|
|
45
|
+
}
|
|
46
|
+
})();
|
|
12
47
|
}
|
|
13
48
|
async close() {
|
|
14
|
-
|
|
15
|
-
|
|
49
|
+
try {
|
|
50
|
+
await this.#eventLog?.close();
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
this.#database.close();
|
|
54
|
+
}
|
|
16
55
|
}
|
|
17
56
|
instance() {
|
|
18
57
|
return doc(this.#one('SELECT * FROM instance WHERE id = 1'));
|
|
@@ -33,23 +72,16 @@ export class Store {
|
|
|
33
72
|
JOIN runs r ON r.id = s.run_id
|
|
34
73
|
WHERE ma.kind = 'compaction' AND ma.state IN ('PREPARED', 'RUNNING')
|
|
35
74
|
`).map((row) => String(row.id)));
|
|
36
|
-
const interruptedAgentIds = new Set(this.#all(`SELECT DISTINCT agent_id FROM runs WHERE state
|
|
75
|
+
const interruptedAgentIds = new Set(this.#all(`SELECT DISTINCT agent_id FROM runs WHERE state != 'TERMINAL'`).map((row) => String(row.agent_id)));
|
|
37
76
|
const attempts = this.#all("SELECT id FROM model_attempts WHERE state IN ('PREPARED', 'RUNNING')");
|
|
38
77
|
for (const row of attempts)
|
|
39
78
|
interrupted.push({ type: 'model_attempt', id: String(row.id) });
|
|
40
79
|
this.#database.prepare(`UPDATE model_attempts SET state = 'TERMINAL', outcome = 'interrupted', error_code = 'PROCESS_INTERRUPTED', updated_at = ? WHERE state IN ('PREPARED', 'RUNNING')`).run(timestamp);
|
|
41
|
-
|
|
42
|
-
for (const row of calls) {
|
|
43
|
-
|
|
44
|
-
interrupted.push({ type: 'call', id: callId });
|
|
45
|
-
const running = row.state === 'RUNNING';
|
|
46
|
-
const outcome = running ? 'interrupted' : 'cancelled';
|
|
47
|
-
const errorCode = running ? 'PROCESS_INTERRUPTED' : 'RECOVERY_CANCELLED';
|
|
48
|
-
this.#database.prepare(`UPDATE calls SET state = 'TERMINAL', outcome = ?, outcome_unknown = ?, revision = revision + 1, updated_at = ? WHERE id = ?`)
|
|
49
|
-
.run(outcome, running ? 1 : 0, timestamp, callId);
|
|
50
|
-
this.#database.prepare(`INSERT OR IGNORE INTO results(id, call_id, outcome, content_json, error_code, error_message, created_at) VALUES (?, ?, ?, '{}', ?, 'The Agent workflow was interrupted by process shutdown.', ?)`)
|
|
51
|
-
.run(id('result'), callId, outcome, errorCode, timestamp);
|
|
80
|
+
interrupted.push(...this.#materializeInterruptedCalls(timestamp));
|
|
81
|
+
for (const row of this.#all("SELECT * FROM calls WHERE state != 'TERMINAL'")) {
|
|
82
|
+
interrupted.push(...this.#interruptCall(row, row.state === 'RUNNING' ? 'PROCESS_INTERRUPTED' : 'RECOVERY_CANCELLED', 'Execution stopped when the process exited; the Call will not be resumed.', timestamp));
|
|
52
83
|
}
|
|
84
|
+
interrupted.push(...this.#recoverResultContext(timestamp));
|
|
53
85
|
const ledgerSteps = this.#all(`SELECT DISTINCT s.id, r.agent_id FROM steps s JOIN runs r ON r.id = s.run_id JOIN calls c ON c.step_id = s.id WHERE s.state IN ('RUNNING', 'WAITING')`);
|
|
54
86
|
for (const row of ledgerSteps) {
|
|
55
87
|
const stepId = String(row.id);
|
|
@@ -81,7 +113,7 @@ export class Store {
|
|
|
81
113
|
.run(compactionFailed ? 'failed' : 'interrupted', compactionFailed ? 'COMPACTION_FAILED' : 'PROCESS_INTERRUPTED', timestamp, stepId);
|
|
82
114
|
}
|
|
83
115
|
this.#database.prepare(`UPDATE context_turn_groups SET state = 'ABORTED', revision = revision + 1, completed_at = ? WHERE state = 'OPEN'`).run(timestamp);
|
|
84
|
-
const runs = this.#all("SELECT id FROM runs WHERE state
|
|
116
|
+
const runs = this.#all("SELECT id FROM runs WHERE state != 'TERMINAL'");
|
|
85
117
|
for (const row of runs) {
|
|
86
118
|
const runId = String(row.id);
|
|
87
119
|
const compactionFailed = compactionRunIds.has(runId);
|
|
@@ -97,6 +129,7 @@ export class Store {
|
|
|
97
129
|
this.#database.prepare('UPDATE instance SET state_revision = state_revision + 1 WHERE id = 1').run();
|
|
98
130
|
});
|
|
99
131
|
transaction();
|
|
132
|
+
this.#observe(interrupted);
|
|
100
133
|
return interrupted;
|
|
101
134
|
}
|
|
102
135
|
/**
|
|
@@ -115,7 +148,7 @@ export class Store {
|
|
|
115
148
|
getProject(projectId) {
|
|
116
149
|
return doc(this.#require('projects', projectId));
|
|
117
150
|
}
|
|
118
|
-
createProject(name, paths) {
|
|
151
|
+
createProject(name, paths, executorId = 'remote') {
|
|
119
152
|
const projectId = id('project');
|
|
120
153
|
const workspaceId = id('workspace');
|
|
121
154
|
const timestamp = now();
|
|
@@ -125,6 +158,7 @@ export class Store {
|
|
|
125
158
|
this.#database.prepare(`INSERT INTO workspaces(id, project_id, name, is_default, revision, created_at, updated_at) VALUES (?, ?, 'Default', 1, 0, ?, ?)`)
|
|
126
159
|
.run(workspaceId, projectId, timestamp, timestamp);
|
|
127
160
|
this.#replaceWorkspacePaths(workspaceId, paths);
|
|
161
|
+
this.#database.prepare('UPDATE workspaces SET executor_ref = ? WHERE id = ?').run(executorId, workspaceId);
|
|
128
162
|
return this.getProject(projectId);
|
|
129
163
|
});
|
|
130
164
|
}
|
|
@@ -183,7 +217,7 @@ export class Store {
|
|
|
183
217
|
owned(row, 'project_id', projectId);
|
|
184
218
|
return this.#workspace(row);
|
|
185
219
|
}
|
|
186
|
-
createWorkspace(projectId, name, paths, makeDefault) {
|
|
220
|
+
createWorkspace(projectId, name, paths, makeDefault, executorId = 'remote') {
|
|
187
221
|
const workspaceId = id('workspace');
|
|
188
222
|
const timestamp = now();
|
|
189
223
|
return this.#write(projectId, refs(['workspace', workspaceId]), () => {
|
|
@@ -198,6 +232,7 @@ export class Store {
|
|
|
198
232
|
.run(workspaceId, projectId, name, makeDefault ? 1 : 0, timestamp, timestamp);
|
|
199
233
|
});
|
|
200
234
|
this.#replaceWorkspacePaths(workspaceId, paths);
|
|
235
|
+
this.#database.prepare('UPDATE workspaces SET executor_ref = ? WHERE id = ?').run(executorId, workspaceId);
|
|
201
236
|
return this.getWorkspace(projectId, workspaceId);
|
|
202
237
|
});
|
|
203
238
|
}
|
|
@@ -324,14 +359,14 @@ export class Store {
|
|
|
324
359
|
return this.getTask(projectId, taskId);
|
|
325
360
|
});
|
|
326
361
|
}
|
|
327
|
-
deleteTask(projectId, taskId, expectedRevision) {
|
|
362
|
+
deleteTask(projectId, taskId, expectedRevision, executorStateId) {
|
|
328
363
|
return this.#write(projectId, refs(['task', taskId]), () => {
|
|
329
364
|
const current = this.#require('tasks', taskId);
|
|
330
365
|
owned(current, 'project_id', projectId);
|
|
331
366
|
this.#expect(current, expectedRevision);
|
|
332
367
|
if (current.archived_at === null)
|
|
333
368
|
fail('TASK_NOT_ARCHIVED', 409, 'Archive the Task before deleting it.');
|
|
334
|
-
return this.#purgeTaskRows([taskId]);
|
|
369
|
+
return this.#purgeTaskRows(projectId, [taskId], executorStateId);
|
|
335
370
|
});
|
|
336
371
|
}
|
|
337
372
|
listSessions(projectId, taskId, options, archived = false) {
|
|
@@ -352,10 +387,10 @@ export class Store {
|
|
|
352
387
|
#sessionSummary(session, projectId) {
|
|
353
388
|
const sessionId = stringField(session.id);
|
|
354
389
|
const rootAgent = this.#all('SELECT id FROM agents WHERE session_id = ? AND parent_agent_id IS NULL ORDER BY created_at, id LIMIT 1', sessionId)[0];
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
const waiting =
|
|
358
|
-
|
|
390
|
+
const phases = this.#all("SELECT r.* FROM runs r JOIN agents a ON a.id = r.agent_id WHERE a.session_id = ? AND r.state != 'TERMINAL'", sessionId)
|
|
391
|
+
.map(run => this.#runPhase(doc(run)));
|
|
392
|
+
const waiting = phases.filter(phase => phase === 'WAITING_USER').length;
|
|
393
|
+
const running = phases.filter(phase => phase !== 'WAITING_USER' && phase !== 'QUEUED').length;
|
|
359
394
|
const pending = this.#count(`SELECT COUNT(*) AS count FROM runs r JOIN agents a ON a.id = r.agent_id
|
|
360
395
|
WHERE a.session_id = ? AND r.state <> 'TERMINAL'`, sessionId);
|
|
361
396
|
// The outcome of the most recent Run, not a count of every Run that ever failed. Counting made the
|
|
@@ -414,11 +449,11 @@ export class Store {
|
|
|
414
449
|
};
|
|
415
450
|
}
|
|
416
451
|
/**
|
|
417
|
-
* 02A `AgentSessionRuntimeSummary`.
|
|
418
|
-
* token-estimate fields have no column yet, so they report `null` rather than a fabricated value.
|
|
452
|
+
* 02A `AgentSessionRuntimeSummary`. Source and Working Context revisions are independent.
|
|
419
453
|
*/
|
|
420
454
|
#agentRuntime(agent) {
|
|
421
455
|
const agentId = stringField(agent.id);
|
|
456
|
+
const working = this.#database.prepare('SELECT revision, digest FROM working_contexts WHERE agent_id = ?').get(agentId);
|
|
422
457
|
const pending = this.#all('SELECT COUNT(*) AS count, MAX(ordinal) AS high FROM pending_messages WHERE agent_id = ?', agentId)[0];
|
|
423
458
|
return {
|
|
424
459
|
session_id: agent.session_id ?? null,
|
|
@@ -428,8 +463,8 @@ export class Store {
|
|
|
428
463
|
context_high_water: this.#count('SELECT COUNT(*) AS count FROM context_items WHERE agent_id = ?', agentId),
|
|
429
464
|
pending_seq_high_water: Number(pending?.high ?? 0),
|
|
430
465
|
pending_revision: Number(pending?.count ?? 0),
|
|
431
|
-
work_context_revision: null,
|
|
432
|
-
work_context_digest: null,
|
|
466
|
+
work_context_revision: working === undefined ? null : Number(working.revision),
|
|
467
|
+
work_context_digest: working === undefined ? null : String(working.digest),
|
|
433
468
|
request_context_frame_digest: null,
|
|
434
469
|
token_estimate: null
|
|
435
470
|
};
|
|
@@ -455,9 +490,10 @@ export class Store {
|
|
|
455
490
|
session_id: run.agent_id ?? null,
|
|
456
491
|
agent_session_id: run.agent_id ?? null,
|
|
457
492
|
retry_of_run_id: run.retry_of_run_id ?? null,
|
|
458
|
-
initial_step_pending: run.initial_step_pending ===
|
|
493
|
+
initial_step_pending: run.initial_step_pending === true,
|
|
459
494
|
tool_continuation_pending: false,
|
|
460
|
-
|
|
495
|
+
phase: this.#runPhase(run),
|
|
496
|
+
status: state === 'TERMINAL' && outcome !== null ? terminal[outcome] ?? 'FAILED' : this.#runPhase(run) === 'WAITING_USER' ? 'WAITING' : state,
|
|
461
497
|
started_at: run.started_at ?? null,
|
|
462
498
|
ended_at: run.ended_at ?? null,
|
|
463
499
|
error_code: run.error_code ?? null
|
|
@@ -613,6 +649,30 @@ export class Store {
|
|
|
613
649
|
* `ended_at` have no storage at all, so they report `false`/`null` rather than a fabricated value,
|
|
614
650
|
* and `revision` is null because `steps` carries no revision column to report.
|
|
615
651
|
*/
|
|
652
|
+
#runPhase(run) {
|
|
653
|
+
if (run.state === 'TERMINAL')
|
|
654
|
+
return 'TERMINAL';
|
|
655
|
+
if (run.state === 'READY')
|
|
656
|
+
return 'QUEUED';
|
|
657
|
+
const step = this.#database.prepare("SELECT * FROM steps WHERE run_id = ? AND state != 'TERMINAL' ORDER BY ordinal DESC LIMIT 1").get(run.id);
|
|
658
|
+
return step === undefined ? 'PREPARING' : this.#stepPhase(doc(step));
|
|
659
|
+
}
|
|
660
|
+
#stepPhase(step) {
|
|
661
|
+
if (step.state === 'TERMINAL')
|
|
662
|
+
return 'TERMINAL';
|
|
663
|
+
if (this.#count("SELECT COUNT(*) AS count FROM model_attempts WHERE step_id = ? AND state IN ('PREPARED', 'RUNNING')", step.id) > 0)
|
|
664
|
+
return 'MODEL_STREAMING';
|
|
665
|
+
const calls = this.#all("SELECT state, wait_reason FROM calls WHERE step_id = ? AND state != 'TERMINAL'", step.id);
|
|
666
|
+
if (calls.some(call => call.state === 'RUNNING'))
|
|
667
|
+
return 'TOOLS_RUNNING';
|
|
668
|
+
if (calls.some(call => call.state === 'WAITING' && call.wait_reason === 'policy_review'))
|
|
669
|
+
return 'POLICY_REVIEW';
|
|
670
|
+
if (calls.some(call => call.state === 'WAITING'))
|
|
671
|
+
return 'WAITING_USER';
|
|
672
|
+
if (calls.some(call => call.state === 'QUEUED'))
|
|
673
|
+
return 'TOOLS_QUEUED';
|
|
674
|
+
return 'PREPARING';
|
|
675
|
+
}
|
|
616
676
|
#stepRuntime(step) {
|
|
617
677
|
const state = step.state ?? null;
|
|
618
678
|
const outcome = typeof step.outcome === 'string' ? step.outcome : null;
|
|
@@ -628,8 +688,9 @@ export class Store {
|
|
|
628
688
|
outcome,
|
|
629
689
|
status: state === 'TERMINAL'
|
|
630
690
|
? terminal[outcome ?? ''] ?? 'FAILED'
|
|
631
|
-
:
|
|
632
|
-
|
|
691
|
+
: this.#stepPhase(step),
|
|
692
|
+
phase: this.#stepPhase(step),
|
|
693
|
+
stream_closed: this.#stepPhase(step) !== 'MODEL_STREAMING',
|
|
633
694
|
reactive_compaction_attempted: false,
|
|
634
695
|
assistant_message_id: null,
|
|
635
696
|
ended_at: null,
|
|
@@ -702,6 +763,8 @@ export class Store {
|
|
|
702
763
|
...(queuedIds === null ? [] : messageRefs(queuedIds))
|
|
703
764
|
];
|
|
704
765
|
return this.#write(projectId, refs(...changes), () => {
|
|
766
|
+
if (input.expectedParentContextRevision !== undefined && this.getAgent(projectId, parentAgentId).context_revision !== input.expectedParentContextRevision)
|
|
767
|
+
fail('REVISION_CONFLICT', 409, 'Parent Context changed while preparing the Subagent.');
|
|
705
768
|
const timestamp = now();
|
|
706
769
|
this.#insertAgent(projectId, agentId, {
|
|
707
770
|
taskId: stringField(parent.task_id),
|
|
@@ -716,12 +779,20 @@ export class Store {
|
|
|
716
779
|
loadedCapabilities: input.loadedCapabilities
|
|
717
780
|
}, timestamp);
|
|
718
781
|
if (input.forkContext === 'full') {
|
|
719
|
-
const
|
|
720
|
-
if (
|
|
721
|
-
this.#copyContextRange(parentAgentId, agentId, Number(
|
|
782
|
+
const working = this.#workingItems(parentAgentId);
|
|
783
|
+
if (working.length > 0)
|
|
784
|
+
this.#copyContextRange(parentAgentId, agentId, Math.max(...working.map((item) => Number(item.ordinal))), timestamp, undefined, true);
|
|
722
785
|
}
|
|
723
786
|
else if (input.forkContext === 'compact' && input.compactSummary !== undefined) {
|
|
724
|
-
this.#insertStandaloneContextMessage(agentId, '
|
|
787
|
+
this.#insertStandaloneContextMessage(agentId, 'assistant', input.compactSummary, 'compaction', timestamp);
|
|
788
|
+
const checkpoint = this.#one("SELECT id FROM context_items WHERE agent_id = ? AND kind = 'compaction'", agentId);
|
|
789
|
+
this.#database.prepare('INSERT INTO context_compactions(context_item_id, source_revision, covered_item_ids_json, focus) VALUES (?, ?, ?, ?)')
|
|
790
|
+
.run(checkpoint.id, Number(parent.context_revision), json(input.compactCoveredItemIds ?? []), input.compactFocus ?? null);
|
|
791
|
+
const retained = input.compactRetainedItemIds ?? [];
|
|
792
|
+
if (retained.length > 0) {
|
|
793
|
+
const through = Math.max(...retained.map((itemId) => Number(this.#require('context_items', itemId).ordinal)));
|
|
794
|
+
this.#copyContextRange(parentAgentId, agentId, through, timestamp, retained);
|
|
795
|
+
}
|
|
725
796
|
}
|
|
726
797
|
if (input.initialPrompt !== undefined)
|
|
727
798
|
this.#insertStandaloneContextMessage(agentId, 'system', input.initialPrompt, 'message', timestamp);
|
|
@@ -807,10 +878,7 @@ export class Store {
|
|
|
807
878
|
return replay;
|
|
808
879
|
const ids = messageIdsForWrite(this.#activeRunId(targetAgentId));
|
|
809
880
|
return this.#write(projectId, refs(...messageRefs(ids)), () => {
|
|
810
|
-
const target = this.#require('agents', targetAgentId);
|
|
811
881
|
this.#ownedAgent(parentAgentId, projectId);
|
|
812
|
-
if (target.parent_agent_id !== parentAgentId)
|
|
813
|
-
fail('AGENT_NOT_FOUND', 404, 'The target is not a direct child Agent.');
|
|
814
882
|
const response = this.#insertQueuedMessage(projectId, targetAgentId, content, parentAgentId, ids, now());
|
|
815
883
|
this.#recordIngress(ingress, response);
|
|
816
884
|
return response;
|
|
@@ -835,32 +903,53 @@ export class Store {
|
|
|
835
903
|
return response;
|
|
836
904
|
});
|
|
837
905
|
}
|
|
838
|
-
async waitForSubagent(projectId, sourceAgentId, targetAgentId, timeoutMs, signal) {
|
|
906
|
+
async waitForSubagent(projectId, sourceAgentId, targetAgentId, timeoutMs, signal, afterMessageId) {
|
|
839
907
|
const source = this.getAgent(projectId, sourceAgentId);
|
|
840
908
|
const target = this.getAgent(projectId, targetAgentId);
|
|
841
|
-
if (source.
|
|
842
|
-
fail('SUBAGENT_TARGET_INVALID', 400, 'The target must be
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
909
|
+
if (source.session_id !== target.session_id || sourceAgentId === targetAgentId)
|
|
910
|
+
fail('SUBAGENT_TARGET_INVALID', 400, 'The target must be another Agent in the same Session.');
|
|
911
|
+
let after = 0;
|
|
912
|
+
if (afterMessageId !== undefined) {
|
|
913
|
+
const cursor = this.#database.prepare('SELECT rowid FROM messages WHERE id = ? AND agent_id = ? AND source_agent_id = ?').get(afterMessageId, sourceAgentId, targetAgentId);
|
|
914
|
+
if (cursor === undefined)
|
|
915
|
+
fail('INVALID_INPUT', 400, 'The message cursor must identify a message from the target to this Agent.');
|
|
916
|
+
after = Number(cursor.rowid);
|
|
917
|
+
}
|
|
918
|
+
return this.#events.waitFor(projectId, () => {
|
|
919
|
+
const message = this.#database.prepare(`SELECT * FROM messages WHERE agent_id = ? AND source_agent_id = ? AND rowid > ?
|
|
920
|
+
AND COALESCE(json_extract(metadata_json, '$.context_copy'), 0) = 0 ORDER BY rowid LIMIT 1`)
|
|
921
|
+
.get(sourceAgentId, targetAgentId, after);
|
|
922
|
+
if (message !== undefined) {
|
|
923
|
+
const pending = this.#database.prepare('SELECT * FROM pending_messages WHERE message_id = ?').get(message.id);
|
|
849
924
|
return {
|
|
850
925
|
status: 'message',
|
|
851
926
|
target_agent_id: targetAgentId,
|
|
852
|
-
pending_message: doc(pending),
|
|
853
|
-
message: this.getMessage(projectId, String(
|
|
927
|
+
pending_message: pending === undefined ? null : doc(pending),
|
|
928
|
+
message: this.getMessage(projectId, String(message.id)),
|
|
929
|
+
after_message_id: String(message.id)
|
|
854
930
|
};
|
|
855
931
|
}
|
|
856
932
|
const activeRunIds = this.#all(`SELECT id FROM runs WHERE agent_id = ? AND state IN ('READY', 'RUNNING', 'WAITING') ORDER BY created_at, id`, targetAgentId)
|
|
857
933
|
.map((row) => String(row.id));
|
|
858
|
-
if (activeRunIds.length === 0)
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
934
|
+
if (activeRunIds.length === 0) {
|
|
935
|
+
const run = this.#database.prepare('SELECT * FROM runs WHERE agent_id = ? ORDER BY rowid DESC LIMIT 1').get(targetAgentId);
|
|
936
|
+
const answer = run === undefined ? undefined : this.#database.prepare(`SELECT m.id FROM messages m JOIN context_items ci ON ci.source_id = m.id AND ci.kind = 'message'
|
|
937
|
+
WHERE ci.agent_id = ? AND m.role = 'assistant' AND m.source_agent_id IS NULL
|
|
938
|
+
AND ci.turn_group_id IN (SELECT turn_group_id FROM steps WHERE run_id = ?)
|
|
939
|
+
ORDER BY ci.ordinal DESC LIMIT 1`).get(targetAgentId, run.id);
|
|
940
|
+
return {
|
|
941
|
+
status: run === undefined ? 'idle' : run.outcome === 'success' ? 'completed' : String(run.outcome),
|
|
942
|
+
target_agent_id: targetAgentId,
|
|
943
|
+
active_run_ids: [],
|
|
944
|
+
run: run === undefined ? null : this.getRun(projectId, String(run.id)),
|
|
945
|
+
message: answer === undefined ? null : this.getMessage(projectId, String(answer.id))
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
return undefined;
|
|
949
|
+
}, signal, {
|
|
950
|
+
ms: Math.min(timeoutMs, 120000),
|
|
951
|
+
error: new LensError('CALL_TIMEOUT', 408, 'The Subagent wait timed out.', { target_agent_id: targetAgentId })
|
|
952
|
+
});
|
|
864
953
|
}
|
|
865
954
|
listPending(projectId, agentId, options) {
|
|
866
955
|
this.getAgent(projectId, agentId);
|
|
@@ -880,9 +969,10 @@ export class Store {
|
|
|
880
969
|
this.#database.prepare('UPDATE pending_messages SET content = ?, revision = revision + 1, updated_at = ? WHERE id = ?')
|
|
881
970
|
.run(content, now(), pendingId);
|
|
882
971
|
this.#database.prepare('UPDATE messages SET content = ? WHERE id = ?').run(content, current.message_id);
|
|
883
|
-
this.#database.prepare(`UPDATE context_items SET content = ?, revision = revision + 1, updated_at = ? WHERE agent_id = ? AND kind = 'message' AND source_id = ?`)
|
|
972
|
+
const updated = this.#database.prepare(`UPDATE context_items SET content = ?, revision = revision + 1, updated_at = ? WHERE agent_id = ? AND kind = 'message' AND source_id = ?`)
|
|
884
973
|
.run(content, now(), agentId, current.message_id);
|
|
885
|
-
|
|
974
|
+
if (updated.changes > 0)
|
|
975
|
+
this.#touchContext(agentId);
|
|
886
976
|
return this.getPending(projectId, pendingId);
|
|
887
977
|
});
|
|
888
978
|
}
|
|
@@ -896,6 +986,10 @@ export class Store {
|
|
|
896
986
|
this.#database.prepare(`UPDATE context_turn_groups SET state = 'ABORTED', revision = revision + 1, completed_at = ? WHERE id IN (SELECT turn_group_id FROM context_items WHERE agent_id = ? AND kind = 'message' AND source_id = ?) AND state = 'OPEN'`)
|
|
897
987
|
.run(timestamp, agentId, current.message_id);
|
|
898
988
|
this.#database.prepare('DELETE FROM pending_messages WHERE id = ?').run(pendingId);
|
|
989
|
+
const updated = this.#database.prepare(`UPDATE context_items SET visible = 0, revision = revision + 1, updated_at = ? WHERE agent_id = ? AND kind = 'message' AND source_id = ?`)
|
|
990
|
+
.run(timestamp, agentId, current.message_id);
|
|
991
|
+
if (updated.changes > 0)
|
|
992
|
+
this.#touchContext(agentId);
|
|
899
993
|
if (this.#count('SELECT COUNT(*) AS count FROM pending_messages WHERE agent_id = ?', agentId) === 0) {
|
|
900
994
|
this.#database.prepare(`UPDATE runs SET state = 'TERMINAL', outcome = 'cancelled', error_code = 'PENDING_DELETED', revision = revision + 1, updated_at = ? WHERE agent_id = ? AND state = 'READY' AND initial_step_pending = 0`)
|
|
901
995
|
.run(timestamp, agentId);
|
|
@@ -909,7 +1003,8 @@ export class Store {
|
|
|
909
1003
|
if (rows.length !== ids.length || rows.some((entry) => !ids.includes(entry)))
|
|
910
1004
|
fail('PENDING_SET_MISMATCH', 409, 'The pending Message set changed.');
|
|
911
1005
|
const temporary = this.#database.prepare('UPDATE pending_messages SET ordinal = ?, revision = revision + 1, updated_at = ? WHERE id = ?');
|
|
912
|
-
|
|
1006
|
+
const offset = this.#nextOrdinal('pending_messages', 'agent_id', agentId);
|
|
1007
|
+
ids.forEach((entry, ordinal) => temporary.run(ordinal + offset, now(), entry));
|
|
913
1008
|
ids.forEach((entry, ordinal) => temporary.run(ordinal, now(), entry));
|
|
914
1009
|
return this.#all('SELECT * FROM pending_messages WHERE agent_id = ? ORDER BY ordinal', agentId).map(doc);
|
|
915
1010
|
});
|
|
@@ -1057,23 +1152,12 @@ export class Store {
|
|
|
1057
1152
|
const runId = String(ready.id);
|
|
1058
1153
|
const stepId = id('step');
|
|
1059
1154
|
const pending = this.#all('SELECT * FROM pending_messages WHERE agent_id = ? ORDER BY ordinal, id', agentId);
|
|
1060
|
-
|
|
1155
|
+
const changes = [...refs(['run', runId], ['step', stepId], ['agent', agentId])];
|
|
1156
|
+
return this.#write(projectId, changes, () => {
|
|
1061
1157
|
const timestamp = now();
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
this.#insertTurnGroup(turnGroupId, agentId, 'turn', 'OPEN', timestamp);
|
|
1066
|
-
}
|
|
1067
|
-
else {
|
|
1068
|
-
const items = pending.map((entry) => this.#one(`SELECT * FROM context_items WHERE agent_id = ? AND kind = 'message' AND source_id = ? ORDER BY ordinal DESC LIMIT 1`, agentId, entry.message_id));
|
|
1069
|
-
turnGroupId = String(items[0]?.turn_group_id);
|
|
1070
|
-
items.forEach((item, ordinal) => {
|
|
1071
|
-
const sourceGroupId = String(item.turn_group_id);
|
|
1072
|
-
this.#database.prepare('UPDATE context_items SET turn_group_id = ?, turn_group_ordinal = ? WHERE id = ?').run(turnGroupId, ordinal, item.id);
|
|
1073
|
-
if (sourceGroupId !== turnGroupId)
|
|
1074
|
-
this.#database.prepare('DELETE FROM context_turn_groups WHERE id = ?').run(sourceGroupId);
|
|
1075
|
-
});
|
|
1076
|
-
}
|
|
1158
|
+
const turnGroupId = id('turn');
|
|
1159
|
+
this.#insertTurnGroup(turnGroupId, agentId, 'turn', 'OPEN', timestamp);
|
|
1160
|
+
changes.push({ type: 'turn_group', id: turnGroupId }, ...this.#appendPendingContext(agentId, turnGroupId, pending, timestamp));
|
|
1077
1161
|
const goals = strings(parseJson(String(ready.goal_message_ids_json)));
|
|
1078
1162
|
const goalMessageIds = [...new Set([...goals, ...pending.map((entry) => String(entry.message_id))])];
|
|
1079
1163
|
const update = this.#database.prepare(`UPDATE runs SET state = 'RUNNING', goal_message_ids_json = ?, initial_step_pending = 0, revision = revision + 1, updated_at = ? WHERE id = ? AND state = 'READY'`)
|
|
@@ -1134,6 +1218,7 @@ export class Store {
|
|
|
1134
1218
|
const sequence = this.#count('SELECT COUNT(*) AS count FROM model_attempts WHERE agent_id = ? AND kind = ?', input.agentId, input.kind);
|
|
1135
1219
|
this.#database.prepare(`INSERT INTO model_attempts(id, agent_id, step_id, kind, sequence, model, state, outcome, profile_id, profile_version, profile_digest, config_digest, context_revision, request_digest, preparation_json, usage_json, error_code, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, 'PREPARED', NULL, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?)`)
|
|
1136
1220
|
.run(attemptId, input.agentId, input.stepId, input.kind, sequence, input.model, input.profileId, input.profileVersion, input.profileDigest, input.configDigest, input.contextRevision, digest(input.request), json(input.preparation), timestamp, timestamp);
|
|
1221
|
+
this.#database.prepare('UPDATE model_attempts SET request_json = ? WHERE id = ?').run(json(input.request), attemptId);
|
|
1137
1222
|
const statement = this.#database.prepare('INSERT INTO attempt_capabilities(attempt_id, ordinal, capability_id, schema_json) VALUES (?, ?, ?, ?)');
|
|
1138
1223
|
input.capabilities.forEach((entry, ordinal) => statement.run(attemptId, ordinal, entry.id, json(entry.schema)));
|
|
1139
1224
|
return this.getAttempt(projectId, attemptId);
|
|
@@ -1149,9 +1234,28 @@ export class Store {
|
|
|
1149
1234
|
this.getAgent(projectId, ownerId);
|
|
1150
1235
|
else
|
|
1151
1236
|
this.getStep(projectId, ownerId);
|
|
1152
|
-
return this.#page(`SELECT * FROM model_attempts WHERE ${field} = ?`, [ownerId], options, '
|
|
1237
|
+
return this.#page(`SELECT * FROM model_attempts WHERE ${field} = ?`, [ownerId], options, 'rowid DESC');
|
|
1238
|
+
}
|
|
1239
|
+
latestModelAttempt(projectId, agentId, kind, model) {
|
|
1240
|
+
this.getAgent(projectId, agentId);
|
|
1241
|
+
const row = this.#database.prepare(`SELECT * FROM model_attempts WHERE agent_id = ? AND state != 'PREPARED' ${kind === undefined ? '' : 'AND kind = ?'} ${model === undefined ? '' : 'AND model = ?'} ORDER BY rowid DESC LIMIT 1`)
|
|
1242
|
+
.get(agentId, ...(kind === undefined ? [] : [kind]), ...(model === undefined ? [] : [model]));
|
|
1243
|
+
return row === undefined ? null : doc(row);
|
|
1153
1244
|
}
|
|
1154
|
-
|
|
1245
|
+
cacheUsage(projectId, agentId) {
|
|
1246
|
+
this.getAgent(projectId, agentId);
|
|
1247
|
+
const row = this.#one(`SELECT COUNT(*) AS attempts,
|
|
1248
|
+
SUM(CASE WHEN json_type(usage_json, '$.cache_read_tokens') = 'integer' THEN 1 ELSE 0 END) AS reported_attempts,
|
|
1249
|
+
SUM(CASE WHEN json_type(usage_json, '$.cache_read_tokens') = 'integer' THEN json_extract(usage_json, '$.input_tokens') ELSE 0 END) AS reported_input_tokens,
|
|
1250
|
+
SUM(json_extract(usage_json, '$.cache_read_tokens')) AS cache_read_tokens,
|
|
1251
|
+
SUM(json_extract(usage_json, '$.cache_write_tokens')) AS cache_write_tokens,
|
|
1252
|
+
SUM(json_extract(usage_json, '$.uncached_input_tokens')) AS uncached_input_tokens
|
|
1253
|
+
FROM model_attempts WHERE agent_id = ?`, agentId);
|
|
1254
|
+
const denominator = Number(row.reported_input_tokens ?? 0);
|
|
1255
|
+
return { ...doc(row), unknown_attempts: Number(row.attempts) - Number(row.reported_attempts ?? 0),
|
|
1256
|
+
reported_hit_ratio: denominator > 0 ? Number(row.cache_read_tokens) / denominator : null };
|
|
1257
|
+
}
|
|
1258
|
+
transitionAttempt(projectId, attemptId, from, to, outcome = null, usage = null, errorCode = null, observation = null) {
|
|
1155
1259
|
return this.#write(projectId, refs(['model_attempt', attemptId]), () => {
|
|
1156
1260
|
const current = this.#require('model_attempts', attemptId);
|
|
1157
1261
|
this.#ownedAgent(String(current.agent_id), projectId);
|
|
@@ -1159,6 +1263,8 @@ export class Store {
|
|
|
1159
1263
|
.run(to, outcome, usage === null ? null : json(usage), errorCode, now(), attemptId, from);
|
|
1160
1264
|
if (result.changes !== 1)
|
|
1161
1265
|
fail('ATTEMPT_STATE_CONFLICT', 409, 'The ModelAttempt state changed.');
|
|
1266
|
+
if (observation !== null)
|
|
1267
|
+
this.#database.prepare('UPDATE model_attempts SET observation_json = ? WHERE id = ?').run(json(observation), attemptId);
|
|
1162
1268
|
return this.getAttempt(projectId, attemptId);
|
|
1163
1269
|
});
|
|
1164
1270
|
}
|
|
@@ -1197,8 +1303,8 @@ export class Store {
|
|
|
1197
1303
|
this.getStep(projectId, input.stepId);
|
|
1198
1304
|
if (input.attemptId !== undefined)
|
|
1199
1305
|
this.getAttempt(projectId, input.attemptId);
|
|
1200
|
-
this.#database.prepare(`INSERT INTO calls(id, project_id, workspace_id, agent_id, step_id, attempt_id, partition_id, parent_call_id, source, capability_id, provider_call_id, call_ordinal, input_json, effect, policy, state, wait_reason, outcome, outcome_unknown, auto_review_passed, revision, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, 0, ?, 0, ?, ?)`)
|
|
1201
|
-
.run(callId, projectId, input.workspaceId, input.agentId ?? null, input.stepId ?? null, input.attemptId ?? null, input.partitionId ?? null, input.parentCallId ?? null, input.source, input.capabilityId, input.providerCallId ?? null, input.callOrdinal ?? null, json(input.arguments), input.effect, input.policy ?? null, input.state, input.waitReason ?? null, input.autoReviewPassed === undefined ? null : input.autoReviewPassed ? 1 : 0, timestamp, timestamp);
|
|
1306
|
+
this.#database.prepare(`INSERT INTO calls(id, project_id, workspace_id, agent_id, step_id, attempt_id, partition_id, parent_call_id, source, capability_id, provider_call_id, call_ordinal, input_json, continuation_json, effect, policy, state, wait_reason, outcome, outcome_unknown, auto_review_passed, revision, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, 0, ?, 0, ?, ?)`)
|
|
1307
|
+
.run(callId, projectId, input.workspaceId, input.agentId ?? null, input.stepId ?? null, input.attemptId ?? null, input.partitionId ?? null, input.parentCallId ?? null, input.source, input.capabilityId, input.providerCallId ?? null, input.callOrdinal ?? null, json(input.arguments), json(input.prompt === undefined ? {} : { prompt: input.prompt }), input.effect, input.policy ?? null, input.state, input.waitReason ?? null, input.autoReviewPassed === undefined ? null : input.autoReviewPassed ? 1 : 0, timestamp, timestamp);
|
|
1202
1308
|
const call = this.getCall(projectId, callId);
|
|
1203
1309
|
if (input.ingress !== undefined)
|
|
1204
1310
|
this.#recordIngress(input.ingress, call);
|
|
@@ -1236,15 +1342,12 @@ export class Store {
|
|
|
1236
1342
|
const call = this.#callRow(projectId, callId);
|
|
1237
1343
|
const result = this.#database.prepare('SELECT id FROM results WHERE call_id = ?').get(callId);
|
|
1238
1344
|
const summary = this.#callSummaries([call])[0] ?? {};
|
|
1239
|
-
const input = jsonRecord(call.input);
|
|
1240
1345
|
return {
|
|
1241
1346
|
id: call.id ?? null,
|
|
1242
1347
|
call_id: call.id ?? null,
|
|
1243
1348
|
summary,
|
|
1244
1349
|
provider_call_id: call.provider_call_id ?? null,
|
|
1245
|
-
|
|
1246
|
-
// it becomes `{ arguments, answer }` -- the same test `answerCall` makes before rewriting it.
|
|
1247
|
-
arguments: input !== null && 'arguments' in input ? input.arguments ?? null : call.input ?? null,
|
|
1350
|
+
arguments: call.input ?? null,
|
|
1248
1351
|
user_prompt: summary.user_prompt ?? null,
|
|
1249
1352
|
result_id: result === undefined ? null : String(result.id),
|
|
1250
1353
|
agent_id: call.agent_id ?? null
|
|
@@ -1286,7 +1389,8 @@ export class Store {
|
|
|
1286
1389
|
wait_reason: call.wait_reason ?? null,
|
|
1287
1390
|
legacy_state: legacyCallState(call),
|
|
1288
1391
|
auto_review_passed: call.auto_review_passed ?? null,
|
|
1289
|
-
|
|
1392
|
+
auto_review: call.auto_review ?? null,
|
|
1393
|
+
user_prompt: jsonRecord(jsonRecord(call.continuation)?.prompt),
|
|
1290
1394
|
outcome: call.outcome ?? null,
|
|
1291
1395
|
outcome_unknown: call.outcome_unknown === true,
|
|
1292
1396
|
yielded_at: null,
|
|
@@ -1349,13 +1453,13 @@ export class Store {
|
|
|
1349
1453
|
this.#expect(current, expectedRevision);
|
|
1350
1454
|
const state = String(current.state);
|
|
1351
1455
|
if (decision !== 'cancel') {
|
|
1352
|
-
const prompt = jsonRecord(jsonRecord(
|
|
1456
|
+
const prompt = jsonRecord(jsonRecord(doc(current).continuation)?.prompt);
|
|
1353
1457
|
if (state !== 'WAITING' || current.wait_reason !== 'user_input' || prompt?.kind !== 'policy_approval') {
|
|
1354
1458
|
fail('CALL_STATE_CONFLICT', 409, 'The Call is not waiting for a policy decision.');
|
|
1355
1459
|
}
|
|
1356
1460
|
}
|
|
1357
1461
|
if (decision === 'approve') {
|
|
1358
|
-
this.#database.prepare(`UPDATE calls SET state = 'QUEUED', wait_reason = NULL, revision = revision + 1, updated_at = ? WHERE id = ?`).run(now(), callId);
|
|
1462
|
+
this.#database.prepare(`UPDATE calls SET state = 'QUEUED', wait_reason = NULL, continuation_json = '{}', revision = revision + 1, updated_at = ? WHERE id = ?`).run(now(), callId);
|
|
1359
1463
|
return { call: this.getCall(projectId, callId), result: null };
|
|
1360
1464
|
}
|
|
1361
1465
|
if (state === 'TERMINAL')
|
|
@@ -1376,8 +1480,8 @@ export class Store {
|
|
|
1376
1480
|
this.#expect(current, expectedRevision);
|
|
1377
1481
|
if (current.state !== 'WAITING' || current.wait_reason !== 'user_input')
|
|
1378
1482
|
fail('CALL_STATE_CONFLICT', 409, 'The Call is not waiting for user input.');
|
|
1379
|
-
const
|
|
1380
|
-
const prompt =
|
|
1483
|
+
const continuation = jsonRecord(doc(current).continuation);
|
|
1484
|
+
const prompt = continuation?.prompt;
|
|
1381
1485
|
validateAnswer(jsonRecord(prompt), answer);
|
|
1382
1486
|
if (jsonRecord(prompt)?.kind === 'policy_approval' && policyAnswer(answer) === 'reject') {
|
|
1383
1487
|
const resultId = id('result');
|
|
@@ -1387,46 +1491,209 @@ export class Store {
|
|
|
1387
1491
|
.run(resultId, callId, timestamp);
|
|
1388
1492
|
return { call: this.getCall(projectId, callId), result: doc(this.#require('results', resultId)) };
|
|
1389
1493
|
}
|
|
1390
|
-
const
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
this.#database.prepare(`UPDATE calls SET input_json = ?, state = 'QUEUED', wait_reason = NULL, revision = revision + 1, updated_at = ? WHERE id = ?`)
|
|
1394
|
-
.run(json(nextInput), now(), callId);
|
|
1494
|
+
const nextContinuation = jsonRecord(prompt)?.kind === 'policy_approval' ? {} : { ...continuation, answer };
|
|
1495
|
+
this.#database.prepare(`UPDATE calls SET continuation_json = ?, state = 'QUEUED', wait_reason = NULL, revision = revision + 1, updated_at = ? WHERE id = ?`)
|
|
1496
|
+
.run(json(nextContinuation), now(), callId);
|
|
1395
1497
|
return { call: this.getCall(projectId, callId), result: null };
|
|
1396
1498
|
});
|
|
1397
1499
|
}
|
|
1398
|
-
reviewCall(projectId, callId,
|
|
1500
|
+
reviewCall(projectId, callId, review) {
|
|
1399
1501
|
return this.#write(projectId, refs(['call', callId]), () => {
|
|
1400
1502
|
const current = this.#require('calls', callId);
|
|
1401
1503
|
owned(current, 'project_id', projectId);
|
|
1402
1504
|
if (current.state !== 'WAITING' || current.wait_reason !== 'policy_review')
|
|
1403
1505
|
fail('CALL_STATE_CONFLICT', 409, 'The Call is not waiting for policy review.');
|
|
1404
|
-
const
|
|
1405
|
-
this.#database.prepare(`UPDATE calls SET
|
|
1406
|
-
.run(approved ? json(
|
|
1506
|
+
const approved = review.status === 'reviewed' && review.outcome === 'allow';
|
|
1507
|
+
this.#database.prepare(`UPDATE calls SET continuation_json = ?, state = ?, wait_reason = ?, auto_review_passed = ?, auto_review_json = ?, revision = revision + 1, updated_at = ? WHERE id = ?`)
|
|
1508
|
+
.run(approved ? json({}) : json({ prompt: policyPrompt(String(current.capability_id), review.rationale) }), approved ? 'QUEUED' : 'WAITING', approved ? null : 'user_input', review.status === 'failed' ? null : approved ? 1 : 0, json(review), now(), callId);
|
|
1509
|
+
return this.getCall(projectId, callId);
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
autoReviewContext(projectId, agentId) {
|
|
1513
|
+
const agent = this.getAgent(projectId, agentId);
|
|
1514
|
+
const authorization = [];
|
|
1515
|
+
let ancestor = agent;
|
|
1516
|
+
while (ancestor !== null) {
|
|
1517
|
+
// Read original messages, including compacted ones. Copied/delegated text and deleted
|
|
1518
|
+
// pending messages must not turn into user permission merely because their role is user.
|
|
1519
|
+
authorization.push(...this.#all(`SELECT m.id, m.agent_id, m.content, m.created_at, m.rowid AS sequence,
|
|
1520
|
+
COALESCE(json_extract(m.metadata_json, '$.ingress_origin'), 'unknown') AS ingress_origin
|
|
1521
|
+
FROM messages m WHERE m.agent_id = ? AND m.role = 'user' AND m.source_agent_id IS NULL
|
|
1522
|
+
AND (m.metadata_json IS NULL OR json_extract(m.metadata_json, '$.ingress_origin') = 'user')
|
|
1523
|
+
AND (EXISTS (SELECT 1 FROM pending_messages p WHERE p.message_id = m.id)
|
|
1524
|
+
OR EXISTS (SELECT 1 FROM context_items ci JOIN context_turn_groups g ON g.id = ci.turn_group_id
|
|
1525
|
+
WHERE ci.source_id = m.id AND ci.kind = 'message' AND g.state != 'ABORTED'))
|
|
1526
|
+
ORDER BY m.rowid`, stringField(ancestor.id)).map(doc));
|
|
1527
|
+
ancestor = typeof ancestor.parent_agent_id === 'string' ? this.getAgent(projectId, ancestor.parent_agent_id) : null;
|
|
1528
|
+
}
|
|
1529
|
+
authorization.sort((a, b) => Number(a.sequence) - Number(b.sequence));
|
|
1530
|
+
const evidence = this.#all(`SELECT ci.role, ci.kind, ci.tool_name,
|
|
1531
|
+
substr(ci.content, 1, 1000) AS excerpt, CASE WHEN length(ci.content) > 1000 THEN 1 ELSE 0 END AS truncated
|
|
1532
|
+
FROM context_items ci JOIN context_turn_groups g ON g.id = ci.turn_group_id
|
|
1533
|
+
WHERE ci.agent_id = ? AND ci.visible = 1 AND g.state != 'ABORTED'
|
|
1534
|
+
ORDER BY ci.ordinal DESC LIMIT 12`, agentId).map(doc).reverse();
|
|
1535
|
+
return { authorization, workspacePaths: this.projectWorkspacePaths(projectId, stringField(agent.workspace_id)), evidence };
|
|
1536
|
+
}
|
|
1537
|
+
consumePendingForStep(projectId, stepId) {
|
|
1538
|
+
const step = this.getStep(projectId, stepId);
|
|
1539
|
+
const run = this.getRun(projectId, stringField(step.run_id));
|
|
1540
|
+
const agentId = stringField(run.agent_id);
|
|
1541
|
+
const pending = this.#all('SELECT * FROM pending_messages WHERE agent_id = ? ORDER BY ordinal, id', agentId);
|
|
1542
|
+
if (pending.length === 0)
|
|
1543
|
+
return [];
|
|
1544
|
+
const changes = [...refs(['agent', agentId], ['run', stringField(run.id)], ...pending.map(entry => ['pending_message', String(entry.id)]))];
|
|
1545
|
+
return this.#write(projectId, changes, () => {
|
|
1546
|
+
if (step.state === 'TERMINAL' || run.state === 'TERMINAL')
|
|
1547
|
+
fail('RUN_STATE_CONFLICT', 409, 'The Run no longer accepts pending input.');
|
|
1548
|
+
this.#assertContext(agentId, false);
|
|
1549
|
+
const timestamp = now();
|
|
1550
|
+
changes.push(...this.#appendPendingContext(agentId, stringField(step.turn_group_id), pending, timestamp));
|
|
1551
|
+
const goals = [...new Set([...strings(run.goal_message_ids ?? []), ...pending.map(entry => String(entry.message_id))])];
|
|
1552
|
+
this.#database.prepare('UPDATE runs SET goal_message_ids_json = ?, revision = revision + 1, updated_at = ? WHERE id = ?')
|
|
1553
|
+
.run(json(goals), timestamp, run.id);
|
|
1554
|
+
const remove = this.#database.prepare('DELETE FROM pending_messages WHERE id = ?');
|
|
1555
|
+
for (const entry of pending)
|
|
1556
|
+
remove.run(entry.id);
|
|
1557
|
+
return pending.map(doc);
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
#appendPendingContext(agentId, turnGroupId, pending, timestamp) {
|
|
1561
|
+
const changes = [];
|
|
1562
|
+
for (const entry of pending) {
|
|
1563
|
+
const item = this.#database.prepare(`SELECT * FROM context_items WHERE agent_id = ? AND kind = 'message' AND source_id = ?`).get(agentId, entry.message_id);
|
|
1564
|
+
// claimRun has already placed its initial batch in this group and queue order.
|
|
1565
|
+
if (item?.turn_group_id === turnGroupId)
|
|
1566
|
+
continue;
|
|
1567
|
+
const contextId = item === undefined ? id('context') : String(item.id);
|
|
1568
|
+
const ordinal = this.#nextOrdinal('context_items', 'agent_id', agentId);
|
|
1569
|
+
const groupOrdinal = this.#nextOrdinal('context_items', 'turn_group_id', turnGroupId);
|
|
1570
|
+
if (item === undefined) {
|
|
1571
|
+
const message = this.#require('messages', String(entry.message_id));
|
|
1572
|
+
this.#database.prepare(`INSERT INTO context_items(id, agent_id, turn_group_id, turn_group_ordinal, kind, source_id, role, content, ordinal, visible, locked, revision, created_at, updated_at, metadata_json) VALUES (?, ?, ?, ?, 'message', ?, 'user', ?, ?, 1, 0, 0, ?, ?, ?)`)
|
|
1573
|
+
.run(contextId, agentId, turnGroupId, groupOrdinal, entry.message_id, entry.content, ordinal, timestamp, timestamp, message.metadata_json);
|
|
1574
|
+
}
|
|
1575
|
+
else {
|
|
1576
|
+
this.#database.prepare('UPDATE context_items SET turn_group_id = ?, turn_group_ordinal = ?, ordinal = ?, visible = 1, revision = revision + 1, updated_at = ? WHERE id = ?')
|
|
1577
|
+
.run(turnGroupId, groupOrdinal, ordinal, timestamp, item.id);
|
|
1578
|
+
if (this.#count('SELECT COUNT(*) AS count FROM context_items WHERE turn_group_id = ?', item.turn_group_id) === 0) {
|
|
1579
|
+
this.#database.prepare('DELETE FROM context_turn_groups WHERE id = ? AND NOT EXISTS (SELECT 1 FROM steps WHERE turn_group_id = ?)').run(item.turn_group_id, item.turn_group_id);
|
|
1580
|
+
changes.push({ type: 'turn_group', id: String(item.turn_group_id) });
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
changes.push({ type: 'context_item', id: contextId });
|
|
1584
|
+
}
|
|
1585
|
+
if (changes.length > 0)
|
|
1586
|
+
this.#touchContext(agentId);
|
|
1587
|
+
return changes;
|
|
1588
|
+
}
|
|
1589
|
+
interruptCall(projectId, callId, errorCode, message) {
|
|
1590
|
+
const changes = [];
|
|
1591
|
+
return this.#write(projectId, changes, () => {
|
|
1592
|
+
const call = this.#require('calls', callId);
|
|
1593
|
+
owned(call, 'project_id', projectId);
|
|
1594
|
+
changes.push(...this.#interruptCall(call, errorCode, message, now()));
|
|
1407
1595
|
return this.getCall(projectId, callId);
|
|
1408
1596
|
});
|
|
1409
1597
|
}
|
|
1410
|
-
|
|
1598
|
+
interruptProjectCalls(projectId) {
|
|
1599
|
+
const changes = [];
|
|
1600
|
+
this.#write(projectId, changes, () => {
|
|
1601
|
+
for (const call of this.#all("SELECT * FROM calls WHERE project_id = ? AND state != 'TERMINAL'", projectId)) {
|
|
1602
|
+
changes.push(...this.#interruptCall(call, 'PROCESS_INTERRUPTED', 'The runtime stopped; this Call will not be resumed.', now()));
|
|
1603
|
+
}
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
#interruptCall(call, errorCode, message, timestamp) {
|
|
1607
|
+
if (call.state === 'TERMINAL')
|
|
1608
|
+
return [];
|
|
1609
|
+
const running = call.state === 'RUNNING';
|
|
1610
|
+
const outcome = errorCode === 'CALL_CANCELLED' || !running ? 'cancelled' : 'interrupted';
|
|
1611
|
+
const detail = running ? `${message} External side effects may already have occurred.` : message;
|
|
1612
|
+
this.#database.prepare("UPDATE calls SET state = 'TERMINAL', wait_reason = NULL, outcome = ?, outcome_unknown = ?, revision = revision + 1, updated_at = ? WHERE id = ?")
|
|
1613
|
+
.run(outcome, running ? 1 : 0, timestamp, call.id);
|
|
1614
|
+
const resultId = id('result');
|
|
1615
|
+
this.#database.prepare('INSERT OR IGNORE INTO results(id, call_id, outcome, content_json, error_code, error_message, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)')
|
|
1616
|
+
.run(resultId, call.id, outcome, json({ error_code: errorCode, error_message: detail, outcome_unknown: running }), errorCode, detail, timestamp);
|
|
1617
|
+
return [{ type: 'call', id: String(call.id) }, { type: 'result', id: resultId }];
|
|
1618
|
+
}
|
|
1619
|
+
callAdmission(projectId, callId) {
|
|
1620
|
+
return this.#callAdmission(projectId, this.#callRow(projectId, callId));
|
|
1621
|
+
}
|
|
1622
|
+
#callAdmission(projectId, call) {
|
|
1623
|
+
if (call.state !== 'QUEUED')
|
|
1624
|
+
return 'inactive';
|
|
1625
|
+
if (typeof call.parent_call_id === 'string') {
|
|
1626
|
+
const parent = this.#callRow(projectId, call.parent_call_id);
|
|
1627
|
+
if (parent.state !== 'RUNNING')
|
|
1628
|
+
fail('CALL_PARENT_INACTIVE', 409, 'The parent Call is no longer running.');
|
|
1629
|
+
if (call.source !== 'program' || parent.capability_id !== 'programmatic_tool_run' ||
|
|
1630
|
+
typeof call.agent_id !== 'string' || typeof call.step_id !== 'string' || typeof call.attempt_id !== 'string' ||
|
|
1631
|
+
parent.agent_id !== call.agent_id || parent.step_id !== call.step_id || parent.attempt_id !== call.attempt_id) {
|
|
1632
|
+
fail('CALL_PARENT_INVALID', 409, 'The nested Call does not belong to its program parent.');
|
|
1633
|
+
}
|
|
1634
|
+
const frozen = this.#database.prepare('SELECT 1 FROM attempt_capabilities WHERE attempt_id = ? AND capability_id = ?')
|
|
1635
|
+
.get(call.attempt_id, call.capability_id);
|
|
1636
|
+
if (frozen === undefined)
|
|
1637
|
+
fail('CAPABILITY_NOT_LOADED', 403, 'The nested Capability is outside the frozen model attempt.');
|
|
1638
|
+
const selected = jsonRecord(parent.input)?.capabilityIds;
|
|
1639
|
+
if (Array.isArray(selected) && !selected.includes(call.capability_id ?? null)) {
|
|
1640
|
+
fail('CAPABILITY_NOT_LOADED', 403, 'The nested Capability is outside the parent program selection.');
|
|
1641
|
+
}
|
|
1642
|
+
// Program dependencies are inside the parent; its unfinished top-level batch cannot gate a child.
|
|
1643
|
+
return 'ready';
|
|
1644
|
+
}
|
|
1645
|
+
if (typeof call.partition_id !== 'string')
|
|
1646
|
+
return 'ready';
|
|
1647
|
+
const partition = this.#require('call_partitions', call.partition_id);
|
|
1648
|
+
if (partition.step_id !== call.step_id)
|
|
1649
|
+
fail('CALL_PARTITION_INVALID', 409, 'The Call does not belong to its partition Step.');
|
|
1650
|
+
const predecessor = this.#database.prepare(`SELECT 1 FROM calls c JOIN call_partitions p ON p.id = c.partition_id
|
|
1651
|
+
WHERE p.step_id = ? AND p.ordinal < ? AND c.parent_call_id IS NULL
|
|
1652
|
+
AND c.attempt_id IS ? AND c.state != 'TERMINAL' LIMIT 1`)
|
|
1653
|
+
.get(partition.step_id, partition.ordinal, call.attempt_id ?? null);
|
|
1654
|
+
return predecessor === undefined ? 'ready' : 'blocked';
|
|
1655
|
+
}
|
|
1656
|
+
startCall(projectId, callId, validate) {
|
|
1411
1657
|
return this.#write(projectId, refs(['call', callId]), () => {
|
|
1658
|
+
const call = this.#callRow(projectId, callId);
|
|
1659
|
+
const admission = this.#callAdmission(projectId, call);
|
|
1660
|
+
if (admission === 'blocked')
|
|
1661
|
+
fail('CALL_BLOCKED', 409, 'An earlier top-level partition has not finished.');
|
|
1662
|
+
if (admission === 'inactive')
|
|
1663
|
+
fail('CALL_STATE_CONFLICT', 409, 'The Call is not queued.');
|
|
1664
|
+
validate?.(call);
|
|
1412
1665
|
const result = this.#database.prepare(`UPDATE calls SET state = 'RUNNING', revision = revision + 1, updated_at = ? WHERE id = ? AND state = 'QUEUED'`).run(now(), callId);
|
|
1413
1666
|
if (result.changes !== 1)
|
|
1414
1667
|
fail('CALL_STATE_CONFLICT', 409, 'The Call is not queued.');
|
|
1415
1668
|
return this.getCall(projectId, callId);
|
|
1416
1669
|
});
|
|
1417
1670
|
}
|
|
1671
|
+
failQueuedCall(projectId, callId, errorCode, errorMessage, content = {}) {
|
|
1672
|
+
const resultId = id('result');
|
|
1673
|
+
const timestamp = now();
|
|
1674
|
+
return this.#write(projectId, refs(['call', callId], ['result', resultId]), () => {
|
|
1675
|
+
this.#callRow(projectId, callId);
|
|
1676
|
+
const update = this.#database.prepare(`UPDATE calls SET state = 'TERMINAL', outcome = 'failed', wait_reason = NULL,
|
|
1677
|
+
outcome_unknown = 0, revision = revision + 1, updated_at = ? WHERE id = ? AND state = 'QUEUED'`).run(timestamp, callId);
|
|
1678
|
+
if (update.changes !== 1)
|
|
1679
|
+
fail('CALL_STATE_CONFLICT', 409, 'Only a queued Call can fail without executing.');
|
|
1680
|
+
this.#database.prepare(`INSERT INTO results(id, call_id, outcome, content_json, error_code, error_message, created_at)
|
|
1681
|
+
VALUES (?, ?, 'failed', ?, ?, ?, ?)`).run(resultId, callId, json(content), errorCode, errorMessage, timestamp);
|
|
1682
|
+
return { call: this.getCall(projectId, callId), result: doc(this.#require('results', resultId)) };
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1418
1685
|
waitCall(projectId, callId, prompt) {
|
|
1419
1686
|
return this.#write(projectId, refs(['call', callId]), () => {
|
|
1420
1687
|
const current = this.#require('calls', callId);
|
|
1421
1688
|
owned(current, 'project_id', projectId);
|
|
1422
1689
|
if (current.state !== 'RUNNING')
|
|
1423
1690
|
fail('CALL_STATE_CONFLICT', 409, 'Only a running Call can request user input.');
|
|
1424
|
-
this.#database.prepare(`UPDATE calls SET state = 'WAITING', wait_reason = 'user_input',
|
|
1425
|
-
.run(json({
|
|
1691
|
+
this.#database.prepare(`UPDATE calls SET state = 'WAITING', wait_reason = 'user_input', continuation_json = ?, revision = revision + 1, updated_at = ? WHERE id = ?`)
|
|
1692
|
+
.run(json({ prompt }), now(), callId);
|
|
1426
1693
|
return this.getCall(projectId, callId);
|
|
1427
1694
|
});
|
|
1428
1695
|
}
|
|
1429
|
-
completeCall(projectId, callId, outcome, content, errorCode = null, errorMessage = null, artifacts = [], truncated = false) {
|
|
1696
|
+
completeCall(projectId, callId, outcome, content, errorCode = null, errorMessage = null, artifacts = [], truncated = false, capture = 'unknown', stored = 'unknown') {
|
|
1430
1697
|
const resultId = id('result');
|
|
1431
1698
|
const timestamp = now();
|
|
1432
1699
|
return this.#write(projectId, refs(['call', callId], ['result', resultId]), () => {
|
|
@@ -1436,6 +1703,7 @@ export class Store {
|
|
|
1436
1703
|
fail('CALL_STATE_CONFLICT', 409, 'The Call is not running.');
|
|
1437
1704
|
this.#database.prepare(`INSERT INTO results(id, call_id, outcome, content_json, truncated, error_code, error_message, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1438
1705
|
.run(resultId, callId, outcome, json(content), truncated ? 1 : 0, errorCode, errorMessage, timestamp);
|
|
1706
|
+
this.#database.prepare('UPDATE results SET capture = ?, stored = ? WHERE id = ?').run(capture, stored, resultId);
|
|
1439
1707
|
const link = this.#database.prepare('INSERT INTO result_artifacts(result_id, artifact_id) VALUES (?, ?)');
|
|
1440
1708
|
for (const artifactId of artifacts)
|
|
1441
1709
|
link.run(resultId, artifactId);
|
|
@@ -1455,10 +1723,9 @@ export class Store {
|
|
|
1455
1723
|
}
|
|
1456
1724
|
/**
|
|
1457
1725
|
* 02A `ResultDocument`. One `content_json` column becomes `text` or `structured` according to its
|
|
1458
|
-
* JSON type
|
|
1459
|
-
*
|
|
1460
|
-
*
|
|
1461
|
-
* That is why a client reading a large Result has to fall back to `artifact_ids` and ART-01.
|
|
1726
|
+
* JSON type. Short results retain their original type; long non-PTC results carry a text preview
|
|
1727
|
+
* and full-body Artifact. Earlier results may have null content with only an Artifact reference.
|
|
1728
|
+
* Clients reading the full large Result use `artifact_ids` and ART-01.
|
|
1462
1729
|
*
|
|
1463
1730
|
* `error_code` stays off the wire here because the documented shape does not carry it; a failed
|
|
1464
1731
|
* Call reports its code on `CallSummary.error_code`.
|
|
@@ -1478,11 +1745,13 @@ export class Store {
|
|
|
1478
1745
|
.map((artifact) => jsonRecord(artifact)?.id)
|
|
1479
1746
|
.filter((value) => typeof value === 'string'),
|
|
1480
1747
|
truncated: result.truncated === true,
|
|
1748
|
+
payload: { capture: result.capture ?? 'unknown', stored: result.stored ?? 'unknown', view: result.truncated === true ? 'preview' : 'full' },
|
|
1481
1749
|
created_at: result.created_at ?? null
|
|
1482
1750
|
};
|
|
1483
1751
|
}
|
|
1484
1752
|
includeResult(projectId, resultId, agentId, expectedContextRevision) {
|
|
1485
1753
|
const result = this.getResult(projectId, resultId);
|
|
1754
|
+
const call = this.#require('calls', stringField(result.call_id));
|
|
1486
1755
|
const contextId = id('context');
|
|
1487
1756
|
const turnGroupId = id('turn');
|
|
1488
1757
|
const timestamp = now();
|
|
@@ -1493,7 +1762,7 @@ export class Store {
|
|
|
1493
1762
|
const ordinal = this.#nextOrdinal('context_items', 'agent_id', agentId);
|
|
1494
1763
|
this.#insertTurnGroup(turnGroupId, agentId, 'included', 'COMPLETE', timestamp);
|
|
1495
1764
|
this.#database.prepare(`INSERT INTO context_items(id, agent_id, turn_group_id, turn_group_ordinal, kind, source_id, role, content, ordinal, visible, locked, revision, created_at, updated_at) VALUES (?, ?, ?, 0, 'result', ?, 'tool', ?, ?, 1, 0, 0, ?, ?)`)
|
|
1496
|
-
.run(contextId, agentId, turnGroupId, resultId,
|
|
1765
|
+
.run(contextId, agentId, turnGroupId, resultId, this.#resultContextContent(call, result), ordinal, timestamp, timestamp);
|
|
1497
1766
|
this.#touchContext(agentId);
|
|
1498
1767
|
return this.getContextItem(projectId, contextId);
|
|
1499
1768
|
});
|
|
@@ -1516,6 +1785,75 @@ export class Store {
|
|
|
1516
1785
|
owned(row, 'project_id', projectId);
|
|
1517
1786
|
return doc(row);
|
|
1518
1787
|
}
|
|
1788
|
+
getAgentArtifact(projectId, agentId, artifactId) {
|
|
1789
|
+
this.getAgent(projectId, agentId);
|
|
1790
|
+
const artifact = this.getArtifact(projectId, artifactId);
|
|
1791
|
+
const linked = this.#database.prepare(`SELECT 1 FROM context_items ci
|
|
1792
|
+
JOIN result_artifacts ra ON ra.result_id = ci.source_id
|
|
1793
|
+
WHERE ci.agent_id = ? AND ci.kind = 'result' AND ci.visible = 1 AND ra.artifact_id = ? LIMIT 1`).get(agentId, artifactId);
|
|
1794
|
+
if (linked === undefined)
|
|
1795
|
+
fail('ARTIFACT_NOT_ACCESSIBLE', 404, 'Artifact is not referenced by this Agent visible history.');
|
|
1796
|
+
const captures = this.#all(`SELECT DISTINCT r.capture FROM results r JOIN result_artifacts ra ON ra.result_id = r.id
|
|
1797
|
+
JOIN context_items ci ON ci.source_id = r.id AND ci.kind = 'result'
|
|
1798
|
+
WHERE ci.agent_id = ? AND ci.visible = 1 AND ra.artifact_id = ?`, agentId, artifactId).map((row) => row.capture);
|
|
1799
|
+
return { ...artifact, capture: captures.includes('partial') ? 'partial' : captures.includes('unknown') ? 'unknown' : 'complete' };
|
|
1800
|
+
}
|
|
1801
|
+
readHistoryItem(projectId, agentId, contextId) {
|
|
1802
|
+
this.getAgent(projectId, agentId);
|
|
1803
|
+
const row = this.#database.prepare(`SELECT ci.*, CASE WHEN co.instructions IS NULL THEN ci.content
|
|
1804
|
+
ELSE co.instructions || char(10) || char(10) || ci.content END AS content
|
|
1805
|
+
, a.session_id, a.task_id, a.name AS agent_name, a.parent_agent_id
|
|
1806
|
+
FROM context_items ci JOIN agents a ON a.id = ci.agent_id JOIN tasks t ON t.id = a.task_id
|
|
1807
|
+
LEFT JOIN context_overrides co ON co.agent_id = ci.agent_id
|
|
1808
|
+
AND co.target_kind = ci.kind AND co.target_id = ci.source_id
|
|
1809
|
+
WHERE ci.id = ? AND t.project_id = ? AND ci.visible = 1`).get(contextId, projectId);
|
|
1810
|
+
if (row === undefined)
|
|
1811
|
+
fail('HISTORY_NOT_ACCESSIBLE', 404, 'History item is not visible in this Project.');
|
|
1812
|
+
return doc(row);
|
|
1813
|
+
}
|
|
1814
|
+
searchHistory(projectId, agentId, query, after, limit, scope = {}) {
|
|
1815
|
+
this.getAgent(projectId, agentId);
|
|
1816
|
+
if (scope.agent_id !== undefined)
|
|
1817
|
+
this.getAgent(projectId, scope.agent_id);
|
|
1818
|
+
if (scope.session_id !== undefined)
|
|
1819
|
+
this.getSession(projectId, scope.session_id);
|
|
1820
|
+
if (scope.task_id !== undefined)
|
|
1821
|
+
this.getTask(projectId, scope.task_id);
|
|
1822
|
+
const filters = ['t.project_id = ?', 'ci.visible = 1', 'ci.ordinal > ?'];
|
|
1823
|
+
const parameters = [projectId, after];
|
|
1824
|
+
for (const field of ['agent_id', 'session_id', 'task_id']) {
|
|
1825
|
+
const value = scope[field];
|
|
1826
|
+
if (value === undefined)
|
|
1827
|
+
continue;
|
|
1828
|
+
filters.push(`${field === 'agent_id' ? 'ci' : 'a'}.${field} = ?`);
|
|
1829
|
+
parameters.push(value);
|
|
1830
|
+
}
|
|
1831
|
+
if (scope.after_context_item_id !== undefined) {
|
|
1832
|
+
const cursor = this.readHistoryItem(projectId, agentId, scope.after_context_item_id);
|
|
1833
|
+
for (const field of ['agent_id', 'session_id', 'task_id']) {
|
|
1834
|
+
if (scope[field] !== undefined && scope[field] !== cursor[field])
|
|
1835
|
+
fail('INVALID_INPUT', 400, 'History continuation must use the same scope.');
|
|
1836
|
+
}
|
|
1837
|
+
const owner = this.getAgent(projectId, stringField(cursor.agent_id));
|
|
1838
|
+
filters.push('(a.created_at, a.id, ci.ordinal) > (?, ?, ?)');
|
|
1839
|
+
parameters.push(stringField(owner.created_at), stringField(cursor.agent_id), Number(cursor.ordinal));
|
|
1840
|
+
}
|
|
1841
|
+
const rows = this.#all(`WITH history AS (
|
|
1842
|
+
SELECT ci.id, ci.source_id, ci.agent_id, a.session_id, a.task_id, a.name AS agent_name, a.parent_agent_id,
|
|
1843
|
+
a.created_at AS agent_created_at, ci.ordinal, ci.role, ci.kind, ci.locked, ci.revision,
|
|
1844
|
+
CASE WHEN co.instructions IS NULL THEN ci.content ELSE co.instructions || char(10) || char(10) || ci.content END AS body
|
|
1845
|
+
FROM context_items ci JOIN agents a ON a.id = ci.agent_id JOIN tasks t ON t.id = a.task_id
|
|
1846
|
+
LEFT JOIN context_overrides co ON co.agent_id = ci.agent_id
|
|
1847
|
+
AND co.target_kind = ci.kind AND co.target_id = ci.source_id
|
|
1848
|
+
WHERE ${filters.join(' AND ')})
|
|
1849
|
+
SELECT id AS context_item_id, source_id, agent_id, session_id, task_id, agent_name, parent_agent_id, ordinal, role, kind, locked, revision,
|
|
1850
|
+
substr(body, max(1, instr(body, ?) - 50), 160) AS snippet
|
|
1851
|
+
FROM history WHERE ? = '' OR instr(body, ?) > 0 ORDER BY agent_created_at, agent_id, ordinal LIMIT ?`, ...parameters, query, query, query, limit + 1);
|
|
1852
|
+
const matches = rows.slice(0, limit).map(doc);
|
|
1853
|
+
return { matches, has_more: rows.length > limit,
|
|
1854
|
+
next_after_context_item_id: rows.length > limit ? matches.at(-1)?.context_item_id ?? null : null,
|
|
1855
|
+
next_after_ordinal: rows.length > limit && scope.agent_id !== undefined ? matches.at(-1)?.ordinal ?? null : null };
|
|
1856
|
+
}
|
|
1519
1857
|
listArtifacts(projectId, options) {
|
|
1520
1858
|
this.getProject(projectId);
|
|
1521
1859
|
// Explicit order: the shared pager defaults to `updated_at`, which this table does not have
|
|
@@ -1534,27 +1872,100 @@ export class Store {
|
|
|
1534
1872
|
const order = direction === 'backward' ? 'ordinal DESC, id DESC' : 'ordinal ASC, id ASC';
|
|
1535
1873
|
return this.#page('SELECT * FROM context_items WHERE agent_id = ?', [agentId], options, order);
|
|
1536
1874
|
}
|
|
1537
|
-
contextSource(projectId, agentId) {
|
|
1875
|
+
contextSource(projectId, agentId, completedOnly = false) {
|
|
1876
|
+
const items = this.#all(`SELECT * FROM context_items WHERE agent_id = ? AND visible = 1 ${completedOnly ? "AND turn_group_id IN (SELECT id FROM context_turn_groups WHERE state = 'COMPLETE')" : ''} ORDER BY ordinal, id`, agentId).map(doc);
|
|
1877
|
+
const ids = new Set(items.map((item) => item.id));
|
|
1538
1878
|
return {
|
|
1539
1879
|
agent: this.getAgent(projectId, agentId),
|
|
1540
|
-
items
|
|
1880
|
+
items,
|
|
1881
|
+
todo: this.getTodo(projectId, agentId),
|
|
1882
|
+
unsettledItemIds: this.#unsettledContextItems(agentId),
|
|
1883
|
+
workingItems: this.#workingItems(agentId).filter((item) => ids.has(item.id)),
|
|
1541
1884
|
overrides: this.#all('SELECT * FROM context_overrides WHERE agent_id = ? ORDER BY created_at, id', agentId).map(doc),
|
|
1542
1885
|
capabilities: this.listCapabilities(projectId, agentId).filter((entry) => entry.loaded === true)
|
|
1543
1886
|
};
|
|
1544
1887
|
}
|
|
1545
|
-
|
|
1888
|
+
workingContext(projectId, agentId) {
|
|
1889
|
+
this.getAgent(projectId, agentId);
|
|
1890
|
+
const state = this.#database.prepare('SELECT * FROM working_contexts WHERE agent_id = ?').get(agentId);
|
|
1891
|
+
return { ...(state === undefined ? {} : doc(state)), items: this.#workingItems(agentId) };
|
|
1892
|
+
}
|
|
1893
|
+
saveWorkingContext(projectId, agentId, sourceRevision, edits, messages, prefixes, requestFrame) {
|
|
1894
|
+
const state = this.workingContext(projectId, agentId);
|
|
1895
|
+
const messageJson = json(messages);
|
|
1896
|
+
if (edits.length === 0 && json(state.messages ?? null) === messageJson && json(state.request_frame ?? null) === json(requestFrame))
|
|
1897
|
+
return;
|
|
1898
|
+
this.#write(projectId, refs(['agent', agentId]), () => {
|
|
1899
|
+
if (this.getAgent(projectId, agentId).context_revision !== sourceRevision)
|
|
1900
|
+
fail('REVISION_CONFLICT', 409, 'Context changed before Working Context publication.');
|
|
1901
|
+
const update = this.#database.prepare('UPDATE working_context_items SET content = ?, snipped = 1 WHERE agent_id = ? AND context_item_id = ?');
|
|
1902
|
+
for (const edit of edits) {
|
|
1903
|
+
if (update.run(edit.content, agentId, edit.id).changes !== 1)
|
|
1904
|
+
fail('CONTEXT_ITEM_CONFLICT', 409, 'A Working Context source is no longer active.');
|
|
1905
|
+
}
|
|
1906
|
+
this.#database.prepare('UPDATE working_contexts SET revision = revision + 1, digest = ?, messages_json = ?, prefixes_json = ?, request_frame_json = ?, updated_at = ? WHERE agent_id = ?')
|
|
1907
|
+
.run(digest({ items: this.#workingItems(agentId), prefixes }), messageJson, json(prefixes), json(requestFrame), now(), agentId);
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
compactionSources(projectId, contextItemId) {
|
|
1911
|
+
this.getContextItem(projectId, contextItemId);
|
|
1912
|
+
const row = this.#database.prepare('SELECT * FROM context_compactions WHERE context_item_id = ?').get(contextItemId);
|
|
1913
|
+
if (row === undefined)
|
|
1914
|
+
return { context_item_id: contextItemId, covered_item_ids: [], coverage_known: false };
|
|
1915
|
+
const record = doc(row);
|
|
1916
|
+
return { ...record, coverage_known: true, items: strings(record.covered_item_ids ?? []).map((itemId) => this.getContextItem(projectId, itemId)) };
|
|
1917
|
+
}
|
|
1918
|
+
getTodo(projectId, agentId) {
|
|
1546
1919
|
this.getAgent(projectId, agentId);
|
|
1547
1920
|
const row = this.#database.prepare('SELECT * FROM agent_plans WHERE agent_id = ?').get(agentId);
|
|
1548
1921
|
return row === undefined ? null : doc(row);
|
|
1549
1922
|
}
|
|
1550
|
-
|
|
1923
|
+
updateTodo(projectId, agentId, items) {
|
|
1924
|
+
const current = this.getTodo(projectId, agentId);
|
|
1925
|
+
if (current !== null && digest(current.items ?? []) === digest(items))
|
|
1926
|
+
return current;
|
|
1551
1927
|
return this.#write(projectId, refs(['agent_plan', agentId]), () => {
|
|
1552
1928
|
this.getAgent(projectId, agentId);
|
|
1553
1929
|
this.#database.prepare(`INSERT INTO agent_plans(agent_id, items_json, revision, updated_at) VALUES (?, ?, 0, ?) ON CONFLICT(agent_id) DO UPDATE SET items_json = excluded.items_json, revision = agent_plans.revision + 1, updated_at = excluded.updated_at`)
|
|
1554
1930
|
.run(agentId, json(items), now());
|
|
1931
|
+
this.#touchContext(agentId);
|
|
1555
1932
|
return doc(this.#one('SELECT * FROM agent_plans WHERE agent_id = ?', agentId));
|
|
1556
1933
|
});
|
|
1557
1934
|
}
|
|
1935
|
+
syncTodoReminder(projectId, agentId) {
|
|
1936
|
+
const source = this.contextSource(projectId, agentId);
|
|
1937
|
+
if (!source.capabilities.some((entry) => entry.capability_id === 'todo'))
|
|
1938
|
+
return;
|
|
1939
|
+
const state = this.#database.prepare('SELECT * FROM todo_reminder_state WHERE agent_id = ?').get(agentId);
|
|
1940
|
+
const cursor = Number(state?.last_context_rowid ?? 0);
|
|
1941
|
+
// Count each newly projected payload once. Compact/Snip/retries must not reset or
|
|
1942
|
+
// increase the cadence; locked original history remains available across restart.
|
|
1943
|
+
const added = this.#all(`SELECT ci.*, ci.rowid AS context_rowid, COALESCE(w.content, ci.content) AS content
|
|
1944
|
+
FROM context_items ci LEFT JOIN working_context_items w ON w.context_item_id = ci.id
|
|
1945
|
+
WHERE ci.agent_id = ? AND ci.rowid > ? ORDER BY ci.rowid`, agentId, cursor);
|
|
1946
|
+
let pending = Number(state?.pending_tokens ?? 0);
|
|
1947
|
+
for (const item of added.map(doc)) {
|
|
1948
|
+
const metadata = item.metadata;
|
|
1949
|
+
if (item.visible !== true || item.locked === true || item.kind === 'compaction' || item.role === 'system' || metadata?.purpose === 'todo_reminder')
|
|
1950
|
+
continue;
|
|
1951
|
+
pending += estimateTokens(json(boundaryMessages([item]).map((message) => ({ role: message.role, content: message.content, ...(message.toolCalls === undefined ? {} : { toolCalls: message.toolCalls }) })))).expected;
|
|
1952
|
+
}
|
|
1953
|
+
const messages = boundaryMessages(activeContextItems(source.items));
|
|
1954
|
+
const boundaries = assertContextBoundaries(messages, { allowOpenTail: true, unsettledItemIds: source.unsettledItemIds });
|
|
1955
|
+
const due = pending >= 30000 && boundaries.legalCuts.at(-1) === messages.length;
|
|
1956
|
+
if (added.length === 0 && !due)
|
|
1957
|
+
return;
|
|
1958
|
+
this.#write(projectId, refs(['agent', agentId]), () => {
|
|
1959
|
+
if (due) {
|
|
1960
|
+
this.#insertStandaloneContextMessage(agentId, 'user', 'Todo reminder: read your current checklist with todo({}). Keep the original user goal and applicable instructions in view. Maintain and update the checklist with todo({items: [...]}) as progress or instructions change; create one if needed. This reminder does not change the user goal or prove completion.', 'message', now(), { purpose: 'todo_reminder' });
|
|
1961
|
+
pending %= 30000;
|
|
1962
|
+
}
|
|
1963
|
+
const latest = this.#one('SELECT COALESCE(MAX(rowid), 0) AS cursor FROM context_items WHERE agent_id = ?', agentId);
|
|
1964
|
+
this.#database.prepare(`INSERT INTO todo_reminder_state(agent_id, last_context_rowid, pending_tokens) VALUES (?, ?, ?)
|
|
1965
|
+
ON CONFLICT(agent_id) DO UPDATE SET last_context_rowid = excluded.last_context_rowid, pending_tokens = excluded.pending_tokens`)
|
|
1966
|
+
.run(agentId, Number(latest.cursor), pending);
|
|
1967
|
+
});
|
|
1968
|
+
}
|
|
1558
1969
|
getContextItem(projectId, contextItemId) {
|
|
1559
1970
|
const row = this.#require('context_items', contextItemId);
|
|
1560
1971
|
this.#ownedAgent(String(row.agent_id), projectId);
|
|
@@ -1568,19 +1979,30 @@ export class Store {
|
|
|
1568
1979
|
this.#expect(current, expectedRevision);
|
|
1569
1980
|
if (Number(current.locked) === 1)
|
|
1570
1981
|
fail('CONTEXT_LOCKED', 409, 'The ContextItem is before the latest Compaction boundary.');
|
|
1982
|
+
if (current.kind === 'tool_calls') {
|
|
1983
|
+
const identity = (text) => json(contextToolCalls(text).map(({ id, name }) => ({ id, name })));
|
|
1984
|
+
if (identity(String(current.content)) !== identity(content))
|
|
1985
|
+
fail('CONTEXT_BOUNDARY_INVALID', 409, 'Editing a tool batch cannot change its call identities or members.');
|
|
1986
|
+
}
|
|
1987
|
+
const before = this.#assertContext(agentId);
|
|
1571
1988
|
const timestamp = now();
|
|
1572
1989
|
this.#database.prepare('UPDATE context_items SET content = ?, revision = revision + 1, updated_at = ? WHERE id = ?')
|
|
1573
1990
|
.run(content, timestamp, contextItemId);
|
|
1574
|
-
this.#
|
|
1575
|
-
|
|
1991
|
+
assertContextTransition(before, this.#assertContext(agentId), this.#unsettledContextItems(agentId));
|
|
1992
|
+
this.#touchContext(agentId);
|
|
1576
1993
|
return this.getContextItem(projectId, contextItemId);
|
|
1577
1994
|
});
|
|
1578
1995
|
}
|
|
1579
1996
|
setContextVisibility(projectId, agentId, turnGroupIds, visible, expectedContextRevision) {
|
|
1580
1997
|
return this.#write(projectId, turnGroupIds.map((entry) => ({ type: 'turn_group', id: entry })), () => {
|
|
1581
1998
|
const agent = this.getAgent(projectId, agentId);
|
|
1999
|
+
const before = this.#assertContext(agentId);
|
|
1582
2000
|
if (agent.context_revision !== expectedContextRevision)
|
|
1583
2001
|
fail('REVISION_CONFLICT', 409, 'The Context changed before this command was applied.');
|
|
2002
|
+
const selected = new Set(turnGroupIds);
|
|
2003
|
+
const selection = this.#all('SELECT * FROM context_items WHERE agent_id = ? ORDER BY ordinal, id', agentId).map(doc)
|
|
2004
|
+
.filter((item) => selected.has(stringField(item.turn_group_id)));
|
|
2005
|
+
assertContextBoundaries(boundaryMessages(selection), { unsettledItemIds: this.#unsettledContextItems(agentId) });
|
|
1584
2006
|
const statement = this.#database.prepare(`UPDATE context_items SET visible = ?, revision = revision + 1, updated_at = ? WHERE turn_group_id = ? AND agent_id = ? AND locked = 0`);
|
|
1585
2007
|
for (const groupId of turnGroupIds) {
|
|
1586
2008
|
const group = this.#require('context_turn_groups', groupId);
|
|
@@ -1590,6 +2012,7 @@ export class Store {
|
|
|
1590
2012
|
if (statement.run(visible ? 1 : 0, now(), groupId, agentId).changes !== expected)
|
|
1591
2013
|
fail('CONTEXT_ITEM_CONFLICT', 409, 'A locked TurnGroup cannot change visibility.');
|
|
1592
2014
|
}
|
|
2015
|
+
assertContextTransition(before, this.#assertContext(agentId), this.#unsettledContextItems(agentId));
|
|
1593
2016
|
this.#touchContext(agentId);
|
|
1594
2017
|
return this.#all(`SELECT * FROM context_items WHERE turn_group_id IN (${turnGroupIds.map(() => '?').join(',')}) ORDER BY ordinal`, ...turnGroupIds).map(doc);
|
|
1595
2018
|
});
|
|
@@ -1597,26 +2020,38 @@ export class Store {
|
|
|
1597
2020
|
reorderContext(projectId, agentId, turnGroupIds, expectedContextRevision) {
|
|
1598
2021
|
return this.#write(projectId, turnGroupIds.map((entry) => ({ type: 'turn_group', id: entry })), () => {
|
|
1599
2022
|
const agent = this.getAgent(projectId, agentId);
|
|
2023
|
+
const before = this.#assertContext(agentId);
|
|
1600
2024
|
if (agent.context_revision !== expectedContextRevision)
|
|
1601
2025
|
fail('REVISION_CONFLICT', 409, 'The Context changed before this command was applied.');
|
|
1602
2026
|
const current = this.#all(`SELECT g.id FROM context_turn_groups g WHERE g.agent_id = ? AND g.state = 'COMPLETE' AND NOT EXISTS (SELECT 1 FROM context_items i WHERE i.turn_group_id = g.id AND i.locked = 1) ORDER BY g.ordinal`, agentId).map((row) => String(row.id));
|
|
1603
2027
|
if (current.length !== turnGroupIds.length || current.some((entry) => !turnGroupIds.includes(entry)) || new Set(turnGroupIds).size !== turnGroupIds.length)
|
|
1604
2028
|
fail('CONTEXT_SET_MISMATCH', 409, 'The complete unlocked TurnGroup order is required.');
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
const
|
|
2029
|
+
// A Compact boundary may split a TurnGroup. Reuse only movable slots, leaving archived
|
|
2030
|
+
// rows and the retained part of a partially locked group in place.
|
|
2031
|
+
const movable = new Set(current);
|
|
2032
|
+
const slots = this.#all('SELECT ordinal, turn_group_id FROM context_items WHERE agent_id = ? ORDER BY ordinal', agentId)
|
|
2033
|
+
.filter((row) => movable.has(String(row.turn_group_id))).map((row) => Number(row.ordinal));
|
|
2034
|
+
const groupSlots = this.#all('SELECT id, ordinal FROM context_turn_groups WHERE agent_id = ? ORDER BY ordinal', agentId)
|
|
2035
|
+
.filter((row) => movable.has(String(row.id))).map((row) => Number(row.ordinal));
|
|
2036
|
+
const offset = this.#nextOrdinal('context_items', 'agent_id', agentId);
|
|
2037
|
+
const move = this.#database.prepare('UPDATE context_items SET ordinal = ordinal + ? WHERE turn_group_id = ?');
|
|
2038
|
+
for (const groupId of turnGroupIds)
|
|
2039
|
+
move.run(offset, groupId);
|
|
2040
|
+
const moveGroup = this.#database.prepare('UPDATE context_turn_groups SET ordinal = ordinal + ? WHERE id = ?');
|
|
2041
|
+
const groupOffset = this.#nextOrdinal('context_turn_groups', 'agent_id', agentId);
|
|
1608
2042
|
for (const groupId of turnGroupIds)
|
|
1609
|
-
|
|
1610
|
-
const updateItem = this.#database.prepare('UPDATE context_items SET ordinal = ?,
|
|
2043
|
+
moveGroup.run(groupOffset, groupId);
|
|
2044
|
+
const updateItem = this.#database.prepare('UPDATE context_items SET ordinal = ?, updated_at = ? WHERE id = ?');
|
|
1611
2045
|
const updateGroup = this.#database.prepare('UPDATE context_turn_groups SET ordinal = ?, revision = revision + 1 WHERE id = ?');
|
|
1612
|
-
let
|
|
1613
|
-
for (const [
|
|
1614
|
-
updateGroup.run(
|
|
2046
|
+
let slot = 0;
|
|
2047
|
+
for (const [index, groupId] of turnGroupIds.entries()) {
|
|
2048
|
+
updateGroup.run(groupSlots[index], groupId);
|
|
1615
2049
|
for (const row of this.#all('SELECT id FROM context_items WHERE turn_group_id = ? ORDER BY turn_group_ordinal', groupId)) {
|
|
1616
|
-
updateItem.run(
|
|
1617
|
-
|
|
2050
|
+
updateItem.run(slots[slot], now(), row.id);
|
|
2051
|
+
slot += 1;
|
|
1618
2052
|
}
|
|
1619
2053
|
}
|
|
2054
|
+
assertContextTransition(before, this.#assertContext(agentId), this.#unsettledContextItems(agentId));
|
|
1620
2055
|
this.#touchContext(agentId);
|
|
1621
2056
|
return this.#all('SELECT * FROM context_items WHERE agent_id = ? ORDER BY ordinal', agentId).map(doc);
|
|
1622
2057
|
});
|
|
@@ -1676,11 +2111,14 @@ export class Store {
|
|
|
1676
2111
|
const agent = this.getAgent(projectId, agentId);
|
|
1677
2112
|
if (agent.context_revision !== expectedContextRevision)
|
|
1678
2113
|
fail('REVISION_CONFLICT', 409, 'The Context changed before this command was applied.');
|
|
1679
|
-
if (stages
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
this.#database.prepare(
|
|
1683
|
-
|
|
2114
|
+
if (stages?.some((stage) => stage !== 'snip'))
|
|
2115
|
+
fail('VALIDATION_ERROR', 400, 'Only the snip projection can be reset.');
|
|
2116
|
+
if (stages === undefined || stages.includes('snip')) {
|
|
2117
|
+
this.#database.prepare("DELETE FROM projection_decisions WHERE agent_id = ? AND stage = 'snip'").run(agentId);
|
|
2118
|
+
this.#database.prepare(`UPDATE working_context_items SET content = (SELECT content FROM context_items WHERE id = context_item_id), snipped = 0 WHERE agent_id = ? AND snipped = 1`).run(agentId);
|
|
2119
|
+
this.#touchContext(agentId);
|
|
2120
|
+
}
|
|
2121
|
+
return { agent_id: agentId, context_revision: Number(this.getAgent(projectId, agentId).context_revision) };
|
|
1684
2122
|
});
|
|
1685
2123
|
}
|
|
1686
2124
|
appendProjectionDecision(projectId, agentId, attemptId, stage, sourceTokens, projectedTokens, details) {
|
|
@@ -1692,18 +2130,41 @@ export class Store {
|
|
|
1692
2130
|
return doc(this.#require('projection_decisions', decisionId));
|
|
1693
2131
|
});
|
|
1694
2132
|
}
|
|
1695
|
-
compactContext(projectId, agentId, summary,
|
|
2133
|
+
compactContext(projectId, agentId, summary, coveredItemIds, expectedContextRevision, focus, expectedAgentRevision) {
|
|
1696
2134
|
const contextId = id('context');
|
|
1697
2135
|
const turnGroupId = id('turn');
|
|
1698
2136
|
const timestamp = now();
|
|
1699
2137
|
return this.#write(projectId, refs(['turn_group', turnGroupId], ['context_item', contextId], ['agent', agentId]), () => {
|
|
1700
|
-
this.getAgent(projectId, agentId);
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
2138
|
+
const agent = this.getAgent(projectId, agentId);
|
|
2139
|
+
if (agent.context_revision !== expectedContextRevision)
|
|
2140
|
+
fail('REVISION_CONFLICT', 409, 'Context changed while Compaction was running.');
|
|
2141
|
+
if (expectedAgentRevision !== undefined && agent.revision !== expectedAgentRevision)
|
|
2142
|
+
fail('REVISION_CONFLICT', 409, 'Agent state changed while Compaction was running.');
|
|
2143
|
+
if (summary.trim().length === 0 || coveredItemIds.length === 0)
|
|
2144
|
+
fail('COMPACTION_INVALID', 422, 'Compaction requires a nonempty summary and source range.');
|
|
2145
|
+
const active = activeContextItems(this.#all('SELECT * FROM context_items WHERE agent_id = ? ORDER BY ordinal', agentId).map(doc))
|
|
2146
|
+
.filter((item) => !(item.kind === 'message' && item.role === 'system'));
|
|
2147
|
+
const covered = active.slice(0, coveredItemIds.length);
|
|
2148
|
+
if (covered.length !== coveredItemIds.length || covered.some((item, index) => item.id !== coveredItemIds[index]))
|
|
2149
|
+
fail('CONTEXT_SET_MISMATCH', 409, 'Compaction must cover an active history prefix.');
|
|
2150
|
+
const boundaries = assertContextBoundaries(boundaryMessages(active), { allowOpenTail: true, unsettledItemIds: this.#unsettledContextItems(agentId) });
|
|
2151
|
+
if (!boundaries.legalCuts.includes(coveredItemIds.length))
|
|
2152
|
+
fail('CONTEXT_BOUNDARY_INVALID', 409, 'Compaction cannot split or cover an unfinished protocol group.', { legal_cuts: boundaries.legalCuts });
|
|
2153
|
+
const firstRetained = active[coveredItemIds.length];
|
|
2154
|
+
const ordinal = firstRetained === undefined ? Math.max(...covered.map((item) => Number(item.ordinal))) + 1 : Number(firstRetained.ordinal);
|
|
2155
|
+
const lock = this.#database.prepare('UPDATE context_items SET locked = 1, revision = revision + 1, updated_at = ? WHERE id = ?');
|
|
2156
|
+
for (const itemId of coveredItemIds)
|
|
2157
|
+
lock.run(timestamp, itemId);
|
|
2158
|
+
// Insert at the cutoff, before the retained tail. Descending moves respect the unique ordinal.
|
|
2159
|
+
const shift = this.#database.prepare('UPDATE context_items SET ordinal = ordinal + 1 WHERE id = ?');
|
|
2160
|
+
for (const row of this.#all('SELECT id FROM context_items WHERE agent_id = ? AND ordinal >= ? ORDER BY ordinal DESC', agentId, ordinal))
|
|
2161
|
+
shift.run(row.id);
|
|
1704
2162
|
this.#insertTurnGroup(turnGroupId, agentId, 'compaction', 'COMPLETE', timestamp);
|
|
1705
|
-
this.#database.prepare(`INSERT INTO context_items(id, agent_id, turn_group_id, turn_group_ordinal, kind, source_id, role, content, ordinal, visible, locked, revision, created_at, updated_at) VALUES (?, ?, ?, 0, 'compaction', ?, '
|
|
2163
|
+
this.#database.prepare(`INSERT INTO context_items(id, agent_id, turn_group_id, turn_group_ordinal, kind, source_id, role, content, ordinal, visible, locked, revision, created_at, updated_at) VALUES (?, ?, ?, 0, 'compaction', ?, 'assistant', ?, ?, 1, 0, 0, ?, ?)`)
|
|
1706
2164
|
.run(contextId, agentId, turnGroupId, contextId, summary, ordinal, timestamp, timestamp);
|
|
2165
|
+
this.#database.prepare('INSERT INTO context_compactions(context_item_id, source_revision, covered_item_ids_json, focus) VALUES (?, ?, ?, ?)')
|
|
2166
|
+
.run(contextId, expectedContextRevision, json(coveredItemIds), focus ?? null);
|
|
2167
|
+
this.#database.prepare('DELETE FROM working_context_items WHERE agent_id = ?').run(agentId);
|
|
1707
2168
|
this.#touchContext(agentId);
|
|
1708
2169
|
return this.getContextItem(projectId, contextId);
|
|
1709
2170
|
});
|
|
@@ -1724,7 +2185,7 @@ export class Store {
|
|
|
1724
2185
|
const boundary = this.#require('context_turn_groups', input.throughTurnGroupId);
|
|
1725
2186
|
if (boundary.agent_id !== sourceAgentId || boundary.state !== 'COMPLETE')
|
|
1726
2187
|
fail('CONTEXT_GROUP_INVALID', 409, 'Context Fork requires a complete source TurnGroup.');
|
|
1727
|
-
if (this.#count('SELECT COUNT(*) AS count FROM context_items WHERE turn_group_id = ? AND locked =
|
|
2188
|
+
if (this.#count('SELECT COUNT(*) AS count FROM context_items WHERE turn_group_id = ? AND locked = 0', input.throughTurnGroupId) === 0)
|
|
1728
2189
|
fail('CONTEXT_LOCKED', 409, 'The Context Fork boundary is locked.');
|
|
1729
2190
|
const throughOrdinal = Number(this.#one('SELECT MAX(ordinal) AS ordinal FROM context_items WHERE turn_group_id = ?', input.throughTurnGroupId).ordinal);
|
|
1730
2191
|
const taskId = stringField(source.task_id);
|
|
@@ -1754,6 +2215,26 @@ export class Store {
|
|
|
1754
2215
|
return this.getAgent(projectId, targetAgentId);
|
|
1755
2216
|
});
|
|
1756
2217
|
}
|
|
2218
|
+
/** Persist one completed Provider response atomically with its tool batch. */
|
|
2219
|
+
appendModelResponse(projectId, agentId, stepId, text, calls, response) {
|
|
2220
|
+
const messageId = id('message');
|
|
2221
|
+
const contextId = id('context');
|
|
2222
|
+
const timestamp = now();
|
|
2223
|
+
const metadata = json({ source_step_id: stepId, model_response: response });
|
|
2224
|
+
return this.#write(projectId, refs(['message', messageId], ['context_item', contextId]), () => {
|
|
2225
|
+
this.getAgent(projectId, agentId);
|
|
2226
|
+
const turnGroupId = stringField(this.getStep(projectId, stepId).turn_group_id);
|
|
2227
|
+
// Display text remains separate from protocol blocks; one Context item keeps the
|
|
2228
|
+
// reasoning, text and tool calls indivisible at Compact/Fork boundaries.
|
|
2229
|
+
this.#database.prepare(`INSERT INTO messages(id, agent_id, source_agent_id, role, content, metadata_json, created_at) VALUES (?, ?, NULL, 'assistant', ?, ?, ?)`)
|
|
2230
|
+
.run(messageId, agentId, text, metadata, timestamp);
|
|
2231
|
+
const hasCalls = calls.length > 0;
|
|
2232
|
+
this.#database.prepare(`INSERT INTO context_items(id, agent_id, turn_group_id, turn_group_ordinal, kind, source_id, role, content, ordinal, visible, locked, revision, created_at, updated_at, metadata_json) VALUES (?, ?, ?, ?, ?, ?, 'assistant', ?, ?, 1, 0, 0, ?, ?, ?)`)
|
|
2233
|
+
.run(contextId, agentId, turnGroupId, this.#nextOrdinal('context_items', 'turn_group_id', turnGroupId), hasCalls ? 'tool_calls' : 'message', hasCalls ? contextId : messageId, hasCalls ? json(calls) : text, this.#nextOrdinal('context_items', 'agent_id', agentId), timestamp, timestamp, metadata);
|
|
2234
|
+
this.#touchContext(agentId);
|
|
2235
|
+
return this.getContextItem(projectId, contextId);
|
|
2236
|
+
});
|
|
2237
|
+
}
|
|
1757
2238
|
appendAssistantMessage(projectId, agentId, stepId, content) {
|
|
1758
2239
|
const messageId = id('message');
|
|
1759
2240
|
const contextId = id('context');
|
|
@@ -1781,6 +2262,7 @@ export class Store {
|
|
|
1781
2262
|
const groupOrdinal = this.#nextOrdinal('context_items', 'turn_group_id', turnGroupId);
|
|
1782
2263
|
this.#database.prepare(`INSERT INTO context_items(id, agent_id, turn_group_id, turn_group_ordinal, kind, source_id, role, content, ordinal, visible, locked, revision, created_at, updated_at) VALUES (?, ?, ?, ?, 'tool_calls', ?, 'assistant', ?, ?, 1, 0, 0, ?, ?)`)
|
|
1783
2264
|
.run(contextId, agentId, turnGroupId, groupOrdinal, contextId, json(calls), ordinal, timestamp, timestamp);
|
|
2265
|
+
this.#database.prepare('UPDATE context_items SET metadata_json = ? WHERE id = ?').run(json({ source_step_id: stepId }), contextId);
|
|
1784
2266
|
this.#touchContext(agentId);
|
|
1785
2267
|
return this.getContextItem(projectId, contextId);
|
|
1786
2268
|
});
|
|
@@ -1794,17 +2276,31 @@ export class Store {
|
|
|
1794
2276
|
const runId = String(step.run_id);
|
|
1795
2277
|
const run = this.#require('runs', runId);
|
|
1796
2278
|
const agentId = String(run.agent_id);
|
|
1797
|
-
|
|
2279
|
+
const changes = [...refs(['step', stepId], ['run', runId], ['agent', agentId])];
|
|
2280
|
+
return this.#write(projectId, changes, () => {
|
|
1798
2281
|
const timestamp = now();
|
|
1799
2282
|
this.#database.prepare(`UPDATE steps SET state = 'TERMINAL', outcome = ?, error_code = ?, error_message = ?, updated_at = ? WHERE id = ? AND state != 'TERMINAL'`)
|
|
1800
2283
|
.run(outcome, errorCode, errorMessage, timestamp, stepId);
|
|
1801
2284
|
const hasPending = this.#count('SELECT COUNT(*) AS count FROM pending_messages WHERE agent_id = ?', agentId) > 0;
|
|
2285
|
+
for (const call of this.#all("SELECT c.* FROM calls c JOIN steps s ON s.id = c.step_id WHERE s.run_id = ? AND c.state != 'TERMINAL'", runId)) {
|
|
2286
|
+
changes.push(...this.#interruptCall(call, errorCode ?? 'RUN_ENDED', errorMessage ?? 'The Run ended before this Call completed.', timestamp));
|
|
2287
|
+
}
|
|
2288
|
+
changes.push(...this.#recoverResultContext(timestamp, agentId));
|
|
1802
2289
|
const continueRun = outcome === 'success' && hasPending;
|
|
1803
2290
|
this.#database.prepare(`UPDATE runs SET state = ?, outcome = ?, error_code = ?, error_message = ?, revision = revision + 1, updated_at = ? WHERE id = ?`)
|
|
1804
2291
|
.run(continueRun ? 'READY' : 'TERMINAL', continueRun ? null : outcome, continueRun ? null : errorCode, continueRun ? null : errorMessage, timestamp, runId);
|
|
1805
|
-
if (
|
|
1806
|
-
|
|
1807
|
-
|
|
2292
|
+
if (outcome === 'failed' && run.state !== 'TERMINAL' && hasPending) {
|
|
2293
|
+
const consumed = new Set(strings(parseJson(String(run.goal_message_ids_json))));
|
|
2294
|
+
const freshInput = this.#all('SELECT message_id FROM pending_messages WHERE agent_id = ?', agentId)
|
|
2295
|
+
.some(message => !consumed.has(String(message.message_id)));
|
|
2296
|
+
// Restored input from a failed compaction is not a new execution request.
|
|
2297
|
+
// Only input not consumed by this Run can schedule a fresh Run; old Calls stay terminal.
|
|
2298
|
+
if (freshInput) {
|
|
2299
|
+
const nextRunId = id('run');
|
|
2300
|
+
this.#database.prepare(`INSERT INTO runs(id, agent_id, goal_message_ids_json, state, outcome, error_code, revision, created_at, updated_at) VALUES (?, ?, '[]', 'READY', NULL, NULL, 0, ?, ?)`)
|
|
2301
|
+
.run(nextRunId, agentId, timestamp, timestamp);
|
|
2302
|
+
changes.push({ type: 'run', id: nextRunId });
|
|
2303
|
+
}
|
|
1808
2304
|
}
|
|
1809
2305
|
this.#database.prepare(`UPDATE agents SET state = 'IDLE', revision = revision + 1, updated_at = ? WHERE id = ?`)
|
|
1810
2306
|
.run(timestamp, agentId);
|
|
@@ -1834,15 +2330,18 @@ export class Store {
|
|
|
1834
2330
|
}
|
|
1835
2331
|
yieldResults(projectId, agentId, callIds) {
|
|
1836
2332
|
const timestamp = now();
|
|
1837
|
-
|
|
2333
|
+
const changes = [];
|
|
2334
|
+
return this.#write(projectId, changes, () => {
|
|
1838
2335
|
this.getAgent(projectId, agentId);
|
|
1839
2336
|
const rows = callIds.map((callId) => {
|
|
1840
2337
|
const call = this.#require('calls', callId);
|
|
2338
|
+
owned(call, 'agent_id', agentId);
|
|
1841
2339
|
if (call.state !== 'TERMINAL')
|
|
1842
2340
|
fail('CALL_STATE_CONFLICT', 409, 'All Calls must be terminal before Result Yield.');
|
|
1843
2341
|
const result = this.#database.prepare('SELECT * FROM results WHERE call_id = ?').get(callId);
|
|
1844
2342
|
if (result === undefined)
|
|
1845
2343
|
fail('RESULT_NOT_FOUND', 503, 'A terminal Call has no Result.');
|
|
2344
|
+
changes.push({ type: 'result', id: String(result.id) });
|
|
1846
2345
|
return { call, result };
|
|
1847
2346
|
}).sort((left, right) => Number(left.call.call_ordinal ?? 0) - Number(right.call.call_ordinal ?? 0));
|
|
1848
2347
|
const yielded = [];
|
|
@@ -1851,26 +2350,153 @@ export class Store {
|
|
|
1851
2350
|
fail('DATABASE_CORRUPT', 503, 'An LLM Call has no Provider Call ID.');
|
|
1852
2351
|
if (typeof call.step_id !== 'string')
|
|
1853
2352
|
fail('DATABASE_CORRUPT', 503, 'An LLM Call has no Step.');
|
|
2353
|
+
if (this.#count("SELECT COUNT(*) AS count FROM context_items WHERE agent_id = ? AND kind = 'result' AND source_id = ? AND tool_call_id IS NOT NULL", agentId, result.id) > 0)
|
|
2354
|
+
continue;
|
|
1854
2355
|
const turnGroupId = String(this.#require('steps', call.step_id).turn_group_id);
|
|
1855
|
-
|
|
1856
|
-
const ordinal = this.#nextOrdinal('context_items', 'agent_id', agentId);
|
|
1857
|
-
const groupOrdinal = this.#nextOrdinal('context_items', 'turn_group_id', turnGroupId);
|
|
1858
|
-
const artifacts = this.#all('SELECT artifact_id FROM result_artifacts WHERE result_id = ? ORDER BY artifact_id', result.id).map((row) => String(row.artifact_id));
|
|
1859
|
-
const content = json({ content: parseJson(String(result.content_json)), artifacts, truncated: Number(result.truncated) === 1 });
|
|
1860
|
-
this.#database.prepare(`INSERT INTO context_items(id, agent_id, turn_group_id, turn_group_ordinal, kind, source_id, role, content, tool_call_id, tool_name, ordinal, visible, locked, revision, created_at, updated_at) VALUES (?, ?, ?, ?, 'result', ?, 'tool', ?, ?, ?, ?, 1, 0, 0, ?, ?)`)
|
|
1861
|
-
.run(contextId, agentId, turnGroupId, groupOrdinal, String(result.id), content, call.provider_call_id, String(call.capability_id), ordinal, timestamp, timestamp);
|
|
2356
|
+
this.#insertResultContext(agentId, turnGroupId, call, result, timestamp, this.#nextOrdinal('context_items', 'agent_id', agentId), this.#nextOrdinal('context_items', 'turn_group_id', turnGroupId));
|
|
1862
2357
|
yielded.push({ ...doc(result), tool_call_id: call.provider_call_id, tool_name: String(call.capability_id) });
|
|
1863
2358
|
}
|
|
1864
2359
|
this.#touchContext(agentId);
|
|
1865
2360
|
return yielded;
|
|
1866
2361
|
});
|
|
1867
2362
|
}
|
|
2363
|
+
#insertResultContext(agentId, turnGroupId, call, result, timestamp, ordinal, groupOrdinal) {
|
|
2364
|
+
const contextId = id('context');
|
|
2365
|
+
const content = this.#resultContextContent(call, doc(result));
|
|
2366
|
+
this.#database.prepare(`INSERT INTO context_items(id, agent_id, turn_group_id, turn_group_ordinal, kind, source_id, role, content, tool_call_id, tool_name, ordinal, visible, locked, revision, created_at, updated_at) VALUES (?, ?, ?, ?, 'result', ?, 'tool', ?, ?, ?, ?, 1, 0, 0, ?, ?)`)
|
|
2367
|
+
.run(contextId, agentId, turnGroupId, groupOrdinal, String(result.id), content, call.provider_call_id, String(call.capability_id), ordinal, timestamp, timestamp);
|
|
2368
|
+
return contextId;
|
|
2369
|
+
}
|
|
2370
|
+
// Old partial batches may have persisted tool IDs but no Call. Create only an
|
|
2371
|
+
// interrupted fact anchored to the recorded Step; never schedule these tools.
|
|
2372
|
+
#materializeInterruptedCalls(timestamp) {
|
|
2373
|
+
const changes = [];
|
|
2374
|
+
for (const batch of this.#all("SELECT * FROM context_items WHERE kind = 'tool_calls' AND visible = 1 AND locked = 0")) {
|
|
2375
|
+
const metadata = jsonRecord(parseJson(typeof batch.metadata_json === 'string' ? batch.metadata_json : null));
|
|
2376
|
+
if (typeof metadata?.source_step_id !== 'string')
|
|
2377
|
+
continue;
|
|
2378
|
+
const step = this.#database.prepare('SELECT s.*, a.workspace_id, t.project_id FROM steps s JOIN runs r ON r.id = s.run_id JOIN agents a ON a.id = r.agent_id JOIN tasks t ON t.id = a.task_id WHERE s.id = ? AND r.agent_id = ? AND s.turn_group_id = ?').get(metadata.source_step_id, batch.agent_id, batch.turn_group_id);
|
|
2379
|
+
if (step === undefined)
|
|
2380
|
+
continue;
|
|
2381
|
+
for (const [ordinal, tool] of contextToolCalls(String(batch.content)).entries()) {
|
|
2382
|
+
if (this.#count('SELECT COUNT(*) AS count FROM calls WHERE step_id = ? AND provider_call_id = ? AND parent_call_id IS NULL', step.id, tool.id) > 0)
|
|
2383
|
+
continue;
|
|
2384
|
+
const callId = id('call');
|
|
2385
|
+
this.#database.prepare(`INSERT INTO calls(id, project_id, workspace_id, agent_id, step_id, source, capability_id, provider_call_id, call_ordinal, input_json, effect, state, outcome_unknown, revision, created_at, updated_at)
|
|
2386
|
+
VALUES (?, ?, ?, ?, ?, 'llm', ?, ?, ?, '{}', 'full', 'QUEUED', 0, 0, ?, ?)`)
|
|
2387
|
+
.run(callId, step.project_id, step.workspace_id, batch.agent_id, step.id, tool.name, tool.id, ordinal, timestamp, timestamp);
|
|
2388
|
+
changes.push(...this.#interruptCall(this.#require('calls', callId), 'PROCESS_INTERRUPTED', 'Execution was interrupted before a Call was recorded; no automatic retry will occur.', timestamp));
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
return changes;
|
|
2392
|
+
}
|
|
2393
|
+
#recoverResultContext(timestamp, onlyAgentId) {
|
|
2394
|
+
const changes = [];
|
|
2395
|
+
const touched = new Set();
|
|
2396
|
+
const batches = this.#all(`SELECT i.id FROM context_items i WHERE i.kind = 'tool_calls' AND i.visible = 1 AND i.locked = 0
|
|
2397
|
+
AND EXISTS (SELECT 1 FROM calls c JOIN steps s ON s.id = c.step_id JOIN results r ON r.call_id = c.id
|
|
2398
|
+
WHERE c.agent_id = i.agent_id AND s.turn_group_id = i.turn_group_id AND c.source = 'llm' AND c.state = 'TERMINAL'
|
|
2399
|
+
AND NOT EXISTS (SELECT 1 FROM context_items response WHERE response.agent_id = i.agent_id AND response.kind = 'result'
|
|
2400
|
+
AND response.source_id = r.id AND response.tool_call_id IS NOT NULL)) ORDER BY i.agent_id, i.ordinal`);
|
|
2401
|
+
for (const { id: batchId } of batches) {
|
|
2402
|
+
// Earlier insertions can move this batch; always use current ordinals.
|
|
2403
|
+
const batch = this.#require('context_items', String(batchId));
|
|
2404
|
+
const agentId = String(batch.agent_id);
|
|
2405
|
+
if (onlyAgentId !== undefined && agentId !== onlyAgentId)
|
|
2406
|
+
continue;
|
|
2407
|
+
const groupId = String(batch.turn_group_id);
|
|
2408
|
+
const tools = contextToolCalls(String(batch.content));
|
|
2409
|
+
const toolIds = new Set(tools.map((tool) => tool.id));
|
|
2410
|
+
const following = this.#all('SELECT * FROM context_items WHERE agent_id = ? AND ordinal > ? ORDER BY ordinal', agentId, batch.ordinal);
|
|
2411
|
+
const existing = [];
|
|
2412
|
+
for (const item of following) {
|
|
2413
|
+
if (item.kind !== 'result' || item.turn_group_id !== groupId || !toolIds.has(String(item.tool_call_id)))
|
|
2414
|
+
break;
|
|
2415
|
+
existing.push(item);
|
|
2416
|
+
}
|
|
2417
|
+
const present = new Set(existing.map((item) => String(item.tool_call_id)));
|
|
2418
|
+
if (tools.every((tool) => present.has(tool.id)))
|
|
2419
|
+
continue;
|
|
2420
|
+
const metadata = jsonRecord(parseJson(typeof batch.metadata_json === 'string' ? batch.metadata_json : null));
|
|
2421
|
+
let stepId = typeof metadata?.source_step_id === 'string' ? metadata.source_step_id : undefined;
|
|
2422
|
+
if (stepId === undefined && existing.length > 0) {
|
|
2423
|
+
const steps = new Set(existing.flatMap((item) => this.#all('SELECT c.step_id FROM results r JOIN calls c ON c.id = r.call_id WHERE r.id = ? AND c.agent_id = ?', item.source_id, agentId).map((row) => String(row.step_id))));
|
|
2424
|
+
if (steps.size === 1)
|
|
2425
|
+
stepId = [...steps][0];
|
|
2426
|
+
}
|
|
2427
|
+
const last = existing.at(-1) ?? batch;
|
|
2428
|
+
let ordinal = Number(last.ordinal) + 1;
|
|
2429
|
+
let groupOrdinal = Number(last.turn_group_ordinal) + 1;
|
|
2430
|
+
for (const tool of tools) {
|
|
2431
|
+
if (present.has(tool.id))
|
|
2432
|
+
continue;
|
|
2433
|
+
// Legacy batches without an explicit Step or an existing Result anchor must
|
|
2434
|
+
// have an unambiguous batch identity within this TurnGroup.
|
|
2435
|
+
if (stepId === undefined) {
|
|
2436
|
+
const owners = this.#all("SELECT content FROM context_items WHERE agent_id = ? AND turn_group_id = ? AND kind = 'tool_calls'", agentId, groupId)
|
|
2437
|
+
.filter((row) => contextToolCalls(String(row.content)).some((entry) => entry.id === tool.id));
|
|
2438
|
+
if (owners.length !== 1)
|
|
2439
|
+
continue;
|
|
2440
|
+
}
|
|
2441
|
+
const candidates = this.#all(`SELECT c.* FROM calls c JOIN steps s ON s.id = c.step_id
|
|
2442
|
+
WHERE c.agent_id = ? AND s.turn_group_id = ? AND c.source = 'llm' AND c.parent_call_id IS NULL
|
|
2443
|
+
AND c.provider_call_id = ? AND c.capability_id = ? ${stepId === undefined ? '' : 'AND c.step_id = ?'}`, agentId, groupId, tool.id, tool.name, ...(stepId === undefined ? [] : [stepId]));
|
|
2444
|
+
if (candidates.length !== 1)
|
|
2445
|
+
continue;
|
|
2446
|
+
const call = candidates[0];
|
|
2447
|
+
if (call.state !== 'TERMINAL')
|
|
2448
|
+
continue;
|
|
2449
|
+
const result = this.#database.prepare('SELECT * FROM results WHERE call_id = ?').get(call.id);
|
|
2450
|
+
if (result === undefined)
|
|
2451
|
+
continue;
|
|
2452
|
+
// A copied/hidden/already projected response must never be resurrected or duplicated.
|
|
2453
|
+
if (this.#count("SELECT COUNT(*) AS count FROM context_items WHERE agent_id = ? AND kind = 'result' AND source_id = ? AND tool_call_id IS NOT NULL", agentId, result.id) > 0)
|
|
2454
|
+
continue;
|
|
2455
|
+
const shift = this.#database.prepare('UPDATE context_items SET ordinal = ordinal + 1 WHERE id = ?');
|
|
2456
|
+
for (const item of this.#all('SELECT id FROM context_items WHERE agent_id = ? AND ordinal >= ? ORDER BY ordinal DESC', agentId, ordinal))
|
|
2457
|
+
shift.run(item.id);
|
|
2458
|
+
const shiftGroup = this.#database.prepare('UPDATE context_items SET turn_group_ordinal = turn_group_ordinal + 1 WHERE id = ?');
|
|
2459
|
+
for (const item of this.#all('SELECT id FROM context_items WHERE turn_group_id = ? AND turn_group_ordinal >= ? ORDER BY turn_group_ordinal DESC', groupId, groupOrdinal))
|
|
2460
|
+
shiftGroup.run(item.id);
|
|
2461
|
+
const contextId = this.#insertResultContext(agentId, groupId, call, result, timestamp, ordinal++, groupOrdinal++);
|
|
2462
|
+
changes.push({ type: 'context_item', id: contextId });
|
|
2463
|
+
touched.add(agentId);
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
for (const agentId of touched) {
|
|
2467
|
+
this.#touchContext(agentId);
|
|
2468
|
+
changes.push({ type: 'agent', id: agentId });
|
|
2469
|
+
}
|
|
2470
|
+
return changes;
|
|
2471
|
+
}
|
|
2472
|
+
#resultContextContent(call, result) {
|
|
2473
|
+
const artifacts = this.#all('SELECT artifact_id FROM result_artifacts WHERE result_id = ? ORDER BY artifact_id', result.id).map((row) => String(row.artifact_id));
|
|
2474
|
+
const view = { content: result.content ?? null, artifacts, truncated: result.truncated === true };
|
|
2475
|
+
// PTC roots retain their existing protocol result; nested program results are not auto-yielded.
|
|
2476
|
+
if (call.source === 'program' || call.capability_id === 'programmatic_tool_run')
|
|
2477
|
+
return json(view);
|
|
2478
|
+
const envelope = {
|
|
2479
|
+
result_id: stringField(result.id),
|
|
2480
|
+
call_id: String(call.id),
|
|
2481
|
+
outcome: stringField(result.outcome),
|
|
2482
|
+
outcome_unknown: Number(call.outcome_unknown) === 1,
|
|
2483
|
+
error_code: result.error_code === null ? null : stringField(result.error_code),
|
|
2484
|
+
error_message: result.error_message === null ? null : stringField(result.error_message),
|
|
2485
|
+
...view,
|
|
2486
|
+
payload: { capture: result.capture === 'complete' || result.capture === 'partial' ? result.capture : 'unknown', stored: result.stored === 'full' ? 'full' : 'unknown', view: result.truncated === true ? 'preview' : 'full' }
|
|
2487
|
+
};
|
|
2488
|
+
return json(envelope);
|
|
2489
|
+
}
|
|
1868
2490
|
cancelAgent(projectId, agentId) {
|
|
1869
|
-
|
|
2491
|
+
const changes = [...refs(['agent', agentId]), ...this.#all("SELECT id FROM runs WHERE agent_id = ? AND state != 'TERMINAL'", agentId).map(row => ({ type: 'run', id: String(row.id) }))];
|
|
2492
|
+
return this.#write(projectId, changes, () => {
|
|
1870
2493
|
this.getAgent(projectId, agentId);
|
|
1871
2494
|
const timestamp = now();
|
|
1872
2495
|
this.#database.prepare(`UPDATE model_attempts SET state = 'TERMINAL', outcome = 'cancelled', error_code = 'USER_CANCELLED', updated_at = ? WHERE agent_id = ? AND state != 'TERMINAL'`).run(timestamp, agentId);
|
|
1873
|
-
|
|
2496
|
+
for (const call of this.#all("SELECT * FROM calls WHERE agent_id = ? AND state != 'TERMINAL'", agentId)) {
|
|
2497
|
+
changes.push(...this.#interruptCall(call, 'CALL_CANCELLED', 'The Agent was stopped; this Call will not be resumed.', timestamp));
|
|
2498
|
+
}
|
|
2499
|
+
changes.push(...this.#recoverResultContext(timestamp, agentId));
|
|
1874
2500
|
this.#database.prepare(`UPDATE steps SET state = 'TERMINAL', outcome = 'cancelled', error_code = 'USER_CANCELLED', updated_at = ? WHERE run_id IN (SELECT id FROM runs WHERE agent_id = ?) AND state != 'TERMINAL'`).run(timestamp, agentId);
|
|
1875
2501
|
this.#database.prepare(`UPDATE runs SET state = 'TERMINAL', outcome = 'cancelled', error_code = 'USER_CANCELLED', revision = revision + 1, updated_at = ? WHERE agent_id = ? AND state != 'TERMINAL'`).run(timestamp, agentId);
|
|
1876
2502
|
this.#database.prepare(`UPDATE agents SET state = 'IDLE', revision = revision + 1, updated_at = ? WHERE id = ?`).run(timestamp, agentId);
|
|
@@ -1907,17 +2533,29 @@ export class Store {
|
|
|
1907
2533
|
'instance', 'projects', 'workspaces', 'workspace_paths', 'tasks', 'sessions', 'agents', 'messages',
|
|
1908
2534
|
'pending_messages', 'runs', 'steps', 'model_attempts', 'attempt_capabilities', 'attempt_tool_ordinals',
|
|
1909
2535
|
'call_partitions', 'calls', 'results', 'artifacts', 'result_artifacts', 'context_turn_groups',
|
|
1910
|
-
'context_items', 'context_overrides', 'projection_decisions', 'capability_assignments', 'capability_loads',
|
|
1911
|
-
'agent_plans', 'ingress_requests', 'side_effect_ledgers'
|
|
2536
|
+
'context_items', 'context_overrides', 'working_contexts', 'working_context_items', 'context_compactions', 'projection_decisions', 'capability_assignments', 'capability_loads',
|
|
2537
|
+
'agent_plans', 'todo_reminder_state', 'ingress_requests', 'side_effect_ledgers', 'purge_cleanups'
|
|
1912
2538
|
];
|
|
1913
|
-
return Object.fromEntries(tables.map((table) => [table, this.#all(`SELECT * FROM ${table}`).map(rawJson)]));
|
|
2539
|
+
return Object.fromEntries(tables.map((table) => [table, this.#all(`SELECT ${table === 'context_items' ? 'rowid, *' : '*'} FROM ${table} ORDER BY rowid`).map(rawJson)]));
|
|
1914
2540
|
}
|
|
1915
|
-
purgeArchived(projectId, before) {
|
|
2541
|
+
purgeArchived(projectId, before, executorStateId) {
|
|
1916
2542
|
return this.#write(projectId, refs(['maintenance', projectId]), () => {
|
|
1917
2543
|
const tasks = this.#all('SELECT id FROM tasks WHERE project_id = ? AND archived_at IS NOT NULL AND archived_at < ?', projectId, before).map((row) => String(row.id));
|
|
1918
|
-
return { purged_tasks: tasks.length, ...this.#purgeTaskRows(tasks) };
|
|
2544
|
+
return { purged_tasks: tasks.length, ...this.#purgeTaskRows(projectId, tasks, executorStateId) };
|
|
1919
2545
|
});
|
|
1920
2546
|
}
|
|
2547
|
+
pendingPurges(projectId) {
|
|
2548
|
+
this.getProject(projectId);
|
|
2549
|
+
return this.#all('SELECT * FROM purge_cleanups WHERE project_id = ? ORDER BY created_at, id', projectId).map(doc);
|
|
2550
|
+
}
|
|
2551
|
+
completePurge(projectId, cleanupId) {
|
|
2552
|
+
this.#write(projectId, refs(['maintenance', projectId]), () => {
|
|
2553
|
+
this.#database.prepare('DELETE FROM purge_cleanups WHERE project_id = ? AND id = ?').run(projectId, cleanupId);
|
|
2554
|
+
});
|
|
2555
|
+
}
|
|
2556
|
+
artifactPathReferenced(relativePath) {
|
|
2557
|
+
return this.#count('SELECT COUNT(*) AS count FROM artifacts WHERE relative_path = ?', relativePath) > 0;
|
|
2558
|
+
}
|
|
1921
2559
|
snapshot(projectId, maxEntities) {
|
|
1922
2560
|
const project = this.getProject(projectId);
|
|
1923
2561
|
const workspaces = this.listWorkspaces(projectId);
|
|
@@ -1948,7 +2586,47 @@ export class Store {
|
|
|
1948
2586
|
this.#database.prepare('INSERT INTO ingress_requests(client_id, key, operation, request_digest, response_json, created_at) VALUES (?, ?, ?, ?, ?, ?)')
|
|
1949
2587
|
.run(input.clientId, input.key, input.operation, input.digest, json(response), now());
|
|
1950
2588
|
}
|
|
2589
|
+
#observationStates(changes) {
|
|
2590
|
+
const values = new Map();
|
|
2591
|
+
for (const entry of changes) {
|
|
2592
|
+
if (!['call', 'run', 'agent'].includes(entry.type))
|
|
2593
|
+
continue;
|
|
2594
|
+
const row = this.#database.prepare(`SELECT state, ${entry.type === 'agent' ? 'NULL' : 'outcome'} AS outcome FROM ${entry.type}s WHERE id = ?`).get(entry.id);
|
|
2595
|
+
if (row !== undefined)
|
|
2596
|
+
values.set(`${entry.type}:${entry.id}`, `${String(row.state)}:${String(row.outcome)}`);
|
|
2597
|
+
}
|
|
2598
|
+
return values;
|
|
2599
|
+
}
|
|
2600
|
+
#observe(changes, previous = new Map()) {
|
|
2601
|
+
if (this.observability === undefined)
|
|
2602
|
+
return;
|
|
2603
|
+
const seen = new Set();
|
|
2604
|
+
for (const entry of changes) {
|
|
2605
|
+
const key = `${entry.type}:${entry.id}`;
|
|
2606
|
+
if (seen.has(key) || !['call', 'run', 'agent'].includes(entry.type))
|
|
2607
|
+
continue;
|
|
2608
|
+
seen.add(key);
|
|
2609
|
+
const row = this.#database.prepare(`SELECT * FROM ${entry.type}s WHERE id = ?`).get(entry.id);
|
|
2610
|
+
if (row === undefined || previous.get(key) === `${String(row.state)}:${typeof row.outcome === 'string' ? row.outcome : 'null'}`)
|
|
2611
|
+
continue;
|
|
2612
|
+
const result = entry.type === 'call' ? this.#database.prepare('SELECT error_code FROM results WHERE call_id = ?').get(entry.id) : undefined;
|
|
2613
|
+
this.observability.record(entry.type === 'call' ? 'call' : 'agent', entry.type, String(row.outcome ?? row.state).toLowerCase(), 0, {
|
|
2614
|
+
[`${entry.type}_id`]: entry.id,
|
|
2615
|
+
project_id: typeof row.project_id === 'string' ? row.project_id : null,
|
|
2616
|
+
agent_id: entry.type === 'agent' ? entry.id : typeof row.agent_id === 'string' ? row.agent_id : null,
|
|
2617
|
+
state: String(row.state), outcome: typeof row.outcome === 'string' ? row.outcome : null,
|
|
2618
|
+
error_code: typeof result?.error_code === 'string' ? result.error_code : typeof row.error_code === 'string' ? row.error_code : null,
|
|
2619
|
+
outcome_unknown: row.outcome_unknown === 1
|
|
2620
|
+
});
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
1951
2623
|
#write(projectId, changes, command) {
|
|
2624
|
+
if (this.#batchChanges !== undefined) {
|
|
2625
|
+
this.#batchChanges.push(...changes);
|
|
2626
|
+
return command();
|
|
2627
|
+
}
|
|
2628
|
+
const previous = this.observability === undefined ? new Map() : this.#observationStates(changes);
|
|
2629
|
+
const before = new Map(changes.map((entry) => [`${entry.type}:${entry.id}`, this.#describeChange(entry)]));
|
|
1952
2630
|
const transaction = this.#database.transaction(() => {
|
|
1953
2631
|
const value = command();
|
|
1954
2632
|
this.#database.prepare('UPDATE instance SET state_revision = state_revision + 1 WHERE id = 1').run();
|
|
@@ -1961,9 +2639,14 @@ export class Store {
|
|
|
1961
2639
|
const change = {
|
|
1962
2640
|
projectId,
|
|
1963
2641
|
stateRevision: result.stateRevision,
|
|
1964
|
-
changes: changes.map((entry) =>
|
|
2642
|
+
changes: changes.map((entry) => {
|
|
2643
|
+
const current = this.#describeChange(entry);
|
|
2644
|
+
const prior = before.get(`${entry.type}:${entry.id}`);
|
|
2645
|
+
return current.change === 'deleted' && prior !== undefined ? { ...prior, change: 'deleted', revision: null } : current;
|
|
2646
|
+
})
|
|
1965
2647
|
};
|
|
1966
2648
|
this.#events.publish(change);
|
|
2649
|
+
this.#observe(changes, previous);
|
|
1967
2650
|
return result.value;
|
|
1968
2651
|
}
|
|
1969
2652
|
/**
|
|
@@ -1978,7 +2661,7 @@ export class Store {
|
|
|
1978
2661
|
const owner = OWNERSHIP[entry.type];
|
|
1979
2662
|
const row = TABLE_BY_ENTITY[entry.type] === undefined
|
|
1980
2663
|
? undefined
|
|
1981
|
-
: this.#all(`SELECT * FROM ${TABLE_BY_ENTITY[entry.type] ?? ''} WHERE id = ?`, entry.id)[0];
|
|
2664
|
+
: this.#all(`SELECT * FROM ${TABLE_BY_ENTITY[entry.type] ?? ''} WHERE ${entry.type === 'agent_plan' ? 'agent_id' : 'id'} = ?`, entry.id)[0];
|
|
1982
2665
|
const ownerId = owner === undefined || row === undefined ? null : row[owner.column];
|
|
1983
2666
|
return {
|
|
1984
2667
|
type: entry.type,
|
|
@@ -1986,7 +2669,7 @@ export class Store {
|
|
|
1986
2669
|
entity_type: entry.type,
|
|
1987
2670
|
entity_id: entry.id,
|
|
1988
2671
|
owner_id: typeof ownerId === 'string' ? ownerId : null,
|
|
1989
|
-
read_route: readRouteFor(entry.type, entry.id),
|
|
2672
|
+
read_route: readRouteFor(entry.type, entry.id, row),
|
|
1990
2673
|
revision: typeof row?.revision === 'number' ? row.revision : null,
|
|
1991
2674
|
change: row === undefined && TABLE_BY_ENTITY[entry.type] !== undefined ? 'deleted' : 'updated',
|
|
1992
2675
|
...this.#changeScope(entry.type, row)
|
|
@@ -2068,35 +2751,65 @@ export class Store {
|
|
|
2068
2751
|
if (input.loadedCapabilities.includes(capabilityId))
|
|
2069
2752
|
load.run(agentId, capabilityId, timestamp);
|
|
2070
2753
|
}
|
|
2754
|
+
this.#syncWorkingContext(agentId);
|
|
2071
2755
|
return this.getAgent(projectId, agentId);
|
|
2072
2756
|
}
|
|
2073
|
-
#copyContextRange(sourceAgentId, targetAgentId, throughOrdinal, timestamp) {
|
|
2074
|
-
|
|
2075
|
-
const
|
|
2076
|
-
|
|
2757
|
+
#copyContextRange(sourceAgentId, targetAgentId, throughOrdinal, timestamp, selectedIds, working = false) {
|
|
2758
|
+
// Copy archived Full Context too, so checkpoint provenance survives independently of the parent.
|
|
2759
|
+
const selected = selectedIds === undefined ? undefined : new Set(selectedIds);
|
|
2760
|
+
let source = (working
|
|
2761
|
+
? this.#all(`SELECT c.*, w.content AS content, w.snipped AS snipped FROM working_context_items w
|
|
2762
|
+
JOIN context_items c ON c.id = w.context_item_id WHERE w.agent_id = ? AND c.ordinal <= ? AND c.visible = 1 ORDER BY c.ordinal`, sourceAgentId, throughOrdinal)
|
|
2763
|
+
: this.#all('SELECT * FROM context_items WHERE agent_id = ? AND ordinal <= ? AND visible = 1 ORDER BY ordinal', sourceAgentId, throughOrdinal))
|
|
2764
|
+
.filter((row) => selected === undefined || selected.has(String(row.id)));
|
|
2077
2765
|
if (source.length === 0 || Number(source.at(-1)?.ordinal) !== throughOrdinal)
|
|
2078
2766
|
fail('CONTEXT_GROUP_INVALID', 409, 'The Context fork boundary does not exist.');
|
|
2079
2767
|
const lastGroupId = String(source.at(-1)?.turn_group_id);
|
|
2080
|
-
if (this.#count('SELECT COUNT(*) AS count FROM context_items WHERE turn_group_id = ? AND ordinal > ?', lastGroupId, throughOrdinal) !== 0)
|
|
2768
|
+
if (!working && this.#count('SELECT COUNT(*) AS count FROM context_items WHERE turn_group_id = ? AND ordinal > ?', lastGroupId, throughOrdinal) !== 0)
|
|
2081
2769
|
fail('CONTEXT_GROUP_INVALID', 409, 'Context Fork must end at a complete TurnGroup boundary.');
|
|
2770
|
+
const active = activeContextItems(source.map(doc));
|
|
2771
|
+
const boundaries = assertContextBoundaries(boundaryMessages(active), { allowOpenTail: working, unsettledItemIds: this.#unsettledContextItems(sourceAgentId) });
|
|
2772
|
+
if (working) {
|
|
2773
|
+
// The child inherits observations, never the parent's live execution obligations.
|
|
2774
|
+
const unfinished = new Set(boundaries.protectedRanges.filter((range) => !range.complete)
|
|
2775
|
+
.flatMap((range) => active.slice(range.start, range.end).map((row) => stringField(row.id))));
|
|
2776
|
+
const overrides = new Map(this.#all('SELECT target_kind, target_id, instructions FROM context_overrides WHERE agent_id = ?', sourceAgentId)
|
|
2777
|
+
.map((row) => [`${String(row.target_kind)}:${String(row.target_id)}`, String(row.instructions)]));
|
|
2778
|
+
source = source.map((row) => {
|
|
2779
|
+
const override = overrides.get(`${String(row.kind)}:${String(row.source_id)}`);
|
|
2780
|
+
const content = override === undefined ? row.content : `${override}\n\n${String(row.content)}`;
|
|
2781
|
+
if (!unfinished.has(String(row.id)))
|
|
2782
|
+
return { ...row, content };
|
|
2783
|
+
if (row.kind === 'tool_calls') {
|
|
2784
|
+
const metadata = { ...jsonRecord(parseJson(typeof row.metadata_json === 'string' ? row.metadata_json : null)) };
|
|
2785
|
+
delete metadata.model_response;
|
|
2786
|
+
return { ...row, kind: 'message', role: 'assistant', content: `Unfinished parent tool calls at fork time (historical evidence, not requests to execute):\n${String(row.content)}`, tool_call_id: null, tool_name: null, metadata_json: json(metadata) };
|
|
2787
|
+
}
|
|
2788
|
+
return { ...row, content, tool_call_id: null, tool_name: null };
|
|
2789
|
+
});
|
|
2790
|
+
const instructions = overrides.get(`agent:${sourceAgentId}`);
|
|
2791
|
+
if (instructions !== undefined)
|
|
2792
|
+
this.#insertStandaloneContextMessage(targetAgentId, 'system', instructions, 'message', timestamp);
|
|
2793
|
+
}
|
|
2082
2794
|
const groupIds = new Map();
|
|
2083
2795
|
for (const sourceGroupId of new Set(source.map((row) => String(row.turn_group_id)))) {
|
|
2084
2796
|
const group = this.#require('context_turn_groups', sourceGroupId);
|
|
2085
|
-
if (group.state !== 'COMPLETE')
|
|
2797
|
+
if (!working && group.state !== 'COMPLETE')
|
|
2086
2798
|
fail('CONTEXT_GROUP_INVALID', 409, 'Only complete TurnGroups can be forked.');
|
|
2087
2799
|
const targetGroupId = id('turn');
|
|
2088
2800
|
groupIds.set(sourceGroupId, targetGroupId);
|
|
2089
2801
|
this.#insertTurnGroup(targetGroupId, targetAgentId, String(group.kind), 'COMPLETE', timestamp);
|
|
2090
2802
|
}
|
|
2091
|
-
const insert = this.#database.prepare(`INSERT INTO context_items(id, agent_id, turn_group_id, turn_group_ordinal, kind, source_id, role, content, tool_call_id, tool_name, ordinal, visible, locked, revision, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
2803
|
+
const insert = this.#database.prepare(`INSERT INTO context_items(id, agent_id, turn_group_id, turn_group_ordinal, kind, source_id, role, content, tool_call_id, tool_name, ordinal, visible, locked, revision, created_at, updated_at, metadata_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`);
|
|
2804
|
+
const contextIds = new Map(source.map((row) => [String(row.id), id('context')]));
|
|
2092
2805
|
let ordinal = this.#nextOrdinal('context_items', 'agent_id', targetAgentId);
|
|
2093
2806
|
for (const row of source) {
|
|
2094
|
-
const contextId = id
|
|
2807
|
+
const contextId = contextIds.get(String(row.id));
|
|
2095
2808
|
let sourceId = String(row.source_id);
|
|
2096
2809
|
if (row.kind === 'message') {
|
|
2097
2810
|
const messageId = id('message');
|
|
2098
|
-
this.#database.prepare(`INSERT INTO messages(id, agent_id, source_agent_id, role, content, metadata_json, created_at) VALUES (?, ?, ?, ?, ?,
|
|
2099
|
-
.run(messageId, targetAgentId, sourceAgentId, row.role, row.content, timestamp);
|
|
2811
|
+
this.#database.prepare(`INSERT INTO messages(id, agent_id, source_agent_id, role, content, metadata_json, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
|
2812
|
+
.run(messageId, targetAgentId, sourceAgentId, row.role, row.content, json({ ingress_origin: 'agent', context_copy: true }), timestamp);
|
|
2100
2813
|
sourceId = messageId;
|
|
2101
2814
|
}
|
|
2102
2815
|
else if (row.kind !== 'result')
|
|
@@ -2104,27 +2817,43 @@ export class Store {
|
|
|
2104
2817
|
const targetGroupId = groupIds.get(String(row.turn_group_id));
|
|
2105
2818
|
if (targetGroupId === undefined)
|
|
2106
2819
|
fail('DATABASE_CORRUPT', 503, 'A Context TurnGroup mapping is missing.');
|
|
2107
|
-
insert.run(contextId, targetAgentId, targetGroupId, row.turn_group_ordinal, row.kind, sourceId, row.role, row.content, row.tool_call_id, row.tool_name, ordinal, row.visible, timestamp, timestamp);
|
|
2820
|
+
insert.run(contextId, targetAgentId, targetGroupId, row.turn_group_ordinal, row.kind, sourceId, row.role, row.content, row.tool_call_id, row.tool_name, ordinal, row.visible, row.locked, timestamp, timestamp, row.metadata_json);
|
|
2821
|
+
const coverage = this.#database.prepare('SELECT * FROM context_compactions WHERE context_item_id = ?').get(row.id);
|
|
2822
|
+
if (coverage !== undefined) {
|
|
2823
|
+
const coveredIds = strings(parseJson(String(coverage.covered_item_ids_json))).map((itemId) => contextIds.get(itemId) ?? itemId);
|
|
2824
|
+
this.#database.prepare('INSERT INTO context_compactions(context_item_id, source_revision, covered_item_ids_json, focus) VALUES (?, ?, ?, ?)')
|
|
2825
|
+
.run(contextId, 0, json(coveredIds), coverage.focus);
|
|
2826
|
+
}
|
|
2108
2827
|
ordinal += 1;
|
|
2109
2828
|
}
|
|
2829
|
+
this.#assertContext(targetAgentId, false);
|
|
2830
|
+
if (working) {
|
|
2831
|
+
this.#syncWorkingContext(targetAgentId);
|
|
2832
|
+
for (const row of source) {
|
|
2833
|
+
if (Number(row.snipped) === 1)
|
|
2834
|
+
this.#database.prepare('UPDATE working_context_items SET snipped = 1 WHERE context_item_id = ?').run(contextIds.get(String(row.id)));
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2110
2837
|
this.#touchContext(targetAgentId);
|
|
2111
2838
|
return this.#all('SELECT * FROM context_items WHERE agent_id = ? ORDER BY ordinal', targetAgentId).map(doc);
|
|
2112
2839
|
}
|
|
2113
|
-
#insertStandaloneContextMessage(agentId, role, content, kind, timestamp) {
|
|
2840
|
+
#insertStandaloneContextMessage(agentId, role, content, kind, timestamp, metadata = null) {
|
|
2114
2841
|
const messageId = id('message');
|
|
2115
2842
|
const turnGroupId = id('turn');
|
|
2116
2843
|
const contextId = id('context');
|
|
2117
|
-
this.#database.prepare(`INSERT INTO messages(id, agent_id, source_agent_id, role, content, metadata_json, created_at) VALUES (?, ?, NULL, ?, ?,
|
|
2118
|
-
.run(messageId, agentId, role, content, timestamp);
|
|
2844
|
+
this.#database.prepare(`INSERT INTO messages(id, agent_id, source_agent_id, role, content, metadata_json, created_at) VALUES (?, ?, NULL, ?, ?, ?, ?)`)
|
|
2845
|
+
.run(messageId, agentId, role, content, metadata === null ? null : json(metadata), timestamp);
|
|
2119
2846
|
this.#insertTurnGroup(turnGroupId, agentId, kind === 'compaction' ? 'compaction' : 'turn', 'COMPLETE', timestamp);
|
|
2120
2847
|
this.#database.prepare(`INSERT INTO context_items(id, agent_id, turn_group_id, turn_group_ordinal, kind, source_id, role, content, ordinal, visible, locked, revision, created_at, updated_at) VALUES (?, ?, ?, 0, ?, ?, ?, ?, ?, 1, 0, 0, ?, ?)`)
|
|
2121
2848
|
.run(contextId, agentId, turnGroupId, kind, messageId, role, content, this.#nextOrdinal('context_items', 'agent_id', agentId), timestamp, timestamp);
|
|
2849
|
+
if (metadata !== null)
|
|
2850
|
+
this.#database.prepare('UPDATE context_items SET metadata_json = ? WHERE id = ?').run(json(metadata), contextId);
|
|
2122
2851
|
this.#touchContext(agentId);
|
|
2123
2852
|
}
|
|
2124
2853
|
// Returns what the caller still has to clean up outside the transaction: Artifact files on disk,
|
|
2125
2854
|
// and the Calls whose file history the Executor is still holding. Neither can happen in here --
|
|
2126
2855
|
// Store transactions do no file or network I/O.
|
|
2127
|
-
#purgeTaskRows(taskIds) {
|
|
2856
|
+
#purgeTaskRows(projectId, taskIds, executorStateId) {
|
|
2128
2857
|
for (const taskId of taskIds) {
|
|
2129
2858
|
const references = this.#count(`SELECT COUNT(*) AS count FROM context_items ci JOIN agents target ON target.id = ci.agent_id WHERE ci.kind = 'result' AND target.task_id != ? AND ci.source_id IN (SELECT r.id FROM results r JOIN calls c ON c.id = r.call_id WHERE c.agent_id IN (SELECT id FROM agents WHERE task_id = ?))`, taskId, taskId);
|
|
2130
2859
|
if (references > 0)
|
|
@@ -2133,6 +2862,8 @@ export class Store {
|
|
|
2133
2862
|
const placeholders = taskIds.map(() => '?').join(',') || "''";
|
|
2134
2863
|
const purgedCalls = this.#all(`SELECT id, workspace_id FROM calls WHERE agent_id IN (SELECT id FROM agents WHERE task_id IN (${placeholders}))`, ...taskIds)
|
|
2135
2864
|
.map((row) => ({ call_id: String(row.id), workspace_id: String(row.workspace_id) }));
|
|
2865
|
+
if (purgedCalls.length > 0 && !executorStateId)
|
|
2866
|
+
fail('EXECUTOR_IDENTITY_REQUIRED', 503, 'Executor state identity is required before purge.');
|
|
2136
2867
|
const candidateArtifacts = this.#all(`
|
|
2137
2868
|
SELECT DISTINCT a.id, a.relative_path FROM artifacts a WHERE a.id IN (
|
|
2138
2869
|
SELECT ra.artifact_id FROM result_artifacts ra JOIN results r ON r.id = ra.result_id JOIN calls c ON c.id = r.call_id JOIN agents owner ON owner.id = c.agent_id WHERE owner.task_id IN (${placeholders})
|
|
@@ -2154,7 +2885,12 @@ export class Store {
|
|
|
2154
2885
|
if (this.#count('SELECT COUNT(*) AS count FROM artifacts WHERE relative_path = ?', relativePath) === 0)
|
|
2155
2886
|
orphanedPaths.add(relativePath);
|
|
2156
2887
|
}
|
|
2157
|
-
|
|
2888
|
+
const payload = { orphaned_artifact_paths: [...orphanedPaths], purged_calls: purgedCalls };
|
|
2889
|
+
if (orphanedPaths.size > 0 || purgedCalls.length > 0) {
|
|
2890
|
+
this.#database.prepare('INSERT INTO purge_cleanups(id, project_id, executor_state_id, payload_json, created_at) VALUES (?, ?, ?, ?, ?)')
|
|
2891
|
+
.run(id('purge'), projectId, executorStateId, json(payload), now());
|
|
2892
|
+
}
|
|
2893
|
+
return payload;
|
|
2158
2894
|
}
|
|
2159
2895
|
#insertContextMessage(agentId, role, content, ids, timestamp, groupKind = 'included') {
|
|
2160
2896
|
this.#database.prepare(`INSERT INTO messages(id, agent_id, source_agent_id, role, content, metadata_json, created_at) VALUES (?, ?, NULL, ?, ?, NULL, ?)`)
|
|
@@ -2171,24 +2907,25 @@ export class Store {
|
|
|
2171
2907
|
if (sourceAgentId !== undefined) {
|
|
2172
2908
|
const source = this.#require('agents', sourceAgentId);
|
|
2173
2909
|
this.#ownedTask(String(source.task_id), projectId);
|
|
2174
|
-
if (source.
|
|
2175
|
-
fail('AGENT_NOT_FOUND', 404, 'Cross-Agent Messages must remain inside one
|
|
2910
|
+
if (source.session_id !== agent.session_id)
|
|
2911
|
+
fail('AGENT_NOT_FOUND', 404, 'Cross-Agent Messages must remain inside one Session.');
|
|
2176
2912
|
}
|
|
2177
2913
|
const pendingOrdinal = this.#nextOrdinal('pending_messages', 'agent_id', agentId);
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
.run(ids.messageId, agentId, sourceAgentId ?? null, content, timestamp);
|
|
2914
|
+
this.#database.prepare(`INSERT INTO messages(id, agent_id, source_agent_id, role, content, metadata_json, created_at) VALUES (?, ?, ?, 'user', ?, ?, ?)`)
|
|
2915
|
+
.run(ids.messageId, agentId, sourceAgentId ?? null, content, json({ ingress_origin: sourceAgentId === undefined ? 'user' : 'agent' }), timestamp);
|
|
2181
2916
|
this.#database.prepare(`INSERT INTO pending_messages(id, agent_id, message_id, source_agent_id, content, ordinal, revision, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)`)
|
|
2182
2917
|
.run(ids.pendingId, agentId, ids.messageId, sourceAgentId ?? null, content, pendingOrdinal, timestamp, timestamp);
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
.
|
|
2918
|
+
if (agent.state === 'IDLE' || agent.state === 'STOPPED') {
|
|
2919
|
+
this.#insertTurnGroup(ids.turnGroupId, agentId, 'turn', 'OPEN', timestamp);
|
|
2920
|
+
this.#database.prepare(`INSERT INTO context_items(id, agent_id, turn_group_id, turn_group_ordinal, kind, source_id, role, content, ordinal, visible, locked, revision, created_at, updated_at) VALUES (?, ?, ?, 0, 'message', ?, 'user', ?, ?, 1, 0, 0, ?, ?)`)
|
|
2921
|
+
.run(ids.contextId, agentId, ids.turnGroupId, ids.messageId, content, this.#nextOrdinal('context_items', 'agent_id', agentId), timestamp, timestamp);
|
|
2922
|
+
this.#touchContext(agentId);
|
|
2923
|
+
}
|
|
2186
2924
|
const active = this.#database.prepare(`SELECT * FROM runs WHERE id = ? AND agent_id = ? AND state != 'TERMINAL'`).get(ids.runId, agentId);
|
|
2187
2925
|
if (active === undefined) {
|
|
2188
2926
|
this.#database.prepare(`INSERT INTO runs(id, agent_id, goal_message_ids_json, state, outcome, error_code, revision, created_at, updated_at) VALUES (?, ?, '[]', 'READY', NULL, NULL, 0, ?, ?)`)
|
|
2189
2927
|
.run(ids.runId, agentId, timestamp, timestamp);
|
|
2190
2928
|
}
|
|
2191
|
-
this.#touchContext(agentId);
|
|
2192
2929
|
return {
|
|
2193
2930
|
message: this.getMessage(projectId, ids.messageId),
|
|
2194
2931
|
pending: doc(this.#require('pending_messages', ids.pendingId)),
|
|
@@ -2201,7 +2938,12 @@ export class Store {
|
|
|
2201
2938
|
}
|
|
2202
2939
|
#workspace(row) {
|
|
2203
2940
|
const paths = this.#all('SELECT path FROM workspace_paths WHERE workspace_id = ? ORDER BY ordinal', String(row.id)).map((entry) => String(entry.path));
|
|
2204
|
-
|
|
2941
|
+
const value = { ...doc(row) };
|
|
2942
|
+
delete value.executor_ref;
|
|
2943
|
+
return { ...value, executor_id: typeof row.executor_ref === 'string' ? row.executor_ref : String(row.executor_id), paths };
|
|
2944
|
+
}
|
|
2945
|
+
workspaceOwner(workspaceId) {
|
|
2946
|
+
return { projectId: String(this.#require('workspaces', workspaceId).project_id) };
|
|
2205
2947
|
}
|
|
2206
2948
|
/**
|
|
2207
2949
|
* 02A `AgentSummary`. `is_subagent` and `subagent_list` are derived rather than stored, per the
|
|
@@ -2232,9 +2974,49 @@ export class Store {
|
|
|
2232
2974
|
this.#database.prepare(`INSERT INTO context_turn_groups(id, agent_id, ordinal, kind, state, revision, created_at, completed_at) VALUES (?, ?, ?, ?, ?, 0, ?, ?)`)
|
|
2233
2975
|
.run(idValue, agentId, ordinal, kind, state, timestamp, state === 'OPEN' ? null : timestamp);
|
|
2234
2976
|
}
|
|
2977
|
+
#unsettledContextItems(agentId) {
|
|
2978
|
+
// Follow Result -> Call provenance, including copied results in forked histories.
|
|
2979
|
+
return this.#all(`SELECT i.id FROM context_items i LEFT JOIN results r ON r.id = i.source_id
|
|
2980
|
+
LEFT JOIN calls c ON c.id = r.call_id WHERE i.agent_id = ? AND i.kind = 'result'
|
|
2981
|
+
AND i.tool_call_id IS NOT NULL AND (c.id IS NULL OR c.state != 'TERMINAL'
|
|
2982
|
+
OR c.provider_call_id IS NULL OR c.provider_call_id != i.tool_call_id OR c.capability_id != i.tool_name)`, agentId).map((row) => String(row.id));
|
|
2983
|
+
}
|
|
2984
|
+
#assertContext(agentId, allowOpenTail = true) {
|
|
2985
|
+
const items = activeContextItems(this.#all('SELECT * FROM context_items WHERE agent_id = ? ORDER BY ordinal, id', agentId).map(doc));
|
|
2986
|
+
const messages = boundaryMessages(items);
|
|
2987
|
+
assertContextBoundaries(messages, { allowOpenTail, unsettledItemIds: this.#unsettledContextItems(agentId) });
|
|
2988
|
+
return messages;
|
|
2989
|
+
}
|
|
2235
2990
|
#touchContext(agentId) {
|
|
2236
2991
|
this.#database.prepare('UPDATE agents SET context_revision = context_revision + 1, revision = revision + 1, updated_at = ? WHERE id = ?')
|
|
2237
2992
|
.run(now(), agentId);
|
|
2993
|
+
this.#syncWorkingContext(agentId);
|
|
2994
|
+
}
|
|
2995
|
+
#workingItems(agentId) {
|
|
2996
|
+
return this.#all(`SELECT c.*, w.content AS content, w.snipped AS snipped FROM working_context_items w
|
|
2997
|
+
JOIN context_items c ON c.id = w.context_item_id WHERE w.agent_id = ? ORDER BY c.ordinal, c.id`, agentId).map(doc);
|
|
2998
|
+
}
|
|
2999
|
+
#syncWorkingContext(agentId) {
|
|
3000
|
+
const active = activeContextItems(this.#all('SELECT * FROM context_items WHERE agent_id = ? ORDER BY ordinal, id', agentId).map(doc));
|
|
3001
|
+
const ids = new Set(active.map((item) => stringField(item.id)));
|
|
3002
|
+
const remove = this.#database.prepare('DELETE FROM working_context_items WHERE context_item_id = ?');
|
|
3003
|
+
for (const row of this.#all('SELECT context_item_id FROM working_context_items WHERE agent_id = ?', agentId)) {
|
|
3004
|
+
if (!ids.has(String(row.context_item_id)))
|
|
3005
|
+
remove.run(row.context_item_id);
|
|
3006
|
+
}
|
|
3007
|
+
const upsert = this.#database.prepare(`INSERT INTO working_context_items(context_item_id, agent_id, source_revision, content) VALUES (?, ?, ?, ?)
|
|
3008
|
+
ON CONFLICT(context_item_id) DO UPDATE SET source_revision = excluded.source_revision, content = excluded.content, snipped = 0
|
|
3009
|
+
WHERE working_context_items.source_revision != excluded.source_revision`);
|
|
3010
|
+
for (const item of active)
|
|
3011
|
+
upsert.run(item.id, agentId, item.revision, item.content);
|
|
3012
|
+
const sourceRevision = Number(this.#require('agents', agentId).context_revision);
|
|
3013
|
+
const current = this.#database.prepare('SELECT source_revision, prefixes_json FROM working_contexts WHERE agent_id = ?').get(agentId);
|
|
3014
|
+
if (current?.source_revision === sourceRevision)
|
|
3015
|
+
return;
|
|
3016
|
+
this.#database.prepare(`INSERT INTO working_contexts(agent_id, source_revision, revision, digest, updated_at) VALUES (?, ?, 0, ?, ?)
|
|
3017
|
+
ON CONFLICT(agent_id) DO UPDATE SET source_revision = excluded.source_revision, revision = working_contexts.revision + 1,
|
|
3018
|
+
digest = excluded.digest, messages_json = NULL, request_frame_json = NULL, updated_at = excluded.updated_at`)
|
|
3019
|
+
.run(agentId, sourceRevision, digest({ items: this.#workingItems(agentId), prefixes: current === undefined ? [] : parseJson(String(current.prefixes_json)) }), now());
|
|
2238
3020
|
}
|
|
2239
3021
|
#activeProject(projectId) {
|
|
2240
3022
|
if (this.#require('projects', projectId).archived_at !== null)
|
|
@@ -2328,6 +3110,8 @@ function doc(row) {
|
|
|
2328
3110
|
fail('DATABASE_CORRUPT', 503, 'A stored JSON field is invalid.');
|
|
2329
3111
|
result[key.slice(0, -5)] = parseJson(value);
|
|
2330
3112
|
}
|
|
3113
|
+
else if (key === 'auto_review_passed' && value === null)
|
|
3114
|
+
result[key] = null;
|
|
2331
3115
|
else if (booleans.has(key))
|
|
2332
3116
|
result[key] = Number(value) === 1;
|
|
2333
3117
|
else if (value === null || typeof value === 'string' || typeof value === 'number')
|
|
@@ -2337,13 +3121,15 @@ function doc(row) {
|
|
|
2337
3121
|
}
|
|
2338
3122
|
return result;
|
|
2339
3123
|
}
|
|
2340
|
-
const booleans = new Set(['is_default', 'visible', 'locked', 'complete', 'loaded', 'truncated', 'outcome_unknown', 'auto_review_passed', 'initial_step_pending', 'existed_before']);
|
|
3124
|
+
const booleans = new Set(['is_default', 'visible', 'locked', 'complete', 'loaded', 'truncated', 'snipped', 'outcome_unknown', 'auto_review_passed', 'initial_step_pending', 'existed_before']);
|
|
2341
3125
|
/** The table each published entity type lives in, used to resolve its owner and revision. */
|
|
2342
3126
|
const TABLE_BY_ENTITY = {
|
|
2343
3127
|
project: 'projects', workspace: 'workspaces', task: 'tasks', session: 'sessions', agent: 'agents',
|
|
2344
3128
|
pending_message: 'pending_messages', run: 'runs', step: 'steps', model_attempt: 'model_attempts',
|
|
2345
3129
|
call: 'calls', message: 'messages', context_item: 'context_items',
|
|
2346
|
-
context_override: 'context_overrides', result: 'results', artifact: 'artifacts'
|
|
3130
|
+
context_override: 'context_overrides', result: 'results', artifact: 'artifacts',
|
|
3131
|
+
call_partition: 'call_partitions', turn_group: 'context_turn_groups', projection_decision: 'projection_decisions',
|
|
3132
|
+
agent_plan: 'agent_plans', side_effect_ledger: 'side_effect_ledgers'
|
|
2347
3133
|
};
|
|
2348
3134
|
/** The column naming each entity's owner, which a subscriber uses to refresh the owning view. */
|
|
2349
3135
|
const OWNERSHIP = {
|
|
@@ -2351,12 +3137,23 @@ const OWNERSHIP = {
|
|
|
2351
3137
|
agent: { column: 'session_id' }, pending_message: { column: 'agent_id' }, run: { column: 'agent_id' },
|
|
2352
3138
|
step: { column: 'run_id' }, model_attempt: { column: 'step_id' }, call: { column: 'step_id' },
|
|
2353
3139
|
message: { column: 'agent_id' }, context_item: { column: 'agent_id' },
|
|
2354
|
-
context_override: { column: 'agent_id' }, result: { column: 'call_id' }, artifact: { column: 'project_id' }
|
|
3140
|
+
context_override: { column: 'agent_id' }, result: { column: 'call_id' }, artifact: { column: 'project_id' },
|
|
3141
|
+
call_partition: { column: 'step_id' }, turn_group: { column: 'agent_id' }, projection_decision: { column: 'agent_id' },
|
|
3142
|
+
agent_plan: { column: 'agent_id' }, side_effect_ledger: { column: 'step_id' }
|
|
2355
3143
|
};
|
|
2356
3144
|
/** The Route a subscriber re-reads for a changed entity. */
|
|
2357
|
-
function readRouteFor(type, entityId) {
|
|
3145
|
+
function readRouteFor(type, entityId, row) {
|
|
3146
|
+
if (row === undefined && ['workspace', 'call_partition', 'turn_group', 'projection_decision', 'side_effect_ledger'].includes(type))
|
|
3147
|
+
return '/api/v1/snapshot';
|
|
2358
3148
|
const routes = {
|
|
2359
3149
|
project: `/api/v1/projects/${entityId}`,
|
|
3150
|
+
workspace: `/api/v1/projects/${String(row?.project_id)}/workspaces`,
|
|
3151
|
+
call_partition: `/api/v1/steps/${String(row?.step_id)}/partitions`,
|
|
3152
|
+
turn_group: `/api/v1/agents/${String(row?.agent_id)}/context/turn-groups`,
|
|
3153
|
+
projection_decision: `/api/v1/agents/${String(row?.agent_id)}/context/projection/decisions`,
|
|
3154
|
+
agent_plan: `/api/v1/agents/${entityId}`,
|
|
3155
|
+
side_effect_ledger: `/api/v1/steps/${String(row?.step_id)}`,
|
|
3156
|
+
maintenance: '/api/v1/snapshot',
|
|
2360
3157
|
task: `/api/v1/tasks/${entityId}`,
|
|
2361
3158
|
session: `/api/v1/sessions/${entityId}`,
|
|
2362
3159
|
agent: `/api/v1/agents/${entityId}`,
|
|
@@ -2413,7 +3210,8 @@ function attentionItems(calls) {
|
|
|
2413
3210
|
*/
|
|
2414
3211
|
const SCOPE_PARENT = {
|
|
2415
3212
|
step: { type: 'run', table: 'runs', column: 'run_id' },
|
|
2416
|
-
|
|
3213
|
+
call_partition: { type: 'step', table: 'steps', column: 'step_id' },
|
|
3214
|
+
side_effect_ledger: { type: 'step', table: 'steps', column: 'step_id' },
|
|
2417
3215
|
result: { type: 'call', table: 'calls', column: 'call_id' }
|
|
2418
3216
|
};
|
|
2419
3217
|
/**
|