Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Monorepo for @parallel-web npm packages.
- [`@parallel-web/ai-sdk-tools`](./packages/ai-sdk-tools) - AI SDK tools for Parallel Web
- [`@parallel-web/dsh-web-search`](./packages/dsh-web-search) - Parallel Search provider for DeepSeek Harness
- [`@parallel-web/opencode-plugin`](./packages/opencode-plugin) - Opencode plugin for Parallel Web
- [`@parallel-web/pi-extension`](./packages/pi-extension) - pi agent extension for Parallel Web
- [`@parallel-web/pi-extension`](./packages/pi-extension) - Pi web tools and a native Parallel Responses research subagent
- `@parallel-web/oauth` - Internal, unpublished shared PKCE OAuth helper. Bundled into the opencode plugin and pi extension at build time (`noExternal`), so it is never installed by consumers and is intentionally marked `private`.

## Development
Expand Down
85 changes: 81 additions & 4 deletions packages/pi-extension/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# @parallel-web/pi-extension

Pi extension that adds `web_search` and `web_fetch` backed by Parallel.
Pi extension that adds `web_search`, `web_fetch`, and a cited research model
backed by Parallel.

Install it with:
```
Expand All @@ -11,6 +12,10 @@ pi install npm:@parallel-web/pi-extension

- Registers `web_search`
- Registers `web_fetch`
- Registers the `parallel/research` model, which makes one stateless Parallel
Responses API call
- Ships a `parallel-research` agent for
[pi-subagents](https://github.com/nicobailon/pi-subagents)
- Registers a `parallel` auth provider, so Pi's own `/login parallel` runs the
Parallel browser OAuth flow and stores the API key in Pi's auth store
(`auth.json`) alongside every other provider credential
Expand All @@ -21,11 +26,80 @@ Auth resolution order (owned by Pi, not the extension):
1. The credential Pi stored for provider `parallel`
2. `PARALLEL_API_KEY`

`/logout parallel` removes the stored credential, and
`pi auth check --provider parallel` reports whether it is configured.
Run `/parallel-login` inside Pi to check whether Parallel is configured. To
remove a stored credential, run `/logout` and select Parallel. Environment
variables are not affected by Pi's logout flow.

Requires `@earendil-works/pi-coding-agent` 0.83.0 or newer.

## Parallel Research Subagent

Install both packages to add the native research agent:

```bash
pi install npm:pi-subagents
pi install npm:@parallel-web/pi-extension
```

This integration requires pi-subagents 0.50.0 or newer. The rest of the Pi
extension still works without pi-subagents.

Run one research child directly:

```text
/run parallel-research Compare the current JavaScript runtimes in Node and Bun. Cite primary sources.
```

The agent is also an ordinary pi-subagents child in JavaScript code mode. Its
`output` is the cited research text, so a later branch can use it directly:

```javascript
const research = await runs.run("research", {
agent: "parallel-research",
task: "Which JavaScript runtime currently has stronger Node API compatibility? Cite primary sources.",
thinking: "medium",
context: "fresh",
worktree: false
});

