tinker-agent 1.5.1 → 1.7.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 +52 -1
- package/README.md +15 -7
- package/package.json +8 -7
- package/src/agent/assistant-text-delta.ts +10 -0
- package/src/agent/loop.ts +116 -22
- package/src/agent/runtime-session.ts +248 -1
- package/src/cli/command-line.ts +9 -1
- package/src/cli/config.ts +17 -4
- package/src/cli/main.ts +1 -0
- package/src/cli/public-cli-contract.ts +4 -0
- package/src/cli/public-config-contract.ts +25 -1
- package/src/cli/run-runner.ts +5 -0
- package/src/cli/runner-dependencies.ts +4 -1
- package/src/cli/tui-runner.tsx +17 -2
- package/src/events/bash-result-detail.ts +13 -6
- package/src/events/observation-text-log.ts +26 -1
- package/src/events/stdout-event-printer.ts +18 -2
- package/src/events/types.ts +14 -2
- package/src/model/fake-model-client.ts +177 -0
- package/src/model/model-client.ts +3 -0
- package/src/model/openai-chat-model-client.ts +54 -15
- package/src/model/openai-chat-stream.ts +95 -72
- package/src/observation/observation-builder.ts +54 -6
- package/src/session/session-catalog.ts +17 -11
- package/src/session/session-store.ts +2 -0
- package/src/tools/bash-guard.ts +131 -0
- package/src/tools/bash-task.ts +129 -90
- package/src/tools/bash.ts +75 -13
- package/src/tools/delete.ts +182 -0
- package/src/tools/edit.ts +68 -9
- package/src/tools/registry.ts +49 -3
- package/src/tools/shell-process.ts +296 -0
- package/src/tools/task-input.ts +229 -0
- package/src/tools/task-output-tool.ts +4 -1
- package/src/tools/terminal-screen.ts +105 -0
- package/src/tools/turn-undo-manager.ts +794 -0
- package/src/tools/types.ts +45 -0
- package/src/tools/write.ts +65 -14
- package/src/tui/app.tsx +161 -45
- package/src/tui/assistant-markdown-section-framer.ts +135 -0
- package/src/tui/components/background-tasks.tsx +3 -2
- package/src/tui/components/bash-confirmation.tsx +27 -0
- package/src/tui/components/context-status.tsx +11 -1
- package/src/tui/components/footer.tsx +8 -5
- package/src/tui/components/prompt-input.tsx +13 -1
- package/src/tui/components/resume-session-picker.tsx +292 -46
- package/src/tui/components/timeline.tsx +10 -0
- package/src/tui/context-format.ts +17 -0
- package/src/tui/event-store.ts +76 -4
- package/src/tui/slash-commands.ts +28 -0
- package/src/tui/tui-projection-store.ts +246 -7
- package/src/tui/tui-session-controller.ts +19 -1
package/src/tui/event-store.ts
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
bashResultDetail,
|
|
4
4
|
type BashDisplayDetail,
|
|
5
5
|
} from "../events/bash-result-detail";
|
|
6
|
-
import type { AgentEvent } from "../events/types";
|
|
6
|
+
import type { AgentEvent, ModelRequestFailedData } from "../events/types";
|
|
7
7
|
import type { ContextUsageSnapshot } from "../agent/context-meter";
|
|
8
8
|
import type {
|
|
9
9
|
ModelContextBudget,
|
|
@@ -162,12 +162,21 @@ export function reduceTuiProjection(
|
|
|
162
162
|
})
|
|
163
163
|
: updateTurnItem(turn, modelRequestRef(event.iterationId), (item) => ({
|
|
164
164
|
...item,
|
|
165
|
-
text: `model iteration ${event.iterationNumber} · retrying`,
|
|
165
|
+
text: `model iteration ${event.iterationNumber} · retrying (attempt ${event.data.attemptNumber}/${event.data.maxAttempts})`,
|
|
166
166
|
status: "running",
|
|
167
167
|
})),
|
|
168
168
|
);
|
|
169
169
|
case "model.request.failed":
|
|
170
|
-
|
|
170
|
+
if (event.data.retryDisposition !== "scheduled") {
|
|
171
|
+
return state;
|
|
172
|
+
}
|
|
173
|
+
return updateActiveTurn(state, event, policy, (turn) =>
|
|
174
|
+
updateTurnItem(turn, modelRequestRef(event.iterationId), (item) => ({
|
|
175
|
+
...item,
|
|
176
|
+
text: `model iteration ${requireEventNumber(event.iterationNumber, "model.request.failed iterationNumber")} · ${modelRequestRetryText(event.data)}`,
|
|
177
|
+
status: "running",
|
|
178
|
+
})),
|
|
179
|
+
);
|
|
171
180
|
case "model.request.finished":
|
|
172
181
|
return updateActiveTurn(state, event, policy, (turn) =>
|
|
173
182
|
updateTurnItem(turn, modelRequestRef(event.iterationId), (item) => ({
|
|
@@ -214,6 +223,37 @@ export function reduceTuiProjection(
|
|
|
214
223
|
status: event.data.ok ? "ok" : "failed",
|
|
215
224
|
})),
|
|
216
225
|
);
|
|
226
|
+
case "tool.confirmation.requested":
|
|
227
|
+
return updateActiveTurn(state, event, policy, (turn) =>
|
|
228
|
+
updateTurnItem(
|
|
229
|
+
turn,
|
|
230
|
+
toolCallRef(
|
|
231
|
+
requireEventString(
|
|
232
|
+
event.toolCallId,
|
|
233
|
+
"tool.confirmation.requested toolCallId",
|
|
234
|
+
),
|
|
235
|
+
),
|
|
236
|
+
(item) => ({
|
|
237
|
+
...item,
|
|
238
|
+
text: `Bash confirmation requested · ${event.data.reason}`,
|
|
239
|
+
status: "running",
|
|
240
|
+
}),
|
|
241
|
+
),
|
|
242
|
+
);
|
|
243
|
+
case "tool.confirmation.resolved":
|
|
244
|
+
return updateActiveTurn(state, event, policy, (turn) =>
|
|
245
|
+
appendTurnItem(turn, {
|
|
246
|
+
id: `confirmation-${event.toolCallId}-${event.eventSequence}`,
|
|
247
|
+
label: "bash guard",
|
|
248
|
+
text: `${event.data.decision} · ${event.data.reason}`,
|
|
249
|
+
status:
|
|
250
|
+
event.data.decision === "allow"
|
|
251
|
+
? "ok"
|
|
252
|
+
: event.data.decision === "cancelled"
|
|
253
|
+
? "cancelled"
|
|
254
|
+
: "failed",
|
|
255
|
+
}),
|
|
256
|
+
);
|
|
217
257
|
case "bash.task.backgrounded":
|
|
218
258
|
case "bash.task.stopping":
|
|
219
259
|
case "bash.task.finished":
|
|
@@ -679,6 +719,21 @@ export function completedModelRequestText(
|
|
|
679
719
|
return `model iteration ${iterationNumber} -> assistant response`;
|
|
680
720
|
}
|
|
681
721
|
|
|
722
|
+
function modelRequestRetryText(data: ModelRequestFailedData): string {
|
|
723
|
+
const attempt = `attempt ${data.attemptNumber + 1}/${data.maxAttempts}`;
|
|
724
|
+
if (data.retryDelayMs !== undefined && data.retryDelayMs > 0) {
|
|
725
|
+
const seconds = Math.max(1, Math.round(data.retryDelayMs / 1000));
|
|
726
|
+
const reason =
|
|
727
|
+
data.code === "provider_rate_limited"
|
|
728
|
+
? "rate limited"
|
|
729
|
+
: data.code === "provider_unavailable"
|
|
730
|
+
? "provider unavailable"
|
|
731
|
+
: data.code;
|
|
732
|
+
return `${reason} · retrying in ${seconds}s (${attempt})`;
|
|
733
|
+
}
|
|
734
|
+
return `retrying (${attempt})`;
|
|
735
|
+
}
|
|
736
|
+
|
|
682
737
|
export function toolCallStartedProjection(input: {
|
|
683
738
|
name: string;
|
|
684
739
|
args: unknown;
|
|
@@ -716,7 +771,11 @@ function toolCallSummary(input: { name: string; args: unknown }): string {
|
|
|
716
771
|
if (input.name === "WebFetch") {
|
|
717
772
|
return `WebFetch ${toolUrl(input.args) ?? ""}`.trim();
|
|
718
773
|
}
|
|
719
|
-
if (
|
|
774
|
+
if (
|
|
775
|
+
input.name === "TaskOutput" ||
|
|
776
|
+
input.name === "TaskInput" ||
|
|
777
|
+
input.name === "TaskStop"
|
|
778
|
+
) {
|
|
720
779
|
return `${input.name} ${toolTaskId(input.args) ?? ""}`.trim();
|
|
721
780
|
}
|
|
722
781
|
if (input.name === "TaskList") {
|
|
@@ -774,6 +833,8 @@ function toolRawResultSummary(name: string, args: unknown, raw: ToolRawResult):
|
|
|
774
833
|
? base
|
|
775
834
|
: `${base} -> ${raw.bytesWritten} bytes`;
|
|
776
835
|
}
|
|
836
|
+
case "delete":
|
|
837
|
+
return `${base} -> deleted`;
|
|
777
838
|
case "web_search":
|
|
778
839
|
return raw.resultCount === undefined
|
|
779
840
|
? base
|
|
@@ -809,6 +870,10 @@ function toolRawResultSummary(name: string, args: unknown, raw: ToolRawResult):
|
|
|
809
870
|
return `${base} -> ${raw.status}, ${raw.outputLines} line${raw.outputLines === 1 ? "" : "s"}`;
|
|
810
871
|
}
|
|
811
872
|
return base;
|
|
873
|
+
case "task_input":
|
|
874
|
+
return raw.ok
|
|
875
|
+
? `${base} -> ${raw.status}, wrote ${raw.writtenBytes} byte${raw.writtenBytes === 1 ? "" : "s"}, ${raw.screenColumns}x${raw.screenRows}`
|
|
876
|
+
: base;
|
|
812
877
|
case "task_stop": {
|
|
813
878
|
if (raw.status === undefined) {
|
|
814
879
|
return base;
|
|
@@ -854,9 +919,14 @@ function toolRawResultBashDetail(raw: ToolRawResult): Pick<TimelineItem, "bash">
|
|
|
854
919
|
const detail = bashResultDetail(raw);
|
|
855
920
|
return detail === undefined ? {} : { bash: detail };
|
|
856
921
|
}
|
|
922
|
+
case "task_input": {
|
|
923
|
+
const detail = bashResultDetail(raw);
|
|
924
|
+
return detail === undefined ? {} : { bash: detail };
|
|
925
|
+
}
|
|
857
926
|
case "read":
|
|
858
927
|
case "write":
|
|
859
928
|
case "edit":
|
|
929
|
+
case "delete":
|
|
860
930
|
case "glob":
|
|
861
931
|
case "grep":
|
|
862
932
|
case "task_list":
|
|
@@ -890,11 +960,13 @@ function toolRawResultDiff(
|
|
|
890
960
|
diffTruncated: raw.patchTruncated === true,
|
|
891
961
|
};
|
|
892
962
|
case "read":
|
|
963
|
+
case "delete":
|
|
893
964
|
case "glob":
|
|
894
965
|
case "grep":
|
|
895
966
|
case "bash":
|
|
896
967
|
case "task_list":
|
|
897
968
|
case "task_output":
|
|
969
|
+
case "task_input":
|
|
898
970
|
case "task_stop":
|
|
899
971
|
case "web_search":
|
|
900
972
|
case "web_fetch":
|
|
@@ -24,6 +24,11 @@ export const SLASH_COMMANDS: readonly BuiltInSlashCommand[] = [
|
|
|
24
24
|
usage: "/mcp",
|
|
25
25
|
description: "Show MCP servers and runtime tools",
|
|
26
26
|
},
|
|
27
|
+
{
|
|
28
|
+
name: "yolo",
|
|
29
|
+
usage: "/yolo [on|off]",
|
|
30
|
+
description: "Show or change destructive Bash confirmation",
|
|
31
|
+
},
|
|
27
32
|
{
|
|
28
33
|
name: "memory",
|
|
29
34
|
usage: "/memory",
|
|
@@ -34,6 +39,11 @@ export const SLASH_COMMANDS: readonly BuiltInSlashCommand[] = [
|
|
|
34
39
|
usage: "/compact [retire]",
|
|
35
40
|
description: "Swap tool output or retire a cold history prefix",
|
|
36
41
|
},
|
|
42
|
+
{
|
|
43
|
+
name: "undo",
|
|
44
|
+
usage: "/undo",
|
|
45
|
+
description: "Undo the latest Write/Edit/Delete turn",
|
|
46
|
+
},
|
|
37
47
|
{
|
|
38
48
|
name: "clear",
|
|
39
49
|
usage: "/clear",
|
|
@@ -70,11 +80,14 @@ export const SLASH_COMMANDS: readonly BuiltInSlashCommand[] = [
|
|
|
70
80
|
|
|
71
81
|
export type ParsedSlashCommand =
|
|
72
82
|
| { type: "status" }
|
|
83
|
+
| { type: "yolo_status" }
|
|
84
|
+
| { type: "yolo"; enabled: boolean }
|
|
73
85
|
| { type: "skills" }
|
|
74
86
|
| { type: "mcp" }
|
|
75
87
|
| { type: "memory" }
|
|
76
88
|
| { type: "compact" }
|
|
77
89
|
| { type: "compact_retire" }
|
|
90
|
+
| { type: "undo" }
|
|
78
91
|
| { type: "clear" }
|
|
79
92
|
| { type: "fork" }
|
|
80
93
|
| { type: "view"; filePath: string }
|
|
@@ -111,6 +124,15 @@ export function parseSlashCommand(input: string): ParsedSlashCommand {
|
|
|
111
124
|
if (command === "/status" && tokens.length === 1) {
|
|
112
125
|
return { type: "status" };
|
|
113
126
|
}
|
|
127
|
+
if (command === "/yolo") {
|
|
128
|
+
if (tokens.length === 1) {
|
|
129
|
+
return { type: "yolo_status" };
|
|
130
|
+
}
|
|
131
|
+
if (tokens.length === 2 && (tokens[1] === "on" || tokens[1] === "off")) {
|
|
132
|
+
return { type: "yolo", enabled: tokens[1] === "on" };
|
|
133
|
+
}
|
|
134
|
+
throw slashCommandUsageError("yolo");
|
|
135
|
+
}
|
|
114
136
|
if (command === "/skills" && tokens.length === 1) {
|
|
115
137
|
return { type: "skills" };
|
|
116
138
|
}
|
|
@@ -135,6 +157,12 @@ export function parseSlashCommand(input: string): ParsedSlashCommand {
|
|
|
135
157
|
}
|
|
136
158
|
throw slashCommandUsageError("compact");
|
|
137
159
|
}
|
|
160
|
+
if (command === "/undo") {
|
|
161
|
+
if (tokens.length === 1) {
|
|
162
|
+
return { type: "undo" };
|
|
163
|
+
}
|
|
164
|
+
throw slashCommandUsageError("undo");
|
|
165
|
+
}
|
|
138
166
|
if (command === "/clear") {
|
|
139
167
|
if (tokens.length === 1) {
|
|
140
168
|
return { type: "clear" };
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { EventSink } from "../events/event-sink";
|
|
2
2
|
import type { AgentEvent } from "../events/types";
|
|
3
|
+
import type {
|
|
4
|
+
AssistantTextDeltaSink,
|
|
5
|
+
AssistantTextDeltaUpdate,
|
|
6
|
+
} from "../agent/assistant-text-delta";
|
|
3
7
|
import {
|
|
4
8
|
createInitialTuiProjectionState,
|
|
5
9
|
firstRunningIndex,
|
|
@@ -14,6 +18,7 @@ import {
|
|
|
14
18
|
type TuiProjectionPolicy,
|
|
15
19
|
validateTuiProjectionPolicy,
|
|
16
20
|
} from "./tui-projection-policy";
|
|
21
|
+
import { MarkdownSectionFramer } from "./assistant-markdown-section-framer";
|
|
17
22
|
|
|
18
23
|
export type TuiProjectionStoreInput = {
|
|
19
24
|
sessionId: string;
|
|
@@ -23,18 +28,49 @@ export type TuiProjectionStoreInput = {
|
|
|
23
28
|
initialSnapshot?: TuiProjectionState;
|
|
24
29
|
};
|
|
25
30
|
|
|
31
|
+
export type AssistantStreamSectionItem = Readonly<{
|
|
32
|
+
kind: "assistant-stream-section";
|
|
33
|
+
id: string;
|
|
34
|
+
iterationId: string;
|
|
35
|
+
attemptNumber: number;
|
|
36
|
+
sectionNumber: number;
|
|
37
|
+
markdown: string;
|
|
38
|
+
showAssistantLabel: boolean;
|
|
39
|
+
}>;
|
|
40
|
+
|
|
41
|
+
export type TuiCommittedItem = TimelineItem | AssistantStreamSectionItem;
|
|
42
|
+
|
|
43
|
+
export function isAssistantStreamSectionItem(
|
|
44
|
+
item: TuiCommittedItem,
|
|
45
|
+
): item is AssistantStreamSectionItem {
|
|
46
|
+
return "kind" in item && item.kind === "assistant-stream-section";
|
|
47
|
+
}
|
|
48
|
+
|
|
26
49
|
export type TuiTimelineLog = Readonly<{
|
|
27
|
-
committed: readonly
|
|
50
|
+
committed: readonly TuiCommittedItem[];
|
|
28
51
|
live: readonly TimelineItem[];
|
|
29
52
|
}>;
|
|
30
53
|
|
|
31
|
-
|
|
54
|
+
type AssistantStreamAttempt = {
|
|
55
|
+
readonly sessionId: string;
|
|
56
|
+
readonly turnId: string;
|
|
57
|
+
readonly turnNumber: number;
|
|
58
|
+
readonly iterationId: string;
|
|
59
|
+
readonly iterationNumber: number;
|
|
60
|
+
readonly attemptNumber: number;
|
|
61
|
+
readonly framer: MarkdownSectionFramer;
|
|
62
|
+
sectionCount: number;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export class TuiProjectionStore implements EventSink, AssistantTextDeltaSink {
|
|
32
66
|
readonly name = "tui-projection-store";
|
|
33
67
|
private readonly listeners = new Set<() => void>();
|
|
34
68
|
private readonly policy: TuiProjectionPolicy;
|
|
35
69
|
private readonly printed = new Set<string>();
|
|
70
|
+
private readonly physicallyAdoptedIterations = new Map<string, string>();
|
|
36
71
|
private snapshot: TuiProjectionState;
|
|
37
72
|
private log: TuiTimelineLog = { committed: [], live: [] };
|
|
73
|
+
private assistantStreamAttempt?: AssistantStreamAttempt;
|
|
38
74
|
|
|
39
75
|
constructor(input: TuiProjectionStoreInput) {
|
|
40
76
|
this.policy = validateTuiProjectionPolicy(
|
|
@@ -62,15 +98,40 @@ export class TuiProjectionStore implements EventSink {
|
|
|
62
98
|
|
|
63
99
|
async append(event: AgentEvent): Promise<void> {
|
|
64
100
|
const next = reduceTuiProjection(this.snapshot, event, this.policy);
|
|
65
|
-
|
|
101
|
+
const snapshotChanged = next !== this.snapshot;
|
|
102
|
+
this.snapshot = next;
|
|
103
|
+
const presentationChanged = this.applyAssistantStreamEvent(event);
|
|
104
|
+
if (!snapshotChanged && !presentationChanged) {
|
|
66
105
|
return;
|
|
67
106
|
}
|
|
68
107
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
108
|
+
if (snapshotChanged) {
|
|
109
|
+
this.refreshLog();
|
|
110
|
+
}
|
|
111
|
+
this.notifyListeners();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
updateAssistantTextDelta(update: AssistantTextDeltaUpdate): void {
|
|
115
|
+
const attempt = this.assistantStreamAttempt;
|
|
116
|
+
if (
|
|
117
|
+
attempt === undefined ||
|
|
118
|
+
update.content === "" ||
|
|
119
|
+
!sameAssistantStreamAttempt(attempt, update)
|
|
120
|
+
) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const frames = attempt.framer.push(update.content);
|
|
125
|
+
if (frames.length === 0) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const committed = [...this.log.committed];
|
|
129
|
+
for (const frame of frames) {
|
|
130
|
+
attempt.sectionCount += 1;
|
|
131
|
+
committed.push(this.sectionItem(attempt, frame.markdown));
|
|
73
132
|
}
|
|
133
|
+
this.log = { committed, live: this.log.live };
|
|
134
|
+
this.notifyListeners();
|
|
74
135
|
}
|
|
75
136
|
|
|
76
137
|
hydrate(snapshot: TuiProjectionState): void {
|
|
@@ -91,6 +152,130 @@ export class TuiProjectionStore implements EventSink {
|
|
|
91
152
|
this.policy,
|
|
92
153
|
);
|
|
93
154
|
this.refreshLog(visibleTimelineItems(this.snapshot));
|
|
155
|
+
this.notifyListeners();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private applyAssistantStreamEvent(event: AgentEvent): boolean {
|
|
159
|
+
switch (event.type) {
|
|
160
|
+
case "model.request.started":
|
|
161
|
+
this.assistantStreamAttempt = assistantStreamAttempt(event);
|
|
162
|
+
return false;
|
|
163
|
+
case "model.request.failed":
|
|
164
|
+
return this.failAssistantStreamAttempt(event);
|
|
165
|
+
case "model.request.finished":
|
|
166
|
+
return this.finishAssistantStreamAttempt(event);
|
|
167
|
+
case "assistant.progress":
|
|
168
|
+
if (
|
|
169
|
+
event.iterationId !== undefined &&
|
|
170
|
+
this.physicallyAdoptedIterations.has(event.iterationId)
|
|
171
|
+
) {
|
|
172
|
+
this.printed.add(`assistant-${event.iterationId}-${event.eventSequence}`);
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
case "turn.finished": {
|
|
176
|
+
const adopted = this.physicallyAdoptedIterations.has(
|
|
177
|
+
event.data.lastIteration.iterationId,
|
|
178
|
+
);
|
|
179
|
+
if (adopted) {
|
|
180
|
+
this.printed.add(`turn-${event.turnId}-final-${event.eventSequence}`);
|
|
181
|
+
}
|
|
182
|
+
this.clearAdoptedTurn(event.turnId);
|
|
183
|
+
this.assistantStreamAttempt = undefined;
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
case "turn.failed":
|
|
187
|
+
case "turn.cancelled":
|
|
188
|
+
this.clearAdoptedTurn(event.turnId);
|
|
189
|
+
this.assistantStreamAttempt = undefined;
|
|
190
|
+
return false;
|
|
191
|
+
case "session.finished":
|
|
192
|
+
this.physicallyAdoptedIterations.clear();
|
|
193
|
+
this.assistantStreamAttempt = undefined;
|
|
194
|
+
return false;
|
|
195
|
+
default:
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private failAssistantStreamAttempt(
|
|
201
|
+
event: Extract<AgentEvent, { type: "model.request.failed" }>,
|
|
202
|
+
): boolean {
|
|
203
|
+
const attempt = this.assistantStreamAttempt;
|
|
204
|
+
if (attempt === undefined || !sameAssistantStreamEvent(attempt, event)) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
this.assistantStreamAttempt = undefined;
|
|
208
|
+
if (event.data.retryDisposition !== "scheduled" || attempt.sectionCount === 0) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
this.appendCommitted({
|
|
212
|
+
id: `assistant-stream-retry-${attempt.iterationId}-${attempt.attemptNumber}`,
|
|
213
|
+
text: "assistant response interrupted · retrying",
|
|
214
|
+
status: "info",
|
|
215
|
+
});
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private finishAssistantStreamAttempt(
|
|
220
|
+
event: Extract<AgentEvent, { type: "model.request.finished" }>,
|
|
221
|
+
): boolean {
|
|
222
|
+
const attempt = this.assistantStreamAttempt;
|
|
223
|
+
if (attempt === undefined || !sameAssistantStreamEvent(attempt, event)) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
this.assistantStreamAttempt = undefined;
|
|
227
|
+
const result = attempt.framer.finish();
|
|
228
|
+
if (
|
|
229
|
+
attempt.sectionCount === 0 ||
|
|
230
|
+
result.content !== (event.data.output.message.content ?? "")
|
|
231
|
+
) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
this.printed.add(`model-${attempt.iterationId}`);
|
|
236
|
+
this.physicallyAdoptedIterations.set(attempt.iterationId, attempt.turnId);
|
|
237
|
+
if (result.tail === "") {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
attempt.sectionCount += 1;
|
|
241
|
+
this.appendCommitted(this.sectionItem(attempt, result.tail));
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private sectionItem(
|
|
246
|
+
attempt: AssistantStreamAttempt,
|
|
247
|
+
markdown: string,
|
|
248
|
+
): AssistantStreamSectionItem {
|
|
249
|
+
return Object.freeze({
|
|
250
|
+
kind: "assistant-stream-section",
|
|
251
|
+
id: `assistant-stream-${attempt.iterationId}-${attempt.attemptNumber}-${attempt.sectionCount}`,
|
|
252
|
+
iterationId: attempt.iterationId,
|
|
253
|
+
attemptNumber: attempt.attemptNumber,
|
|
254
|
+
sectionNumber: attempt.sectionCount,
|
|
255
|
+
markdown,
|
|
256
|
+
showAssistantLabel: attempt.sectionCount === 1,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private appendCommitted(item: TuiCommittedItem): void {
|
|
261
|
+
this.log = {
|
|
262
|
+
committed: [...this.log.committed, item],
|
|
263
|
+
live: this.log.live,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private clearAdoptedTurn(turnId: string | undefined): void {
|
|
268
|
+
if (turnId === undefined) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
for (const [iterationId, adoptedTurnId] of this.physicallyAdoptedIterations) {
|
|
272
|
+
if (adoptedTurnId === turnId) {
|
|
273
|
+
this.physicallyAdoptedIterations.delete(iterationId);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private notifyListeners(): void {
|
|
94
279
|
for (const listener of this.listeners) {
|
|
95
280
|
listener();
|
|
96
281
|
}
|
|
@@ -112,6 +297,60 @@ export class TuiProjectionStore implements EventSink {
|
|
|
112
297
|
}
|
|
113
298
|
}
|
|
114
299
|
|
|
300
|
+
function assistantStreamAttempt(
|
|
301
|
+
event: Extract<AgentEvent, { type: "model.request.started" }>,
|
|
302
|
+
): AssistantStreamAttempt | undefined {
|
|
303
|
+
if (
|
|
304
|
+
event.turnId === undefined ||
|
|
305
|
+
event.turnNumber === undefined ||
|
|
306
|
+
event.iterationId === undefined ||
|
|
307
|
+
event.iterationNumber === undefined
|
|
308
|
+
) {
|
|
309
|
+
return undefined;
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
sessionId: event.sessionId,
|
|
313
|
+
turnId: event.turnId,
|
|
314
|
+
turnNumber: event.turnNumber,
|
|
315
|
+
iterationId: event.iterationId,
|
|
316
|
+
iterationNumber: event.iterationNumber,
|
|
317
|
+
attemptNumber: event.data.attemptNumber,
|
|
318
|
+
framer: new MarkdownSectionFramer(),
|
|
319
|
+
sectionCount: 0,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function sameAssistantStreamAttempt(
|
|
324
|
+
attempt: AssistantStreamAttempt,
|
|
325
|
+
update: AssistantTextDeltaUpdate,
|
|
326
|
+
): boolean {
|
|
327
|
+
return (
|
|
328
|
+
attempt.sessionId === update.sessionId &&
|
|
329
|
+
attempt.turnId === update.turnId &&
|
|
330
|
+
attempt.turnNumber === update.turnNumber &&
|
|
331
|
+
attempt.iterationId === update.iterationId &&
|
|
332
|
+
attempt.iterationNumber === update.iterationNumber &&
|
|
333
|
+
attempt.attemptNumber === update.attemptNumber
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function sameAssistantStreamEvent(
|
|
338
|
+
attempt: AssistantStreamAttempt,
|
|
339
|
+
event: Extract<
|
|
340
|
+
AgentEvent,
|
|
341
|
+
{ type: "model.request.failed" | "model.request.finished" }
|
|
342
|
+
>,
|
|
343
|
+
): boolean {
|
|
344
|
+
return (
|
|
345
|
+
attempt.sessionId === event.sessionId &&
|
|
346
|
+
attempt.turnId === event.turnId &&
|
|
347
|
+
attempt.turnNumber === event.turnNumber &&
|
|
348
|
+
attempt.iterationId === event.iterationId &&
|
|
349
|
+
attempt.iterationNumber === event.iterationNumber &&
|
|
350
|
+
attempt.attemptNumber === event.data.attemptNumber
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
115
354
|
function validateInitialSnapshot(
|
|
116
355
|
input: Pick<TuiProjectionStoreInput, "sessionId" | "modelName" | "workspaceRoot">,
|
|
117
356
|
snapshot: TuiProjectionState,
|
|
@@ -5,11 +5,13 @@ import type {
|
|
|
5
5
|
import type {
|
|
6
6
|
ExecuteTurnInput,
|
|
7
7
|
AcceptedTurn,
|
|
8
|
+
BashGuardSnapshot,
|
|
8
9
|
RuntimeSession,
|
|
9
10
|
RuntimeSkillsSnapshot,
|
|
10
11
|
SessionDisposeReason,
|
|
11
12
|
} from "../agent/runtime-session";
|
|
12
13
|
import type { RunAgentResult, UserMessage } from "../agent/types";
|
|
14
|
+
import type { TurnUndoResult } from "../tools/turn-undo-manager";
|
|
13
15
|
import type { ImageAssetRef } from "../image/image-types";
|
|
14
16
|
import type { ImportedImageAsset } from "../image/image-asset-store";
|
|
15
17
|
import type { SessionId } from "../ids/runtime-id";
|
|
@@ -39,6 +41,10 @@ export type TuiSessionBinding = {
|
|
|
39
41
|
) => Promise<void>;
|
|
40
42
|
admitTurn?: (userMessage: UserMessage, signal: AbortSignal) => Promise<AcceptedTurn>;
|
|
41
43
|
executeTurn(userMessage: UserMessage, signal: AbortSignal): Promise<RunAgentResult>;
|
|
44
|
+
bashGuard(): BashGuardSnapshot;
|
|
45
|
+
subscribeBashGuard(listener: () => void): () => void;
|
|
46
|
+
setYoloMode(enabled: boolean): void;
|
|
47
|
+
resolveBashConfirmation(decision: "allow" | "deny"): Promise<void>;
|
|
42
48
|
};
|
|
43
49
|
|
|
44
50
|
export type TuiSessionController = {
|
|
@@ -47,6 +53,7 @@ export type TuiSessionController = {
|
|
|
47
53
|
listSessions: () => Promise<readonly SessionSummary[]>;
|
|
48
54
|
compact: () => Promise<ContextCompactionResult>;
|
|
49
55
|
retire: () => Promise<ContextRetirementResult>;
|
|
56
|
+
undo: () => Promise<TurnUndoResult>;
|
|
50
57
|
fork: (beforeCommit?: () => void) => Promise<SessionId>;
|
|
51
58
|
clear: (beforeCommit?: () => void) => Promise<void>;
|
|
52
59
|
resume: (sessionId: SessionId, beforeCommit?: () => void) => Promise<void>;
|
|
@@ -89,7 +96,7 @@ export class DefaultTuiSessionController implements TuiSessionController {
|
|
|
89
96
|
};
|
|
90
97
|
|
|
91
98
|
listSessions(): Promise<readonly SessionSummary[]> {
|
|
92
|
-
return this.catalog.
|
|
99
|
+
return this.catalog.listAll(this.binding.sessionId);
|
|
93
100
|
}
|
|
94
101
|
|
|
95
102
|
compact(): Promise<ContextCompactionResult> {
|
|
@@ -100,6 +107,12 @@ export class DefaultTuiSessionController implements TuiSessionController {
|
|
|
100
107
|
return this.serialize(() => this.binding.runtimeSession.retireContext());
|
|
101
108
|
}
|
|
102
109
|
|
|
110
|
+
undo(): Promise<TurnUndoResult> {
|
|
111
|
+
return this.serialize(() =>
|
|
112
|
+
this.binding.runtimeSession.undoLatestFileMutationTurn(),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
103
116
|
fork(beforeCommit?: () => void): Promise<SessionId> {
|
|
104
117
|
return this.serialize(async () => {
|
|
105
118
|
const targetSessionId = createUuidV7() as SessionId;
|
|
@@ -230,6 +243,11 @@ export function managedTuiBinding(input: {
|
|
|
230
243
|
userMessage,
|
|
231
244
|
signal,
|
|
232
245
|
} satisfies ExecuteTurnInput),
|
|
246
|
+
bashGuard: () => input.runtimeSession.bashGuard(),
|
|
247
|
+
subscribeBashGuard: (listener) => input.runtimeSession.subscribeBashGuard(listener),
|
|
248
|
+
setYoloMode: (enabled) => input.runtimeSession.setYoloMode(enabled),
|
|
249
|
+
resolveBashConfirmation: (decision) =>
|
|
250
|
+
input.runtimeSession.resolveBashConfirmation(decision),
|
|
233
251
|
};
|
|
234
252
|
}
|
|
235
253
|
|