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
package/src/tools/types.ts
CHANGED
|
@@ -72,6 +72,13 @@ export type EditFileRawResult = {
|
|
|
72
72
|
error?: string;
|
|
73
73
|
};
|
|
74
74
|
|
|
75
|
+
export type DeleteFileRawResult = {
|
|
76
|
+
ok: boolean;
|
|
77
|
+
filePath: string;
|
|
78
|
+
absolutePath?: string;
|
|
79
|
+
error?: string;
|
|
80
|
+
};
|
|
81
|
+
|
|
75
82
|
export type GlobRawResult = {
|
|
76
83
|
ok: boolean;
|
|
77
84
|
pattern: string;
|
|
@@ -313,6 +320,7 @@ export type ToolRawResultByKind = {
|
|
|
313
320
|
read: ReadFileRawResult;
|
|
314
321
|
write: WriteFileRawResult;
|
|
315
322
|
edit: EditFileRawResult;
|
|
323
|
+
delete: DeleteFileRawResult;
|
|
316
324
|
glob: GlobRawResult;
|
|
317
325
|
grep: GrepRawResult;
|
|
318
326
|
bash: BashRawResult;
|
|
@@ -367,6 +375,11 @@ export function defineToolExecutor<TKind extends ToolRawResultKind>(
|
|
|
367
375
|
|
|
368
376
|
export type ToolExecutionContext = {
|
|
369
377
|
signal: AbortSignal;
|
|
378
|
+
confirmBashCommand?: (request: {
|
|
379
|
+
command: string;
|
|
380
|
+
reason: string;
|
|
381
|
+
}) => Promise<"allow" | "deny">;
|
|
382
|
+
bashGuardSurface?: "tui" | "one-shot";
|
|
370
383
|
};
|
|
371
384
|
|
|
372
385
|
export class ToolExecutionFatalError extends Error {
|
package/src/tools/write.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { computeFilePatch } from "./file-diff";
|
|
|
5
5
|
import { ensureParentDirectory } from "./ensure-parent-directory";
|
|
6
6
|
import { sha256Bytes, sha256Text } from "./hash";
|
|
7
7
|
import { resolveWorkspacePath } from "./path-safety";
|
|
8
|
+
import type { TurnUndoManager } from "./turn-undo-manager";
|
|
8
9
|
import { defineToolExecutor } from "./types";
|
|
9
10
|
import type {
|
|
10
11
|
FileSnapshotStore,
|
|
@@ -21,6 +22,7 @@ type WriteArgs = {
|
|
|
21
22
|
export type WriteToolOptions = {
|
|
22
23
|
workspaceRoot: string;
|
|
23
24
|
snapshots: FileSnapshotStore;
|
|
25
|
+
undoManager?: TurnUndoManager;
|
|
24
26
|
};
|
|
25
27
|
|
|
26
28
|
export function createWriteToolExecutor(options: WriteToolOptions): ToolExecutor {
|
|
@@ -47,7 +49,7 @@ export function createWriteToolExecutor(options: WriteToolOptions): ToolExecutor
|
|
|
47
49
|
},
|
|
48
50
|
async execute(
|
|
49
51
|
args,
|
|
50
|
-
|
|
52
|
+
call,
|
|
51
53
|
context: ToolExecutionContext,
|
|
52
54
|
): Promise<WriteFileRawResult> {
|
|
53
55
|
throwIfTurnCancelled(context.signal);
|
|
@@ -118,10 +120,24 @@ export function createWriteToolExecutor(options: WriteToolOptions): ToolExecutor
|
|
|
118
120
|
oldContent = target.content;
|
|
119
121
|
}
|
|
120
122
|
|
|
123
|
+
const undoCapture = await options.undoManager?.captureBeforeMutation({
|
|
124
|
+
turnId: call.turnId,
|
|
125
|
+
turnNumber: call.turnNumber,
|
|
126
|
+
absolutePath,
|
|
127
|
+
displayPath: input.file_path,
|
|
128
|
+
loadBefore: async () =>
|
|
129
|
+
target.exists
|
|
130
|
+
? { state: "present", bytes: target.bytes }
|
|
131
|
+
: { state: "absent" },
|
|
132
|
+
});
|
|
133
|
+
|
|
121
134
|
throwIfTurnCancelled(context.signal);
|
|
122
135
|
try {
|
|
123
136
|
await ensureParentDirectory(absolutePath);
|
|
124
137
|
} catch (error) {
|
|
138
|
+
if (undoCapture !== undefined) {
|
|
139
|
+
await options.undoManager?.recordMutationFailure(undoCapture);
|
|
140
|
+
}
|
|
125
141
|
return {
|
|
126
142
|
ok: false,
|
|
127
143
|
filePath: input.file_path,
|
|
@@ -129,15 +145,39 @@ export function createWriteToolExecutor(options: WriteToolOptions): ToolExecutor
|
|
|
129
145
|
error: `Failed to create parent directory: ${errorMessage(error)}`,
|
|
130
146
|
};
|
|
131
147
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
148
|
+
let newSha256: string;
|
|
149
|
+
try {
|
|
150
|
+
throwIfTurnCancelled(context.signal);
|
|
151
|
+
await writeFile(absolutePath, input.content, "utf8");
|
|
152
|
+
const expectedSha256 = sha256Text(input.content);
|
|
153
|
+
const written = await targetFileState(absolutePath);
|
|
154
|
+
if (!written.ok) {
|
|
155
|
+
throw new Error(`Failed to verify written file: ${written.error}`);
|
|
156
|
+
}
|
|
157
|
+
if (!written.exists) {
|
|
158
|
+
throw new Error("Failed to verify written file: File does not exist.");
|
|
159
|
+
}
|
|
160
|
+
if (written.sha256 !== expectedSha256) {
|
|
161
|
+
throw new Error("File changed while Write was being verified.");
|
|
162
|
+
}
|
|
163
|
+
newSha256 = written.sha256;
|
|
164
|
+
if (undoCapture !== undefined) {
|
|
165
|
+
options.undoManager?.recordMutationResult(undoCapture, {
|
|
166
|
+
state: "present",
|
|
167
|
+
sha256: newSha256,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
options.snapshots.set(absolutePath, {
|
|
171
|
+
sha256: newSha256,
|
|
172
|
+
mtimeMs: written.mtimeMs,
|
|
173
|
+
source: "write",
|
|
174
|
+
});
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (undoCapture !== undefined) {
|
|
177
|
+
await options.undoManager?.recordMutationFailure(undoCapture);
|
|
178
|
+
}
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
141
181
|
|
|
142
182
|
const patch = computeFilePatch({
|
|
143
183
|
filePath: input.file_path,
|
|
@@ -184,11 +224,16 @@ function parseWriteArgs(
|
|
|
184
224
|
};
|
|
185
225
|
}
|
|
186
226
|
|
|
187
|
-
async function targetFileState(
|
|
188
|
-
absolutePath: string,
|
|
189
|
-
): Promise<
|
|
227
|
+
async function targetFileState(absolutePath: string): Promise<
|
|
190
228
|
| { ok: true; exists: false }
|
|
191
|
-
| {
|
|
229
|
+
| {
|
|
230
|
+
ok: true;
|
|
231
|
+
exists: true;
|
|
232
|
+
sha256: string;
|
|
233
|
+
content: string;
|
|
234
|
+
bytes: Buffer;
|
|
235
|
+
mtimeMs: number;
|
|
236
|
+
}
|
|
192
237
|
| { ok: false; error: string }
|
|
193
238
|
> {
|
|
194
239
|
try {
|
|
@@ -198,11 +243,17 @@ async function targetFileState(
|
|
|
198
243
|
}
|
|
199
244
|
|
|
200
245
|
const bytes = await readFile(absolutePath);
|
|
246
|
+
const currentInfo = await stat(absolutePath);
|
|
247
|
+
if (currentInfo.mtimeMs > info.mtimeMs) {
|
|
248
|
+
return { ok: false, error: "File changed while it was being read." };
|
|
249
|
+
}
|
|
201
250
|
return {
|
|
202
251
|
ok: true,
|
|
203
252
|
exists: true,
|
|
204
253
|
sha256: sha256Bytes(bytes),
|
|
205
254
|
content: bytes.toString("utf8"),
|
|
255
|
+
bytes,
|
|
256
|
+
mtimeMs: currentInfo.mtimeMs,
|
|
206
257
|
};
|
|
207
258
|
} catch (error) {
|
|
208
259
|
if (isNotFound(error)) {
|