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,213 @@
1
+ import { createBashToolExecutor } from "./bash";
2
+ import { ShellTaskManager } from "./bash-task";
3
+ import { createCwdState } from "./cwd-state";
4
+ import { createEditToolExecutor } from "./edit";
5
+ import { createGlobToolExecutor } from "./glob";
6
+ import { createGrepToolExecutor } from "./grep";
7
+ import { createReadToolExecutor } from "./read";
8
+ import { createRecallToolExecutor } from "./recall";
9
+ import { createTaskListToolExecutor } from "./task-list";
10
+ import { createTaskOutputToolExecutor } from "./task-output-tool";
11
+ import { createTaskStopToolExecutor } from "./task-stop";
12
+ import { createWebFetchToolExecutor } from "./web-fetch";
13
+ import type { Refiner } from "./web-fetch/refiner";
14
+ import { createWebSearchToolExecutor } from "./web-search";
15
+ import { createWriteToolExecutor } from "./write";
16
+ import { cancellationError, throwIfTurnCancelled } from "../agent/turn-cancellation";
17
+ import type {
18
+ RuntimeSessionContext,
19
+ SessionDisposeReason,
20
+ } from "../agent/runtime-session";
21
+ import type {
22
+ ReadSnapshotStore,
23
+ ToolDefinition,
24
+ ToolExecutionContext,
25
+ ToolExecutor,
26
+ ToolRawResult,
27
+ } from "./types";
28
+ import type { ToolCall } from "../agent/types";
29
+ import type { SessionHistoryReader } from "../session/session-history-reader";
30
+ import { ToolExecutionFatalError } from "./types";
31
+
32
+ export class ToolRegistry {
33
+ private readonly tools = new Map<string, ToolExecutor>();
34
+ private readonly sources = new Map<string, string>();
35
+
36
+ register(tool: ToolExecutor, source = "built-in"): void {
37
+ const name = tool.definition.name;
38
+ const existingSource = this.sources.get(name);
39
+ if (existingSource !== undefined) {
40
+ throw new Error(
41
+ `Tool ${name} from ${source} conflicts with an existing registration from ${existingSource}.`,
42
+ );
43
+ }
44
+ this.tools.set(name, tool);
45
+ this.sources.set(name, source);
46
+ }
47
+
48
+ definitions(): ToolDefinition[] {
49
+ return [...this.tools.values()].map((tool) => tool.definition);
50
+ }
51
+
52
+ get(name: string): ToolExecutor | undefined {
53
+ return this.tools.get(name);
54
+ }
55
+ }
56
+
57
+ export class ToolRuntime {
58
+ constructor(private readonly registry: ToolRegistry) {}
59
+
60
+ async execute(call: ToolCall, context: ToolExecutionContext): Promise<ToolRawResult> {
61
+ throwIfTurnCancelled(context.signal);
62
+
63
+ if (call.argsParseError !== undefined) {
64
+ return {
65
+ kind: "generic",
66
+ ok: false,
67
+ toolName: call.name,
68
+ error: `Invalid tool arguments JSON: ${call.argsParseError}`,
69
+ };
70
+ }
71
+
72
+ const tool = this.registry.get(call.name);
73
+
74
+ if (tool === undefined) {
75
+ return {
76
+ kind: "generic",
77
+ ok: false,
78
+ toolName: call.name,
79
+ error: `Unknown tool: ${call.name}`,
80
+ };
81
+ }
82
+
83
+ try {
84
+ return await tool.execute(call.args, call, context);
85
+ } catch (error) {
86
+ if (context.signal.aborted) {
87
+ throw cancellationError(context.signal, error);
88
+ }
89
+ if (error instanceof ToolExecutionFatalError) {
90
+ throw error;
91
+ }
92
+
93
+ return {
94
+ kind: "generic",
95
+ ok: false,
96
+ toolName: call.name,
97
+ error: error instanceof Error ? error.message : String(error),
98
+ };
99
+ }
100
+ }
101
+ }
102
+
103
+ export type DefaultTooling = {
104
+ registry: ToolRegistry;
105
+ runtime: ToolRuntime;
106
+ snapshots: ReadSnapshotStore;
107
+ bashState: BashToolingState;
108
+ taskManager: ShellTaskManager;
109
+ dispose(reason?: SessionDisposeReason["type"]): Promise<void>;
110
+ };
111
+
112
+ export type BashToolingState = {
113
+ cwd: string;
114
+ sessionId: string;
115
+ workspaceRoot: string;
116
+ };
117
+
118
+ export function createDefaultTooling(options: {
119
+ workspaceRoot: string;
120
+ runtimeSession: RuntimeSessionContext;
121
+ historyReader: SessionHistoryReader;
122
+ maxReadContentBytes?: number;
123
+ exaApiKey?: string;
124
+ webFetchRefiner?: Refiner;
125
+ taskStopGraceMs?: number;
126
+ }): DefaultTooling {
127
+ const snapshots: ReadSnapshotStore = new Map();
128
+ const registry = new ToolRegistry();
129
+ const runtimeSession = options.runtimeSession;
130
+ const cwdState = createCwdState(options.workspaceRoot);
131
+ const taskManager = new ShellTaskManager({
132
+ workspaceRoot: options.workspaceRoot,
133
+ cwdState,
134
+ runtimeSession,
135
+ stopGraceMs: options.taskStopGraceMs,
136
+ });
137
+
138
+ registry.register(
139
+ createGlobToolExecutor({
140
+ workspaceRoot: options.workspaceRoot,
141
+ }),
142
+ );
143
+ registry.register(
144
+ createGrepToolExecutor({
145
+ workspaceRoot: options.workspaceRoot,
146
+ cwdState,
147
+ }),
148
+ );
149
+ registry.register(
150
+ createReadToolExecutor({
151
+ workspaceRoot: options.workspaceRoot,
152
+ snapshots,
153
+ maxContentBytes: options.maxReadContentBytes,
154
+ }),
155
+ );
156
+ registry.register(createRecallToolExecutor({ historyReader: options.historyReader }));
157
+ registry.register(
158
+ createWriteToolExecutor({
159
+ workspaceRoot: options.workspaceRoot,
160
+ snapshots,
161
+ }),
162
+ );
163
+ registry.register(
164
+ createEditToolExecutor({
165
+ workspaceRoot: options.workspaceRoot,
166
+ snapshots,
167
+ }),
168
+ );
169
+ registry.register(
170
+ createBashToolExecutor({
171
+ workspaceRoot: options.workspaceRoot,
172
+ cwdState,
173
+ taskManager,
174
+ }),
175
+ );
176
+ registry.register(createTaskListToolExecutor({ taskManager }));
177
+ registry.register(createTaskOutputToolExecutor({ taskManager }));
178
+ registry.register(createTaskStopToolExecutor({ taskManager }));
179
+
180
+ const exaApiKey = options.exaApiKey ?? process.env.EXA_API_KEY;
181
+ const hasExaKey = exaApiKey !== undefined && exaApiKey.trim() !== "";
182
+
183
+ if (hasExaKey) {
184
+ registry.register(createWebSearchToolExecutor({ apiKey: exaApiKey }));
185
+ }
186
+
187
+ registry.register(
188
+ createWebFetchToolExecutor({
189
+ exaApiKey: hasExaKey ? exaApiKey : undefined,
190
+ refiner: options.webFetchRefiner,
191
+ }),
192
+ );
193
+
194
+ return {
195
+ registry,
196
+ runtime: new ToolRuntime(registry),
197
+ snapshots,
198
+ taskManager,
199
+ bashState: {
200
+ get cwd() {
201
+ return cwdState.cwd;
202
+ },
203
+ set cwd(value: string) {
204
+ cwdState.cwd = value;
205
+ },
206
+ sessionId: runtimeSession.sessionId,
207
+ workspaceRoot: options.workspaceRoot,
208
+ },
209
+ async dispose(reason = "oneshot_complete") {
210
+ await taskManager.shutdown(reason);
211
+ },
212
+ };
213
+ }
@@ -0,0 +1,220 @@
1
+ import { execFile } from "node:child_process";
2
+ import { cancellationError, throwIfTurnCancelled } from "../agent/turn-cancellation";
3
+
4
+ export const RIPGREP_MISSING_ERROR =
5
+ "ripgrep is required. Install rg and ensure it is available on PATH.";
6
+
7
+ const defaultTimeoutMs = 20_000;
8
+ const defaultMaxBufferBytes = 20_000_000;
9
+
10
+ export type RipgrepResult = {
11
+ ok: boolean;
12
+ lines: string[];
13
+ exitCode?: number;
14
+ truncated: boolean;
15
+ error?: string;
16
+ };
17
+
18
+ export type RipgrepOptions = {
19
+ signal: AbortSignal;
20
+ timeoutMs?: number;
21
+ maxBufferBytes?: number;
22
+ };
23
+
24
+ export function findRipgrepCommand(): string {
25
+ return process.env.TINKER_RIPGREP_PATH ?? "rg";
26
+ }
27
+
28
+ export async function ripGrep(
29
+ args: string[],
30
+ options: RipgrepOptions,
31
+ ): Promise<RipgrepResult> {
32
+ throwIfTurnCancelled(options.signal);
33
+ const timeoutMs =
34
+ options.timeoutMs ??
35
+ parsePositiveInteger(process.env.TINKER_GREP_TIMEOUT_MS, defaultTimeoutMs);
36
+ const maxBufferBytes =
37
+ options.maxBufferBytes ??
38
+ parsePositiveInteger(
39
+ process.env.TINKER_GREP_MAX_BUFFER_BYTES,
40
+ defaultMaxBufferBytes,
41
+ );
42
+
43
+ const first = await runRipgrep(args, timeoutMs, maxBufferBytes, options.signal);
44
+ if (first.retryWithSingleThread) {
45
+ throwIfTurnCancelled(options.signal);
46
+ return finalizeResult(
47
+ await runRipgrep(["-j", "1", ...args], timeoutMs, maxBufferBytes, options.signal),
48
+ );
49
+ }
50
+
51
+ return finalizeResult(first);
52
+ }
53
+
54
+ type RipgrepAttempt = {
55
+ ok: boolean;
56
+ stdout: string;
57
+ exitCode?: number;
58
+ truncated: boolean;
59
+ error?: string;
60
+ retryWithSingleThread?: boolean;
61
+ };
62
+
63
+ function runRipgrep(
64
+ args: string[],
65
+ timeoutMs: number,
66
+ maxBufferBytes: number,
67
+ signal: AbortSignal,
68
+ ): Promise<RipgrepAttempt> {
69
+ return new Promise((resolve, reject) => {
70
+ if (signal.aborted) {
71
+ reject(cancellationError(signal));
72
+ return;
73
+ }
74
+
75
+ execFile(
76
+ findRipgrepCommand(),
77
+ args,
78
+ { timeout: timeoutMs, maxBuffer: maxBufferBytes, signal },
79
+ (error, stdout, stderr) => {
80
+ if (signal.aborted) {
81
+ reject(cancellationError(signal, error));
82
+ return;
83
+ }
84
+
85
+ if (error === null) {
86
+ resolve({ ok: true, stdout, exitCode: 0, truncated: false });
87
+ return;
88
+ }
89
+
90
+ const execError = error as Error & {
91
+ killed?: boolean;
92
+ signal?: string | null;
93
+ code?: number | string;
94
+ };
95
+
96
+ if (execError.code === "ENOENT") {
97
+ resolve({
98
+ ok: false,
99
+ stdout: "",
100
+ truncated: false,
101
+ error: RIPGREP_MISSING_ERROR,
102
+ });
103
+ return;
104
+ }
105
+
106
+ if (isEagainError(execError, stderr)) {
107
+ resolve({
108
+ ok: false,
109
+ stdout: "",
110
+ truncated: false,
111
+ retryWithSingleThread: true,
112
+ });
113
+ return;
114
+ }
115
+
116
+ if (execError.code === 1 && stderr.trim() === "") {
117
+ resolve({ ok: true, stdout, exitCode: 1, truncated: false });
118
+ return;
119
+ }
120
+
121
+ if (execError.killed === true || typeof execError.signal === "string") {
122
+ resolve({
123
+ ok: false,
124
+ stdout,
125
+ truncated: true,
126
+ error: `ripgrep timed out after ${timeoutMs}ms. Narrow the search with path, glob, or a more specific pattern.`,
127
+ });
128
+ return;
129
+ }
130
+
131
+ if (execError.message.includes("maxBuffer")) {
132
+ resolve({
133
+ ok: false,
134
+ stdout,
135
+ truncated: true,
136
+ error: `ripgrep output exceeded ${maxBufferBytes} bytes. Narrow the search with path, glob, or a more specific pattern.`,
137
+ });
138
+ return;
139
+ }
140
+
141
+ resolve({
142
+ ok: false,
143
+ stdout,
144
+ exitCode: typeof execError.code === "number" ? execError.code : undefined,
145
+ truncated: false,
146
+ error: stderr.trim() !== "" ? stderr.trim() : execError.message,
147
+ });
148
+ },
149
+ );
150
+ });
151
+ }
152
+
153
+ function finalizeResult(attempt: RipgrepAttempt): RipgrepResult {
154
+ const lines = splitCompleteLines(attempt.stdout, attempt.truncated);
155
+
156
+ if (attempt.ok) {
157
+ return {
158
+ ok: true,
159
+ lines,
160
+ exitCode: attempt.exitCode,
161
+ truncated: false,
162
+ };
163
+ }
164
+
165
+ if (attempt.truncated && lines.length > 0) {
166
+ return {
167
+ ok: true,
168
+ lines,
169
+ exitCode: attempt.exitCode,
170
+ truncated: true,
171
+ error: attempt.error,
172
+ };
173
+ }
174
+
175
+ return {
176
+ ok: false,
177
+ lines: [],
178
+ exitCode: attempt.exitCode,
179
+ truncated: attempt.truncated,
180
+ error: attempt.error ?? "ripgrep failed.",
181
+ };
182
+ }
183
+
184
+ function splitCompleteLines(stdout: string, droppedPartialTail: boolean): string[] {
185
+ if (stdout === "") {
186
+ return [];
187
+ }
188
+
189
+ const lines = stdout.split("\n");
190
+ const last = lines.at(-1);
191
+
192
+ if (last === "") {
193
+ lines.pop();
194
+ } else if (droppedPartialTail) {
195
+ lines.pop();
196
+ }
197
+
198
+ return lines;
199
+ }
200
+
201
+ function isEagainError(
202
+ error: Error & { code?: number | string },
203
+ stderr: string,
204
+ ): boolean {
205
+ return (
206
+ error.code === "EAGAIN" ||
207
+ error.message.includes("EAGAIN") ||
208
+ error.message.includes("Resource temporarily unavailable") ||
209
+ stderr.includes("Resource temporarily unavailable")
210
+ );
211
+ }
212
+
213
+ function parsePositiveInteger(value: string | undefined, fallback: number): number {
214
+ if (value === undefined || value.trim() === "") {
215
+ return fallback;
216
+ }
217
+
218
+ const parsed = Number(value);
219
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
220
+ }
@@ -0,0 +1,59 @@
1
+ import type { ShellTaskManager } from "./bash-task";
2
+ import { throwIfTurnCancelled } from "../agent/turn-cancellation";
3
+ import { defineToolExecutor } from "./types";
4
+ import type { TaskListRawResult, ToolExecutionContext, ToolExecutor } from "./types";
5
+
6
+ export function createTaskListToolExecutor(options: {
7
+ taskManager: ShellTaskManager;
8
+ }): ToolExecutor {
9
+ return defineToolExecutor("task_list", {
10
+ definition: {
11
+ name: "TaskList",
12
+ description: "List background shell tasks in the current session.",
13
+ parameters: {
14
+ type: "object",
15
+ additionalProperties: false,
16
+ properties: {},
17
+ },
18
+ },
19
+ async execute(
20
+ args,
21
+ _call,
22
+ context: ToolExecutionContext,
23
+ ): Promise<TaskListRawResult> {
24
+ throwIfTurnCancelled(context.signal);
25
+ if (!isRecord(args)) {
26
+ return {
27
+ ok: false,
28
+ runningCount: 0,
29
+ tasks: [],
30
+ error: "TaskList arguments must be an object.",
31
+ };
32
+ }
33
+
34
+ const unexpected = Object.keys(args)[0];
35
+ if (unexpected !== undefined) {
36
+ return {
37
+ ok: false,
38
+ runningCount: 0,
39
+ tasks: [],
40
+ error: `TaskList received unexpected argument: ${unexpected}.`,
41
+ };
42
+ }
43
+
44
+ const tasks = options.taskManager.listBackgroundTasks();
45
+ throwIfTurnCancelled(context.signal);
46
+ return {
47
+ ok: true,
48
+ runningCount: tasks.filter(
49
+ (task) => task.status === "running" || task.status === "stopping",
50
+ ).length,
51
+ tasks,
52
+ };
53
+ },
54
+ });
55
+ }
56
+
57
+ function isRecord(value: unknown): value is Record<string, unknown> {
58
+ return typeof value === "object" && value !== null && !Array.isArray(value);
59
+ }
@@ -0,0 +1,47 @@
1
+ import { Buffer } from "node:buffer";
2
+ import type { TaskOutputSnapshot } from "./task-output";
3
+
4
+ const maxPreviewLines = 200;
5
+ const previewEdgeLines = 100;
6
+
7
+ export function buildOutputSnapshotFromText(bytes: Buffer): TaskOutputSnapshot {
8
+ const text = bytes.toString("utf8");
9
+ const lines = splitLines(text);
10
+
11
+ if (lines.length <= maxPreviewLines) {
12
+ return {
13
+ outputBytes: bytes.byteLength,
14
+ outputLines: lines.length,
15
+ preview: lines.join("\n"),
16
+ truncated: false,
17
+ };
18
+ }
19
+
20
+ const omittedLines = lines.length - maxPreviewLines;
21
+ const omittedStartLine = previewEdgeLines + 1;
22
+ const omittedEndLine = lines.length - previewEdgeLines;
23
+ return {
24
+ outputBytes: bytes.byteLength,
25
+ outputLines: lines.length,
26
+ preview: [
27
+ ...lines.slice(0, previewEdgeLines),
28
+ `... output omitted: lines ${omittedStartLine}-${omittedEndLine} (${omittedLines} ${omittedLines === 1 ? "line" : "lines"}). Full output is available at outputFilePath.`,
29
+ ...lines.slice(-previewEdgeLines),
30
+ ].join("\n"),
31
+ truncated: true,
32
+ omittedLines,
33
+ };
34
+ }
35
+
36
+ function splitLines(text: string): string[] {
37
+ if (text.length === 0) {
38
+ return [];
39
+ }
40
+
41
+ const lines = text.split(/\r\n|\n|\r/);
42
+ if (text.endsWith("\n") || text.endsWith("\r")) {
43
+ lines.pop();
44
+ }
45
+
46
+ return lines;
47
+ }
@@ -0,0 +1,62 @@
1
+ import type { ShellTaskManager } from "./bash-task";
2
+ import { throwIfTurnCancelled } from "../agent/turn-cancellation";
3
+ import { parseTaskIdArgs } from "./task-tool-args";
4
+ import { defineToolExecutor } from "./types";
5
+ import type { TaskOutputRawResult, ToolExecutionContext, ToolExecutor } from "./types";
6
+
7
+ export function createTaskOutputToolExecutor(options: {
8
+ taskManager: ShellTaskManager;
9
+ }): ToolExecutor {
10
+ return defineToolExecutor("task_output", {
11
+ definition: {
12
+ name: "TaskOutput",
13
+ description: "Get the current status and latest output of a shell task.",
14
+ parameters: {
15
+ type: "object",
16
+ additionalProperties: false,
17
+ properties: {
18
+ task_id: {
19
+ type: "string",
20
+ description: "The task ID returned by Bash or TaskList.",
21
+ },
22
+ },
23
+ required: ["task_id"],
24
+ },
25
+ },
26
+ async execute(
27
+ args,
28
+ _call,
29
+ context: ToolExecutionContext,
30
+ ): Promise<TaskOutputRawResult> {
31
+ throwIfTurnCancelled(context.signal);
32
+ const parsed = parseTaskIdArgs(args, "TaskOutput");
33
+ if (!parsed.ok) {
34
+ return { ok: false, taskId: "", error: parsed.error };
35
+ }
36
+
37
+ const inspection = options.taskManager.inspectTask(parsed.taskId);
38
+ throwIfTurnCancelled(context.signal);
39
+ if (inspection === undefined) {
40
+ return {
41
+ ok: false,
42
+ taskId: parsed.taskId,
43
+ error: `Unknown task ID: ${parsed.taskId}`,
44
+ };
45
+ }
46
+
47
+ return {
48
+ ok: true,
49
+ taskId: parsed.taskId,
50
+ task: inspection.task,
51
+ status: inspection.task.status,
52
+ command: inspection.task.command,
53
+ outputBytes: inspection.output.outputBytes,
54
+ outputLines: inspection.output.outputLines,
55
+ preview: inspection.output.preview,
56
+ truncated: inspection.output.truncated,
57
+ omittedLines: inspection.output.omittedLines,
58
+ outputFilePath: inspection.task.outputFilePath,
59
+ };
60
+ },
61
+ });
62
+ }