if (/Bun/i.test(research.output)) {
return { recommendation: "evaluate-bun", evidence: research.output };
}
return { recommendation: "stay-on-node", evidence: research.output };
```

The default research effort is `medium`. A run may select `low`, `medium`, or
`high` with its `thinking` option. Current
[prices](https://docs.parallel.ai/getting-started/pricing) per successful
response are:

| Thinking | Price | Typical use |
| --- | ---: | --- |
| `low` | $0.01 | Focused lookup |
| `medium` | $0.05 | General research |
| `high` | $0.25 | Hard, high-value research |

The provider makes one `POST /v1/responses` request and does not retry it. It
does not use `previous_response_id`, background jobs, or a remote status loop.
Stopping the child aborts the local HTTP request on a best-effort basis;
Parallel does not expose acknowledged server-side cancellation for Responses.

The research request contains only the packaged agent instructions and the
latest textual child task. It does not send parent history, local files, cwd,
environment variables, Pi tools, session state, or git worktree data. The
agent cannot read or edit the local filesystem. A worktree therefore adds no
research capability and should normally remain disabled.

These data boundaries describe normal `parallel-research` runs. Pi's low-level
provider API also exposes `onPayload` and custom-header hooks to trusted caller
code. A caller that deliberately uses those hooks to replace or extend the
request owns the resulting data boundary.

Parallel Responses accepts at most 20,000 combined instruction and input
characters. The adapter fails before making a request when that boundary is
exceeded. It renders the returned URL citations as a deduplicated Markdown
source list.

## Dogfooding Locally

Build the extension first:
Expand All @@ -50,6 +124,7 @@ If the extension loads successfully, Pi will have:
- the `web_fetch` tool
- `parallel` listed under `/login`
- the `parallel-login` status command
- the `parallel/research` model
- per-session Parallel `session_id` reuse inside that Pi session

### Option 2: Symlink It Into Pi Extensions
Expand Down Expand Up @@ -95,7 +170,7 @@ Inside Pi, run:

That opens the browser for Parallel OAuth. On success, Pi stores the API key in
its auth store under `parallel`. Run `/parallel-login` to see the current status,
and `/logout parallel` to remove the credential.
and run `/logout` and select Parallel to remove the stored credential.

### Use Environment Variable Instead

Expand Down Expand Up @@ -149,4 +224,6 @@ pnpm --filter @parallel-web/pi-extension typecheck
- If automatic callback capture does not complete, the login dialog asks you to paste the callback URL.
- Credential storage is entirely Pi's; the extension only reads the resolved key
through `ctx.modelRegistry.getApiKeyForProvider("parallel")`.
- The research model is stateless and separate from the Search/Extract
`session_id` used by the web tools.
- Skill suppression inside the extension is prompt-level only. If you want a clean dogfooding session without your usual skills list, start Pi with `--no-skills`.
17 changes: 17 additions & 0 deletions packages/pi-extension/agents/parallel-research.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
name: parallel-research
description: One-shot cited web research through Parallel Responses
model: parallel/research
thinking: medium
systemPromptMode: replace
inheritProjectContext: false
inheritSkills: false
defaultContext: fresh
completionGuard: false
turnBudget: {"maxTurns":1,"graceTurns":0}
acceptance: {"level":"none","reason":"One-shot remote research provider"}
---

You are a read-only research agent backed by Parallel Responses.

Research the user's task using current web sources. Return a direct, evidence-based answer with the citations supplied by the provider. Do not claim to inspect local files, run tools, change code, or access the parent session.
12 changes: 10 additions & 2 deletions packages/pi-extension/package.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
{
"name": "@parallel-web/pi-extension",
"version": "1.2.0",
"description": "Add web search and web fetch to your pi agent",
"description": "Add Parallel web tools and cited research to your pi agent",
"author": "Parallel Web",
"license": "MIT",
"type": "module",
"pi": {
"image": "https://assets.parallel.ai/white-parallel-avatar-540.png",
"extensions": [
"./dist/index.js"
]
],
"subagents": {
"agents": [
"./agents"
]
}
},
"files": [
"dist",
"agents",
"package.json",
"README.md"
],
Expand Down Expand Up @@ -41,6 +47,8 @@
"pi agent",
"extension",
"parallel",
"research",
"subagents",
"web",
"search",
"fetch",
Expand Down
8 changes: 8 additions & 0 deletions packages/pi-extension/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,10 @@ describe('@parallel-web/pi-extension', () => {
expect.stringContaining('authenticated (stored)'),
'info'
);
expect(ctx.ui.notify).toHaveBeenCalledWith(
expect.stringContaining('`/logout` and select Parallel'),
'info'
);
});

it('parallel-login should recognize a key that only PARALLEL_API_KEY provides', async () => {
Expand All @@ -260,6 +264,10 @@ describe('@parallel-web/pi-extension', () => {
expect.stringContaining('authenticated (PARALLEL_API_KEY)'),
'info'
);
expect(ctx.ui.notify).toHaveBeenCalledWith(
expect.stringContaining('unset PARALLEL_API_KEY'),
'info'
);
});

it('web_search should use the stored api key when available', async () => {
Expand Down
40 changes: 40 additions & 0 deletions packages/pi-extension/src/__tests__/package.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';

const packageRoot = resolve(
dirname(fileURLToPath(import.meta.url)),
'..',
'..'
);

describe('pi-subagents package contract', () => {
it('ships the Parallel research agent through the Pi manifest', () => {
const manifest = JSON.parse(
readFileSync(resolve(packageRoot, 'package.json'), 'utf8')
);

expect(manifest.name).toBe('@parallel-web/pi-extension');
expect(manifest.version).toBe('1.2.0');
expect(manifest.files).toContain('agents');
expect(manifest.pi.subagents.agents).toEqual(['./agents']);
});

it('pins a one-turn fresh agent to the Parallel research model', () => {
const agent = readFileSync(
resolve(packageRoot, 'agents', 'parallel-research.md'),
'utf8'
);

expect(agent).toContain('name: parallel-research');
expect(agent).toContain('model: parallel/research');
expect(agent).toContain('thinking: medium');
expect(agent).toContain('systemPromptMode: replace');
expect(agent).toContain('inheritProjectContext: false');
expect(agent).toContain('inheritSkills: false');
expect(agent).toContain('defaultContext: fresh');
expect(agent).toContain('completionGuard: false');
expect(agent).toContain('turnBudget: {"maxTurns":1,"graceTurns":0}');
});
});
15 changes: 13 additions & 2 deletions packages/pi-extension/src/__tests__/parallel-auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,23 @@ describe('parallel-auth', () => {
} = await import('../parallel-auth.js'));
});

it('registers a Parallel provider that serves no models', () => {
it('registers a Parallel provider with one Responses research model', () => {
const provider = registerProvider();

expect(provider.id).toBe('parallel');
expect(provider.name).toBe('Parallel');
expect(provider.getModels()).toEqual([]);
expect(provider.getModels()).toEqual([
expect.objectContaining({
id: 'research',
name: 'Parallel Research',
api: 'parallel-responses',
provider: 'parallel',
reasoning: true,
input: ['text'],
contextWindow: 37_000,
maxTokens: 32_000,
}),
]);
expect(provider.auth.apiKey).toBeDefined();
expect(provider.auth.oauth).toBeUndefined();
});
Expand Down
Loading
Loading