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.
Files changed (48) hide show
  1. package/CHANGELOG.md +48 -1
  2. package/README.md +13 -5
  3. package/package.json +7 -5
  4. package/src/agent/assistant-text-delta.ts +10 -0
  5. package/src/agent/loop.ts +116 -22
  6. package/src/agent/runtime-session.ts +248 -1
  7. package/src/cli/command-line.ts +9 -1
  8. package/src/cli/config.ts +17 -4
  9. package/src/cli/main.ts +1 -0
  10. package/src/cli/public-cli-contract.ts +4 -0
  11. package/src/cli/public-config-contract.ts +25 -1
  12. package/src/cli/run-runner.ts +5 -0
  13. package/src/cli/tui-runner.tsx +21 -2
  14. package/src/events/observation-text-log.ts +21 -0
  15. package/src/events/stdout-event-printer.ts +11 -0
  16. package/src/events/types.ts +14 -2
  17. package/src/model/fake-model-client.ts +190 -0
  18. package/src/model/model-client.ts +3 -0
  19. package/src/model/openai-chat-model-client.ts +54 -15
  20. package/src/model/openai-chat-stream.ts +95 -72
  21. package/src/observation/observation-builder.ts +11 -0
  22. package/src/session/session-store.ts +1 -0
  23. package/src/tools/bash-guard.ts +131 -0
  24. package/src/tools/bash.ts +31 -0
  25. package/src/tools/delete.ts +182 -0
  26. package/src/tools/edit.ts +68 -9
  27. package/src/tools/registry.ts +47 -3
  28. package/src/tools/turn-undo-manager.ts +794 -0
  29. package/src/tools/types.ts +13 -0
  30. package/src/tools/write.ts +65 -14
  31. package/src/tui/app.tsx +301 -134
  32. package/src/tui/assistant-markdown-section-framer.ts +135 -0
  33. package/src/tui/components/assistant-markdown.tsx +27 -26
  34. package/src/tui/components/background-tasks.tsx +7 -2
  35. package/src/tui/components/bash-confirmation.tsx +27 -0
  36. package/src/tui/components/context-status.tsx +11 -1
  37. package/src/tui/components/file-viewer.tsx +2 -2
  38. package/src/tui/components/footer.tsx +9 -12
  39. package/src/tui/components/memory-browser.tsx +1 -1
  40. package/src/tui/components/prompt-input.tsx +13 -1
  41. package/src/tui/components/resume-session-picker.tsx +3 -1
  42. package/src/tui/components/timeline.tsx +19 -11
  43. package/src/tui/context-format.ts +17 -0
  44. package/src/tui/event-store.ts +75 -3
  45. package/src/tui/shiki-highlighter.ts +104 -0
  46. package/src/tui/slash-commands.ts +28 -0
  47. package/src/tui/tui-projection-store.ts +277 -5
  48. package/src/tui/tui-session-controller.ts +32 -8
@@ -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 {
@@ -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
- _call,
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
- throwIfTurnCancelled(context.signal);
133
- await writeFile(absolutePath, input.content, "utf8");
134
- const newSha256 = sha256Text(input.content);
135
- const writtenInfo = await stat(absolutePath);
136
- options.snapshots.set(absolutePath, {
137
- sha256: newSha256,
138
- mtimeMs: writtenInfo.mtimeMs,
139
- source: "write",
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
- | { ok: true; exists: true; sha256: string; content: string }
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)) {