tinker-agent 2.8.0 → 2.10.0
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/CHANGELOG.md +79 -1
- package/README.md +81 -11
- package/package.json +5 -3
- package/src/agent/runtime-context-capabilities.ts +19 -0
- package/src/agent/runtime-context-events.ts +127 -0
- package/src/agent/runtime-context-maintenance.ts +780 -0
- package/src/agent/runtime-hosted-session.ts +443 -0
- package/src/agent/runtime-interactions.ts +291 -0
- package/src/agent/runtime-prompt-scheduler.ts +182 -0
- package/src/agent/runtime-session-contracts.ts +317 -0
- package/src/agent/runtime-session.ts +250 -2130
- package/src/agent/runtime-skills.ts +544 -0
- package/src/cli/command-line.ts +26 -2
- package/src/cli/connect-runner.tsx +26 -0
- package/src/cli/main.ts +26 -0
- package/src/cli/output.ts +1 -1
- package/src/cli/public-cli-contract.ts +18 -0
- package/src/cli/public-config-contract.ts +1 -1
- package/src/cli/runner-dependencies.ts +6 -5
- package/src/cli/serve-runner.ts +45 -0
- package/src/cli/serve-runtime.ts +100 -0
- package/src/context/context-automation-policy.ts +12 -118
- package/src/context/context-swap-renderer.ts +14 -0
- package/src/events/types.ts +12 -0
- package/src/memory/memory-get-tool.ts +1 -1
- package/src/observation/observation-builder.ts +128 -48
- package/src/remote/client.ts +350 -0
- package/src/remote/config.ts +95 -0
- package/src/remote/http-server.ts +240 -0
- package/src/remote/protocol.ts +228 -0
- package/src/remote/service-store.ts +175 -0
- package/src/remote/service.ts +219 -0
- package/src/remote/sync-hub.ts +95 -0
- package/src/session/remote-history-reader.ts +143 -0
- package/src/session/resume-projection.ts +47 -21
- package/src/session/session-history-access.ts +238 -0
- package/src/session/session-store-context-readers.ts +183 -0
- package/src/session/session-store-ledger-writer.ts +315 -0
- package/src/session/session-store-record-writer.ts +318 -0
- package/src/session/session-store-recovery.ts +225 -0
- package/src/session/session-store-revisions.ts +1004 -0
- package/src/session/session-store-sql.ts +40 -0
- package/src/session/session-store-validation.ts +657 -0
- package/src/session/session-store.ts +756 -3186
- package/src/tools/bash-task.ts +46 -18
- package/src/tools/bash.ts +44 -2
- package/src/tools/glob.ts +107 -19
- package/src/tools/grep-output.ts +130 -0
- package/src/tools/grep-pagination.ts +73 -0
- package/src/tools/grep-path.ts +11 -0
- package/src/tools/grep-snippets.ts +111 -0
- package/src/tools/grep.ts +139 -154
- package/src/tools/read.ts +0 -9
- package/src/tools/recall.ts +106 -50
- package/src/tools/registry.ts +4 -6
- package/src/tools/ripgrep.ts +19 -26
- package/src/tools/shell-process.ts +30 -4
- package/src/tools/task-output-range.ts +146 -0
- package/src/tools/task-output-tool.ts +35 -5
- package/src/tools/task-output.ts +35 -0
- package/src/tools/task-stop.ts +2 -1
- package/src/tools/task-tool-args.ts +34 -0
- package/src/tools/terminal-screen.ts +11 -2
- package/src/tools/types.ts +39 -2
- package/src/tui/event-store.ts +23 -5
- package/src/tui/remote-app.tsx +210 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
|
|
3
|
+
export function runTransaction<T>(database: Database, operation: () => T): T {
|
|
4
|
+
database.exec("BEGIN IMMEDIATE");
|
|
5
|
+
try {
|
|
6
|
+
const result = operation();
|
|
7
|
+
database.exec("COMMIT");
|
|
8
|
+
return result;
|
|
9
|
+
} catch (error) {
|
|
10
|
+
try {
|
|
11
|
+
database.exec("ROLLBACK");
|
|
12
|
+
} catch {
|
|
13
|
+
// Preserve the mutation error; the session will fault and close the database.
|
|
14
|
+
}
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function requireSingleChange(
|
|
20
|
+
database: Database,
|
|
21
|
+
reportedChanges: number | bigint,
|
|
22
|
+
operation: string,
|
|
23
|
+
): void {
|
|
24
|
+
const row = database.query("SELECT changes() AS changes").get() as {
|
|
25
|
+
changes: number | bigint;
|
|
26
|
+
};
|
|
27
|
+
if (Number(row.changes) !== 1) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`${operation} must change exactly one row; changed ${row.changes} (driver reported ${reportedChanges}).`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function requireItem<T>(items: readonly T[], index: number, name: string): T {
|
|
35
|
+
const item = items[index];
|
|
36
|
+
if (item === undefined) {
|
|
37
|
+
throw new Error(`Missing ${name} at index ${index}.`);
|
|
38
|
+
}
|
|
39
|
+
return item;
|
|
40
|
+
}
|
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import { activeOverrideManifestHash } from "../context/compiled-context-hash";
|
|
3
|
+
import type {
|
|
4
|
+
StoredContextOverrideV8,
|
|
5
|
+
StoredContextRevisionV8,
|
|
6
|
+
StoredContextSnapshotV8,
|
|
7
|
+
SwapOverride,
|
|
8
|
+
} from "../context/context-revision";
|
|
9
|
+
import { ContextRevisionCompiler } from "../context/context-revision-compiler";
|
|
10
|
+
import {
|
|
11
|
+
contextSurfaceChangeManifestHash,
|
|
12
|
+
contextSurfaceChanges,
|
|
13
|
+
validateStoredContextSurface,
|
|
14
|
+
type StoredContextSurfaceV8,
|
|
15
|
+
} from "../context/context-surface";
|
|
16
|
+
import {
|
|
17
|
+
ContextSwapRenderer,
|
|
18
|
+
SWAP_OBSERVATION_FORMAT,
|
|
19
|
+
SWAP_TOOL_IMAGE_FORMAT,
|
|
20
|
+
} from "../context/context-swap-renderer";
|
|
21
|
+
import {
|
|
22
|
+
type CanonicalMessageRecord,
|
|
23
|
+
type ProtocolContextView,
|
|
24
|
+
type ToolResultRecord,
|
|
25
|
+
} from "../context/protocol-frame";
|
|
26
|
+
import type {
|
|
27
|
+
ContextRevisionId,
|
|
28
|
+
ContextSurfaceId,
|
|
29
|
+
MessageId,
|
|
30
|
+
SessionId,
|
|
31
|
+
} from "../ids/runtime-id";
|
|
32
|
+
import { stableJsonStringify } from "../model/model-request-preflight";
|
|
33
|
+
import {
|
|
34
|
+
renderSkillActivationReceipt,
|
|
35
|
+
SKILL_ACTIVATION_RECEIPT_FORMAT,
|
|
36
|
+
} from "../skills/skill-context";
|
|
37
|
+
import type { SessionStore } from "./session-store";
|
|
38
|
+
import {
|
|
39
|
+
loadMeasuredContextState,
|
|
40
|
+
previousRevision,
|
|
41
|
+
requireActiveRevisionId,
|
|
42
|
+
} from "./session-store-context-readers";
|
|
43
|
+
import {
|
|
44
|
+
skillActivationManifestSha256,
|
|
45
|
+
type StoredSessionMetaV10,
|
|
46
|
+
} from "./session-store-contracts";
|
|
47
|
+
import {
|
|
48
|
+
decodeContextRevision,
|
|
49
|
+
decodeContextSurface,
|
|
50
|
+
decodeStoredSwapOverride,
|
|
51
|
+
protocolPrefixView,
|
|
52
|
+
stripStoredOverride,
|
|
53
|
+
} from "./session-store-record-codecs";
|
|
54
|
+
import { requireItem } from "./session-store-sql";
|
|
55
|
+
import {
|
|
56
|
+
enumFromSql,
|
|
57
|
+
nullableStringFromSql,
|
|
58
|
+
numberFromSql,
|
|
59
|
+
stringFromSql,
|
|
60
|
+
} from "./session-store-value-codecs";
|
|
61
|
+
|
|
62
|
+
/** Validates persisted state without mutating canonical history. */
|
|
63
|
+
export class SessionStoreValidation {
|
|
64
|
+
private readonly revisionCompiler = new ContextRevisionCompiler();
|
|
65
|
+
private readonly swapRenderer = new ContextSwapRenderer();
|
|
66
|
+
constructor(
|
|
67
|
+
private readonly database: Database,
|
|
68
|
+
private readonly sessionId: SessionId,
|
|
69
|
+
private readonly loadSkillActivations: SessionStore["loadSkillActivations"],
|
|
70
|
+
) {}
|
|
71
|
+
|
|
72
|
+
loadValidatedContextSnapshot(
|
|
73
|
+
meta: StoredSessionMetaV10,
|
|
74
|
+
canonical: ProtocolContextView,
|
|
75
|
+
): StoredContextSnapshotV8 {
|
|
76
|
+
const activeRevisionId = requireActiveRevisionId(meta);
|
|
77
|
+
const surfaces = this.database
|
|
78
|
+
.query("SELECT * FROM context_surfaces ORDER BY rowid")
|
|
79
|
+
.all()
|
|
80
|
+
.map(decodeContextSurface);
|
|
81
|
+
const surfacesById = new Map<ContextSurfaceId, StoredContextSurfaceV8>();
|
|
82
|
+
for (const surface of surfaces) {
|
|
83
|
+
validateStoredContextSurface(surface);
|
|
84
|
+
if (surface.sessionId !== this.sessionId || surfacesById.has(surface.surfaceId)) {
|
|
85
|
+
throw new Error("Context surface identity is invalid or duplicated.");
|
|
86
|
+
}
|
|
87
|
+
surfacesById.set(surface.surfaceId, surface);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const revisions = this.database
|
|
91
|
+
.query("SELECT * FROM context_revisions ORDER BY revision_number")
|
|
92
|
+
.all()
|
|
93
|
+
.map(decodeContextRevision);
|
|
94
|
+
if (revisions.length === 0) {
|
|
95
|
+
throw new Error("Session has no context revision.");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const revisionNumberById = new Map<ContextRevisionId, number>();
|
|
99
|
+
for (let index = 0; index < revisions.length; index += 1) {
|
|
100
|
+
const revision = requireItem(revisions, index, "context revision");
|
|
101
|
+
const previous = revisions[index - 1];
|
|
102
|
+
const surface = surfacesById.get(revision.surfaceId);
|
|
103
|
+
if (
|
|
104
|
+
revision.sessionId !== this.sessionId ||
|
|
105
|
+
revision.revisionNumber !== index + 1 ||
|
|
106
|
+
surface === undefined ||
|
|
107
|
+
surface.surfaceSha256 !== revision.surfaceSha256 ||
|
|
108
|
+
(index === 0
|
|
109
|
+
? revision.kind !== "initial_full" || revision.parentRevisionId !== null
|
|
110
|
+
: revision.kind === "initial_full" ||
|
|
111
|
+
revision.parentRevisionId !== previous?.revisionId) ||
|
|
112
|
+
((revision.kind === "swap_only" || revision.kind === "prefix_retirement") &&
|
|
113
|
+
revision.surfaceId !== previous?.surfaceId) ||
|
|
114
|
+
(revision.kind === "surface_refresh" &&
|
|
115
|
+
revision.surfaceId === previous?.surfaceId) ||
|
|
116
|
+
(previous !== undefined &&
|
|
117
|
+
(revision.keepFromOrdinal < previous.keepFromOrdinal ||
|
|
118
|
+
((revision.kind === "swap_only" ||
|
|
119
|
+
revision.kind === "surface_refresh" ||
|
|
120
|
+
revision.kind === "skills_update") &&
|
|
121
|
+
revision.keepFromOrdinal !== previous.keepFromOrdinal) ||
|
|
122
|
+
(revision.kind === "prefix_retirement" &&
|
|
123
|
+
revision.keepFromOrdinal <= previous.keepFromOrdinal)))
|
|
124
|
+
) {
|
|
125
|
+
throw new Error("Context revision chain is not linear and contiguous.");
|
|
126
|
+
}
|
|
127
|
+
const boundary = canonical.frames.find(
|
|
128
|
+
(frame) => frame.lastOrdinal === revision.sourceThroughOrdinal,
|
|
129
|
+
);
|
|
130
|
+
if (
|
|
131
|
+
revision.sourceThroughOrdinal > canonical.messages.length ||
|
|
132
|
+
boundary?.state !== "closed"
|
|
133
|
+
) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`Context revision ${revision.revisionId} has an invalid source boundary.`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
revisionNumberById.set(revision.revisionId, revision.revisionNumber);
|
|
139
|
+
}
|
|
140
|
+
const introducedSurfaceIds = new Set(
|
|
141
|
+
revisions.flatMap((revision, index) => {
|
|
142
|
+
const previous = revisions[index - 1];
|
|
143
|
+
return revision.kind === "initial_full" ||
|
|
144
|
+
revision.kind === "surface_refresh" ||
|
|
145
|
+
(revision.kind === "skills_update" &&
|
|
146
|
+
revision.surfaceId !== previous?.surfaceId)
|
|
147
|
+
? [revision.surfaceId]
|
|
148
|
+
: [];
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
if (
|
|
152
|
+
introducedSurfaceIds.size !== surfaces.length ||
|
|
153
|
+
surfaces.some((surface) => !introducedSurfaceIds.has(surface.surfaceId))
|
|
154
|
+
) {
|
|
155
|
+
throw new Error("Context surface chain contains an orphan or duplicate surface.");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const activeRevision = requireItem(
|
|
159
|
+
revisions,
|
|
160
|
+
revisions.length - 1,
|
|
161
|
+
"active context revision",
|
|
162
|
+
);
|
|
163
|
+
if (activeRevision.revisionId !== activeRevisionId) {
|
|
164
|
+
throw new Error("Active context revision is not the latest committed revision.");
|
|
165
|
+
}
|
|
166
|
+
const activeSurface = surfacesById.get(activeRevision.surfaceId);
|
|
167
|
+
if (activeSurface === undefined) {
|
|
168
|
+
throw new Error("Active context revision surface is missing.");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const overrides = this.database
|
|
172
|
+
.query(
|
|
173
|
+
`SELECT co.* FROM context_overrides co
|
|
174
|
+
JOIN context_revisions cr
|
|
175
|
+
ON cr.revision_id = co.introduced_revision_id
|
|
176
|
+
ORDER BY cr.revision_number, co.ordinal`,
|
|
177
|
+
)
|
|
178
|
+
.all()
|
|
179
|
+
.map(decodeStoredSwapOverride);
|
|
180
|
+
this.validateStoredOverrides(overrides, canonical, revisions, revisionNumberById);
|
|
181
|
+
this.validateSkillActivationRows(canonical, revisions, overrides, surfaces);
|
|
182
|
+
|
|
183
|
+
for (const revision of revisions) {
|
|
184
|
+
const surface = surfacesById.get(revision.surfaceId);
|
|
185
|
+
if (surface === undefined) {
|
|
186
|
+
throw new Error(`Context revision ${revision.revisionId} has no surface.`);
|
|
187
|
+
}
|
|
188
|
+
const activeOverrides = overrides.filter(
|
|
189
|
+
(override) =>
|
|
190
|
+
(revisionNumberById.get(override.introducedRevisionId) ??
|
|
191
|
+
Number.POSITIVE_INFINITY) <= revision.revisionNumber &&
|
|
192
|
+
override.ordinal >= revision.keepFromOrdinal,
|
|
193
|
+
);
|
|
194
|
+
const introducedCount = overrides.filter(
|
|
195
|
+
(override) => override.introducedRevisionId === revision.revisionId,
|
|
196
|
+
).length;
|
|
197
|
+
if (
|
|
198
|
+
introducedCount !== revision.addedOverrideCount ||
|
|
199
|
+
activeOverrides.length !== revision.activeOverrideCount ||
|
|
200
|
+
activeOverrideManifestHash(activeOverrides) !==
|
|
201
|
+
revision.activeOverrideManifestSha256
|
|
202
|
+
) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`Context revision ${revision.revisionId} override manifest is invalid.`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
if (
|
|
208
|
+
revision.kind === "surface_refresh" &&
|
|
209
|
+
previousRevision(revisions, revision)?.activeOverrideManifestSha256 !==
|
|
210
|
+
revision.activeOverrideManifestSha256
|
|
211
|
+
) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`Context surface revision ${revision.revisionId} changed overrides.`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
if (revision.kind === "skills_update") {
|
|
217
|
+
const parent = previousRevision(revisions, revision);
|
|
218
|
+
const parentSurface =
|
|
219
|
+
parent === undefined ? undefined : surfacesById.get(parent.surfaceId);
|
|
220
|
+
const settlements = this.loadSkillActivations().filter(
|
|
221
|
+
(activation) => activation.settledRevisionId === revision.revisionId,
|
|
222
|
+
);
|
|
223
|
+
if (
|
|
224
|
+
parentSurface === undefined ||
|
|
225
|
+
contextSurfaceChangeManifestHash(
|
|
226
|
+
contextSurfaceChanges(parentSurface, surface),
|
|
227
|
+
) !== revision.changeManifestSha256 ||
|
|
228
|
+
settlements.length !== revision.addedOverrideCount ||
|
|
229
|
+
skillActivationManifestSha256(
|
|
230
|
+
settlements.map((activation) => ({
|
|
231
|
+
activationMessageId: activation.activationMessageId,
|
|
232
|
+
name: activation.name,
|
|
233
|
+
state: activation.state === "promoted" ? "promoted" : "rejected",
|
|
234
|
+
...(activation.rejectionReason === undefined
|
|
235
|
+
? {}
|
|
236
|
+
: { rejectionReason: activation.rejectionReason }),
|
|
237
|
+
})),
|
|
238
|
+
) !== revision.activationManifestSha256
|
|
239
|
+
) {
|
|
240
|
+
throw new Error(
|
|
241
|
+
`Agent Skills revision ${revision.revisionId} manifest is invalid.`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (revision.kind === "prefix_retirement") {
|
|
246
|
+
const parent = previousRevision(revisions, revision);
|
|
247
|
+
if (parent === undefined) {
|
|
248
|
+
throw new Error("Prefix retirement revision has no parent.");
|
|
249
|
+
}
|
|
250
|
+
const retiredStart = Math.max(parent.keepFromOrdinal, 2);
|
|
251
|
+
const retiredMessages = canonical.messages.filter(
|
|
252
|
+
(message) =>
|
|
253
|
+
message.ordinal >= retiredStart &&
|
|
254
|
+
message.ordinal < revision.keepFromOrdinal,
|
|
255
|
+
);
|
|
256
|
+
const retiredFrames = canonical.frames.filter(
|
|
257
|
+
(frame) =>
|
|
258
|
+
frame.firstOrdinal >= retiredStart &&
|
|
259
|
+
(frame.lastOrdinal ?? Number.POSITIVE_INFINITY) < revision.keepFromOrdinal,
|
|
260
|
+
);
|
|
261
|
+
const retiredTurns = new Set(
|
|
262
|
+
retiredMessages.flatMap((message) =>
|
|
263
|
+
message.role === "system" ? [] : [message.turnId],
|
|
264
|
+
),
|
|
265
|
+
);
|
|
266
|
+
if (
|
|
267
|
+
revision.retiredThroughOrdinal !== revision.keepFromOrdinal - 1 ||
|
|
268
|
+
revision.retiredMessageCount !== retiredMessages.length ||
|
|
269
|
+
revision.retiredFrameCount !== retiredFrames.length ||
|
|
270
|
+
revision.retiredTurnCount !== retiredTurns.size
|
|
271
|
+
) {
|
|
272
|
+
throw new Error(
|
|
273
|
+
`Context retirement revision ${revision.revisionId} counts are invalid.`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (revision.kind === "surface_refresh") {
|
|
278
|
+
const parent = previousRevision(revisions, revision);
|
|
279
|
+
const parentSurface =
|
|
280
|
+
parent === undefined ? undefined : surfacesById.get(parent.surfaceId);
|
|
281
|
+
if (
|
|
282
|
+
parentSurface === undefined ||
|
|
283
|
+
contextSurfaceChangeManifestHash(
|
|
284
|
+
contextSurfaceChanges(parentSurface, surface),
|
|
285
|
+
) !== revision.changeManifestSha256
|
|
286
|
+
) {
|
|
287
|
+
throw new Error(
|
|
288
|
+
`Context surface revision ${revision.revisionId} change manifest is invalid.`,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const prefix = protocolPrefixView(canonical, revision.sourceThroughOrdinal);
|
|
293
|
+
this.revisionCompiler.compileActive({
|
|
294
|
+
meta: Object.freeze({
|
|
295
|
+
sessionId: this.sessionId,
|
|
296
|
+
activeRevisionId: revision.revisionId,
|
|
297
|
+
}),
|
|
298
|
+
revision,
|
|
299
|
+
surface,
|
|
300
|
+
activeOverrides,
|
|
301
|
+
canonical: prefix,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const measurement = loadMeasuredContextState(this.database, this.sessionId);
|
|
306
|
+
if (
|
|
307
|
+
measurement !== undefined &&
|
|
308
|
+
measurement.revisionId !== activeRevision.revisionId
|
|
309
|
+
) {
|
|
310
|
+
throw new Error("Context measurement is not bound to the active revision.");
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return Object.freeze({
|
|
314
|
+
meta: Object.freeze({
|
|
315
|
+
sessionId: meta.sessionId,
|
|
316
|
+
activeRevisionId,
|
|
317
|
+
}),
|
|
318
|
+
revision: activeRevision,
|
|
319
|
+
surface: activeSurface,
|
|
320
|
+
activeOverrides: Object.freeze(
|
|
321
|
+
overrides.filter(
|
|
322
|
+
(override) =>
|
|
323
|
+
(revisionNumberById.get(override.introducedRevisionId) ??
|
|
324
|
+
Number.POSITIVE_INFINITY) <= activeRevision.revisionNumber &&
|
|
325
|
+
override.ordinal >= activeRevision.keepFromOrdinal,
|
|
326
|
+
),
|
|
327
|
+
),
|
|
328
|
+
canonical,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
private validateStoredOverrides(
|
|
333
|
+
overrides: readonly StoredContextOverrideV8[],
|
|
334
|
+
canonical: ProtocolContextView,
|
|
335
|
+
revisions: readonly StoredContextRevisionV8[],
|
|
336
|
+
revisionNumberById: ReadonlyMap<ContextRevisionId, number>,
|
|
337
|
+
): void {
|
|
338
|
+
const messages = new Map(
|
|
339
|
+
canonical.messages.map((message) => [message.messageId, message] as const),
|
|
340
|
+
);
|
|
341
|
+
const frames = new Map(
|
|
342
|
+
canonical.frames.map((frame) => [frame.frameId, frame] as const),
|
|
343
|
+
);
|
|
344
|
+
const results = new Map(
|
|
345
|
+
canonical.toolResults.map((result) => [result.toolMessageId, result] as const),
|
|
346
|
+
);
|
|
347
|
+
const revisionsById = new Map(
|
|
348
|
+
revisions.map((revision) => [revision.revisionId, revision] as const),
|
|
349
|
+
);
|
|
350
|
+
const seenMessages = new Set<MessageId>();
|
|
351
|
+
for (const override of overrides) {
|
|
352
|
+
const revision = revisionsById.get(override.introducedRevisionId);
|
|
353
|
+
const message = messages.get(override.messageId);
|
|
354
|
+
const frame = frames.get(override.frameId);
|
|
355
|
+
const result = results.get(override.messageId);
|
|
356
|
+
if (
|
|
357
|
+
(revision?.kind !== "swap_only" && revision?.kind !== "skills_update") ||
|
|
358
|
+
(revision.kind === "swap_only" &&
|
|
359
|
+
override.rendererFormat !== SWAP_OBSERVATION_FORMAT &&
|
|
360
|
+
override.rendererFormat !== SWAP_TOOL_IMAGE_FORMAT) ||
|
|
361
|
+
(revision.kind === "skills_update" &&
|
|
362
|
+
override.rendererFormat !== SKILL_ACTIVATION_RECEIPT_FORMAT) ||
|
|
363
|
+
revisionNumberById.get(revision.revisionId) === undefined ||
|
|
364
|
+
override.ordinal < revision.keepFromOrdinal ||
|
|
365
|
+
override.ordinal > revision.sourceThroughOrdinal ||
|
|
366
|
+
seenMessages.has(override.messageId) ||
|
|
367
|
+
message?.role !== "tool" ||
|
|
368
|
+
message.frameId !== override.frameId ||
|
|
369
|
+
message.ordinal !== override.ordinal ||
|
|
370
|
+
frame?.kind !== "tool_exchange" ||
|
|
371
|
+
frame.state !== "closed" ||
|
|
372
|
+
result === undefined
|
|
373
|
+
) {
|
|
374
|
+
throw new Error("Stored context override canonical identity is invalid.");
|
|
375
|
+
}
|
|
376
|
+
const rendered =
|
|
377
|
+
override.rendererFormat === SWAP_OBSERVATION_FORMAT ||
|
|
378
|
+
override.rendererFormat === SWAP_TOOL_IMAGE_FORMAT
|
|
379
|
+
? this.swapRenderer.render({ message, result })
|
|
380
|
+
: this.renderStoredSkillReceipt(message, result, revision.revisionId);
|
|
381
|
+
if (
|
|
382
|
+
stableJsonStringify(rendered) !==
|
|
383
|
+
stableJsonStringify(stripStoredOverride(override))
|
|
384
|
+
) {
|
|
385
|
+
throw new Error(
|
|
386
|
+
`Stored context override ${override.messageId} does not match deterministic rendering.`,
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
seenMessages.add(override.messageId);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
private validateSkillActivationRows(
|
|
394
|
+
canonical: ProtocolContextView,
|
|
395
|
+
revisions: readonly StoredContextRevisionV8[],
|
|
396
|
+
overrides: readonly StoredContextOverrideV8[],
|
|
397
|
+
surfaces: readonly StoredContextSurfaceV8[],
|
|
398
|
+
): void {
|
|
399
|
+
const activations = this.loadSkillActivations();
|
|
400
|
+
const activationByMessage = new Map(
|
|
401
|
+
activations.map((activation) => [activation.activationMessageId, activation]),
|
|
402
|
+
);
|
|
403
|
+
const messages = new Map(
|
|
404
|
+
canonical.messages.map((message) => [message.messageId, message]),
|
|
405
|
+
);
|
|
406
|
+
const results = new Map(
|
|
407
|
+
canonical.toolResults.map((result) => [result.toolMessageId, result]),
|
|
408
|
+
);
|
|
409
|
+
const loadedResults = canonical.toolResults.filter(
|
|
410
|
+
(result) =>
|
|
411
|
+
result.completion.kind === "returned" &&
|
|
412
|
+
result.completion.raw.kind === "skill" &&
|
|
413
|
+
result.completion.raw.ok &&
|
|
414
|
+
result.completion.raw.status === "loaded",
|
|
415
|
+
);
|
|
416
|
+
if (loadedResults.length !== activations.length) {
|
|
417
|
+
throw new Error("Loaded Agent Skill results and activation rows differ.");
|
|
418
|
+
}
|
|
419
|
+
const revisionsById = new Map(
|
|
420
|
+
revisions.map((revision) => [revision.revisionId, revision]),
|
|
421
|
+
);
|
|
422
|
+
const overridesByMessage = new Map(
|
|
423
|
+
overrides.map((override) => [override.messageId, override]),
|
|
424
|
+
);
|
|
425
|
+
for (const activation of activations) {
|
|
426
|
+
const message = messages.get(activation.activationMessageId);
|
|
427
|
+
const result = results.get(activation.activationMessageId);
|
|
428
|
+
const raw =
|
|
429
|
+
result?.completion.kind === "returned" ? result.completion.raw : undefined;
|
|
430
|
+
if (
|
|
431
|
+
activation.sessionId !== this.sessionId ||
|
|
432
|
+
message?.role !== "tool" ||
|
|
433
|
+
message.name !== "Skill" ||
|
|
434
|
+
message.toolCallId !== activation.toolCallId ||
|
|
435
|
+
raw?.kind !== "skill" ||
|
|
436
|
+
!raw.ok ||
|
|
437
|
+
raw.status !== "loaded" ||
|
|
438
|
+
raw.name !== activation.name ||
|
|
439
|
+
raw.scope !== activation.scope ||
|
|
440
|
+
raw.sha256 !== activation.skillFileSha256
|
|
441
|
+
) {
|
|
442
|
+
throw new Error("Agent Skill activation canonical identity is invalid.");
|
|
443
|
+
}
|
|
444
|
+
if (activation.settledRevisionId !== undefined) {
|
|
445
|
+
const revision = revisionsById.get(activation.settledRevisionId);
|
|
446
|
+
const override = overridesByMessage.get(activation.activationMessageId);
|
|
447
|
+
if (
|
|
448
|
+
revision?.kind !== "skills_update" ||
|
|
449
|
+
override?.introducedRevisionId !== revision.revisionId ||
|
|
450
|
+
override.rendererFormat !== SKILL_ACTIVATION_RECEIPT_FORMAT
|
|
451
|
+
) {
|
|
452
|
+
throw new Error("Settled Agent Skill activation receipt is invalid.");
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
for (const surface of surfaces) {
|
|
457
|
+
for (const active of surface.activeSkills) {
|
|
458
|
+
const activation = activationByMessage.get(active.activationMessageId);
|
|
459
|
+
if (activation?.state !== "promoted" || activation.name !== active.name) {
|
|
460
|
+
throw new Error(
|
|
461
|
+
"Context surface references an invalid Agent Skill activation.",
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
private renderStoredSkillReceipt(
|
|
469
|
+
message: Extract<CanonicalMessageRecord, { role: "tool" }>,
|
|
470
|
+
result: ToolResultRecord,
|
|
471
|
+
revisionId: ContextRevisionId,
|
|
472
|
+
): SwapOverride {
|
|
473
|
+
if (
|
|
474
|
+
message.name !== "Skill" ||
|
|
475
|
+
result.completion.kind !== "returned" ||
|
|
476
|
+
result.completion.raw.kind !== "skill" ||
|
|
477
|
+
!result.completion.raw.ok ||
|
|
478
|
+
result.completion.raw.status !== "loaded"
|
|
479
|
+
) {
|
|
480
|
+
throw new Error("Agent Skill receipt does not target a loaded Skill result.");
|
|
481
|
+
}
|
|
482
|
+
const activation = this.loadSkillActivations().find(
|
|
483
|
+
(entry) => entry.activationMessageId === message.messageId,
|
|
484
|
+
);
|
|
485
|
+
if (
|
|
486
|
+
activation === undefined ||
|
|
487
|
+
activation.settledRevisionId !== revisionId ||
|
|
488
|
+
(activation.state !== "promoted" && activation.state !== "rejected")
|
|
489
|
+
) {
|
|
490
|
+
throw new Error("Agent Skill receipt has no matching settled activation.");
|
|
491
|
+
}
|
|
492
|
+
return renderSkillActivationReceipt({
|
|
493
|
+
message: {
|
|
494
|
+
messageId: message.messageId,
|
|
495
|
+
frameId: message.frameId,
|
|
496
|
+
ordinal: message.ordinal,
|
|
497
|
+
content: message.displayText,
|
|
498
|
+
contentSha256: message.contentSha256,
|
|
499
|
+
},
|
|
500
|
+
name: activation.name,
|
|
501
|
+
outcome:
|
|
502
|
+
activation.state === "promoted"
|
|
503
|
+
? "promoted"
|
|
504
|
+
: activation.rejectionReason === "unavailable"
|
|
505
|
+
? "unavailable"
|
|
506
|
+
: "rejected",
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
validateCounters(meta: StoredSessionMetaV10, view: ProtocolContextView): void {
|
|
511
|
+
const turns = this.database
|
|
512
|
+
.query("SELECT * FROM turns ORDER BY turn_number")
|
|
513
|
+
.all() as Array<Record<string, unknown>>;
|
|
514
|
+
for (let index = 0; index < turns.length; index += 1) {
|
|
515
|
+
const turn = turns[index];
|
|
516
|
+
if (numberFromSql(turn.turn_number, "turn_number") !== index + 1) {
|
|
517
|
+
throw new Error("Turn number sequence has a gap.");
|
|
518
|
+
}
|
|
519
|
+
const turnId = stringFromSql(turn.turn_id, "turn_id");
|
|
520
|
+
const turnStatus = enumFromSql(
|
|
521
|
+
turn.status,
|
|
522
|
+
["open", "completed", "failed", "cancelled", "interrupted"] as const,
|
|
523
|
+
"turn status",
|
|
524
|
+
);
|
|
525
|
+
const iterations = this.database
|
|
526
|
+
.query("SELECT * FROM iterations WHERE turn_id = ? ORDER BY iteration_number")
|
|
527
|
+
.all(turnId) as Array<Record<string, unknown>>;
|
|
528
|
+
const storedLastIterationId = nullableStringFromSql(
|
|
529
|
+
turn.last_iteration_id,
|
|
530
|
+
"last_iteration_id",
|
|
531
|
+
);
|
|
532
|
+
const actualLastIterationId =
|
|
533
|
+
iterations.length === 0
|
|
534
|
+
? null
|
|
535
|
+
: stringFromSql(iterations.at(-1)!.iteration_id, "iteration_id");
|
|
536
|
+
if (storedLastIterationId !== actualLastIterationId) {
|
|
537
|
+
throw new Error(`Last iteration identity is invalid in turn ${turnId}.`);
|
|
538
|
+
}
|
|
539
|
+
for (
|
|
540
|
+
let iterationIndex = 0;
|
|
541
|
+
iterationIndex < iterations.length;
|
|
542
|
+
iterationIndex += 1
|
|
543
|
+
) {
|
|
544
|
+
const iteration = iterations[iterationIndex];
|
|
545
|
+
if (
|
|
546
|
+
numberFromSql(iteration.iteration_number, "iteration_number") !==
|
|
547
|
+
iterationIndex + 1
|
|
548
|
+
) {
|
|
549
|
+
throw new Error(`Iteration number sequence has a gap in turn ${turnId}.`);
|
|
550
|
+
}
|
|
551
|
+
const iterationId = stringFromSql(iteration.iteration_id, "iteration_id");
|
|
552
|
+
const outcome = enumFromSql(
|
|
553
|
+
iteration.outcome,
|
|
554
|
+
[
|
|
555
|
+
"open",
|
|
556
|
+
"continue",
|
|
557
|
+
"completed",
|
|
558
|
+
"failed",
|
|
559
|
+
"cancelled",
|
|
560
|
+
"interrupted",
|
|
561
|
+
] as const,
|
|
562
|
+
"iteration outcome",
|
|
563
|
+
);
|
|
564
|
+
if (iterationIndex < iterations.length - 1 && outcome !== "continue") {
|
|
565
|
+
throw new Error(
|
|
566
|
+
`Non-final iteration ${iterationId} must have continue outcome.`,
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
const toolCalls = view.messages.flatMap((message) =>
|
|
570
|
+
message.role === "assistant" && message.iterationId === iterationId
|
|
571
|
+
? (message.toolCalls ?? [])
|
|
572
|
+
: [],
|
|
573
|
+
);
|
|
574
|
+
if (
|
|
575
|
+
numberFromSql(iteration.next_tool_call_number, "next_tool_call_number") !==
|
|
576
|
+
toolCalls.length + 1
|
|
577
|
+
) {
|
|
578
|
+
throw new Error(`Tool call counter is invalid in iteration ${iterationId}.`);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
if (
|
|
582
|
+
numberFromSql(turn.next_iteration_number, "next_iteration_number") !==
|
|
583
|
+
iterations.length + 1
|
|
584
|
+
) {
|
|
585
|
+
throw new Error(`Iteration counter is invalid in turn ${turnId}.`);
|
|
586
|
+
}
|
|
587
|
+
const openIterationCount = iterations.filter(
|
|
588
|
+
(iteration) => iteration.outcome === "open",
|
|
589
|
+
).length;
|
|
590
|
+
if (turnStatus === "open" && openIterationCount > 1) {
|
|
591
|
+
throw new Error(`Open turn ${turnId} has multiple open iterations.`);
|
|
592
|
+
}
|
|
593
|
+
if (turnStatus !== "open" && openIterationCount !== 0) {
|
|
594
|
+
throw new Error(`Terminal turn ${turnId} still has an open iteration.`);
|
|
595
|
+
}
|
|
596
|
+
const lastOutcome = iterations.at(-1)?.outcome;
|
|
597
|
+
if (
|
|
598
|
+
turnStatus !== "open" &&
|
|
599
|
+
iterations.length > 0 &&
|
|
600
|
+
lastOutcome !== turnStatus &&
|
|
601
|
+
!(turnStatus === "interrupted" && lastOutcome === "continue")
|
|
602
|
+
) {
|
|
603
|
+
throw new Error(
|
|
604
|
+
`Terminal turn ${turnId} does not match its last iteration outcome.`,
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
const finalMessageId = nullableStringFromSql(
|
|
608
|
+
turn.final_message_id,
|
|
609
|
+
"final_message_id",
|
|
610
|
+
);
|
|
611
|
+
if (turnStatus === "completed") {
|
|
612
|
+
const finalMessage = view.messages.find(
|
|
613
|
+
(message) => message.messageId === finalMessageId,
|
|
614
|
+
);
|
|
615
|
+
const lastTurnMessage = [...view.messages]
|
|
616
|
+
.reverse()
|
|
617
|
+
.find((message) => "turnId" in message && message.turnId === turnId);
|
|
618
|
+
if (
|
|
619
|
+
finalMessage?.role !== "assistant" ||
|
|
620
|
+
finalMessage.turnId !== turnId ||
|
|
621
|
+
(finalMessage.toolCalls?.length ?? 0) !== 0 ||
|
|
622
|
+
lastTurnMessage?.messageId !== finalMessage.messageId
|
|
623
|
+
) {
|
|
624
|
+
throw new Error(`Final message identity is invalid in turn ${turnId}.`);
|
|
625
|
+
}
|
|
626
|
+
} else if (finalMessageId !== null) {
|
|
627
|
+
throw new Error(`Non-completed turn ${turnId} has a final message.`);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (meta.nextTurnNumber !== turns.length + 1) {
|
|
631
|
+
throw new Error("Session turn counter is invalid.");
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
validateAddedOverrides(
|
|
636
|
+
overrides: readonly SwapOverride[],
|
|
637
|
+
canonical: ProtocolContextView,
|
|
638
|
+
): void {
|
|
639
|
+
const messages = new Map(
|
|
640
|
+
canonical.messages.map((message) => [message.messageId, message] as const),
|
|
641
|
+
);
|
|
642
|
+
const results = new Map(
|
|
643
|
+
canonical.toolResults.map((result) => [result.toolMessageId, result] as const),
|
|
644
|
+
);
|
|
645
|
+
for (const override of overrides) {
|
|
646
|
+
const message = messages.get(override.messageId);
|
|
647
|
+
const result = results.get(override.messageId);
|
|
648
|
+
if (message?.role !== "tool" || result === undefined) {
|
|
649
|
+
throw new Error("Added context override does not target a tool result.");
|
|
650
|
+
}
|
|
651
|
+
const expected = this.swapRenderer.render({ message, result });
|
|
652
|
+
if (stableJsonStringify(expected) !== stableJsonStringify(override)) {
|
|
653
|
+
throw new Error("Added context override is not deterministic.");
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|