@oh-my-pi/pi-coding-agent 17.0.2 → 17.0.3
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 +70 -20
- package/dist/cli.js +4087 -4108
- package/dist/types/config/settings-schema.d.ts +7 -3
- package/dist/types/eval/agent-bridge.d.ts +7 -19
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +15 -1
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/lsp/types.d.ts +3 -0
- package/dist/types/mcp/transports/stdio.d.ts +29 -0
- package/dist/types/modes/components/agent-hub.d.ts +15 -0
- package/dist/types/registry/persisted-agents.d.ts +3 -0
- package/dist/types/sdk.d.ts +14 -3
- package/dist/types/session/session-entries.d.ts +6 -1
- package/dist/types/session/session-manager.d.ts +5 -0
- package/dist/types/task/executor.d.ts +26 -1
- package/dist/types/task/index.d.ts +1 -1
- package/dist/types/task/parallel.d.ts +14 -0
- package/dist/types/task/structured-subagent.d.ts +111 -0
- package/dist/types/task/types.d.ts +51 -0
- package/dist/types/tools/fetch.d.ts +4 -5
- package/dist/types/tools/hub/messaging.d.ts +5 -1
- package/dist/types/tools/index.d.ts +16 -1
- package/dist/types/tools/todo.d.ts +31 -0
- package/dist/types/tui/tree-list.d.ts +7 -0
- package/dist/types/utils/markit.d.ts +11 -0
- package/package.json +12 -12
- package/src/cli/file-processor.ts +1 -2
- package/src/config/settings-schema.ts +4 -3
- package/src/eval/__tests__/agent-bridge.test.ts +133 -47
- package/src/eval/__tests__/prelude-agent.test.ts +29 -0
- package/src/eval/agent-bridge.ts +104 -477
- package/src/eval/jl/prelude.jl +7 -6
- package/src/eval/js/shared/prelude.txt +5 -4
- package/src/eval/py/prelude.py +11 -39
- package/src/eval/rb/prelude.rb +5 -6
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +53 -7
- package/src/lsp/client.ts +57 -0
- package/src/lsp/index.ts +62 -6
- package/src/lsp/types.ts +3 -0
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/mcp/transports/stdio.test.ts +269 -1
- package/src/mcp/transports/stdio.ts +152 -1
- package/src/modes/components/agent-hub.ts +1 -72
- package/src/modes/components/bash-execution.ts +7 -3
- package/src/modes/components/eval-execution.ts +3 -1
- package/src/modes/interactive-mode.ts +30 -8
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/tools/eval.md +5 -2
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/task.md +12 -8
- package/src/prompts/tools/write.md +1 -1
- package/src/registry/persisted-agents.ts +74 -0
- package/src/sdk.ts +129 -87
- package/src/session/agent-session.ts +75 -21
- package/src/session/session-entries.ts +6 -1
- package/src/session/session-manager.ts +9 -0
- package/src/session/streaming-output.ts +41 -1
- package/src/system-prompt.ts +7 -2
- package/src/task/executor.ts +99 -21
- package/src/task/index.ts +258 -429
- package/src/task/parallel.ts +43 -0
- package/src/task/persisted-revive.ts +19 -7
- package/src/task/structured-subagent.ts +642 -0
- package/src/task/types.ts +58 -0
- package/src/tools/__tests__/eval-description.test.ts +1 -1
- package/src/tools/fetch.ts +28 -105
- package/src/tools/hub/messaging.ts +16 -3
- package/src/tools/index.ts +47 -14
- package/src/tools/path-utils.ts +1 -0
- package/src/tools/read.ts +14 -26
- package/src/tools/todo.ts +126 -13
- package/src/tui/tree-list.ts +39 -0
- package/src/utils/markit.ts +12 -0
- package/src/utils/zip.ts +16 -1
package/src/task/types.ts
CHANGED
|
@@ -7,6 +7,35 @@ import type { NestedRepoPatch } from "./worktree";
|
|
|
7
7
|
|
|
8
8
|
/** Source of an agent definition */
|
|
9
9
|
export type AgentSource = "bundled" | "user" | "project";
|
|
10
|
+
/**
|
|
11
|
+
* Enforcement policy for a structured subagent output schema.
|
|
12
|
+
*
|
|
13
|
+
* `permissive` preserves legacy retry-budget overrides; `strict` turns every
|
|
14
|
+
* invalid final payload, including an exhausted retry override, into a failed
|
|
15
|
+
* `schema_violation` result.
|
|
16
|
+
*/
|
|
17
|
+
export type StructuredSubagentSchemaMode = "permissive" | "strict";
|
|
18
|
+
|
|
19
|
+
/** Origin of the schema selected for a structured subagent invocation. */
|
|
20
|
+
export type StructuredSubagentSchemaSource = "caller" | "agent" | "session" | "none";
|
|
21
|
+
|
|
22
|
+
/** Final validation state of a structured subagent invocation. */
|
|
23
|
+
export type StructuredSubagentValidationStatus = "valid" | "invalid" | "unavailable";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parsed structured completion and its schema-validation metadata.
|
|
27
|
+
*
|
|
28
|
+
* `data` is present whenever a payload could be assembled or parsed, even when
|
|
29
|
+
* strict validation rejects it. `error` explains unavailable or invalid
|
|
30
|
+
* validation without requiring consumers to parse presentation text.
|
|
31
|
+
*/
|
|
32
|
+
export interface StructuredSubagentOutput {
|
|
33
|
+
source: StructuredSubagentSchemaSource;
|
|
34
|
+
mode: StructuredSubagentSchemaMode;
|
|
35
|
+
status: StructuredSubagentValidationStatus;
|
|
36
|
+
data?: unknown;
|
|
37
|
+
error?: string;
|
|
38
|
+
}
|
|
10
39
|
|
|
11
40
|
const parseNumber = (value: string | undefined, defaultValue: number): number => {
|
|
12
41
|
if (value) {
|
|
@@ -81,12 +110,16 @@ export const taskItemSchema = type({
|
|
|
81
110
|
"name?": "string",
|
|
82
111
|
agent: "string = 'task'",
|
|
83
112
|
task: "string",
|
|
113
|
+
"outputSchema?": "unknown",
|
|
114
|
+
"schemaMode?": '"permissive" | "strict"',
|
|
84
115
|
"+": "delete",
|
|
85
116
|
});
|
|
86
117
|
const taskItemSchemaIsolated = type({
|
|
87
118
|
"name?": "string",
|
|
88
119
|
agent: "string = 'task'",
|
|
89
120
|
task: "string",
|
|
121
|
+
"outputSchema?": "unknown",
|
|
122
|
+
"schemaMode?": '"permissive" | "strict"',
|
|
90
123
|
"isolated?": "boolean",
|
|
91
124
|
"+": "delete",
|
|
92
125
|
});
|
|
@@ -99,6 +132,10 @@ export interface TaskItem {
|
|
|
99
132
|
agent?: string;
|
|
100
133
|
/** The work; required by the schema. */
|
|
101
134
|
task?: string;
|
|
135
|
+
/** Caller-provided output schema; its presence overrides the selected agent's schema. */
|
|
136
|
+
outputSchema?: unknown;
|
|
137
|
+
/** Validation behavior for a caller-provided or inherited output schema. */
|
|
138
|
+
schemaMode?: "permissive" | "strict";
|
|
102
139
|
/** Run this spawn in an isolated worktree (batch form; flat form carries it top-level). */
|
|
103
140
|
isolated?: boolean;
|
|
104
141
|
}
|
|
@@ -107,6 +144,8 @@ export const taskSchema = type({
|
|
|
107
144
|
"name?": "string",
|
|
108
145
|
agent: "string = 'task'",
|
|
109
146
|
task: "string",
|
|
147
|
+
"outputSchema?": "unknown",
|
|
148
|
+
"schemaMode?": '"permissive" | "strict"',
|
|
110
149
|
"isolated?": "boolean",
|
|
111
150
|
"+": "delete",
|
|
112
151
|
});
|
|
@@ -114,6 +153,8 @@ const taskSchemaNoIsolation = type({
|
|
|
114
153
|
"name?": "string",
|
|
115
154
|
agent: "string = 'task'",
|
|
116
155
|
task: "string",
|
|
156
|
+
"outputSchema?": "unknown",
|
|
157
|
+
"schemaMode?": '"permissive" | "strict"',
|
|
117
158
|
"+": "delete",
|
|
118
159
|
});
|
|
119
160
|
const taskSchemaBatch = type({
|
|
@@ -156,6 +197,8 @@ function createTaskSchema(options: {
|
|
|
156
197
|
"name?": "string",
|
|
157
198
|
agent,
|
|
158
199
|
task: "string",
|
|
200
|
+
"outputSchema?": "unknown",
|
|
201
|
+
"schemaMode?": '"permissive" | "strict"',
|
|
159
202
|
"isolated?": "boolean",
|
|
160
203
|
"+": "delete",
|
|
161
204
|
});
|
|
@@ -169,6 +212,8 @@ function createTaskSchema(options: {
|
|
|
169
212
|
"name?": "string",
|
|
170
213
|
agent,
|
|
171
214
|
task: "string",
|
|
215
|
+
"outputSchema?": "unknown",
|
|
216
|
+
"schemaMode?": '"permissive" | "strict"',
|
|
172
217
|
"+": "delete",
|
|
173
218
|
});
|
|
174
219
|
return type.raw({
|
|
@@ -182,6 +227,8 @@ function createTaskSchema(options: {
|
|
|
182
227
|
"name?": "string",
|
|
183
228
|
agent,
|
|
184
229
|
task: "string",
|
|
230
|
+
"outputSchema?": "unknown",
|
|
231
|
+
"schemaMode?": '"permissive" | "strict"',
|
|
185
232
|
"isolated?": "boolean",
|
|
186
233
|
"+": "delete",
|
|
187
234
|
});
|
|
@@ -190,6 +237,8 @@ function createTaskSchema(options: {
|
|
|
190
237
|
"name?": "string",
|
|
191
238
|
agent,
|
|
192
239
|
task: "string",
|
|
240
|
+
"outputSchema?": "unknown",
|
|
241
|
+
"schemaMode?": '"permissive" | "strict"',
|
|
193
242
|
"+": "delete",
|
|
194
243
|
});
|
|
195
244
|
}
|
|
@@ -231,6 +280,10 @@ export interface TaskParams {
|
|
|
231
280
|
agent?: string;
|
|
232
281
|
/** The work (flat form). */
|
|
233
282
|
task?: string;
|
|
283
|
+
/** Caller-provided output schema; its presence overrides the selected agent's schema. */
|
|
284
|
+
outputSchema?: unknown;
|
|
285
|
+
/** Validation behavior for a caller-provided or inherited output schema. */
|
|
286
|
+
schemaMode?: "permissive" | "strict";
|
|
234
287
|
/** Batch form (`task.batch`): one subagent per item. */
|
|
235
288
|
tasks?: TaskItem[];
|
|
236
289
|
/** Batch form: shared background prepended to every assignment; required by the batch schema. */
|
|
@@ -413,6 +466,11 @@ export interface SingleResult {
|
|
|
413
466
|
output: string;
|
|
414
467
|
stderr: string;
|
|
415
468
|
truncated: boolean;
|
|
469
|
+
/**
|
|
470
|
+
* Parsed structured completion and validation metadata, when this invocation
|
|
471
|
+
* selected an output schema or strict schema mode.
|
|
472
|
+
*/
|
|
473
|
+
structuredOutput?: StructuredSubagentOutput;
|
|
416
474
|
durationMs: number;
|
|
417
475
|
/** Cumulative input + output + cacheWrite tokens across all turns. Excludes cacheRead (re-reads cached context every turn, making cumulative sum misleading). */
|
|
418
476
|
tokens: number;
|
|
@@ -6,7 +6,7 @@ describe("eval tool description", () => {
|
|
|
6
6
|
const description = getEvalToolDescription({ py: true, js: false, spawns: "fact-finder,oracle" });
|
|
7
7
|
|
|
8
8
|
expect(description).toContain('agent(prompt, agent?="fact-finder"');
|
|
9
|
-
expect(description).toContain("Allowed: `fact-finder`, `oracle`.");
|
|
9
|
+
expect(description).toContain("Allowed agents: `fact-finder`, `oracle`.");
|
|
10
10
|
});
|
|
11
11
|
|
|
12
12
|
it("omits agent() when spawning is disabled", () => {
|
package/src/tools/fetch.ts
CHANGED
|
@@ -7,7 +7,6 @@ import type { FetchImpl, ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
|
7
7
|
import { htmlToMarkdown } from "@oh-my-pi/pi-natives";
|
|
8
8
|
import { type Component, Text } from "@oh-my-pi/pi-tui";
|
|
9
9
|
import { $which, ptree, truncate } from "@oh-my-pi/pi-utils";
|
|
10
|
-
import { LRUCache } from "lru-cache/raw";
|
|
11
10
|
import type { Settings } from "../config/settings";
|
|
12
11
|
import { readEditableNotebookText } from "../edit/notebook";
|
|
13
12
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
@@ -19,6 +18,7 @@ import { renderStatusLine, urlHyperlink } from "../tui";
|
|
|
19
18
|
import { CachedOutputBlock, markFramedBlockComponent } from "../tui/output-block";
|
|
20
19
|
import { webpExclusionForModel } from "../utils/image-loading";
|
|
21
20
|
import { formatDimensionNote, resizeImage } from "../utils/image-resize";
|
|
21
|
+
import { CONVERTIBLE_EXTENSIONS } from "../utils/markit";
|
|
22
22
|
import { ensureTool } from "../utils/tools-manager";
|
|
23
23
|
import { type ArchiveFormat, listArchiveRoot, sniffArchiveFormat } from "../utils/zip";
|
|
24
24
|
import { extractWithParallel, findParallelApiKey, getParallelExtractContent } from "../web/parallel";
|
|
@@ -39,21 +39,17 @@ import { clampTimeout } from "./tool-timeouts";
|
|
|
39
39
|
// =============================================================================
|
|
40
40
|
|
|
41
41
|
const FETCH_DEFAULT_MAX_LINES = 300;
|
|
42
|
-
//
|
|
42
|
+
// MIME types markit can convert — one per registered converter (pdf, docx,
|
|
43
|
+
// pptx, xlsx, epub). Legacy `application/msword`, `application/vnd.ms-*`, and
|
|
44
|
+
// `application/rtf` are intentionally absent: markit has no converter for them.
|
|
43
45
|
const CONVERTIBLE_MIMES = new Set([
|
|
44
46
|
"application/pdf",
|
|
45
|
-
"application/msword",
|
|
46
|
-
"application/vnd.ms-powerpoint",
|
|
47
|
-
"application/vnd.ms-excel",
|
|
48
47
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
49
48
|
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
50
49
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
51
|
-
"application/rtf",
|
|
52
50
|
"application/epub+zip",
|
|
53
51
|
]);
|
|
54
52
|
|
|
55
|
-
const CONVERTIBLE_EXTENSIONS = new Set([".pdf", ".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx", ".rtf", ".epub"]);
|
|
56
|
-
|
|
57
53
|
const NOTEBOOK_MIMES = new Set(["application/x-ipynb+json"]);
|
|
58
54
|
const NOTEBOOK_EXTENSIONS = new Set([".ipynb"]);
|
|
59
55
|
|
|
@@ -1553,24 +1549,15 @@ export interface ReadUrlToolDetails {
|
|
|
1553
1549
|
meta?: OutputMeta;
|
|
1554
1550
|
}
|
|
1555
1551
|
|
|
1556
|
-
interface
|
|
1552
|
+
interface ReadUrlEntry {
|
|
1557
1553
|
artifactId?: string;
|
|
1558
1554
|
artifactPath?: string;
|
|
1559
|
-
contentPath?: string;
|
|
1560
1555
|
details: ReadUrlToolDetails;
|
|
1561
1556
|
image?: FetchImagePayload;
|
|
1562
1557
|
output: string;
|
|
1563
1558
|
content: string;
|
|
1564
1559
|
}
|
|
1565
1560
|
|
|
1566
|
-
const READ_URL_CACHE_MAX_ENTRIES = 100;
|
|
1567
|
-
const readUrlCache = new LRUCache<string, ReadUrlCacheEntry>({ max: READ_URL_CACHE_MAX_ENTRIES });
|
|
1568
|
-
|
|
1569
|
-
function getReadUrlCacheKey(session: ToolSession, requestedUrl: string, raw: boolean): string {
|
|
1570
|
-
const scope = session.getSessionFile() ?? session.cwd;
|
|
1571
|
-
return `${scope}::${raw ? "raw" : "rendered"}::${normalizeUrl(requestedUrl)}`;
|
|
1572
|
-
}
|
|
1573
|
-
|
|
1574
1561
|
async function findArtifactPath(session: ToolSession, artifactId: string): Promise<string | null> {
|
|
1575
1562
|
const artifactsDir = session.getArtifactsDir?.();
|
|
1576
1563
|
if (!artifactsDir) return null;
|
|
@@ -1584,25 +1571,6 @@ async function findArtifactPath(session: ToolSession, artifactId: string): Promi
|
|
|
1584
1571
|
}
|
|
1585
1572
|
}
|
|
1586
1573
|
|
|
1587
|
-
async function readArtifactOutput(session: ToolSession, artifactId: string): Promise<string | null> {
|
|
1588
|
-
const artifactPath = await findArtifactPath(session, artifactId);
|
|
1589
|
-
return artifactPath ? await Bun.file(artifactPath).text() : null;
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
|
-
async function materializeReadUrlCacheEntry(
|
|
1593
|
-
session: ToolSession,
|
|
1594
|
-
entry: ReadUrlCacheEntry,
|
|
1595
|
-
): Promise<ReadUrlCacheEntry | null> {
|
|
1596
|
-
if (entry.artifactId) {
|
|
1597
|
-
const artifactOutput = await readArtifactOutput(session, entry.artifactId);
|
|
1598
|
-
if (artifactOutput !== null) {
|
|
1599
|
-
return { ...entry, output: artifactOutput };
|
|
1600
|
-
}
|
|
1601
|
-
}
|
|
1602
|
-
|
|
1603
|
-
return entry.output.length > 0 ? entry : null;
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
1574
|
async function persistReadUrlArtifact(
|
|
1607
1575
|
session: ToolSession,
|
|
1608
1576
|
output: string,
|
|
@@ -1613,7 +1581,7 @@ async function persistReadUrlArtifact(
|
|
|
1613
1581
|
return artifact;
|
|
1614
1582
|
}
|
|
1615
1583
|
|
|
1616
|
-
async function
|
|
1584
|
+
async function ensureReadUrlArtifact(session: ToolSession, entry: ReadUrlEntry): Promise<ReadUrlEntry> {
|
|
1617
1585
|
if (entry.artifactId && entry.artifactPath) return entry;
|
|
1618
1586
|
if (entry.artifactId) {
|
|
1619
1587
|
const artifactPath = await findArtifactPath(session, entry.artifactId);
|
|
@@ -1632,19 +1600,7 @@ function readUrlContentExtension(finalUrl: string): string {
|
|
|
1632
1600
|
}
|
|
1633
1601
|
}
|
|
1634
1602
|
|
|
1635
|
-
async function
|
|
1636
|
-
session: ToolSession,
|
|
1637
|
-
entry: ReadUrlCacheEntry,
|
|
1638
|
-
raw: boolean,
|
|
1639
|
-
): Promise<ReadUrlCacheEntry> {
|
|
1640
|
-
if (entry.contentPath) {
|
|
1641
|
-
try {
|
|
1642
|
-
await Bun.file(entry.contentPath).stat();
|
|
1643
|
-
return entry;
|
|
1644
|
-
} catch {
|
|
1645
|
-
// Recreate below when the cached scratch file was removed.
|
|
1646
|
-
}
|
|
1647
|
-
}
|
|
1603
|
+
async function materializeReadUrlContent(session: ToolSession, entry: ReadUrlEntry, raw: boolean): Promise<string> {
|
|
1648
1604
|
const root = session.getArtifactsDir?.();
|
|
1649
1605
|
if (!root) {
|
|
1650
1606
|
throw new ToolError("Cannot search URL output because this session cannot materialize read artifacts.");
|
|
@@ -1654,20 +1610,16 @@ async function ensureReadUrlContentFile(
|
|
|
1654
1610
|
const hash = Bun.hash(`${raw ? "raw" : "rendered"}:${entry.details.finalUrl}`).toString(36);
|
|
1655
1611
|
const contentPath = path.join(dir, `${hash}${readUrlContentExtension(entry.details.finalUrl)}`);
|
|
1656
1612
|
await Bun.write(contentPath, entry.content);
|
|
1657
|
-
return
|
|
1658
|
-
}
|
|
1659
|
-
|
|
1660
|
-
function cacheReadUrlEntry(session: ToolSession, requestedUrl: string, raw: boolean, entry: ReadUrlCacheEntry): void {
|
|
1661
|
-
readUrlCache.set(getReadUrlCacheKey(session, requestedUrl, raw), entry);
|
|
1662
|
-
readUrlCache.set(getReadUrlCacheKey(session, entry.details.finalUrl, raw), entry);
|
|
1613
|
+
return contentPath;
|
|
1663
1614
|
}
|
|
1664
1615
|
|
|
1665
|
-
|
|
1616
|
+
/** Fetch and render a URL for a read or search operation. */
|
|
1617
|
+
export async function fetchReadUrl(
|
|
1666
1618
|
session: ToolSession,
|
|
1667
1619
|
params: { path: string; raw?: boolean },
|
|
1668
1620
|
signal?: AbortSignal,
|
|
1669
1621
|
options?: { ensureArtifact?: boolean },
|
|
1670
|
-
): Promise<
|
|
1622
|
+
): Promise<ReadUrlEntry> {
|
|
1671
1623
|
const { path: url, raw = false } = params;
|
|
1672
1624
|
|
|
1673
1625
|
const effectiveTimeout = clampTimeout("fetch", 30);
|
|
@@ -1708,30 +1660,6 @@ async function buildReadUrlCacheEntry(
|
|
|
1708
1660
|
};
|
|
1709
1661
|
}
|
|
1710
1662
|
|
|
1711
|
-
export async function loadReadUrlCacheEntry(
|
|
1712
|
-
session: ToolSession,
|
|
1713
|
-
params: { path: string; raw?: boolean },
|
|
1714
|
-
signal?: AbortSignal,
|
|
1715
|
-
options?: { ensureArtifact?: boolean; preferCached?: boolean },
|
|
1716
|
-
): Promise<ReadUrlCacheEntry> {
|
|
1717
|
-
const raw = params.raw ?? false;
|
|
1718
|
-
const cached = readUrlCache.get(getReadUrlCacheKey(session, params.path, raw));
|
|
1719
|
-
if (options?.preferCached && cached) {
|
|
1720
|
-
const prepared = options.ensureArtifact ? await ensureReadUrlCacheArtifact(session, cached) : cached;
|
|
1721
|
-
const materialized = await materializeReadUrlCacheEntry(session, prepared);
|
|
1722
|
-
if (materialized) {
|
|
1723
|
-
cacheReadUrlEntry(session, params.path, raw, materialized);
|
|
1724
|
-
return materialized;
|
|
1725
|
-
}
|
|
1726
|
-
}
|
|
1727
|
-
|
|
1728
|
-
const fresh = await buildReadUrlCacheEntry(session, params, signal, {
|
|
1729
|
-
ensureArtifact: options?.ensureArtifact,
|
|
1730
|
-
});
|
|
1731
|
-
cacheReadUrlEntry(session, params.path, raw, fresh);
|
|
1732
|
-
return fresh;
|
|
1733
|
-
}
|
|
1734
|
-
|
|
1735
1663
|
/** Materialize rendered URL body text to a local file for tools that require filesystem paths. */
|
|
1736
1664
|
export async function materializeReadUrlToFile(
|
|
1737
1665
|
session: ToolSession,
|
|
@@ -1741,13 +1669,9 @@ export async function materializeReadUrlToFile(
|
|
|
1741
1669
|
if (!session.settings.get("fetch.enabled")) {
|
|
1742
1670
|
throw new ToolError("URL reads are disabled by settings.");
|
|
1743
1671
|
}
|
|
1744
|
-
const
|
|
1745
|
-
const
|
|
1746
|
-
|
|
1747
|
-
if (!materialized.contentPath) {
|
|
1748
|
-
throw new ToolError("Cannot search URL output because this session cannot materialize read artifacts.");
|
|
1749
|
-
}
|
|
1750
|
-
return { path: materialized.contentPath, details: materialized.details };
|
|
1672
|
+
const entry = await fetchReadUrl(session, params, signal);
|
|
1673
|
+
const contentPath = await materializeReadUrlContent(session, entry, params.raw ?? false);
|
|
1674
|
+
return { path: contentPath, details: entry.details };
|
|
1751
1675
|
}
|
|
1752
1676
|
|
|
1753
1677
|
function buildUrlReadOutput(result: FetchRenderResult, content: string): string {
|
|
@@ -1768,36 +1692,35 @@ export async function executeReadUrl(
|
|
|
1768
1692
|
params: { path: string; raw?: boolean },
|
|
1769
1693
|
signal?: AbortSignal,
|
|
1770
1694
|
): Promise<AgentToolResult<ReadUrlToolDetails>> {
|
|
1771
|
-
let
|
|
1772
|
-
const truncation = truncateHead(
|
|
1695
|
+
let entry = await fetchReadUrl(session, params, signal);
|
|
1696
|
+
const truncation = truncateHead(entry.output, {
|
|
1773
1697
|
maxBytes: DEFAULT_MAX_BYTES,
|
|
1774
1698
|
maxLines: FETCH_DEFAULT_MAX_LINES,
|
|
1775
1699
|
});
|
|
1776
1700
|
const needsArtifact = truncation.truncated;
|
|
1777
|
-
if (needsArtifact && !
|
|
1778
|
-
|
|
1779
|
-
cacheReadUrlEntry(session, params.path, params.raw ?? false, cacheEntry);
|
|
1701
|
+
if (needsArtifact && !entry.artifactId) {
|
|
1702
|
+
entry = await ensureReadUrlArtifact(session, entry);
|
|
1780
1703
|
}
|
|
1781
|
-
const output = needsArtifact ? truncation.content :
|
|
1704
|
+
const output = needsArtifact ? truncation.content : entry.output;
|
|
1782
1705
|
const details: ReadUrlToolDetails = {
|
|
1783
|
-
...
|
|
1784
|
-
truncated: Boolean(
|
|
1706
|
+
...entry.details,
|
|
1707
|
+
truncated: Boolean(entry.details.truncated || needsArtifact),
|
|
1785
1708
|
};
|
|
1786
1709
|
|
|
1787
1710
|
const contentBlocks: Array<TextContent | ImageContent> = [{ type: "text", text: output }];
|
|
1788
|
-
if (
|
|
1789
|
-
contentBlocks.push({ type: "image", data:
|
|
1711
|
+
if (entry.image) {
|
|
1712
|
+
contentBlocks.push({ type: "image", data: entry.image.data, mimeType: entry.image.mimeType });
|
|
1790
1713
|
}
|
|
1791
1714
|
|
|
1792
1715
|
const resultBuilder = toolResult(details).content(contentBlocks).sourceUrl(details.finalUrl);
|
|
1793
1716
|
if (needsArtifact) {
|
|
1794
|
-
resultBuilder.truncation(truncation, { direction: "head", artifactId:
|
|
1795
|
-
} else if (
|
|
1796
|
-
const outputLines =
|
|
1797
|
-
const outputBytes = Buffer.byteLength(
|
|
1717
|
+
resultBuilder.truncation(truncation, { direction: "head", artifactId: entry.artifactId });
|
|
1718
|
+
} else if (entry.details.truncated) {
|
|
1719
|
+
const outputLines = entry.output.split("\n").length;
|
|
1720
|
+
const outputBytes = Buffer.byteLength(entry.output, "utf-8");
|
|
1798
1721
|
const totalBytes = Math.max(outputBytes + 1, MAX_OUTPUT_CHARS + 1);
|
|
1799
1722
|
const totalLines = outputLines + 1;
|
|
1800
|
-
resultBuilder.truncationFromText(
|
|
1723
|
+
resultBuilder.truncationFromText(entry.output, {
|
|
1801
1724
|
direction: "tail",
|
|
1802
1725
|
totalLines,
|
|
1803
1726
|
totalBytes,
|
|
@@ -17,6 +17,7 @@ import type { RenderResultOptions } from "../../extensibility/custom-tools/types
|
|
|
17
17
|
import { IrcBus, type IrcDeliveryReceipt, type IrcMessage } from "../../irc/bus";
|
|
18
18
|
import type { Theme } from "../../modes/theme/theme";
|
|
19
19
|
import { type AgentRegistry, MAIN_AGENT_ID } from "../../registry/agent-registry";
|
|
20
|
+
import { registerPersistedSubagents } from "../../registry/persisted-agents";
|
|
20
21
|
import { canSpawnAtDepth } from "../../task/types";
|
|
21
22
|
import { Ellipsis, renderStatusLine, renderTreeList, truncateToWidth } from "../../tui";
|
|
22
23
|
import {
|
|
@@ -81,10 +82,22 @@ export function messageResult(senderId: string, waited: IrcMessage): AgentToolRe
|
|
|
81
82
|
};
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
|
|
85
|
+
/**
|
|
86
|
+
* List every addressable peer, restoring parked refs from disk when a resumed
|
|
87
|
+
* session has no in-memory roster.
|
|
88
|
+
*/
|
|
89
|
+
export async function executeList(
|
|
90
|
+
registry: AgentRegistry,
|
|
91
|
+
senderId: string,
|
|
92
|
+
): Promise<AgentToolResult<CoordinationDetails>> {
|
|
93
|
+
let refs = registry.list();
|
|
94
|
+
if (!refs.some(ref => ref.id !== senderId && ref.status !== "aborted" && ref.kind !== "advisor")) {
|
|
95
|
+
await registerPersistedSubagents(registry, registry.get(senderId)?.sessionFile);
|
|
96
|
+
refs = registry.list();
|
|
97
|
+
}
|
|
98
|
+
|
|
85
99
|
const bus = IrcBus.global();
|
|
86
|
-
const peers =
|
|
87
|
-
.list()
|
|
100
|
+
const peers = refs
|
|
88
101
|
.filter(ref => ref.id !== senderId && ref.status !== "aborted" && ref.kind !== "advisor")
|
|
89
102
|
.map(ref => ({
|
|
90
103
|
id: ref.id,
|
package/src/tools/index.ts
CHANGED
|
@@ -28,7 +28,7 @@ import type { UsageStatistics } from "../session/session-entries";
|
|
|
28
28
|
import type { ToolChoiceQueue } from "../session/tool-choice-queue";
|
|
29
29
|
import { TaskTool } from "../task";
|
|
30
30
|
import type { AgentOutputManager } from "../task/output-manager";
|
|
31
|
-
import { canSpawnAtDepth } from "../task/types";
|
|
31
|
+
import { canSpawnAtDepth, type StructuredSubagentSchemaMode } from "../task/types";
|
|
32
32
|
import type { EventBus } from "../utils/event-bus";
|
|
33
33
|
import { WebSearchTool } from "../web/search";
|
|
34
34
|
import type { WorkspaceTree } from "../workspace-tree";
|
|
@@ -45,7 +45,7 @@ import { resolveEvalBackends } from "./eval-backends";
|
|
|
45
45
|
import { GithubTool } from "./gh";
|
|
46
46
|
import { GlobTool } from "./glob";
|
|
47
47
|
import { GrepTool } from "./grep";
|
|
48
|
-
import { HubTool } from "./hub";
|
|
48
|
+
import { HubTool, isIrcEnabled } from "./hub";
|
|
49
49
|
import { InspectImageTool } from "./inspect-image";
|
|
50
50
|
import { LearnTool } from "./learn";
|
|
51
51
|
import { ManageSkillTool } from "./manage-skill";
|
|
@@ -183,17 +183,31 @@ export interface ToolSession {
|
|
|
183
183
|
customToolPaths?: ToolPathWithSource[];
|
|
184
184
|
/** Whether LSP integrations are enabled */
|
|
185
185
|
enableLsp?: boolean;
|
|
186
|
+
/** Whether this invocation may expose IRC. `false` removes it even for subagents. */
|
|
187
|
+
enableIrc?: boolean;
|
|
188
|
+
/**
|
|
189
|
+
* Whether MCP capabilities may be forwarded to child sessions. `false`
|
|
190
|
+
* prohibits inherited-manager and process-global MCP fallback.
|
|
191
|
+
*/
|
|
192
|
+
enableMCP?: boolean;
|
|
186
193
|
/** Whether an edit-capable tool is available in this session (controls hashline output) */
|
|
187
194
|
hasEditTool?: boolean;
|
|
188
195
|
/** Event bus for tool/extension communication */
|
|
189
196
|
eventBus?: EventBus;
|
|
190
|
-
/** Output schema for structured completion (subagents) */
|
|
197
|
+
/** Output schema for structured completion (subagents). */
|
|
191
198
|
outputSchema?: unknown;
|
|
199
|
+
/** Enforcement policy for {@link outputSchema}; defaults to legacy permissive behavior. */
|
|
200
|
+
outputSchemaMode?: StructuredSubagentSchemaMode;
|
|
192
201
|
/** Whether to include the yield tool by default */
|
|
193
202
|
requireYieldTool?: boolean;
|
|
194
203
|
/** Session starts with a prewalk hand-off armed. Keeps `todo` in yield-gated
|
|
195
204
|
* (subagent) registries: the prewalk plan nudge + todo gate need it. */
|
|
196
205
|
prewalkArmed?: boolean;
|
|
206
|
+
/**
|
|
207
|
+
* Constrain the active set to the caller's explicit built-in names (plus a
|
|
208
|
+
* required yield tool). Suppresses automatic tool-set expansion.
|
|
209
|
+
*/
|
|
210
|
+
restrictToolNames?: boolean;
|
|
197
211
|
/** Task recursion depth (0 = top-level, 1 = first child, etc.) */
|
|
198
212
|
taskDepth?: number;
|
|
199
213
|
/** Get shared eval executor session ID. Subagents inherit this to share JS/Python/Ruby/Julia state. */
|
|
@@ -403,13 +417,18 @@ export type ToolName = BuiltinToolName;
|
|
|
403
417
|
* Create tools from BUILTIN_TOOLS registry.
|
|
404
418
|
*/
|
|
405
419
|
export async function createTools(session: ToolSession, toolNames?: string[]): Promise<Tool[]> {
|
|
420
|
+
const restrictToolNames = session.restrictToolNames === true;
|
|
406
421
|
const includeYield = session.requireYieldTool === true;
|
|
407
422
|
const enableLsp = session.enableLsp ?? true;
|
|
408
|
-
|
|
423
|
+
const requestedTools = restrictToolNames
|
|
424
|
+
? normalizeToolNames(toolNames ?? [])
|
|
425
|
+
: toolNames && toolNames.length > 0
|
|
426
|
+
? normalizeToolNames(toolNames)
|
|
427
|
+
: undefined;
|
|
409
428
|
const goalEnabled = session.settings.get("goal.enabled");
|
|
410
|
-
const goalModeActive = goalEnabled && session.getGoalModeState?.()?.enabled === true;
|
|
429
|
+
const goalModeActive = !restrictToolNames && goalEnabled && session.getGoalModeState?.()?.enabled === true;
|
|
411
430
|
if (goalModeActive && requestedTools && !requestedTools.includes("goal")) {
|
|
412
|
-
requestedTools
|
|
431
|
+
requestedTools.push("goal");
|
|
413
432
|
}
|
|
414
433
|
const backends = resolveEvalBackends(session);
|
|
415
434
|
const allowPython = backends.python;
|
|
@@ -466,8 +485,12 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
466
485
|
// unreachable, in which case eval dispatches exclusively to the others.
|
|
467
486
|
const allowEval = effectivePythonAllowed || allowJs || effectiveRubyAllowed || effectiveJuliaAllowed;
|
|
468
487
|
|
|
469
|
-
// Auto-include AST counterparts when their text-based sibling is present
|
|
470
|
-
|
|
488
|
+
// Auto-include AST counterparts when their text-based sibling is present.
|
|
489
|
+
// Restricted callers own the active list and must not have it widened.
|
|
490
|
+
if (requestedTools && !restrictToolNames) {
|
|
491
|
+
if (goalModeActive && !requestedTools.includes("goal")) {
|
|
492
|
+
requestedTools.push("goal");
|
|
493
|
+
}
|
|
471
494
|
if (
|
|
472
495
|
requestedTools.includes("grep") &&
|
|
473
496
|
!requestedTools.includes("ast_grep") &&
|
|
@@ -522,6 +545,11 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
522
545
|
if (name === "ask") return session.settings.get("ask.enabled");
|
|
523
546
|
if (name === "browser") return session.settings.get("browser.enabled");
|
|
524
547
|
if (name === "checkpoint" || name === "rewind") return session.settings.get("checkpoint.enabled");
|
|
548
|
+
if (name === "hub") {
|
|
549
|
+
return (
|
|
550
|
+
!restrictToolNames && session.enableIrc !== false && isIrcEnabled(session.settings, session.taskDepth ?? 0)
|
|
551
|
+
);
|
|
552
|
+
}
|
|
525
553
|
if (name === "retain" || name === "recall" || name === "reflect") {
|
|
526
554
|
return ["hindsight", "mnemopi"].includes(session.settings.get("memory.backend") ?? "");
|
|
527
555
|
}
|
|
@@ -569,10 +597,11 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
569
597
|
);
|
|
570
598
|
let tools = baseResults.filter((r): r is Tool => r !== null);
|
|
571
599
|
|
|
572
|
-
//
|
|
573
|
-
//
|
|
574
|
-
//
|
|
575
|
-
|
|
600
|
+
// Ordinary sessions use xd:// for discoverable built-ins, custom tools, and
|
|
601
|
+
// MCP tools. Structured children must expose only their host-provided names,
|
|
602
|
+
// so never allocate a registry that later SDK assembly could populate.
|
|
603
|
+
// Explicitly requested built-ins retain their top-level presentation.
|
|
604
|
+
const xdevEnabled = !restrictToolNames && session.settings.get("tools.xdev");
|
|
576
605
|
const mountBuiltinTools = requestedTools === undefined;
|
|
577
606
|
if (xdevEnabled) {
|
|
578
607
|
const mounted: Tool[] = [];
|
|
@@ -595,13 +624,17 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
595
624
|
// (e.g. ast_edit) also resolve through a `write` to xd://resolve/reject. Retain
|
|
596
625
|
// both whenever any device is mounted or a deferrable tool can stage one.
|
|
597
626
|
const xdevMounted = (session.xdevRegistry?.size ?? 0) > 0;
|
|
598
|
-
if (
|
|
627
|
+
if (
|
|
628
|
+
!restrictToolNames &&
|
|
629
|
+
(tools.some(tool => tool.deferrable === true) || xdevMounted) &&
|
|
630
|
+
!tools.some(tool => tool.name === "write")
|
|
631
|
+
) {
|
|
599
632
|
const writeTool = await logger.time("createTools:write", BUILTIN_TOOLS.write, session);
|
|
600
633
|
if (writeTool) {
|
|
601
634
|
tools.push(wrapToolWithMetaNotice(writeTool));
|
|
602
635
|
}
|
|
603
636
|
}
|
|
604
|
-
if (xdevMounted && !tools.some(tool => tool.name === "read")) {
|
|
637
|
+
if (!restrictToolNames && xdevMounted && !tools.some(tool => tool.name === "read")) {
|
|
605
638
|
const readTool = await logger.time("createTools:read", BUILTIN_TOOLS.read, session);
|
|
606
639
|
if (readTool) {
|
|
607
640
|
tools.push(wrapToolWithMetaNotice(readTool));
|
package/src/tools/path-utils.ts
CHANGED
package/src/tools/read.ts
CHANGED
|
@@ -62,7 +62,7 @@ import {
|
|
|
62
62
|
MAX_IMAGE_INPUT_BYTES,
|
|
63
63
|
webpExclusionForModel,
|
|
64
64
|
} from "../utils/image-loading";
|
|
65
|
-
import { convertFileWithMarkit } from "../utils/markit";
|
|
65
|
+
import { CONVERTIBLE_EXTENSIONS, convertFileWithMarkit } from "../utils/markit";
|
|
66
66
|
import { type ArchiveReader, formatArchiveEntryLines, openArchive, parseArchivePathCandidates } from "../utils/zip";
|
|
67
67
|
import { buildDirectoryTree, type DirectoryTree } from "../workspace-tree";
|
|
68
68
|
import {
|
|
@@ -78,7 +78,7 @@ import {
|
|
|
78
78
|
} from "./conflict-detect";
|
|
79
79
|
import {
|
|
80
80
|
executeReadUrl,
|
|
81
|
-
|
|
81
|
+
fetchReadUrl,
|
|
82
82
|
parseReadUrlTarget,
|
|
83
83
|
type ReadUrlToolDetails,
|
|
84
84
|
renderReadUrlCall,
|
|
@@ -151,9 +151,6 @@ function getSummaryParseCache(session: object): LRUCache<string, SummaryResult |
|
|
|
151
151
|
return cache;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
// Document types converted to markdown via markit.
|
|
155
|
-
const CONVERTIBLE_EXTENSIONS = new Set([".pdf", ".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx", ".rtf", ".epub"]);
|
|
156
|
-
|
|
157
154
|
const MAX_SUMMARY_BYTES = 2 * 1024 * 1024;
|
|
158
155
|
const MAX_SUMMARY_LINES = 20_000;
|
|
159
156
|
const MAX_ARTIFACT_RAW_INLINE_BYTES = DEFAULT_MAX_BYTES;
|
|
@@ -2167,15 +2164,12 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2167
2164
|
const urlRaw = parsedUrlTarget.raw;
|
|
2168
2165
|
const urlRanges = parsedUrlTarget.ranges;
|
|
2169
2166
|
if (urlRanges !== undefined && urlRanges.length > 1) {
|
|
2170
|
-
const
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
return this.#buildInMemoryMultiRangeResult(cached.output, urlRanges, {
|
|
2177
|
-
details: { ...cached.details },
|
|
2178
|
-
sourceUrl: cached.details.finalUrl,
|
|
2167
|
+
const entry = await fetchReadUrl(this.session, { path: parsedUrlTarget.path, raw: urlRaw }, signal, {
|
|
2168
|
+
ensureArtifact: true,
|
|
2169
|
+
});
|
|
2170
|
+
return this.#buildInMemoryMultiRangeResult(entry.output, urlRanges, {
|
|
2171
|
+
details: { ...entry.details },
|
|
2172
|
+
sourceUrl: entry.details.finalUrl,
|
|
2179
2173
|
entityLabel: "URL output",
|
|
2180
2174
|
raw: urlRaw,
|
|
2181
2175
|
immutable: true,
|
|
@@ -2184,18 +2178,12 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2184
2178
|
const urlOffset = parsedUrlTarget.offset;
|
|
2185
2179
|
const urlLimit = parsedUrlTarget.limit;
|
|
2186
2180
|
if (urlOffset !== undefined || urlLimit !== undefined) {
|
|
2187
|
-
const
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
{
|
|
2192
|
-
|
|
2193
|
-
preferCached: true,
|
|
2194
|
-
},
|
|
2195
|
-
);
|
|
2196
|
-
return this.#buildInMemoryTextResult(cached.output, urlOffset, urlLimit, {
|
|
2197
|
-
details: { ...cached.details },
|
|
2198
|
-
sourceUrl: cached.details.finalUrl,
|
|
2181
|
+
const entry = await fetchReadUrl(this.session, { path: parsedUrlTarget.path, raw: urlRaw }, signal, {
|
|
2182
|
+
ensureArtifact: true,
|
|
2183
|
+
});
|
|
2184
|
+
return this.#buildInMemoryTextResult(entry.output, urlOffset, urlLimit, {
|
|
2185
|
+
details: { ...entry.details },
|
|
2186
|
+
sourceUrl: entry.details.finalUrl,
|
|
2199
2187
|
entityLabel: "URL output",
|
|
2200
2188
|
raw: urlRaw,
|
|
2201
2189
|
immutable: true,
|