tinker-agent 1.0.65
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 +173 -0
- package/package.json +78 -0
- package/patches/markdansi@0.3.2.patch +37 -0
- package/src/agent/context-builder.ts +43 -0
- package/src/agent/context-meter.ts +310 -0
- package/src/agent/loop.ts +525 -0
- package/src/agent/runtime-session.ts +1212 -0
- package/src/agent/session-ledger.ts +828 -0
- package/src/agent/turn-cancellation.ts +44 -0
- package/src/agent/types.ts +77 -0
- package/src/cli/config.ts +283 -0
- package/src/cli/index.ts +29 -0
- package/src/cli/model-profiles.ts +289 -0
- package/src/cli/run-runner.ts +107 -0
- package/src/cli/tui-runner.tsx +290 -0
- package/src/context/compiled-context-hash.ts +138 -0
- package/src/context/compiled-context-validator.ts +209 -0
- package/src/context/context-manager.ts +362 -0
- package/src/context/context-policy.ts +8 -0
- package/src/context/context-protocol-validator.ts +463 -0
- package/src/context/context-revision-compiler.ts +281 -0
- package/src/context/context-revision.ts +111 -0
- package/src/context/context-source.ts +30 -0
- package/src/context/context-swap-renderer.ts +272 -0
- package/src/context/protocol-frame.ts +240 -0
- package/src/context/swap-planner.ts +725 -0
- package/src/events/append-private-file.ts +16 -0
- package/src/events/bash-result-detail.ts +70 -0
- package/src/events/composite-event-sink.ts +82 -0
- package/src/events/event-sink.ts +16 -0
- package/src/events/jsonl-event-log.ts +13 -0
- package/src/events/observation-text-log.ts +195 -0
- package/src/events/stdout-event-printer.ts +396 -0
- package/src/events/types.ts +263 -0
- package/src/ids/runtime-id.ts +68 -0
- package/src/ids/uuid-v7.ts +5 -0
- package/src/instructions/project-instructions.ts +242 -0
- package/src/mcp/mcp-config.ts +144 -0
- package/src/mcp/mcp-manager.ts +216 -0
- package/src/mcp/mcp-tool-executor.ts +178 -0
- package/src/model/committed-prefix-auditor.ts +68 -0
- package/src/model/fake-model-client.ts +280 -0
- package/src/model/model-client.ts +64 -0
- package/src/model/model-context-profile.ts +134 -0
- package/src/model/model-request-preflight.ts +120 -0
- package/src/model/openai-chat-mapping.ts +444 -0
- package/src/model/openai-chat-model-client.ts +190 -0
- package/src/model/prompt-prefix-hash.ts +47 -0
- package/src/model/token-estimator.ts +148 -0
- package/src/observation/observation-builder.ts +481 -0
- package/src/session/resume-projection.ts +616 -0
- package/src/session/session-catalog.ts +270 -0
- package/src/session/session-errors.ts +121 -0
- package/src/session/session-history-reader.ts +535 -0
- package/src/session/session-lock.ts +291 -0
- package/src/session/session-schema.ts +741 -0
- package/src/session/session-store.ts +3067 -0
- package/src/session/sqlite-session-ledger.ts +153 -0
- package/src/tools/bash-task.ts +617 -0
- package/src/tools/bash.ts +450 -0
- package/src/tools/cwd-state.ts +22 -0
- package/src/tools/edit.ts +428 -0
- package/src/tools/file-diff.ts +116 -0
- package/src/tools/glob.ts +202 -0
- package/src/tools/grep.ts +550 -0
- package/src/tools/hash.ts +9 -0
- package/src/tools/path-safety.ts +33 -0
- package/src/tools/read.ts +319 -0
- package/src/tools/recall.ts +400 -0
- package/src/tools/registry.ts +213 -0
- package/src/tools/ripgrep.ts +220 -0
- package/src/tools/task-list.ts +59 -0
- package/src/tools/task-output-snapshot.ts +47 -0
- package/src/tools/task-output-tool.ts +62 -0
- package/src/tools/task-output.ts +159 -0
- package/src/tools/task-stop.ts +59 -0
- package/src/tools/task-tool-args.ts +29 -0
- package/src/tools/types.ts +330 -0
- package/src/tools/web-fetch/backend.ts +27 -0
- package/src/tools/web-fetch/browser-backend.ts +126 -0
- package/src/tools/web-fetch/exa-backend.ts +172 -0
- package/src/tools/web-fetch/index.ts +298 -0
- package/src/tools/web-fetch/local-backend.ts +267 -0
- package/src/tools/web-fetch/refiner.ts +78 -0
- package/src/tools/web-fetch/route.ts +95 -0
- package/src/tools/web-search.ts +300 -0
- package/src/tools/write.ts +244 -0
- package/src/tui/app.tsx +497 -0
- package/src/tui/components/assistant-markdown.tsx +47 -0
- package/src/tui/components/background-tasks.tsx +92 -0
- package/src/tui/components/bash-result-view.tsx +47 -0
- package/src/tui/components/context-status.tsx +127 -0
- package/src/tui/components/diff-view.tsx +151 -0
- package/src/tui/components/file-viewer.tsx +212 -0
- package/src/tui/components/footer.tsx +60 -0
- package/src/tui/components/header.tsx +21 -0
- package/src/tui/components/model-picker.tsx +142 -0
- package/src/tui/components/prompt-input.tsx +432 -0
- package/src/tui/components/resume-session-picker.tsx +273 -0
- package/src/tui/components/timeline.tsx +121 -0
- package/src/tui/context-format.ts +24 -0
- package/src/tui/event-store.ts +865 -0
- package/src/tui/git-branch.ts +23 -0
- package/src/tui/line-editor.ts +157 -0
- package/src/tui/prompt-history.ts +94 -0
- package/src/tui/slash-commands.ts +126 -0
- package/src/tui/tui-projection-policy.ts +35 -0
- package/src/tui/tui-projection-store.ts +123 -0
- package/src/tui/tui-session-controller.ts +170 -0
- package/src/tui/view-file.ts +122 -0
|
@@ -0,0 +1,828 @@
|
|
|
1
|
+
import type { ContextRevisionId, RuntimeIdFactory, SessionId } from "../ids/runtime-id";
|
|
2
|
+
import type { ToolDefinition } from "../tools/types";
|
|
3
|
+
import type {
|
|
4
|
+
BuiltContextRequest,
|
|
5
|
+
StoredContextRevisionV5,
|
|
6
|
+
StoredContextSnapshotV5,
|
|
7
|
+
StoredSwapOverrideV5,
|
|
8
|
+
} from "../context/context-revision";
|
|
9
|
+
import {
|
|
10
|
+
ContextRevisionCompiler,
|
|
11
|
+
createInitialContextRevision,
|
|
12
|
+
} from "../context/context-revision-compiler";
|
|
13
|
+
import {
|
|
14
|
+
ContextProtocolError,
|
|
15
|
+
ContextProtocolValidator,
|
|
16
|
+
} from "../context/context-protocol-validator";
|
|
17
|
+
import {
|
|
18
|
+
TOOL_OBSERVATION_FORMAT,
|
|
19
|
+
contentHash,
|
|
20
|
+
immutableCanonicalClone,
|
|
21
|
+
immutableRecord,
|
|
22
|
+
observationForCompletion,
|
|
23
|
+
rawResultHash,
|
|
24
|
+
type CanonicalMessageRecord,
|
|
25
|
+
type ProtocolContextView,
|
|
26
|
+
type ProtocolFrame,
|
|
27
|
+
type ToolCompletion,
|
|
28
|
+
type ToolCompletionInput,
|
|
29
|
+
type ToolResultRecord,
|
|
30
|
+
} from "../context/protocol-frame";
|
|
31
|
+
import { ContextBuilder } from "./context-builder";
|
|
32
|
+
import type {
|
|
33
|
+
AssistantMessage,
|
|
34
|
+
IterationIdentity,
|
|
35
|
+
RunAgentResult,
|
|
36
|
+
ToolCall,
|
|
37
|
+
TurnIdentity,
|
|
38
|
+
} from "./types";
|
|
39
|
+
|
|
40
|
+
export type SessionLedger = {
|
|
41
|
+
beginTurn(input: { turn: TurnIdentity; userPrompt: string }): PendingLedgerTurn;
|
|
42
|
+
buildCommittedModelRequest(tools: readonly ToolDefinition[]): BuiltContextRequest;
|
|
43
|
+
buildCandidateModelRequest(
|
|
44
|
+
userPrompt: string,
|
|
45
|
+
tools: readonly ToolDefinition[],
|
|
46
|
+
): BuiltContextRequest;
|
|
47
|
+
committedMessageCount(): number;
|
|
48
|
+
snapshot(options?: {
|
|
49
|
+
fullIntegrity?: boolean;
|
|
50
|
+
allowOpenTail?: boolean;
|
|
51
|
+
allowFaulted?: boolean;
|
|
52
|
+
}): ProtocolContextView;
|
|
53
|
+
fault(error: unknown): void;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type PendingLedgerTurn = {
|
|
57
|
+
readonly agent: AgentTurnLedger;
|
|
58
|
+
projectedMessageCount(): number;
|
|
59
|
+
finish(result: RunAgentResult): void;
|
|
60
|
+
fault(error: unknown): void;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type AgentTurnLedger = {
|
|
64
|
+
appendAssistant(input: {
|
|
65
|
+
iteration: IterationIdentity;
|
|
66
|
+
message: AssistantMessage;
|
|
67
|
+
provider: string;
|
|
68
|
+
model: string;
|
|
69
|
+
}): void;
|
|
70
|
+
assertCanExecuteTool(call: ToolCall): void;
|
|
71
|
+
commitToolCompletions(completions: readonly ToolCompletionInput[]): void;
|
|
72
|
+
buildModelRequest(tools: readonly ToolDefinition[]): BuiltContextRequest;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export type LedgerMutation =
|
|
76
|
+
| {
|
|
77
|
+
kind: "begin_turn";
|
|
78
|
+
turn: TurnIdentity;
|
|
79
|
+
frame: ProtocolFrame;
|
|
80
|
+
message: CanonicalMessageRecord;
|
|
81
|
+
next: ProtocolContextView;
|
|
82
|
+
}
|
|
83
|
+
| {
|
|
84
|
+
kind: "append_assistant";
|
|
85
|
+
iteration: IterationIdentity;
|
|
86
|
+
frame: ProtocolFrame;
|
|
87
|
+
message: CanonicalMessageRecord;
|
|
88
|
+
next: ProtocolContextView;
|
|
89
|
+
}
|
|
90
|
+
| {
|
|
91
|
+
kind: "commit_tool_completions";
|
|
92
|
+
frameBefore: ProtocolFrame;
|
|
93
|
+
frameAfter: ProtocolFrame;
|
|
94
|
+
messages: readonly CanonicalMessageRecord[];
|
|
95
|
+
toolResults: readonly ToolResultRecord[];
|
|
96
|
+
next: ProtocolContextView;
|
|
97
|
+
}
|
|
98
|
+
| {
|
|
99
|
+
kind: "finish_turn";
|
|
100
|
+
turn: TurnIdentity;
|
|
101
|
+
result: RunAgentResult;
|
|
102
|
+
finalMessageId?: CanonicalMessageRecord["messageId"];
|
|
103
|
+
next: ProtocolContextView;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export type SessionLedgerCommitter = {
|
|
107
|
+
commit(mutation: LedgerMutation): void;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export class SessionLedgerWriteError extends Error {
|
|
111
|
+
readonly operation: LedgerMutation["kind"];
|
|
112
|
+
|
|
113
|
+
constructor(operation: LedgerMutation["kind"], options?: ErrorOptions) {
|
|
114
|
+
super(`Session ledger ${operation} commit failed.`, options);
|
|
115
|
+
this.name = "SessionLedgerWriteError";
|
|
116
|
+
this.operation = operation;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export type CreateInMemorySessionLedgerInput = {
|
|
121
|
+
sessionId: SessionId;
|
|
122
|
+
idFactory: RuntimeIdFactory;
|
|
123
|
+
systemPrompt?: string;
|
|
124
|
+
initialView?: ProtocolContextView;
|
|
125
|
+
initialSnapshot?: StoredContextSnapshotV5;
|
|
126
|
+
initialRevisionId?: ContextRevisionId;
|
|
127
|
+
contextBuilder?: ContextBuilder;
|
|
128
|
+
revisionCompiler?: ContextRevisionCompiler;
|
|
129
|
+
clock?: () => string;
|
|
130
|
+
committer?: SessionLedgerCommitter;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
type PendingState = "open" | "finished" | "faulted";
|
|
134
|
+
|
|
135
|
+
export class InMemorySessionLedger implements SessionLedger {
|
|
136
|
+
private view: ProtocolContextView;
|
|
137
|
+
private pending?: InMemoryPendingLedgerTurn;
|
|
138
|
+
private readonly validator = new ContextProtocolValidator();
|
|
139
|
+
private readonly contextBuilder: ContextBuilder;
|
|
140
|
+
private readonly revisionCompiler: ContextRevisionCompiler;
|
|
141
|
+
private readonly revision: StoredContextRevisionV5;
|
|
142
|
+
private readonly activeOverrides: readonly StoredSwapOverrideV5[];
|
|
143
|
+
private readonly clock: () => string;
|
|
144
|
+
|
|
145
|
+
constructor(private readonly input: CreateInMemorySessionLedgerInput) {
|
|
146
|
+
this.contextBuilder = input.contextBuilder ?? new ContextBuilder();
|
|
147
|
+
this.revisionCompiler = input.revisionCompiler ?? new ContextRevisionCompiler();
|
|
148
|
+
this.clock = input.clock ?? (() => new Date().toISOString());
|
|
149
|
+
const sourceCount = [
|
|
150
|
+
input.systemPrompt,
|
|
151
|
+
input.initialView,
|
|
152
|
+
input.initialSnapshot,
|
|
153
|
+
].filter((value) => value !== undefined).length;
|
|
154
|
+
if (sourceCount !== 1) {
|
|
155
|
+
throw new Error("Session ledger requires exactly one canonical history source.");
|
|
156
|
+
}
|
|
157
|
+
if (input.initialSnapshot !== undefined) {
|
|
158
|
+
if (input.initialRevisionId !== undefined) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
"Session ledger cannot override the revision in an initial snapshot.",
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
this.view = immutableView(input.initialSnapshot.canonical);
|
|
164
|
+
this.revision = input.initialSnapshot.revision;
|
|
165
|
+
this.activeOverrides = input.initialSnapshot.activeOverrides;
|
|
166
|
+
} else {
|
|
167
|
+
this.view =
|
|
168
|
+
input.initialView === undefined
|
|
169
|
+
? createInitialView(
|
|
170
|
+
input.sessionId,
|
|
171
|
+
input.systemPrompt!,
|
|
172
|
+
input.idFactory,
|
|
173
|
+
this.clock,
|
|
174
|
+
)
|
|
175
|
+
: immutableView(input.initialView);
|
|
176
|
+
this.revision = createInitialContextRevision({
|
|
177
|
+
revisionId:
|
|
178
|
+
input.initialRevisionId ?? input.idFactory.createContextRevisionId(),
|
|
179
|
+
canonical: this.view,
|
|
180
|
+
createdAt: this.view.frames[0]?.createdAt ?? this.clock(),
|
|
181
|
+
});
|
|
182
|
+
this.activeOverrides = Object.freeze([]);
|
|
183
|
+
}
|
|
184
|
+
if (
|
|
185
|
+
this.view.sessionId !== input.sessionId ||
|
|
186
|
+
this.revision.sessionId !== input.sessionId
|
|
187
|
+
) {
|
|
188
|
+
throw new Error("Session ledger history or revision belongs to another session.");
|
|
189
|
+
}
|
|
190
|
+
if (input.initialSnapshot === undefined) {
|
|
191
|
+
this.validator.validate(this.view, {
|
|
192
|
+
allowOpenTail: true,
|
|
193
|
+
fullIntegrity: true,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
beginTurn(input: { turn: TurnIdentity; userPrompt: string }): PendingLedgerTurn {
|
|
199
|
+
this.requireHealthy("begin a turn");
|
|
200
|
+
if (input.userPrompt.trim() === "") {
|
|
201
|
+
throw new Error("Cannot begin a ledger turn with an empty prompt.");
|
|
202
|
+
}
|
|
203
|
+
if (input.turn.sessionId !== this.input.sessionId) {
|
|
204
|
+
throw new Error(`Turn ${input.turn.turnId} belongs to another session.`);
|
|
205
|
+
}
|
|
206
|
+
if (this.pending !== undefined) {
|
|
207
|
+
throw new Error("Cannot begin a ledger turn while another turn is open.");
|
|
208
|
+
}
|
|
209
|
+
this.assertNoOpenFrame();
|
|
210
|
+
|
|
211
|
+
const createdAt = this.clock();
|
|
212
|
+
const ordinal = this.view.messages.length + 1;
|
|
213
|
+
const frameId = this.input.idFactory.createProtocolFrameId();
|
|
214
|
+
const message = immutableRecord<CanonicalMessageRecord>({
|
|
215
|
+
messageId: this.input.idFactory.createMessageId(),
|
|
216
|
+
sessionId: this.input.sessionId,
|
|
217
|
+
frameId,
|
|
218
|
+
ordinal,
|
|
219
|
+
contentSha256: contentHash(input.userPrompt),
|
|
220
|
+
createdAt,
|
|
221
|
+
role: "user",
|
|
222
|
+
turnId: input.turn.turnId,
|
|
223
|
+
content: input.userPrompt,
|
|
224
|
+
origin: "user",
|
|
225
|
+
});
|
|
226
|
+
const frame = immutableRecord<ProtocolFrame>({
|
|
227
|
+
frameId,
|
|
228
|
+
sessionId: this.input.sessionId,
|
|
229
|
+
turnId: input.turn.turnId,
|
|
230
|
+
kind: "user",
|
|
231
|
+
state: "closed",
|
|
232
|
+
firstOrdinal: ordinal,
|
|
233
|
+
lastOrdinal: ordinal,
|
|
234
|
+
createdAt,
|
|
235
|
+
closedAt: createdAt,
|
|
236
|
+
});
|
|
237
|
+
const next = appendView(this.view, [frame], [message], []);
|
|
238
|
+
this.validator.validate(next, { fullIntegrity: true });
|
|
239
|
+
this.commit({ kind: "begin_turn", turn: input.turn, frame, message, next });
|
|
240
|
+
|
|
241
|
+
const pending = new InMemoryPendingLedgerTurn(this, input.turn);
|
|
242
|
+
this.pending = pending;
|
|
243
|
+
return pending;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
buildCommittedModelRequest(tools: readonly ToolDefinition[]): BuiltContextRequest {
|
|
247
|
+
this.requireHealthy("build committed context");
|
|
248
|
+
if (this.pending !== undefined) {
|
|
249
|
+
throw new Error("Cannot build committed context while a turn is open.");
|
|
250
|
+
}
|
|
251
|
+
return this.buildRequest(tools);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
buildCandidateModelRequest(
|
|
255
|
+
userPrompt: string,
|
|
256
|
+
tools: readonly ToolDefinition[],
|
|
257
|
+
): BuiltContextRequest {
|
|
258
|
+
this.requireHealthy("build candidate context");
|
|
259
|
+
if (this.pending !== undefined) {
|
|
260
|
+
throw new Error("Cannot build candidate context while a turn is open.");
|
|
261
|
+
}
|
|
262
|
+
return this.buildRequest(tools, userPrompt);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
committedMessageCount(): number {
|
|
266
|
+
return this.view.messages.length;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
snapshot(
|
|
270
|
+
options: {
|
|
271
|
+
fullIntegrity?: boolean;
|
|
272
|
+
allowOpenTail?: boolean;
|
|
273
|
+
allowFaulted?: boolean;
|
|
274
|
+
} = {},
|
|
275
|
+
): ProtocolContextView {
|
|
276
|
+
const validationView =
|
|
277
|
+
options.allowFaulted === true && this.view.faulted
|
|
278
|
+
? { ...this.view, faulted: false }
|
|
279
|
+
: this.view;
|
|
280
|
+
this.validator.validate(validationView, {
|
|
281
|
+
fullIntegrity: options.fullIntegrity,
|
|
282
|
+
allowOpenTail: options.allowOpenTail,
|
|
283
|
+
});
|
|
284
|
+
return this.view;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
fault(error?: unknown): void {
|
|
288
|
+
void error;
|
|
289
|
+
if (!this.view.faulted) {
|
|
290
|
+
this.view = immutableView({ ...this.view, faulted: true });
|
|
291
|
+
}
|
|
292
|
+
this.pending?.markFaulted();
|
|
293
|
+
this.pending = undefined;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
appendAssistant(
|
|
297
|
+
pending: InMemoryPendingLedgerTurn,
|
|
298
|
+
input: {
|
|
299
|
+
iteration: IterationIdentity;
|
|
300
|
+
message: AssistantMessage;
|
|
301
|
+
provider: string;
|
|
302
|
+
model: string;
|
|
303
|
+
},
|
|
304
|
+
): void {
|
|
305
|
+
this.requirePending(pending, "append an assistant message");
|
|
306
|
+
requireIterationInTurn(input.iteration, pending.turn);
|
|
307
|
+
requireNonEmpty(input.provider, "assistant provider");
|
|
308
|
+
requireNonEmpty(input.model, "assistant model");
|
|
309
|
+
this.assertNoOpenFrame();
|
|
310
|
+
|
|
311
|
+
const content = input.message.content ?? null;
|
|
312
|
+
const toolCalls = normalizeToolCalls(input.message.toolCalls);
|
|
313
|
+
if ((content === null || content.trim() === "") && toolCalls === undefined) {
|
|
314
|
+
throw new ContextProtocolError(
|
|
315
|
+
"invalid_assistant_frame",
|
|
316
|
+
"Assistant message must contain non-empty text or tool calls.",
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
const createdAt = this.clock();
|
|
320
|
+
const ordinal = this.view.messages.length + 1;
|
|
321
|
+
const frameId = this.input.idFactory.createProtocolFrameId();
|
|
322
|
+
const message = immutableRecord<CanonicalMessageRecord>({
|
|
323
|
+
messageId: this.input.idFactory.createMessageId(),
|
|
324
|
+
sessionId: this.input.sessionId,
|
|
325
|
+
frameId,
|
|
326
|
+
ordinal,
|
|
327
|
+
contentSha256: contentHash(content),
|
|
328
|
+
createdAt,
|
|
329
|
+
role: "assistant",
|
|
330
|
+
turnId: pending.turn.turnId,
|
|
331
|
+
iterationId: input.iteration.iterationId,
|
|
332
|
+
content,
|
|
333
|
+
...(input.message.reasoningContent === undefined
|
|
334
|
+
? {}
|
|
335
|
+
: { reasoningContent: input.message.reasoningContent }),
|
|
336
|
+
...(toolCalls === undefined ? {} : { toolCalls }),
|
|
337
|
+
provider: input.provider,
|
|
338
|
+
model: input.model,
|
|
339
|
+
origin: "model",
|
|
340
|
+
});
|
|
341
|
+
const frame = immutableRecord<ProtocolFrame>({
|
|
342
|
+
frameId,
|
|
343
|
+
sessionId: this.input.sessionId,
|
|
344
|
+
turnId: pending.turn.turnId,
|
|
345
|
+
iterationId: input.iteration.iterationId,
|
|
346
|
+
kind: toolCalls === undefined ? "assistant_text" : "tool_exchange",
|
|
347
|
+
state: toolCalls === undefined ? "closed" : "open",
|
|
348
|
+
firstOrdinal: ordinal,
|
|
349
|
+
...(toolCalls === undefined ? { lastOrdinal: ordinal, closedAt: createdAt } : {}),
|
|
350
|
+
createdAt,
|
|
351
|
+
});
|
|
352
|
+
const next = appendView(this.view, [frame], [message], []);
|
|
353
|
+
this.validator.validate(next, {
|
|
354
|
+
allowOpenTail: toolCalls !== undefined,
|
|
355
|
+
fullIntegrity: true,
|
|
356
|
+
});
|
|
357
|
+
this.commit({
|
|
358
|
+
kind: "append_assistant",
|
|
359
|
+
iteration: input.iteration,
|
|
360
|
+
frame,
|
|
361
|
+
message,
|
|
362
|
+
next,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
assertCanExecuteTool(pending: InMemoryPendingLedgerTurn, call: ToolCall): void {
|
|
367
|
+
this.requirePending(pending, "execute a tool");
|
|
368
|
+
const expected = this.expectedToolCall();
|
|
369
|
+
if (expected === undefined) {
|
|
370
|
+
throw new Error("Cannot execute a tool without an open tool exchange.");
|
|
371
|
+
}
|
|
372
|
+
assertSameToolCall(expected, call);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
commitToolCompletions(
|
|
376
|
+
pending: InMemoryPendingLedgerTurn,
|
|
377
|
+
completions: readonly ToolCompletionInput[],
|
|
378
|
+
): void {
|
|
379
|
+
this.requirePending(pending, "commit tool completions");
|
|
380
|
+
if (completions.length === 0) {
|
|
381
|
+
throw new Error("Tool completion batch must not be empty.");
|
|
382
|
+
}
|
|
383
|
+
const frameBefore = this.openFrame();
|
|
384
|
+
const assistant = this.assistantForFrame(frameBefore);
|
|
385
|
+
const calls = assistant.toolCalls!;
|
|
386
|
+
const completedCount = this.messagesForFrame(frameBefore).length - 1;
|
|
387
|
+
if (completedCount + completions.length > calls.length) {
|
|
388
|
+
throw new Error(
|
|
389
|
+
`Tool completion batch exceeds remaining calls in frame ${frameBefore.frameId}.`,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const messages: CanonicalMessageRecord[] = [];
|
|
394
|
+
const toolResults: ToolResultRecord[] = [];
|
|
395
|
+
for (let index = 0; index < completions.length; index += 1) {
|
|
396
|
+
const completionInput = requireItem(completions, index, "tool completion");
|
|
397
|
+
const expectedCall = requireItem(
|
|
398
|
+
calls,
|
|
399
|
+
completedCount + index,
|
|
400
|
+
"expected tool call",
|
|
401
|
+
);
|
|
402
|
+
assertSameToolCall(expectedCall, completionInput.call);
|
|
403
|
+
validateCompletionInput(completionInput);
|
|
404
|
+
const content = observationForCompletion(completionInput);
|
|
405
|
+
const createdAt = this.clock();
|
|
406
|
+
const messageId = this.input.idFactory.createMessageId();
|
|
407
|
+
const message = immutableRecord<CanonicalMessageRecord>({
|
|
408
|
+
messageId,
|
|
409
|
+
sessionId: this.input.sessionId,
|
|
410
|
+
frameId: frameBefore.frameId,
|
|
411
|
+
ordinal: this.view.messages.length + messages.length + 1,
|
|
412
|
+
contentSha256: contentHash(content),
|
|
413
|
+
createdAt,
|
|
414
|
+
role: "tool",
|
|
415
|
+
turnId: pending.turn.turnId,
|
|
416
|
+
iterationId: expectedCall.iterationId,
|
|
417
|
+
toolCallId: expectedCall.toolCallId,
|
|
418
|
+
providerToolCallId: expectedCall.providerToolCallId,
|
|
419
|
+
name: expectedCall.name,
|
|
420
|
+
content,
|
|
421
|
+
origin: completionInput.kind === "returned" ? "tool" : "runtime",
|
|
422
|
+
});
|
|
423
|
+
const completion = canonicalCompletion(completionInput);
|
|
424
|
+
const result = immutableRecord<ToolResultRecord>({
|
|
425
|
+
sessionId: this.input.sessionId,
|
|
426
|
+
frameId: frameBefore.frameId,
|
|
427
|
+
toolCallId: expectedCall.toolCallId,
|
|
428
|
+
toolMessageId: messageId,
|
|
429
|
+
completion,
|
|
430
|
+
observationSha256: contentHash(content),
|
|
431
|
+
createdAt,
|
|
432
|
+
});
|
|
433
|
+
messages.push(message);
|
|
434
|
+
toolResults.push(result);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const closesFrame = completedCount + completions.length === calls.length;
|
|
438
|
+
const closedAt = closesFrame ? this.clock() : undefined;
|
|
439
|
+
const frameAfter = immutableRecord<ProtocolFrame>({
|
|
440
|
+
...frameBefore,
|
|
441
|
+
state: closesFrame ? "closed" : "open",
|
|
442
|
+
...(closesFrame
|
|
443
|
+
? {
|
|
444
|
+
lastOrdinal: this.view.messages.length + messages.length,
|
|
445
|
+
closedAt,
|
|
446
|
+
}
|
|
447
|
+
: {}),
|
|
448
|
+
});
|
|
449
|
+
const next = replaceFrameAndAppend(this.view, frameAfter, messages, toolResults);
|
|
450
|
+
this.validator.validate(next, {
|
|
451
|
+
allowOpenTail: !closesFrame,
|
|
452
|
+
fullIntegrity: true,
|
|
453
|
+
});
|
|
454
|
+
this.commit({
|
|
455
|
+
kind: "commit_tool_completions",
|
|
456
|
+
frameBefore,
|
|
457
|
+
frameAfter,
|
|
458
|
+
messages: Object.freeze(messages),
|
|
459
|
+
toolResults: Object.freeze(toolResults),
|
|
460
|
+
next,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
buildTurnModelRequest(
|
|
465
|
+
pending: InMemoryPendingLedgerTurn,
|
|
466
|
+
tools: readonly ToolDefinition[],
|
|
467
|
+
): BuiltContextRequest {
|
|
468
|
+
this.requirePending(pending, "build a model request");
|
|
469
|
+
return this.buildRequest(tools);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
finishTurn(pending: InMemoryPendingLedgerTurn, result: RunAgentResult): void {
|
|
473
|
+
this.requirePending(pending, "finish a turn");
|
|
474
|
+
this.assertNoOpenFrame();
|
|
475
|
+
requireIterationInTurn(result.lastIteration, pending.turn);
|
|
476
|
+
const finalMessage =
|
|
477
|
+
result.status === "completed"
|
|
478
|
+
? [...this.view.messages]
|
|
479
|
+
.reverse()
|
|
480
|
+
.find(
|
|
481
|
+
(message) =>
|
|
482
|
+
message.role === "assistant" &&
|
|
483
|
+
message.turnId === pending.turn.turnId &&
|
|
484
|
+
(message.toolCalls?.length ?? 0) === 0,
|
|
485
|
+
)
|
|
486
|
+
: undefined;
|
|
487
|
+
const mutation: LedgerMutation = {
|
|
488
|
+
kind: "finish_turn",
|
|
489
|
+
turn: pending.turn,
|
|
490
|
+
result,
|
|
491
|
+
...(finalMessage === undefined ? {} : { finalMessageId: finalMessage.messageId }),
|
|
492
|
+
next: this.view,
|
|
493
|
+
};
|
|
494
|
+
this.commit(mutation);
|
|
495
|
+
pending.markFinished();
|
|
496
|
+
this.pending = undefined;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
faultTurn(pending: InMemoryPendingLedgerTurn, error: unknown): void {
|
|
500
|
+
if (this.pending !== pending) {
|
|
501
|
+
throw new Error("Cannot fault a turn: ledger turn ownership was lost.");
|
|
502
|
+
}
|
|
503
|
+
pending.requireOpen("fault a turn");
|
|
504
|
+
pending.markFaulted();
|
|
505
|
+
this.pending = undefined;
|
|
506
|
+
this.fault(error);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
private buildRequest(
|
|
510
|
+
tools: readonly ToolDefinition[],
|
|
511
|
+
candidateUserPrompt?: string,
|
|
512
|
+
): BuiltContextRequest {
|
|
513
|
+
try {
|
|
514
|
+
const canonical = this.view;
|
|
515
|
+
const compiled = this.revisionCompiler.compileActive(
|
|
516
|
+
snapshotFor(canonical, this.revision, this.activeOverrides),
|
|
517
|
+
);
|
|
518
|
+
return this.contextBuilder.build({
|
|
519
|
+
canonical,
|
|
520
|
+
revision: this.revision,
|
|
521
|
+
activeOverrides: this.activeOverrides,
|
|
522
|
+
compiled,
|
|
523
|
+
tools,
|
|
524
|
+
...(candidateUserPrompt === undefined ? {} : { candidateUserPrompt }),
|
|
525
|
+
});
|
|
526
|
+
} catch (error) {
|
|
527
|
+
if (error instanceof ContextProtocolError && error.code !== "open_frame") {
|
|
528
|
+
this.fault(error);
|
|
529
|
+
}
|
|
530
|
+
throw error;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
private commit(mutation: LedgerMutation): void {
|
|
535
|
+
try {
|
|
536
|
+
this.input.committer?.commit(mutation);
|
|
537
|
+
} catch (error) {
|
|
538
|
+
this.view = immutableView({ ...this.view, faulted: true });
|
|
539
|
+
throw new SessionLedgerWriteError(mutation.kind, { cause: error });
|
|
540
|
+
}
|
|
541
|
+
this.view = mutation.next;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
private requirePending(pending: InMemoryPendingLedgerTurn, action: string): void {
|
|
545
|
+
this.requireHealthy(action);
|
|
546
|
+
if (this.pending !== pending) {
|
|
547
|
+
throw new Error(`Cannot ${action}: ledger turn ownership was lost.`);
|
|
548
|
+
}
|
|
549
|
+
pending.requireOpen(action);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
private requireHealthy(action: string): void {
|
|
553
|
+
if (this.view.faulted) {
|
|
554
|
+
throw new Error(`Cannot ${action} after the session ledger faulted.`);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
private assertNoOpenFrame(): void {
|
|
559
|
+
const open = this.view.frames.find((frame) => frame.state === "open");
|
|
560
|
+
if (open !== undefined) {
|
|
561
|
+
throw new ContextProtocolError("open_frame", `Frame ${open.frameId} is open.`, {
|
|
562
|
+
frameId: open.frameId,
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
private openFrame(): ProtocolFrame {
|
|
568
|
+
const frame = this.view.frames.at(-1);
|
|
569
|
+
if (frame === undefined || frame.state !== "open") {
|
|
570
|
+
throw new Error("Session ledger has no open protocol frame.");
|
|
571
|
+
}
|
|
572
|
+
return frame;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
private assistantForFrame(
|
|
576
|
+
frame: ProtocolFrame,
|
|
577
|
+
): Extract<CanonicalMessageRecord, { role: "assistant" }> {
|
|
578
|
+
const assistant = this.messagesForFrame(frame)[0];
|
|
579
|
+
if (assistant?.role !== "assistant" || assistant.toolCalls === undefined) {
|
|
580
|
+
throw new Error(`Open frame ${frame.frameId} has no assistant tool calls.`);
|
|
581
|
+
}
|
|
582
|
+
return assistant;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
private messagesForFrame(frame: ProtocolFrame): CanonicalMessageRecord[] {
|
|
586
|
+
return this.view.messages.filter((message) => message.frameId === frame.frameId);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
private expectedToolCall(): ToolCall | undefined {
|
|
590
|
+
const frame = this.view.frames.at(-1);
|
|
591
|
+
if (frame?.state !== "open") {
|
|
592
|
+
return undefined;
|
|
593
|
+
}
|
|
594
|
+
const assistant = this.assistantForFrame(frame);
|
|
595
|
+
return assistant.toolCalls?.[this.messagesForFrame(frame).length - 1];
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function snapshotFor(
|
|
600
|
+
canonical: ProtocolContextView,
|
|
601
|
+
revision: StoredContextRevisionV5,
|
|
602
|
+
activeOverrides: readonly StoredSwapOverrideV5[],
|
|
603
|
+
): StoredContextSnapshotV5 {
|
|
604
|
+
return Object.freeze({
|
|
605
|
+
meta: Object.freeze({
|
|
606
|
+
sessionId: canonical.sessionId,
|
|
607
|
+
activeRevisionId: revision.revisionId,
|
|
608
|
+
}),
|
|
609
|
+
revision,
|
|
610
|
+
activeOverrides,
|
|
611
|
+
canonical,
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
class InMemoryPendingLedgerTurn implements PendingLedgerTurn {
|
|
616
|
+
readonly agent: AgentTurnLedger;
|
|
617
|
+
private state: PendingState = "open";
|
|
618
|
+
|
|
619
|
+
constructor(
|
|
620
|
+
private readonly ledger: InMemorySessionLedger,
|
|
621
|
+
readonly turn: TurnIdentity,
|
|
622
|
+
) {
|
|
623
|
+
this.agent = {
|
|
624
|
+
appendAssistant: (input) => this.ledger.appendAssistant(this, input),
|
|
625
|
+
assertCanExecuteTool: (call) => this.ledger.assertCanExecuteTool(this, call),
|
|
626
|
+
commitToolCompletions: (completions) =>
|
|
627
|
+
this.ledger.commitToolCompletions(this, completions),
|
|
628
|
+
buildModelRequest: (tools) => this.ledger.buildTurnModelRequest(this, tools),
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
projectedMessageCount(): number {
|
|
633
|
+
this.requireOpen("read projected message count");
|
|
634
|
+
return this.ledger.committedMessageCount();
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
finish(result: RunAgentResult): void {
|
|
638
|
+
this.ledger.finishTurn(this, result);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
fault(error: unknown): void {
|
|
642
|
+
this.ledger.faultTurn(this, error);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
requireOpen(action: string): void {
|
|
646
|
+
if (this.state !== "open") {
|
|
647
|
+
throw new Error(`Cannot ${action} after pending ledger turn was ${this.state}.`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
markFinished(): void {
|
|
652
|
+
this.requireOpen("finish");
|
|
653
|
+
this.state = "finished";
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
markFaulted(): void {
|
|
657
|
+
if (this.state === "open") {
|
|
658
|
+
this.state = "faulted";
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function createInitialView(
|
|
664
|
+
sessionId: SessionId,
|
|
665
|
+
systemPrompt: string,
|
|
666
|
+
idFactory: RuntimeIdFactory,
|
|
667
|
+
clock: () => string,
|
|
668
|
+
): ProtocolContextView {
|
|
669
|
+
if (systemPrompt.trim() === "") {
|
|
670
|
+
throw new Error("Session ledger system prompt must not be empty.");
|
|
671
|
+
}
|
|
672
|
+
const createdAt = clock();
|
|
673
|
+
const frameId = idFactory.createProtocolFrameId();
|
|
674
|
+
const message = immutableRecord<CanonicalMessageRecord>({
|
|
675
|
+
messageId: idFactory.createMessageId(),
|
|
676
|
+
sessionId,
|
|
677
|
+
frameId,
|
|
678
|
+
ordinal: 1,
|
|
679
|
+
contentSha256: contentHash(systemPrompt),
|
|
680
|
+
createdAt,
|
|
681
|
+
role: "system",
|
|
682
|
+
content: systemPrompt,
|
|
683
|
+
origin: "runtime",
|
|
684
|
+
});
|
|
685
|
+
const frame = immutableRecord<ProtocolFrame>({
|
|
686
|
+
frameId,
|
|
687
|
+
sessionId,
|
|
688
|
+
kind: "system",
|
|
689
|
+
state: "closed",
|
|
690
|
+
firstOrdinal: 1,
|
|
691
|
+
lastOrdinal: 1,
|
|
692
|
+
createdAt,
|
|
693
|
+
closedAt: createdAt,
|
|
694
|
+
});
|
|
695
|
+
return immutableView({
|
|
696
|
+
sessionId,
|
|
697
|
+
faulted: false,
|
|
698
|
+
frames: [frame],
|
|
699
|
+
messages: [message],
|
|
700
|
+
toolResults: [],
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function appendView(
|
|
705
|
+
view: ProtocolContextView,
|
|
706
|
+
frames: readonly ProtocolFrame[],
|
|
707
|
+
messages: readonly CanonicalMessageRecord[],
|
|
708
|
+
toolResults: readonly ToolResultRecord[],
|
|
709
|
+
): ProtocolContextView {
|
|
710
|
+
return immutableView({
|
|
711
|
+
...view,
|
|
712
|
+
frames: [...view.frames, ...frames],
|
|
713
|
+
messages: [...view.messages, ...messages],
|
|
714
|
+
toolResults: [...view.toolResults, ...toolResults],
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function replaceFrameAndAppend(
|
|
719
|
+
view: ProtocolContextView,
|
|
720
|
+
frame: ProtocolFrame,
|
|
721
|
+
messages: readonly CanonicalMessageRecord[],
|
|
722
|
+
toolResults: readonly ToolResultRecord[],
|
|
723
|
+
): ProtocolContextView {
|
|
724
|
+
return immutableView({
|
|
725
|
+
...view,
|
|
726
|
+
frames: view.frames.map((current) =>
|
|
727
|
+
current.frameId === frame.frameId ? frame : current,
|
|
728
|
+
),
|
|
729
|
+
messages: [...view.messages, ...messages],
|
|
730
|
+
toolResults: [...view.toolResults, ...toolResults],
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function immutableView(view: ProtocolContextView): ProtocolContextView {
|
|
735
|
+
return Object.freeze({
|
|
736
|
+
sessionId: view.sessionId,
|
|
737
|
+
faulted: view.faulted,
|
|
738
|
+
frames: Object.freeze([...view.frames]),
|
|
739
|
+
messages: Object.freeze([...view.messages]),
|
|
740
|
+
toolResults: Object.freeze([...view.toolResults]),
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function normalizeToolCalls(
|
|
745
|
+
calls: AssistantMessage["toolCalls"],
|
|
746
|
+
): readonly ToolCall[] | undefined {
|
|
747
|
+
if (calls === undefined || calls.length === 0) {
|
|
748
|
+
return undefined;
|
|
749
|
+
}
|
|
750
|
+
return Object.freeze(immutableCanonicalClone(calls));
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function canonicalCompletion(input: ToolCompletionInput): ToolCompletion {
|
|
754
|
+
if (input.kind === "synthetic") {
|
|
755
|
+
return immutableRecord({
|
|
756
|
+
kind: "synthetic",
|
|
757
|
+
reason: input.reason,
|
|
758
|
+
...(input.detail === undefined ? {} : { detail: input.detail }),
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
const raw = immutableCanonicalClone(input.raw);
|
|
762
|
+
return immutableRecord({
|
|
763
|
+
kind: "returned",
|
|
764
|
+
raw,
|
|
765
|
+
rawSha256: rawResultHash(raw),
|
|
766
|
+
observationFormat: TOOL_OBSERVATION_FORMAT,
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function validateCompletionInput(input: ToolCompletionInput): void {
|
|
771
|
+
if (input.kind === "returned") {
|
|
772
|
+
if (typeof input.observation !== "string") {
|
|
773
|
+
throw new Error("Returned tool completion observation must be a string.");
|
|
774
|
+
}
|
|
775
|
+
immutableCanonicalClone(input.raw);
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
if (input.reason === "failed_active") {
|
|
779
|
+
requireNonEmpty(input.detail ?? "", "failed_active detail");
|
|
780
|
+
} else if (input.detail !== undefined) {
|
|
781
|
+
throw new Error(`Synthetic reason ${input.reason} does not accept detail.`);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function requireIterationInTurn(
|
|
786
|
+
iteration: IterationIdentity,
|
|
787
|
+
turn: TurnIdentity,
|
|
788
|
+
): void {
|
|
789
|
+
if (
|
|
790
|
+
iteration.sessionId !== turn.sessionId ||
|
|
791
|
+
iteration.turnId !== turn.turnId ||
|
|
792
|
+
iteration.turnNumber !== turn.turnNumber
|
|
793
|
+
) {
|
|
794
|
+
throw new Error(
|
|
795
|
+
`Iteration ${iteration.iterationId} does not belong to turn ${turn.turnId}.`,
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function assertSameToolCall(expected: ToolCall, actual: ToolCall): void {
|
|
801
|
+
if (
|
|
802
|
+
actual.toolCallId !== expected.toolCallId ||
|
|
803
|
+
actual.sessionId !== expected.sessionId ||
|
|
804
|
+
actual.turnId !== expected.turnId ||
|
|
805
|
+
actual.iterationId !== expected.iterationId ||
|
|
806
|
+
actual.toolCallNumber !== expected.toolCallNumber ||
|
|
807
|
+
actual.providerToolCallId !== expected.providerToolCallId ||
|
|
808
|
+
actual.name !== expected.name
|
|
809
|
+
) {
|
|
810
|
+
throw new Error(
|
|
811
|
+
`Tool call ${actual.toolCallId} does not match expected call ${expected.toolCallId}.`,
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function requireNonEmpty(value: string, name: string): void {
|
|
817
|
+
if (value.trim() === "") {
|
|
818
|
+
throw new Error(`${name} must not be empty.`);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function requireItem<T>(items: readonly T[], index: number, name: string): T {
|
|
823
|
+
const item = items[index];
|
|
824
|
+
if (item === undefined) {
|
|
825
|
+
throw new Error(`Missing ${name} at index ${index}.`);
|
|
826
|
+
}
|
|
827
|
+
return item;
|
|
828
|
+
}
|