tinker-agent 1.6.0 → 1.8.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 +35 -1
- package/README.md +8 -2
- package/package.json +5 -3
- package/src/agent/runtime-session.ts +1 -0
- package/src/cli/command-line.ts +24 -2
- package/src/cli/main.ts +26 -0
- package/src/cli/output.ts +4 -4
- package/src/cli/public-cli-contract.ts +4 -0
- package/src/cli/runner-dependencies.ts +4 -1
- package/src/cli/update-runner.ts +308 -0
- package/src/events/bash-result-detail.ts +13 -6
- package/src/events/observation-text-log.ts +5 -1
- package/src/events/stdout-event-printer.ts +7 -2
- package/src/mcp/mcp-manager.ts +34 -3
- package/src/model/fake-model-client.ts +84 -0
- package/src/observation/observation-builder.ts +43 -6
- package/src/session/session-catalog.ts +17 -11
- package/src/session/session-store.ts +1 -0
- package/src/tools/bash-task.ts +125 -90
- package/src/tools/bash.ts +44 -13
- package/src/tools/registry.ts +2 -0
- package/src/tools/shell-process.ts +296 -0
- package/src/tools/task-input.ts +229 -0
- package/src/tools/task-output-tool.ts +4 -1
- package/src/tools/terminal-screen.ts +105 -0
- package/src/tools/types.ts +32 -0
- package/src/tui/components/background-tasks.tsx +3 -2
- package/src/tui/components/resume-session-picker.tsx +292 -46
- package/src/tui/event-store.ts +14 -1
- package/src/tui/tui-session-controller.ts +1 -1
|
@@ -202,6 +202,7 @@ function formatToolRawResult(call: ToolCall, raw: ToolRawResult): string[] {
|
|
|
202
202
|
return optionalLine(formatDiff(call, raw));
|
|
203
203
|
case "bash":
|
|
204
204
|
case "task_output":
|
|
205
|
+
case "task_input":
|
|
205
206
|
return optionalLine(formatBashResult(call, raw));
|
|
206
207
|
case "task_list":
|
|
207
208
|
case "task_stop":
|
|
@@ -278,7 +279,7 @@ function formatDiff(call: ToolCall, raw: ToolRawResult): string | undefined {
|
|
|
278
279
|
}
|
|
279
280
|
|
|
280
281
|
function formatBashResult(_call: ToolCall, raw: ToolRawResult): string | undefined {
|
|
281
|
-
if (raw.kind !== "bash" && raw.kind !== "task_output") {
|
|
282
|
+
if (raw.kind !== "bash" && raw.kind !== "task_output" && raw.kind !== "task_input") {
|
|
282
283
|
return undefined;
|
|
283
284
|
}
|
|
284
285
|
|
|
@@ -348,7 +349,11 @@ function formatToolLine(prefix: string, call: ToolCall): string {
|
|
|
348
349
|
return `${prefix} name=${call.name}\n`;
|
|
349
350
|
}
|
|
350
351
|
|
|
351
|
-
if (
|
|
352
|
+
if (
|
|
353
|
+
call.name === "TaskOutput" ||
|
|
354
|
+
call.name === "TaskInput" ||
|
|
355
|
+
call.name === "TaskStop"
|
|
356
|
+
) {
|
|
352
357
|
const taskId = toolTaskId(call);
|
|
353
358
|
return `${prefix} name=${call.name}${taskId === undefined ? "" : ` task=${taskId}`}\n`;
|
|
354
359
|
}
|
package/src/mcp/mcp-manager.ts
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
2
6
|
import {
|
|
3
7
|
StdioClientTransport,
|
|
4
8
|
getDefaultEnvironment,
|
|
5
9
|
} from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
6
10
|
import type { RuntimeSessionContext } from "../agent/runtime-session";
|
|
11
|
+
import { ListRootsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
7
12
|
import type { ToolExecutor } from "../tools/types";
|
|
8
13
|
import type { McpConfig, McpServerConfig } from "./mcp-config";
|
|
9
14
|
import { createMcpToolExecutor } from "./mcp-tool-executor";
|
|
10
15
|
|
|
11
16
|
const STDERR_TAIL_MAX_CHARS = 2_000;
|
|
12
17
|
|
|
18
|
+
function temporaryDirectoryEnvironment(): Record<string, string> {
|
|
19
|
+
return process.platform === "win32" ? { TEMP: tmpdir() } : { TMPDIR: tmpdir() };
|
|
20
|
+
}
|
|
21
|
+
|
|
13
22
|
export type McpClientConnection = {
|
|
14
23
|
client: Client;
|
|
15
24
|
close(): Promise<void>;
|
|
@@ -18,6 +27,7 @@ export type McpClientConnection = {
|
|
|
18
27
|
export type McpClientFactory = (
|
|
19
28
|
serverName: string,
|
|
20
29
|
serverConfig: McpServerConfig,
|
|
30
|
+
workspaceRoot: string,
|
|
21
31
|
) => Promise<McpClientConnection>;
|
|
22
32
|
|
|
23
33
|
export type McpManager = {
|
|
@@ -37,6 +47,7 @@ export type McpServerInventory = {
|
|
|
37
47
|
|
|
38
48
|
export type CreateMcpManagerOptions = {
|
|
39
49
|
config: McpConfig;
|
|
50
|
+
workspaceRoot: string;
|
|
40
51
|
runtimeSession: RuntimeSessionContext;
|
|
41
52
|
clientFactory?: McpClientFactory;
|
|
42
53
|
timeoutMs?: number;
|
|
@@ -70,7 +81,11 @@ export async function createMcpManager(
|
|
|
70
81
|
let tools;
|
|
71
82
|
|
|
72
83
|
try {
|
|
73
|
-
connection = await clientFactory(
|
|
84
|
+
connection = await clientFactory(
|
|
85
|
+
serverName,
|
|
86
|
+
serverConfig,
|
|
87
|
+
options.workspaceRoot,
|
|
88
|
+
);
|
|
74
89
|
} catch (error) {
|
|
75
90
|
await options.runtimeSession.append({
|
|
76
91
|
type: "mcp.server.failed",
|
|
@@ -227,11 +242,16 @@ async function closeConnections(
|
|
|
227
242
|
async function stdioClientFactory(
|
|
228
243
|
serverName: string,
|
|
229
244
|
serverConfig: McpServerConfig,
|
|
245
|
+
workspaceRoot: string,
|
|
230
246
|
): Promise<McpClientConnection> {
|
|
231
247
|
const transport = new StdioClientTransport({
|
|
232
248
|
command: serverConfig.command,
|
|
233
249
|
args: serverConfig.args,
|
|
234
|
-
env: {
|
|
250
|
+
env: {
|
|
251
|
+
...getDefaultEnvironment(),
|
|
252
|
+
...temporaryDirectoryEnvironment(),
|
|
253
|
+
...serverConfig.env,
|
|
254
|
+
},
|
|
235
255
|
cwd: serverConfig.cwd,
|
|
236
256
|
stderr: "pipe",
|
|
237
257
|
});
|
|
@@ -241,7 +261,18 @@ async function stdioClientFactory(
|
|
|
241
261
|
stderrTail = (stderrTail + chunk.toString("utf8")).slice(-STDERR_TAIL_MAX_CHARS);
|
|
242
262
|
});
|
|
243
263
|
|
|
244
|
-
const client = new Client(
|
|
264
|
+
const client = new Client(
|
|
265
|
+
{ name: "tinker", version: "0.1.0" },
|
|
266
|
+
{ capabilities: { roots: { listChanged: false } } },
|
|
267
|
+
);
|
|
268
|
+
client.setRequestHandler(ListRootsRequestSchema, () => ({
|
|
269
|
+
roots: [
|
|
270
|
+
{
|
|
271
|
+
uri: pathToFileURL(workspaceRoot).href,
|
|
272
|
+
name: path.basename(workspaceRoot),
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
}));
|
|
245
276
|
|
|
246
277
|
try {
|
|
247
278
|
await client.connect(transport);
|
|
@@ -247,6 +247,9 @@ export class FakeModelClient implements ModelClient {
|
|
|
247
247
|
if (this.mode === "pty-background-task") {
|
|
248
248
|
return this.ptyBackgroundTask(input, prepared, options);
|
|
249
249
|
}
|
|
250
|
+
if (this.mode === "pty-interactive-terminal") {
|
|
251
|
+
return this.ptyInteractiveTerminal(input, prepared, options);
|
|
252
|
+
}
|
|
250
253
|
if (this.mode === "pty-resume") {
|
|
251
254
|
return this.ptyResume(input, prepared, options);
|
|
252
255
|
}
|
|
@@ -658,6 +661,87 @@ export class FakeModelClient implements ModelClient {
|
|
|
658
661
|
return textOutput(prepared, "PTY_BACKGROUND_STOPPED");
|
|
659
662
|
}
|
|
660
663
|
|
|
664
|
+
private ptyInteractiveTerminal(
|
|
665
|
+
input: ModelRequestInput,
|
|
666
|
+
prepared: PreparedModelRequest,
|
|
667
|
+
options: ModelRequestOptions,
|
|
668
|
+
): ModelRequestOutput {
|
|
669
|
+
requireTools(input, ["Bash", "TaskOutput", "TaskInput"]);
|
|
670
|
+
const prompt = lastUserMessage(input.messages);
|
|
671
|
+
if (prompt === "PTY_INTERACTIVE_FOLLOWUP") {
|
|
672
|
+
requireMessage(input.messages, "assistant", "PTY_INTERACTIVE_DONE");
|
|
673
|
+
return textOutput(prepared, "PTY_INTERACTIVE_FOLLOWUP_DONE");
|
|
674
|
+
}
|
|
675
|
+
if (prompt !== "PTY_INTERACTIVE_TERMINAL" && prompt !== "PTY_INTERACTIVE_QUIT") {
|
|
676
|
+
throw new Error(
|
|
677
|
+
`Unexpected pty-interactive-terminal prompt: ${JSON.stringify(prompt)}.`,
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const tools = toolMessagesAfterLastUser(input.messages);
|
|
682
|
+
const bash = tools.find((message) => message.name === "Bash");
|
|
683
|
+
if (bash === undefined) {
|
|
684
|
+
return toolCallOutput(prepared, options, "Bash", {
|
|
685
|
+
command: "python3 -q",
|
|
686
|
+
description: "Start interactive Python fixture",
|
|
687
|
+
tty: true,
|
|
688
|
+
timeout: 25,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
if (!bash.content.includes("taskId=") || !bash.content.includes("tty=true")) {
|
|
692
|
+
throw new Error("PTY Bash task did not return an interactive task ID.");
|
|
693
|
+
}
|
|
694
|
+
const taskId = requireObservationValue(bash.content, "taskId");
|
|
695
|
+
|
|
696
|
+
const outputs = tools.filter((message) => message.name === "TaskOutput");
|
|
697
|
+
const output = outputs.at(-1);
|
|
698
|
+
if (output === undefined || !output.content.includes(">>>")) {
|
|
699
|
+
if (outputs.length >= 20) {
|
|
700
|
+
throw new Error("Interactive Python fixture did not show its prompt.");
|
|
701
|
+
}
|
|
702
|
+
return toolCallOutput(prepared, options, "TaskOutput", {
|
|
703
|
+
task_id: taskId,
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const inputs = tools.filter((message) => message.name === "TaskInput");
|
|
708
|
+
if (inputs.length === 0) {
|
|
709
|
+
return toolCallOutput(prepared, options, "TaskInput", {
|
|
710
|
+
task_id: taskId,
|
|
711
|
+
chars:
|
|
712
|
+
prompt === "PTY_INTERACTIVE_QUIT"
|
|
713
|
+
? "import os; print('PTY_INTERACTIVE_PID=' + str(os.getpid()))\n"
|
|
714
|
+
: "print(6 * 7)\n",
|
|
715
|
+
wait_ms: 250,
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const latestInput = inputs.at(-1);
|
|
720
|
+
const expected = prompt === "PTY_INTERACTIVE_QUIT" ? "PTY_INTERACTIVE_PID=" : "42";
|
|
721
|
+
if (!latestInput?.content.includes(expected)) {
|
|
722
|
+
if (inputs.length >= 20) {
|
|
723
|
+
throw new Error(`Interactive Python fixture did not show ${expected}.`);
|
|
724
|
+
}
|
|
725
|
+
return toolCallOutput(prepared, options, "TaskInput", {
|
|
726
|
+
task_id: taskId,
|
|
727
|
+
chars: "",
|
|
728
|
+
wait_ms: 250,
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (prompt === "PTY_INTERACTIVE_QUIT") {
|
|
733
|
+
return textOutput(prepared, "PTY_INTERACTIVE_RUNNING");
|
|
734
|
+
}
|
|
735
|
+
if (!inputs.some((message) => message.content.includes("status=completed"))) {
|
|
736
|
+
return toolCallOutput(prepared, options, "TaskInput", {
|
|
737
|
+
task_id: taskId,
|
|
738
|
+
chars: "exit()\n",
|
|
739
|
+
wait_ms: 500,
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
return textOutput(prepared, "PTY_INTERACTIVE_DONE");
|
|
743
|
+
}
|
|
744
|
+
|
|
661
745
|
private ptyResume(
|
|
662
746
|
input: ModelRequestInput,
|
|
663
747
|
prepared: PreparedModelRequest,
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
ReadFileRawResult,
|
|
12
12
|
RecallRawResult,
|
|
13
13
|
SkillRawResult,
|
|
14
|
+
TaskInputRawResult,
|
|
14
15
|
TaskListRawResult,
|
|
15
16
|
TaskOutputRawResult,
|
|
16
17
|
TaskStopRawResult,
|
|
@@ -51,6 +52,8 @@ export class ObservationBuilder {
|
|
|
51
52
|
return { content: renderTaskListObservation(input.raw) };
|
|
52
53
|
case "task_output":
|
|
53
54
|
return { content: renderTaskOutputObservation(input.raw) };
|
|
55
|
+
case "task_input":
|
|
56
|
+
return { content: renderTaskInputObservation(input.raw) };
|
|
54
57
|
case "task_stop":
|
|
55
58
|
return { content: renderTaskStopObservation(input.raw) };
|
|
56
59
|
case "web_search":
|
|
@@ -331,8 +334,11 @@ function renderBashObservation(raw: BashRawResult): string {
|
|
|
331
334
|
`timeoutMs=${raw.timeoutMs ?? 0}`,
|
|
332
335
|
`command=${raw.command}`,
|
|
333
336
|
`cwd=${raw.cwd}`,
|
|
337
|
+
`tty=${raw.tty}`,
|
|
334
338
|
`outputFilePath=${raw.outputFilePath}`,
|
|
335
|
-
|
|
339
|
+
raw.tty
|
|
340
|
+
? "Use TaskOutput to inspect the current terminal screen and TaskInput to interact."
|
|
341
|
+
: "Use TaskOutput to inspect current output.",
|
|
336
342
|
].join("\n");
|
|
337
343
|
}
|
|
338
344
|
|
|
@@ -342,8 +348,11 @@ function renderBashObservation(raw: BashRawResult): string {
|
|
|
342
348
|
`taskId=${raw.taskId}`,
|
|
343
349
|
`command=${raw.command}`,
|
|
344
350
|
`cwd=${raw.cwd}`,
|
|
351
|
+
`tty=${raw.tty}`,
|
|
345
352
|
`outputFilePath=${raw.outputFilePath}`,
|
|
346
|
-
|
|
353
|
+
raw.tty
|
|
354
|
+
? "Use TaskOutput to inspect the current terminal screen and TaskInput to interact."
|
|
355
|
+
: "Use TaskOutput to inspect current output.",
|
|
347
356
|
].join("\n");
|
|
348
357
|
}
|
|
349
358
|
|
|
@@ -353,6 +362,7 @@ function renderBashObservation(raw: BashRawResult): string {
|
|
|
353
362
|
`exitCode=${raw.exitCode ?? "null"}`,
|
|
354
363
|
`status=${raw.status}`,
|
|
355
364
|
`cwd=${raw.cwd}`,
|
|
365
|
+
`tty=${raw.tty}`,
|
|
356
366
|
`outputFilePath=${raw.outputFilePath}`,
|
|
357
367
|
`outputBytes=${raw.outputBytes}`,
|
|
358
368
|
`outputLines=${raw.outputLines}`,
|
|
@@ -362,8 +372,9 @@ function renderBashObservation(raw: BashRawResult): string {
|
|
|
362
372
|
? undefined
|
|
363
373
|
: `returnCodeInterpretation=${raw.returnCodeInterpretation}`,
|
|
364
374
|
raw.error === undefined ? undefined : `error=${raw.error}`,
|
|
365
|
-
|
|
366
|
-
raw.preview,
|
|
375
|
+
raw.tty ? `screen=${raw.screenColumns ?? 80}x${raw.screenRows ?? 24}` : undefined,
|
|
376
|
+
raw.tty ? "current screen:" : "preview:",
|
|
377
|
+
raw.tty ? (raw.screen ?? "") : raw.preview,
|
|
367
378
|
]
|
|
368
379
|
.filter((line): line is string => line !== undefined)
|
|
369
380
|
.join("\n");
|
|
@@ -387,23 +398,48 @@ function renderTaskOutputObservation(raw: TaskOutputRawResult): string {
|
|
|
387
398
|
return `TaskOutput failed for ${raw.taskId || "(unknown task ID)"}: ${raw.error ?? "Unknown error."}`;
|
|
388
399
|
}
|
|
389
400
|
|
|
401
|
+
const terminalScreen = raw.task.tty;
|
|
390
402
|
return [
|
|
391
403
|
"Task output retrieved.",
|
|
392
404
|
`taskId=${raw.taskId}`,
|
|
393
405
|
`status=${raw.task.status}`,
|
|
394
406
|
`command=${raw.task.command}`,
|
|
407
|
+
`tty=${terminalScreen}`,
|
|
395
408
|
`outputFilePath=${raw.outputFilePath}`,
|
|
396
409
|
`outputBytes=${raw.outputBytes ?? 0}`,
|
|
397
410
|
`outputLines=${raw.outputLines ?? 0}`,
|
|
398
411
|
`truncated=${raw.truncated ?? false}`,
|
|
399
412
|
raw.omittedLines === undefined ? undefined : `omittedLines=${raw.omittedLines}`,
|
|
400
|
-
|
|
401
|
-
|
|
413
|
+
terminalScreen
|
|
414
|
+
? `screen=${raw.screenColumns ?? 80}x${raw.screenRows ?? 24}`
|
|
415
|
+
: undefined,
|
|
416
|
+
terminalScreen ? "current screen:" : "preview:",
|
|
417
|
+
terminalScreen ? (raw.screen ?? "") : (raw.preview ?? ""),
|
|
402
418
|
]
|
|
403
419
|
.filter((line): line is string => line !== undefined)
|
|
404
420
|
.join("\n");
|
|
405
421
|
}
|
|
406
422
|
|
|
423
|
+
function renderTaskInputObservation(raw: TaskInputRawResult): string {
|
|
424
|
+
if (!raw.ok) {
|
|
425
|
+
return `TaskInput failed for ${raw.taskId || "(unknown task ID)"}: ${raw.error}`;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return [
|
|
429
|
+
"Terminal input sent.",
|
|
430
|
+
`taskId=${raw.taskId}`,
|
|
431
|
+
`status=${raw.status}`,
|
|
432
|
+
`writtenBytes=${raw.writtenBytes}`,
|
|
433
|
+
`waitedMs=${raw.waitedMs}`,
|
|
434
|
+
`screen=${raw.screenColumns}x${raw.screenRows}`,
|
|
435
|
+
`outputFilePath=${raw.outputFilePath}`,
|
|
436
|
+
`outputBytes=${raw.outputBytes}`,
|
|
437
|
+
`outputLines=${raw.outputLines}`,
|
|
438
|
+
"current screen:",
|
|
439
|
+
raw.screen,
|
|
440
|
+
].join("\n");
|
|
441
|
+
}
|
|
442
|
+
|
|
407
443
|
function renderTaskStopObservation(raw: TaskStopRawResult): string {
|
|
408
444
|
if (!raw.ok || raw.task === undefined) {
|
|
409
445
|
return `TaskStop failed for ${raw.taskId || "(unknown task ID)"}: ${raw.error ?? "Unknown error."}`;
|
|
@@ -428,6 +464,7 @@ function renderTaskSummary(task: TaskListRawResult["tasks"][number]): string {
|
|
|
428
464
|
`taskId=${task.taskId}`,
|
|
429
465
|
`description=${task.description}`,
|
|
430
466
|
`status=${task.status}`,
|
|
467
|
+
`tty=${task.tty}`,
|
|
431
468
|
`startedAt=${task.startedAt}`,
|
|
432
469
|
task.endedAt === undefined ? undefined : `endedAt=${task.endedAt}`,
|
|
433
470
|
task.exitCode === undefined ? undefined : `exitCode=${task.exitCode}`,
|
|
@@ -34,6 +34,15 @@ export class SessionCatalog {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
async list(currentSessionId?: SessionId): Promise<readonly SessionSummary[]> {
|
|
37
|
+
const summaries = await this.scan(currentSessionId);
|
|
38
|
+
return Object.freeze(summaries.slice(0, this.input.limit ?? 20));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async listAll(currentSessionId?: SessionId): Promise<readonly SessionSummary[]> {
|
|
42
|
+
return Object.freeze(await this.scan(currentSessionId));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private async scan(currentSessionId?: SessionId): Promise<SessionSummary[]> {
|
|
37
46
|
const workspaceRoot = await this.workspaceRootPromise;
|
|
38
47
|
const sessionsRoot = path.join(workspaceRoot, ".tinker", "sessions");
|
|
39
48
|
let entries;
|
|
@@ -63,17 +72,14 @@ export class SessionCatalog {
|
|
|
63
72
|
);
|
|
64
73
|
}
|
|
65
74
|
|
|
66
|
-
return
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))
|
|
75
|
-
.slice(0, this.input.limit ?? 20),
|
|
76
|
-
);
|
|
75
|
+
return summaries
|
|
76
|
+
.filter(
|
|
77
|
+
(summary) =>
|
|
78
|
+
summary.turnCount > 0 ||
|
|
79
|
+
summary.status === "incomplete" ||
|
|
80
|
+
summary.status === "unavailable",
|
|
81
|
+
)
|
|
82
|
+
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
77
83
|
}
|
|
78
84
|
|
|
79
85
|
async get(
|