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,1212 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { CompositeEventSink } from "../events/composite-event-sink";
|
|
3
|
+
import type { EventSink } from "../events/event-sink";
|
|
4
|
+
import { JsonlEventLog } from "../events/jsonl-event-log";
|
|
5
|
+
import { ObservationTextLog } from "../events/observation-text-log";
|
|
6
|
+
import type {
|
|
7
|
+
AgentEvent,
|
|
8
|
+
AgentEventInput,
|
|
9
|
+
AgentEventType,
|
|
10
|
+
ContextRevisionFinishedData,
|
|
11
|
+
} from "../events/types";
|
|
12
|
+
import {
|
|
13
|
+
runtimeIdFactory,
|
|
14
|
+
type RuntimeIdFactory,
|
|
15
|
+
type SessionId,
|
|
16
|
+
} from "../ids/runtime-id";
|
|
17
|
+
import { loadMcpConfig } from "../mcp/mcp-config";
|
|
18
|
+
import { createMcpManager, type McpManager } from "../mcp/mcp-manager";
|
|
19
|
+
import type { ModelClient } from "../model/model-client";
|
|
20
|
+
import { CommittedPrefixAuditor } from "../model/committed-prefix-auditor";
|
|
21
|
+
import { SwapPlanner } from "../context/swap-planner";
|
|
22
|
+
import {
|
|
23
|
+
ContextManager,
|
|
24
|
+
ContextManagerError,
|
|
25
|
+
type ContextCompactionResult,
|
|
26
|
+
type ContextCompactionTrigger,
|
|
27
|
+
} from "../context/context-manager";
|
|
28
|
+
import {
|
|
29
|
+
assertMatchingContextBudget,
|
|
30
|
+
type ModelContextBudget,
|
|
31
|
+
type ModelContextProfile,
|
|
32
|
+
} from "../model/model-context-profile";
|
|
33
|
+
import { ObservationBuilder } from "../observation/observation-builder";
|
|
34
|
+
import { ContextProtocolError } from "../context/context-protocol-validator";
|
|
35
|
+
import { CompiledContextError } from "../context/compiled-context-validator";
|
|
36
|
+
import { ContextRevisionError } from "../context/context-revision-compiler";
|
|
37
|
+
import { createDefaultTooling, type DefaultTooling } from "../tools/registry";
|
|
38
|
+
import type { Refiner } from "../tools/web-fetch/refiner";
|
|
39
|
+
import type { ProjectInstructionManifest } from "../instructions/project-instructions";
|
|
40
|
+
import {
|
|
41
|
+
SessionStore,
|
|
42
|
+
createRuntimeContract,
|
|
43
|
+
type SessionRecoveryResult,
|
|
44
|
+
} from "../session/session-store";
|
|
45
|
+
import { SqliteSessionLedger } from "../session/sqlite-session-ledger";
|
|
46
|
+
import { SessionError } from "../session/session-errors";
|
|
47
|
+
import { FatalAgentTurnError, runAgent, type RunAgentInput } from "./loop";
|
|
48
|
+
import { SessionLedgerWriteError, type SessionLedger } from "./session-ledger";
|
|
49
|
+
import { TurnCancelledError } from "./turn-cancellation";
|
|
50
|
+
import type {
|
|
51
|
+
IterationIdentity,
|
|
52
|
+
RunAgentResult,
|
|
53
|
+
ToolCallIdentity,
|
|
54
|
+
TurnIdentity,
|
|
55
|
+
} from "./types";
|
|
56
|
+
import { ContextMeter } from "./context-meter";
|
|
57
|
+
|
|
58
|
+
export type ExecuteTurnInput = {
|
|
59
|
+
userPrompt: string;
|
|
60
|
+
signal: AbortSignal;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type SessionDisposeReason =
|
|
64
|
+
| { type: "oneshot_complete" }
|
|
65
|
+
| { type: "tui_exit" }
|
|
66
|
+
| { type: "session_switch" }
|
|
67
|
+
| { type: "runner_failed"; error: string }
|
|
68
|
+
| { type: "initialization_failed"; error: string };
|
|
69
|
+
|
|
70
|
+
export type RuntimeSession = {
|
|
71
|
+
readonly sessionId: SessionId;
|
|
72
|
+
readonly resumed: boolean;
|
|
73
|
+
readonly recovery: SessionRecoveryResult;
|
|
74
|
+
executeTurn(input: ExecuteTurnInput): Promise<RunAgentResult>;
|
|
75
|
+
compactContext(): Promise<ContextCompactionResult>;
|
|
76
|
+
canSwitchSession(): boolean;
|
|
77
|
+
dispose(reason: SessionDisposeReason): Promise<void>;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export type RuntimeSessionContext = {
|
|
81
|
+
readonly sessionId: SessionId;
|
|
82
|
+
createIteration(turn: TurnIdentity, iterationNumber: number): IterationIdentity;
|
|
83
|
+
createToolCall(
|
|
84
|
+
iteration: IterationIdentity,
|
|
85
|
+
toolCallNumber: number,
|
|
86
|
+
): ToolCallIdentity;
|
|
87
|
+
finishIterationForContinuation(iteration: IterationIdentity): void;
|
|
88
|
+
append(input: AgentEventInput): Promise<void>;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
type CommonRuntimeSessionInput = {
|
|
92
|
+
workspaceRoot: string;
|
|
93
|
+
modelName: string;
|
|
94
|
+
profileName?: string;
|
|
95
|
+
maxIterations: number;
|
|
96
|
+
includeReasoningContent: boolean;
|
|
97
|
+
contextProfile: ModelContextProfile;
|
|
98
|
+
contextBudget: ModelContextBudget;
|
|
99
|
+
modelClient: ModelClient;
|
|
100
|
+
presentationSinks?: EventSink[];
|
|
101
|
+
persistence?:
|
|
102
|
+
| false
|
|
103
|
+
| {
|
|
104
|
+
eventLogPath?: string;
|
|
105
|
+
observationLogPath?: string;
|
|
106
|
+
};
|
|
107
|
+
webFetchRefiner?: Refiner;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
type CreateNewRuntimeSessionInput = CommonRuntimeSessionInput & {
|
|
111
|
+
selection: { mode: "new"; sessionId: SessionId };
|
|
112
|
+
systemPrompt: string;
|
|
113
|
+
projectInstruction?: ProjectInstructionManifest;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
type ResumeRuntimeSessionInput = CommonRuntimeSessionInput & {
|
|
117
|
+
selection: { mode: "resume"; sessionId: SessionId };
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export type CreateRuntimeSessionInput =
|
|
121
|
+
| CreateNewRuntimeSessionInput
|
|
122
|
+
| ResumeRuntimeSessionInput;
|
|
123
|
+
|
|
124
|
+
export type RuntimeSessionFactoryDependencies = {
|
|
125
|
+
idFactory: RuntimeIdFactory;
|
|
126
|
+
createTooling: typeof createDefaultTooling;
|
|
127
|
+
loadMcpConfig: typeof loadMcpConfig;
|
|
128
|
+
createMcpManager: typeof createMcpManager;
|
|
129
|
+
createObservationBuilder: () => ObservationBuilder;
|
|
130
|
+
openStore: (
|
|
131
|
+
input: CreateRuntimeSessionInput,
|
|
132
|
+
idFactory: RuntimeIdFactory,
|
|
133
|
+
) => Promise<SessionStore>;
|
|
134
|
+
createLedger: (store: SessionStore, idFactory: RuntimeIdFactory) => SessionLedger;
|
|
135
|
+
createEventSink: (input: CreateRuntimeSessionInput) => EventSink;
|
|
136
|
+
selectShadowPlanning: NonNullable<RunAgentInput["shadowPlanning"]>["select"];
|
|
137
|
+
onShadowPlanningResult?: NonNullable<RunAgentInput["shadowPlanning"]>["onResult"];
|
|
138
|
+
manualCompactionTrigger: () => ContextCompactionTrigger;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export class RuntimeEventAppendError extends Error {
|
|
142
|
+
readonly eventType: AgentEventType;
|
|
143
|
+
|
|
144
|
+
constructor(eventType: AgentEventType, options?: ErrorOptions) {
|
|
145
|
+
super(`Failed to append runtime event ${eventType}.`, options);
|
|
146
|
+
this.name = "RuntimeEventAppendError";
|
|
147
|
+
this.eventType = eventType;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
type RuntimeSessionState =
|
|
152
|
+
| "initializing"
|
|
153
|
+
| "ready"
|
|
154
|
+
| "executing"
|
|
155
|
+
| "compacting"
|
|
156
|
+
| "faulted"
|
|
157
|
+
| "disposing"
|
|
158
|
+
| "disposed";
|
|
159
|
+
|
|
160
|
+
type ActiveTurn = {
|
|
161
|
+
controller: AbortController;
|
|
162
|
+
completion: Promise<RunAgentResult>;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const defaultDependencies: RuntimeSessionFactoryDependencies = {
|
|
166
|
+
idFactory: runtimeIdFactory,
|
|
167
|
+
createTooling: createDefaultTooling,
|
|
168
|
+
loadMcpConfig,
|
|
169
|
+
createMcpManager,
|
|
170
|
+
createObservationBuilder: () => new ObservationBuilder(),
|
|
171
|
+
openStore: (input, idFactory) =>
|
|
172
|
+
isNewSessionInput(input)
|
|
173
|
+
? SessionStore.createNew({
|
|
174
|
+
workspaceRoot: input.workspaceRoot,
|
|
175
|
+
sessionId: input.selection.sessionId,
|
|
176
|
+
modelName: input.modelName,
|
|
177
|
+
systemPrompt: input.systemPrompt,
|
|
178
|
+
projectInstruction: input.projectInstruction,
|
|
179
|
+
idFactory,
|
|
180
|
+
})
|
|
181
|
+
: SessionStore.openExisting({
|
|
182
|
+
workspaceRoot: input.workspaceRoot,
|
|
183
|
+
sessionId: input.selection.sessionId,
|
|
184
|
+
}),
|
|
185
|
+
createLedger: (store, idFactory) => new SqliteSessionLedger(store, idFactory),
|
|
186
|
+
createEventSink,
|
|
187
|
+
selectShadowPlanning: ({ preflight }) =>
|
|
188
|
+
preflight.pressure === "normal" ? undefined : { trigger: "runtime_pressure" },
|
|
189
|
+
manualCompactionTrigger: () => ({ kind: "manual" }),
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
class DefaultRuntimeSession implements RuntimeSession {
|
|
193
|
+
readonly sessionId: SessionId;
|
|
194
|
+
readonly resumed: boolean;
|
|
195
|
+
recovery: SessionRecoveryResult = {
|
|
196
|
+
syntheticCompletionCount: 0,
|
|
197
|
+
recallIndexRebuilt: false,
|
|
198
|
+
};
|
|
199
|
+
private state: RuntimeSessionState = "initializing";
|
|
200
|
+
private nextTurnNumber: number;
|
|
201
|
+
private readonly turns = new Map<string, TurnIdentity>();
|
|
202
|
+
private readonly iterations = new Map<string, IterationIdentity>();
|
|
203
|
+
private readonly toolCalls = new Map<string, ToolCallIdentity>();
|
|
204
|
+
private readonly nextIterationNumberByTurn = new Map<string, number>();
|
|
205
|
+
private readonly nextToolCallNumberByIteration = new Map<string, number>();
|
|
206
|
+
private eventTail: Promise<void> = Promise.resolve();
|
|
207
|
+
private tooling?: DefaultTooling;
|
|
208
|
+
private mcpManager?: McpManager;
|
|
209
|
+
private ledger?: SessionLedger;
|
|
210
|
+
private activeTurn?: ActiveTurn;
|
|
211
|
+
private activeCompaction?: Promise<ContextCompactionResult>;
|
|
212
|
+
private disposePromise?: Promise<void>;
|
|
213
|
+
private faultCause?: unknown;
|
|
214
|
+
private readonly contextMeter: ContextMeter;
|
|
215
|
+
private readonly committedPrefixAuditor = new CommittedPrefixAuditor();
|
|
216
|
+
private readonly shadowPlanner: SwapPlanner;
|
|
217
|
+
private contextManager?: ContextManager;
|
|
218
|
+
|
|
219
|
+
private readonly context: RuntimeSessionContext;
|
|
220
|
+
|
|
221
|
+
private constructor(
|
|
222
|
+
private readonly input: CreateRuntimeSessionInput,
|
|
223
|
+
private readonly dependencies: RuntimeSessionFactoryDependencies,
|
|
224
|
+
private readonly eventSink: EventSink,
|
|
225
|
+
private readonly observationBuilder: ObservationBuilder,
|
|
226
|
+
private readonly store: SessionStore,
|
|
227
|
+
) {
|
|
228
|
+
this.sessionId = input.selection.sessionId;
|
|
229
|
+
this.resumed = input.selection.mode === "resume";
|
|
230
|
+
this.nextTurnNumber = store.nextTurnNumber();
|
|
231
|
+
this.contextMeter = new ContextMeter(input.contextBudget, {
|
|
232
|
+
onMeasuredAnchor: (anchor) => store.writeMeasuredContextAnchor(anchor),
|
|
233
|
+
});
|
|
234
|
+
this.shadowPlanner = new SwapPlanner(input.modelClient);
|
|
235
|
+
this.context = {
|
|
236
|
+
sessionId: this.sessionId,
|
|
237
|
+
createIteration: (turn, iterationNumber) =>
|
|
238
|
+
this.createIteration(turn, iterationNumber),
|
|
239
|
+
createToolCall: (iteration, toolCallNumber) =>
|
|
240
|
+
this.createToolCall(iteration, toolCallNumber),
|
|
241
|
+
finishIterationForContinuation: (iteration) =>
|
|
242
|
+
this.finishIterationForContinuation(iteration),
|
|
243
|
+
append: (event) => this.append(event),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
static async create(
|
|
248
|
+
input: CreateRuntimeSessionInput,
|
|
249
|
+
dependencies: RuntimeSessionFactoryDependencies,
|
|
250
|
+
): Promise<RuntimeSession> {
|
|
251
|
+
validateCreateInput(input);
|
|
252
|
+
const store = await dependencies.openStore(input, dependencies.idFactory);
|
|
253
|
+
let session: DefaultRuntimeSession;
|
|
254
|
+
let systemPrompt: string;
|
|
255
|
+
try {
|
|
256
|
+
systemPrompt = isNewSessionInput(input)
|
|
257
|
+
? input.systemPrompt
|
|
258
|
+
: store.readStoredSystemPrompt();
|
|
259
|
+
session = new DefaultRuntimeSession(
|
|
260
|
+
input,
|
|
261
|
+
dependencies,
|
|
262
|
+
dependencies.createEventSink(input),
|
|
263
|
+
dependencies.createObservationBuilder(),
|
|
264
|
+
store,
|
|
265
|
+
);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
if (isNewSessionInput(input)) {
|
|
268
|
+
await store
|
|
269
|
+
.deleteFromDisk()
|
|
270
|
+
.catch(() => store.abandon().catch(() => undefined));
|
|
271
|
+
} else {
|
|
272
|
+
await store.abandon().catch(() => undefined);
|
|
273
|
+
}
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
let started = false;
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
if (isNewSessionInput(input)) {
|
|
280
|
+
await session.append({
|
|
281
|
+
type: "session.started",
|
|
282
|
+
sessionId: session.sessionId,
|
|
283
|
+
data: {
|
|
284
|
+
workspaceRoot: store.workspaceRoot,
|
|
285
|
+
model: input.modelName,
|
|
286
|
+
...(input.profileName === undefined
|
|
287
|
+
? {}
|
|
288
|
+
: { profileName: input.profileName }),
|
|
289
|
+
maxIterations: input.maxIterations,
|
|
290
|
+
includeReasoningContent: input.includeReasoningContent,
|
|
291
|
+
contextProfile: input.contextProfile,
|
|
292
|
+
contextBudget: input.contextBudget,
|
|
293
|
+
projectInstructions: {
|
|
294
|
+
...(input.projectInstruction === undefined
|
|
295
|
+
? {}
|
|
296
|
+
: { instruction: input.projectInstruction }),
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
started = true;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
session.tooling = dependencies.createTooling({
|
|
304
|
+
workspaceRoot: input.workspaceRoot,
|
|
305
|
+
runtimeSession: session.context,
|
|
306
|
+
historyReader: store.historyReader(),
|
|
307
|
+
webFetchRefiner: input.webFetchRefiner,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const mcpConfig = await dependencies.loadMcpConfig(input.workspaceRoot);
|
|
311
|
+
if (mcpConfig !== undefined) {
|
|
312
|
+
session.mcpManager = await dependencies.createMcpManager({
|
|
313
|
+
config: mcpConfig,
|
|
314
|
+
runtimeSession: session.context,
|
|
315
|
+
});
|
|
316
|
+
for (const executor of session.mcpManager.executors) {
|
|
317
|
+
session.tooling.registry.register(executor, "MCP");
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const definitions = session.requireTooling().registry.definitions();
|
|
322
|
+
const contractPrepared = input.modelClient.prepare({
|
|
323
|
+
messages: [{ role: "system", content: systemPrompt }],
|
|
324
|
+
tools: definitions,
|
|
325
|
+
});
|
|
326
|
+
const runtimeContract = createRuntimeContract({
|
|
327
|
+
modelName: input.modelName,
|
|
328
|
+
profileName: input.profileName,
|
|
329
|
+
includeReasoningContent: input.includeReasoningContent,
|
|
330
|
+
contextProfile: input.contextProfile,
|
|
331
|
+
contextBudget: input.contextBudget,
|
|
332
|
+
systemPrompt,
|
|
333
|
+
toolSchemaSha256: contractPrepared.toolSchemaHash,
|
|
334
|
+
requestConfigSha256: contractPrepared.requestConfigHash,
|
|
335
|
+
});
|
|
336
|
+
if (input.selection.mode === "new") {
|
|
337
|
+
store.finalizeRuntimeContract(runtimeContract);
|
|
338
|
+
} else {
|
|
339
|
+
store.assertRuntimeContract(runtimeContract);
|
|
340
|
+
session.recovery = store.recoverInterruptedState(dependencies.idFactory);
|
|
341
|
+
const openCount = store.markResumed();
|
|
342
|
+
if (
|
|
343
|
+
session.recovery.recoveredTurnId !== undefined &&
|
|
344
|
+
session.recovery.recoveredFrameId !== undefined
|
|
345
|
+
) {
|
|
346
|
+
await session.append({
|
|
347
|
+
type: "session.interrupted_frame_recovered",
|
|
348
|
+
sessionId: session.sessionId,
|
|
349
|
+
data: {
|
|
350
|
+
turnId: session.recovery.recoveredTurnId,
|
|
351
|
+
frameId: session.recovery.recoveredFrameId,
|
|
352
|
+
syntheticCompletionCount: session.recovery.syntheticCompletionCount,
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
const projectInstruction = store.readProjectInstructionManifest();
|
|
357
|
+
await session.append({
|
|
358
|
+
type: "session.resumed",
|
|
359
|
+
sessionId: session.sessionId,
|
|
360
|
+
data: {
|
|
361
|
+
openCount,
|
|
362
|
+
...session.recovery,
|
|
363
|
+
...(projectInstruction === undefined
|
|
364
|
+
? {}
|
|
365
|
+
: {
|
|
366
|
+
projectInstructionFile: projectInstruction.path,
|
|
367
|
+
}),
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
session.ledger = dependencies.createLedger(store, dependencies.idFactory);
|
|
373
|
+
session.contextManager = new ContextManager({
|
|
374
|
+
store,
|
|
375
|
+
ledger: session.requireLedger(),
|
|
376
|
+
model: input.modelClient,
|
|
377
|
+
contextMeter: session.contextMeter,
|
|
378
|
+
committedPrefixAuditor: session.committedPrefixAuditor,
|
|
379
|
+
idFactory: dependencies.idFactory,
|
|
380
|
+
tools: () => session.requireTooling().registry.definitions(),
|
|
381
|
+
onUsageUpdated: (snapshot) =>
|
|
382
|
+
session.append({
|
|
383
|
+
type: "context.usage.updated",
|
|
384
|
+
sessionId: session.sessionId,
|
|
385
|
+
data: { phase: "revision", snapshot },
|
|
386
|
+
}),
|
|
387
|
+
});
|
|
388
|
+
const initialBuilt = session
|
|
389
|
+
.requireLedger()
|
|
390
|
+
.buildCommittedModelRequest(definitions);
|
|
391
|
+
const initialPrepared = input.modelClient.prepare(initialBuilt.request);
|
|
392
|
+
session.committedPrefixAuditor.audit(
|
|
393
|
+
initialBuilt.compiled.revisionId,
|
|
394
|
+
initialPrepared,
|
|
395
|
+
);
|
|
396
|
+
if (input.selection.mode === "resume") {
|
|
397
|
+
const storedAnchor = store.readActiveMeasuredContextAnchor();
|
|
398
|
+
if (storedAnchor !== undefined) {
|
|
399
|
+
session.contextMeter.restoreExactMeasuredAnchor(
|
|
400
|
+
initialPrepared,
|
|
401
|
+
storedAnchor,
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const initialSnapshot = session.contextMeter.measure(initialPrepared);
|
|
406
|
+
await session.append({
|
|
407
|
+
type: "context.usage.updated",
|
|
408
|
+
sessionId: session.sessionId,
|
|
409
|
+
data: { phase: "initial", snapshot: initialSnapshot },
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
session.state = "ready";
|
|
413
|
+
return session;
|
|
414
|
+
} catch (error) {
|
|
415
|
+
return session.rollbackInitialization(error, started);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
executeTurn(input: ExecuteTurnInput): Promise<RunAgentResult> {
|
|
420
|
+
if (this.state !== "ready") {
|
|
421
|
+
throw new Error(`Cannot execute a turn while RuntimeSession is ${this.state}.`);
|
|
422
|
+
}
|
|
423
|
+
if (input.userPrompt.trim() === "") {
|
|
424
|
+
throw new Error("Cannot execute a turn with an empty prompt.");
|
|
425
|
+
}
|
|
426
|
+
if (this.activeTurn !== undefined) {
|
|
427
|
+
throw new Error("Cannot execute concurrent turns in one RuntimeSession.");
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
let admissionPrepared;
|
|
431
|
+
try {
|
|
432
|
+
admissionPrepared = this.input.modelClient.prepare(
|
|
433
|
+
this.requireLedger().buildCandidateModelRequest(
|
|
434
|
+
input.userPrompt,
|
|
435
|
+
this.requireTooling().registry.definitions(),
|
|
436
|
+
).request,
|
|
437
|
+
);
|
|
438
|
+
} catch (error) {
|
|
439
|
+
if (isCanonicalRuntimeFault(error)) {
|
|
440
|
+
this.fault(error);
|
|
441
|
+
}
|
|
442
|
+
throw error;
|
|
443
|
+
}
|
|
444
|
+
const admissionSnapshot = this.contextMeter.measure(admissionPrepared);
|
|
445
|
+
this.contextMeter.assertWithinBudget(admissionSnapshot);
|
|
446
|
+
|
|
447
|
+
const controller = new AbortController();
|
|
448
|
+
const removeExternalAbortListener = forwardExternalAbort(input.signal, controller);
|
|
449
|
+
const turn = this.stageTurn(input.userPrompt);
|
|
450
|
+
let pendingLedgerTurn;
|
|
451
|
+
try {
|
|
452
|
+
pendingLedgerTurn = this.requireLedger().beginTurn({
|
|
453
|
+
turn,
|
|
454
|
+
userPrompt: input.userPrompt,
|
|
455
|
+
});
|
|
456
|
+
this.registerTurn(turn);
|
|
457
|
+
} catch (error) {
|
|
458
|
+
this.fault(error);
|
|
459
|
+
throw error;
|
|
460
|
+
}
|
|
461
|
+
this.state = "executing";
|
|
462
|
+
const completion = this.performExecuteTurn(
|
|
463
|
+
input.userPrompt,
|
|
464
|
+
turn,
|
|
465
|
+
pendingLedgerTurn,
|
|
466
|
+
controller.signal,
|
|
467
|
+
removeExternalAbortListener,
|
|
468
|
+
);
|
|
469
|
+
this.activeTurn = { controller, completion };
|
|
470
|
+
return completion;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
compactContext(): Promise<ContextCompactionResult> {
|
|
474
|
+
if (this.state !== "ready") {
|
|
475
|
+
throw new Error(`Cannot compact context while RuntimeSession is ${this.state}.`);
|
|
476
|
+
}
|
|
477
|
+
if (this.activeTurn !== undefined) {
|
|
478
|
+
throw new Error("Cannot compact context while a turn is active.");
|
|
479
|
+
}
|
|
480
|
+
const completion = this.performCompactContext();
|
|
481
|
+
this.activeCompaction = completion;
|
|
482
|
+
void completion.then(
|
|
483
|
+
() => {
|
|
484
|
+
if (this.activeCompaction === completion) {
|
|
485
|
+
this.activeCompaction = undefined;
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
() => {
|
|
489
|
+
if (this.activeCompaction === completion) {
|
|
490
|
+
this.activeCompaction = undefined;
|
|
491
|
+
}
|
|
492
|
+
},
|
|
493
|
+
);
|
|
494
|
+
return completion;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
private async performCompactContext(): Promise<ContextCompactionResult> {
|
|
498
|
+
if (this.state !== "ready") {
|
|
499
|
+
throw new Error(`Cannot compact context while RuntimeSession is ${this.state}.`);
|
|
500
|
+
}
|
|
501
|
+
if (this.activeTurn !== undefined) {
|
|
502
|
+
throw new Error("Cannot compact context while a turn is active.");
|
|
503
|
+
}
|
|
504
|
+
this.store.assertContextRevisionIdle();
|
|
505
|
+
this.state = "compacting";
|
|
506
|
+
let started = false;
|
|
507
|
+
try {
|
|
508
|
+
await this.append({
|
|
509
|
+
type: "context.revision.started",
|
|
510
|
+
sessionId: this.sessionId,
|
|
511
|
+
data: {
|
|
512
|
+
strategy: "swap",
|
|
513
|
+
reason: "manual",
|
|
514
|
+
policyVersion: "swap-only-v1",
|
|
515
|
+
rendererFormat: "swap-observation-v1",
|
|
516
|
+
},
|
|
517
|
+
});
|
|
518
|
+
started = true;
|
|
519
|
+
const result = await this.requireContextManager().compact(
|
|
520
|
+
this.dependencies.manualCompactionTrigger(),
|
|
521
|
+
);
|
|
522
|
+
await this.append({
|
|
523
|
+
type: "context.revision.finished",
|
|
524
|
+
sessionId: this.sessionId,
|
|
525
|
+
data: contextRevisionFinishedData(result),
|
|
526
|
+
});
|
|
527
|
+
if (this.state === "compacting") {
|
|
528
|
+
this.state = "ready";
|
|
529
|
+
}
|
|
530
|
+
return result;
|
|
531
|
+
} catch (error) {
|
|
532
|
+
if (started && !(error instanceof RuntimeEventAppendError)) {
|
|
533
|
+
const failure =
|
|
534
|
+
error instanceof ContextManagerError
|
|
535
|
+
? error
|
|
536
|
+
: new ContextManagerError(
|
|
537
|
+
"activate",
|
|
538
|
+
error instanceof Error ? error.name : "CONTEXT_COMPACTION_FAILED",
|
|
539
|
+
true,
|
|
540
|
+
false,
|
|
541
|
+
"Context compaction failed.",
|
|
542
|
+
{ cause: error },
|
|
543
|
+
);
|
|
544
|
+
await this.append({
|
|
545
|
+
type: "context.revision.failed",
|
|
546
|
+
sessionId: this.sessionId,
|
|
547
|
+
data: {
|
|
548
|
+
strategy: "swap",
|
|
549
|
+
reason: "manual",
|
|
550
|
+
stage: failure.stage,
|
|
551
|
+
errorCode: boundedContextErrorCode(failure.code),
|
|
552
|
+
error: `Context compaction failed at ${failure.stage}.`,
|
|
553
|
+
},
|
|
554
|
+
}).catch(() => undefined);
|
|
555
|
+
}
|
|
556
|
+
if (!(error instanceof ContextManagerError) || error.fatal) {
|
|
557
|
+
this.fault(error);
|
|
558
|
+
} else if (this.state === "compacting") {
|
|
559
|
+
this.state = "ready";
|
|
560
|
+
}
|
|
561
|
+
throw error;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
dispose(reason: SessionDisposeReason): Promise<void> {
|
|
566
|
+
this.disposePromise ??= this.performDispose(reason);
|
|
567
|
+
return this.disposePromise;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
canSwitchSession(): boolean {
|
|
571
|
+
return (
|
|
572
|
+
this.state === "ready" &&
|
|
573
|
+
this.activeTurn === undefined &&
|
|
574
|
+
(this.tooling?.taskManager
|
|
575
|
+
.listBackgroundTasks()
|
|
576
|
+
.every((task) => task.status !== "running" && task.status !== "stopping") ??
|
|
577
|
+
true)
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
private async performExecuteTurn(
|
|
582
|
+
userPrompt: string,
|
|
583
|
+
turn: TurnIdentity,
|
|
584
|
+
pendingLedgerTurn: ReturnType<SessionLedger["beginTurn"]>,
|
|
585
|
+
signal: AbortSignal,
|
|
586
|
+
removeExternalAbortListener: () => void,
|
|
587
|
+
): Promise<RunAgentResult> {
|
|
588
|
+
let settled = false;
|
|
589
|
+
|
|
590
|
+
try {
|
|
591
|
+
await this.append({
|
|
592
|
+
type: "turn.started",
|
|
593
|
+
...turn,
|
|
594
|
+
data: { userPrompt },
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
let result: RunAgentResult;
|
|
598
|
+
try {
|
|
599
|
+
result = await runAgent({
|
|
600
|
+
ledger: pendingLedgerTurn.agent,
|
|
601
|
+
maxIterations: this.input.maxIterations,
|
|
602
|
+
model: this.input.modelClient,
|
|
603
|
+
contextMeter: this.contextMeter,
|
|
604
|
+
committedPrefixAuditor: this.committedPrefixAuditor,
|
|
605
|
+
shadowPlanning: {
|
|
606
|
+
planner: this.shadowPlanner,
|
|
607
|
+
select: this.dependencies.selectShadowPlanning,
|
|
608
|
+
...(this.dependencies.onShadowPlanningResult === undefined
|
|
609
|
+
? {}
|
|
610
|
+
: { onResult: this.dependencies.onShadowPlanningResult }),
|
|
611
|
+
},
|
|
612
|
+
tools: this.requireTooling().registry,
|
|
613
|
+
toolRuntime: this.requireTooling().runtime,
|
|
614
|
+
observationBuilder: this.observationBuilder,
|
|
615
|
+
runtimeSession: this.context,
|
|
616
|
+
turn,
|
|
617
|
+
signal,
|
|
618
|
+
});
|
|
619
|
+
} catch (error) {
|
|
620
|
+
if (error instanceof RuntimeEventAppendError) {
|
|
621
|
+
throw error;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (error instanceof FatalAgentTurnError) {
|
|
625
|
+
await this.appendTerminalEvent(
|
|
626
|
+
turn,
|
|
627
|
+
error.result,
|
|
628
|
+
pendingLedgerTurn.projectedMessageCount(),
|
|
629
|
+
);
|
|
630
|
+
pendingLedgerTurn.finish(error.result);
|
|
631
|
+
settled = true;
|
|
632
|
+
throw error;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
await this.append({
|
|
636
|
+
type: "turn.failed",
|
|
637
|
+
...turn,
|
|
638
|
+
data: { error: errorMessage(error) },
|
|
639
|
+
});
|
|
640
|
+
throw error;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const projectedMessageCount = pendingLedgerTurn.projectedMessageCount();
|
|
644
|
+
await this.appendTerminalEvent(turn, result, projectedMessageCount);
|
|
645
|
+
pendingLedgerTurn.finish(result);
|
|
646
|
+
settled = true;
|
|
647
|
+
return result;
|
|
648
|
+
} catch (error) {
|
|
649
|
+
if (!settled) {
|
|
650
|
+
pendingLedgerTurn.fault(error);
|
|
651
|
+
}
|
|
652
|
+
this.fault(error);
|
|
653
|
+
throw error;
|
|
654
|
+
} finally {
|
|
655
|
+
removeExternalAbortListener();
|
|
656
|
+
this.activeTurn = undefined;
|
|
657
|
+
if (this.state === "executing") {
|
|
658
|
+
this.state = "ready";
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
private async appendTerminalEvent(
|
|
664
|
+
turn: TurnIdentity,
|
|
665
|
+
result: RunAgentResult,
|
|
666
|
+
projectedMessageCount: number,
|
|
667
|
+
): Promise<void> {
|
|
668
|
+
if (result.status === "completed") {
|
|
669
|
+
await this.append({
|
|
670
|
+
type: "turn.finished",
|
|
671
|
+
...turn,
|
|
672
|
+
data: {
|
|
673
|
+
status: result.status,
|
|
674
|
+
finalText: result.finalText,
|
|
675
|
+
lastIteration: result.lastIteration,
|
|
676
|
+
messageCount: projectedMessageCount,
|
|
677
|
+
},
|
|
678
|
+
});
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (result.status === "cancelled") {
|
|
683
|
+
await this.append({
|
|
684
|
+
type: "turn.cancelled",
|
|
685
|
+
...result.lastIteration,
|
|
686
|
+
data: { cancellation: result.cancellation },
|
|
687
|
+
});
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
await this.append({
|
|
692
|
+
type: "turn.failed",
|
|
693
|
+
...result.lastIteration,
|
|
694
|
+
data: { error: result.error },
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
private async performDispose(reason: SessionDisposeReason): Promise<void> {
|
|
699
|
+
if (this.state === "disposed") {
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
this.state = "disposing";
|
|
704
|
+
const errors: unknown[] = this.faultCause === undefined ? [] : [this.faultCause];
|
|
705
|
+
const activeCompaction = this.activeCompaction;
|
|
706
|
+
if (activeCompaction !== undefined) {
|
|
707
|
+
try {
|
|
708
|
+
await activeCompaction;
|
|
709
|
+
} catch {
|
|
710
|
+
// The compactContext caller owns its primary error. Disposal still continues.
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
if (this.faultCause !== undefined && !errors.includes(this.faultCause)) {
|
|
714
|
+
errors.push(this.faultCause);
|
|
715
|
+
}
|
|
716
|
+
const activeTurn = this.activeTurn;
|
|
717
|
+
if (activeTurn !== undefined) {
|
|
718
|
+
activeTurn.controller.abort(new TurnCancelledError("session_dispose"));
|
|
719
|
+
try {
|
|
720
|
+
await activeTurn.completion;
|
|
721
|
+
} catch {
|
|
722
|
+
// The executeTurn caller owns its primary error. Disposal still continues.
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (this.mcpManager !== undefined) {
|
|
727
|
+
await collectError(errors, () => this.mcpManager!.dispose());
|
|
728
|
+
}
|
|
729
|
+
if (this.tooling !== undefined) {
|
|
730
|
+
await collectError(errors, () => this.tooling!.dispose(reason.type));
|
|
731
|
+
}
|
|
732
|
+
await collectError(errors, () =>
|
|
733
|
+
this.append({
|
|
734
|
+
type: "session.finished",
|
|
735
|
+
sessionId: this.sessionId,
|
|
736
|
+
data: {
|
|
737
|
+
reason: reason.type,
|
|
738
|
+
...("error" in reason ? { error: reason.error } : {}),
|
|
739
|
+
},
|
|
740
|
+
}),
|
|
741
|
+
);
|
|
742
|
+
await collectError(errors, () => this.store.close(reason.type));
|
|
743
|
+
|
|
744
|
+
this.state = "disposed";
|
|
745
|
+
throwCollectedErrors(errors, "RuntimeSession disposal failed.");
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
private async rollbackInitialization(
|
|
749
|
+
initializationError: unknown,
|
|
750
|
+
started: boolean,
|
|
751
|
+
): Promise<never> {
|
|
752
|
+
this.state = "disposing";
|
|
753
|
+
const errors: unknown[] = [initializationError];
|
|
754
|
+
|
|
755
|
+
if (this.mcpManager !== undefined) {
|
|
756
|
+
await collectError(errors, () => this.mcpManager!.dispose());
|
|
757
|
+
}
|
|
758
|
+
if (this.tooling !== undefined) {
|
|
759
|
+
await collectError(errors, () => this.tooling!.dispose("initialization_failed"));
|
|
760
|
+
}
|
|
761
|
+
if (started) {
|
|
762
|
+
await collectError(errors, () =>
|
|
763
|
+
this.append({
|
|
764
|
+
type: "session.finished",
|
|
765
|
+
sessionId: this.sessionId,
|
|
766
|
+
data: {
|
|
767
|
+
reason: "initialization_failed",
|
|
768
|
+
error: errorMessage(initializationError),
|
|
769
|
+
},
|
|
770
|
+
}),
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
await collectError(errors, async () => {
|
|
774
|
+
let meta;
|
|
775
|
+
try {
|
|
776
|
+
meta = this.store.readMeta();
|
|
777
|
+
} catch (error) {
|
|
778
|
+
await this.store.abandon().catch(() => undefined);
|
|
779
|
+
throw error;
|
|
780
|
+
}
|
|
781
|
+
if (
|
|
782
|
+
this.input.selection.mode === "new" &&
|
|
783
|
+
meta.initializationState === "creating" &&
|
|
784
|
+
meta.nextTurnNumber === 1
|
|
785
|
+
) {
|
|
786
|
+
await this.store.deleteFromDisk();
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
await this.store.close("initialization_failed");
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
this.state = "disposed";
|
|
793
|
+
throwCollectedErrors(errors, "RuntimeSession initialization failed.");
|
|
794
|
+
throw new Error("Initialization error collection unexpectedly returned.");
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
private stageTurn(userPrompt: string): TurnIdentity {
|
|
798
|
+
if (userPrompt.trim() === "") {
|
|
799
|
+
throw new Error("Cannot create an AgentTurn for an empty prompt.");
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
const identity: TurnIdentity = {
|
|
803
|
+
sessionId: this.sessionId,
|
|
804
|
+
turnId: this.dependencies.idFactory.createTurnId(),
|
|
805
|
+
turnNumber: this.nextTurnNumber,
|
|
806
|
+
};
|
|
807
|
+
return identity;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
private registerTurn(identity: TurnIdentity): void {
|
|
811
|
+
if (identity.turnNumber !== this.nextTurnNumber) {
|
|
812
|
+
throw new Error(
|
|
813
|
+
`turnNumber must be ${this.nextTurnNumber}; received ${identity.turnNumber}.`,
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
this.nextTurnNumber += 1;
|
|
817
|
+
this.turns.set(identity.turnId, identity);
|
|
818
|
+
this.nextIterationNumberByTurn.set(identity.turnId, 1);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
private createIteration(
|
|
822
|
+
turn: TurnIdentity,
|
|
823
|
+
iterationNumber: number,
|
|
824
|
+
): IterationIdentity {
|
|
825
|
+
this.requireTurn(turn);
|
|
826
|
+
requirePositiveNumber(iterationNumber, "iterationNumber");
|
|
827
|
+
const expected = this.nextIterationNumberByTurn.get(turn.turnId);
|
|
828
|
+
if (iterationNumber !== expected) {
|
|
829
|
+
throw new Error(
|
|
830
|
+
`iterationNumber for turn ${turn.turnId} must be ${expected}; received ${iterationNumber}.`,
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const identity: IterationIdentity = {
|
|
835
|
+
...turn,
|
|
836
|
+
iterationId: this.dependencies.idFactory.createIterationId(),
|
|
837
|
+
iterationNumber,
|
|
838
|
+
};
|
|
839
|
+
this.store.beginIteration(identity);
|
|
840
|
+
this.iterations.set(identity.iterationId, identity);
|
|
841
|
+
this.nextIterationNumberByTurn.set(turn.turnId, iterationNumber + 1);
|
|
842
|
+
this.nextToolCallNumberByIteration.set(identity.iterationId, 1);
|
|
843
|
+
return identity;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
private createToolCall(
|
|
847
|
+
iteration: IterationIdentity,
|
|
848
|
+
toolCallNumber: number,
|
|
849
|
+
): ToolCallIdentity {
|
|
850
|
+
this.requireIteration(iteration);
|
|
851
|
+
requirePositiveNumber(toolCallNumber, "toolCallNumber");
|
|
852
|
+
const expected = this.nextToolCallNumberByIteration.get(iteration.iterationId);
|
|
853
|
+
if (toolCallNumber !== expected) {
|
|
854
|
+
throw new Error(
|
|
855
|
+
`toolCallNumber for iteration ${iteration.iterationId} must be ${expected}; received ${toolCallNumber}.`,
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
const identity: ToolCallIdentity = {
|
|
860
|
+
...iteration,
|
|
861
|
+
toolCallId: this.dependencies.idFactory.createToolCallId(),
|
|
862
|
+
toolCallNumber,
|
|
863
|
+
};
|
|
864
|
+
this.toolCalls.set(identity.toolCallId, identity);
|
|
865
|
+
this.nextToolCallNumberByIteration.set(iteration.iterationId, toolCallNumber + 1);
|
|
866
|
+
return identity;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
private finishIterationForContinuation(iteration: IterationIdentity): void {
|
|
870
|
+
this.requireIteration(iteration);
|
|
871
|
+
this.store.finishIterationForContinuation(iteration);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
private append(input: AgentEventInput): Promise<void> {
|
|
875
|
+
if (this.state === "disposed") {
|
|
876
|
+
throw new Error("Cannot append events after RuntimeSession is disposed.");
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
this.validateEventIdentity(input);
|
|
880
|
+
let eventSequence: number;
|
|
881
|
+
try {
|
|
882
|
+
eventSequence = this.store.allocateEventSequence();
|
|
883
|
+
} catch (error) {
|
|
884
|
+
this.fault(error);
|
|
885
|
+
throw error;
|
|
886
|
+
}
|
|
887
|
+
const event: AgentEvent = {
|
|
888
|
+
...input,
|
|
889
|
+
eventSequence,
|
|
890
|
+
timestamp: new Date().toISOString(),
|
|
891
|
+
} as AgentEvent;
|
|
892
|
+
|
|
893
|
+
const write = this.eventTail
|
|
894
|
+
.then(async () => {
|
|
895
|
+
const result = await this.eventSink.append(event);
|
|
896
|
+
for (const diagnostic of result?.diagnostics ?? []) {
|
|
897
|
+
void this.append({
|
|
898
|
+
type: "diagnostic.sink_failed",
|
|
899
|
+
sessionId: this.sessionId,
|
|
900
|
+
data: diagnostic,
|
|
901
|
+
}).catch(() => undefined);
|
|
902
|
+
}
|
|
903
|
+
})
|
|
904
|
+
.catch((error) => {
|
|
905
|
+
const appendError =
|
|
906
|
+
error instanceof RuntimeEventAppendError
|
|
907
|
+
? error
|
|
908
|
+
: new RuntimeEventAppendError(input.type, { cause: error });
|
|
909
|
+
this.fault(appendError);
|
|
910
|
+
throw appendError;
|
|
911
|
+
});
|
|
912
|
+
this.eventTail = write.catch(() => undefined);
|
|
913
|
+
return write;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
private validateEventIdentity(input: AgentEventInput): void {
|
|
917
|
+
if (input.sessionId !== this.sessionId) {
|
|
918
|
+
throw new Error(
|
|
919
|
+
`Event ${input.type} belongs to session ${input.sessionId}, expected ${this.sessionId}.`,
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
if ("toolCallId" in input) {
|
|
924
|
+
this.requireToolCall(input);
|
|
925
|
+
this.requireMatchingEventData(input);
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
if ("iterationId" in input) {
|
|
929
|
+
this.requireIteration(input);
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
if ("turnId" in input) {
|
|
933
|
+
this.requireTurn(input);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
private requireMatchingEventData(input: AgentEventInput): void {
|
|
938
|
+
if (!("toolCallId" in input)) {
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
let relatedIdentity: ToolCallIdentity | undefined;
|
|
943
|
+
switch (input.type) {
|
|
944
|
+
case "tool.started":
|
|
945
|
+
case "tool.raw_result":
|
|
946
|
+
case "tool.finished":
|
|
947
|
+
case "tool.observation":
|
|
948
|
+
relatedIdentity = input.data.call;
|
|
949
|
+
break;
|
|
950
|
+
case "bash.task.backgrounded":
|
|
951
|
+
case "bash.task.stopping":
|
|
952
|
+
case "bash.task.finished":
|
|
953
|
+
relatedIdentity = input.data.task.origin;
|
|
954
|
+
break;
|
|
955
|
+
default:
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
if (
|
|
959
|
+
relatedIdentity !== undefined &&
|
|
960
|
+
relatedIdentity.toolCallId !== input.toolCallId
|
|
961
|
+
) {
|
|
962
|
+
throw new Error(
|
|
963
|
+
`Event ${input.type} data belongs to tool call ${relatedIdentity.toolCallId}, expected ${input.toolCallId}.`,
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
private requireTurn(turn: TurnIdentity): void {
|
|
969
|
+
const registered = this.turns.get(turn.turnId);
|
|
970
|
+
if (
|
|
971
|
+
registered === undefined ||
|
|
972
|
+
registered.sessionId !== turn.sessionId ||
|
|
973
|
+
registered.turnNumber !== turn.turnNumber
|
|
974
|
+
) {
|
|
975
|
+
throw new Error(`Unknown or mismatched turn identity: ${turn.turnId}.`);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
private requireIteration(iteration: IterationIdentity): void {
|
|
980
|
+
this.requireTurn(iteration);
|
|
981
|
+
const registered = this.iterations.get(iteration.iterationId);
|
|
982
|
+
if (
|
|
983
|
+
registered === undefined ||
|
|
984
|
+
registered.turnId !== iteration.turnId ||
|
|
985
|
+
registered.iterationNumber !== iteration.iterationNumber
|
|
986
|
+
) {
|
|
987
|
+
throw new Error(
|
|
988
|
+
`Unknown or mismatched iteration identity: ${iteration.iterationId}.`,
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
private requireToolCall(toolCall: ToolCallIdentity): void {
|
|
994
|
+
this.requireIteration(toolCall);
|
|
995
|
+
const registered = this.toolCalls.get(toolCall.toolCallId);
|
|
996
|
+
if (
|
|
997
|
+
registered === undefined ||
|
|
998
|
+
registered.iterationId !== toolCall.iterationId ||
|
|
999
|
+
registered.toolCallNumber !== toolCall.toolCallNumber
|
|
1000
|
+
) {
|
|
1001
|
+
throw new Error(
|
|
1002
|
+
`Unknown or mismatched tool call identity: ${toolCall.toolCallId}.`,
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
private requireTooling(): DefaultTooling {
|
|
1008
|
+
if (this.tooling === undefined) {
|
|
1009
|
+
throw new Error("RuntimeSession tooling is not initialized.");
|
|
1010
|
+
}
|
|
1011
|
+
return this.tooling;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
private requireLedger(): SessionLedger {
|
|
1015
|
+
if (this.ledger === undefined) {
|
|
1016
|
+
throw new Error("RuntimeSession ledger is not initialized.");
|
|
1017
|
+
}
|
|
1018
|
+
return this.ledger;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
private requireContextManager(): ContextManager {
|
|
1022
|
+
if (this.contextManager === undefined) {
|
|
1023
|
+
throw new Error("RuntimeSession ContextManager is not initialized.");
|
|
1024
|
+
}
|
|
1025
|
+
return this.contextManager;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
private fault(error: unknown): void {
|
|
1029
|
+
this.faultCause ??= error;
|
|
1030
|
+
if (this.state !== "disposing" && this.state !== "disposed") {
|
|
1031
|
+
this.state = "faulted";
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
export async function createRuntimeSession(
|
|
1037
|
+
input: CreateRuntimeSessionInput,
|
|
1038
|
+
dependencyOverrides: Partial<RuntimeSessionFactoryDependencies> = {},
|
|
1039
|
+
): Promise<RuntimeSession> {
|
|
1040
|
+
return DefaultRuntimeSession.create(input, {
|
|
1041
|
+
...defaultDependencies,
|
|
1042
|
+
...dependencyOverrides,
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function createEventSink(input: CreateRuntimeSessionInput): EventSink {
|
|
1047
|
+
const requiredSinks: EventSink[] = [];
|
|
1048
|
+
if (input.persistence !== false) {
|
|
1049
|
+
const basePath = path.join(
|
|
1050
|
+
input.workspaceRoot,
|
|
1051
|
+
".tinker",
|
|
1052
|
+
"sessions",
|
|
1053
|
+
input.selection.sessionId,
|
|
1054
|
+
);
|
|
1055
|
+
requiredSinks.push(
|
|
1056
|
+
new JsonlEventLog(
|
|
1057
|
+
input.persistence?.eventLogPath ?? path.join(basePath, "events.jsonl"),
|
|
1058
|
+
),
|
|
1059
|
+
new ObservationTextLog(
|
|
1060
|
+
input.persistence?.observationLogPath ?? path.join(basePath, "observations.md"),
|
|
1061
|
+
),
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
return new CompositeEventSink({
|
|
1065
|
+
requiredSinks,
|
|
1066
|
+
auxiliarySinks: input.presentationSinks ?? [],
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function validateCreateInput(input: CreateRuntimeSessionInput): void {
|
|
1071
|
+
if (input.selection.sessionId.trim() === "") {
|
|
1072
|
+
throw new Error("RuntimeSession sessionId must not be empty.");
|
|
1073
|
+
}
|
|
1074
|
+
if (!path.isAbsolute(input.workspaceRoot)) {
|
|
1075
|
+
throw new Error("RuntimeSession workspaceRoot must be an absolute path.");
|
|
1076
|
+
}
|
|
1077
|
+
if (input.modelName.trim() === "") {
|
|
1078
|
+
throw new Error("RuntimeSession modelName must not be empty.");
|
|
1079
|
+
}
|
|
1080
|
+
requirePositiveNumber(input.maxIterations, "maxIterations");
|
|
1081
|
+
if (isNewSessionInput(input) && input.systemPrompt.trim() === "") {
|
|
1082
|
+
throw new Error("RuntimeSession systemPrompt must not be empty.");
|
|
1083
|
+
}
|
|
1084
|
+
if (
|
|
1085
|
+
typeof input.modelClient !== "object" ||
|
|
1086
|
+
input.modelClient === null ||
|
|
1087
|
+
typeof input.modelClient.prepare !== "function" ||
|
|
1088
|
+
typeof input.modelClient.request !== "function"
|
|
1089
|
+
) {
|
|
1090
|
+
throw new Error(
|
|
1091
|
+
"RuntimeSession modelClient must implement prepare() and request().",
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
assertMatchingContextBudget(input.contextProfile, input.contextBudget);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function isNewSessionInput(
|
|
1098
|
+
input: CreateRuntimeSessionInput,
|
|
1099
|
+
): input is CreateNewRuntimeSessionInput {
|
|
1100
|
+
return input.selection.mode === "new";
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function forwardExternalAbort(
|
|
1104
|
+
externalSignal: AbortSignal,
|
|
1105
|
+
internalController: AbortController,
|
|
1106
|
+
): () => void {
|
|
1107
|
+
const forward = () => {
|
|
1108
|
+
internalController.abort(
|
|
1109
|
+
new TurnCancelledError("user", undefined, {
|
|
1110
|
+
cause: externalSignal.reason,
|
|
1111
|
+
}),
|
|
1112
|
+
);
|
|
1113
|
+
};
|
|
1114
|
+
|
|
1115
|
+
if (externalSignal.aborted) {
|
|
1116
|
+
forward();
|
|
1117
|
+
return () => undefined;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
externalSignal.addEventListener("abort", forward, { once: true });
|
|
1121
|
+
return () => externalSignal.removeEventListener("abort", forward);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
async function collectError(
|
|
1125
|
+
errors: unknown[],
|
|
1126
|
+
operation: () => Promise<void>,
|
|
1127
|
+
): Promise<void> {
|
|
1128
|
+
try {
|
|
1129
|
+
await operation();
|
|
1130
|
+
} catch (error) {
|
|
1131
|
+
if (!errors.includes(error)) {
|
|
1132
|
+
errors.push(error);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
function throwCollectedErrors(errors: unknown[], message: string): void {
|
|
1138
|
+
if (errors.length === 0) {
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
if (errors.length === 1) {
|
|
1142
|
+
throw errors[0];
|
|
1143
|
+
}
|
|
1144
|
+
throw new AggregateError(errors, message);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
function errorMessage(error: unknown): string {
|
|
1148
|
+
return error instanceof Error ? error.message : String(error);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function contextRevisionFinishedData(
|
|
1152
|
+
result: ContextCompactionResult,
|
|
1153
|
+
): ContextRevisionFinishedData {
|
|
1154
|
+
if (result.status === "unchanged") {
|
|
1155
|
+
return {
|
|
1156
|
+
strategy: "swap",
|
|
1157
|
+
reason: "manual",
|
|
1158
|
+
policyVersion: "swap-only-v1",
|
|
1159
|
+
outcome: result.outcome,
|
|
1160
|
+
baseRevisionNumber: result.revisionNumber,
|
|
1161
|
+
addedOverrideCount: 0,
|
|
1162
|
+
totalOverrideCount: result.totalOverrideCount,
|
|
1163
|
+
originalObservationBytes: 0,
|
|
1164
|
+
projectedObservationBytes: 0,
|
|
1165
|
+
rawTokensBefore: result.rawTokensBefore,
|
|
1166
|
+
guardedTokensBefore: result.guardedTokensBefore,
|
|
1167
|
+
targetTokens: result.targetTokens,
|
|
1168
|
+
durationMs: result.durationMs,
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
return {
|
|
1172
|
+
strategy: "swap",
|
|
1173
|
+
reason: "manual",
|
|
1174
|
+
policyVersion: "swap-only-v1",
|
|
1175
|
+
outcome: result.outcome,
|
|
1176
|
+
baseRevisionNumber: result.previousRevisionNumber,
|
|
1177
|
+
revisionNumber: result.revisionNumber,
|
|
1178
|
+
addedOverrideCount: result.addedOverrideCount,
|
|
1179
|
+
totalOverrideCount: result.totalOverrideCount,
|
|
1180
|
+
originalObservationBytes: result.originalObservationBytes,
|
|
1181
|
+
projectedObservationBytes: result.projectedObservationBytes,
|
|
1182
|
+
rawTokensBefore: result.rawTokensBefore,
|
|
1183
|
+
rawTokensAfter: result.rawTokensAfter,
|
|
1184
|
+
guardedTokensBefore: result.guardedTokensBefore,
|
|
1185
|
+
guardedTokensAfter: result.guardedTokensAfter,
|
|
1186
|
+
targetTokens: result.targetTokens,
|
|
1187
|
+
planHash: result.planHash,
|
|
1188
|
+
durationMs: result.durationMs,
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
function boundedContextErrorCode(code: string): string {
|
|
1193
|
+
return /^[A-Za-z0-9_]+$/.test(code) && code.length <= 80
|
|
1194
|
+
? code
|
|
1195
|
+
: "CONTEXT_COMPACTION_FAILED";
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function requirePositiveNumber(value: number, name: string): void {
|
|
1199
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
1200
|
+
throw new Error(`${name} must be a positive integer; received ${value}.`);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
function isCanonicalRuntimeFault(error: unknown): boolean {
|
|
1205
|
+
return (
|
|
1206
|
+
error instanceof ContextProtocolError ||
|
|
1207
|
+
error instanceof ContextRevisionError ||
|
|
1208
|
+
error instanceof CompiledContextError ||
|
|
1209
|
+
error instanceof SessionLedgerWriteError ||
|
|
1210
|
+
error instanceof SessionError
|
|
1211
|
+
);
|
|
1212
|
+
}
|