tinker-agent 1.5.0 → 1.6.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 +48 -1
- package/README.md +13 -5
- package/package.json +7 -5
- 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/tui-runner.tsx +21 -2
- package/src/events/observation-text-log.ts +21 -0
- package/src/events/stdout-event-printer.ts +11 -0
- package/src/events/types.ts +14 -2
- package/src/model/fake-model-client.ts +190 -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 +11 -0
- package/src/session/session-store.ts +1 -0
- package/src/tools/bash-guard.ts +131 -0
- package/src/tools/bash.ts +31 -0
- package/src/tools/delete.ts +182 -0
- package/src/tools/edit.ts +68 -9
- package/src/tools/registry.ts +47 -3
- package/src/tools/turn-undo-manager.ts +794 -0
- package/src/tools/types.ts +13 -0
- package/src/tools/write.ts +65 -14
- package/src/tui/app.tsx +301 -134
- package/src/tui/assistant-markdown-section-framer.ts +135 -0
- package/src/tui/components/assistant-markdown.tsx +27 -26
- package/src/tui/components/background-tasks.tsx +7 -2
- package/src/tui/components/bash-confirmation.tsx +27 -0
- package/src/tui/components/context-status.tsx +11 -1
- package/src/tui/components/file-viewer.tsx +2 -2
- package/src/tui/components/footer.tsx +9 -12
- package/src/tui/components/memory-browser.tsx +1 -1
- package/src/tui/components/prompt-input.tsx +13 -1
- package/src/tui/components/resume-session-picker.tsx +3 -1
- package/src/tui/components/timeline.tsx +19 -11
- package/src/tui/context-format.ts +17 -0
- package/src/tui/event-store.ts +75 -3
- package/src/tui/shiki-highlighter.ts +104 -0
- package/src/tui/slash-commands.ts +28 -0
- package/src/tui/tui-projection-store.ts +277 -5
- package/src/tui/tui-session-controller.ts +32 -8
|
@@ -73,8 +73,13 @@ import {
|
|
|
73
73
|
} from "../context/compiled-context-hash";
|
|
74
74
|
import { createDefaultTooling, type DefaultTooling } from "../tools/registry";
|
|
75
75
|
import type { ToolExecutor } from "../tools/types";
|
|
76
|
+
import type { TurnUndoResult } from "../tools/turn-undo-manager";
|
|
76
77
|
import type { Refiner } from "../tools/web-fetch/refiner";
|
|
77
78
|
import type { ProjectInstructionManifest } from "../instructions/project-instructions";
|
|
79
|
+
import type {
|
|
80
|
+
AssistantTextDeltaSink,
|
|
81
|
+
AssistantTextDeltaUpdate,
|
|
82
|
+
} from "./assistant-text-delta";
|
|
78
83
|
import {
|
|
79
84
|
SessionStore,
|
|
80
85
|
createSessionCompatibilityContract,
|
|
@@ -91,7 +96,7 @@ import {
|
|
|
91
96
|
type AdmissionBaseToken,
|
|
92
97
|
type SessionLedger,
|
|
93
98
|
} from "./session-ledger";
|
|
94
|
-
import { TurnCancelledError } from "./turn-cancellation";
|
|
99
|
+
import { cancellationError, TurnCancelledError } from "./turn-cancellation";
|
|
95
100
|
import type { ToolCompletionInput } from "../context/protocol-frame";
|
|
96
101
|
import type { BuiltContextRequest } from "../context/context-revision";
|
|
97
102
|
import type { CommittedToolCompletion } from "./session-ledger";
|
|
@@ -159,11 +164,27 @@ export type RuntimeSession = {
|
|
|
159
164
|
executeTurn(input: ExecuteTurnInput): Promise<RunAgentResult>;
|
|
160
165
|
compactContext(): Promise<ContextCompactionResult>;
|
|
161
166
|
retireContext(): Promise<ContextRetirementResult>;
|
|
167
|
+
undoLatestFileMutationTurn(): Promise<TurnUndoResult>;
|
|
162
168
|
cloneSession(targetSessionId: SessionId): Promise<void>;
|
|
163
169
|
canSwitchSession(): boolean;
|
|
170
|
+
bashGuard(): BashGuardSnapshot;
|
|
171
|
+
subscribeBashGuard(listener: () => void): () => void;
|
|
172
|
+
setYoloMode(enabled: boolean): void;
|
|
173
|
+
resolveBashConfirmation(decision: "allow" | "deny"): Promise<void>;
|
|
164
174
|
dispose(reason: SessionDisposeReason): Promise<void>;
|
|
165
175
|
};
|
|
166
176
|
|
|
177
|
+
export type BashGuardSource = "default" | "environment" | "cli" | "session";
|
|
178
|
+
|
|
179
|
+
export type BashGuardSnapshot = {
|
|
180
|
+
readonly mode: "guard" | "yolo";
|
|
181
|
+
readonly source: BashGuardSource;
|
|
182
|
+
readonly pending?: {
|
|
183
|
+
readonly command: string;
|
|
184
|
+
readonly reason: string;
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
|
|
167
188
|
const EMPTY_MCP_INVENTORY: McpInventorySnapshot = Object.freeze({
|
|
168
189
|
servers: Object.freeze([]),
|
|
169
190
|
});
|
|
@@ -187,6 +208,7 @@ export type RuntimeSessionContext = {
|
|
|
187
208
|
): ToolCallIdentity;
|
|
188
209
|
finishIterationForContinuation(iteration: IterationIdentity): void;
|
|
189
210
|
append(input: AgentEventInput): Promise<void>;
|
|
211
|
+
updateAssistantTextDelta?(update: AssistantTextDeltaUpdate): void;
|
|
190
212
|
onToolCompletionsCommitted?(input: {
|
|
191
213
|
completions: readonly ToolCompletionInput[];
|
|
192
214
|
committed: readonly CommittedToolCompletion[];
|
|
@@ -247,6 +269,7 @@ type CommonRuntimeSessionInput = {
|
|
|
247
269
|
projectInstruction?: ProjectInstructionManifest;
|
|
248
270
|
skillCatalog?: SkillCatalogSnapshot;
|
|
249
271
|
presentationSinks?: EventSink[];
|
|
272
|
+
assistantTextDeltaSink?: AssistantTextDeltaSink;
|
|
250
273
|
persistence?:
|
|
251
274
|
| false
|
|
252
275
|
| {
|
|
@@ -257,6 +280,12 @@ type CommonRuntimeSessionInput = {
|
|
|
257
280
|
toolingConfig?: PublicToolingConfig;
|
|
258
281
|
memorySearch?: ToolExecutor;
|
|
259
282
|
completedTurnHook?: CompletedTurnHook;
|
|
283
|
+
enableTurnUndo?: boolean;
|
|
284
|
+
bashGuard?: {
|
|
285
|
+
readonly mode: "guard" | "yolo";
|
|
286
|
+
readonly source: Exclude<BashGuardSource, "session">;
|
|
287
|
+
readonly surface: "tui" | "one-shot";
|
|
288
|
+
};
|
|
260
289
|
};
|
|
261
290
|
|
|
262
291
|
type CreateNewRuntimeSessionInput = CommonRuntimeSessionInput & {
|
|
@@ -309,6 +338,7 @@ type RuntimeSessionState =
|
|
|
309
338
|
| "executing"
|
|
310
339
|
| "compacting"
|
|
311
340
|
| "maintaining_context"
|
|
341
|
+
| "undoing"
|
|
312
342
|
| "faulted"
|
|
313
343
|
| "disposing"
|
|
314
344
|
| "disposed";
|
|
@@ -388,6 +418,20 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
388
418
|
private pendingAutomaticContextMaintenance = false;
|
|
389
419
|
private readonly skillCatalog: SkillCatalogSnapshot;
|
|
390
420
|
private skillCoordinator = new SkillActivationCoordinator();
|
|
421
|
+
private bashGuardMode: "guard" | "yolo";
|
|
422
|
+
private bashGuardSource: BashGuardSource;
|
|
423
|
+
private bashGuardSnapshot: BashGuardSnapshot;
|
|
424
|
+
private readonly bashGuardListeners = new Set<() => void>();
|
|
425
|
+
private assistantTextDeltaSinkDisabled = false;
|
|
426
|
+
private pendingBashConfirmation?: {
|
|
427
|
+
readonly command: string;
|
|
428
|
+
readonly reason: string;
|
|
429
|
+
readonly startedAt: number;
|
|
430
|
+
readonly call: ToolCallIdentity;
|
|
431
|
+
readonly resolve: (decision: "allow" | "deny") => void;
|
|
432
|
+
readonly reject: (error: unknown) => void;
|
|
433
|
+
readonly removeAbortListener: () => void;
|
|
434
|
+
};
|
|
391
435
|
|
|
392
436
|
private readonly context: RuntimeSessionContext;
|
|
393
437
|
|
|
@@ -401,6 +445,12 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
401
445
|
) {
|
|
402
446
|
this.sessionId = input.selection.sessionId;
|
|
403
447
|
this.resumed = input.selection.mode === "resume";
|
|
448
|
+
this.bashGuardMode = input.bashGuard?.mode ?? "guard";
|
|
449
|
+
this.bashGuardSource = input.bashGuard?.source ?? "default";
|
|
450
|
+
this.bashGuardSnapshot = Object.freeze({
|
|
451
|
+
mode: this.bashGuardMode,
|
|
452
|
+
source: this.bashGuardSource,
|
|
453
|
+
});
|
|
404
454
|
this.skillCatalog =
|
|
405
455
|
input.skillCatalog ??
|
|
406
456
|
createSkillCatalogSnapshot({
|
|
@@ -423,6 +473,12 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
423
473
|
finishIterationForContinuation: (iteration) =>
|
|
424
474
|
this.finishIterationForContinuation(iteration),
|
|
425
475
|
append: (event) => this.append(event),
|
|
476
|
+
...(input.assistantTextDeltaSink === undefined
|
|
477
|
+
? {}
|
|
478
|
+
: {
|
|
479
|
+
updateAssistantTextDelta: (update: AssistantTextDeltaUpdate) =>
|
|
480
|
+
this.updateAssistantTextDelta(update),
|
|
481
|
+
}),
|
|
426
482
|
onToolCompletionsCommitted: (completion) =>
|
|
427
483
|
this.onToolCompletionsCommitted(completion),
|
|
428
484
|
prepareModelDispatch: (dispatch) => this.prepareModelDispatch(dispatch),
|
|
@@ -556,8 +612,14 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
556
612
|
workspaceRoot: input.workspaceRoot,
|
|
557
613
|
runtimeSession: session.context,
|
|
558
614
|
historyReader: store.historyReader(),
|
|
615
|
+
...(input.enableTurnUndo === true ? { enableTurnUndo: true } : {}),
|
|
559
616
|
webFetchRefiner: input.webFetchRefiner,
|
|
560
617
|
toolingConfig: input.toolingConfig,
|
|
618
|
+
bashGuard: {
|
|
619
|
+
surface: input.bashGuard?.surface ?? "one-shot",
|
|
620
|
+
confirm: (call, request, signal) =>
|
|
621
|
+
session.confirmBashCommand(call, request, signal),
|
|
622
|
+
},
|
|
561
623
|
...(input.memorySearch === undefined
|
|
562
624
|
? {}
|
|
563
625
|
: { memorySearch: input.memorySearch }),
|
|
@@ -902,6 +964,134 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
902
964
|
return this.input.modelClient.inputModalities?.includes("image") === true;
|
|
903
965
|
}
|
|
904
966
|
|
|
967
|
+
bashGuard(): BashGuardSnapshot {
|
|
968
|
+
return this.bashGuardSnapshot;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
private refreshBashGuardSnapshot(): void {
|
|
972
|
+
this.bashGuardSnapshot = Object.freeze({
|
|
973
|
+
mode: this.bashGuardMode,
|
|
974
|
+
source: this.bashGuardSource,
|
|
975
|
+
...(this.pendingBashConfirmation === undefined
|
|
976
|
+
? {}
|
|
977
|
+
: {
|
|
978
|
+
pending: Object.freeze({
|
|
979
|
+
command: this.pendingBashConfirmation.command,
|
|
980
|
+
reason: this.pendingBashConfirmation.reason,
|
|
981
|
+
}),
|
|
982
|
+
}),
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
subscribeBashGuard(listener: () => void): () => void {
|
|
987
|
+
this.bashGuardListeners.add(listener);
|
|
988
|
+
return () => this.bashGuardListeners.delete(listener);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
setYoloMode(enabled: boolean): void {
|
|
992
|
+
this.bashGuardMode = enabled ? "yolo" : "guard";
|
|
993
|
+
this.bashGuardSource = "session";
|
|
994
|
+
this.refreshBashGuardSnapshot();
|
|
995
|
+
this.notifyBashGuardListeners();
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
async resolveBashConfirmation(decision: "allow" | "deny"): Promise<void> {
|
|
999
|
+
const pending = this.pendingBashConfirmation;
|
|
1000
|
+
if (pending === undefined) {
|
|
1001
|
+
throw new Error("No Bash confirmation is pending.");
|
|
1002
|
+
}
|
|
1003
|
+
this.pendingBashConfirmation = undefined;
|
|
1004
|
+
this.refreshBashGuardSnapshot();
|
|
1005
|
+
pending.removeAbortListener();
|
|
1006
|
+
await this.append({
|
|
1007
|
+
type: "tool.confirmation.resolved",
|
|
1008
|
+
...pending.call,
|
|
1009
|
+
data: {
|
|
1010
|
+
command: pending.command,
|
|
1011
|
+
reason: pending.reason,
|
|
1012
|
+
decision,
|
|
1013
|
+
durationMs: Date.now() - pending.startedAt,
|
|
1014
|
+
},
|
|
1015
|
+
});
|
|
1016
|
+
pending.resolve(decision);
|
|
1017
|
+
this.notifyBashGuardListeners();
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
private async confirmBashCommand(
|
|
1021
|
+
call: ToolCallIdentity,
|
|
1022
|
+
request: { command: string; reason: string },
|
|
1023
|
+
signal: AbortSignal,
|
|
1024
|
+
): Promise<"allow" | "deny"> {
|
|
1025
|
+
const startedAt = Date.now();
|
|
1026
|
+
await this.append({
|
|
1027
|
+
type: "tool.confirmation.requested",
|
|
1028
|
+
...call,
|
|
1029
|
+
data: request,
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
const surface = this.input.bashGuard?.surface ?? "one-shot";
|
|
1033
|
+
if (this.bashGuardMode === "yolo" || surface === "one-shot") {
|
|
1034
|
+
const decision = this.bashGuardMode === "yolo" ? "allow" : "deny";
|
|
1035
|
+
await this.append({
|
|
1036
|
+
type: "tool.confirmation.resolved",
|
|
1037
|
+
...call,
|
|
1038
|
+
data: {
|
|
1039
|
+
...request,
|
|
1040
|
+
decision,
|
|
1041
|
+
durationMs: Date.now() - startedAt,
|
|
1042
|
+
},
|
|
1043
|
+
});
|
|
1044
|
+
return decision;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
if (this.pendingBashConfirmation !== undefined) {
|
|
1048
|
+
throw new Error("Another Bash confirmation is already pending.");
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
return new Promise<"allow" | "deny">((resolve, reject) => {
|
|
1052
|
+
const onAbort = () => {
|
|
1053
|
+
const pending = this.pendingBashConfirmation;
|
|
1054
|
+
if (pending?.call.toolCallId !== call.toolCallId) {
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
this.pendingBashConfirmation = undefined;
|
|
1058
|
+
this.refreshBashGuardSnapshot();
|
|
1059
|
+
void this.append({
|
|
1060
|
+
type: "tool.confirmation.resolved",
|
|
1061
|
+
...call,
|
|
1062
|
+
data: {
|
|
1063
|
+
...request,
|
|
1064
|
+
decision: "cancelled",
|
|
1065
|
+
durationMs: Date.now() - startedAt,
|
|
1066
|
+
},
|
|
1067
|
+
}).finally(() => {
|
|
1068
|
+
reject(cancellationError(signal));
|
|
1069
|
+
this.notifyBashGuardListeners();
|
|
1070
|
+
});
|
|
1071
|
+
};
|
|
1072
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1073
|
+
this.pendingBashConfirmation = {
|
|
1074
|
+
...request,
|
|
1075
|
+
startedAt,
|
|
1076
|
+
call,
|
|
1077
|
+
resolve,
|
|
1078
|
+
reject,
|
|
1079
|
+
removeAbortListener: () => signal.removeEventListener("abort", onAbort),
|
|
1080
|
+
};
|
|
1081
|
+
this.refreshBashGuardSnapshot();
|
|
1082
|
+
this.notifyBashGuardListeners();
|
|
1083
|
+
if (signal.aborted) {
|
|
1084
|
+
onAbort();
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
private notifyBashGuardListeners(): void {
|
|
1090
|
+
for (const listener of this.bashGuardListeners) {
|
|
1091
|
+
listener();
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
905
1095
|
async importImage(
|
|
906
1096
|
sourcePath: string,
|
|
907
1097
|
signal: AbortSignal,
|
|
@@ -1613,6 +1803,36 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
1613
1803
|
);
|
|
1614
1804
|
}
|
|
1615
1805
|
|
|
1806
|
+
async undoLatestFileMutationTurn(): Promise<TurnUndoResult> {
|
|
1807
|
+
if (
|
|
1808
|
+
this.state !== "ready" ||
|
|
1809
|
+
this.activeTurn !== undefined ||
|
|
1810
|
+
this.activeContextRevision !== undefined
|
|
1811
|
+
) {
|
|
1812
|
+
throw new Error("Cannot undo while a turn or context operation is active.");
|
|
1813
|
+
}
|
|
1814
|
+
const tooling = this.requireTooling();
|
|
1815
|
+
if (
|
|
1816
|
+
tooling.taskManager
|
|
1817
|
+
.listBackgroundTasks()
|
|
1818
|
+
.some((task) => task.status === "running" || task.status === "stopping")
|
|
1819
|
+
) {
|
|
1820
|
+
throw new Error("Cannot undo while a background task is active.");
|
|
1821
|
+
}
|
|
1822
|
+
if (tooling.turnUndoManager === undefined) {
|
|
1823
|
+
return { status: "nothing" };
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
this.state = "undoing";
|
|
1827
|
+
try {
|
|
1828
|
+
return await tooling.turnUndoManager.undoLatest();
|
|
1829
|
+
} finally {
|
|
1830
|
+
if (this.state === "undoing") {
|
|
1831
|
+
this.state = "ready";
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1616
1836
|
async cloneSession(targetSessionId: SessionId): Promise<void> {
|
|
1617
1837
|
if (!this.canSwitchSession()) {
|
|
1618
1838
|
throw new Error(
|
|
@@ -1744,6 +1964,7 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
1744
1964
|
await this.appendTerminalEvent(turn, result, projectedMessageCount);
|
|
1745
1965
|
pendingLedgerTurn.finish(result);
|
|
1746
1966
|
settled = true;
|
|
1967
|
+
this.requireTooling().turnUndoManager?.completeTurn(turn);
|
|
1747
1968
|
if (result.status === "completed") {
|
|
1748
1969
|
this.notifyCompletedTurn(turn);
|
|
1749
1970
|
}
|
|
@@ -2156,6 +2377,32 @@ class DefaultRuntimeSession implements RuntimeSession {
|
|
|
2156
2377
|
return identity;
|
|
2157
2378
|
}
|
|
2158
2379
|
|
|
2380
|
+
private updateAssistantTextDelta(update: AssistantTextDeltaUpdate): void {
|
|
2381
|
+
const sink = this.input.assistantTextDeltaSink;
|
|
2382
|
+
const iteration = this.iterations.get(update.iterationId);
|
|
2383
|
+
if (
|
|
2384
|
+
sink === undefined ||
|
|
2385
|
+
this.assistantTextDeltaSinkDisabled ||
|
|
2386
|
+
this.state !== "executing" ||
|
|
2387
|
+
update.content === "" ||
|
|
2388
|
+
!Number.isSafeInteger(update.attemptNumber) ||
|
|
2389
|
+
update.attemptNumber < 1 ||
|
|
2390
|
+
iteration === undefined ||
|
|
2391
|
+
iteration.sessionId !== update.sessionId ||
|
|
2392
|
+
iteration.turnId !== update.turnId ||
|
|
2393
|
+
iteration.turnNumber !== update.turnNumber ||
|
|
2394
|
+
iteration.iterationNumber !== update.iterationNumber
|
|
2395
|
+
) {
|
|
2396
|
+
return;
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
try {
|
|
2400
|
+
sink.updateAssistantTextDelta(update);
|
|
2401
|
+
} catch {
|
|
2402
|
+
this.assistantTextDeltaSinkDisabled = true;
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2159
2406
|
private createToolCall(
|
|
2160
2407
|
iteration: IterationIdentity,
|
|
2161
2408
|
toolCallNumber: number,
|
package/src/cli/command-line.ts
CHANGED
|
@@ -8,6 +8,7 @@ export type CliCommand =
|
|
|
8
8
|
| {
|
|
9
9
|
readonly type: "run";
|
|
10
10
|
readonly profileName?: string;
|
|
11
|
+
readonly yolo?: boolean;
|
|
11
12
|
readonly promptSource: PromptSource;
|
|
12
13
|
};
|
|
13
14
|
|
|
@@ -72,6 +73,7 @@ export async function parseCommandLine(
|
|
|
72
73
|
.option(contract.run.profileOption.flags, contract.run.profileOption.description)
|
|
73
74
|
.option(contract.run.stdinOption.flags, contract.run.stdinOption.description)
|
|
74
75
|
.option(contract.run.fileOption.flags, contract.run.fileOption.description)
|
|
76
|
+
.option(contract.run.yoloOption.flags, contract.run.yoloOption.description)
|
|
75
77
|
.addHelpText("after", `\n${contract.run.helpAfter}\n`)
|
|
76
78
|
.showHelpAfterError('Run "tinker run --help" for usage.')
|
|
77
79
|
.showSuggestionAfterError(false)
|
|
@@ -80,7 +82,12 @@ export async function parseCommandLine(
|
|
|
80
82
|
.action(
|
|
81
83
|
(
|
|
82
84
|
prompt: string | undefined,
|
|
83
|
-
options: {
|
|
85
|
+
options: {
|
|
86
|
+
profile?: string;
|
|
87
|
+
stdin?: boolean;
|
|
88
|
+
file?: string;
|
|
89
|
+
yolo?: boolean;
|
|
90
|
+
},
|
|
84
91
|
) => {
|
|
85
92
|
const sources: PromptSource[] = [];
|
|
86
93
|
if (prompt !== undefined) {
|
|
@@ -116,6 +123,7 @@ export async function parseCommandLine(
|
|
|
116
123
|
...(options.profile === undefined
|
|
117
124
|
? {}
|
|
118
125
|
: { profileName: validateProfile(options.profile, "run") }),
|
|
126
|
+
...(options.yolo === true ? { yolo: true } : {}),
|
|
119
127
|
promptSource,
|
|
120
128
|
});
|
|
121
129
|
},
|
package/src/cli/config.ts
CHANGED
|
@@ -37,11 +37,14 @@ export type RunnerConfig = {
|
|
|
37
37
|
readonly profileName?: string;
|
|
38
38
|
readonly inputModalities: readonly ModelInputModality[];
|
|
39
39
|
readonly tokenEstimator?: ModelTokenEstimatorProfile;
|
|
40
|
+
readonly bashGuardMode: "guard" | "yolo";
|
|
41
|
+
readonly bashGuardSource: "default" | "environment" | "cli";
|
|
40
42
|
};
|
|
41
43
|
|
|
42
44
|
export type RunnerConfigSelection = {
|
|
43
45
|
readonly sessionId: SessionId;
|
|
44
46
|
readonly profileName?: string;
|
|
47
|
+
readonly yolo?: boolean;
|
|
45
48
|
};
|
|
46
49
|
|
|
47
50
|
type RunnerConfigTemplate = Omit<RunnerConfig, "sessionId">;
|
|
@@ -144,7 +147,7 @@ export function deriveRunnerConfig(
|
|
|
144
147
|
if (template === undefined) {
|
|
145
148
|
throw unknownProfileError(profileName, snapshot.profiles);
|
|
146
149
|
}
|
|
147
|
-
return runnerConfigFromTemplate(template, selection
|
|
150
|
+
return runnerConfigFromTemplate(template, selection);
|
|
148
151
|
}
|
|
149
152
|
|
|
150
153
|
if (selection.profileName !== undefined) {
|
|
@@ -152,7 +155,7 @@ export function deriveRunnerConfig(
|
|
|
152
155
|
`Cannot select model profile ${JSON.stringify(selection.profileName)} because TINKER_MODELS is not configured.`,
|
|
153
156
|
);
|
|
154
157
|
}
|
|
155
|
-
return runnerConfigFromTemplate(snapshot.template, selection
|
|
158
|
+
return runnerConfigFromTemplate(snapshot.template, selection);
|
|
156
159
|
}
|
|
157
160
|
|
|
158
161
|
function runnerConfigTemplateFromProfile(
|
|
@@ -172,6 +175,8 @@ function runnerConfigTemplateFromProfile(
|
|
|
172
175
|
contextBudget: deriveModelContextBudget(contextProfile),
|
|
173
176
|
profileName: profile.name,
|
|
174
177
|
inputModalities: profile.inputModalities,
|
|
178
|
+
bashGuardMode: environment.bashGuardMode,
|
|
179
|
+
bashGuardSource: environment.bashGuardSource,
|
|
175
180
|
...(profile.tokenEstimator === undefined
|
|
176
181
|
? {}
|
|
177
182
|
: { tokenEstimator: profile.tokenEstimator }),
|
|
@@ -196,14 +201,22 @@ function runnerConfigTemplateFromEnvironment(
|
|
|
196
201
|
contextProfile,
|
|
197
202
|
contextBudget: deriveModelContextBudget(contextProfile),
|
|
198
203
|
inputModalities: Object.freeze(["text"] as const),
|
|
204
|
+
bashGuardMode: environment.bashGuardMode,
|
|
205
|
+
bashGuardSource: environment.bashGuardSource,
|
|
199
206
|
});
|
|
200
207
|
}
|
|
201
208
|
|
|
202
209
|
function runnerConfigFromTemplate(
|
|
203
210
|
template: RunnerConfigTemplate,
|
|
204
|
-
|
|
211
|
+
selection: RunnerConfigSelection,
|
|
205
212
|
): RunnerConfig {
|
|
206
|
-
return Object.freeze({
|
|
213
|
+
return Object.freeze({
|
|
214
|
+
...template,
|
|
215
|
+
sessionId: selection.sessionId,
|
|
216
|
+
...(selection.yolo === true
|
|
217
|
+
? { bashGuardMode: "yolo" as const, bashGuardSource: "cli" as const }
|
|
218
|
+
: {}),
|
|
219
|
+
});
|
|
207
220
|
}
|
|
208
221
|
|
|
209
222
|
export function eventLogPath(workspaceRoot: string, sessionId: SessionId): string {
|
package/src/cli/main.ts
CHANGED
|
@@ -144,6 +144,7 @@ export async function main(
|
|
|
144
144
|
...(parsed.command.profileName === undefined
|
|
145
145
|
? {}
|
|
146
146
|
: { profileName: parsed.command.profileName }),
|
|
147
|
+
...(parsed.command.type === "run" && parsed.command.yolo ? { yolo: true } : {}),
|
|
147
148
|
});
|
|
148
149
|
} catch (error) {
|
|
149
150
|
await writeCliOutput(
|
|
@@ -60,6 +60,10 @@ export const PUBLIC_CLI_CONTRACT = Object.freeze({
|
|
|
60
60
|
description: "Read the prompt from a UTF-8 text file.",
|
|
61
61
|
valueName: "path",
|
|
62
62
|
} satisfies PublicCliOption),
|
|
63
|
+
yoloOption: Object.freeze({
|
|
64
|
+
flags: "--yolo",
|
|
65
|
+
description: "Skip destructive Bash command confirmation; use at your own risk.",
|
|
66
|
+
} satisfies PublicCliOption),
|
|
63
67
|
promptSources: RUN_PROMPT_SOURCES,
|
|
64
68
|
helpAfter:
|
|
65
69
|
"Use exactly one prompt source. For complex or sensitive prompts, prefer --stdin or --file.",
|
|
@@ -174,6 +174,17 @@ export const PUBLIC_CONFIG_FIELDS = Object.freeze([
|
|
|
174
174
|
section: "tooling",
|
|
175
175
|
description: "Maximum Bash foreground timeout in milliseconds.",
|
|
176
176
|
}),
|
|
177
|
+
publicField({
|
|
178
|
+
name: "TINKER_YOLO",
|
|
179
|
+
valueKind: "boolean",
|
|
180
|
+
requiredIn: "never",
|
|
181
|
+
appliesIn: "always",
|
|
182
|
+
defaultValue: false,
|
|
183
|
+
secret: false,
|
|
184
|
+
section: "tooling",
|
|
185
|
+
description:
|
|
186
|
+
"Allow high-confidence destructive Bash commands without confirmation.",
|
|
187
|
+
}),
|
|
177
188
|
publicField({
|
|
178
189
|
name: "TINKER_GREP_TIMEOUT_MS",
|
|
179
190
|
valueKind: "positive-integer",
|
|
@@ -479,6 +490,8 @@ type ParsedCommonEnvironment = {
|
|
|
479
490
|
readonly workspaceRoot: string;
|
|
480
491
|
readonly maxIterations: number;
|
|
481
492
|
readonly tooling: PublicToolingConfig;
|
|
493
|
+
readonly bashGuardMode: "guard" | "yolo";
|
|
494
|
+
readonly bashGuardSource: "default" | "environment";
|
|
482
495
|
};
|
|
483
496
|
|
|
484
497
|
export type ParsedPublicEnvironment =
|
|
@@ -562,7 +575,18 @@ export function parsePublicEnvironment(
|
|
|
562
575
|
);
|
|
563
576
|
}
|
|
564
577
|
|
|
565
|
-
const common = {
|
|
578
|
+
const common = {
|
|
579
|
+
workspaceRoot,
|
|
580
|
+
maxIterations,
|
|
581
|
+
tooling,
|
|
582
|
+
bashGuardMode: requiredBooleanValue(values, "TINKER_YOLO")
|
|
583
|
+
? ("yolo" as const)
|
|
584
|
+
: ("guard" as const),
|
|
585
|
+
bashGuardSource:
|
|
586
|
+
optionalRawString(env.TINKER_YOLO) === undefined
|
|
587
|
+
? ("default" as const)
|
|
588
|
+
: ("environment" as const),
|
|
589
|
+
};
|
|
566
590
|
if (mode === "profile") {
|
|
567
591
|
if (configuredModelsPath === undefined) {
|
|
568
592
|
throw new Error("Profile mode requires TINKER_MODELS.");
|
package/src/cli/run-runner.ts
CHANGED
|
@@ -74,6 +74,11 @@ export async function runOneShot(
|
|
|
74
74
|
options.eventLogPath === false ? false : { eventLogPath: options.eventLogPath },
|
|
75
75
|
webFetchRefiner: createWebFetchRefiner(config, options.env),
|
|
76
76
|
toolingConfig: options.tooling,
|
|
77
|
+
bashGuard: {
|
|
78
|
+
mode: config.bashGuardMode,
|
|
79
|
+
source: config.bashGuardSource,
|
|
80
|
+
surface: "one-shot",
|
|
81
|
+
},
|
|
77
82
|
});
|
|
78
83
|
|
|
79
84
|
const result = await session.executeTurn({
|
package/src/cli/tui-runner.tsx
CHANGED
|
@@ -7,6 +7,10 @@ import {
|
|
|
7
7
|
} from "../agent/runtime-session";
|
|
8
8
|
import type { EventSink } from "../events/event-sink";
|
|
9
9
|
import type { AgentEvent } from "../events/types";
|
|
10
|
+
import type {
|
|
11
|
+
AssistantTextDeltaSink,
|
|
12
|
+
AssistantTextDeltaUpdate,
|
|
13
|
+
} from "../agent/assistant-text-delta";
|
|
10
14
|
import { createUuidV7 } from "../ids/uuid-v7";
|
|
11
15
|
import type { SessionId } from "../ids/runtime-id";
|
|
12
16
|
import {
|
|
@@ -43,6 +47,7 @@ import { loadProjectSlashCommands } from "../tui/project-slash-commands";
|
|
|
43
47
|
import { createWorkspaceFileLister } from "../tui/workspace-file-search";
|
|
44
48
|
import { clipboardWriterForEnvironment } from "../tui/clipboard";
|
|
45
49
|
import { initializeTuiMemory } from "./tui-memory";
|
|
50
|
+
import { prepareShikiHighlighter } from "../tui/shiki-highlighter";
|
|
46
51
|
|
|
47
52
|
export type RunTuiOptions = {
|
|
48
53
|
readonly publicConfig: ResolvedPublicConfig;
|
|
@@ -51,6 +56,7 @@ export type RunTuiOptions = {
|
|
|
51
56
|
};
|
|
52
57
|
|
|
53
58
|
export async function runTui(options: RunTuiOptions): Promise<void> {
|
|
59
|
+
const shikiPreparation = prepareShikiHighlighter();
|
|
54
60
|
const profiles =
|
|
55
61
|
options.publicConfig.mode === "profile" ? options.publicConfig.profiles : undefined;
|
|
56
62
|
const config = options.initialRunnerConfig;
|
|
@@ -74,7 +80,7 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
|
|
|
74
80
|
sessionConfig: RunnerConfig,
|
|
75
81
|
mode: "new" | "resume",
|
|
76
82
|
sessionId: SessionId,
|
|
77
|
-
sink: EventSink,
|
|
83
|
+
sink: EventSink & AssistantTextDeltaSink,
|
|
78
84
|
): Promise<RuntimeSession> => {
|
|
79
85
|
const modelClient = createRunnerModelClient(
|
|
80
86
|
sessionConfig,
|
|
@@ -100,8 +106,15 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
|
|
|
100
106
|
projectInstruction: projectInstructionManifest(projectInstructions),
|
|
101
107
|
skillCatalog,
|
|
102
108
|
presentationSinks: [sink],
|
|
109
|
+
assistantTextDeltaSink: sink,
|
|
103
110
|
webFetchRefiner: createWebFetchRefiner(sessionConfig, options.env),
|
|
104
111
|
toolingConfig: options.publicConfig.tooling,
|
|
112
|
+
enableTurnUndo: true,
|
|
113
|
+
bashGuard: {
|
|
114
|
+
mode: sessionConfig.bashGuardMode,
|
|
115
|
+
source: sessionConfig.bashGuardSource,
|
|
116
|
+
surface: "tui" as const,
|
|
117
|
+
},
|
|
105
118
|
...(memoryCoordinator === undefined
|
|
106
119
|
? {}
|
|
107
120
|
: {
|
|
@@ -256,6 +269,7 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
|
|
|
256
269
|
createFreshSession,
|
|
257
270
|
);
|
|
258
271
|
|
|
272
|
+
await shikiPreparation;
|
|
259
273
|
instance = render(
|
|
260
274
|
<App
|
|
261
275
|
sessionController={controller}
|
|
@@ -285,6 +299,7 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
|
|
|
285
299
|
: () => memoryCoordinator.listStoredMemories()
|
|
286
300
|
}
|
|
287
301
|
/>,
|
|
302
|
+
{ incrementalRendering: true },
|
|
288
303
|
);
|
|
289
304
|
await instance.waitUntilExit();
|
|
290
305
|
} catch (error) {
|
|
@@ -317,7 +332,7 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
|
|
|
317
332
|
}
|
|
318
333
|
}
|
|
319
334
|
|
|
320
|
-
class DeferredProjectionSink implements EventSink {
|
|
335
|
+
class DeferredProjectionSink implements EventSink, AssistantTextDeltaSink {
|
|
321
336
|
readonly name = "deferred-tui-projection";
|
|
322
337
|
private target?: TuiProjectionStore;
|
|
323
338
|
private readonly buffered: AgentEvent[] = [];
|
|
@@ -330,6 +345,10 @@ class DeferredProjectionSink implements EventSink {
|
|
|
330
345
|
await this.target.append(event);
|
|
331
346
|
}
|
|
332
347
|
|
|
348
|
+
updateAssistantTextDelta(update: AssistantTextDeltaUpdate): void {
|
|
349
|
+
this.target?.updateAssistantTextDelta(update);
|
|
350
|
+
}
|
|
351
|
+
|
|
333
352
|
async attach(target: TuiProjectionStore): Promise<void> {
|
|
334
353
|
if (this.target !== undefined) {
|
|
335
354
|
throw new Error("Deferred projection sink is already attached.");
|
|
@@ -39,6 +39,27 @@ export function renderObservationLogEvent(event: AgentEvent): string | undefined
|
|
|
39
39
|
return renderAssistantProgress(event);
|
|
40
40
|
case "tool.observation":
|
|
41
41
|
return renderToolObservation(event);
|
|
42
|
+
case "tool.confirmation.requested":
|
|
43
|
+
return [
|
|
44
|
+
"## Bash confirmation requested",
|
|
45
|
+
"",
|
|
46
|
+
`Reason: ${event.data.reason}`,
|
|
47
|
+
"",
|
|
48
|
+
event.data.command,
|
|
49
|
+
"",
|
|
50
|
+
"---",
|
|
51
|
+
"",
|
|
52
|
+
].join("\n");
|
|
53
|
+
case "tool.confirmation.resolved":
|
|
54
|
+
return [
|
|
55
|
+
"## Bash confirmation resolved",
|
|
56
|
+
"",
|
|
57
|
+
`Decision: ${event.data.decision}`,
|
|
58
|
+
`Duration: ${event.data.durationMs} ms`,
|
|
59
|
+
"",
|
|
60
|
+
"---",
|
|
61
|
+
"",
|
|
62
|
+
].join("\n");
|
|
42
63
|
case "turn.finished":
|
|
43
64
|
return renderTurnFinished(event.data);
|
|
44
65
|
case "turn.cancelled":
|
|
@@ -117,6 +117,16 @@ export class StdoutEventPrinter implements EventSink {
|
|
|
117
117
|
`${formatToolLine("tool.finished", event.data.call).trimEnd()} ok=${event.data.ok}\n`,
|
|
118
118
|
);
|
|
119
119
|
break;
|
|
120
|
+
case "tool.confirmation.requested":
|
|
121
|
+
this.stdout.write(
|
|
122
|
+
`tool.confirmation.requested toolCallId=${event.toolCallId} reason=${JSON.stringify(event.data.reason)} command=${JSON.stringify(event.data.command)}\n`,
|
|
123
|
+
);
|
|
124
|
+
break;
|
|
125
|
+
case "tool.confirmation.resolved":
|
|
126
|
+
this.stdout.write(
|
|
127
|
+
`tool.confirmation.resolved toolCallId=${event.toolCallId} decision=${event.data.decision} durationMs=${event.data.durationMs}\n`,
|
|
128
|
+
);
|
|
129
|
+
break;
|
|
120
130
|
case "mcp.server.connected":
|
|
121
131
|
this.stdout.write(
|
|
122
132
|
`mcp.server.connected name=${event.data.serverName} tools=${event.data.toolCount}\n`,
|
|
@@ -199,6 +209,7 @@ function formatToolRawResult(call: ToolCall, raw: ToolRawResult): string[] {
|
|
|
199
209
|
case "skill":
|
|
200
210
|
return [formatSkillResult(raw)];
|
|
201
211
|
case "read":
|
|
212
|
+
case "delete":
|
|
202
213
|
case "glob":
|
|
203
214
|
case "grep":
|
|
204
215
|
case "web_search":
|