tinker-agent 2.8.0 → 2.10.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 (66) hide show
  1. package/CHANGELOG.md +79 -1
  2. package/README.md +81 -11
  3. package/package.json +5 -3
  4. package/src/agent/runtime-context-capabilities.ts +19 -0
  5. package/src/agent/runtime-context-events.ts +127 -0
  6. package/src/agent/runtime-context-maintenance.ts +780 -0
  7. package/src/agent/runtime-hosted-session.ts +443 -0
  8. package/src/agent/runtime-interactions.ts +291 -0
  9. package/src/agent/runtime-prompt-scheduler.ts +182 -0
  10. package/src/agent/runtime-session-contracts.ts +317 -0
  11. package/src/agent/runtime-session.ts +250 -2130
  12. package/src/agent/runtime-skills.ts +544 -0
  13. package/src/cli/command-line.ts +26 -2
  14. package/src/cli/connect-runner.tsx +26 -0
  15. package/src/cli/main.ts +26 -0
  16. package/src/cli/output.ts +1 -1
  17. package/src/cli/public-cli-contract.ts +18 -0
  18. package/src/cli/public-config-contract.ts +1 -1
  19. package/src/cli/runner-dependencies.ts +6 -5
  20. package/src/cli/serve-runner.ts +45 -0
  21. package/src/cli/serve-runtime.ts +100 -0
  22. package/src/context/context-automation-policy.ts +12 -118
  23. package/src/context/context-swap-renderer.ts +14 -0
  24. package/src/events/types.ts +12 -0
  25. package/src/memory/memory-get-tool.ts +1 -1
  26. package/src/observation/observation-builder.ts +128 -48
  27. package/src/remote/client.ts +350 -0
  28. package/src/remote/config.ts +95 -0
  29. package/src/remote/http-server.ts +240 -0
  30. package/src/remote/protocol.ts +228 -0
  31. package/src/remote/service-store.ts +175 -0
  32. package/src/remote/service.ts +219 -0
  33. package/src/remote/sync-hub.ts +95 -0
  34. package/src/session/remote-history-reader.ts +143 -0
  35. package/src/session/resume-projection.ts +47 -21
  36. package/src/session/session-history-access.ts +238 -0
  37. package/src/session/session-store-context-readers.ts +183 -0
  38. package/src/session/session-store-ledger-writer.ts +315 -0
  39. package/src/session/session-store-record-writer.ts +318 -0
  40. package/src/session/session-store-recovery.ts +225 -0
  41. package/src/session/session-store-revisions.ts +1004 -0
  42. package/src/session/session-store-sql.ts +40 -0
  43. package/src/session/session-store-validation.ts +657 -0
  44. package/src/session/session-store.ts +756 -3186
  45. package/src/tools/bash-task.ts +46 -18
  46. package/src/tools/bash.ts +44 -2
  47. package/src/tools/glob.ts +107 -19
  48. package/src/tools/grep-output.ts +130 -0
  49. package/src/tools/grep-pagination.ts +73 -0
  50. package/src/tools/grep-path.ts +11 -0
  51. package/src/tools/grep-snippets.ts +111 -0
  52. package/src/tools/grep.ts +139 -154
  53. package/src/tools/read.ts +0 -9
  54. package/src/tools/recall.ts +106 -50
  55. package/src/tools/registry.ts +4 -6
  56. package/src/tools/ripgrep.ts +19 -26
  57. package/src/tools/shell-process.ts +30 -4
  58. package/src/tools/task-output-range.ts +146 -0
  59. package/src/tools/task-output-tool.ts +35 -5
  60. package/src/tools/task-output.ts +35 -0
  61. package/src/tools/task-stop.ts +2 -1
  62. package/src/tools/task-tool-args.ts +34 -0
  63. package/src/tools/terminal-screen.ts +11 -2
  64. package/src/tools/types.ts +39 -2
  65. package/src/tui/event-store.ts +23 -5
  66. package/src/tui/remote-app.tsx +210 -0
