tinker-agent 1.9.0 → 1.11.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 +36 -1
- package/README.md +64 -6
- package/package.json +1 -1
- package/src/agent/loop.ts +17 -0
- package/src/agent/runtime-session.ts +341 -1
- package/src/agent/session-ledger.ts +100 -3
- package/src/cli/config.ts +11 -2
- package/src/cli/model-profiles.ts +58 -0
- package/src/cli/public-config-contract.ts +73 -7
- package/src/cli/run-runner.ts +4 -1
- package/src/cli/runner-dependencies.ts +28 -4
- package/src/cli/tui-memory.ts +4 -0
- package/src/cli/tui-runner.tsx +8 -1
- package/src/context/context-automation-policy.ts +22 -21
- package/src/context/context-manager.ts +91 -15
- package/src/context/context-policy.ts +0 -2
- package/src/context/context-swap-renderer.ts +1 -1
- package/src/context/prefix-retirement-planner.ts +58 -8
- package/src/context/recall-retirement-contract.ts +5 -4
- package/src/context/swap-planner.ts +33 -27
- package/src/events/observation-text-log.ts +4 -0
- package/src/events/stdout-event-printer.ts +5 -0
- package/src/events/types.ts +5 -1
- package/src/model/fake-model-client.ts +55 -16
- package/src/model/model-api.ts +12 -0
- package/src/model/model-client.ts +9 -1
- package/src/model/moonshot-input-token-estimator.ts +5 -1
- package/src/model/openai-chat-mapping.ts +2 -24
- package/src/model/openai-chat-model-client.ts +18 -294
- package/src/model/openai-image-mapping.ts +20 -0
- package/src/model/openai-model-utils.ts +304 -0
- package/src/model/openai-responses-mapping.ts +532 -0
- package/src/model/openai-responses-model-client.ts +295 -0
- package/src/model/openai-responses-stream.ts +96 -0
- package/src/model/openai-responses-token-estimator.ts +155 -0
- package/src/model/reasoning-effort.ts +60 -0
- package/src/session/session-catalog.ts +2 -2
- package/src/session/session-history-reader.ts +6 -1
- package/src/session/session-schema.ts +268 -4
- package/src/session/session-store.ts +134 -26
- package/src/skills/skill-context.ts +2 -2
- package/src/tools/bounded-output-preview.ts +276 -0
- package/src/tools/recall.ts +67 -36
- package/src/tools/registry.ts +7 -2
- package/src/tools/task-output-snapshot.ts +6 -22
- package/src/tools/task-output.ts +23 -27
- package/src/tui/app.tsx +153 -11
- package/src/tui/components/footer.tsx +6 -1
- package/src/tui/components/prompt-input.tsx +9 -1
- package/src/tui/event-store.ts +15 -0
- package/src/tui/slash-commands.ts +20 -0
- package/src/tui/tui-session-controller.ts +14 -0
|
@@ -93,6 +93,7 @@ import { FatalAgentTurnError, runAgent, type RunAgentInput } from "./loop";
|
|
|
93
93
|
import {
|
|
94
94
|
AdmissionStaleError,
|
|
95
95
|
SessionLedgerWriteError,
|
|
96
|
+
type AgentTurnLedger,
|
|
96
97
|
type AdmissionBaseToken,
|
|
97
98
|
type SessionLedger,
|
|
98
99
|
} from "./session-ledger";
|
|
@@ -114,6 +115,7 @@ import {
|
|
|
114
115
|
type ContextAutomationDecision,
|
|
115
116
|
} from "../context/context-automation-policy";
|
|
116
117
|
import type { SkillCatalogSnapshot } from "../skills/skill-loader";
|
|
118
|
+
import type { ReasoningEffortSnapshot } from "../model/reasoning-effort";
|
|
117
119
|
import {
|
|
118
120
|
activeSkillManifestEntry,
|
|
119
121
|
createSkillCatalogSnapshot,
|
|
@@ -137,6 +139,18 @@ export type AcceptedTurn = {
|
|
|
137
139
|
readonly completion: Promise<RunAgentResult>;
|
|
138
140
|
};
|
|
139
141
|
|
|
142
|
+
export type PromptSchedulerSnapshot = {
|
|
143
|
+
readonly state: "idle" | "running";
|
|
144
|
+
readonly activeTurnId?: TurnIdentity["turnId"];
|
|
145
|
+
readonly pendingCount: number;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export type QueueFollowUpResult = {
|
|
149
|
+
readonly kind: "queued";
|
|
150
|
+
readonly pendingCount: number;
|
|
151
|
+
readonly activeTurnId?: TurnIdentity["turnId"];
|
|
152
|
+
};
|
|
153
|
+
|
|
140
154
|
export type SessionDisposeReason =
|
|
141
155
|
| { type: "oneshot_complete" }
|
|
142
156
|
| { type: "tui_exit" }
|
|
@@ -151,6 +165,9 @@ export type RuntimeSession = {
|
|
|
151
165
|
skills(): RuntimeSkillsSnapshot;
|
|
152
166
|
mcp(): McpInventorySnapshot;
|
|
153
167
|
supportsImageInput(): boolean;
|
|
168
|
+
reasoningEffort(): ReasoningEffortSnapshot | undefined;
|
|
169
|
+
setReasoningEffort(effort: string): ReasoningEffortSnapshot;
|
|
170
|
+
resetReasoningEffort(): ReasoningEffortSnapshot;
|
|
154
171
|
importImage(
|
|
155
172
|
sourcePath: string,
|
|
156
173
|
signal: AbortSignal,
|
|
@@ -162,6 +179,9 @@ export type RuntimeSession = {
|
|
|
162
179
|
): Promise<void>;
|
|
163
180
|
admitTurn(input: ExecuteTurnInput): Promise<AcceptedTurn>;
|
|
164
181
|
executeTurn(input: ExecuteTurnInput): Promise<RunAgentResult>;
|
|
182
|
+
promptScheduler(): PromptSchedulerSnapshot;
|
|
183
|
+
subscribePromptScheduler(listener: () => void): () => void;
|
|
184
|
+
queueFollowUp(userMessage: UserMessage): QueueFollowUpResult;
|
|
165
185
|
compactContext(): Promise<ContextCompactionResult>;
|
|
166
186
|
retireContext(): Promise<ContextRetirementResult>;
|
|
167
187
|
undoLatestFileMutationTurn(): Promise<TurnUndoResult>;
|
|
@@ -217,6 +237,15 @@ export type RuntimeSessionContext = {
|
|
|
217
237
|
iteration: IterationIdentity;
|
|
218
238
|
built: BuiltContextRequest;
|
|
219
239
|
}): void;
|
|
240
|
+
maintainContextAfterIteration?(input: {
|
|
241
|
+
turn: TurnIdentity;
|
|
242
|
+
consumedThroughOrdinal: number;
|
|
243
|
+
ledger: AgentTurnLedger;
|
|
244
|
+
}): Promise<void>;
|
|
245
|
+
applyQueuedSteering?(input: {
|
|
246
|
+
turn: TurnIdentity;
|
|
247
|
+
ledger: AgentTurnLedger;
|
|
248
|
+
}): Promise<number>;
|
|
220
249
|
};
|
|
221
250
|
|
|
222
251
|
export type ContextSurfaceRefreshSummary = {
|
|
@@ -344,10 +373,18 @@ type RuntimeSessionState =
|
|
|
344
373
|
| "disposed";
|
|
345
374
|
|
|
346
375
|
type ActiveTurn = {
|
|
376
|
+
turn: TurnIdentity;
|
|
347
377
|
controller: AbortController;
|
|
348
378
|
completion: Promise<RunAgentResult>;
|
|
349
379
|
};
|
|
350
380
|
|
|
381
|
+
type QueuedPrompt = {
|
|
382
|
+
readonly userMessage: UserMessage;
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const MAX_QUEUED_PROMPTS = 8;
|
|
386
|
+
const MAX_QUEUED_PROMPT_TEXT_BYTES = 64 * 1024;
|
|
387
|
+
|
|
351
388
|
type ActiveAdmission = {
|
|
352
389
|
controller: AbortController;
|
|
353
390
|
settled: Promise<void>;
|
|
@@ -405,6 +442,13 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
405
442
|
private ledger?: SessionLedger;
|
|
406
443
|
private activeAdmission?: ActiveAdmission;
|
|
407
444
|
private activeTurn?: ActiveTurn;
|
|
445
|
+
private executionChainRunning = false;
|
|
446
|
+
private readonly queuedPrompts: QueuedPrompt[] = [];
|
|
447
|
+
private promptSchedulerSnapshot: PromptSchedulerSnapshot = Object.freeze({
|
|
448
|
+
state: "idle",
|
|
449
|
+
pendingCount: 0,
|
|
450
|
+
});
|
|
451
|
+
private readonly promptSchedulerListeners = new Set<() => void>();
|
|
408
452
|
private activeContextRevision?: Promise<
|
|
409
453
|
ContextCompactionResult | ContextRetirementResult
|
|
410
454
|
>;
|
|
@@ -482,6 +526,9 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
482
526
|
onToolCompletionsCommitted: (completion) =>
|
|
483
527
|
this.onToolCompletionsCommitted(completion),
|
|
484
528
|
prepareModelDispatch: (dispatch) => this.prepareModelDispatch(dispatch),
|
|
529
|
+
maintainContextAfterIteration: (maintenance) =>
|
|
530
|
+
this.performActiveTurnContextMaintenance(maintenance),
|
|
531
|
+
applyQueuedSteering: (steering) => this.applyQueuedSteering(steering),
|
|
485
532
|
};
|
|
486
533
|
}
|
|
487
534
|
|
|
@@ -965,6 +1012,36 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
965
1012
|
return this.input.modelClient.inputModalities?.includes("image") === true;
|
|
966
1013
|
}
|
|
967
1014
|
|
|
1015
|
+
reasoningEffort(): ReasoningEffortSnapshot | undefined {
|
|
1016
|
+
return this.input.modelClient.reasoningEffort?.snapshot();
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
setReasoningEffort(effort: string): ReasoningEffortSnapshot {
|
|
1020
|
+
if (this.state !== "ready" || this.activeTurn !== undefined) {
|
|
1021
|
+
throw new Error(
|
|
1022
|
+
`Cannot change reasoning effort while RuntimeSession is ${this.state}.`,
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
const reasoningEffort = this.input.modelClient.reasoningEffort;
|
|
1026
|
+
if (reasoningEffort === undefined) {
|
|
1027
|
+
throw new Error("Current model profile does not configure reasoning effort.");
|
|
1028
|
+
}
|
|
1029
|
+
return reasoningEffort.set(effort);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
resetReasoningEffort(): ReasoningEffortSnapshot {
|
|
1033
|
+
if (this.state !== "ready" || this.activeTurn !== undefined) {
|
|
1034
|
+
throw new Error(
|
|
1035
|
+
`Cannot change reasoning effort while RuntimeSession is ${this.state}.`,
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
const reasoningEffort = this.input.modelClient.reasoningEffort;
|
|
1039
|
+
if (reasoningEffort === undefined) {
|
|
1040
|
+
throw new Error("Current model profile does not configure reasoning effort.");
|
|
1041
|
+
}
|
|
1042
|
+
return reasoningEffort.reset();
|
|
1043
|
+
}
|
|
1044
|
+
|
|
968
1045
|
bashGuard(): BashGuardSnapshot {
|
|
969
1046
|
return this.bashGuardSnapshot;
|
|
970
1047
|
}
|
|
@@ -1467,11 +1544,71 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
1467
1544
|
}
|
|
1468
1545
|
}
|
|
1469
1546
|
|
|
1547
|
+
promptScheduler(): PromptSchedulerSnapshot {
|
|
1548
|
+
return this.promptSchedulerSnapshot;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
subscribePromptScheduler(listener: () => void): () => void {
|
|
1552
|
+
this.promptSchedulerListeners.add(listener);
|
|
1553
|
+
return () => this.promptSchedulerListeners.delete(listener);
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
queueFollowUp(userMessage: UserMessage): QueueFollowUpResult {
|
|
1557
|
+
if (!this.executionChainRunning) {
|
|
1558
|
+
throw new Error("Cannot queue a follow-up while no execution chain is running.");
|
|
1559
|
+
}
|
|
1560
|
+
validateUserMessage(userMessage);
|
|
1561
|
+
if (userMessage.attachments !== undefined) {
|
|
1562
|
+
throw new Error("Active-turn follow-ups do not support image attachments.");
|
|
1563
|
+
}
|
|
1564
|
+
if (this.queuedPrompts.length >= MAX_QUEUED_PROMPTS) {
|
|
1565
|
+
throw new Error(`At most ${MAX_QUEUED_PROMPTS} follow-ups may be queued.`);
|
|
1566
|
+
}
|
|
1567
|
+
const queuedBytes = this.queuedPrompts.reduce(
|
|
1568
|
+
(total, entry) => total + Buffer.byteLength(entry.userMessage.content, "utf8"),
|
|
1569
|
+
0,
|
|
1570
|
+
);
|
|
1571
|
+
const nextBytes = Buffer.byteLength(userMessage.content, "utf8");
|
|
1572
|
+
if (queuedBytes + nextBytes > MAX_QUEUED_PROMPT_TEXT_BYTES) {
|
|
1573
|
+
throw new Error("Queued follow-ups exceed the 64 KiB text limit.");
|
|
1574
|
+
}
|
|
1575
|
+
this.queuedPrompts.push({
|
|
1576
|
+
userMessage: Object.freeze({ ...userMessage }),
|
|
1577
|
+
});
|
|
1578
|
+
this.notifyPromptScheduler();
|
|
1579
|
+
return Object.freeze({
|
|
1580
|
+
kind: "queued",
|
|
1581
|
+
pendingCount: this.queuedPrompts.length,
|
|
1582
|
+
...(this.activeTurn === undefined
|
|
1583
|
+
? {}
|
|
1584
|
+
: { activeTurnId: this.activeTurn.turn.turnId }),
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1470
1588
|
async executeTurn(input: ExecuteTurnInput): Promise<RunAgentResult> {
|
|
1471
1589
|
return (await this.admitTurn(input)).completion;
|
|
1472
1590
|
}
|
|
1473
1591
|
|
|
1474
1592
|
async admitTurn(input: ExecuteTurnInput): Promise<AcceptedTurn> {
|
|
1593
|
+
if (this.executionChainRunning) {
|
|
1594
|
+
throw new Error(
|
|
1595
|
+
`Cannot execute a turn while RuntimeSession is ${this.state}; a prompt chain is already executing.`,
|
|
1596
|
+
);
|
|
1597
|
+
}
|
|
1598
|
+
this.executionChainRunning = true;
|
|
1599
|
+
this.notifyPromptScheduler();
|
|
1600
|
+
try {
|
|
1601
|
+
const accepted = await this.admitSingleTurn(input);
|
|
1602
|
+
const completion = this.continueExecutionChain(accepted.completion, input.signal);
|
|
1603
|
+
return Object.freeze({ ...accepted, completion });
|
|
1604
|
+
} catch (error) {
|
|
1605
|
+
this.executionChainRunning = false;
|
|
1606
|
+
this.notifyPromptScheduler();
|
|
1607
|
+
throw error;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
private async admitSingleTurn(input: ExecuteTurnInput): Promise<AcceptedTurn> {
|
|
1475
1612
|
if (this.state !== "ready") {
|
|
1476
1613
|
throw new Error(`Cannot execute a turn while RuntimeSession is ${this.state}.`);
|
|
1477
1614
|
}
|
|
@@ -1544,7 +1681,8 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
1544
1681
|
usage: admissionSnapshot,
|
|
1545
1682
|
},
|
|
1546
1683
|
});
|
|
1547
|
-
this.activeTurn = { controller, completion };
|
|
1684
|
+
this.activeTurn = { turn, controller, completion };
|
|
1685
|
+
this.notifyPromptScheduler();
|
|
1548
1686
|
return Object.freeze({
|
|
1549
1687
|
turnId: turn.turnId,
|
|
1550
1688
|
userMessage: input.userMessage,
|
|
@@ -1566,6 +1704,79 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
1566
1704
|
}
|
|
1567
1705
|
}
|
|
1568
1706
|
|
|
1707
|
+
private async continueExecutionChain(
|
|
1708
|
+
initialCompletion: Promise<RunAgentResult>,
|
|
1709
|
+
signal: AbortSignal,
|
|
1710
|
+
): Promise<RunAgentResult> {
|
|
1711
|
+
let completion = initialCompletion;
|
|
1712
|
+
let finalResult: RunAgentResult;
|
|
1713
|
+
try {
|
|
1714
|
+
for (;;) {
|
|
1715
|
+
finalResult = await completion;
|
|
1716
|
+
if (finalResult.status !== "completed" || this.queuedPrompts.length === 0) {
|
|
1717
|
+
return finalResult;
|
|
1718
|
+
}
|
|
1719
|
+
const next = this.queuedPrompts[0];
|
|
1720
|
+
if (next === undefined) {
|
|
1721
|
+
return finalResult;
|
|
1722
|
+
}
|
|
1723
|
+
const accepted = await this.admitSingleTurn({
|
|
1724
|
+
userMessage: next.userMessage,
|
|
1725
|
+
signal,
|
|
1726
|
+
});
|
|
1727
|
+
this.queuedPrompts.shift();
|
|
1728
|
+
this.notifyPromptScheduler();
|
|
1729
|
+
completion = accepted.completion;
|
|
1730
|
+
}
|
|
1731
|
+
} finally {
|
|
1732
|
+
this.queuedPrompts.splice(0);
|
|
1733
|
+
this.executionChainRunning = false;
|
|
1734
|
+
this.notifyPromptScheduler();
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
private notifyPromptScheduler(): void {
|
|
1739
|
+
this.promptSchedulerSnapshot = Object.freeze({
|
|
1740
|
+
state: this.executionChainRunning ? "running" : "idle",
|
|
1741
|
+
...(this.activeTurn === undefined
|
|
1742
|
+
? {}
|
|
1743
|
+
: { activeTurnId: this.activeTurn.turn.turnId }),
|
|
1744
|
+
pendingCount: this.queuedPrompts.length,
|
|
1745
|
+
});
|
|
1746
|
+
for (const listener of this.promptSchedulerListeners) listener();
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
private async applyQueuedSteering(input: {
|
|
1750
|
+
turn: TurnIdentity;
|
|
1751
|
+
ledger: AgentTurnLedger;
|
|
1752
|
+
}): Promise<number> {
|
|
1753
|
+
if (this.activeTurn?.turn.turnId !== input.turn.turnId) {
|
|
1754
|
+
throw new Error("Cannot apply steering outside the active turn.");
|
|
1755
|
+
}
|
|
1756
|
+
if (this.queuedPrompts.length === 0) return 0;
|
|
1757
|
+
const drained = this.queuedPrompts.splice(0);
|
|
1758
|
+
const records = input.ledger.appendSteeringUserMessages(
|
|
1759
|
+
drained.map((entry) => entry.userMessage),
|
|
1760
|
+
);
|
|
1761
|
+
this.notifyPromptScheduler();
|
|
1762
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
1763
|
+
const record = records[index];
|
|
1764
|
+
const queued = drained[index];
|
|
1765
|
+
if (record === undefined || queued === undefined) {
|
|
1766
|
+
throw new Error("Steering ledger result did not match the drained queue.");
|
|
1767
|
+
}
|
|
1768
|
+
await this.append({
|
|
1769
|
+
type: "turn.steering.applied",
|
|
1770
|
+
...input.turn,
|
|
1771
|
+
data: {
|
|
1772
|
+
userPrompt: projectUserMessage(queued.userMessage),
|
|
1773
|
+
ordinal: record.ordinal,
|
|
1774
|
+
},
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
return records.length;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1569
1780
|
private settleAdmission(admission: ActiveAdmission): void {
|
|
1570
1781
|
if (this.activeAdmission !== admission) {
|
|
1571
1782
|
throw new Error("Runtime admission ownership was lost.");
|
|
@@ -1796,6 +2007,8 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
1796
2007
|
canSwitchSession(): boolean {
|
|
1797
2008
|
return (
|
|
1798
2009
|
this.state === "ready" &&
|
|
2010
|
+
!this.executionChainRunning &&
|
|
2011
|
+
this.queuedPrompts.length === 0 &&
|
|
1799
2012
|
this.activeTurn === undefined &&
|
|
1800
2013
|
(this.tooling?.taskManager
|
|
1801
2014
|
.listBackgroundTasks()
|
|
@@ -1983,6 +2196,7 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
1983
2196
|
} finally {
|
|
1984
2197
|
removeExternalAbortListener();
|
|
1985
2198
|
this.activeTurn = undefined;
|
|
2199
|
+
this.notifyPromptScheduler();
|
|
1986
2200
|
this.pendingAutomaticContextMaintenance = false;
|
|
1987
2201
|
if (this.state === "executing") {
|
|
1988
2202
|
this.state = "ready";
|
|
@@ -2016,6 +2230,129 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
2016
2230
|
}
|
|
2017
2231
|
}
|
|
2018
2232
|
|
|
2233
|
+
private async performActiveTurnContextMaintenance(input: {
|
|
2234
|
+
turn: TurnIdentity;
|
|
2235
|
+
consumedThroughOrdinal: number;
|
|
2236
|
+
ledger: AgentTurnLedger;
|
|
2237
|
+
}): Promise<void> {
|
|
2238
|
+
const automation = this.requireContextAutomation();
|
|
2239
|
+
if (!automation.automaticSwapOnly) return;
|
|
2240
|
+
if (this.state !== "executing") {
|
|
2241
|
+
throw new Error(
|
|
2242
|
+
`Cannot maintain active-turn context while RuntimeSession is ${this.state}.`,
|
|
2243
|
+
);
|
|
2244
|
+
}
|
|
2245
|
+
const manager = this.requireContextManager();
|
|
2246
|
+
const usage = manager.measureCurrent(input.turn.turnId, input.ledger);
|
|
2247
|
+
if (usage.pressure === "normal") return;
|
|
2248
|
+
|
|
2249
|
+
const qualificationId = requireAutomationQualificationId(automation);
|
|
2250
|
+
const compactionTrigger = {
|
|
2251
|
+
kind: "runtime_pressure",
|
|
2252
|
+
activeTurn: {
|
|
2253
|
+
turnId: input.turn.turnId,
|
|
2254
|
+
consumedThroughOrdinal: input.consumedThroughOrdinal,
|
|
2255
|
+
},
|
|
2256
|
+
} as const;
|
|
2257
|
+
this.pendingAutomaticContextMaintenance = false;
|
|
2258
|
+
this.state = "maintaining_context";
|
|
2259
|
+
try {
|
|
2260
|
+
await this.append({
|
|
2261
|
+
type: "context.revision.started",
|
|
2262
|
+
sessionId: this.sessionId,
|
|
2263
|
+
data: {
|
|
2264
|
+
strategy: "swap",
|
|
2265
|
+
reason: "runtime_pressure",
|
|
2266
|
+
policyVersion: "swap-only-v1",
|
|
2267
|
+
rendererFormat: "swap-observation-v1",
|
|
2268
|
+
qualificationId,
|
|
2269
|
+
},
|
|
2270
|
+
});
|
|
2271
|
+
let swap: ContextCompactionResult;
|
|
2272
|
+
try {
|
|
2273
|
+
swap = await manager.compact(compactionTrigger, input.ledger);
|
|
2274
|
+
await this.append({
|
|
2275
|
+
type: "context.revision.finished",
|
|
2276
|
+
sessionId: this.sessionId,
|
|
2277
|
+
data: contextRevisionFinishedData(swap, "runtime_pressure", qualificationId),
|
|
2278
|
+
});
|
|
2279
|
+
} catch (error) {
|
|
2280
|
+
const failure = automaticContextFailure(error, "compaction");
|
|
2281
|
+
await this.append({
|
|
2282
|
+
type: "context.revision.failed",
|
|
2283
|
+
sessionId: this.sessionId,
|
|
2284
|
+
data: {
|
|
2285
|
+
strategy: "swap",
|
|
2286
|
+
reason: "runtime_pressure",
|
|
2287
|
+
stage: failure.stage,
|
|
2288
|
+
errorCode: boundedContextErrorCode(failure.code),
|
|
2289
|
+
error: `Automatic context compaction failed at ${failure.stage}.`,
|
|
2290
|
+
qualificationId,
|
|
2291
|
+
},
|
|
2292
|
+
}).catch(() => undefined);
|
|
2293
|
+
if (failure.fatal) throw error;
|
|
2294
|
+
return;
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
if (
|
|
2298
|
+
!automation.automaticPrefixRetirement ||
|
|
2299
|
+
!automaticSwapNeedsRetirement(swap)
|
|
2300
|
+
) {
|
|
2301
|
+
return;
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
await this.append({
|
|
2305
|
+
type: "context.revision.started",
|
|
2306
|
+
sessionId: this.sessionId,
|
|
2307
|
+
data: {
|
|
2308
|
+
strategy: "retire_prefix",
|
|
2309
|
+
reason: "runtime_pressure",
|
|
2310
|
+
policyVersion: "recall-first-retirement-v1",
|
|
2311
|
+
baseRevisionNumber: this.store.loadContextSnapshot().revision.revisionNumber,
|
|
2312
|
+
qualificationId,
|
|
2313
|
+
},
|
|
2314
|
+
});
|
|
2315
|
+
try {
|
|
2316
|
+
const retirement = await manager.retirePrefix(
|
|
2317
|
+
{
|
|
2318
|
+
kind: "runtime_pressure",
|
|
2319
|
+
activeTurnId: input.turn.turnId,
|
|
2320
|
+
},
|
|
2321
|
+
input.ledger,
|
|
2322
|
+
);
|
|
2323
|
+
await this.append({
|
|
2324
|
+
type: "context.revision.finished",
|
|
2325
|
+
sessionId: this.sessionId,
|
|
2326
|
+
data: contextRetirementFinishedData(
|
|
2327
|
+
retirement,
|
|
2328
|
+
"runtime_pressure",
|
|
2329
|
+
qualificationId,
|
|
2330
|
+
),
|
|
2331
|
+
});
|
|
2332
|
+
} catch (error) {
|
|
2333
|
+
const failure = automaticContextFailure(error, "retirement");
|
|
2334
|
+
await this.append({
|
|
2335
|
+
type: "context.revision.failed",
|
|
2336
|
+
sessionId: this.sessionId,
|
|
2337
|
+
data: {
|
|
2338
|
+
strategy: "retire_prefix",
|
|
2339
|
+
reason: "runtime_pressure",
|
|
2340
|
+
stage: failure.stage,
|
|
2341
|
+
errorCode: boundedContextErrorCode(failure.code),
|
|
2342
|
+
error: `Automatic context retirement failed at ${failure.stage}.`,
|
|
2343
|
+
committed: failure.committed,
|
|
2344
|
+
qualificationId,
|
|
2345
|
+
},
|
|
2346
|
+
}).catch(() => undefined);
|
|
2347
|
+
if (failure.fatal) throw error;
|
|
2348
|
+
}
|
|
2349
|
+
} finally {
|
|
2350
|
+
if (this.state === "maintaining_context") {
|
|
2351
|
+
this.state = "executing";
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2019
2356
|
private notifyCompletedTurn(turn: TurnIdentity): void {
|
|
2020
2357
|
const hook = this.input.completedTurnHook;
|
|
2021
2358
|
if (hook === undefined) {
|
|
@@ -2233,6 +2570,9 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
2233
2570
|
}
|
|
2234
2571
|
|
|
2235
2572
|
this.state = "disposing";
|
|
2573
|
+
this.queuedPrompts.splice(0);
|
|
2574
|
+
this.executionChainRunning = false;
|
|
2575
|
+
this.notifyPromptScheduler();
|
|
2236
2576
|
const errors: unknown[] = this.faultCause === undefined ? [] : [this.faultCause];
|
|
2237
2577
|
const activeAdmission = this.activeAdmission;
|
|
2238
2578
|
if (activeAdmission !== undefined) {
|
|
@@ -100,6 +100,9 @@ export type PendingLedgerTurn = {
|
|
|
100
100
|
};
|
|
101
101
|
|
|
102
102
|
export type AgentTurnLedger = {
|
|
103
|
+
appendSteeringUserMessages(
|
|
104
|
+
messages: readonly UserMessage[],
|
|
105
|
+
): readonly CanonicalMessageRecord[];
|
|
103
106
|
appendAssistant(input: {
|
|
104
107
|
iteration: IterationIdentity;
|
|
105
108
|
message: AssistantMessage;
|
|
@@ -111,6 +114,7 @@ export type AgentTurnLedger = {
|
|
|
111
114
|
completions: readonly ToolCompletionInput[],
|
|
112
115
|
): readonly CommittedToolCompletion[];
|
|
113
116
|
buildModelRequest(tools: readonly ToolDefinition[]): BuiltContextRequest;
|
|
117
|
+
activateContextSnapshot(snapshot: StoredContextSnapshotV8): void;
|
|
114
118
|
};
|
|
115
119
|
|
|
116
120
|
export type CommittedToolCompletion = {
|
|
@@ -128,6 +132,13 @@ export type LedgerMutation =
|
|
|
128
132
|
admissionBase?: AdmissionBaseToken;
|
|
129
133
|
next: ProtocolContextView;
|
|
130
134
|
}
|
|
135
|
+
| {
|
|
136
|
+
kind: "append_steering_users";
|
|
137
|
+
turn: TurnIdentity;
|
|
138
|
+
frames: readonly ProtocolFrame[];
|
|
139
|
+
messages: readonly CanonicalMessageRecord[];
|
|
140
|
+
next: ProtocolContextView;
|
|
141
|
+
}
|
|
131
142
|
| {
|
|
132
143
|
kind: "append_assistant";
|
|
133
144
|
iteration: IterationIdentity;
|
|
@@ -188,9 +199,9 @@ export class InMemorySessionLedger implements SessionLedger {
|
|
|
188
199
|
private readonly validator = new ContextProtocolValidator();
|
|
189
200
|
private readonly contextBuilder: ContextBuilder;
|
|
190
201
|
private readonly revisionCompiler: ContextRevisionCompiler;
|
|
191
|
-
private
|
|
192
|
-
private
|
|
193
|
-
private
|
|
202
|
+
private revision: StoredContextRevisionV8;
|
|
203
|
+
private surface: StoredContextSurfaceV8;
|
|
204
|
+
private activeOverrides: readonly StoredContextOverrideV8[];
|
|
194
205
|
private readonly clock: () => string;
|
|
195
206
|
|
|
196
207
|
constructor(private readonly input: CreateInMemorySessionLedgerInput) {
|
|
@@ -354,6 +365,20 @@ export class InMemorySessionLedger implements SessionLedger {
|
|
|
354
365
|
return this.view.messages.length;
|
|
355
366
|
}
|
|
356
367
|
|
|
368
|
+
activateContextSnapshot(snapshot: StoredContextSnapshotV8): void {
|
|
369
|
+
this.requireHealthy("activate a context snapshot");
|
|
370
|
+
if (
|
|
371
|
+
snapshot.meta.sessionId !== this.input.sessionId ||
|
|
372
|
+
stableJsonStringify(snapshot.canonical) !== stableJsonStringify(this.view)
|
|
373
|
+
) {
|
|
374
|
+
throw new Error("Activated context snapshot does not match canonical history.");
|
|
375
|
+
}
|
|
376
|
+
this.revisionCompiler.compileActive(snapshot);
|
|
377
|
+
this.revision = snapshot.revision;
|
|
378
|
+
this.surface = snapshot.surface;
|
|
379
|
+
this.activeOverrides = snapshot.activeOverrides;
|
|
380
|
+
}
|
|
381
|
+
|
|
357
382
|
snapshot(
|
|
358
383
|
options: {
|
|
359
384
|
fullIntegrity?: boolean;
|
|
@@ -381,6 +406,74 @@ export class InMemorySessionLedger implements SessionLedger {
|
|
|
381
406
|
this.pending = undefined;
|
|
382
407
|
}
|
|
383
408
|
|
|
409
|
+
appendSteeringUserMessages(
|
|
410
|
+
pending: InMemoryPendingLedgerTurn,
|
|
411
|
+
userMessages: readonly UserMessage[],
|
|
412
|
+
): readonly CanonicalMessageRecord[] {
|
|
413
|
+
this.requirePending(pending, "append steering user messages");
|
|
414
|
+
this.assertNoOpenFrame();
|
|
415
|
+
if (userMessages.length === 0) {
|
|
416
|
+
return Object.freeze([]);
|
|
417
|
+
}
|
|
418
|
+
for (const userMessage of userMessages) {
|
|
419
|
+
validateUserMessage(userMessage);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const frames: ProtocolFrame[] = [];
|
|
423
|
+
const messages: CanonicalMessageRecord[] = [];
|
|
424
|
+
for (const userMessage of userMessages) {
|
|
425
|
+
const createdAt = this.clock();
|
|
426
|
+
const ordinal = this.view.messages.length + messages.length + 1;
|
|
427
|
+
const frameId = this.input.idFactory.createProtocolFrameId();
|
|
428
|
+
messages.push(
|
|
429
|
+
immutableRecord<CanonicalMessageRecord>({
|
|
430
|
+
messageId: this.input.idFactory.createMessageId(),
|
|
431
|
+
sessionId: this.input.sessionId,
|
|
432
|
+
frameId,
|
|
433
|
+
ordinal,
|
|
434
|
+
contentSha256: userMessageHash(userMessage),
|
|
435
|
+
createdAt,
|
|
436
|
+
role: "user",
|
|
437
|
+
turnId: pending.turn.turnId,
|
|
438
|
+
content: userMessage.content,
|
|
439
|
+
...(userMessage.attachments === undefined
|
|
440
|
+
? {}
|
|
441
|
+
: {
|
|
442
|
+
attachments: Object.freeze(
|
|
443
|
+
immutableCanonicalClone(userMessage.attachments),
|
|
444
|
+
),
|
|
445
|
+
}),
|
|
446
|
+
origin: "user",
|
|
447
|
+
}),
|
|
448
|
+
);
|
|
449
|
+
frames.push(
|
|
450
|
+
immutableRecord<ProtocolFrame>({
|
|
451
|
+
frameId,
|
|
452
|
+
sessionId: this.input.sessionId,
|
|
453
|
+
turnId: pending.turn.turnId,
|
|
454
|
+
kind: "user",
|
|
455
|
+
state: "closed",
|
|
456
|
+
firstOrdinal: ordinal,
|
|
457
|
+
lastOrdinal: ordinal,
|
|
458
|
+
createdAt,
|
|
459
|
+
closedAt: createdAt,
|
|
460
|
+
}),
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
const immutableFrames = Object.freeze(frames);
|
|
464
|
+
const immutableMessages = Object.freeze(messages);
|
|
465
|
+
const next = appendView(this.view, immutableFrames, immutableMessages, []);
|
|
466
|
+
this.validator.validate(next, { fullIntegrity: true });
|
|
467
|
+
this.commit({
|
|
468
|
+
kind: "append_steering_users",
|
|
469
|
+
turn: pending.turn,
|
|
470
|
+
frames: immutableFrames,
|
|
471
|
+
messages: immutableMessages,
|
|
472
|
+
next,
|
|
473
|
+
});
|
|
474
|
+
return immutableMessages;
|
|
475
|
+
}
|
|
476
|
+
|
|
384
477
|
appendAssistant(
|
|
385
478
|
pending: InMemoryPendingLedgerTurn,
|
|
386
479
|
input: {
|
|
@@ -761,11 +854,15 @@ class InMemoryPendingLedgerTurn implements PendingLedgerTurn {
|
|
|
761
854
|
readonly turn: TurnIdentity,
|
|
762
855
|
) {
|
|
763
856
|
this.agent = {
|
|
857
|
+
appendSteeringUserMessages: (messages) =>
|
|
858
|
+
this.ledger.appendSteeringUserMessages(this, messages),
|
|
764
859
|
appendAssistant: (input) => this.ledger.appendAssistant(this, input),
|
|
765
860
|
assertCanExecuteTool: (call) => this.ledger.assertCanExecuteTool(this, call),
|
|
766
861
|
commitToolCompletions: (completions) =>
|
|
767
862
|
this.ledger.commitToolCompletions(this, completions),
|
|
768
863
|
buildModelRequest: (tools) => this.ledger.buildTurnModelRequest(this, tools),
|
|
864
|
+
activateContextSnapshot: (snapshot) =>
|
|
865
|
+
this.ledger.activateContextSnapshot(snapshot),
|
|
769
866
|
};
|
|
770
867
|
}
|
|
771
868
|
|
package/src/cli/config.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type ModelContextBudget,
|
|
7
7
|
type ModelContextProfile,
|
|
8
8
|
} from "../model/model-context-profile";
|
|
9
|
+
import type { ModelApi } from "../model/model-api";
|
|
9
10
|
import {
|
|
10
11
|
loadModelProfiles,
|
|
11
12
|
persistDefaultProfile,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
unknownProfileError,
|
|
18
19
|
} from "./model-profiles";
|
|
19
20
|
import type { MemoryEmbeddingConfig } from "../memory/contracts";
|
|
21
|
+
import type { ReasoningEffortConfig } from "../model/reasoning-effort";
|
|
20
22
|
import {
|
|
21
23
|
parsePublicEnvironment,
|
|
22
24
|
type ParsedPublicEnvironment,
|
|
@@ -27,9 +29,11 @@ export type RunnerConfig = {
|
|
|
27
29
|
readonly sessionId: SessionId;
|
|
28
30
|
readonly workspaceRoot: string;
|
|
29
31
|
readonly modelName: string;
|
|
32
|
+
readonly api: ModelApi;
|
|
30
33
|
readonly apiKey: string;
|
|
31
34
|
readonly apiBase: string;
|
|
32
35
|
readonly maxIterations: number;
|
|
36
|
+
readonly reasoning?: ReasoningEffortConfig;
|
|
33
37
|
readonly includeReasoningContent: boolean;
|
|
34
38
|
readonly stream: boolean;
|
|
35
39
|
readonly contextProfile: ModelContextProfile;
|
|
@@ -166,10 +170,13 @@ function runnerConfigTemplateFromProfile(
|
|
|
166
170
|
return Object.freeze({
|
|
167
171
|
workspaceRoot: environment.workspaceRoot,
|
|
168
172
|
modelName: profile.model,
|
|
173
|
+
api: profile.api,
|
|
169
174
|
apiKey: profile.apiKey,
|
|
170
175
|
apiBase: profile.apiBase,
|
|
171
176
|
maxIterations: environment.maxIterations,
|
|
172
|
-
|
|
177
|
+
...(profile.reasoning === undefined ? {} : { reasoning: profile.reasoning }),
|
|
178
|
+
includeReasoningContent:
|
|
179
|
+
profile.api === "chat-completions" && profile.includeReasoningContent,
|
|
173
180
|
stream: profile.stream,
|
|
174
181
|
contextProfile,
|
|
175
182
|
contextBudget: deriveModelContextBudget(contextProfile),
|
|
@@ -193,10 +200,12 @@ function runnerConfigTemplateFromEnvironment(
|
|
|
193
200
|
return Object.freeze({
|
|
194
201
|
workspaceRoot: environment.workspaceRoot,
|
|
195
202
|
modelName: environment.modelName,
|
|
203
|
+
api: environment.api,
|
|
196
204
|
apiKey: environment.apiKey,
|
|
197
205
|
apiBase: environment.apiBase,
|
|
198
206
|
maxIterations: environment.maxIterations,
|
|
199
|
-
includeReasoningContent:
|
|
207
|
+
includeReasoningContent:
|
|
208
|
+
environment.api === "chat-completions" && environment.includeReasoningContent,
|
|
200
209
|
stream: environment.stream,
|
|
201
210
|
contextProfile,
|
|
202
211
|
contextBudget: deriveModelContextBudget(contextProfile),
|