@shanepadgett/tau-agent 0.33.1 → 0.35.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/docs/context.md +29 -20
- package/docs/extending-tau-agent.md +63 -24
- package/extensions/appshot/index.ts +112 -111
- package/extensions/aside/README.md +7 -0
- package/extensions/aside/index.ts +168 -0
- package/extensions/aside/panel.ts +83 -0
- package/extensions/bash-approval/README.md +20 -0
- package/extensions/bash-approval/index.ts +235 -0
- package/extensions/bash-approval/settings.ts +24 -0
- package/extensions/branch/index.ts +38 -21
- package/extensions/cache-diagnostics/index.ts +70 -58
- package/extensions/checkpoint/README.md +9 -0
- package/extensions/checkpoint/checkpoint-budget.ts +66 -0
- package/extensions/checkpoint/checkpoint.ts +279 -0
- package/extensions/checkpoint/index.ts +102 -0
- package/extensions/checkpoint/messages.ts +170 -0
- package/extensions/checkpoint/prompt.ts +24 -0
- package/extensions/checkpoint/settings.ts +27 -0
- package/extensions/commit/commit-effort.ts +3 -3
- package/extensions/commit/commit-plan.ts +2 -2
- package/extensions/context/README.md +12 -8
- package/extensions/context/definitions.ts +178 -55
- package/extensions/context/index.ts +192 -160
- package/extensions/context/panel.ts +57 -37
- package/extensions/context/sync.ts +139 -89
- package/extensions/context/validation.ts +37 -25
- package/extensions/effort/README.md +1 -1
- package/extensions/effort/index.ts +3 -3
- package/extensions/effort/state.ts +3 -18
- package/extensions/explore/guidance.ts +6 -47
- package/extensions/explore/index.ts +38 -53
- package/extensions/explore/{ast/read → read}/hook.ts +12 -7
- package/extensions/explore/{ast/tools → tools}/ast-search.ts +15 -19
- package/extensions/explore/{ast/tools → tools}/context.ts +16 -23
- package/extensions/explore/{ast/tools → tools}/deps.ts +11 -15
- package/extensions/explore/{ast/tools → tools}/discover.ts +15 -17
- package/extensions/explore/{ast/tools → tools}/impact.ts +20 -23
- package/extensions/explore/{ast/tools → tools}/outline.ts +21 -24
- package/extensions/explore/{ast/tools → tools}/relationships.ts +23 -32
- package/extensions/explore/{ast/tools → tools}/render.ts +1 -1
- package/extensions/explore/{ast/tools → tools}/reverse-deps.ts +11 -15
- package/extensions/explore/{ast/tools → tools}/show.ts +16 -19
- package/extensions/explore/tools/target-params.ts +9 -0
- package/extensions/footer/index.ts +75 -56
- package/extensions/handoff/index.ts +8 -30
- package/extensions/image-gen/index.ts +46 -40
- package/extensions/patch/executor.ts +190 -135
- package/extensions/patch/matcher.ts +82 -48
- package/extensions/patch/parser.ts +149 -105
- package/extensions/patch/render.ts +32 -39
- package/extensions/patch/summary.ts +30 -26
- package/extensions/ready/README.md +7 -0
- package/extensions/ready/index.ts +58 -0
- package/extensions/ready/model.ts +128 -0
- package/extensions/ready/packs.ts +272 -0
- package/extensions/ready/render.ts +445 -0
- package/extensions/ready/scan.ts +760 -0
- package/extensions/reference/index.ts +2 -0
- package/extensions/reference/panel.ts +94 -31
- package/extensions/review/README.md +14 -6
- package/extensions/review/index.ts +58 -99
- package/extensions/review/model.ts +13 -33
- package/extensions/review/session.ts +9 -23
- package/extensions/runtime-context/context.ts +30 -26
- package/extensions/script-runner/README.md +2 -2
- package/extensions/script-runner/index.ts +122 -82
- package/extensions/silent-command-runner/index.ts +38 -23
- package/extensions/soul/README.md +2 -2
- package/extensions/soul/index.ts +4 -4
- package/extensions/soul/prompt.ts +6 -10
- package/extensions/soul/settings.ts +6 -3
- package/extensions/subagent/agents/context-sync.md +185 -33
- package/extensions/subagent/agents/scout.md +2 -2
- package/extensions/subagent/agents/web-research.md +2 -2
- package/extensions/subagent/agents.ts +137 -84
- package/extensions/subagent/cmux-dashboard.ts +39 -30
- package/extensions/subagent/index.ts +79 -59
- package/extensions/subagent/resume.ts +1 -1
- package/extensions/subagent/run.ts +308 -193
- package/extensions/subagent/runtime.ts +781 -343
- package/extensions/tau/index.ts +47 -38
- package/extensions/tau-help/help.md +20 -8
- package/extensions/tool-loader/README.md +4 -2
- package/extensions/tool-loader/index.ts +77 -52
- package/extensions/web/index.ts +6 -3
- package/extensions/web/webfetch.ts +114 -86
- package/package.json +3 -3
- package/schemas/tau.schema.json +38 -35
- package/shared/bounded-text-result.ts +1 -0
- package/shared/checkpoint-visibility.ts +9 -0
- package/shared/events.ts +28 -10
- package/shared/isolated-session.ts +72 -30
- package/shared/model-effort.ts +23 -22
- package/shared/model-fallback/index.ts +66 -41
- package/shared/settings/load.ts +45 -38
- package/shared/temporary-output-store.ts +16 -10
- package/{extensions/explore → src}/ast/adapter.ts +11 -2
- package/{extensions/explore → src}/ast/engine.ts +54 -38
- package/{extensions/explore → src}/ast/format/ast-search.ts +15 -6
- package/{extensions/explore → src}/ast/format/context.ts +3 -6
- package/src/ast/format/deps.ts +94 -0
- package/{extensions/explore → src}/ast/format/discover.ts +2 -2
- package/{extensions/explore → src}/ast/format/impact.ts +9 -11
- package/{extensions/explore → src}/ast/format/outline.ts +2 -2
- package/{extensions/explore → src}/ast/format/relationships.ts +11 -7
- package/{extensions/explore → src}/ast/format/show.ts +1 -1
- package/src/ast/format/targeted-result.ts +20 -0
- package/src/ast/grammars/c_sharp.wasm +0 -0
- package/{extensions/explore → src}/ast/grammars/manifest.json +9 -12
- package/{extensions/explore → src}/ast/grammars/manifest.ts +0 -4
- package/{extensions/explore → src}/ast/graph/file-graph.ts +210 -98
- package/{extensions/explore → src}/ast/graph/relationships.ts +285 -72
- package/{extensions/explore → src}/ast/ir.ts +6 -0
- package/src/ast/languages/csharp-file-deps.ts +90 -0
- package/{extensions/explore → src}/ast/languages/csharp.ts +9 -40
- package/src/ast/languages/dotted-index.ts +131 -0
- package/{extensions/explore → src}/ast/languages/file-dep-util.ts +22 -13
- package/src/ast/languages/fixtures/sample.cs +53 -0
- package/src/ast/languages/fixtures/sample.cs.expected.json +156 -0
- package/src/ast/languages/fixtures/sample.go +37 -0
- package/src/ast/languages/fixtures/sample.go.expected.json +96 -0
- package/src/ast/languages/fixtures/sample.java +40 -0
- package/src/ast/languages/fixtures/sample.java.expected.json +126 -0
- package/src/ast/languages/fixtures/sample.kt +31 -0
- package/src/ast/languages/fixtures/sample.kt.expected.json +134 -0
- package/src/ast/languages/fixtures/sample.md +23 -0
- package/src/ast/languages/fixtures/sample.md.expected.json +44 -0
- package/src/ast/languages/fixtures/sample.rs +54 -0
- package/src/ast/languages/fixtures/sample.rs.expected.json +178 -0
- package/src/ast/languages/fixtures/sample.swift +45 -0
- package/src/ast/languages/fixtures/sample.swift.expected.json +147 -0
- package/src/ast/languages/fixtures/sample.ts +45 -0
- package/src/ast/languages/fixtures/sample.ts.expected.json +141 -0
- package/src/ast/languages/fixtures/sample.tsx +36 -0
- package/src/ast/languages/fixtures/sample.tsx.expected.json +59 -0
- package/{extensions/explore → src}/ast/languages/go-file-deps.ts +6 -0
- package/{extensions/explore → src}/ast/languages/go.ts +21 -3
- package/{extensions/explore → src}/ast/languages/java.ts +5 -38
- package/src/ast/languages/jvm-file-deps.ts +135 -0
- package/{extensions/explore → src}/ast/languages/kotlin.ts +5 -39
- package/{extensions/explore → src}/ast/languages/rust-file-deps.ts +3 -3
- package/{extensions/explore → src}/ast/languages/rust.ts +21 -13
- package/{extensions/explore → src}/ast/languages/swift-file-deps.ts +22 -15
- package/{extensions/explore → src}/ast/languages/swift.ts +9 -39
- package/{extensions/explore → src}/ast/languages/tree.ts +47 -0
- package/{extensions/explore → src}/ast/languages/typescript-file-deps.ts +108 -0
- package/{extensions/explore → src}/ast/languages/typescript-package-surface.ts +4 -3
- package/{extensions/explore → src}/ast/languages/typescript.ts +3 -5
- package/{extensions/explore → src}/ast/markdown.ts +72 -47
- package/{extensions/explore → src}/ast/queries/ast-search.ts +164 -146
- package/{extensions/explore → src}/ast/queries/composite-target.ts +1 -1
- package/{extensions/explore → src}/ast/queries/context.ts +116 -80
- package/{extensions/explore → src}/ast/queries/discover.ts +154 -105
- package/src/ast/queries/impact.ts +291 -0
- package/{extensions/explore → src}/ast/queries/outline.ts +4 -2
- package/{extensions/explore → src}/ast/queries/show.ts +46 -42
- package/{extensions/explore → src}/ast/query.ts +1 -1
- package/{extensions/explore → src}/ast/registry.ts +1 -2
- package/{extensions/explore → src}/ast/scan.ts +1 -1
- package/src/ast/session.ts +50 -0
- package/{extensions/explore → src}/ast/slice.ts +8 -0
- package/{extensions/explore → src/ast}/traverse.ts +32 -20
- package/src/file-injection/index.ts +547 -0
- package/src/image-generation/client.ts +50 -31
- package/src/image-generation/index.ts +177 -134
- package/src/index.ts +2 -0
- package/src/tool-loading/index.ts +58 -0
- package/extensions/context/evidence.ts +0 -517
- package/extensions/context/projection.ts +0 -141
- package/extensions/context/state.ts +0 -30
- package/extensions/context/write-scope.ts +0 -109
- package/extensions/explore/ast/format/deps.ts +0 -86
- package/extensions/explore/ast/grammars/odin.wasm +0 -0
- package/extensions/explore/ast/languages/csharp-file-deps.ts +0 -189
- package/extensions/explore/ast/languages/fixtures/sample.cs +0 -17
- package/extensions/explore/ast/languages/fixtures/sample.java +0 -21
- package/extensions/explore/ast/languages/fixtures/sample.kt +0 -15
- package/extensions/explore/ast/languages/fixtures/sample.odin +0 -28
- package/extensions/explore/ast/languages/fixtures/sample.rs +0 -31
- package/extensions/explore/ast/languages/fixtures/sample.swift +0 -25
- package/extensions/explore/ast/languages/jvm-file-deps.ts +0 -217
- package/extensions/explore/ast/languages/odin-file-deps.ts +0 -175
- package/extensions/explore/ast/languages/odin.ts +0 -407
- package/extensions/explore/ast/queries/impact.ts +0 -250
- package/extensions/explore/outline-injection.ts +0 -151
- package/extensions/explore/read/autoread.ts +0 -118
- package/extensions/review/panel.ts +0 -128
- package/extensions/working-memory/README.md +0 -17
- package/extensions/working-memory/checkpoint.ts +0 -242
- package/extensions/working-memory/index.ts +0 -341
- package/extensions/working-memory/memory.ts +0 -235
- package/extensions/working-memory/render.ts +0 -185
- package/extensions/working-memory/settings.ts +0 -38
- package/extensions/working-memory/state.ts +0 -158
- package/shared/autoread.ts +0 -234
- package/shared/context-messages.ts +0 -19
- package/shared/outline-injection.ts +0 -56
- /package/{extensions/explore → src}/ast/format/composite.ts +0 -0
- /package/{extensions/explore → src}/ast/grammars/kotlin.wasm +0 -0
- /package/{extensions/explore → src}/ast/grammars/swift.wasm +0 -0
- /package/{extensions/explore → src}/ast/identity.ts +0 -0
- /package/{extensions/explore → src}/ast/package-surface.ts +0 -0
- /package/{extensions/explore/ast/read/policy.ts → src/ast/read-policy.ts} +0 -0
package/extensions/tau/index.ts
CHANGED
|
@@ -64,6 +64,52 @@ async function init(ctx: ExtensionCommandContext, scope: "global" | "project"):
|
|
|
64
64
|
ctx.ui.notify(`tau init wrote ${path}`, "info");
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
function pushExtensionFindings(
|
|
68
|
+
findings: Finding[],
|
|
69
|
+
level: Finding["level"],
|
|
70
|
+
path: string,
|
|
71
|
+
extensions: unknown,
|
|
72
|
+
specs: Awaited<ReturnType<typeof discoverTauSettingsSpecs>>,
|
|
73
|
+
): void {
|
|
74
|
+
if (extensions !== undefined && (!extensions || typeof extensions !== "object" || Array.isArray(extensions))) {
|
|
75
|
+
findings.push({ level, message: `extensions must be an object: ${path}`, startup: true });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const extensionRecord = (extensions ?? {}) as Record<string, unknown>;
|
|
79
|
+
for (const spec of specs) {
|
|
80
|
+
const section = extensionRecord[spec.key];
|
|
81
|
+
if (section === undefined) continue;
|
|
82
|
+
if (!Value.Check(spec.schema, section)) {
|
|
83
|
+
findings.push({ level, message: `invalid extensions.${spec.key}: ${path}`, startup: true });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function inspectPath(
|
|
89
|
+
findings: Finding[],
|
|
90
|
+
item: { level: Finding["level"]; path: string; trusted: boolean },
|
|
91
|
+
specs: Awaited<ReturnType<typeof discoverTauSettingsSpecs>>,
|
|
92
|
+
): Promise<void> {
|
|
93
|
+
const status = await readJsonStatus(item.path);
|
|
94
|
+
if (!status.exists) return;
|
|
95
|
+
if (!item.trusted) {
|
|
96
|
+
findings.push({
|
|
97
|
+
level: item.level,
|
|
98
|
+
message: `project Tau settings ignored because project is not trusted: ${item.path}. Run /trust, then /reload.`,
|
|
99
|
+
startup: true,
|
|
100
|
+
});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (!status.ok) {
|
|
104
|
+
findings.push({ level: item.level, message: `malformed JSON: ${item.path}: ${status.error}`, startup: true });
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (typeof status.value.$schema !== "string") {
|
|
108
|
+
findings.push({ level: item.level, message: `missing $schema: ${item.path}`, startup: false });
|
|
109
|
+
}
|
|
110
|
+
pushExtensionFindings(findings, item.level, item.path, status.value.extensions, specs);
|
|
111
|
+
}
|
|
112
|
+
|
|
67
113
|
async function inspect(ctx: Pick<ExtensionContext, "cwd" | "isProjectTrusted">): Promise<Finding[]> {
|
|
68
114
|
const findings: Finding[] = [];
|
|
69
115
|
const specs = await discoverTauSettingsSpecs(ctx.cwd);
|
|
@@ -71,44 +117,7 @@ async function inspect(ctx: Pick<ExtensionContext, "cwd" | "isProjectTrusted">):
|
|
|
71
117
|
{ level: "global" as const, path: globalTauSettingsPath(), trusted: true },
|
|
72
118
|
{ level: "project" as const, path: await projectTauSettingsPath(ctx.cwd), trusted: ctx.isProjectTrusted() },
|
|
73
119
|
];
|
|
74
|
-
|
|
75
|
-
for (const item of paths) {
|
|
76
|
-
const status = await readJsonStatus(item.path);
|
|
77
|
-
if (!status.exists) continue;
|
|
78
|
-
if (!item.trusted) {
|
|
79
|
-
findings.push({
|
|
80
|
-
level: item.level,
|
|
81
|
-
message: `project Tau settings ignored because project is not trusted: ${item.path}. Run /trust, then /reload.`,
|
|
82
|
-
startup: true,
|
|
83
|
-
});
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
if (!status.ok) {
|
|
87
|
-
findings.push({ level: item.level, message: `malformed JSON: ${item.path}: ${status.error}`, startup: true });
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
if (typeof status.value.$schema !== "string") {
|
|
91
|
-
findings.push({ level: item.level, message: `missing $schema: ${item.path}`, startup: false });
|
|
92
|
-
}
|
|
93
|
-
const extensions = status.value.extensions;
|
|
94
|
-
if (extensions !== undefined && (!extensions || typeof extensions !== "object" || Array.isArray(extensions))) {
|
|
95
|
-
findings.push({ level: item.level, message: `extensions must be an object: ${item.path}`, startup: true });
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
const extensionRecord = (extensions ?? {}) as Record<string, unknown>;
|
|
99
|
-
for (const spec of specs) {
|
|
100
|
-
const section = extensionRecord[spec.key];
|
|
101
|
-
if (section === undefined) continue;
|
|
102
|
-
if (!Value.Check(spec.schema, section)) {
|
|
103
|
-
findings.push({
|
|
104
|
-
level: item.level,
|
|
105
|
-
message: `invalid extensions.${spec.key}: ${item.path}`,
|
|
106
|
-
startup: true,
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
120
|
+
for (const item of paths) await inspectPath(findings, item, specs);
|
|
112
121
|
return findings;
|
|
113
122
|
}
|
|
114
123
|
|
|
@@ -6,6 +6,10 @@ Tau is a coding agent built to be a reliable partner during software work. It ca
|
|
|
6
6
|
|
|
7
7
|
Gives the agent macOS window discovery, screenshots, and app activation tools for visual validation. Requires macOS 14 and Screen & System Audio Recording permission.
|
|
8
8
|
|
|
9
|
+
## aside
|
|
10
|
+
|
|
11
|
+
Adds `/aside <question>` for a one-off question to the current model without putting the question or answer in the conversation. Choose the current conversation branch or no context. A thinking widget clears when the answer opens. Run `/aside` to reopen the latest answer and `/aside clear` to cancel or clear it.
|
|
12
|
+
|
|
9
13
|
## attention
|
|
10
14
|
|
|
11
15
|
Shows attention state when Tau needs the user to look at the chat, finishes compacting a session, or summarizes an abandoned branch.
|
|
@@ -18,6 +22,10 @@ Names sessions from their first request so saved sessions remain findable.
|
|
|
18
22
|
|
|
19
23
|
Adds `/branch` to create and switch Git branches from the TUI.
|
|
20
24
|
|
|
25
|
+
## bash-approval
|
|
26
|
+
|
|
27
|
+
Reviews every agent `bash` call with a quick-effort model before execution. Set `extensions.bashApproval.autoApprove` to run every reviewer-approved command without another confirmation. The reviewer approves routine local development work. Concrete destructive, system, production, privileged, or security-sensitive effects require human approval with one explanatory paragraph. Reviewer failures fall back to human approval and send an attention notification.
|
|
28
|
+
|
|
21
29
|
## cache-diagnostics
|
|
22
30
|
|
|
23
31
|
Records private prompt-cache fingerprints without storing prompt content. Run `/cache-debug` after suspicious cache misses to write a bounded investigation report under `~/.pi/agent/cache-diagnostics/reports/`.
|
|
@@ -32,15 +40,15 @@ Adds `/commit` for semantic commit grouping, review, and committing selected rep
|
|
|
32
40
|
|
|
33
41
|
## context
|
|
34
42
|
|
|
35
|
-
Adds `/context` to
|
|
43
|
+
Adds `/context` to inject reusable repository work scopes from `.pi/contexts`, and `/context-sync` or `/context-sync <nudge>` for human-driven catalog sync. Selecting entries injects them once into the conversation: `read` paths as complete files, `show` targets as current declaration slices, `outline` paths as Explore structures, and one hidden note listing `references` plus instructions to treat the injected material as current. Run `/context` again to inject more. Manual sync replaces the editor with a status panel; Escape or Ctrl+C cancels. When `sync.automation` is on, coding agent can also run `context-sync` after meaningful uncommitted work. Sync catalogs durable code and long-lived documentation; recurring scratch, planning, interview, and rough-idea paths belong in `validation.ignoreGlobs`. `sync.enabled` is master switch for command, automation, and validation auto-run. Context validation is off by default; when on (and sync enabled), Tau auto-runs context-sync on failure. Domain folders are `NN_slug` tabs (ordered by the two-digit prefix; UI shows the slug), TOML files are concepts, and TOML sections are selectable entries.
|
|
36
44
|
|
|
37
|
-
##
|
|
45
|
+
## checkpoint
|
|
38
46
|
|
|
39
|
-
|
|
47
|
+
Adds the `checkpoint` tool for retaining exact user and assistant messages, current file reads and outlines, durable work state, an agent-written resume directive, and deferred file paths as a rolling continuation context. Checkpoint rows are hidden by default. Set `extensions.checkpoint.showToolRows` to `true` before `/reload` to watch checkpoint and newly injected-file rows while working; existing injected-file rows keep their saved display state. The agent receives hidden checkpoint nudges at 50% and 75% of `extensions.checkpoint.checkpointTokenLimit` (150,000 by default); non-checkpoint tools are blocked at the limit until checkpoint succeeds.
|
|
40
48
|
|
|
41
49
|
## effort
|
|
42
50
|
|
|
43
|
-
Adds `/effort [
|
|
51
|
+
Adds `/effort [quick|standard|deep]` to select effort and a provider from current logins. Tau selects provider’s best available model for tier, then tries its configured model fallback. `Ctrl+Shift+E` cycles tiers on current provider. Footer derives effort from current provider, model, and thinking level, and hides it when no configured tier matches.
|
|
44
52
|
|
|
45
53
|
## explore
|
|
46
54
|
|
|
@@ -74,9 +82,13 @@ Replaces separate edit/write operations with one multi-file `patch` tool. It can
|
|
|
74
82
|
|
|
75
83
|
Adds `/qna` for when the agent has asked you several questions in chat and you want a friendly UI for answering them on your own terms. It is only active when you manually run the command.
|
|
76
84
|
|
|
85
|
+
## ready
|
|
86
|
+
|
|
87
|
+
Adds `/ready` to scan agent-readiness rails (cold start, toolchain, verify, lint/entropy, policy, standards, context, and related signals). Choose Markdown or HTML; Tau writes a timestamped report under `.pi/tau/ready/` and notifies with the path. Scan-only in v1 — no model judgment and no scores.
|
|
88
|
+
|
|
77
89
|
## review
|
|
78
90
|
|
|
79
|
-
Adds `/review` for
|
|
91
|
+
Adds `/review [direction]` for an isolated simplify, architecture, or correctness review. With no direction, it reviews current Git changes. Free-form direction reviews the requested part of the repository even when the working tree is clean. Choose a review type and logged-in provider, then Tau writes the result as Markdown under `.pi/tau/reviews/` without adding it to the parent agent context.
|
|
80
92
|
|
|
81
93
|
## reference
|
|
82
94
|
|
|
@@ -92,7 +104,7 @@ Supplies the agent with the current local date and an initial root directory sna
|
|
|
92
104
|
|
|
93
105
|
## script-runner
|
|
94
106
|
|
|
95
|
-
Gives the agent a first-class `script_runner` tool to execute Python 3 and
|
|
107
|
+
Gives the agent a first-class `script_runner` tool to execute Python 3, Node.js, and Deno scripts instead of bash. On failure it returns a `scriptId`; the agent retries with targeted `{oldText,newText}` edits against the script it already wrote rather than resending the whole script. Runtimes are detected from the environment (Python 3 via `python3`; `node` is the local Node.js runtime with `--experimental-strip-types`, Node 22.6+ — full Node APIs, TypeScript with erasable syntax or plain JavaScript; `deno` via `deno run -A` — full permissions, native TypeScript/JavaScript, Deno APIs). The tool registers only available runtimes and is hidden from the prompt if none are present.
|
|
96
108
|
|
|
97
109
|
## silent-command-runner
|
|
98
110
|
|
|
@@ -100,7 +112,7 @@ Runs configured commands while keeping their output out of agent context when th
|
|
|
100
112
|
|
|
101
113
|
## soul
|
|
102
114
|
|
|
103
|
-
Adds two independently toggleable sections to Pi’s native assistant prompt: `ponytail` (a lazy-senior-dev build ethos) and `
|
|
115
|
+
Adds two independently toggleable sections to Pi’s native assistant prompt: `ponytail` (a lazy-senior-dev build ethos) and `simplified` (Simplified Technical English with short, explanatory responses and small planning chunks). Both on by default.
|
|
104
116
|
|
|
105
117
|
## stash
|
|
106
118
|
|
|
@@ -120,7 +132,7 @@ Adds `/tau`, `/tau init [--global|--project]`, and `/tau doctor` for Tau setup a
|
|
|
120
132
|
|
|
121
133
|
## tool-loader
|
|
122
134
|
|
|
123
|
-
Progressively exposes specialist
|
|
135
|
+
Progressively exposes registered specialist tool groups through `load_tools`. Tau registers `web`, `image`, and `appshot`; project or global package extensions can add groups with `registerDeferredToolGroup()` from `@shanepadgett/tau-agent`. Supported providers can preserve more prompt-cache reuse.
|
|
124
136
|
|
|
125
137
|
## web
|
|
126
138
|
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# Tool Loader
|
|
2
2
|
|
|
3
|
-
Tau progressively exposes specialist
|
|
3
|
+
Tau progressively exposes registered specialist tool groups. Most coding turns do not need web, image, macOS application, or application-specific schemas, so Pi can load those tools later without discarding supported provider cache prefixes.
|
|
4
4
|
|
|
5
|
-
The agent normally calls `load_tools` itself.
|
|
5
|
+
The agent normally calls `load_tools` itself. Tau's built-in groups are:
|
|
6
6
|
|
|
7
7
|
- `web` for public web and implementation research
|
|
8
8
|
- `image` for raster image generation and editing
|
|
9
9
|
- `appshot` for macOS window discovery, capture, and activation
|
|
10
10
|
|
|
11
|
+
Project and global package extensions can add groups with `registerDeferredToolGroup()` from `@shanepadgett/tau-agent`. The group description is included in the loader catalog so the agent can select it when a task needs that capability.
|
|
12
|
+
|
|
11
13
|
Supported models optimize prompt caching when a group loads. Other models keep the same functional behavior.
|
|
12
14
|
|
|
13
15
|
After changing this extension during development, run `/reload` before testing.
|
|
@@ -1,21 +1,12 @@
|
|
|
1
|
-
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
1
|
import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
2
|
import { type Static, Type } from "typebox";
|
|
4
|
-
|
|
5
|
-
const CAPABILITIES = ["web", "image", "appshot"] as const;
|
|
6
|
-
type Capability = (typeof CAPABILITIES)[number];
|
|
7
|
-
|
|
8
|
-
const CAPABILITY_TOOLS: Record<Capability, readonly string[]> = {
|
|
9
|
-
web: ["webfetch", "websearch", "codesearch"],
|
|
10
|
-
image: ["image_gen"],
|
|
11
|
-
appshot: ["list_windows", "screenshot_window", "activate_app"],
|
|
12
|
-
};
|
|
13
|
-
const SPECIALIST_TOOLS = CAPABILITIES.flatMap((capability) => CAPABILITY_TOOLS[capability]);
|
|
3
|
+
import { registeredDeferredToolGroups, type DeferredToolGroupInfo } from "../../src/tool-loading/index.ts";
|
|
14
4
|
|
|
15
5
|
const loadToolsSchema = Type.Object(
|
|
16
6
|
{
|
|
17
|
-
capability:
|
|
18
|
-
|
|
7
|
+
capability: Type.String({
|
|
8
|
+
minLength: 1,
|
|
9
|
+
description: "Registered specialist group ID, such as web, image, appshot, or a package-provided group",
|
|
19
10
|
}),
|
|
20
11
|
},
|
|
21
12
|
{ additionalProperties: false },
|
|
@@ -25,50 +16,85 @@ type LoadToolsParams = Static<typeof loadToolsSchema>;
|
|
|
25
16
|
|
|
26
17
|
interface LoadToolsDetails {
|
|
27
18
|
version: 1;
|
|
28
|
-
capability:
|
|
19
|
+
capability: string;
|
|
29
20
|
requestedToolNames: string[];
|
|
30
21
|
addedToolNames: string[];
|
|
31
22
|
}
|
|
32
23
|
|
|
33
24
|
export default function toolLoaderExtension(pi: ExtensionAPI): void {
|
|
34
25
|
let managed = false;
|
|
35
|
-
let
|
|
26
|
+
let allowedToolNames = new Map<string, ReadonlySet<string>>();
|
|
27
|
+
let managedToolNames = new Set<string>();
|
|
28
|
+
|
|
29
|
+
pi.registerTool(createLoadToolsTool(pi, []));
|
|
30
|
+
|
|
31
|
+
pi.on("session_start", (_event, ctx) => {
|
|
32
|
+
const groups = registeredDeferredToolGroups(pi);
|
|
33
|
+
pi.registerTool(createLoadToolsTool(pi, groups));
|
|
34
|
+
|
|
35
|
+
const initial = pi.getActiveTools();
|
|
36
|
+
const initialSet = new Set(initial);
|
|
37
|
+
allowedToolNames = new Map(
|
|
38
|
+
groups.map((group) => [group.id, new Set(group.toolNames.filter((name) => initialSet.has(name)))]),
|
|
39
|
+
);
|
|
40
|
+
managedToolNames = new Set(groups.flatMap((group) => group.toolNames));
|
|
41
|
+
managed = initialSet.has("load_tools") && groups.length > 0;
|
|
42
|
+
if (managed) restoreActiveTools(pi, initial, loadedCapabilities(ctx.sessionManager.getBranch()), groups);
|
|
43
|
+
});
|
|
36
44
|
|
|
37
|
-
pi.
|
|
38
|
-
|
|
45
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
46
|
+
if (managed) {
|
|
47
|
+
restoreActiveTools(
|
|
48
|
+
pi,
|
|
49
|
+
pi.getActiveTools(),
|
|
50
|
+
loadedCapabilities(ctx.sessionManager.getBranch()),
|
|
51
|
+
registeredDeferredToolGroups(pi),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
function createLoadToolsTool(pi: ExtensionAPI, groups: readonly DeferredToolGroupInfo[]) {
|
|
57
|
+
return defineTool<typeof loadToolsSchema, LoadToolsDetails>({
|
|
39
58
|
name: "load_tools",
|
|
40
59
|
label: "Load Tools",
|
|
41
|
-
description:
|
|
42
|
-
|
|
43
|
-
promptSnippet: "Load a specialist Tau tool group for web research, image generation, or macOS app inspection",
|
|
60
|
+
description: `Load one registered Tau specialist tool group for the current session.${formatGroupCatalog(groups)}`,
|
|
61
|
+
promptSnippet: "Load a registered specialist tool group when the current tools cannot perform the task",
|
|
44
62
|
promptGuidelines: [
|
|
45
|
-
"Use load_tools before attempting a specialist capability whose tools are not currently available.",
|
|
63
|
+
"Use load_tools before attempting a registered specialist capability whose tools are not currently available.",
|
|
46
64
|
],
|
|
47
65
|
parameters: loadToolsSchema,
|
|
48
66
|
async execute(_toolCallId, params: LoadToolsParams) {
|
|
67
|
+
const group = registeredDeferredToolGroups(pi).find((candidate) => candidate.id === params.capability);
|
|
68
|
+
if (group === undefined) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`Unknown specialist tool group: ${params.capability}.${formatGroupCatalog(registeredDeferredToolGroups(pi))}`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
49
74
|
const before = pi.getActiveTools();
|
|
50
|
-
const requested = [...
|
|
75
|
+
const requested = [...group.toolNames];
|
|
51
76
|
const registered = new Set(pi.getAllTools().map((tool) => tool.name));
|
|
52
|
-
const
|
|
77
|
+
const allowed = allowedToolNames.get(group.id) ?? new Set<string>();
|
|
78
|
+
const loadable = requested.filter((name) => registered.has(name) && allowed.has(name));
|
|
53
79
|
if (loadable.length === 0) {
|
|
54
80
|
throw new Error(`No ${params.capability} tools are available in this session's tool configuration.`);
|
|
55
81
|
}
|
|
82
|
+
|
|
56
83
|
const beforeSet = new Set(before);
|
|
57
|
-
|
|
58
|
-
pi.setActiveTools(next);
|
|
84
|
+
pi.setActiveTools([...before, ...loadable.filter((name) => !beforeSet.has(name))]);
|
|
59
85
|
const after = pi.getActiveTools();
|
|
60
|
-
const addedToolNames =
|
|
86
|
+
const addedToolNames = requested.filter((name) => !beforeSet.has(name) && after.includes(name));
|
|
61
87
|
const available = requested.filter((name) => after.includes(name));
|
|
62
88
|
const unavailable = requested.filter((name) => !after.includes(name));
|
|
63
|
-
const label = `${params.capability[0]?.toUpperCase()}${params.capability.slice(1)}`;
|
|
64
89
|
const text =
|
|
65
90
|
addedToolNames.length > 0
|
|
66
91
|
? `Loaded ${params.capability} tools: ${addedToolNames.join(", ")}.`
|
|
67
|
-
: `${
|
|
92
|
+
: `${params.capability} tools are already loaded: ${available.join(", ")}.`;
|
|
93
|
+
|
|
68
94
|
return {
|
|
69
95
|
content: [
|
|
70
96
|
{
|
|
71
|
-
type: "text",
|
|
97
|
+
type: "text" as const,
|
|
72
98
|
text: unavailable.length ? `${text} Unavailable: ${unavailable.join(", ")}.` : text,
|
|
73
99
|
},
|
|
74
100
|
],
|
|
@@ -80,33 +106,33 @@ export default function toolLoaderExtension(pi: ExtensionAPI): void {
|
|
|
80
106
|
},
|
|
81
107
|
};
|
|
82
108
|
},
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
pi.on("session_start", (_event, ctx) => {
|
|
87
|
-
const initial = pi.getActiveTools();
|
|
88
|
-
const initialSet = new Set(initial);
|
|
89
|
-
allowedSpecialistNames = new Set(SPECIALIST_TOOLS.filter((name) => initialSet.has(name)));
|
|
90
|
-
managed = initialSet.has("load_tools") && SPECIALIST_TOOLS.every((name) => initialSet.has(name));
|
|
91
|
-
if (managed) restoreActiveTools(pi, initial, loadedCapabilities(ctx.sessionManager.getBranch()));
|
|
92
|
-
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
93
111
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
112
|
+
function restoreActiveTools(
|
|
113
|
+
pi: ExtensionAPI,
|
|
114
|
+
current: readonly string[],
|
|
115
|
+
loaded: ReadonlySet<string>,
|
|
116
|
+
groups: readonly DeferredToolGroupInfo[],
|
|
117
|
+
): void {
|
|
118
|
+
const next = current.filter((name) => !managedToolNames.has(name));
|
|
119
|
+
for (const group of groups) {
|
|
120
|
+
if (!loaded.has(group.id)) continue;
|
|
121
|
+
const allowed = allowedToolNames.get(group.id) ?? new Set<string>();
|
|
122
|
+
next.push(...group.toolNames.filter((name) => allowed.has(name)));
|
|
123
|
+
}
|
|
124
|
+
pi.setActiveTools([...new Set(next)]);
|
|
125
|
+
}
|
|
97
126
|
}
|
|
98
127
|
|
|
99
|
-
function
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
if (loaded.has(capability)) next.push(...CAPABILITY_TOOLS[capability]);
|
|
104
|
-
}
|
|
105
|
-
pi.setActiveTools([...new Set(next)]);
|
|
128
|
+
function formatGroupCatalog(groups: readonly DeferredToolGroupInfo[]): string {
|
|
129
|
+
if (groups.length === 0) return " No specialist groups are registered.";
|
|
130
|
+
const catalog = groups.map((group) => `${group.id}: ${group.description}`).join("; ");
|
|
131
|
+
return ` Registered groups: ${catalog}.`;
|
|
106
132
|
}
|
|
107
133
|
|
|
108
|
-
function loadedCapabilities(entries: readonly unknown[]): Set<
|
|
109
|
-
const loaded = new Set<
|
|
134
|
+
function loadedCapabilities(entries: readonly unknown[]): Set<string> {
|
|
135
|
+
const loaded = new Set<string>();
|
|
110
136
|
for (const value of entries) {
|
|
111
137
|
if (!value || typeof value !== "object") continue;
|
|
112
138
|
const entry = value as Record<string, unknown>;
|
|
@@ -125,7 +151,6 @@ function isLoadToolsDetails(value: unknown): value is LoadToolsDetails {
|
|
|
125
151
|
return (
|
|
126
152
|
details.version === 1 &&
|
|
127
153
|
typeof details.capability === "string" &&
|
|
128
|
-
CAPABILITIES.includes(details.capability as Capability) &&
|
|
129
154
|
Array.isArray(details.requestedToolNames) &&
|
|
130
155
|
details.requestedToolNames.every((name) => typeof name === "string") &&
|
|
131
156
|
Array.isArray(details.addedToolNames) &&
|
package/extensions/web/index.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { createToolRowStateStore } from "../../shared/tool-row-state.js";
|
|
3
|
+
import { registerDeferredToolGroup } from "../../src/tool-loading/index.ts";
|
|
3
4
|
import { createCodeSearchTool } from "./codesearch.ts";
|
|
4
5
|
import { createWebFetchTool } from "./webfetch.ts";
|
|
5
6
|
import { createWebSearchTool } from "./websearch.ts";
|
|
6
7
|
|
|
7
8
|
export default function webExtension(pi: ExtensionAPI): void {
|
|
8
9
|
const rowState = createToolRowStateStore(pi, "web.tool-row-state");
|
|
9
|
-
pi
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
registerDeferredToolGroup(pi, {
|
|
11
|
+
id: "web",
|
|
12
|
+
description: "Public web and implementation research",
|
|
13
|
+
tools: [createWebFetchTool(rowState), createWebSearchTool(rowState), createCodeSearchTool(rowState)],
|
|
14
|
+
});
|
|
12
15
|
pi.on("session_start", () => rowState.clear());
|
|
13
16
|
}
|
|
@@ -27,14 +27,22 @@ const webFetchParams = Type.Object(
|
|
|
27
27
|
);
|
|
28
28
|
|
|
29
29
|
type WebFetchParams = Static<typeof webFetchParams>;
|
|
30
|
+
type WebFetchFormat = "markdown" | "text" | "html";
|
|
30
31
|
interface WebFetchDetails {
|
|
31
32
|
url: string;
|
|
32
|
-
format:
|
|
33
|
+
format: WebFetchFormat;
|
|
33
34
|
mime: string;
|
|
34
35
|
bytes: number;
|
|
35
36
|
truncation?: TruncationResult;
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
type WebFetchContent = { type: "text"; text: string } | { type: "image"; data: string; mimeType: string };
|
|
40
|
+
|
|
41
|
+
interface WebFetchResult {
|
|
42
|
+
content: WebFetchContent[];
|
|
43
|
+
details: WebFetchDetails;
|
|
44
|
+
}
|
|
45
|
+
|
|
38
46
|
async function readResponseBody(response: FetchResponse): Promise<Uint8Array> {
|
|
39
47
|
if (!response.body) return new Uint8Array();
|
|
40
48
|
const reader = response.body.getReader();
|
|
@@ -61,6 +69,110 @@ function renderCallSummary(args: WebFetchParams): string {
|
|
|
61
69
|
return truncateCallSummary((args.url ?? "").trim());
|
|
62
70
|
}
|
|
63
71
|
|
|
72
|
+
function parseFetchUrl(raw: string): URL {
|
|
73
|
+
let url: URL;
|
|
74
|
+
try {
|
|
75
|
+
url = new URL(raw);
|
|
76
|
+
} catch {
|
|
77
|
+
throw new Error(`Invalid URL: ${raw}`);
|
|
78
|
+
}
|
|
79
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
80
|
+
throw new Error("URL must use http:// or https://");
|
|
81
|
+
}
|
|
82
|
+
return url;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function acceptHeaderForFormat(format: WebFetchFormat): string {
|
|
86
|
+
if (format === "markdown") return "text/markdown;q=1.0, text/plain;q=0.8, text/html;q=0.7, */*;q=0.1";
|
|
87
|
+
if (format === "text") return "text/plain;q=1.0, text/markdown;q=0.9, text/html;q=0.8, */*;q=0.1";
|
|
88
|
+
return "text/html;q=1.0, application/xhtml+xml;q=0.9, */*;q=0.1";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function fetchWithChallengeRetry(
|
|
92
|
+
url: string,
|
|
93
|
+
headers: Record<string, string>,
|
|
94
|
+
signal: AbortSignal,
|
|
95
|
+
): Promise<FetchResponse> {
|
|
96
|
+
const first = (await fetch(url, { method: "GET", headers, signal })) as FetchResponse;
|
|
97
|
+
if (first.status !== 403 || first.headers.get("cf-mitigated")?.toLowerCase() !== "challenge") return first;
|
|
98
|
+
await first.body?.cancel().catch(() => undefined);
|
|
99
|
+
return (await fetch(url, {
|
|
100
|
+
method: "GET",
|
|
101
|
+
headers: { ...headers, "User-Agent": "pi" },
|
|
102
|
+
signal,
|
|
103
|
+
})) as FetchResponse;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function assertDeclaredBodySizeOk(response: FetchResponse): Promise<void> {
|
|
107
|
+
const declaredLength = response.headers.get("content-length");
|
|
108
|
+
if (declaredLength === null) return;
|
|
109
|
+
const bytes = Number.parseInt(declaredLength, 10);
|
|
110
|
+
if (!Number.isFinite(bytes) || bytes <= MAX_RESPONSE_BYTES) return;
|
|
111
|
+
await response.body?.cancel().catch(() => undefined);
|
|
112
|
+
throw new Error("Response too large (limit is 5MB)");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function formatFetchedText(raw: string, format: WebFetchFormat, mime: string): string {
|
|
116
|
+
if (format === "html") return raw;
|
|
117
|
+
const isHtml = mime === "text/html" || mime === "application/xhtml+xml";
|
|
118
|
+
if (!isHtml) return raw;
|
|
119
|
+
return format === "text" ? htmlToText(raw) : htmlToMarkdown(raw);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function buildWebFetchResult(url: string, format: WebFetchFormat, mime: string, body: Uint8Array): WebFetchResult {
|
|
123
|
+
const details = { url, format, mime, bytes: body.byteLength } satisfies WebFetchDetails;
|
|
124
|
+
if (mime.startsWith("image/") && mime !== "image/svg+xml") {
|
|
125
|
+
return {
|
|
126
|
+
content: [
|
|
127
|
+
{ type: "text", text: `Fetched image from ${url} (${mime})` },
|
|
128
|
+
{ type: "image", data: Buffer.from(body).toString("base64"), mimeType: mime },
|
|
129
|
+
],
|
|
130
|
+
details,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const truncated = truncateToolOutput(formatFetchedText(new TextDecoder().decode(body), format, mime));
|
|
134
|
+
return {
|
|
135
|
+
content: [{ type: "text", text: truncated.text }],
|
|
136
|
+
details: {
|
|
137
|
+
...details,
|
|
138
|
+
...(truncated.truncation ? { truncation: truncated.truncation } : {}),
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function executeWebFetch(
|
|
144
|
+
params: WebFetchParams,
|
|
145
|
+
signal: AbortSignal | undefined,
|
|
146
|
+
onUpdate: ((update: { content: WebFetchContent[]; details: undefined }) => void | Promise<void>) | undefined,
|
|
147
|
+
): Promise<WebFetchResult> {
|
|
148
|
+
const url = parseFetchUrl(params.url);
|
|
149
|
+
const format = params.format ?? "markdown";
|
|
150
|
+
const timeout = normalizeTimeout(params.timeout, 30);
|
|
151
|
+
await onUpdate?.({ content: [{ type: "text", text: "Fetching page..." }], details: undefined });
|
|
152
|
+
const timeoutSignal = AbortSignal.timeout(timeout * 1000);
|
|
153
|
+
const requestSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const headers = {
|
|
157
|
+
"User-Agent":
|
|
158
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
|
|
159
|
+
Accept: acceptHeaderForFormat(format),
|
|
160
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
161
|
+
};
|
|
162
|
+
const response = await fetchWithChallengeRetry(url.toString(), headers, requestSignal);
|
|
163
|
+
if (!response.ok) throw new Error(`Request failed with status ${response.status}`);
|
|
164
|
+
await assertDeclaredBodySizeOk(response);
|
|
165
|
+
const body = await readResponseBody(response);
|
|
166
|
+
const mime = (response.headers.get("content-type")?.split(";", 1)[0] ?? "").trim().toLowerCase();
|
|
167
|
+
return buildWebFetchResult(url.toString(), format, mime, body);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (timeoutSignal.aborted && signal?.aborted !== true) {
|
|
170
|
+
throw new Error(`Web fetch timed out after ${timeout}s`);
|
|
171
|
+
}
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
64
176
|
export function createWebFetchTool(rowState: ToolRowStateStore) {
|
|
65
177
|
return defineTool<typeof webFetchParams, WebFetchDetails | undefined>({
|
|
66
178
|
name: "webfetch",
|
|
@@ -69,91 +181,7 @@ export function createWebFetchTool(rowState: ToolRowStateStore) {
|
|
|
69
181
|
"Fetch a known HTTP(S) URL as Markdown, text, or HTML. Use webfetch when you already have a URL; use websearch for broad discovery and codesearch for implementation-oriented lookups. Use a separate research workflow when several searches, fetches, and synthesis are needed. Supports inline images, limits response bodies to 5 MB, and truncates text to 2,000 lines or 50 KB.",
|
|
70
182
|
parameters: webFetchParams,
|
|
71
183
|
async execute(_toolCallId, params, signal, onUpdate) {
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
url = new URL(params.url);
|
|
75
|
-
} catch {
|
|
76
|
-
throw new Error(`Invalid URL: ${params.url}`);
|
|
77
|
-
}
|
|
78
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
79
|
-
throw new Error("URL must use http:// or https://");
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const format = params.format ?? "markdown";
|
|
83
|
-
const timeout = normalizeTimeout(params.timeout, 30);
|
|
84
|
-
await onUpdate?.({ content: [{ type: "text", text: "Fetching page..." }], details: undefined });
|
|
85
|
-
const timeoutSignal = AbortSignal.timeout(timeout * 1000);
|
|
86
|
-
const requestSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
87
|
-
|
|
88
|
-
try {
|
|
89
|
-
const accept =
|
|
90
|
-
format === "markdown"
|
|
91
|
-
? "text/markdown;q=1.0, text/plain;q=0.8, text/html;q=0.7, */*;q=0.1"
|
|
92
|
-
: format === "text"
|
|
93
|
-
? "text/plain;q=1.0, text/markdown;q=0.9, text/html;q=0.8, */*;q=0.1"
|
|
94
|
-
: "text/html;q=1.0, application/xhtml+xml;q=0.9, */*;q=0.1";
|
|
95
|
-
const headers = {
|
|
96
|
-
"User-Agent":
|
|
97
|
-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
|
|
98
|
-
Accept: accept,
|
|
99
|
-
"Accept-Language": "en-US,en;q=0.9",
|
|
100
|
-
};
|
|
101
|
-
const first = (await fetch(url.toString(), {
|
|
102
|
-
method: "GET",
|
|
103
|
-
headers,
|
|
104
|
-
signal: requestSignal,
|
|
105
|
-
})) as FetchResponse;
|
|
106
|
-
let response = first;
|
|
107
|
-
if (first.status === 403 && first.headers.get("cf-mitigated")?.toLowerCase() === "challenge") {
|
|
108
|
-
await first.body?.cancel().catch(() => undefined);
|
|
109
|
-
response = (await fetch(url.toString(), {
|
|
110
|
-
method: "GET",
|
|
111
|
-
headers: { ...headers, "User-Agent": "pi" },
|
|
112
|
-
signal: requestSignal,
|
|
113
|
-
})) as FetchResponse;
|
|
114
|
-
}
|
|
115
|
-
if (!response.ok) throw new Error(`Request failed with status ${response.status}`);
|
|
116
|
-
|
|
117
|
-
const declaredLength = response.headers.get("content-length");
|
|
118
|
-
if (declaredLength !== null) {
|
|
119
|
-
const bytes = Number.parseInt(declaredLength, 10);
|
|
120
|
-
if (Number.isFinite(bytes) && bytes > MAX_RESPONSE_BYTES) {
|
|
121
|
-
await response.body?.cancel().catch(() => undefined);
|
|
122
|
-
throw new Error("Response too large (limit is 5MB)");
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const body = await readResponseBody(response);
|
|
127
|
-
const mime = (response.headers.get("content-type")?.split(";", 1)[0] ?? "").trim().toLowerCase();
|
|
128
|
-
const details = { url: url.toString(), format, mime, bytes: body.byteLength } satisfies WebFetchDetails;
|
|
129
|
-
if (mime.startsWith("image/") && mime !== "image/svg+xml") {
|
|
130
|
-
return {
|
|
131
|
-
content: [
|
|
132
|
-
{ type: "text", text: `Fetched image from ${url.toString()} (${mime})` },
|
|
133
|
-
{ type: "image", data: Buffer.from(body).toString("base64"), mimeType: mime },
|
|
134
|
-
],
|
|
135
|
-
details,
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const raw = new TextDecoder().decode(body);
|
|
140
|
-
const isHtml = mime === "text/html" || mime === "application/xhtml+xml";
|
|
141
|
-
const output =
|
|
142
|
-
format === "html" ? raw : isHtml ? (format === "text" ? htmlToText(raw) : htmlToMarkdown(raw)) : raw;
|
|
143
|
-
const truncated = truncateToolOutput(output);
|
|
144
|
-
return {
|
|
145
|
-
content: [{ type: "text", text: truncated.text }],
|
|
146
|
-
details: {
|
|
147
|
-
...details,
|
|
148
|
-
...(truncated.truncation ? { truncation: truncated.truncation } : {}),
|
|
149
|
-
},
|
|
150
|
-
};
|
|
151
|
-
} catch (error) {
|
|
152
|
-
if (timeoutSignal.aborted && signal?.aborted !== true) {
|
|
153
|
-
throw new Error(`Web fetch timed out after ${timeout}s`);
|
|
154
|
-
}
|
|
155
|
-
throw error;
|
|
156
|
-
}
|
|
184
|
+
return executeWebFetch(params, signal, onUpdate);
|
|
157
185
|
},
|
|
158
186
|
renderCall(args, theme, context) {
|
|
159
187
|
rowState.watch(context.toolCallId, context.invalidate);
|