@@ -0,0 +1,443 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type { RuntimeSession } from "./runtime-session";
3
+ import type {
4
+ AssistantTextDeltaSink,
5
+ AssistantTextDeltaUpdate,
6
+ } from "./assistant-text-delta";
7
+ import type { AgentEvent } from "../events/types";
8
+ import type { EventSink } from "../events/event-sink";
9
+ import {
10
+ RemoteHistoryReader,
11
+ type RemoteHistoryPage,
12
+ } from "../session/remote-history-reader";
13
+ import { parseSessionId } from "../ids/runtime-id";
14
+ import {
15
+ RemoteError,
16
+ type RemoteActivity,
17
+ type RemoteOperationInput,
18
+ type OperationReceipt,
19
+ type RemoteView,
20
+ } from "../remote/protocol";
21
+ import { type ManagedSessionRecord, RemoteServiceStore } from "../remote/service-store";
22
+ import { RemoteSyncHub } from "../remote/sync-hub";
23
+
24
+ export type HostedRuntimeFactory = (input: {
25
+ record: ManagedSessionRecord;
26
+ sink: EventSink & AssistantTextDeltaSink;
27
+ }) => Promise<{
28
+ runtime: RuntimeSession;
29
+ databasePath: string;
30
+ modelName: string;
31
+ }>;
32
+
33
+ export class HostedSession implements EventSink, AssistantTextDeltaSink {
34
+ readonly name = "remote-view";
35
+ readonly hub: RemoteSyncHub;
36
+ private runtime?: RuntimeSession;
37
+ private reader?: RemoteHistoryReader;
38
+ private opening?: Promise<void>;
39
+ private readonly queue: OperationReceipt[] = [];
40
+ private active?: {
41
+ receipt: OperationReceipt;
42
+ controller: AbortController;
43
+ completion?: Promise<void>;
44
+ };
45
+ private activity: Omit<RemoteActivity, "session" | "operations"> = {
46
+ status: "idle",
47
+ tools: [],
48
+ };
49
+ private lastOrdinal = 0;
50
+ private streamTimer?: ReturnType<typeof setTimeout>;
51
+ private stopping = false;
52
+ private unsubscribers: (() => void)[] = [];
53
+ private controlTail: Promise<void> = Promise.resolve();
54
+
55
+ constructor(
56
+ private record: ManagedSessionRecord,
57
+ private readonly store: RemoteServiceStore,
58
+ epoch: string,
59
+ private readonly factory: HostedRuntimeFactory,
60
+ ) {
61
+ this.hub = new RemoteSyncHub(epoch, () => this.view());
62
+ }
63
+
64
+ open(): Promise<void> {
65
+ return (this.opening ??= this.initialize());
66
+ }
67
+ private async initialize(): Promise<void> {
68
+ try {
69
+ const opened = await this.factory({ record: this.record, sink: this });
70
+ this.runtime = opened.runtime;
71
+ this.reader = new RemoteHistoryReader(
72
+ opened.databasePath,
73
+ parseSessionId(this.record.id),
74
+ this.record.workspacePath,
75
+ );
76
+ this.record = {
77
+ ...this.record,
78
+ initialized: true,
79
+ modelName: opened.modelName,
80
+ status: "idle",
81
+ };
82
+ this.store.saveSession(this.record);
83
+ const latest = this.reader.latestTurn();
84
+ if (latest && latest.status !== "open") {
85
+ this.activity.status = latest.status as RemoteActivity["status"];
86
+ this.activity.error = latest.error;
87
+ }
88
+ // Canonical terminal status wins over a crash between turn commit and receipt update.
89
+ for (const receipt of this.store.operations(this.record.id)) {
90
+ const status = receipt.turnId
91
+ ? this.reader.turnStatus(receipt.turnId)
92
+ : undefined;
93
+ if (receipt.kind === "prompt" && status && status !== "open") {
94
+ this.store.update({
95
+ ...receipt,
96
+ status: status as OperationReceipt["status"],
97
+ error: status === "interrupted" ? receipt.error : undefined,
98
+ });
99
+ }
100
+ }
101
+ const last = this.store
102
+ .operations(this.record.id)
103
+ .filter((op) => op.kind === "prompt")
104
+ .at(-1);
105
+ if (last?.status === "interrupted") {
106
+ this.activity.status = "interrupted";
107
+ this.activity.error = last.error;
108
+ }
109
+ this.unsubscribers = [
110
+ this.runtime.subscribeAskUser(() => this.updateInteraction()),
111
+ this.runtime.subscribeBashGuard(() => this.updateInteraction()),
112
+ ];
113
+ this.publish();
114
+ } catch (error) {
115
+ this.activity.status = "failed";
116
+ this.activity.error = errorMessage(error);
117
+ if (this.runtime)
118
+ await this.runtime
119
+ .dispose({ type: "runner_failed", error: errorMessage(error) })
120
+ .catch(() => undefined);
121
+ this.publish();
122
+ throw error;
123
+ }
124
+ }
125
+
126
+ view(): RemoteView {
127
+ return { ...this.readActivity(), history: this.history() };
128
+ }
129
+ history(before?: number, limit?: number): RemoteHistoryPage {
130
+ return this.reader?.page(before, limit) ?? { messages: [], hasMore: false };
131
+ }
132
+ private readActivity(): RemoteActivity {
133
+ const { id, workspaceId, title, modelName, owner, updatedAt } = this.record;
134
+ const session = { id, workspaceId, title, modelName, owner, updatedAt };
135
+ return structuredClone({
136
+ ...this.activity,
137
+ session: { ...session, status: this.activity.status },
138
+ operations: this.store.operations(this.record.id),
139
+ });
140
+ }
141
+
142
+ validate(input: RemoteOperationInput): void {
143
+ if (this.stopping)
144
+ throw new RemoteError(503, "SERVICE_STOPPING", "The local service is stopping.");
145
+ if (input.kind === "prompt" && this.queue.length >= 8)
146
+ throw new RemoteError(
147
+ 409,
148
+ "QUEUE_FULL",
149
+ "This session already has eight queued requests.",
150
+ );
151
+ if (input.kind === "answer" || input.kind === "confirm") {
152
+ const pending = this.activity.interaction;
153
+ if (
154
+ !pending ||
155
+ pending.id !== input.interactionId ||
156
+ pending.kind !== (input.kind === "answer" ? "question" : "confirmation")
157
+ )
158
+ throw new RemoteError(
159
+ 409,
160
+ "STALE_INTERACTION",
161
+ "This question or confirmation is no longer pending.",
162
+ );
163
+ if (
164
+ input.kind === "answer" &&
165
+ input.selectedIndex !== null &&
166
+ pending.kind === "question" &&
167
+ !pending.options[input.selectedIndex]
168
+ )
169
+ throw new RemoteError(400, "INVALID_ANSWER", "Answer index is out of range.");
170
+ }
171
+ if (input.kind === "stop") {
172
+ const target = this.store.get(input.targetRequestId);
173
+ if (target.sessionId !== this.record.id || target.kind !== "prompt")
174
+ throw new RemoteError(
175
+ 409,
176
+ "INVALID_STOP_TARGET",
177
+ "Stop must target a prompt in this session.",
178
+ );
179
+ }
180
+ }
181
+
182
+ enqueue(receipt: OperationReceipt): void {
183
+ this.queue.push(receipt);
184
+ if (!this.active) this.activity.status = "accepted";
185
+ if (this.record.title === "New session" && receipt.prompt) {
186
+ this.record = {
187
+ ...this.record,
188
+ title: receipt.prompt.replace(/\s+/g, " ").slice(0, 100),
189
+ };
190
+ this.store.saveSession(this.record);
191
+ }
192
+ this.publish();
193
+ this.pump();
194
+ }
195
+ private pump(): void {
196
+ if (this.active || this.stopping) return;
197
+ const receipt = this.queue.shift();
198
+ if (!receipt) return;
199
+ const active = {
200
+ receipt,
201
+ controller: new AbortController(),
202
+ completion: undefined as Promise<void> | undefined,
203
+ };
204
+ this.active = active;
205
+ active.completion = this.execute(active);
206
+ }
207
+ private async execute(active: NonNullable<HostedSession["active"]>): Promise<void> {
208
+ try {
209
+ await this.open();
210
+ if (active.controller.signal.aborted) {
211
+ this.updateReceipt(active.receipt, { status: "cancelled" });
212
+ return;
213
+ }
214
+ this.activity = {
215
+ status: "running",
216
+ activeRequestId: active.receipt.requestId,
217
+ tools: [],
218
+ };
219
+ this.updateReceipt(active.receipt, { status: "running" });
220
+ this.publish();
221
+ const accepted = await this.runtime!.admitTurn({
222
+ userMessage: { role: "user", content: active.receipt.prompt! },
223
+ signal: active.controller.signal,
224
+ });
225
+ active.receipt = this.updateReceipt(active.receipt, {
226
+ status: "running",
227
+ turnId: accepted.turnId,
228
+ });
229
+ this.activity.activeTurnId = accepted.turnId;
230
+ this.publish();
231
+ const result = await accepted.completion;
232
+ this.updateReceipt(active.receipt, {
233
+ status: result.status,
234
+ ...(result.status === "failed" ? { error: result.error } : {}),
235
+ });
236
+ this.activity.status = result.status;
237
+ if (result.status === "failed") this.activity.error = result.error;
238
+ } catch (error) {
239
+ const status = active.controller.signal.aborted ? "cancelled" : "failed";
240
+ this.updateReceipt(active.receipt, {
241
+ status,
242
+ error: errorMessage(error),
243
+ });
244
+ this.activity.status = status;
245
+ this.activity.error = errorMessage(error);
246
+ } finally {
247
+ this.activity.streaming = undefined;
248
+ this.activity.interaction = undefined;
249
+ this.activity.activeRequestId = undefined;
250
+ this.activity.activeTurnId = undefined;
251
+ for (const tool of this.activity.tools) {
252
+ if (tool.status === "running")
253
+ tool.status = this.activity.status === "cancelled" ? "cancelled" : "failed";
254
+ }
255
+ this.active = undefined;
256
+ this.publish(true);
257
+ this.pump();
258
+ }
259
+ }
260
+
261
+ control(input: RemoteOperationInput, receipt: OperationReceipt): void {
262
+ // Separate from prompt execution: answering a wait cannot wait for that turn.
263
+ this.controlTail = this.controlTail.then(async () => {
264
+ try {
265
+ await this.open();
266
+ this.validate(input);
267
+ if (input.kind === "stop") {
268
+ if (this.active?.receipt.requestId === input.targetRequestId)
269
+ this.active.controller.abort();
270
+ const index = this.queue.findIndex(
271
+ (op) => op.requestId === input.targetRequestId,
272
+ );
273
+ if (index !== -1)
274
+ this.updateReceipt(this.queue.splice(index, 1)[0], {
275
+ status: "cancelled",
276
+ });
277
+ } else if (input.kind === "answer") {
278
+ await this.runtime!.resolveAskUser(
279
+ input.selectedIndex === null
280
+ ? { outcome: "dismissed" }
281
+ : { outcome: "selected", selectedIndex: input.selectedIndex },
282
+ );
283
+ } else if (input.kind === "confirm") {
284
+ await this.runtime!.resolveBashConfirmation(input.decision);
285
+ }
286
+ this.updateReceipt(receipt, { status: "completed" });
287
+ } catch (error) {
288
+ this.updateReceipt(receipt, {
289
+ status: "failed",
290
+ error: errorMessage(error),
291
+ });
292
+ }
293
+ this.publish();
294
+ });
295
+ }
296
+
297
+ private updateReceipt(
298
+ receipt: OperationReceipt,
299
+ patch: Partial<OperationReceipt>,
300
+ ): OperationReceipt {
301
+ const next = this.store.update({
302
+ ...this.store.get(receipt.requestId),
303
+ ...patch,
304
+ });
305
+ this.record = { ...this.record, updatedAt: next.updatedAt };
306
+ this.store.saveSession(this.record);
307
+ if (this.active?.receipt.requestId === receipt.requestId)
308
+ this.active.receipt = next;
309
+ return next;
310
+ }
311
+
312
+ private updateInteraction(): void {
313
+ const question = this.runtime?.askUser().pending;
314
+ const confirmation = this.runtime?.bashGuard().pending;
315
+ if (question || confirmation) {
316
+ if (!this.activity.interaction) {
317
+ this.activity.interaction = question
318
+ ? { id: randomUUID(), kind: "question", ...question }
319
+ : { id: randomUUID(), kind: "confirmation", ...confirmation! };
320
+ }
321
+ this.activity.status = "waiting_input";
322
+ } else {
323
+ this.activity.interaction = undefined;
324
+ if (this.active) this.activity.status = "running";
325
+ }
326
+ if (this.active)
327
+ this.updateReceipt(this.active.receipt, {
328
+ status: this.activity.status as OperationReceipt["status"],
329
+ });
330
+ this.publish();
331
+ }
332
+
333
+ async append(event: AgentEvent): Promise<void> {
334
+ if (!this.reader) return;
335
+ switch (event.type) {
336
+ case "turn.started":
337
+ this.activity.activeTurnId = event.turnId;
338
+ if (this.active)
339
+ this.updateReceipt(this.active.receipt, { turnId: event.turnId });
340
+ break;
341
+ case "model.request.started":
342
+ this.activity.streaming = undefined;
343
+ break;
344
+ case "model.request.finished":
345
+ this.activity.streaming = undefined;
346
+ break;
347
+ case "tool.started":
348
+ this.activity.tools.push({
349
+ id: event.data.call.toolCallId,
350
+ name: event.data.call.name,
351
+ arguments: JSON.stringify(event.data.call.args),
352
+ status: "running",
353
+ });
354
+ this.activity.tools = this.activity.tools.slice(-100);
355
+ break;
356
+ case "tool.finished": {
357
+ const tool = this.activity.tools.find((tool) => tool.id === event.toolCallId);
358
+ if (tool) tool.status = event.data.ok ? "completed" : "failed";
359
+ break;
360
+ }
361
+ case "tool.observation": {
362
+ const tool = this.activity.tools.find((tool) => tool.id === event.toolCallId);
363
+ if (tool) tool.detail = event.data.observation.displayText;
364
+ break;
365
+ }
366
+ case "turn.finished":
367
+ case "turn.failed":
368
+ case "turn.cancelled":
369
+ case "agent.iteration.finished":
370
+ case "turn.steering.applied":
371
+ break;
372
+ default:
373
+ return;
374
+ }
375
+ this.publish();
376
+ }
377
+ updateAssistantTextDelta(update: AssistantTextDeltaUpdate): void {
378
+ const previous = this.activity.streaming;
379
+ this.activity.streaming = {
380
+ iterationId: update.iterationId,
381
+ attempt: update.attemptNumber,
382
+ text:
383
+ previous?.iterationId === update.iterationId &&
384
+ previous.attempt === update.attemptNumber
385
+ ? previous.text + update.content
386
+ : update.content,
387
+ };
388
+ if (!this.streamTimer)
389
+ this.streamTimer = setTimeout(() => {
390
+ this.streamTimer = undefined;
391
+ this.publish();
392
+ }, 50);
393
+ }
394
+ private publish(refreshTurn = false): void {
395
+ if (this.streamTimer) {
396
+ clearTimeout(this.streamTimer);
397
+ this.streamTimer = undefined;
398
+ }
399
+ const messages = this.reader?.after(this.lastOrdinal) ?? [];
400
+ if (messages.length) this.lastOrdinal = messages.at(-1)!.ordinal;
401
+ if (refreshTurn) {
402
+ const latest = this.reader?.latestTurn();
403
+ messages.push(
404
+ ...(this.reader?.page().messages.filter((m) => m.turnId === latest?.id) ?? []),
405
+ );
406
+ }
407
+ this.hub.publish({ activity: this.readActivity(), messages });
408
+ }
409
+
410
+ async close(): Promise<void> {
411
+ this.stopping = true;
412
+ for (const receipt of this.queue.splice(0))
413
+ this.updateReceipt(receipt, {
414
+ status: "interrupted",
415
+ error: "The service was stopped before this queued request began.",
416
+ });
417
+ if (this.opening) await this.opening.catch(() => undefined);
418
+ if (this.runtime)
419
+ await this.runtime.dispose({
420
+ type: "runner_failed",
421
+ error: "Remote service shutdown.",
422
+ });
423
+ await this.active?.completion;
424
+ await this.controlTail;
425
+ for (const unsubscribe of this.unsubscribers) unsubscribe();
426
+ if (this.streamTimer) clearTimeout(this.streamTimer);
427
+ this.hub.close();
428
+ this.reader?.close();
429
+ }
430
+ get pendingCount(): number {
431
+ return this.queue.length + (this.active ? 1 : 0);
432
+ }
433
+ receiptChanged(): void {
434
+ this.publish();
435
+ }
436
+ get initialized(): boolean {
437
+ return this.runtime !== undefined;
438
+ }
439
+ }
440
+
441
+ function errorMessage(error: unknown): string {
442
+ return error instanceof Error ? error.message : String(error);
443
+ }