tinker-agent 1.0.65

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 (110) hide show
  1. package/README.md +173 -0
  2. package/package.json +78 -0
  3. package/patches/markdansi@0.3.2.patch +37 -0
  4. package/src/agent/context-builder.ts +43 -0
  5. package/src/agent/context-meter.ts +310 -0
  6. package/src/agent/loop.ts +525 -0
  7. package/src/agent/runtime-session.ts +1212 -0
  8. package/src/agent/session-ledger.ts +828 -0
  9. package/src/agent/turn-cancellation.ts +44 -0
  10. package/src/agent/types.ts +77 -0
  11. package/src/cli/config.ts +283 -0
  12. package/src/cli/index.ts +29 -0
  13. package/src/cli/model-profiles.ts +289 -0
  14. package/src/cli/run-runner.ts +107 -0
  15. package/src/cli/tui-runner.tsx +290 -0
  16. package/src/context/compiled-context-hash.ts +138 -0
  17. package/src/context/compiled-context-validator.ts +209 -0
  18. package/src/context/context-manager.ts +362 -0
  19. package/src/context/context-policy.ts +8 -0
  20. package/src/context/context-protocol-validator.ts +463 -0
  21. package/src/context/context-revision-compiler.ts +281 -0
  22. package/src/context/context-revision.ts +111 -0
  23. package/src/context/context-source.ts +30 -0
  24. package/src/context/context-swap-renderer.ts +272 -0
  25. package/src/context/protocol-frame.ts +240 -0
  26. package/src/context/swap-planner.ts +725 -0
  27. package/src/events/append-private-file.ts +16 -0
  28. package/src/events/bash-result-detail.ts +70 -0
  29. package/src/events/composite-event-sink.ts +82 -0
  30. package/src/events/event-sink.ts +16 -0
  31. package/src/events/jsonl-event-log.ts +13 -0
  32. package/src/events/observation-text-log.ts +195 -0
  33. package/src/events/stdout-event-printer.ts +396 -0
  34. package/src/events/types.ts +263 -0
  35. package/src/ids/runtime-id.ts +68 -0
  36. package/src/ids/uuid-v7.ts +5 -0
  37. package/src/instructions/project-instructions.ts +242 -0
  38. package/src/mcp/mcp-config.ts +144 -0
  39. package/src/mcp/mcp-manager.ts +216 -0
  40. package/src/mcp/mcp-tool-executor.ts +178 -0
  41. package/src/model/committed-prefix-auditor.ts +68 -0
  42. package/src/model/fake-model-client.ts +280 -0
  43. package/src/model/model-client.ts +64 -0
  44. package/src/model/model-context-profile.ts +134 -0
  45. package/src/model/model-request-preflight.ts +120 -0
  46. package/src/model/openai-chat-mapping.ts +444 -0
  47. package/src/model/openai-chat-model-client.ts +190 -0
  48. package/src/model/prompt-prefix-hash.ts +47 -0
  49. package/src/model/token-estimator.ts +148 -0
  50. package/src/observation/observation-builder.ts +481 -0
  51. package/src/session/resume-projection.ts +616 -0
  52. package/src/session/session-catalog.ts +270 -0
  53. package/src/session/session-errors.ts +121 -0
  54. package/src/session/session-history-reader.ts +535 -0
  55. package/src/session/session-lock.ts +291 -0
  56. package/src/session/session-schema.ts +741 -0
  57. package/src/session/session-store.ts +3067 -0
  58. package/src/session/sqlite-session-ledger.ts +153 -0
  59. package/src/tools/bash-task.ts +617 -0
  60. package/src/tools/bash.ts +450 -0
  61. package/src/tools/cwd-state.ts +22 -0
  62. package/src/tools/edit.ts +428 -0
  63. package/src/tools/file-diff.ts +116 -0
  64. package/src/tools/glob.ts +202 -0
  65. package/src/tools/grep.ts +550 -0
  66. package/src/tools/hash.ts +9 -0
  67. package/src/tools/path-safety.ts +33 -0
  68. package/src/tools/read.ts +319 -0
  69. package/src/tools/recall.ts +400 -0
  70. package/src/tools/registry.ts +213 -0
  71. package/src/tools/ripgrep.ts +220 -0
  72. package/src/tools/task-list.ts +59 -0
  73. package/src/tools/task-output-snapshot.ts +47 -0
  74. package/src/tools/task-output-tool.ts +62 -0
  75. package/src/tools/task-output.ts +159 -0
  76. package/src/tools/task-stop.ts +59 -0
  77. package/src/tools/task-tool-args.ts +29 -0
  78. package/src/tools/types.ts +330 -0
  79. package/src/tools/web-fetch/backend.ts +27 -0
  80. package/src/tools/web-fetch/browser-backend.ts +126 -0
  81. package/src/tools/web-fetch/exa-backend.ts +172 -0
  82. package/src/tools/web-fetch/index.ts +298 -0
  83. package/src/tools/web-fetch/local-backend.ts +267 -0
  84. package/src/tools/web-fetch/refiner.ts +78 -0
  85. package/src/tools/web-fetch/route.ts +95 -0
  86. package/src/tools/web-search.ts +300 -0
  87. package/src/tools/write.ts +244 -0
  88. package/src/tui/app.tsx +497 -0
  89. package/src/tui/components/assistant-markdown.tsx +47 -0
  90. package/src/tui/components/background-tasks.tsx +92 -0
  91. package/src/tui/components/bash-result-view.tsx +47 -0
  92. package/src/tui/components/context-status.tsx +127 -0
  93. package/src/tui/components/diff-view.tsx +151 -0
  94. package/src/tui/components/file-viewer.tsx +212 -0
  95. package/src/tui/components/footer.tsx +60 -0
  96. package/src/tui/components/header.tsx +21 -0
  97. package/src/tui/components/model-picker.tsx +142 -0
  98. package/src/tui/components/prompt-input.tsx +432 -0
  99. package/src/tui/components/resume-session-picker.tsx +273 -0
  100. package/src/tui/components/timeline.tsx +121 -0
  101. package/src/tui/context-format.ts +24 -0
  102. package/src/tui/event-store.ts +865 -0
  103. package/src/tui/git-branch.ts +23 -0
  104. package/src/tui/line-editor.ts +157 -0
  105. package/src/tui/prompt-history.ts +94 -0
  106. package/src/tui/slash-commands.ts +126 -0
  107. package/src/tui/tui-projection-policy.ts +35 -0
  108. package/src/tui/tui-projection-store.ts +123 -0
  109. package/src/tui/tui-session-controller.ts +170 -0
  110. package/src/tui/view-file.ts +122 -0
