@fadouse/pi-web
v0.5.0
Published
Configurable OpenAI or Exa web search with isolated summaries and Impit-only multi-format fetch artifacts.
Maintainers
Readme
@fadouse/pi-web
A Pi extension that provides three selectable web-search pipelines, pageable artifacts, and Impit-only multi-format resource fetching.
Main Agent
├─ web_search
│ ├─ OpenAI Direct → /alpha/search → native report
│ ├─ OpenAI + Summary → /alpha/search → isolated per-result summaries
│ └─ Exa Direct → Exa /search → compact excerpts
├─ web_fetch(url, format, offset, limit)
└─ web_fetch_read(fetch_id, format, offset, limit)Design goals
- The main Agent calls search directly; no Search Agent or Agent Session is created or dispatched.
- OpenAI Standalone Search sends exactly one
/alpha/searchPOST per tool call, with retries explicitly disabled. - Summary mode places every result in a fresh, single-message completion context. Up to four summaries run concurrently without sharing context.
- Exa mode does not call a summary model. It deterministically returns titles, compact excerpts, exact result IDs, and direct URLs.
- Each search mode has its own tool prompt. The main Agent sees only the currently available capabilities—not plugin configuration, backend composition, or model settings.
- Complete reports and raw responses are stored as separate artifacts. Search reports expose a local
report_pathfor Pi's built-inread; fetched content remains pageable throughweb_fetch_read. web_fetchuses Impit exclusively. It does not execute JavaScript, launch a browser, or fall back to another fetch backend.- Before a model request, successful Web tool bodies from older user turns are replaced with compact receipts. Session entries and on-disk artifacts remain unchanged.
Installation
pi install /path/to/pi-webFor development:
npm install
pi -e ./src/index.tsNode.js 22.19 or later is required.
Search configuration
Run:
/web-configChoose one of three pipelines:
- OpenAI direct (no summary) — returns
/alpha/searchoutput directly and requires no summary model. - OpenAI + summary model — splits
/alpha/searchoutput and summarizes each result in an independent completion. - Exa API (direct results) — calls the Exa Search API directly and uses no summary model.
Global configuration is stored at ~/.pi/agent/pi-web.json. Trusted projects may instead use .pi/pi-web.json.
OpenAI Direct
{
"provider": "openai-codex",
"model": "<standalone-search-model>",
"mode": "cached",
"search_context_size": "medium",
"allowed_domains": ["example.com"],
"max_output_tokens": 10000,
"timeout_ms": 300000
}OpenAI + Summary
{
"provider": "openai-codex",
"model": "<standalone-search-model>",
"summary_provider": "anthropic",
"summary_model": "<summary-model>",
"mode": "live",
"timeout_ms": 300000
}summary_provider and summary_model must either both be present or both be omitted.
Exa Direct
{
"provider": "exa",
"exa_api_key": "<your-exa-api-key>",
"mode": "live",
"allowed_domains": ["example.com"],
"timeout_ms": 300000
}When selected, the Exa key is stored in plain text inside pi-web.json. The extension forces file mode 0600, and /web-config show displays only [configured] instead of the key. Never commit a project-level .pi/pi-web.json file.
Configuration precedence:
environment > trusted project config > global configPrimary environment variables:
PI_WEB_PROVIDER=openai-codex
PI_WEB_MODEL='<standalone-search-model>'
PI_WEB_SUMMARY_PROVIDER=anthropic
PI_WEB_SUMMARY_MODEL='<summary-model>'
PI_WEB_SEARCH_MODE=cached
# Exa
PI_WEB_PROVIDER=exa
EXA_API_KEY='<exa-api-key>'Inspect configuration and paths with:
/web-config show
/web-config pathsBoth OpenAI stages reuse Pi's ModelRegistry authentication. Running /web-config switches the active tool description immediately. After editing a configuration file manually, run /reload.
web_search
Minimal search:
{
"search_query": [{ "q": "latest stable release" }]
}Batched search:
{
"search_query": [
{ "q": "official release notes", "domains": ["github.com"] },
{ "q": "official migration guide", "recency": 30 }
],
"response_length": "medium"
}| Capability | OpenAI Direct | OpenAI + Summary | Exa Direct |
|---|:---:|:---:|:---:|
| search_query | ✓ | ✓ | ✓ |
| image_query | ✓ | ✓ | — |
| open / click / find | ✓ | ✓ | — |
| screenshot | ✓ | ✓ | — |
| finance / weather / sports / time | ✓ | ✓ | — |
To inspect an Exa result in depth, pass its direct URL to web_fetch. Unsupported operations are rejected instead of being silently mapped to different semantics.
Exa result limits
response_length controls one result budget shared by the entire tool call, not a separate budget for each query:
| Value | Maximum results per call |
|---|---:|
| short | 5 |
| medium (default) | 10 |
| long | 15 |
For example, four long queries receive quotas of 4 + 4 + 4 + 3 = 15; they do not return 60 results. Exa requests only query-focused highlights of up to 1,200 characters, never complete page text. Duplicate URLs are removed during aggregation.
OpenAI + Summary
- The main Agent emits a structured search command.
- The extension sends one direct
/alpha/searchPOST. - The raw output is split into N results.
- Each result enters an independent, single-message completion, with up to four running concurrently.
- The extension deterministically appends the exact Source ID and direct URL.
- Results are merged in their original order, and only the complete summary report is returned to the main Agent.
If one summary request times out, that item becomes **Error: timeout** while retaining its Source ID and URL. Every other result continues normally. User cancellation and non-timeout errors still propagate.
Search artifacts
report.md # complete report returned to the main Agent
raw-search.txt # raw OpenAI or Exa response
metadata.json # pipeline, provider/model, IDs, counts, hashes, and usageweb_search returns the absolute report_path. After historical content is cleared from active context, read the stored report with Pi's built-in read tool:
{
"path": "/absolute/path/to/ws_.../report.md",
"offset": 81,
"limit": 80
}No additional search-artifact tool is required.
web_fetch
{
"url": "https://example.com/resource",
"offset": 1,
"limit": 80
}When format is omitted, HTML/XHTML responses default to readable Markdown. Other textual formats preserve their source form, while images and binary resources return an absolute local path.
Available explicit format values:
source— preserves textual source formats; binary resources return an absolute local path.text— returns locally extracted text from HTML, PDF, Office, and similar documents.markdown— returns normalized Markdown when it can be generated.html— compatibility alias for legacy HTML/source-view calls.
Supported content includes:
- HTML/XHTML, Markdown, plain text, and common source-code or configuration formats.
- JSON/JSONL, XML/RSS/Atom, CSV/TSV, YAML, and TOML.
- PDF, DOCX, PPTX, XLSX, ODT, ODP, ODS, RTF, and EPUB.
- JPEG, PNG, GIF, WebP, BMP, TIFF, SVG, AVIF, HEIC, and other common image formats.
- Any other binary content, which is still downloaded and exposed through a local path.
PDF, Office, and OpenDocument files are parsed locally from downloaded bytes with officeparser. Parsing does not make network requests or launch external programs, and both OCR and attachment extraction are explicitly disabled. Images are not OCR-processed. They are stored with their real extension, for example:
~/.pi/agent/cache/pi-web/<session>/wf_.../original.pngThe main Agent can read the image from that path. Unknown binary content also preserves its exact original bytes and is never misrepresented as text.
Depending on the source, a successful fetch stores:
original.<ext> # exact bytes returned by Impit
source.<ext> # decoded source-format text, when available
content.txt # extracted plain text, when available
content.md # generated Markdown, when available
metadata.json # MIME type, format, paths, redirects, sizes, and SHA-256 hashesContinue reading the same immutable snapshot with:
{
"fetch_id": "wf_...",
"format": "text",
"offset": 201,
"limit": 80
}web_fetch_read never performs another network request.
Pagination limits
web_fetch and web_fetch_read share the following extension-level pagination limits:
| Limit | Value | |---|---:| | Default lines | 80 | | Maximum lines per call | 200 | | Hard body limit per page | 8 KiB | | Maximum virtual-line size | 2 KiB UTF-8 |
When a limit is reached, the tool returns an exact next_offset and an example continuation call.
Fetching and security
web_fetch performs the following steps:
- Accept only credential-free HTTP(S) URLs and validate the initial address.
- Send a GET through
Impit({ browser: "chrome", vanillaFallback: false }). - Follow at most five redirects manually, repeating DNS and SSRF validation at every hop.
- Stream at most 10 MiB of response data.
- Preserve the original bytes, then classify the content from MIME type, filename, and magic bytes.
- Preserve source views for text, extract documents locally, and return paths for images or unknown binary content.
Additional safeguards:
- Blocks localhost, private networks, metadata endpoints, reserved addresses, IPv4-mapped IPv6 addresses, and URLs containing credentials.
- Resolves and validates every A and AAAA address again after each redirect.
- Uses directory mode
0700and file mode0600for artifacts. - Retains artifacts for seven days by default, with a 256 MiB per-session limit.
- Gives each summary request exactly one untrusted result, no tools, and no shared context.
Development
npm run check
npm pack --dry-runLicense
MIT
