@zhijiewang/openharness 2.25.0 → 2.27.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/dist/Tool.d.ts +4 -0
- package/dist/query/tools.js +9 -6
- package/dist/renderer/json-tree.d.ts +11 -0
- package/dist/renderer/json-tree.js +194 -0
- package/dist/renderer/layout-sections.js +43 -32
- package/dist/renderer/layout.d.ts +2 -0
- package/dist/renderer/output-renderer.d.ts +27 -0
- package/dist/renderer/output-renderer.js +89 -0
- package/dist/renderer/tool-tree.d.ts +13 -0
- package/dist/renderer/tool-tree.js +49 -0
- package/dist/repl.js +9 -1
- package/dist/services/AgentDispatcher.d.ts +9 -1
- package/dist/services/AgentDispatcher.js +22 -1
- package/dist/tools/AgentTool/index.d.ts +13 -1
- package/dist/tools/AgentTool/index.js +28 -0
- package/dist/tools/FileReadTool/index.js +12 -3
- package/dist/tools/ParallelAgentTool/index.js +2 -1
- package/dist/tools/WebFetchTool/index.js +5 -2
- package/dist/types/events.d.ts +5 -0
- package/package.json +1 -1
package/dist/Tool.d.ts
CHANGED
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { z } from "zod";
|
|
6
6
|
import type { Provider } from "./providers/base.js";
|
|
7
|
+
import type { ToolCallComplete, ToolCallEnd, ToolCallStart, ToolOutputDelta } from "./types/events.js";
|
|
7
8
|
import type { PermissionMode, RiskLevel } from "./types/permissions.js";
|
|
8
9
|
export type ToolResult = {
|
|
9
10
|
output: string;
|
|
10
11
|
isError: boolean;
|
|
12
|
+
outputType?: "json" | "markdown" | "image" | "plain";
|
|
11
13
|
};
|
|
12
14
|
export type ToolContext = {
|
|
13
15
|
workingDir: string;
|
|
@@ -25,6 +27,8 @@ export type ToolContext = {
|
|
|
25
27
|
askUserQuestion?: (question: string, options?: string[]) => Promise<string>;
|
|
26
28
|
/** Auto-commit after file-modifying tools */
|
|
27
29
|
gitCommitPerTool?: boolean;
|
|
30
|
+
/** Forward an inner-query tool event to the outer event stream, stamped with the parent's callId. Used by AgentTool and AgentDispatcher to surface nested tool calls. */
|
|
31
|
+
emitChildEvent?: (event: ToolCallStart | ToolCallComplete | ToolCallEnd | ToolOutputDelta) => void;
|
|
28
32
|
};
|
|
29
33
|
export type Tool<Input extends z.ZodType = z.ZodType> = {
|
|
30
34
|
readonly name: string;
|
package/dist/query/tools.js
CHANGED
|
@@ -311,15 +311,18 @@ export async function executeSingleTool(toolCall, tools, context, permissionMode
|
|
|
311
311
|
}
|
|
312
312
|
export async function* executeToolCalls(toolCalls, tools, context, permissionMode, askUser, state, permissionPromptTool) {
|
|
313
313
|
const batches = partitionToolCalls(toolCalls, tools);
|
|
314
|
-
const
|
|
314
|
+
const childEvents = [];
|
|
315
315
|
const onOutputChunk = (callId, chunk) => {
|
|
316
|
-
|
|
316
|
+
childEvents.push({ type: "tool_output_delta", callId, chunk });
|
|
317
|
+
};
|
|
318
|
+
const emitChildEvent = (event) => {
|
|
319
|
+
childEvents.push(event);
|
|
317
320
|
};
|
|
318
321
|
const allToolNames = toolCalls.map((tc) => tc.toolName);
|
|
319
322
|
for (const batch of batches) {
|
|
320
323
|
if (batch.concurrent) {
|
|
321
|
-
const results = await Promise.all(batch.calls.map((tc) => executeSingleTool(tc, tools, { ...context, callId: tc.id, onOutputChunk }, permissionMode, askUser, permissionPromptTool)));
|
|
322
|
-
for (const chunk of
|
|
324
|
+
const results = await Promise.all(batch.calls.map((tc) => executeSingleTool(tc, tools, { ...context, callId: tc.id, onOutputChunk, emitChildEvent }, permissionMode, askUser, permissionPromptTool)));
|
|
325
|
+
for (const chunk of childEvents.splice(0))
|
|
323
326
|
yield chunk;
|
|
324
327
|
for (let i = 0; i < batch.calls.length; i++) {
|
|
325
328
|
const tc = batch.calls[i];
|
|
@@ -330,8 +333,8 @@ export async function* executeToolCalls(toolCalls, tools, context, permissionMod
|
|
|
330
333
|
}
|
|
331
334
|
else {
|
|
332
335
|
for (const tc of batch.calls) {
|
|
333
|
-
const result = await executeSingleTool(tc, tools, { ...context, callId: tc.id, onOutputChunk }, permissionMode, askUser, permissionPromptTool);
|
|
334
|
-
for (const chunk of
|
|
336
|
+
const result = await executeSingleTool(tc, tools, { ...context, callId: tc.id, onOutputChunk, emitChildEvent }, permissionMode, askUser, permissionPromptTool);
|
|
337
|
+
for (const chunk of childEvents.splice(0))
|
|
335
338
|
yield chunk;
|
|
336
339
|
yield { type: "tool_call_end", callId: tc.id, output: result.output, isError: result.isError };
|
|
337
340
|
state?.messages.push(createToolResultMessage({ callId: tc.id, output: result.output, isError: result.isError }));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static JSON tree renderer for tool output.
|
|
3
|
+
* Theme-colored, indented, depth-truncated, line-truncated.
|
|
4
|
+
*/
|
|
5
|
+
import type { CellGrid } from "./cells.js";
|
|
6
|
+
export declare function resetJsonStyleCache(): void;
|
|
7
|
+
export declare function renderJsonTree(grid: CellGrid, row: number, col: number, value: unknown, width: number, opts: {
|
|
8
|
+
maxLines: number;
|
|
9
|
+
limit: number;
|
|
10
|
+
}): number;
|
|
11
|
+
//# sourceMappingURL=json-tree.d.ts.map
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static JSON tree renderer for tool output.
|
|
3
|
+
* Theme-colored, indented, depth-truncated, line-truncated.
|
|
4
|
+
*/
|
|
5
|
+
import { getTheme } from "../utils/theme-data.js";
|
|
6
|
+
const s = (fg, bold = false, dim = false) => ({ fg, bg: null, bold, dim, underline: false });
|
|
7
|
+
const MAX_DEPTH = 3;
|
|
8
|
+
let S_KEY;
|
|
9
|
+
let S_STRING;
|
|
10
|
+
let S_NUMBER;
|
|
11
|
+
let S_PUNCT;
|
|
12
|
+
let S_TRUNC;
|
|
13
|
+
let _stylesInit = false;
|
|
14
|
+
export function resetJsonStyleCache() {
|
|
15
|
+
_stylesInit = false;
|
|
16
|
+
}
|
|
17
|
+
function ensureStyles() {
|
|
18
|
+
if (_stylesInit)
|
|
19
|
+
return;
|
|
20
|
+
_stylesInit = true;
|
|
21
|
+
const t = getTheme();
|
|
22
|
+
S_KEY = s(t.user);
|
|
23
|
+
S_STRING = s(t.success);
|
|
24
|
+
S_NUMBER = s(t.tool);
|
|
25
|
+
S_PUNCT = s(null, false, true);
|
|
26
|
+
S_TRUNC = s(null, false, true);
|
|
27
|
+
}
|
|
28
|
+
export function renderJsonTree(grid, row, col, value, width, opts) {
|
|
29
|
+
ensureStyles();
|
|
30
|
+
const lines = [];
|
|
31
|
+
const seen = new Set();
|
|
32
|
+
emitValue(lines, value, 0, 0, seen);
|
|
33
|
+
const maxRows = Math.min(opts.limit - row, opts.maxLines);
|
|
34
|
+
if (maxRows <= 0)
|
|
35
|
+
return 0;
|
|
36
|
+
const truncated = lines.length > maxRows;
|
|
37
|
+
const visible = truncated ? lines.slice(0, maxRows - 1) : lines;
|
|
38
|
+
let r = row;
|
|
39
|
+
for (const line of visible) {
|
|
40
|
+
if (r >= opts.limit)
|
|
41
|
+
break;
|
|
42
|
+
let c = col + line.indent;
|
|
43
|
+
for (const tok of line.tokens) {
|
|
44
|
+
for (let i = 0; i < tok.text.length; i++) {
|
|
45
|
+
if (c >= col + width)
|
|
46
|
+
break;
|
|
47
|
+
grid.setCell(r, c, tok.text[i], tok.style);
|
|
48
|
+
c++;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
r++;
|
|
52
|
+
}
|
|
53
|
+
if (truncated && r < opts.limit) {
|
|
54
|
+
const footer = `… (${lines.length} lines total)`;
|
|
55
|
+
grid.writeText(r, col, footer.slice(0, width), S_TRUNC);
|
|
56
|
+
r++;
|
|
57
|
+
}
|
|
58
|
+
return r - row;
|
|
59
|
+
}
|
|
60
|
+
function emitValue(out, value, indent, depth, seen) {
|
|
61
|
+
if (value === null) {
|
|
62
|
+
out.push({ indent, tokens: [{ text: "null", style: S_NUMBER }] });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (typeof value === "string") {
|
|
66
|
+
out.push({ indent, tokens: [{ text: JSON.stringify(value), style: S_STRING }] });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
70
|
+
out.push({ indent, tokens: [{ text: String(value), style: S_NUMBER }] });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (Array.isArray(value)) {
|
|
74
|
+
if (value.length === 0) {
|
|
75
|
+
out.push({ indent, tokens: [{ text: "[]", style: S_PUNCT }] });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (depth >= MAX_DEPTH) {
|
|
79
|
+
out.push({ indent, tokens: [{ text: `[${value.length} items]`, style: S_TRUNC }] });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (seen.has(value)) {
|
|
83
|
+
out.push({ indent, tokens: [{ text: "[Circular]", style: S_TRUNC }] });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
seen.add(value);
|
|
87
|
+
out.push({ indent, tokens: [{ text: "[", style: S_PUNCT }] });
|
|
88
|
+
for (let i = 0; i < value.length; i++) {
|
|
89
|
+
emitValueAsItem(out, value[i], indent + 2, depth + 1, seen, i < value.length - 1);
|
|
90
|
+
}
|
|
91
|
+
out.push({ indent, tokens: [{ text: "]", style: S_PUNCT }] });
|
|
92
|
+
seen.delete(value);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (typeof value === "object") {
|
|
96
|
+
const entries = Object.entries(value);
|
|
97
|
+
if (entries.length === 0) {
|
|
98
|
+
out.push({ indent, tokens: [{ text: "{}", style: S_PUNCT }] });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (depth >= MAX_DEPTH) {
|
|
102
|
+
out.push({ indent, tokens: [{ text: "{…}", style: S_TRUNC }] });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (seen.has(value)) {
|
|
106
|
+
out.push({ indent, tokens: [{ text: "[Circular]", style: S_TRUNC }] });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
seen.add(value);
|
|
110
|
+
out.push({ indent, tokens: [{ text: "{", style: S_PUNCT }] });
|
|
111
|
+
for (let i = 0; i < entries.length; i++) {
|
|
112
|
+
const [k, v] = entries[i];
|
|
113
|
+
emitObjectEntry(out, k, v, indent + 2, depth + 1, seen, i < entries.length - 1);
|
|
114
|
+
}
|
|
115
|
+
out.push({ indent, tokens: [{ text: "}", style: S_PUNCT }] });
|
|
116
|
+
seen.delete(value);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
out.push({ indent, tokens: [{ text: String(value), style: S_TRUNC }] });
|
|
120
|
+
}
|
|
121
|
+
function emitValueAsItem(out, value, indent, depth, seen, trailingComma) {
|
|
122
|
+
const before = out.length;
|
|
123
|
+
emitValue(out, value, indent, depth, seen);
|
|
124
|
+
if (trailingComma && out.length > before) {
|
|
125
|
+
const last = out[out.length - 1];
|
|
126
|
+
last.tokens.push({ text: ",", style: S_PUNCT });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function emitObjectEntry(out, key, value, indent, depth, seen, trailingComma) {
|
|
130
|
+
const keyTok = { text: JSON.stringify(key), style: S_KEY };
|
|
131
|
+
const colonTok = { text: ": ", style: S_PUNCT };
|
|
132
|
+
// Inline primitives onto the same line as the key.
|
|
133
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
134
|
+
const valStyle = typeof value === "string" ? S_STRING : S_NUMBER;
|
|
135
|
+
const valText = typeof value === "string" ? JSON.stringify(value) : String(value);
|
|
136
|
+
const tokens = [keyTok, colonTok, { text: valText, style: valStyle }];
|
|
137
|
+
if (trailingComma)
|
|
138
|
+
tokens.push({ text: ",", style: S_PUNCT });
|
|
139
|
+
out.push({ indent, tokens });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// Empty container inlines too: "key": {} / "key": []
|
|
143
|
+
if (Array.isArray(value) && value.length === 0) {
|
|
144
|
+
const tokens = [keyTok, colonTok, { text: "[]", style: S_PUNCT }];
|
|
145
|
+
if (trailingComma)
|
|
146
|
+
tokens.push({ text: ",", style: S_PUNCT });
|
|
147
|
+
out.push({ indent, tokens });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0) {
|
|
151
|
+
const tokens = [keyTok, colonTok, { text: "{}", style: S_PUNCT }];
|
|
152
|
+
if (trailingComma)
|
|
153
|
+
tokens.push({ text: ",", style: S_PUNCT });
|
|
154
|
+
out.push({ indent, tokens });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
// Depth-collapsed container also inlines.
|
|
158
|
+
if (depth >= MAX_DEPTH) {
|
|
159
|
+
const collapsed = Array.isArray(value) ? `[${value.length} items]` : "{…}";
|
|
160
|
+
out.push({ indent, tokens: [keyTok, colonTok, { text: collapsed, style: S_TRUNC }] });
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
// Circular reference: render inline, no descent.
|
|
164
|
+
if (seen.has(value)) {
|
|
165
|
+
const circ = [keyTok, colonTok, { text: "[Circular]", style: S_TRUNC }];
|
|
166
|
+
if (trailingComma)
|
|
167
|
+
circ.push({ text: ",", style: S_PUNCT });
|
|
168
|
+
out.push({ indent, tokens: circ });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
// Non-empty container: open bracket on key line, body indented, close bracket on its own line.
|
|
172
|
+
const opener = Array.isArray(value) ? "[" : "{";
|
|
173
|
+
const closer = Array.isArray(value) ? "]" : "}";
|
|
174
|
+
out.push({ indent, tokens: [keyTok, colonTok, { text: opener, style: S_PUNCT }] });
|
|
175
|
+
seen.add(value);
|
|
176
|
+
if (Array.isArray(value)) {
|
|
177
|
+
for (let i = 0; i < value.length; i++) {
|
|
178
|
+
emitValueAsItem(out, value[i], indent + 2, depth + 1, seen, i < value.length - 1);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
const entries = Object.entries(value);
|
|
183
|
+
for (let i = 0; i < entries.length; i++) {
|
|
184
|
+
const [ck, cv] = entries[i];
|
|
185
|
+
emitObjectEntry(out, ck, cv, indent + 2, depth + 1, seen, i < entries.length - 1);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
seen.delete(value);
|
|
189
|
+
const closerTokens = [{ text: closer, style: S_PUNCT }];
|
|
190
|
+
if (trailingComma)
|
|
191
|
+
closerTokens.push({ text: ",", style: S_PUNCT });
|
|
192
|
+
out.push({ indent, tokens: closerTokens });
|
|
193
|
+
}
|
|
194
|
+
//# sourceMappingURL=json-tree.js.map
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { getTheme } from "../utils/theme-data.js";
|
|
6
6
|
import { renderDiff } from "./diff.js";
|
|
7
|
-
import {
|
|
7
|
+
import { renderToolOutput } from "./output-renderer.js";
|
|
8
8
|
import { deriveSpinnerLabel } from "./spinner-label.js";
|
|
9
9
|
import { toolColor } from "./tool-color.js";
|
|
10
|
+
import { buildToolCallTree } from "./tool-tree.js";
|
|
10
11
|
// ── Style constants ──
|
|
11
12
|
const s = (fg, bold = false, dim = false) => ({ fg, bg: null, bold, dim, underline: false });
|
|
12
13
|
export const S_TEXT = s(null);
|
|
@@ -128,9 +129,10 @@ export function renderErrorSection(state, grid, r, limit) {
|
|
|
128
129
|
}
|
|
129
130
|
export function renderToolCallsSection(state, grid, r, limit, opts) {
|
|
130
131
|
const w = grid.width;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
const tree = buildToolCallTree(state.toolCalls);
|
|
133
|
+
const MAX_DEPTH = 3;
|
|
134
|
+
const renderSingleCall = (callId, tc, depth) => {
|
|
135
|
+
const colOffset = depth * 4;
|
|
134
136
|
const isAgent = tc.isAgent || tc.toolName === "Agent" || tc.toolName === "ParallelAgents";
|
|
135
137
|
const icon = isAgent
|
|
136
138
|
? tc.status === "running"
|
|
@@ -149,11 +151,11 @@ export function renderToolCallsSection(state, grid, r, limit, opts) {
|
|
|
149
151
|
const isExpanded = state.expandedToolCalls.has(callId);
|
|
150
152
|
const canExpand = tc.status !== "running" && tc.output;
|
|
151
153
|
if (canExpand) {
|
|
152
|
-
grid.writeText(r, 0, isExpanded ? "▼" : "▶", S_DIM);
|
|
154
|
+
grid.writeText(r, 0 + colOffset, isExpanded ? "▼" : "▶", S_DIM);
|
|
153
155
|
}
|
|
154
|
-
grid.writeText(r, 2, `${icon} `, statusStyle);
|
|
155
|
-
grid.writeText(r, 4, tc.toolName, nameStyle);
|
|
156
|
-
let afterName = 4 + tc.toolName.length + 1;
|
|
156
|
+
grid.writeText(r, 2 + colOffset, `${icon} `, statusStyle);
|
|
157
|
+
grid.writeText(r, 4 + colOffset, tc.toolName, nameStyle);
|
|
158
|
+
let afterName = 4 + colOffset + tc.toolName.length + 1;
|
|
157
159
|
if (tc.args) {
|
|
158
160
|
const maxArgs = w - afterName - 15;
|
|
159
161
|
if (maxArgs > 5) {
|
|
@@ -177,48 +179,57 @@ export function renderToolCallsSection(state, grid, r, limit, opts) {
|
|
|
177
179
|
}
|
|
178
180
|
r++;
|
|
179
181
|
if (isAgent && tc.agentDescription && r < limit) {
|
|
180
|
-
grid.writeText(r, 6, tc.agentDescription.slice(0, w - 8), S_DIM);
|
|
182
|
+
grid.writeText(r, 6 + colOffset, tc.agentDescription.slice(0, w - 8 - colOffset), S_DIM);
|
|
181
183
|
r++;
|
|
182
184
|
}
|
|
183
185
|
if (tc.status === "running" && tc.liveOutput && tc.liveOutput.length > 0) {
|
|
184
186
|
const overflow = tc.liveOutput.length > opts.maxLiveLines ? tc.liveOutput.length - opts.maxLiveLines : 0;
|
|
185
187
|
if (opts.showOverflow && overflow > 0 && r < limit) {
|
|
186
|
-
grid.writeText(r, 6, `… (${overflow} earlier lines)`, S_DIM);
|
|
188
|
+
grid.writeText(r, 6 + colOffset, `… (${overflow} earlier lines)`, S_DIM);
|
|
187
189
|
r++;
|
|
188
190
|
}
|
|
189
191
|
const visible = overflow > 0 ? tc.liveOutput.slice(-opts.maxLiveLines) : tc.liveOutput;
|
|
190
192
|
for (const line of visible) {
|
|
191
193
|
if (r >= limit)
|
|
192
194
|
break;
|
|
193
|
-
grid.writeTextWithLinks(r, 6, line.slice(0, w - 8), S_DIM, w - 2);
|
|
195
|
+
grid.writeTextWithLinks(r, 6 + colOffset, line.slice(0, w - 8 - colOffset), S_DIM, w - 2);
|
|
194
196
|
r++;
|
|
195
197
|
}
|
|
196
198
|
}
|
|
197
199
|
if (tc.output && tc.status !== "running" && isExpanded && r < limit) {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const outLines = tc.output.split("\n");
|
|
205
|
-
const maxOut = 20;
|
|
206
|
-
const showLines = outLines.slice(0, maxOut);
|
|
207
|
-
for (const line of showLines) {
|
|
208
|
-
if (r >= limit)
|
|
209
|
-
break;
|
|
210
|
-
const lineStyle = tc.status === "error" ? S_ERROR : S_DIM;
|
|
211
|
-
grid.writeTextWithLinks(r, 6, line.slice(0, w - 8), lineStyle, w - 2);
|
|
212
|
-
r++;
|
|
213
|
-
}
|
|
214
|
-
if (outLines.length > maxOut && r < limit) {
|
|
215
|
-
grid.writeText(r, 6, `… (${outLines.length} lines total)`, S_DIM);
|
|
216
|
-
r++;
|
|
217
|
-
}
|
|
200
|
+
const consumed = renderToolOutput(grid, r, 6 + colOffset, tc.output, tc.outputType, w - 8 - colOffset, {
|
|
201
|
+
status: tc.status,
|
|
202
|
+
maxLines: 20,
|
|
203
|
+
limit,
|
|
204
|
+
});
|
|
205
|
+
r += consumed;
|
|
218
206
|
}
|
|
219
|
-
}
|
|
207
|
+
};
|
|
208
|
+
const renderNode = (node) => {
|
|
209
|
+
if (r >= limit)
|
|
210
|
+
return;
|
|
211
|
+
if (node.depth > MAX_DEPTH) {
|
|
212
|
+
const descendants = countDescendants(node) + 1;
|
|
213
|
+
const colOffset = MAX_DEPTH * 4;
|
|
214
|
+
const noun = descendants === 1 ? "level" : "levels";
|
|
215
|
+
grid.writeText(r, colOffset, `… (${descendants} more ${noun})`, S_DIM);
|
|
216
|
+
r++;
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
renderSingleCall(node.callId, node.call, node.depth);
|
|
220
|
+
for (const child of node.children)
|
|
221
|
+
renderNode(child);
|
|
222
|
+
};
|
|
223
|
+
for (const root of tree)
|
|
224
|
+
renderNode(root);
|
|
220
225
|
return r;
|
|
221
226
|
}
|
|
227
|
+
function countDescendants(node) {
|
|
228
|
+
let n = node.children.length;
|
|
229
|
+
for (const c of node.children)
|
|
230
|
+
n += countDescendants(c);
|
|
231
|
+
return n;
|
|
232
|
+
}
|
|
222
233
|
export function renderContextWarningSection(state, grid, r, limit) {
|
|
223
234
|
if (!state.contextWarning || r >= limit)
|
|
224
235
|
return r;
|
|
@@ -11,7 +11,9 @@ export { resetStyleCache } from "./layout-sections.js";
|
|
|
11
11
|
export type ToolCallInfo = {
|
|
12
12
|
toolName: string;
|
|
13
13
|
status: "running" | "done" | "error";
|
|
14
|
+
parentCallId?: string;
|
|
14
15
|
output?: string;
|
|
16
|
+
outputType?: "json" | "markdown" | "image" | "plain";
|
|
15
17
|
args?: string;
|
|
16
18
|
isAgent?: boolean;
|
|
17
19
|
agentDescription?: string;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool output dispatcher.
|
|
3
|
+
*
|
|
4
|
+
* Detection chain (stops at first hit):
|
|
5
|
+
* 1. __IMAGE__: sentinel -> renderImageInline
|
|
6
|
+
* 2. outputType="json" -> renderJsonTree (fallback to plain on parse fail)
|
|
7
|
+
* 3. outputType="markdown" -> renderMarkdown
|
|
8
|
+
* 4. outputType="plain"|"image" -> renderPlain (image without sentinel is malformed)
|
|
9
|
+
* 5. heuristic JSON parse -> renderJsonTree
|
|
10
|
+
* 6. heuristic markdown -> renderMarkdown
|
|
11
|
+
* 7. fallback -> renderPlain
|
|
12
|
+
*/
|
|
13
|
+
import type { CellGrid } from "./cells.js";
|
|
14
|
+
export type OutputType = "json" | "markdown" | "image" | "plain";
|
|
15
|
+
export declare function renderToolOutput(grid: CellGrid, row: number, col: number, output: string, outputType: OutputType | undefined, width: number, opts: {
|
|
16
|
+
status: "running" | "done" | "error";
|
|
17
|
+
maxLines: number;
|
|
18
|
+
limit: number;
|
|
19
|
+
}): number;
|
|
20
|
+
export declare function tryParseJson(s: string): {
|
|
21
|
+
ok: true;
|
|
22
|
+
value: unknown;
|
|
23
|
+
} | {
|
|
24
|
+
ok: false;
|
|
25
|
+
};
|
|
26
|
+
export declare function looksLikeMarkdown(s: string): boolean;
|
|
27
|
+
//# sourceMappingURL=output-renderer.d.ts.map
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool output dispatcher.
|
|
3
|
+
*
|
|
4
|
+
* Detection chain (stops at first hit):
|
|
5
|
+
* 1. __IMAGE__: sentinel -> renderImageInline
|
|
6
|
+
* 2. outputType="json" -> renderJsonTree (fallback to plain on parse fail)
|
|
7
|
+
* 3. outputType="markdown" -> renderMarkdown
|
|
8
|
+
* 4. outputType="plain"|"image" -> renderPlain (image without sentinel is malformed)
|
|
9
|
+
* 5. heuristic JSON parse -> renderJsonTree
|
|
10
|
+
* 6. heuristic markdown -> renderMarkdown
|
|
11
|
+
* 7. fallback -> renderPlain
|
|
12
|
+
*/
|
|
13
|
+
import { isImageOutput, renderImageInline } from "./image.js";
|
|
14
|
+
import { renderJsonTree } from "./json-tree.js";
|
|
15
|
+
import { renderMarkdown } from "./markdown.js";
|
|
16
|
+
const S_DIM = { fg: null, bg: null, bold: false, dim: true, underline: false };
|
|
17
|
+
const S_ERROR = { fg: "red", bg: null, bold: false, dim: false, underline: false };
|
|
18
|
+
export function renderToolOutput(grid, row, col, output, outputType, width, opts) {
|
|
19
|
+
// 1. Image sentinel always wins.
|
|
20
|
+
if (isImageOutput(output)) {
|
|
21
|
+
if (row >= opts.limit)
|
|
22
|
+
return 0;
|
|
23
|
+
const label = renderImageInline(output);
|
|
24
|
+
grid.writeText(row, col, label.slice(0, width), S_DIM);
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
27
|
+
// 2-4. Typed dispatch.
|
|
28
|
+
if (outputType === "json") {
|
|
29
|
+
const parsed = tryParseJson(output);
|
|
30
|
+
if (parsed.ok)
|
|
31
|
+
return renderJsonTree(grid, row, col, parsed.value, width, { maxLines: opts.maxLines, limit: opts.limit });
|
|
32
|
+
return renderPlain(grid, row, col, output, width, opts);
|
|
33
|
+
}
|
|
34
|
+
if (outputType === "markdown") {
|
|
35
|
+
return renderMarkdown(grid, row, col, output, width, false, opts.limit);
|
|
36
|
+
}
|
|
37
|
+
if (outputType === "plain" || outputType === "image") {
|
|
38
|
+
return renderPlain(grid, row, col, output, width, opts);
|
|
39
|
+
}
|
|
40
|
+
// 5-7. Heuristic fallback (outputType undefined).
|
|
41
|
+
const json = tryParseJson(output);
|
|
42
|
+
if (json.ok)
|
|
43
|
+
return renderJsonTree(grid, row, col, json.value, width, { maxLines: opts.maxLines, limit: opts.limit });
|
|
44
|
+
if (looksLikeMarkdown(output))
|
|
45
|
+
return renderMarkdown(grid, row, col, output, width, false, opts.limit);
|
|
46
|
+
return renderPlain(grid, row, col, output, width, opts);
|
|
47
|
+
}
|
|
48
|
+
export function tryParseJson(s) {
|
|
49
|
+
const t = s.trimStart();
|
|
50
|
+
if (t[0] !== "{" && t[0] !== "[")
|
|
51
|
+
return { ok: false };
|
|
52
|
+
try {
|
|
53
|
+
return { ok: true, value: JSON.parse(t) };
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return { ok: false };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const FENCED_RE = /```[\w]*\r?\n/;
|
|
60
|
+
const TABLE_RE = /^\|.+\|\s*\n\|[\s:|-]+\|/m;
|
|
61
|
+
const HEADING_RE = /^#{1,6}\s+\S/gm;
|
|
62
|
+
export function looksLikeMarkdown(s) {
|
|
63
|
+
if (FENCED_RE.test(s))
|
|
64
|
+
return true;
|
|
65
|
+
if (TABLE_RE.test(s))
|
|
66
|
+
return true;
|
|
67
|
+
const headings = s.match(HEADING_RE);
|
|
68
|
+
if (headings && headings.length >= 2)
|
|
69
|
+
return true;
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
function renderPlain(grid, row, col, output, width, opts) {
|
|
73
|
+
const outLines = output.split("\n");
|
|
74
|
+
const showLines = outLines.slice(0, opts.maxLines);
|
|
75
|
+
const lineStyle = opts.status === "error" ? S_ERROR : S_DIM;
|
|
76
|
+
let r = row;
|
|
77
|
+
for (const line of showLines) {
|
|
78
|
+
if (r >= opts.limit)
|
|
79
|
+
break;
|
|
80
|
+
grid.writeTextWithLinks(r, col, line.slice(0, width), lineStyle, col + width);
|
|
81
|
+
r++;
|
|
82
|
+
}
|
|
83
|
+
if (outLines.length > opts.maxLines && r < opts.limit) {
|
|
84
|
+
grid.writeText(r, col, `… (${outLines.length} lines total)`.slice(0, width), S_DIM);
|
|
85
|
+
r++;
|
|
86
|
+
}
|
|
87
|
+
return r - row;
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=output-renderer.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree builder for tool calls — walks the flat callId map and produces a
|
|
3
|
+
* depth-first parent-child tree for rendering.
|
|
4
|
+
*/
|
|
5
|
+
import type { ToolCallInfo } from "./layout.js";
|
|
6
|
+
export type TreeNode = {
|
|
7
|
+
callId: string;
|
|
8
|
+
call: ToolCallInfo;
|
|
9
|
+
children: TreeNode[];
|
|
10
|
+
depth: number;
|
|
11
|
+
};
|
|
12
|
+
export declare function buildToolCallTree(toolCalls: Map<string, ToolCallInfo>): TreeNode[];
|
|
13
|
+
//# sourceMappingURL=tool-tree.d.ts.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree builder for tool calls — walks the flat callId map and produces a
|
|
3
|
+
* depth-first parent-child tree for rendering.
|
|
4
|
+
*/
|
|
5
|
+
export function buildToolCallTree(toolCalls) {
|
|
6
|
+
const childrenOf = new Map();
|
|
7
|
+
const allIds = new Set();
|
|
8
|
+
for (const [callId, info] of toolCalls) {
|
|
9
|
+
allIds.add(callId);
|
|
10
|
+
const parent = info.parentCallId;
|
|
11
|
+
if (parent === undefined)
|
|
12
|
+
continue;
|
|
13
|
+
const list = childrenOf.get(parent) ?? [];
|
|
14
|
+
list.push(callId);
|
|
15
|
+
childrenOf.set(parent, list);
|
|
16
|
+
}
|
|
17
|
+
const roots = [];
|
|
18
|
+
for (const [callId, info] of toolCalls) {
|
|
19
|
+
const parent = info.parentCallId;
|
|
20
|
+
if (parent === undefined || !allIds.has(parent)) {
|
|
21
|
+
roots.push(callId);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
const build = (callId, depth) => {
|
|
26
|
+
if (seen.has(callId))
|
|
27
|
+
return null;
|
|
28
|
+
seen.add(callId);
|
|
29
|
+
const info = toolCalls.get(callId);
|
|
30
|
+
if (!info)
|
|
31
|
+
return null;
|
|
32
|
+
const childIds = childrenOf.get(callId) ?? [];
|
|
33
|
+
const children = [];
|
|
34
|
+
for (const cid of childIds) {
|
|
35
|
+
const child = build(cid, depth + 1);
|
|
36
|
+
if (child)
|
|
37
|
+
children.push(child);
|
|
38
|
+
}
|
|
39
|
+
return { callId, call: info, children, depth };
|
|
40
|
+
};
|
|
41
|
+
const result = [];
|
|
42
|
+
for (const rootId of roots) {
|
|
43
|
+
const node = build(rootId, 0);
|
|
44
|
+
if (node)
|
|
45
|
+
result.push(node);
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=tool-tree.js.map
|
package/dist/repl.js
CHANGED
|
@@ -21,6 +21,7 @@ import { isTrusted, trustSystemActive } from "./harness/trust.js";
|
|
|
21
21
|
import { query } from "./query/index.js";
|
|
22
22
|
import { resetDiffStyleCache } from "./renderer/diff.js";
|
|
23
23
|
import { TerminalRenderer } from "./renderer/index.js";
|
|
24
|
+
import { resetJsonStyleCache } from "./renderer/json-tree.js";
|
|
24
25
|
import { resetStyleCache } from "./renderer/layout.js";
|
|
25
26
|
import { resetMdStyleCache } from "./renderer/markdown.js";
|
|
26
27
|
import { createAssistantMessage, createInfoMessage, createMessage } from "./types/message.js";
|
|
@@ -28,6 +29,8 @@ import { formatTokenCount } from "./utils/format.js";
|
|
|
28
29
|
import { fuzzyFilter } from "./utils/fuzzy.js";
|
|
29
30
|
import { setActiveTheme } from "./utils/theme-data.js";
|
|
30
31
|
import { formatToolArgs, summarizeToolOutput } from "./utils/tool-summary.js";
|
|
32
|
+
/** Per-call cap on rendered tool output in renderer state. Sized to fit typical JSON/markdown files (16 KiB) so JSON.parse / markdown detection works on real content; larger outputs render truncated. */
|
|
33
|
+
const TOOL_OUTPUT_RENDER_CAP = 16384;
|
|
31
34
|
export async function startREPL(config) {
|
|
32
35
|
if (config.theme)
|
|
33
36
|
setActiveTheme(config.theme);
|
|
@@ -814,6 +817,7 @@ export async function startREPL(config) {
|
|
|
814
817
|
resetStyleCache();
|
|
815
818
|
resetMdStyleCache();
|
|
816
819
|
resetDiffStyleCache();
|
|
820
|
+
resetJsonStyleCache();
|
|
817
821
|
// Persist theme to config
|
|
818
822
|
try {
|
|
819
823
|
const cfg = cachedConfig ?? {
|
|
@@ -936,6 +940,7 @@ export async function startREPL(config) {
|
|
|
936
940
|
status: "running",
|
|
937
941
|
startedAt: Date.now(),
|
|
938
942
|
isAgent: isAgentTool,
|
|
943
|
+
parentCallId: event.parentCallId,
|
|
939
944
|
});
|
|
940
945
|
break;
|
|
941
946
|
}
|
|
@@ -952,6 +957,7 @@ export async function startREPL(config) {
|
|
|
952
957
|
status: "running",
|
|
953
958
|
args: formatToolArgs(tcToolName, event.arguments),
|
|
954
959
|
agentDescription: agentDesc ?? existingTc?.agentDescription,
|
|
960
|
+
parentCallId: event.parentCallId ?? existingTc?.parentCallId,
|
|
955
961
|
});
|
|
956
962
|
break;
|
|
957
963
|
}
|
|
@@ -980,7 +986,9 @@ export async function startREPL(config) {
|
|
|
980
986
|
renderer.setToolCall(event.callId, {
|
|
981
987
|
toolName,
|
|
982
988
|
status: event.isError ? "error" : "done",
|
|
983
|
-
output: event.output?.slice(0,
|
|
989
|
+
output: event.output?.slice(0, TOOL_OUTPUT_RENDER_CAP),
|
|
990
|
+
outputType: event.outputType,
|
|
991
|
+
parentCallId: event.parentCallId ?? prevTc?.parentCallId,
|
|
984
992
|
args: prevTc?.args,
|
|
985
993
|
resultSummary: event.output ? summarizeToolOutput(event.output) : undefined,
|
|
986
994
|
startedAt: prevTc?.startedAt,
|
|
@@ -7,7 +7,13 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { Provider } from "../providers/base.js";
|
|
9
9
|
import type { Tools } from "../Tool.js";
|
|
10
|
+
import type { StreamEvent, ToolCallComplete, ToolCallEnd, ToolCallStart, ToolOutputDelta } from "../types/events.js";
|
|
10
11
|
import type { PermissionMode } from "../types/permissions.js";
|
|
12
|
+
/**
|
|
13
|
+
* Forward inner-loop tool events to the outer stream, stamping parentCallId.
|
|
14
|
+
* Exported for direct unit testing.
|
|
15
|
+
*/
|
|
16
|
+
export declare function forwardChildEvent(event: StreamEvent, parentCallId: string | undefined, emit: ((e: ToolCallStart | ToolCallComplete | ToolCallEnd | ToolOutputDelta) => void) | undefined): boolean;
|
|
11
17
|
export type AgentTask = {
|
|
12
18
|
id: string;
|
|
13
19
|
prompt: string;
|
|
@@ -29,10 +35,12 @@ export declare class AgentDispatcher {
|
|
|
29
35
|
private model?;
|
|
30
36
|
private workingDir?;
|
|
31
37
|
private abortSignal?;
|
|
38
|
+
private parentCallId?;
|
|
39
|
+
private emitChildEvent?;
|
|
32
40
|
private tasks;
|
|
33
41
|
private results;
|
|
34
42
|
private maxConcurrency;
|
|
35
|
-
constructor(provider: Provider, tools: Tools, systemPrompt: string, permissionMode: PermissionMode, model?: string | undefined, workingDir?: string | undefined, abortSignal?: AbortSignal | undefined, maxConcurrency?: number);
|
|
43
|
+
constructor(provider: Provider, tools: Tools, systemPrompt: string, permissionMode: PermissionMode, model?: string | undefined, workingDir?: string | undefined, abortSignal?: AbortSignal | undefined, maxConcurrency?: number, parentCallId?: string | undefined, emitChildEvent?: ((event: ToolCallStart | ToolCallComplete | ToolCallEnd | ToolOutputDelta) => void) | undefined);
|
|
36
44
|
addTask(task: AgentTask): void;
|
|
37
45
|
addTasks(tasks: AgentTask[]): void;
|
|
38
46
|
/** Execute all tasks respecting dependencies. Returns results in completion order. */
|
|
@@ -6,6 +6,22 @@
|
|
|
6
6
|
* and triggers dependent tasks when their blockers complete.
|
|
7
7
|
*/
|
|
8
8
|
import { createWorktree, isGitRepo, removeWorktree } from "../git/index.js";
|
|
9
|
+
/**
|
|
10
|
+
* Forward inner-loop tool events to the outer stream, stamping parentCallId.
|
|
11
|
+
* Exported for direct unit testing.
|
|
12
|
+
*/
|
|
13
|
+
export function forwardChildEvent(event, parentCallId, emit) {
|
|
14
|
+
if (!emit || !parentCallId)
|
|
15
|
+
return false;
|
|
16
|
+
if (event.type === "tool_call_start" ||
|
|
17
|
+
event.type === "tool_call_complete" ||
|
|
18
|
+
event.type === "tool_call_end" ||
|
|
19
|
+
event.type === "tool_output_delta") {
|
|
20
|
+
emit({ ...event, parentCallId });
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
9
25
|
export class AgentDispatcher {
|
|
10
26
|
provider;
|
|
11
27
|
tools;
|
|
@@ -14,10 +30,12 @@ export class AgentDispatcher {
|
|
|
14
30
|
model;
|
|
15
31
|
workingDir;
|
|
16
32
|
abortSignal;
|
|
33
|
+
parentCallId;
|
|
34
|
+
emitChildEvent;
|
|
17
35
|
tasks;
|
|
18
36
|
results = new Map();
|
|
19
37
|
maxConcurrency;
|
|
20
|
-
constructor(provider, tools, systemPrompt, permissionMode, model, workingDir, abortSignal, maxConcurrency = 4) {
|
|
38
|
+
constructor(provider, tools, systemPrompt, permissionMode, model, workingDir, abortSignal, maxConcurrency = 4, parentCallId, emitChildEvent) {
|
|
21
39
|
this.provider = provider;
|
|
22
40
|
this.tools = tools;
|
|
23
41
|
this.systemPrompt = systemPrompt;
|
|
@@ -25,6 +43,8 @@ export class AgentDispatcher {
|
|
|
25
43
|
this.model = model;
|
|
26
44
|
this.workingDir = workingDir;
|
|
27
45
|
this.abortSignal = abortSignal;
|
|
46
|
+
this.parentCallId = parentCallId;
|
|
47
|
+
this.emitChildEvent = emitChildEvent;
|
|
28
48
|
this.tasks = new Map();
|
|
29
49
|
this.maxConcurrency = maxConcurrency;
|
|
30
50
|
}
|
|
@@ -162,6 +182,7 @@ export class AgentDispatcher {
|
|
|
162
182
|
if (event.type === "error") {
|
|
163
183
|
return { id: task.id, output: `Error: ${event.message}`, isError: true, durationMs: Date.now() - start };
|
|
164
184
|
}
|
|
185
|
+
forwardChildEvent(event, this.parentCallId, this.emitChildEvent);
|
|
165
186
|
}
|
|
166
187
|
}
|
|
167
188
|
finally {
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import type { Tool } from "../../Tool.js";
|
|
2
|
+
import type { Tool, ToolContext } from "../../Tool.js";
|
|
3
|
+
import type { StreamEvent } from "../../types/events.js";
|
|
4
|
+
/**
|
|
5
|
+
* Forward a single inner-query event to the outer stream via `context.emitChildEvent`,
|
|
6
|
+
* stamping it with `parentCallId = context.callId`.
|
|
7
|
+
*
|
|
8
|
+
* Handles: tool_call_start, tool_call_complete, tool_call_end, tool_output_delta.
|
|
9
|
+
* Returns true when the event was forwarded, false otherwise.
|
|
10
|
+
*
|
|
11
|
+
* Exported for unit testing — the hot path in AgentTool.call() calls this inline
|
|
12
|
+
* to keep the forwarding logic isolated and testable without stubbing query().
|
|
13
|
+
*/
|
|
14
|
+
export declare function forwardInnerEvent(event: StreamEvent, context: ToolContext): boolean;
|
|
3
15
|
declare const inputSchema: z.ZodObject<{
|
|
4
16
|
prompt: z.ZodString;
|
|
5
17
|
description: z.ZodOptional<z.ZodString>;
|
|
@@ -2,6 +2,28 @@ import { z } from "zod";
|
|
|
2
2
|
import { createWorktree, hasWorktreeChanges, isGitRepo, removeWorktree } from "../../git/index.js";
|
|
3
3
|
import { emitHook } from "../../harness/hooks.js";
|
|
4
4
|
import { getMessageBus } from "../../services/agent-messaging.js";
|
|
5
|
+
/**
|
|
6
|
+
* Forward a single inner-query event to the outer stream via `context.emitChildEvent`,
|
|
7
|
+
* stamping it with `parentCallId = context.callId`.
|
|
8
|
+
*
|
|
9
|
+
* Handles: tool_call_start, tool_call_complete, tool_call_end, tool_output_delta.
|
|
10
|
+
* Returns true when the event was forwarded, false otherwise.
|
|
11
|
+
*
|
|
12
|
+
* Exported for unit testing — the hot path in AgentTool.call() calls this inline
|
|
13
|
+
* to keep the forwarding logic isolated and testable without stubbing query().
|
|
14
|
+
*/
|
|
15
|
+
export function forwardInnerEvent(event, context) {
|
|
16
|
+
if (!context.emitChildEvent || !context.callId)
|
|
17
|
+
return false;
|
|
18
|
+
if (event.type === "tool_call_start" ||
|
|
19
|
+
event.type === "tool_call_complete" ||
|
|
20
|
+
event.type === "tool_call_end" ||
|
|
21
|
+
event.type === "tool_output_delta") {
|
|
22
|
+
context.emitChildEvent({ ...event, parentCallId: event.parentCallId ?? context.callId });
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
5
27
|
const inputSchema = z.object({
|
|
6
28
|
prompt: z.string(),
|
|
7
29
|
description: z.string().optional(),
|
|
@@ -146,6 +168,12 @@ export const AgentTool = {
|
|
|
146
168
|
if (context.onOutputChunk && context.callId) {
|
|
147
169
|
context.onOutputChunk(context.callId, event.chunk);
|
|
148
170
|
}
|
|
171
|
+
forwardInnerEvent(event, context);
|
|
172
|
+
}
|
|
173
|
+
else if (event.type === "tool_call_start" ||
|
|
174
|
+
event.type === "tool_call_complete" ||
|
|
175
|
+
event.type === "tool_call_end") {
|
|
176
|
+
forwardInnerEvent(event, context);
|
|
149
177
|
}
|
|
150
178
|
else if (event.type === "error") {
|
|
151
179
|
return { output: `Sub-agent error: ${event.message}`, isError: true };
|
|
@@ -10,6 +10,15 @@ const inputSchema = z.object({
|
|
|
10
10
|
const DEFAULT_LIMIT = 2000;
|
|
11
11
|
const IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".svg"]);
|
|
12
12
|
const NOTEBOOK_EXTENSION = ".ipynb";
|
|
13
|
+
const MARKDOWN_EXTENSIONS = new Set([".md", ".markdown"]);
|
|
14
|
+
const JSON_EXTENSIONS = new Set([".json"]);
|
|
15
|
+
function outputTypeFromExt(ext) {
|
|
16
|
+
if (JSON_EXTENSIONS.has(ext))
|
|
17
|
+
return "json";
|
|
18
|
+
if (MARKDOWN_EXTENSIONS.has(ext))
|
|
19
|
+
return "markdown";
|
|
20
|
+
return "plain";
|
|
21
|
+
}
|
|
13
22
|
function parsePageRange(pages) {
|
|
14
23
|
const result = [];
|
|
15
24
|
for (const part of pages.split(",")) {
|
|
@@ -101,7 +110,7 @@ export const FileReadTool = {
|
|
|
101
110
|
}
|
|
102
111
|
}
|
|
103
112
|
if (pageTexts.length > 0) {
|
|
104
|
-
return { output: pageTexts.join("\n\n"), isError: false };
|
|
113
|
+
return { output: pageTexts.join("\n\n"), isError: false, outputType: "plain" };
|
|
105
114
|
}
|
|
106
115
|
// Fallback: return as base64 for multimodal analysis
|
|
107
116
|
const base64 = buffer.toString("base64");
|
|
@@ -128,7 +137,7 @@ export const FileReadTool = {
|
|
|
128
137
|
}
|
|
129
138
|
}
|
|
130
139
|
}
|
|
131
|
-
return { output: parts.join("\n\n"), isError: false };
|
|
140
|
+
return { output: parts.join("\n\n"), isError: false, outputType: "plain" };
|
|
132
141
|
}
|
|
133
142
|
// Default: text file
|
|
134
143
|
const content = await fs.readFile(filePath, "utf-8");
|
|
@@ -143,7 +152,7 @@ export const FileReadTool = {
|
|
|
143
152
|
if (shown < total) {
|
|
144
153
|
result += `\n\n(Showing lines ${offset + 1}-${offset + shown} of ${total})`;
|
|
145
154
|
}
|
|
146
|
-
return { output: result, isError: false };
|
|
155
|
+
return { output: result, isError: false, outputType: outputTypeFromExt(ext) };
|
|
147
156
|
}
|
|
148
157
|
catch (err) {
|
|
149
158
|
if (err.code === "ENOENT") {
|
|
@@ -25,7 +25,8 @@ export const ParallelAgentTool = {
|
|
|
25
25
|
return { output: "Parallel agents unavailable: provider not in context.", isError: true };
|
|
26
26
|
}
|
|
27
27
|
const systemPrompt = context.systemPrompt ?? "You are a sub-agent. Complete the delegated task concisely.";
|
|
28
|
-
const dispatcher = new AgentDispatcher(context.provider, context.tools, systemPrompt, context.permissionMode ?? "trust", context.model, context.workingDir, context.abortSignal
|
|
28
|
+
const dispatcher = new AgentDispatcher(context.provider, context.tools, systemPrompt, context.permissionMode ?? "trust", context.model, context.workingDir, context.abortSignal, 4, // maxConcurrency default
|
|
29
|
+
context.callId, context.emitChildEvent);
|
|
29
30
|
dispatcher.addTasks(input.tasks);
|
|
30
31
|
const results = await dispatcher.execute();
|
|
31
32
|
const output = results
|
|
@@ -70,7 +70,7 @@ export const WebFetchTool = {
|
|
|
70
70
|
signal: AbortSignal.timeout(30_000),
|
|
71
71
|
});
|
|
72
72
|
// Re-check host after redirect to prevent SSRF via open redirects
|
|
73
|
-
const finalUrl = new URL(response.url);
|
|
73
|
+
const finalUrl = new URL(response.url || input.url);
|
|
74
74
|
if (isBlockedHost(finalUrl.hostname)) {
|
|
75
75
|
return { output: "Error: Redirect to private/internal host blocked.", isError: true };
|
|
76
76
|
}
|
|
@@ -88,7 +88,10 @@ export const WebFetchTool = {
|
|
|
88
88
|
if (text.length > MAX_OUTPUT) {
|
|
89
89
|
text = `${text.slice(0, MAX_OUTPUT)}\n... [truncated]`;
|
|
90
90
|
}
|
|
91
|
-
|
|
91
|
+
const ct = contentType.toLowerCase();
|
|
92
|
+
const isJson = /^application\/(?:[a-z0-9.+-]+\+)?json\b/.test(ct);
|
|
93
|
+
const outputType = isJson ? "json" : ct.includes("text/markdown") ? "markdown" : "plain";
|
|
94
|
+
return { output: text, isError: false, outputType };
|
|
92
95
|
}
|
|
93
96
|
catch (err) {
|
|
94
97
|
return { output: `Error fetching URL: ${err.message}`, isError: true };
|
package/dist/types/events.d.ts
CHANGED
|
@@ -9,18 +9,22 @@ export type ToolCallStart = {
|
|
|
9
9
|
readonly type: "tool_call_start";
|
|
10
10
|
readonly toolName: string;
|
|
11
11
|
readonly callId: string;
|
|
12
|
+
readonly parentCallId?: string;
|
|
12
13
|
};
|
|
13
14
|
export type ToolCallComplete = {
|
|
14
15
|
readonly type: "tool_call_complete";
|
|
15
16
|
readonly callId: string;
|
|
16
17
|
readonly toolName: string;
|
|
17
18
|
readonly arguments: Record<string, unknown>;
|
|
19
|
+
readonly parentCallId?: string;
|
|
18
20
|
};
|
|
19
21
|
export type ToolCallEnd = {
|
|
20
22
|
readonly type: "tool_call_end";
|
|
21
23
|
readonly callId: string;
|
|
22
24
|
readonly output: string;
|
|
25
|
+
readonly outputType?: "json" | "markdown" | "image" | "plain";
|
|
23
26
|
readonly isError: boolean;
|
|
27
|
+
readonly parentCallId?: string;
|
|
24
28
|
};
|
|
25
29
|
export type PermissionRequest = {
|
|
26
30
|
readonly type: "permission_request";
|
|
@@ -58,6 +62,7 @@ export type ToolOutputDelta = {
|
|
|
58
62
|
readonly type: "tool_output_delta";
|
|
59
63
|
readonly callId: string;
|
|
60
64
|
readonly chunk: string;
|
|
65
|
+
readonly parentCallId?: string;
|
|
61
66
|
};
|
|
62
67
|
export type AskUserRequest = {
|
|
63
68
|
readonly type: "ask_user";
|