@aexol/spectral 0.9.58 → 0.9.59
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 +6 -0
- package/README.md +43 -0
- package/dist/agent/index.d.ts +6 -1
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +133 -14
- package/dist/extensions/web/index.d.ts +10 -0
- package/dist/extensions/web/index.d.ts.map +1 -0
- package/dist/extensions/web/index.js +11 -0
- package/dist/extensions/web/tools/fetch.d.ts +3 -0
- package/dist/extensions/web/tools/fetch.d.ts.map +1 -0
- package/dist/extensions/web/tools/fetch.js +80 -0
- package/dist/extensions/web/utils/fetcher.d.ts +34 -0
- package/dist/extensions/web/utils/fetcher.d.ts.map +1 -0
- package/dist/extensions/web/utils/fetcher.js +289 -0
- package/dist/extensions/web/utils/parser.d.ts +15 -0
- package/dist/extensions/web/utils/parser.d.ts.map +1 -0
- package/dist/extensions/web/utils/parser.js +205 -0
- package/dist/memory/branch.d.ts.map +1 -1
- package/dist/memory/branch.js +16 -8
- package/dist/memory/commands/status.d.ts.map +1 -1
- package/dist/memory/commands/status.js +3 -2
- package/dist/memory/config.d.ts +12 -0
- package/dist/memory/config.d.ts.map +1 -1
- package/dist/memory/config.js +21 -2
- package/dist/memory/hooks/compaction-trigger.d.ts.map +1 -1
- package/dist/memory/hooks/compaction-trigger.js +19 -3
- package/dist/memory/index.d.ts.map +1 -1
- package/dist/memory/index.js +2 -0
- package/dist/memory/tool-output-compressor.d.ts +14 -20
- package/dist/memory/tool-output-compressor.d.ts.map +1 -1
- package/dist/memory/tool-output-compressor.js +391 -137
- package/dist/memory/tools/compact-context.d.ts +5 -0
- package/dist/memory/tools/compact-context.d.ts.map +1 -0
- package/dist/memory/tools/compact-context.js +772 -0
- package/dist/relay/dispatcher.d.ts +1 -1
- package/dist/relay/dispatcher.d.ts.map +1 -1
- package/dist/relay/dispatcher.js +61 -1
- package/dist/sdk/coding-agent/core/agent-session.d.ts +56 -3
- package/dist/sdk/coding-agent/core/agent-session.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/agent-session.js +408 -210
- package/dist/sdk/coding-agent/core/compaction/compaction.d.ts +57 -7
- package/dist/sdk/coding-agent/core/compaction/compaction.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/compaction/compaction.js +146 -27
- package/dist/sdk/coding-agent/core/compaction/dcp-lite.d.ts +24 -0
- package/dist/sdk/coding-agent/core/compaction/dcp-lite.d.ts.map +1 -0
- package/dist/sdk/coding-agent/core/compaction/dcp-lite.js +411 -0
- package/dist/sdk/coding-agent/core/compaction/index.d.ts +2 -0
- package/dist/sdk/coding-agent/core/compaction/index.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/compaction/index.js +2 -0
- package/dist/sdk/coding-agent/core/compaction/policy.d.ts +71 -0
- package/dist/sdk/coding-agent/core/compaction/policy.d.ts.map +1 -0
- package/dist/sdk/coding-agent/core/compaction/policy.js +147 -0
- package/dist/sdk/coding-agent/core/extensions/native-extensions.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/extensions/native-extensions.js +9 -0
- package/dist/sdk/coding-agent/core/extensions/types.d.ts +17 -0
- package/dist/sdk/coding-agent/core/extensions/types.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/settings-manager.d.ts +4 -10
- package/dist/sdk/coding-agent/core/settings-manager.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/settings-manager.js +17 -17
- package/dist/sdk/coding-agent/index.d.ts +1 -1
- package/dist/sdk/coding-agent/index.d.ts.map +1 -1
- package/dist/server/agent-bridge.d.ts +12 -5
- package/dist/server/agent-bridge.d.ts.map +1 -1
- package/dist/server/agent-bridge.js +65 -17
- package/dist/server/handlers/paths-home.d.ts +11 -0
- package/dist/server/handlers/paths-home.d.ts.map +1 -0
- package/dist/server/handlers/paths-home.js +10 -0
- package/dist/server/handlers/paths-list.d.ts +24 -0
- package/dist/server/handlers/paths-list.d.ts.map +1 -0
- package/dist/server/handlers/paths-list.js +56 -0
- package/dist/server/handlers/paths-pick-directory.d.ts.map +1 -1
- package/dist/server/handlers/paths-pick-directory.js +2 -1
- package/dist/server/handlers/sessions.d.ts +11 -1
- package/dist/server/handlers/sessions.d.ts.map +1 -1
- package/dist/server/handlers/sessions.js +22 -2
- package/dist/server/handlers/settings.d.ts +18 -1
- package/dist/server/handlers/settings.d.ts.map +1 -1
- package/dist/server/handlers/settings.js +63 -13
- package/dist/server/session-stream.d.ts +65 -37
- package/dist/server/session-stream.d.ts.map +1 -1
- package/dist/server/session-stream.js +525 -119
- package/dist/server/storage.d.ts +3 -0
- package/dist/server/storage.d.ts.map +1 -1
- package/dist/server/storage.js +25 -0
- package/dist/server/wire.d.ts +127 -2
- package/dist/server/wire.d.ts.map +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,12 @@ All notable changes to `@aexol/spectral` are documented here. Format follows
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- Added built-in `web_fetch` tool for fetching public web pages as text,
|
|
11
|
+
Markdown, or raw HTML. Includes SSRF protection, redirect handling, timeout
|
|
12
|
+
and size limits, plus unit and integration tests.
|
|
13
|
+
|
|
8
14
|
## [0.9.40] - 2026-06-22
|
|
9
15
|
|
|
10
16
|
## [0.9.39] - 2026-06-22
|
package/README.md
CHANGED
|
@@ -151,6 +151,49 @@ sends it anywhere. Note that `spectral serve` does **not** use this file — it
|
|
|
151
151
|
routes inference through the backend proxy instead (see
|
|
152
152
|
[How inference is routed](#how-inference-is-routed)).
|
|
153
153
|
|
|
154
|
+
## Web Fetch tool
|
|
155
|
+
|
|
156
|
+
Spectral ships a built-in `web_fetch` tool (enabled by default) for fetching
|
|
157
|
+
public web pages and returning their content as plain text, Markdown, or raw
|
|
158
|
+
HTML. It is intended for quickly reading static pages, documentation, GitHub
|
|
159
|
+
repositories, JSON endpoints, and other public HTTP/HTTPS resources.
|
|
160
|
+
|
|
161
|
+
### Parameters
|
|
162
|
+
|
|
163
|
+
| Parameter | Type | Default | Description |
|
|
164
|
+
| --------- | -------- | ---------- | --------------------------------------------------------------------------- |
|
|
165
|
+
| `url` | string | required | Public URL to fetch. Must begin with `http://` or `https://`. |
|
|
166
|
+
| `format` | string | `markdown` | Output format: `text`, `markdown`, or `html`. |
|
|
167
|
+
| `timeout` | number | `30` | Request timeout in seconds, clamped to the range `1`–`120`. |
|
|
168
|
+
|
|
169
|
+
### When to use `web_fetch`
|
|
170
|
+
|
|
171
|
+
Use `web_fetch` when you only need to **read** publicly available content:
|
|
172
|
+
|
|
173
|
+
- Static documentation sites and README pages.
|
|
174
|
+
- GitHub file, blob, or raw URLs.
|
|
175
|
+
- Public JSON/XML APIs (e.g., Reddit JSON endpoints).
|
|
176
|
+
- Simple marketing pages or blogs that render their main content server-side.
|
|
177
|
+
|
|
178
|
+
`web_fetch` is fast, lightweight, and does not require a browser. It applies
|
|
179
|
+
SSRF protection by rejecting `file://`, `ftp://`, `localhost`, and private IP
|
|
180
|
+
addresses, and it limits responses to 5 MB.
|
|
181
|
+
|
|
182
|
+
### When to use the Browser extension instead
|
|
183
|
+
|
|
184
|
+
Use the full **Browser Automation** extension (also enabled by default) when the
|
|
185
|
+
page requires a real browser:
|
|
186
|
+
|
|
187
|
+
- JavaScript-rendered SPAs where the markup is produced in the browser.
|
|
188
|
+
- Pages that need interaction (clicks, scrolling, form input) before the
|
|
189
|
+
relevant content appears.
|
|
190
|
+
- Sites that block simple HTTP clients or require cookies / session state.
|
|
191
|
+
- Screenshots, visual verification, or network-traffic inspection.
|
|
192
|
+
|
|
193
|
+
The Browser extension controls a real Chromium instance via Playwright and can
|
|
194
|
+
do everything `web_fetch` can, plus interaction, screenshots, and JavaScript
|
|
195
|
+
evaluation.
|
|
196
|
+
|
|
154
197
|
## Multiple machines
|
|
155
198
|
|
|
156
199
|
You can run `spectral serve` on as many machines as you like under one team —
|
package/dist/agent/index.d.ts
CHANGED
|
@@ -18,14 +18,19 @@
|
|
|
18
18
|
*
|
|
19
19
|
* Subagents use an in-process `agentLoop()` for:
|
|
20
20
|
* - Clean context isolation (fresh messages array)
|
|
21
|
-
* - Combined system prompt (
|
|
21
|
+
* - Combined system prompt (shared/base prompt + subagent-specific instructions)
|
|
22
22
|
* - Tool filtering (only the tools specified in agent config)
|
|
23
23
|
* - Model selection (session model preferred, agent-configured as fallback)
|
|
24
24
|
* - Signal propagation (abort passthrough)
|
|
25
25
|
* - Access to observational memory via the recall tool
|
|
26
26
|
*/
|
|
27
27
|
import { type ExtensionAPI } from "../sdk/coding-agent/index.js";
|
|
28
|
+
import { type AgentConfig } from "./agents.js";
|
|
29
|
+
import type { ServerEvent } from "../server/wire.ts";
|
|
30
|
+
export declare function stripInjectedPrimaryAgentPrompt(systemPrompt: string | undefined): string | undefined;
|
|
31
|
+
export declare function buildSubagentSystemPrompt(baseSystemPrompt: string | undefined, agent: Pick<AgentConfig, "name" | "systemPrompt">): string;
|
|
28
32
|
export default function subagentExtension(ext: ExtensionAPI, opts?: {
|
|
29
33
|
disabledAgents?: string[];
|
|
34
|
+
emitSubagentEvent?: (event: ServerEvent) => void;
|
|
30
35
|
}): void;
|
|
31
36
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/agent/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/agent/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAYH,OAAO,EACN,KAAK,YAAY,EAEjB,MAAM,8BAA8B,CAAC;AAGtC,OAAO,EAAE,KAAK,WAAW,EAAmC,MAAM,aAAa,CAAC;AAEhF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAsFrD,wBAAgB,+BAA+B,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CASpG;AAED,wBAAgB,yBAAyB,CACxC,gBAAgB,EAAE,MAAM,GAAG,SAAS,EACpC,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,cAAc,CAAC,GAC/C,MAAM,CAKR;AAkdD,MAAM,CAAC,OAAO,UAAU,iBAAiB,CACxC,GAAG,EAAE,YAAY,EACjB,IAAI,CAAC,EAAE;IAAE,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAA;CAAE,GACpF,IAAI,CAgZN"}
|
package/dist/agent/index.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*
|
|
19
19
|
* Subagents use an in-process `agentLoop()` for:
|
|
20
20
|
* - Clean context isolation (fresh messages array)
|
|
21
|
-
* - Combined system prompt (
|
|
21
|
+
* - Combined system prompt (shared/base prompt + subagent-specific instructions)
|
|
22
22
|
* - Tool filtering (only the tools specified in agent config)
|
|
23
23
|
* - Model selection (session model preferred, agent-configured as fallback)
|
|
24
24
|
* - Signal propagation (abort passthrough)
|
|
@@ -40,6 +40,8 @@ import { wrapToolDefinition } from "../sdk/coding-agent/core/tools/tool-definiti
|
|
|
40
40
|
// ---------------------------------------------------------------------------
|
|
41
41
|
const MAX_PARALLEL_TASKS = 8;
|
|
42
42
|
const MAX_CONCURRENCY = 4;
|
|
43
|
+
const PRIMARY_AGENT_PROMPT_HEADER = "# Primary Agent:";
|
|
44
|
+
const PRIMARY_AGENT_PROMPT_DELIMITER = "\n\n---\n\n";
|
|
43
45
|
// ---------------------------------------------------------------------------
|
|
44
46
|
// Helpers — formatting
|
|
45
47
|
// ---------------------------------------------------------------------------
|
|
@@ -73,6 +75,52 @@ function formatUsageStats(usage, model) {
|
|
|
73
75
|
parts.push(model);
|
|
74
76
|
return parts.join(" ");
|
|
75
77
|
}
|
|
78
|
+
function isRecord(value) {
|
|
79
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
80
|
+
}
|
|
81
|
+
function textFromToolResultContent(value) {
|
|
82
|
+
if (!isRecord(value) || !Array.isArray(value.content))
|
|
83
|
+
return undefined;
|
|
84
|
+
const parts = [];
|
|
85
|
+
for (const part of value.content) {
|
|
86
|
+
if (isRecord(part) && part.type === "text" && typeof part.text === "string") {
|
|
87
|
+
parts.push(part.text);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const text = parts.join("").trim();
|
|
91
|
+
return text ? text : undefined;
|
|
92
|
+
}
|
|
93
|
+
function compactProgressMessage(value) {
|
|
94
|
+
const text = typeof value === "string" ? value.trim() : textFromToolResultContent(value);
|
|
95
|
+
if (text)
|
|
96
|
+
return text.length > 240 ? `${text.slice(0, 237)}...` : text;
|
|
97
|
+
if (isRecord(value) && typeof value.message === "string" && value.message.trim()) {
|
|
98
|
+
const message = value.message.trim();
|
|
99
|
+
return message.length > 240 ? `${message.slice(0, 237)}...` : message;
|
|
100
|
+
}
|
|
101
|
+
if (isRecord(value) && typeof value.error === "string" && value.error.trim()) {
|
|
102
|
+
const error = value.error.trim();
|
|
103
|
+
return error.length > 240 ? `${error.slice(0, 237)}...` : error;
|
|
104
|
+
}
|
|
105
|
+
return value === undefined ? undefined : "Tool progress updated";
|
|
106
|
+
}
|
|
107
|
+
export function stripInjectedPrimaryAgentPrompt(systemPrompt) {
|
|
108
|
+
if (!systemPrompt)
|
|
109
|
+
return systemPrompt;
|
|
110
|
+
if (!systemPrompt.startsWith(PRIMARY_AGENT_PROMPT_HEADER))
|
|
111
|
+
return systemPrompt;
|
|
112
|
+
const delimiterIndex = systemPrompt.indexOf(PRIMARY_AGENT_PROMPT_DELIMITER);
|
|
113
|
+
if (delimiterIndex === -1)
|
|
114
|
+
return undefined;
|
|
115
|
+
const basePrompt = systemPrompt.slice(delimiterIndex + PRIMARY_AGENT_PROMPT_DELIMITER.length);
|
|
116
|
+
return basePrompt.trim().length > 0 ? basePrompt : undefined;
|
|
117
|
+
}
|
|
118
|
+
export function buildSubagentSystemPrompt(baseSystemPrompt, agent) {
|
|
119
|
+
const sharedSystemPrompt = stripInjectedPrimaryAgentPrompt(baseSystemPrompt);
|
|
120
|
+
return sharedSystemPrompt
|
|
121
|
+
? `${sharedSystemPrompt}\n\n---\n\nSubagent: ${agent.name}\n${agent.systemPrompt}`
|
|
122
|
+
: agent.systemPrompt;
|
|
123
|
+
}
|
|
76
124
|
// ---------------------------------------------------------------------------
|
|
77
125
|
// Helpers — message extraction
|
|
78
126
|
// ---------------------------------------------------------------------------
|
|
@@ -176,7 +224,7 @@ async function mapWithConcurrencyLimit(items, concurrency, fn) {
|
|
|
176
224
|
// ---------------------------------------------------------------------------
|
|
177
225
|
// Core — run a single subagent in-process via agentLoop
|
|
178
226
|
// ---------------------------------------------------------------------------
|
|
179
|
-
async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails, ext, ctx) {
|
|
227
|
+
async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails, parentToolCallId, onInternalEvent, ext, ctx) {
|
|
180
228
|
const normalizedAgentName = agentName.replace(/^@/, "");
|
|
181
229
|
const agent = agents.find((a) => a.name === normalizedAgentName);
|
|
182
230
|
if (!agent) {
|
|
@@ -192,11 +240,9 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, si
|
|
|
192
240
|
step,
|
|
193
241
|
};
|
|
194
242
|
}
|
|
195
|
-
// Build combined system prompt
|
|
196
|
-
|
|
197
|
-
const combinedPrompt =
|
|
198
|
-
? `${mainSystemPrompt}\n\n---\n\nSubagent: ${agent.name}\n${agent.systemPrompt}`
|
|
199
|
-
: agent.systemPrompt;
|
|
243
|
+
// Build combined system prompt without leaking an injected primary-agent
|
|
244
|
+
// block into the isolated subagent context.
|
|
245
|
+
const combinedPrompt = buildSubagentSystemPrompt(ctx.getSystemPrompt(), agent);
|
|
200
246
|
// Apply system prompt mutation if enabled in settings (opt-in)
|
|
201
247
|
let effectivePrompt = combinedPrompt;
|
|
202
248
|
if (ctx.settingsManager) {
|
|
@@ -259,11 +305,19 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, si
|
|
|
259
305
|
};
|
|
260
306
|
// Accumulated streaming text from text_delta events (reset per assistant turn).
|
|
261
307
|
let streamingText = "";
|
|
308
|
+
// Accumulated thinking deltas for the live subagent sheet. This stays outside
|
|
309
|
+
// final `SingleResult` so the public subagent tool_result contract is unchanged.
|
|
310
|
+
let thinkingText = "";
|
|
262
311
|
const emitUpdate = () => {
|
|
263
312
|
if (onUpdate) {
|
|
313
|
+
const liveResult = {
|
|
314
|
+
...currentResult,
|
|
315
|
+
...(streamingText ? { streamingText } : {}),
|
|
316
|
+
...(thinkingText ? { thinkingText } : {}),
|
|
317
|
+
};
|
|
264
318
|
onUpdate({
|
|
265
|
-
content: [{ type: "text", text: getFinalOutput(currentResult.messages) || streamingText || "(running...)" }],
|
|
266
|
-
details: makeDetails([
|
|
319
|
+
content: [{ type: "text", text: getFinalOutput(currentResult.messages) || streamingText || (thinkingText ? "(thinking...)" : "(running...)") }],
|
|
320
|
+
details: makeDetails([liveResult]),
|
|
267
321
|
});
|
|
268
322
|
}
|
|
269
323
|
};
|
|
@@ -288,14 +342,19 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, si
|
|
|
288
342
|
hadError = true;
|
|
289
343
|
break;
|
|
290
344
|
}
|
|
345
|
+
onInternalEvent?.(event, parentToolCallId);
|
|
291
346
|
if (event.type === "message_update" && event.message?.role === "assistant") {
|
|
292
|
-
// Forward streaming text deltas so the UI can show the
|
|
293
|
-
//
|
|
347
|
+
// Forward streaming text/thinking deltas so the UI can show the
|
|
348
|
+
// subagent process as it's generated, not just the final result.
|
|
294
349
|
const inner = event.assistantMessageEvent;
|
|
295
350
|
if (inner?.type === "text_delta" && inner.delta) {
|
|
296
351
|
streamingText += inner.delta;
|
|
297
352
|
emitUpdate();
|
|
298
353
|
}
|
|
354
|
+
else if (inner?.type === "thinking_delta" && inner.delta) {
|
|
355
|
+
thinkingText += inner.delta;
|
|
356
|
+
emitUpdate();
|
|
357
|
+
}
|
|
299
358
|
continue;
|
|
300
359
|
}
|
|
301
360
|
if (event.type === "message_end" && event.message) {
|
|
@@ -451,6 +510,66 @@ export default function subagentExtension(ext, opts) {
|
|
|
451
510
|
projectAgentsDir: discovery.projectAgentsDir,
|
|
452
511
|
results,
|
|
453
512
|
});
|
|
513
|
+
// Forward internal subagent tool lifecycle events to the bridge so the
|
|
514
|
+
// frontend sheet can show a live activity log. text_delta streaming
|
|
515
|
+
// still flows through the existing onUpdate/subagent_progress path.
|
|
516
|
+
const emitSubagentEvent = opts?.emitSubagentEvent;
|
|
517
|
+
const subagentStepStartedAt = new Map();
|
|
518
|
+
const subagentStepArgs = new Map();
|
|
519
|
+
const emitSubagentToolProgress = emitSubagentEvent
|
|
520
|
+
? (event, parentToolCallId) => {
|
|
521
|
+
if (event.type === "tool_execution_start") {
|
|
522
|
+
const startedAt = Date.now();
|
|
523
|
+
subagentStepStartedAt.set(event.toolCallId, startedAt);
|
|
524
|
+
subagentStepArgs.set(event.toolCallId, event.args);
|
|
525
|
+
emitSubagentEvent({
|
|
526
|
+
type: "subagent_tool_progress",
|
|
527
|
+
toolCallId: parentToolCallId,
|
|
528
|
+
stepId: event.toolCallId,
|
|
529
|
+
name: event.toolName,
|
|
530
|
+
args: event.args,
|
|
531
|
+
status: "running",
|
|
532
|
+
startedAt,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
else if (event.type === "tool_execution_update") {
|
|
536
|
+
const startedAt = subagentStepStartedAt.get(event.toolCallId) ?? Date.now();
|
|
537
|
+
subagentStepStartedAt.set(event.toolCallId, startedAt);
|
|
538
|
+
subagentStepArgs.set(event.toolCallId, event.args);
|
|
539
|
+
emitSubagentEvent({
|
|
540
|
+
type: "subagent_tool_progress",
|
|
541
|
+
toolCallId: parentToolCallId,
|
|
542
|
+
stepId: event.toolCallId,
|
|
543
|
+
name: event.toolName,
|
|
544
|
+
args: event.args,
|
|
545
|
+
status: "running",
|
|
546
|
+
partialResult: event.partialResult,
|
|
547
|
+
message: compactProgressMessage(event.partialResult),
|
|
548
|
+
startedAt,
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
else if (event.type === "tool_execution_end") {
|
|
552
|
+
const startedAt = subagentStepStartedAt.get(event.toolCallId) ?? Date.now();
|
|
553
|
+
const finishedAt = Date.now();
|
|
554
|
+
const args = subagentStepArgs.get(event.toolCallId);
|
|
555
|
+
subagentStepStartedAt.delete(event.toolCallId);
|
|
556
|
+
subagentStepArgs.delete(event.toolCallId);
|
|
557
|
+
emitSubagentEvent({
|
|
558
|
+
type: "subagent_tool_progress",
|
|
559
|
+
toolCallId: parentToolCallId,
|
|
560
|
+
stepId: event.toolCallId,
|
|
561
|
+
name: event.toolName,
|
|
562
|
+
args,
|
|
563
|
+
status: event.isError ? "error" : "success",
|
|
564
|
+
result: event.result,
|
|
565
|
+
isError: event.isError,
|
|
566
|
+
message: event.isError ? compactProgressMessage(event.result) : undefined,
|
|
567
|
+
startedAt,
|
|
568
|
+
finishedAt,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
: undefined;
|
|
454
573
|
if (modeCount !== 1) {
|
|
455
574
|
const available = agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
|
|
456
575
|
return {
|
|
@@ -513,7 +632,7 @@ export default function subagentExtension(ext, opts) {
|
|
|
513
632
|
}
|
|
514
633
|
}
|
|
515
634
|
: undefined;
|
|
516
|
-
const result = await runSingleAgent(ctx.cwd, agents, step.agent, taskWithContext, step.cwd, i + 1, signal, chainUpdate, makeDetails("chain"), ext, ctx);
|
|
635
|
+
const result = await runSingleAgent(ctx.cwd, agents, step.agent, taskWithContext, step.cwd, i + 1, signal, chainUpdate, makeDetails("chain"), _toolCallId, emitSubagentToolProgress, ext, ctx);
|
|
517
636
|
results.push(result);
|
|
518
637
|
const isError = result.exitCode !== 0 ||
|
|
519
638
|
result.stopReason === "error" ||
|
|
@@ -591,7 +710,7 @@ export default function subagentExtension(ext, opts) {
|
|
|
591
710
|
allResults[index] = partial.details.results[0];
|
|
592
711
|
emitParallelUpdate();
|
|
593
712
|
}
|
|
594
|
-
}, makeDetails("parallel"), ext, ctx);
|
|
713
|
+
}, makeDetails("parallel"), _toolCallId, emitSubagentToolProgress, ext, ctx);
|
|
595
714
|
allResults[index] = result;
|
|
596
715
|
emitParallelUpdate();
|
|
597
716
|
return result;
|
|
@@ -614,7 +733,7 @@ export default function subagentExtension(ext, opts) {
|
|
|
614
733
|
}
|
|
615
734
|
// ── Single mode ─────────────────────────────────────────────
|
|
616
735
|
if (params.agent && params.task) {
|
|
617
|
-
const result = await runSingleAgent(ctx.cwd, agents, params.agent, params.task, params.cwd, undefined, signal, onUpdate, makeDetails("single"), ext, ctx);
|
|
736
|
+
const result = await runSingleAgent(ctx.cwd, agents, params.agent, params.task, params.cwd, undefined, signal, onUpdate, makeDetails("single"), _toolCallId, emitSubagentToolProgress, ext, ctx);
|
|
618
737
|
const isError = result.exitCode !== 0 ||
|
|
619
738
|
result.stopReason === "error" ||
|
|
620
739
|
result.stopReason === "aborted";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Fetch native extension for Spectral.
|
|
3
|
+
*
|
|
4
|
+
* Registers a single tool, `web_fetch`, which fetches a public URL and returns
|
|
5
|
+
* its content as text, markdown, or raw HTML. The model provides URLs itself;
|
|
6
|
+
* there is no search capability.
|
|
7
|
+
*/
|
|
8
|
+
import type { ExtensionAPI } from "../../sdk/coding-agent/index.js";
|
|
9
|
+
export default function webExtension(ext: ExtensionAPI): Promise<void>;
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/extensions/web/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAGpE,wBAA8B,YAAY,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ3E"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { registerWebFetchTool } from "./tools/fetch.js";
|
|
2
|
+
export default async function webExtension(ext) {
|
|
3
|
+
try {
|
|
4
|
+
registerWebFetchTool(ext);
|
|
5
|
+
process.stderr.write("[web] Registered web_fetch tool.\n");
|
|
6
|
+
}
|
|
7
|
+
catch (err) {
|
|
8
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
9
|
+
process.stderr.write(`[web] Failed to register web_fetch tool: ${msg}\n`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../../../src/extensions/web/tools/fetch.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAmB,YAAY,EAAkB,MAAM,oCAAoC,CAAC;AA+BxG,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CA2D5D"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* web_fetch tool definition.
|
|
3
|
+
*/
|
|
4
|
+
import { Type } from "typebox";
|
|
5
|
+
import { truncateTail } from "../../../sdk/coding-agent/core/tools/truncate.js";
|
|
6
|
+
import { fetchWebPage, WebFetchError } from "../utils/fetcher.js";
|
|
7
|
+
import { convertContent } from "../utils/parser.js";
|
|
8
|
+
const MAX_OUTPUT_LINES = 4000;
|
|
9
|
+
const MAX_OUTPUT_BYTES = 200 * 1024; // 200 KB
|
|
10
|
+
const webFetchSchema = Type.Object({
|
|
11
|
+
url: Type.String({
|
|
12
|
+
description: "The URL to fetch. Must begin with http:// or https://.",
|
|
13
|
+
pattern: "^https?://",
|
|
14
|
+
}),
|
|
15
|
+
format: Type.Optional(Type.Union([Type.Literal("text"), Type.Literal("markdown"), Type.Literal("html"), Type.Literal("json")], {
|
|
16
|
+
default: "markdown",
|
|
17
|
+
description: "Output format: text, markdown (default), html, or json.",
|
|
18
|
+
})),
|
|
19
|
+
timeout: Type.Optional(Type.Number({
|
|
20
|
+
default: 30,
|
|
21
|
+
minimum: 1,
|
|
22
|
+
maximum: 120,
|
|
23
|
+
description: "Request timeout in seconds (default 30, maximum 120).",
|
|
24
|
+
})),
|
|
25
|
+
});
|
|
26
|
+
export function registerWebFetchTool(ext) {
|
|
27
|
+
const tool = {
|
|
28
|
+
name: "web_fetch",
|
|
29
|
+
label: "Web Fetch",
|
|
30
|
+
description: "Fetch a public URL and return its content as readable text, markdown, raw HTML, or pretty JSON. " +
|
|
31
|
+
"Useful for reading documentation, GitHub pages, Reddit JSON endpoints, and other web content. " +
|
|
32
|
+
"Supports HTTP/HTTPS only and rejects binary content.",
|
|
33
|
+
promptSnippet: "`web_fetch { url, format?, timeout? }` — fetch a web page as markdown/text/HTML/JSON",
|
|
34
|
+
parameters: webFetchSchema,
|
|
35
|
+
async execute(_toolCallId, params, signal, _onUpdate, _ctx) {
|
|
36
|
+
const { url, format = "markdown", timeout = 30 } = params;
|
|
37
|
+
try {
|
|
38
|
+
const fetchResult = await fetchWebPage(url, {
|
|
39
|
+
timeoutSeconds: timeout,
|
|
40
|
+
signal,
|
|
41
|
+
});
|
|
42
|
+
const converted = await convertContent(fetchResult.content, format);
|
|
43
|
+
const truncation = truncateTail(converted, {
|
|
44
|
+
maxLines: MAX_OUTPUT_LINES,
|
|
45
|
+
maxBytes: MAX_OUTPUT_BYTES,
|
|
46
|
+
});
|
|
47
|
+
const output = truncation.content;
|
|
48
|
+
const truncationNotice = truncation.truncated
|
|
49
|
+
? `\n\n[Output truncated: exceeded ${truncation.truncatedBy === "bytes" ? `${MAX_OUTPUT_BYTES} bytes` : `${MAX_OUTPUT_LINES} lines`}. Original size: ${truncation.totalBytes} bytes, ${truncation.totalLines} lines.]`
|
|
50
|
+
: "";
|
|
51
|
+
return {
|
|
52
|
+
content: [
|
|
53
|
+
{
|
|
54
|
+
type: "text",
|
|
55
|
+
text: `✓ Fetched ${fetchResult.finalUrl}\nContent-Type: ${fetchResult.contentType || "unknown"}\nFormat: ${format}\nSize: ${fetchResult.contentLengthBytes} bytes${truncationNotice}\n\n${output}`,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
details: {
|
|
59
|
+
isError: false,
|
|
60
|
+
url: fetchResult.finalUrl,
|
|
61
|
+
contentType: fetchResult.contentType,
|
|
62
|
+
format,
|
|
63
|
+
output,
|
|
64
|
+
truncated: truncation.truncated,
|
|
65
|
+
truncatedBy: truncation.truncatedBy,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
const reason = err instanceof WebFetchError || err instanceof Error ? err.message : String(err);
|
|
71
|
+
const message = `Unable to fetch ${url}: ${reason}`;
|
|
72
|
+
return {
|
|
73
|
+
content: [{ type: "text", text: `❌ ${message}` }],
|
|
74
|
+
details: { isError: true, message },
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
ext.registerTool(tool);
|
|
80
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export declare class WebFetchError extends Error {
|
|
2
|
+
constructor(message: string);
|
|
3
|
+
}
|
|
4
|
+
export interface WebFetchOptions {
|
|
5
|
+
timeoutSeconds?: number;
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
userAgent?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Bypass SSRF private-host/IP blocking. Used only by tests that spin up a
|
|
10
|
+
* local mock HTTP server; never exposed through the `web_fetch` tool schema.
|
|
11
|
+
*/
|
|
12
|
+
allowPrivateHosts?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface WebFetchResult {
|
|
15
|
+
url: string;
|
|
16
|
+
finalUrl: string;
|
|
17
|
+
contentType: string;
|
|
18
|
+
content: string;
|
|
19
|
+
contentLengthBytes: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Validate a URL with SSRF protection.
|
|
23
|
+
*
|
|
24
|
+
* Throws WebFetchError for invalid URLs, non-http(s) protocols, blocked
|
|
25
|
+
* hostnames, or hostnames that resolve to private IP addresses.
|
|
26
|
+
*/
|
|
27
|
+
export declare function validateUrl(urlStr: string, allowPrivateHosts?: boolean): Promise<URL>;
|
|
28
|
+
/** Validate that the Content-Type is readable text, not binary media. */
|
|
29
|
+
export declare function validateContentType(contentType: string): void;
|
|
30
|
+
/**
|
|
31
|
+
* Fetch a public URL and return its decoded text content.
|
|
32
|
+
*/
|
|
33
|
+
export declare function fetchWebPage(url: string, opts?: WebFetchOptions): Promise<WebFetchResult>;
|
|
34
|
+
//# sourceMappingURL=fetcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetcher.d.ts","sourceRoot":"","sources":["../../../../src/extensions/web/utils/fetcher.ts"],"names":[],"mappings":"AASA,qBAAa,aAAc,SAAQ,KAAK;gBAC1B,OAAO,EAAE,MAAM;CAI5B;AAED,MAAM,WAAW,eAAe;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAuDD;;;;;GAKG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,MAAM,EACd,iBAAiB,UAAQ,GACxB,OAAO,CAAC,GAAG,CAAC,CAoDd;AAcD,yEAAyE;AACzE,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CA+C7D;AAED;;GAEG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,MAAM,EACX,IAAI,GAAE,eAAoB,GACzB,OAAO,CAAC,cAAc,CAAC,CAoIzB"}
|