@@ -0,0 +1,291 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import { lstat, open, readFile, unlink } from "node:fs/promises";
4
+ import { randomUUID } from "node:crypto";
5
+ import type { SessionId } from "../ids/runtime-id";
6
+ import { SessionError } from "./session-errors";
7
+
8
+ export type SessionLockRecordV1 = {
9
+ version: 1;
10
+ lockId: string;
11
+ sessionId: SessionId;
12
+ pid: number;
13
+ hostname: string;
14
+ processStartedAt: string;
15
+ acquiredAt: string;
16
+ };
17
+
18
+ export type SessionLeaseDependencies = {
19
+ hostname(): string;
20
+ pid: number;
21
+ processStartedAt: string;
22
+ now(): string;
23
+ createLockId(): string;
24
+ isProcessAlive(pid: number): "alive" | "dead";
25
+ };
26
+
27
+ const defaultDependencies: SessionLeaseDependencies = {
28
+ hostname: os.hostname,
29
+ pid: process.pid,
30
+ processStartedAt: new Date(Date.now() - process.uptime() * 1_000).toISOString(),
31
+ now: () => new Date().toISOString(),
32
+ createLockId: randomUUID,
33
+ isProcessAlive(pid) {
34
+ try {
35
+ process.kill(pid, 0);
36
+ return "alive";
37
+ } catch (error) {
38
+ const code = (error as NodeJS.ErrnoException).code;
39
+ if (code === "ESRCH") {
40
+ return "dead";
41
+ }
42
+ return "alive";
43
+ }
44
+ },
45
+ };
46
+
47
+ export class SessionLease {
48
+ private released = false;
49
+ private lockPath: string;
50
+
51
+ private constructor(
52
+ readonly record: SessionLockRecordV1,
53
+ lockPath: string,
54
+ ) {
55
+ this.lockPath = lockPath;
56
+ }
57
+
58
+ static async acquire(input: {
59
+ sessionDirectory: string;
60
+ sessionId: SessionId;
61
+ dependencies?: Partial<SessionLeaseDependencies>;
62
+ }): Promise<SessionLease> {
63
+ const dependencies = { ...defaultDependencies, ...input.dependencies };
64
+ const lockPath = path.join(input.sessionDirectory, "active.lock");
65
+ const reclaimPath = path.join(input.sessionDirectory, "active.lock.reclaim");
66
+ const record: SessionLockRecordV1 = {
67
+ version: 1,
68
+ lockId: dependencies.createLockId(),
69
+ sessionId: input.sessionId,
70
+ pid: dependencies.pid,
71
+ hostname: dependencies.hostname(),
72
+ processStartedAt: dependencies.processStartedAt,
73
+ acquiredAt: dependencies.now(),
74
+ };
75
+
76
+ for (let attempt = 0; attempt < 3; attempt += 1) {
77
+ try {
78
+ const handle = await open(lockPath, "wx", 0o600);
79
+ try {
80
+ await handle.writeFile(`${JSON.stringify(record)}\n`, "utf8");
81
+ await handle.sync();
82
+ } finally {
83
+ await handle.close();
84
+ }
85
+ return new SessionLease(record, lockPath);
86
+ } catch (error) {
87
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") {
88
+ throw new SessionError(
89
+ "SESSION_LOCK_CORRUPT",
90
+ "acquire_lease",
91
+ `Cannot create session lock ${lockPath}.`,
92
+ { sessionId: input.sessionId, cause: error },
93
+ );
94
+ }
95
+ }
96
+
97
+ let existing: SessionLockRecordV1;
98
+ try {
99
+ existing = await readLockRecord(lockPath, input.sessionId);
100
+ } catch (error) {
101
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
102
+ continue;
103
+ }
104
+ throw error;
105
+ }
106
+ if (
107
+ existing.hostname !== dependencies.hostname() ||
108
+ dependencies.isProcessAlive(existing.pid) === "alive"
109
+ ) {
110
+ throw new SessionError(
111
+ "SESSION_LOCKED",
112
+ "acquire_lease",
113
+ `Session ${input.sessionId} is active in pid ${existing.pid} since ${existing.acquiredAt}.`,
114
+ { sessionId: input.sessionId },
115
+ );
116
+ }
117
+
118
+ let reclaimHandle;
119
+ try {
120
+ reclaimHandle = await open(reclaimPath, "wx", 0o600);
121
+ } catch (error) {
122
+ if ((error as NodeJS.ErrnoException).code === "EEXIST") {
123
+ continue;
124
+ }
125
+ throw new SessionError(
126
+ "SESSION_LOCK_CORRUPT",
127
+ "reclaim_lease",
128
+ `Cannot create reclaim marker for session ${input.sessionId}.`,
129
+ { sessionId: input.sessionId, cause: error },
130
+ );
131
+ }
132
+
133
+ try {
134
+ let confirmed: SessionLockRecordV1;
135
+ try {
136
+ confirmed = await readLockRecord(lockPath, input.sessionId);
137
+ } catch (error) {
138
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
139
+ continue;
140
+ }
141
+ throw error;
142
+ }
143
+ if (
144
+ confirmed.lockId !== existing.lockId ||
145
+ confirmed.hostname !== dependencies.hostname() ||
146
+ dependencies.isProcessAlive(confirmed.pid) === "alive"
147
+ ) {
148
+ continue;
149
+ }
150
+ await unlink(lockPath);
151
+ } finally {
152
+ await reclaimHandle.close();
153
+ await unlinkIfExists(reclaimPath);
154
+ }
155
+ }
156
+
157
+ throw new SessionError(
158
+ "SESSION_LOCKED",
159
+ "acquire_lease",
160
+ `Session ${input.sessionId} lock changed while it was being acquired.`,
161
+ { sessionId: input.sessionId },
162
+ );
163
+ }
164
+
165
+ relocate(sessionDirectory: string): void {
166
+ this.requireActive();
167
+ this.lockPath = path.join(sessionDirectory, "active.lock");
168
+ }
169
+
170
+ async release(): Promise<void> {
171
+ if (this.released) {
172
+ return;
173
+ }
174
+ const current = await readLockRecord(this.lockPath, this.record.sessionId);
175
+ if (current.lockId !== this.record.lockId) {
176
+ throw new SessionError(
177
+ "SESSION_LOCK_CORRUPT",
178
+ "release_lease",
179
+ `Session lock token changed before release for ${this.record.sessionId}.`,
180
+ { sessionId: this.record.sessionId },
181
+ );
182
+ }
183
+ await unlink(this.lockPath);
184
+ this.released = true;
185
+ }
186
+
187
+ private requireActive(): void {
188
+ if (this.released) {
189
+ throw new Error("Cannot use a released SessionLease.");
190
+ }
191
+ }
192
+ }
193
+
194
+ export async function inspectSessionLock(input: {
195
+ sessionDirectory: string;
196
+ sessionId: SessionId;
197
+ dependencies?: Partial<SessionLeaseDependencies>;
198
+ }): Promise<"none" | "active" | "stale" | "corrupt"> {
199
+ const dependencies = { ...defaultDependencies, ...input.dependencies };
200
+ const lockPath = path.join(input.sessionDirectory, "active.lock");
201
+ try {
202
+ const record = await readLockRecord(lockPath, input.sessionId);
203
+ if (record.hostname !== dependencies.hostname()) {
204
+ return "active";
205
+ }
206
+ return dependencies.isProcessAlive(record.pid) === "alive" ? "active" : "stale";
207
+ } catch (error) {
208
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
209
+ return "none";
210
+ }
211
+ return error instanceof SessionError ? "corrupt" : "corrupt";
212
+ }
213
+ }
214
+
215
+ async function readLockRecord(
216
+ lockPath: string,
217
+ sessionId: SessionId,
218
+ ): Promise<SessionLockRecordV1> {
219
+ const stats = await lstat(lockPath);
220
+ if (!stats.isFile() || stats.isSymbolicLink() || (stats.mode & 0o077) !== 0) {
221
+ throw new SessionError(
222
+ "SESSION_LOCK_CORRUPT",
223
+ "read_lease",
224
+ `Session lock has unsafe type or permissions: ${lockPath}.`,
225
+ { sessionId },
226
+ );
227
+ }
228
+
229
+ let parsed: unknown;
230
+ try {
231
+ parsed = JSON.parse(await readFile(lockPath, "utf8"));
232
+ } catch (error) {
233
+ throw new SessionError(
234
+ "SESSION_LOCK_CORRUPT",
235
+ "read_lease",
236
+ `Session lock is not valid JSON: ${lockPath}.`,
237
+ { sessionId, cause: error },
238
+ );
239
+ }
240
+ if (!isLockRecord(parsed) || parsed.sessionId !== sessionId) {
241
+ throw new SessionError(
242
+ "SESSION_LOCK_CORRUPT",
243
+ "read_lease",
244
+ `Session lock does not match session ${sessionId}.`,
245
+ { sessionId },
246
+ );
247
+ }
248
+ return parsed;
249
+ }
250
+
251
+ function isLockRecord(value: unknown): value is SessionLockRecordV1 {
252
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
253
+ return false;
254
+ }
255
+ const record = value as Record<string, unknown>;
256
+ const expectedKeys = [
257
+ "acquiredAt",
258
+ "hostname",
259
+ "lockId",
260
+ "pid",
261
+ "processStartedAt",
262
+ "sessionId",
263
+ "version",
264
+ ];
265
+ return (
266
+ Object.keys(record).sort().join("\n") === expectedKeys.join("\n") &&
267
+ record.version === 1 &&
268
+ typeof record.lockId === "string" &&
269
+ record.lockId !== "" &&
270
+ typeof record.sessionId === "string" &&
271
+ typeof record.pid === "number" &&
272
+ Number.isInteger(record.pid) &&
273
+ record.pid > 0 &&
274
+ typeof record.hostname === "string" &&
275
+ record.hostname !== "" &&
276
+ typeof record.processStartedAt === "string" &&
277
+ !Number.isNaN(Date.parse(record.processStartedAt)) &&
278
+ typeof record.acquiredAt === "string" &&
279
+ !Number.isNaN(Date.parse(record.acquiredAt))
280
+ );
281
+ }
282
+
283
+ async function unlinkIfExists(filePath: string): Promise<void> {
284
+ try {
285
+ await unlink(filePath);
286
+ } catch (error) {
287
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
288
+ throw error;
289
+ }
290
+ }
291
+ }