From 35726510796c14e5a7e9b8ed0a3fea040c953937 Mon Sep 17 00:00:00 2001 From: George Pickett Date: Mon, 17 Aug 2026 21:05:39 -0700 Subject: [PATCH 1/6] Add DeepSeek Harness Parallel Responses subagent --- .github/workflows/ci.yml | 29 +- .gitignore | 1 + PUBLISHING.md | 10 +- README.md | 1 + .../dsh-responses-subagent/CONTRIBUTING.md | 125 +++++ packages/dsh-responses-subagent/LICENSE | 19 + packages/dsh-responses-subagent/README.md | 137 ++++++ packages/dsh-responses-subagent/SECURITY.md | 31 ++ .../dsh-responses-subagent/cordis.patch.yml | 12 + packages/dsh-responses-subagent/package.json | 91 ++++ .../scripts/check-manifest.mjs | 120 +++++ .../scripts/check-package.mjs | 34 ++ .../scripts/verify-packed-artifact.mjs | 98 ++++ .../scripts/verify-packed-profile.mjs | 85 ++++ .../scripts/verify-profile-overlay.mjs | 84 ++++ packages/dsh-responses-subagent/src/index.ts | 49 ++ .../dsh-responses-subagent/src/provider.ts | 442 ++++++++++++++++++ .../tests/live-responses.e2e.ts | 89 ++++ .../tests/loader-composition.spec.ts | 112 +++++ .../tests/provider.spec.ts | 366 +++++++++++++++ packages/dsh-responses-subagent/tsconfig.json | 25 + .../dsh-responses-subagent/tsdown.config.ts | 23 + .../vitest.e2e.config.ts | 7 + pnpm-lock.yaml | 61 +++ 24 files changed, 2033 insertions(+), 18 deletions(-) create mode 100644 packages/dsh-responses-subagent/CONTRIBUTING.md create mode 100644 packages/dsh-responses-subagent/LICENSE create mode 100644 packages/dsh-responses-subagent/README.md create mode 100644 packages/dsh-responses-subagent/SECURITY.md create mode 100644 packages/dsh-responses-subagent/cordis.patch.yml create mode 100644 packages/dsh-responses-subagent/package.json create mode 100644 packages/dsh-responses-subagent/scripts/check-manifest.mjs create mode 100644 packages/dsh-responses-subagent/scripts/check-package.mjs create mode 100644 packages/dsh-responses-subagent/scripts/verify-packed-artifact.mjs create mode 100644 packages/dsh-responses-subagent/scripts/verify-packed-profile.mjs create mode 100644 packages/dsh-responses-subagent/scripts/verify-profile-overlay.mjs create mode 100644 packages/dsh-responses-subagent/src/index.ts create mode 100644 packages/dsh-responses-subagent/src/provider.ts create mode 100644 packages/dsh-responses-subagent/tests/live-responses.e2e.ts create mode 100644 packages/dsh-responses-subagent/tests/loader-composition.spec.ts create mode 100644 packages/dsh-responses-subagent/tests/provider.spec.ts create mode 100644 packages/dsh-responses-subagent/tsconfig.json create mode 100644 packages/dsh-responses-subagent/tsdown.config.ts create mode 100644 packages/dsh-responses-subagent/vitest.e2e.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8aba3bf..ad9ffd3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,12 +85,13 @@ jobs: run: pnpm build dsh-compatibility: - name: DSH Compatibility (Node ${{ matrix.node }}) + name: DSH Compatibility (${{ matrix.package }}, Node ${{ matrix.node }}) runs-on: ubuntu-latest strategy: fail-fast: false matrix: node: [22.19.0, 24, 26] + package: [dsh-web-search, dsh-responses-subagent] steps: - name: Checkout uses: actions/checkout@v4 @@ -105,10 +106,10 @@ jobs: cache: 'pnpm' - name: Install dependencies - run: pnpm install --filter @parallel-web/dsh-web-search... --frozen-lockfile + run: pnpm install --filter "@parallel-web/${{ matrix.package }}..." --frozen-lockfile - name: Run deterministic plugin checks - run: pnpm --filter @parallel-web/dsh-web-search check + run: pnpm --filter "@parallel-web/${{ matrix.package }}" check - name: Verify packed DSH profile lifecycle shell: bash @@ -118,26 +119,26 @@ jobs: dsh_home="$(mktemp -d)" trap 'rm -rf "$pack_dir" "$dsh_home"' EXIT - pnpm --dir packages/dsh-web-search pack --pack-destination "$pack_dir" + pnpm --dir "packages/${{ matrix.package }}" pack --pack-destination "$pack_dir" artifact="$(find "$pack_dir" -name '*.tgz' -print -quit)" - version="$(node -p "require('./packages/dsh-web-search/package.json').version")" + version="$(node -p "require('./packages/${{ matrix.package }}/package.json').version")" - node packages/dsh-web-search/scripts/verify-packed-artifact.mjs \ + node "packages/${{ matrix.package }}/scripts/verify-packed-artifact.mjs" \ --tarball "$artifact" - DSH_HOME="$dsh_home" pnpm --dir packages/dsh-web-search exec dsh \ + DSH_HOME="$dsh_home" pnpm --dir "packages/${{ matrix.package }}" exec dsh \ --profile web --dump-config > "$dsh_home/before.yml" - DSH_HOME="$dsh_home" pnpm --dir packages/dsh-web-search exec dsh \ + DSH_HOME="$dsh_home" pnpm --dir "packages/${{ matrix.package }}" exec dsh \ plugin --profile web add "$artifact" - DSH_HOME="$dsh_home" pnpm --dir packages/dsh-web-search exec dsh \ + DSH_HOME="$dsh_home" pnpm --dir "packages/${{ matrix.package }}" exec dsh \ --profile web --dump-config > "$dsh_home/after.yml" - node packages/dsh-web-search/scripts/verify-packed-profile.mjs \ + node "packages/${{ matrix.package }}/scripts/verify-packed-profile.mjs" \ --dsh-home "$dsh_home" \ --expected-version "$version" - DSH_HOME="$dsh_home" pnpm --dir packages/dsh-web-search exec dsh \ - plugin --profile web remove @parallel-web/dsh-web-search - DSH_HOME="$dsh_home" pnpm --dir packages/dsh-web-search exec dsh \ + DSH_HOME="$dsh_home" pnpm --dir "packages/${{ matrix.package }}" exec dsh \ + plugin --profile web remove "@parallel-web/${{ matrix.package }}" + DSH_HOME="$dsh_home" pnpm --dir "packages/${{ matrix.package }}" exec dsh \ --profile web --dump-config > "$dsh_home/removed.yml" - node packages/dsh-web-search/scripts/verify-profile-overlay.mjs \ + node "packages/${{ matrix.package }}/scripts/verify-profile-overlay.mjs" \ --before "$dsh_home/before.yml" \ --after "$dsh_home/after.yml" \ --removed "$dsh_home/removed.yml" diff --git a/.gitignore b/.gitignore index 05c7c25..6ab33bb 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ node_modules/ # Build output dist/ packages/dsh-web-search/lib/ +packages/dsh-responses-subagent/lib/ *.tsbuildinfo # Testing diff --git a/PUBLISHING.md b/PUBLISHING.md index 4e6e67d..3580915 100644 --- a/PUBLISHING.md +++ b/PUBLISHING.md @@ -1,8 +1,9 @@ # Publishing Guide -This monorepo publishes four npm packages, each versioned, tagged, and released **independently**: +This monorepo publishes five npm packages, each versioned, tagged, and released **independently**: - `@parallel-web/ai-sdk-tools` — `packages/ai-sdk-tools` +- `@parallel-web/dsh-responses-subagent` — `packages/dsh-responses-subagent` - `@parallel-web/dsh-web-search` — `packages/dsh-web-search` - `@parallel-web/opencode-plugin` — `packages/opencode-plugin` - `@parallel-web/pi-extension` — `packages/pi-extension` @@ -57,12 +58,13 @@ the reviewed bootstrap release manually from a clean, updated `main` checkout: ```bash pnpm install --frozen-lockfile -pnpm --filter @parallel-web/dsh-web-search check +PACKAGE=dsh-responses-subagent +pnpm --filter "@parallel-web/$PACKAGE" check BOOTSTRAP_DIR="$(mktemp -d)" -pnpm --dir packages/dsh-web-search pack --pack-destination "$BOOTSTRAP_DIR" +pnpm --dir "packages/$PACKAGE" pack --pack-destination "$BOOTSTRAP_DIR" BOOTSTRAP_TARBALL="$(find "$BOOTSTRAP_DIR" -name '*.tgz' -print -quit)" npm publish "$BOOTSTRAP_TARBALL" --access public --tag rc -npm view @parallel-web/dsh-web-search dist-tags --json +npm view "@parallel-web/$PACKAGE" dist-tags --json ``` The npm owner should inspect the tarball listing before the publish and complete npm's 2FA prompt. diff --git a/README.md b/README.md index f95d625..31442b2 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ Monorepo for @parallel-web npm packages. ## Packages - [`@parallel-web/ai-sdk-tools`](./packages/ai-sdk-tools) - AI SDK tools for Parallel Web +- [`@parallel-web/dsh-responses-subagent`](./packages/dsh-responses-subagent) - Parallel Responses research subagent for DeepSeek Harness - [`@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 diff --git a/packages/dsh-responses-subagent/CONTRIBUTING.md b/packages/dsh-responses-subagent/CONTRIBUTING.md new file mode 100644 index 0000000..642e429 --- /dev/null +++ b/packages/dsh-responses-subagent/CONTRIBUTING.md @@ -0,0 +1,125 @@ +# Contributing + +Thank you for improving the Parallel Responses research subagent for DeepSeek +Harness. + +## Scope + +Version one is intentionally one remote, answer-producing research worker. A +caller gives it one explicit text prompt; it makes one bounded Responses request +and returns the answer plus source URLs through Harness's existing subagent +contract. + +The following are outside this package's current scope: + +- a general Harness LLM provider or replacement for `web_search`; +- parent conversation, file, workspace, or environment inheritance; +- child tools, structured output, background jobs, continuation, or polling; +- retries, endpoint overrides, arbitrary model selection, or additional knobs; +- changing `@parallel-web/dsh-web-search`; and +- compatibility claims beyond the pinned Harness release. + +Propose broader product or API changes before implementing them. + +## Set up + +Prerequisites are Node.js `^22.19.0 || >=24.0.0` and Corepack. Run these +commands from the monorepo root; the repository pins the pnpm version. + +```sh +corepack enable +pnpm install --frozen-lockfile +``` + +Run the complete keyless package suite: + +```sh +pnpm --filter @parallel-web/dsh-responses-subagent check +``` + +`check` performs strict type checking, linting, deterministic tests, the +ESM/type build, manifest validation, and a package-content allowlist check. + +`pnpm audit --prod` is monorepo-wide even with a workspace filter. Run it +separately and inspect whether a reported dependency path starts with +`packages__dsh-responses-subagent>`; unrelated advisories in another workspace +package do not make this package's keyless suite fail. + +## Verify the packed artifact + +Create one fresh tarball and verify its exact contents: + +```sh +PACK_DIR="$(mktemp -d)" +pnpm --dir packages/dsh-responses-subagent pack --pack-destination "$PACK_DIR" +PACK_TGZ="$(find "$PACK_DIR" -name '*.tgz' -print -quit)" +node packages/dsh-responses-subagent/scripts/verify-packed-artifact.mjs \ + --tarball "$PACK_TGZ" +``` + +Use a disposable DSH home to prove installation, composition, and removal: + +```sh +REVIEW_DSH_HOME="$(mktemp -d)" +DSH_HOME="$REVIEW_DSH_HOME" pnpm --dir packages/dsh-responses-subagent exec \ + dsh --profile web --dump-config > "$REVIEW_DSH_HOME/before.yml" +DSH_HOME="$REVIEW_DSH_HOME" pnpm --dir packages/dsh-responses-subagent exec \ + dsh plugin --profile web add "$PACK_TGZ" +DSH_HOME="$REVIEW_DSH_HOME" pnpm --dir packages/dsh-responses-subagent exec \ + dsh --profile web --dump-config > "$REVIEW_DSH_HOME/after.yml" +PACKAGE_VERSION="$(node -p "require('./packages/dsh-responses-subagent/package.json').version")" +node packages/dsh-responses-subagent/scripts/verify-packed-profile.mjs \ + --dsh-home "$REVIEW_DSH_HOME" \ + --expected-version "$PACKAGE_VERSION" +DSH_HOME="$REVIEW_DSH_HOME" pnpm --dir packages/dsh-responses-subagent exec \ + dsh plugin --profile web remove @parallel-web/dsh-responses-subagent +DSH_HOME="$REVIEW_DSH_HOME" pnpm --dir packages/dsh-responses-subagent exec \ + dsh --profile web --dump-config > "$REVIEW_DSH_HOME/removed.yml" +node packages/dsh-responses-subagent/scripts/verify-profile-overlay.mjs \ + --before "$REVIEW_DSH_HOME/before.yml" \ + --after "$REVIEW_DSH_HOME/after.yml" \ + --removed "$REVIEW_DSH_HOME/removed.yml" +``` + +The packed-profile check verifies the public provider registration and one +shared Cordis/subagent runtime. The overlay check proves that both opt-in rows +appear only while installed and that the existing Search and built-in subagent +rows stay unchanged. + +## Live end-to-end test + +The normal suite is credentialless. `test:e2e` is opt-in because it sends one +real medium-tier Parallel Responses request and may cost $0.05 when successful. +Export `PARALLEL_API_KEY` in the shell first, then run exactly one attempt: + +```sh +pnpm --filter @parallel-web/dsh-responses-subagent test:e2e +``` + +The live receipt contains only package version, status, elapsed time, call +count, citation count, and URL hosts. It must not include the key, request +headers, raw server object, or complete answer. Do not retry a failed live test +automatically; diagnose it first. + +## Secrets and public artifacts + +- Never commit API keys, tokens, `.env` files, request headers, raw live API + responses, or full environment dumps. +- Use obvious sentinel values and `.test` or loopback hosts in deterministic + tests. +- Do not add local absolute paths, private repository facts, planning artifacts, + or evaluation outputs to package contents. +- Inspect `pnpm pack --dry-run` before release. Only the files listed in + `package.json` may ship. +- Preserve exact non-secret request counts and failure statuses in evaluation + receipts; do not log authorization headers or raw response objects. + +## Pull requests + +Keep changes narrow and include focused proof for request shape, citations, +cancellation, disposal, and capacity. Before requesting review, run the keyless +suite and packed install/remove workflow above, then inspect the monorepo-wide +production audit by dependency path. State explicitly whether the single live +test ran. + +For help, contact [support@parallel.ai](mailto:support@parallel.ai). diff --git a/packages/dsh-responses-subagent/LICENSE b/packages/dsh-responses-subagent/LICENSE new file mode 100644 index 0000000..7d7d9ed --- /dev/null +++ b/packages/dsh-responses-subagent/LICENSE @@ -0,0 +1,19 @@ +Copyright 2026 Parallel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/dsh-responses-subagent/README.md b/packages/dsh-responses-subagent/README.md new file mode 100644 index 0000000..65a5bad --- /dev/null +++ b/packages/dsh-responses-subagent/README.md @@ -0,0 +1,137 @@ +# Parallel Responses research for DeepSeek Harness + +This opt-in plugin adds a `parallel_research` tool to DeepSeek Harness. The +tool sends one explicit, self-contained research prompt to +[Parallel Responses](https://docs.parallel.ai/api-reference/responses-beta/create-a-response) +and returns the final answer with its source URLs visible to the parent model. + +This is a focused remote research worker, not a general Harness model provider. +It does not receive parent conversation history, files, workspace state, tools, +or ambient environment details. It does not change Harness's built-in +`web_search` tool or the separate `@parallel-web/dsh-web-search` package. + +## Quick start + +You will need: + +- Node.js 22.19 or later in the 22.x series, or Node.js 24 or newer; +- pnpm 10 or newer; +- a model API key supported by your Harness setup; and +- a [Parallel API key](https://platform.parallel.ai/). + +Check the supported Harness version without installing it globally: + +```sh +npx --yes @deepseek-ai/dsh@0.1.0-rc.6 --version +``` + +You should see `0.1.0-rc.6`. Harness is still a developer preview, so later +versions may need a plugin update. + +### 1. Install the plugin + +Install the release candidate into the `web` profile: + +```sh +npx --yes @deepseek-ai/dsh@0.1.0-rc.6 \ + plugin --profile web add \ + @parallel-web/dsh-responses-subagent@rc +``` + +The `@rc` suffix installs the current release candidate. After version `0.1.0` +is stable, you can leave the suffix off. + +### 2. Start Harness + +Make the Parallel key available in the terminal that starts Harness: + +```sh +export PARALLEL_API_KEY="your-key" +npx --yes @deepseek-ai/dsh@0.1.0-rc.6 web +``` + +Open [http://127.0.0.1:3080](http://127.0.0.1:3080). Configure a parent model +and workspace in Harness, then start a session. + +### 3. Delegate a research question + +Ask Harness to use the dedicated tool and give it a standalone question, for +example: + +> Use `parallel_research` to explain the current DeepSeek Harness plugin model. +> Include useful primary sources and their links. + +The call runs in the foreground. The parent waits for the remote research +answer, then continues with the returned text and source URLs. + +## Check that it installed + +Inspect the profile: + +```sh +npx --yes @deepseek-ai/dsh@0.1.0-rc.6 \ + --profile web --dump-config | \ + grep -E 'subagent-parallel-responses|tool-subagent-parallel-responses|parallel_research' +``` + +You should see one provider row and one tool row. The tool row should name +`parallel_research`, select the `parallel-responses` provider, and set +`enableRunInBackground: false`. + +## Fixed v1 behavior + +Version one intentionally has a small contract: + +- one `POST https://api.parallel.ai/v1/responses` request per accepted run; +- fixed `model: "parallel"`, medium reasoning effort, and streaming transport; +- at most two active requests in one plugin instance; +- a fixed 10-minute internal timeout and no automatic retries; +- text-only, self-contained prompts; +- no background jobs, continuation, child tools, structured output, endpoint + override, or arbitrary model selection; and +- caller cancellation and `SubagentRun.dispose()` abort outstanding work and + wait for cleanup. Plugin disposal unregisters the provider; callers retain + ownership of runs they already started. + +Parallel charges Responses by successful request. Review current pricing before +running many calls; the medium reasoning tier is currently documented at $0.05 +per successful request. + +The only package setting is an optional literal `apiKey`. Prefer +`PARALLEL_API_KEY`: profile files and configuration dumps are readable text, +whereas the plugin's schema only protects a literal key from ordinary config +display. + +## If something goes wrong + +- **The provider is unavailable:** confirm `PARALLEL_API_KEY` is set in the same + terminal that starts Harness. +- **The request is rejected immediately:** pass non-empty text only, and make + the prompt fully understandable without the parent conversation. +- **The remote call fails:** inspect the Harness warning. It reports a safe HTTP, + Responses, timeout, or generic transport diagnostic without response bodies, + headers, nested causes, stack traces, or credentials. The plugin deliberately + does not retry. +- **The capacity limit is reached:** wait for an active call to finish or cancel + one before starting more work. +- **Port 3080 is in use:** stop the older Harness process, then start Harness + again. + +## Remove + +```sh +npx --yes @deepseek-ai/dsh@0.1.0-rc.6 \ + plugin --profile web remove @parallel-web/dsh-responses-subagent +``` + +Restart Harness after removing the plugin. + +## Development and security + +See [CONTRIBUTING.md](./CONTRIBUTING.md) for local validation and the packed +install/remove workflow. Report vulnerabilities according to +[SECURITY.md](./SECURITY.md). + +For help, contact [support@parallel.ai](mailto:support@parallel.ai). + +MIT licensed. diff --git a/packages/dsh-responses-subagent/SECURITY.md b/packages/dsh-responses-subagent/SECURITY.md new file mode 100644 index 0000000..74f4dc2 --- /dev/null +++ b/packages/dsh-responses-subagent/SECURITY.md @@ -0,0 +1,31 @@ +# Security Policy + +## Reporting a vulnerability + +Please report suspected vulnerabilities privately to +[security@parallel.ai](mailto:security@parallel.ai). Include +`dsh-responses-subagent security` in the subject line. + +Include the affected package and Harness versions, a minimal reproduction, the +expected impact, and any suggested mitigation. Remove API keys, tokens, request +headers, parent prompts, full research answers, personal data, and raw customer +or production data before sending the report. + +Do not open a public issue or publish exploit details before Parallel has had a +reasonable opportunity to investigate and coordinate a fix. We appreciate +responsible disclosure and will use the contact information in your report to +coordinate next steps. + +## Supported versions + +Until the first public release, security fixes are prepared on the repository's +main development line. After release, the latest published version is the +supported line unless a release notice states otherwise. + +## Operational guidance + +Keep `PARALLEL_API_KEY` in the environment that launches Harness rather than in +a profile file. Configuration dumps are readable text. Delegate only prompts +that are safe to send to a remote research service: the plugin intentionally +sends the explicit prompt, but it cannot remove sensitive text that a caller +puts in that prompt. Review logs and evaluation artifacts before sharing them. diff --git a/packages/dsh-responses-subagent/cordis.patch.yml b/packages/dsh-responses-subagent/cordis.patch.yml new file mode 100644 index 0000000..ea6bf32 --- /dev/null +++ b/packages/dsh-responses-subagent/cordis.patch.yml @@ -0,0 +1,12 @@ +- insert: + - id: subagent-parallel-responses + name: '@parallel-web/dsh-responses-subagent' + + - id: tool-subagent-parallel-responses + name: '@deepseek-ai/dsh-tool-subagent' + config: + provider: parallel-responses + toolName: parallel_research + enableRunInBackground: false + backgroundMode: one-shot + maxDepth: provider-managed diff --git a/packages/dsh-responses-subagent/package.json b/packages/dsh-responses-subagent/package.json new file mode 100644 index 0000000..ec74f18 --- /dev/null +++ b/packages/dsh-responses-subagent/package.json @@ -0,0 +1,91 @@ +{ + "name": "@parallel-web/dsh-responses-subagent", + "description": "Parallel Responses research subagent for DeepSeek Harness", + "version": "0.1.0-rc.0", + "author": "Parallel Web", + "license": "MIT", + "type": "module", + "sideEffects": false, + "engines": { + "node": "^22.19.0 || >=24.0.0" + }, + "main": "./lib/index.js", + "types": "./lib/index.d.ts", + "exports": { + ".": { + "types": "./lib/index.d.ts", + "default": "./lib/index.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "lib/index.js", + "lib/index.d.ts", + "cordis.patch.yml", + "README.md", + "CONTRIBUTING.md", + "SECURITY.md", + "LICENSE" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/parallel-web/parallel-npm-packages.git", + "directory": "packages/dsh-responses-subagent" + }, + "keywords": [ + "deepseek-harness", + "dsh-plugin", + "parallel-responses", + "research-agent" + ], + "dsh": { + "bundle": { + "patch": "./cordis.patch.yml" + } + }, + "scripts": { + "build": "tsdown", + "check": "pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run build && pnpm run check:manifest && pnpm run check:package", + "check:manifest": "node scripts/check-manifest.mjs", + "check:package": "node scripts/check-package.mjs", + "clean": "rm -rf lib", + "lint": "oxlint src tests scripts tsdown.config.ts vitest.e2e.config.ts", + "prepare": "pnpm run build", + "test": "vitest run --exclude tests/live-responses.e2e.ts", + "test:e2e": "vitest run --config vitest.e2e.config.ts", + "typecheck": "tsc --noEmit" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "4.0.1", + "@deepseek-ai/dsh-launch-environment": "0.1.0-rc.6", + "@deepseek-ai/dsh-session": "0.1.0-rc.6", + "@deepseek-ai/dsh-subagent": "0.1.0-rc.6", + "@deepseek-ai/dsh-tool-subagent": "0.1.0-rc.6" + }, + "dependencies": { + "@deepseek-ai/schemastery": "3.18.1" + }, + "devDependencies": { + "@deepseek-ai/cordis": "4.0.1", + "@deepseek-ai/cordis-plugin-include": "1.0.6", + "@deepseek-ai/cordis-plugin-loader": "1.0.2", + "@deepseek-ai/dsh": "0.1.0-rc.6", + "@deepseek-ai/dsh-launch-environment": "0.1.0-rc.6", + "@deepseek-ai/dsh-llm": "0.1.0-rc.6", + "@deepseek-ai/dsh-session": "0.1.0-rc.6", + "@deepseek-ai/dsh-subagent": "0.1.0-rc.6", + "@deepseek-ai/dsh-system-prompt": "0.1.0-rc.6", + "@deepseek-ai/dsh-tool-subagent": "0.1.0-rc.6", + "@deepseek-ai/dsh-tools": "0.1.0-rc.6", + "@types/node": "26.2.0", + "esbuild": "0.28.2", + "js-yaml": "4.2.0", + "oxlint": "1.76.0", + "tsdown": "0.22.2", + "typescript": "6.0.3", + "vitest": "4.1.8" + } +} diff --git a/packages/dsh-responses-subagent/scripts/check-manifest.mjs b/packages/dsh-responses-subagent/scripts/check-manifest.mjs new file mode 100644 index 0000000..36a0ce4 --- /dev/null +++ b/packages/dsh-responses-subagent/scripts/check-manifest.mjs @@ -0,0 +1,120 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { entryListSchema } from '@deepseek-ai/cordis-plugin-include'; +import yaml from 'js-yaml'; + +const manifest = JSON.parse( + await readFile(new URL('../package.json', import.meta.url), 'utf8') +); +const rootManifest = JSON.parse( + await readFile(new URL('../../../package.json', import.meta.url), 'utf8') +); + +assert.equal(manifest.name, '@parallel-web/dsh-responses-subagent'); +assert.equal(manifest.version, '0.1.0-rc.0'); +assert.equal(manifest.author, 'Parallel Web'); +assert.equal(manifest.type, 'module'); +assert.equal(manifest.sideEffects, false); +assert.equal(manifest.license, 'MIT'); +assert.deepEqual(manifest.repository, { + type: 'git', + url: 'git+https://github.com/parallel-web/parallel-npm-packages.git', + directory: 'packages/dsh-responses-subagent', +}); +assert.equal(manifest.packageManager, undefined); +assert.equal(rootManifest.packageManager, 'pnpm@11.21.0'); +assert.equal(manifest.engines.node, '^22.19.0 || >=24.0.0'); +assert.equal(manifest.dsh?.bundle?.patch, './cordis.patch.yml'); +assert.deepEqual(manifest.dependencies, { + '@deepseek-ai/schemastery': '3.18.1', +}); +assert.deepEqual(manifest.peerDependencies, { + '@deepseek-ai/cordis': '4.0.1', + '@deepseek-ai/dsh-launch-environment': '0.1.0-rc.6', + '@deepseek-ai/dsh-session': '0.1.0-rc.6', + '@deepseek-ai/dsh-subagent': '0.1.0-rc.6', + '@deepseek-ai/dsh-tool-subagent': '0.1.0-rc.6', +}); +for (const [name, version] of Object.entries(manifest.peerDependencies)) { + assert.equal( + manifest.devDependencies[name], + version, + `${name} must have a matching exact development dependency` + ); +} +assert.equal(manifest.devDependencies['@deepseek-ai/dsh'], '0.1.0-rc.6'); +assert.equal(manifest.devDependencies['@deepseek-ai/dsh-llm'], '0.1.0-rc.6'); +assert.equal( + manifest.devDependencies['@deepseek-ai/cordis-plugin-include'], + '1.0.6' +); +assert.equal( + manifest.devDependencies['@deepseek-ai/cordis-plugin-loader'], + '1.0.2' +); +assert.equal( + manifest.devDependencies['@deepseek-ai/dsh-system-prompt'], + '0.1.0-rc.6' +); +assert.equal(manifest.devDependencies['@deepseek-ai/dsh-tools'], '0.1.0-rc.6'); +assert.equal(manifest.devDependencies['js-yaml'], '4.2.0'); +assert.deepEqual(manifest.files, [ + 'lib/index.js', + 'lib/index.d.ts', + 'cordis.patch.yml', + 'README.md', + 'CONTRIBUTING.md', + 'SECURITY.md', + 'LICENSE', +]); + +const patchText = await readFile( + new URL('../cordis.patch.yml', import.meta.url), + 'utf8' +); +const patch = yaml.load(patchText, { schema: entryListSchema }); +assert.deepEqual(patch, [ + { + insert: [ + { + id: 'subagent-parallel-responses', + name: '@parallel-web/dsh-responses-subagent', + }, + { + id: 'tool-subagent-parallel-responses', + name: '@deepseek-ai/dsh-tool-subagent', + config: { + provider: 'parallel-responses', + toolName: 'parallel_research', + enableRunInBackground: false, + backgroundMode: 'one-shot', + maxDepth: 'provider-managed', + }, + }, + ], + }, +]); + +const built = await import( + new URL(`../lib/index.js?check=${Date.now()}`, import.meta.url) +); +assert.equal( + 'default' in built, + false, + 'the Cordis namespace plugin must not have a default export' +); +assert.deepEqual( + Object.keys(built).sort(), + ['Config', 'PARALLEL_RESPONSES_PROVIDER_ID', 'apply', 'inject', 'name'], + 'the public surface must stay limited to the Cordis plugin contract' +); +assert.equal(built.PARALLEL_RESPONSES_PROVIDER_ID, 'parallel-responses'); +assert.equal(built.name, 'subagent-parallel-responses'); +assert.deepEqual(built.inject, ['subagents']); +assert.equal( + built.Config.dict.apiKey.meta.role, + 'secret', + 'the packaged API key schema must retain secret redaction metadata' +); + +process.stdout.write('manifest, patch, and public export surface are valid\n'); diff --git a/packages/dsh-responses-subagent/scripts/check-package.mjs b/packages/dsh-responses-subagent/scripts/check-package.mjs new file mode 100644 index 0000000..9ddd7b6 --- /dev/null +++ b/packages/dsh-responses-subagent/scripts/check-package.mjs @@ -0,0 +1,34 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; + +const pnpmCli = process.env.npm_execpath; +assert.ok(pnpmCli, 'npm_execpath is required to run the pinned pnpm CLI'); + +const result = spawnSync( + process.execPath, + [pnpmCli, 'pack', '--dry-run', '--json'], + { + cwd: new URL('..', import.meta.url), + encoding: 'utf8', + } +); +if (result.status !== 0) { + process.stderr.write(result.stderr); + process.exit(result.status ?? 1); +} + +const jsonStart = result.stdout.lastIndexOf('\n{'); +assert.notEqual(jsonStart, -1, 'pnpm pack did not emit a JSON receipt'); +const pack = JSON.parse(result.stdout.slice(jsonStart + 1)); +const files = pack.files.map((file) => file.path).sort(); +assert.deepEqual(files, [ + 'CONTRIBUTING.md', + 'LICENSE', + 'README.md', + 'SECURITY.md', + 'cordis.patch.yml', + 'lib/index.d.ts', + 'lib/index.js', + 'package.json', +]); +process.stdout.write('package dry-run allowlist is valid\n'); diff --git a/packages/dsh-responses-subagent/scripts/verify-packed-artifact.mjs b/packages/dsh-responses-subagent/scripts/verify-packed-artifact.mjs new file mode 100644 index 0000000..47632af --- /dev/null +++ b/packages/dsh-responses-subagent/scripts/verify-packed-artifact.mjs @@ -0,0 +1,98 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { resolve } from 'node:path'; + +const tarballIndex = process.argv.indexOf('--tarball'); +assert.notEqual( + tarballIndex, + -1, + 'usage: verify-packed-artifact.mjs --tarball ' +); +const tarballArgument = process.argv[tarballIndex + 1]; +assert.ok(tarballArgument, 'missing tarball path'); +const tarball = resolve(tarballArgument); + +function tar(args) { + const result = spawnSync('tar', args, { encoding: 'utf8' }); + if (result.status !== 0) { + process.stderr.write(result.stderr); + process.exit(result.status ?? 1); + } + return result.stdout; +} + +const entries = tar(['-tzf', tarball]) + .split('\n') + .filter(Boolean) + .filter((entry) => !entry.endsWith('/')) + .sort(); + +assert.deepEqual(entries, [ + 'package/CONTRIBUTING.md', + 'package/LICENSE', + 'package/README.md', + 'package/SECURITY.md', + 'package/cordis.patch.yml', + 'package/lib/index.d.ts', + 'package/lib/index.js', + 'package/package.json', +]); + +const manifest = JSON.parse(tar(['-xOzf', tarball, 'package/package.json'])); +assert.equal(manifest.name, '@parallel-web/dsh-responses-subagent'); +assert.equal(manifest.version, '0.1.0-rc.0'); +assert.equal(manifest.author, 'Parallel Web'); +assert.deepEqual(manifest.repository, { + type: 'git', + url: 'git+https://github.com/parallel-web/parallel-npm-packages.git', + directory: 'packages/dsh-responses-subagent', +}); +assert.equal(manifest.dsh?.bundle?.patch, './cordis.patch.yml'); +assert.deepEqual(manifest.dependencies, { + '@deepseek-ai/schemastery': '3.18.1', +}); +for (const name of [ + '@deepseek-ai/cordis', + '@deepseek-ai/dsh-launch-environment', + '@deepseek-ai/dsh-session', + '@deepseek-ai/dsh-subagent', + '@deepseek-ai/dsh-tool-subagent', +]) { + assert.equal( + manifest.peerDependencies[name], + name === '@deepseek-ai/cordis' ? '4.0.1' : '0.1.0-rc.6' + ); +} + +const combinedPublicText = entries + .filter((entry) => /\.(?:js|d\.ts|md|yml|json)$/.test(entry)) + .map((entry) => tar(['-xOzf', tarball, entry])) + .join('\n'); + +for (const forbidden of [ + `/${'Users'}/`, + ['developer', 'parallel', 'code'].join('/'), + `${'.agent'}/`, + 'Authorization: Bearer', +]) { + assert.equal( + combinedPublicText.includes(forbidden), + false, + `packed artifact contains forbidden text: ${forbidden}` + ); +} + +for (const required of [ + 'https://api.parallel.ai/v1/responses', + 'parallel-responses', + 'parallel_research', + 'enableRunInBackground: false', +]) { + assert.equal( + combinedPublicText.includes(required), + true, + `packed artifact must retain contract text: ${required}` + ); +} + +process.stdout.write(`packed artifact valid: ${tarball}\n`); diff --git a/packages/dsh-responses-subagent/scripts/verify-packed-profile.mjs b/packages/dsh-responses-subagent/scripts/verify-packed-profile.mjs new file mode 100644 index 0000000..dc38fd4 --- /dev/null +++ b/packages/dsh-responses-subagent/scripts/verify-packed-profile.mjs @@ -0,0 +1,85 @@ +import assert from 'node:assert/strict'; +import { realpathSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import { join, resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +function argument(name) { + const index = process.argv.indexOf(name); + assert.notEqual(index, -1, `missing ${name}`); + const value = process.argv[index + 1]; + assert.ok(value, `missing value for ${name}`); + return value; +} + +const dshHome = resolve(argument('--dsh-home')); +const expectedVersion = argument('--expected-version'); +const profileManifest = join(dshHome, 'profiles', 'web', 'package.json'); +const profileRequire = createRequire(profileManifest); +const pluginManifest = profileRequire.resolve( + '@parallel-web/dsh-responses-subagent/package.json' +); +const pluginRequire = createRequire(pluginManifest); + +const pluginPackage = pluginRequire(pluginManifest); +assert.equal(pluginPackage.version, expectedVersion); + +const resolutions = {}; +for (const packageName of [ + '@deepseek-ai/cordis', + '@deepseek-ai/dsh-launch-environment', + '@deepseek-ai/dsh-session', + '@deepseek-ai/dsh-subagent', + '@deepseek-ai/dsh-tool-subagent', +]) { + const fromProfile = realpathSync(profileRequire.resolve(packageName)); + const fromPlugin = realpathSync(pluginRequire.resolve(packageName)); + assert.equal( + fromPlugin, + fromProfile, + `${packageName} must resolve to one shared entrypoint` + ); + resolutions[packageName] = fromProfile; +} + +const pluginEntry = profileRequire.resolve( + '@parallel-web/dsh-responses-subagent' +); +const plugin = await import(pathToFileURL(pluginEntry).href); +const cordis = await import( + pathToFileURL(resolutions['@deepseek-ai/cordis']).href +); +const subagents = await import( + pathToFileURL(resolutions['@deepseek-ai/dsh-subagent']).href +); + +assert.equal('default' in plugin, false); +assert.equal(plugin.PARALLEL_RESPONSES_PROVIDER_ID, 'parallel-responses'); +assert.equal(plugin.Config.dict.apiKey.meta.role, 'secret'); + +const ctx = new cordis.Context(); +const runtimeFiber = await ctx.plugin(subagents.default); +const providerFiber = await ctx.plugin(plugin, { + apiKey: 'parallel_test_packed_profile', +}); +assert.deepEqual(ctx.subagents.list(), ['parallel-responses']); +const provider = ctx.subagents.getProvider('parallel-responses'); +assert.ok(provider); +assert.equal(provider.name, 'parallel-responses'); +assert.deepEqual(provider.capabilities, { + outputSchema: false, + depthLimit: false, + toolFilter: false, + persona: false, +}); +assert.equal(provider.inheritsParentContext, false); +assert.equal(typeof provider.start, 'function'); +assert.equal('prepareContinuable' in provider, false); + +await providerFiber.dispose(); +assert.equal(ctx.subagents.getProvider('parallel-responses'), undefined); +await runtimeFiber.dispose(); + +process.stdout.write( + `packed profile valid: ${JSON.stringify({ version: pluginPackage.version, shared: Object.keys(resolutions) })}\n` +); diff --git a/packages/dsh-responses-subagent/scripts/verify-profile-overlay.mjs b/packages/dsh-responses-subagent/scripts/verify-profile-overlay.mjs new file mode 100644 index 0000000..e77dad5 --- /dev/null +++ b/packages/dsh-responses-subagent/scripts/verify-profile-overlay.mjs @@ -0,0 +1,84 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import { entryListSchema } from '@deepseek-ai/cordis-plugin-include'; +import yaml from 'js-yaml'; + +function argument(name) { + const index = process.argv.indexOf(name); + assert.notEqual(index, -1, `missing ${name}`); + const value = process.argv[index + 1]; + assert.ok(value, `missing value for ${name}`); + return resolve(value); +} + +async function load(path) { + return yaml.load(await readFile(path, 'utf8'), { schema: entryListSchema }); +} + +function flatten(entries) { + const rows = []; + for (const entry of entries) { + rows.push(entry); + if (entry?.group === true && Array.isArray(entry.config)) { + rows.push(...flatten(entry.config)); + } + } + return rows; +} + +function one(rows, id) { + const matches = rows.filter((row) => row?.id === id); + assert.equal(matches.length, 1, `expected exactly one ${id} row`); + return matches[0]; +} + +const before = flatten(await load(argument('--before'))); +const after = flatten(await load(argument('--after'))); +const removed = flatten(await load(argument('--removed'))); + +for (const rows of [before, removed]) { + assert.equal( + rows.some((row) => row?.id === 'subagent-parallel-responses'), + false + ); + assert.equal( + rows.some((row) => row?.id === 'tool-subagent-parallel-responses'), + false + ); +} + +assert.deepEqual(one(after, 'subagent-parallel-responses'), { + id: 'subagent-parallel-responses', + name: '@parallel-web/dsh-responses-subagent', +}); +assert.deepEqual(one(after, 'tool-subagent-parallel-responses'), { + id: 'tool-subagent-parallel-responses', + name: '@deepseek-ai/dsh-tool-subagent', + config: { + provider: 'parallel-responses', + toolName: 'parallel_research', + enableRunInBackground: false, + backgroundMode: 'one-shot', + maxDepth: 'provider-managed', + }, +}); + +for (const id of [ + 'subagent', + 'tool-subagent', + 'web', + 'web-search-deepseek', + 'tool-web', +]) { + assert.deepEqual(one(after, id), one(before, id), `${id} changed on install`); + assert.deepEqual( + one(removed, id), + one(before, id), + `${id} changed after removal` + ); +} + +process.stdout.write( + 'profile overlay valid: provider and foreground-only research tool install/remove cleanly; existing Search and subagent rows preserved\n' +); diff --git a/packages/dsh-responses-subagent/src/index.ts b/packages/dsh-responses-subagent/src/index.ts new file mode 100644 index 0000000..45fe5f2 --- /dev/null +++ b/packages/dsh-responses-subagent/src/index.ts @@ -0,0 +1,49 @@ +/** Parallel Responses research-subagent provider for DeepSeek Harness. */ + +import type { Context } from '@deepseek-ai/cordis'; +import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment'; +import z from '@deepseek-ai/schemastery'; +import type {} from '@deepseek-ai/dsh-subagent'; +import { ParallelResponsesProvider } from './provider.ts'; + +export { PARALLEL_RESPONSES_PROVIDER_ID } from './provider.ts'; + +export const name = 'subagent-parallel-responses'; +export const inject = ['subagents']; + +/** Deployment configuration for the fixed Parallel Responses provider. */ +export interface Config { + /** Explicit Parallel API key; omission uses the Harness launch snapshot. */ + apiKey?: string; +} + +export const Config: z = z.object({ + apiKey: z.string().role('secret'), +}); + +/** + * Register the fixed `parallel-responses` provider. + * @param ctx - context carrying the shared subagent service. + * @param config - optional explicit Parallel credential. + */ +export function apply(ctx: Context, config: Config): void { + const apiKey = + config.apiKey ?? + launchEnvironmentOf(ctx).get('PARALLEL_API_KEY')?.value ?? + ''; + if (apiKey.trim().length === 0) { + throw new Error( + 'dsh-responses-subagent: apiKey or PARALLEL_API_KEY is required' + ); + } + ctx.subagents.registerProvider( + new ParallelResponsesProvider({ + apiKey, + onError: (error) => { + ctx.logger.warn( + `subagent-parallel-responses: remote run failed: ${error.message}` + ); + }, + }) + ); +} diff --git a/packages/dsh-responses-subagent/src/provider.ts b/packages/dsh-responses-subagent/src/provider.ts new file mode 100644 index 0000000..e9c5f7c --- /dev/null +++ b/packages/dsh-responses-subagent/src/provider.ts @@ -0,0 +1,442 @@ +/** + * One-shot Parallel Responses transport for the DeepSeek Harness subagent seam. + * + * @module @parallel-web/dsh-responses-subagent/provider + */ + +import { randomUUID } from 'node:crypto'; +import { SessionId } from '@deepseek-ai/dsh-session'; +import { + NO_START_CAPABILITIES, + type ResolvedSubagentStartRequest, + type SubagentProvider, + type SubagentResult, + type SubagentRun, +} from '@deepseek-ai/dsh-subagent'; + +export const PARALLEL_RESPONSES_PROVIDER_ID = 'parallel-responses'; +export const PARALLEL_RESPONSES_URL = 'https://api.parallel.ai/v1/responses'; +export const PARALLEL_RESPONSES_TIMEOUT_MS = 10 * 60_000; + +const MAX_ACTIVE_RUNS = 2; + +type Fetch = ( + input: string | URL | Request, + init?: RequestInit +) => Promise; + +interface CapacityWaiter { + readonly signal: AbortSignal; + readonly resolve: (release: () => void) => void; + readonly reject: (error: Error) => void; + readonly onAbort: () => void; +} + +class Capacity { + private active = 0; + private readonly waiters: CapacityWaiter[] = []; + + async acquire(signal: AbortSignal): Promise<() => void> { + if (signal.aborted) throw cancellationError('before capacity admission'); + if (this.active < MAX_ACTIVE_RUNS) { + this.active += 1; + return this.releaseOnce(); + } + + return await new Promise<() => void>((resolve, reject) => { + const onAbort = (): void => { + const index = this.waiters.indexOf(waiter); + if (index >= 0) this.waiters.splice(index, 1); + reject(cancellationError('while waiting for capacity')); + }; + const waiter: CapacityWaiter = { signal, resolve, reject, onAbort }; + this.waiters.push(waiter); + signal.addEventListener('abort', onAbort, { once: true }); + }); + } + + private releaseOnce(): () => void { + let released = false; + return (): void => { + if (released) return; + released = true; + this.active -= 1; + this.admitNext(); + }; + } + + private admitNext(): void { + for (;;) { + const waiter = this.waiters.shift(); + if (waiter === undefined) return; + waiter.signal.removeEventListener('abort', waiter.onAbort); + if (waiter.signal.aborted) { + waiter.reject(cancellationError('while waiting for capacity')); + continue; + } + this.active += 1; + waiter.resolve(this.releaseOnce()); + return; + } + } +} + +function cancellationError(when: string): Error { + return new Error(`dsh-responses-subagent: request was aborted ${when}`); +} + +function diagnosticError(error: unknown): Error { + if ( + error instanceof Error && + (/^dsh-responses-subagent: Parallel Responses returned HTTP \d{3}$/u.test( + error.message + ) || + error.message === + 'dsh-responses-subagent: Parallel Responses reported response.failed' || + error.message === + 'dsh-responses-subagent: Parallel Responses request timed out') + ) { + return new Error(error.message); + } + return new Error( + 'dsh-responses-subagent: Parallel Responses transport failed' + ); +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +/** + * Preserve the exact text sequence of a non-empty, text-only delegated task. + * @param request - resolved Harness request carrying the standalone prompt. + * @returns the exact concatenated text supplied to Parallel Responses. + */ +export function researchPrompt(request: ResolvedSubagentStartRequest): string { + if (request.prompt.length === 0) { + throw new Error( + 'dsh-responses-subagent: the research prompt must contain only text blocks' + ); + } + const texts: string[] = []; + for (const block of request.prompt) { + if (block.type !== 'text') { + throw new Error( + 'dsh-responses-subagent: the research prompt must contain only text blocks' + ); + } + texts.push(block.text); + } + const prompt = texts.join(''); + if (prompt.trim().length === 0) { + throw new Error( + 'dsh-responses-subagent: the research prompt must not be empty' + ); + } + return prompt; +} + +function responseRequest(prompt: string): RequestInit { + return { + method: 'POST', + headers: { + Accept: 'text/event-stream', + Authorization: '', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + model: 'parallel', + input: prompt, + reasoning: { effort: 'medium' }, + stream: true, + }), + redirect: 'error', + }; +} + +function eventData(frame: string): { event?: string; data?: string } { + let event: string | undefined; + const data: string[] = []; + for (const line of frame.split(/\r?\n/u)) { + if (line.startsWith('event:')) event = line.slice(6).trimStart(); + if (line.startsWith('data:')) data.push(line.slice(5).replace(/^ /u, '')); + } + return { + ...(event === undefined ? {} : { event }), + ...(data.length === 0 ? {} : { data: data.join('\n') }), + }; +} + +function citationLine(title: string | undefined, url: string): string { + const compactTitle = title?.replace(/\s+/gu, ' ').trim(); + return compactTitle === undefined || compactTitle.length === 0 + ? `- ${url}` + : `- ${compactTitle} — ${url}`; +} + +function completedResult(payload: Record): SubagentResult { + const response = payload.response; + if (!isRecord(response) || !Array.isArray(response.output)) { + throw new TypeError( + 'dsh-responses-subagent: completed response has no output array' + ); + } + + const answerParts: string[] = []; + const citations = new Map(); + for (const item of response.output) { + if (!isRecord(item) || item.type !== 'message') continue; + if (!Array.isArray(item.content)) { + throw new TypeError( + 'dsh-responses-subagent: completed message has no content array' + ); + } + for (const content of item.content) { + if (!isRecord(content) || content.type !== 'output_text') continue; + if (typeof content.text !== 'string') { + throw new TypeError( + 'dsh-responses-subagent: completed output text is malformed' + ); + } + answerParts.push(content.text); + if (content.annotations === undefined) continue; + if (!Array.isArray(content.annotations)) { + throw new TypeError( + 'dsh-responses-subagent: completed annotations are malformed' + ); + } + for (const annotation of content.annotations) { + if (!isRecord(annotation) || annotation.type !== 'url_citation') { + continue; + } + if (typeof annotation.url !== 'string' || annotation.url.length === 0) { + throw new TypeError( + 'dsh-responses-subagent: URL citation is malformed' + ); + } + if ( + annotation.title !== undefined && + typeof annotation.title !== 'string' + ) { + throw new TypeError( + 'dsh-responses-subagent: citation title is malformed' + ); + } + const priorTitle = citations.get(annotation.url); + if ( + !citations.has(annotation.url) || + priorTitle === undefined || + priorTitle.trim().length === 0 + ) { + citations.set(annotation.url, annotation.title); + } + } + } + } + + const answer = answerParts.join(''); + if (answer.trim().length === 0) { + throw new TypeError( + 'dsh-responses-subagent: completed response contains no answer text' + ); + } + const sourceLines = [...citations].map(([url, title]) => + citationLine(title, url) + ); + const text = + sourceLines.length === 0 + ? answer + : `${answer}\n\nSources:\n${sourceLines.join('\n')}`; + return { output: [{ type: 'text', text }], stopReason: 'completed' }; +} + +function terminalResult(frame: string): SubagentResult | undefined { + const parsed = eventData(frame); + if (parsed.data === undefined) return undefined; + let value: unknown; + try { + value = JSON.parse(parsed.data); + } catch (error: unknown) { + throw new TypeError('dsh-responses-subagent: malformed SSE data', { + cause: error, + }); + } + if (!isRecord(value) || typeof value.type !== 'string') { + throw new TypeError('dsh-responses-subagent: malformed SSE event'); + } + if (parsed.event !== undefined && parsed.event !== value.type) { + throw new TypeError( + 'dsh-responses-subagent: SSE event name does not match its payload' + ); + } + if (value.type === 'response.completed') return completedResult(value); + if (value.type === 'response.failed') { + throw new Error( + 'dsh-responses-subagent: Parallel Responses reported response.failed' + ); + } + return undefined; +} + +async function readTerminal(response: Response): Promise { + if (!response.ok) { + throw new Error( + `dsh-responses-subagent: Parallel Responses returned HTTP ${response.status}` + ); + } + if (response.body === null) { + throw new Error( + 'dsh-responses-subagent: Parallel Responses returned no body' + ); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + try { + for (;;) { + const chunk = await reader.read(); + buffer += decoder.decode(chunk.value, { stream: !chunk.done }); + for (;;) { + const delimiter = /\r?\n\r?\n/u.exec(buffer); + if (delimiter === null) break; + const frame = buffer.slice(0, delimiter.index); + buffer = buffer.slice(delimiter.index + delimiter[0].length); + const terminal = terminalResult(frame); + if (terminal !== undefined) return terminal; + } + if (chunk.done) break; + } + if (buffer.trim().length > 0) { + const terminal = terminalResult(buffer); + if (terminal !== undefined) return terminal; + } + throw new Error( + 'dsh-responses-subagent: Parallel Responses stream ended without a terminal event' + ); + } finally { + await reader.cancel(); + reader.releaseLock(); + } +} + +/** Construction inputs kept private from Cordis configuration. */ +export interface ParallelResponsesProviderOptions { + readonly apiKey: string; + readonly fetch?: Fetch; + /** Safe diagnostic sink for failures flattened into an error result. */ + readonly onError?: (error: Error) => void; +} + +/** Fixed, one-shot remote research provider. */ +export class ParallelResponsesProvider implements SubagentProvider { + readonly name = PARALLEL_RESPONSES_PROVIDER_ID; + readonly capabilities = NO_START_CAPABILITIES; + readonly inheritsParentContext = false; + + private readonly apiKey: string; + private readonly fetch: Fetch; + private readonly onError: ((error: Error) => void) | undefined; + private readonly capacity = new Capacity(); + + constructor(options: ParallelResponsesProviderOptions) { + this.apiKey = options.apiKey; + this.fetch = options.fetch ?? globalThis.fetch; + this.onError = options.onError; + } + + async start(request: ResolvedSubagentStartRequest): Promise { + const prompt = researchPrompt(request); + if (this.apiKey.trim().length === 0) { + throw new Error('dsh-responses-subagent: PARALLEL_API_KEY is required'); + } + + const release = await this.capacity.acquire(request.signal); + if (request.signal.aborted) { + release(); + throw cancellationError('before run publication'); + } + + const controller = new AbortController(); + let cancelled = false; + const requestCancel = (): void => { + if (cancelled) return; + cancelled = true; + controller.abort(cancellationError('during the remote run')); + }; + const onAbort = (): void => { + requestCancel(); + }; + request.signal.addEventListener('abort', onAbort, { once: true }); + + let timedOut = false; + const timer = setTimeout(() => { + timedOut = true; + controller.abort( + new Error( + 'dsh-responses-subagent: Parallel Responses request timed out' + ) + ); + }, PARALLEL_RESPONSES_TIMEOUT_MS); + + const result: Promise = (async () => { + try { + const init = responseRequest(prompt); + const headers = new Headers(init.headers); + headers.set('Authorization', `Bearer ${this.apiKey}`); + const response = await this.fetch(PARALLEL_RESPONSES_URL, { + ...init, + headers, + signal: controller.signal, + }); + const terminal = await readTerminal(response); + if (cancelled) return { output: [], stopReason: 'aborted' }; + if (timedOut) { + throw new Error( + 'dsh-responses-subagent: Parallel Responses request timed out' + ); + } + return terminal; + } catch (error: unknown) { + if (!cancelled) { + try { + this.onError?.( + diagnosticError( + timedOut + ? new Error( + 'dsh-responses-subagent: Parallel Responses request timed out' + ) + : error + ) + ); + } catch { + // A diagnostic sink must not violate the non-rejecting run seam. + return { output: [], stopReason: 'error' }; + } + } + return { + output: [], + stopReason: cancelled ? 'aborted' : 'error', + }; + } finally { + clearTimeout(timer); + request.signal.removeEventListener('abort', onAbort); + release(); + } + })(); + + let disposal: Promise | undefined; + return { + id: SessionId(randomUUID()), + localAgent: undefined, + result, + dispose: (): Promise => { + if (disposal !== undefined) return disposal; + request.signal.removeEventListener('abort', onAbort); + requestCancel(); + disposal = result.then(() => undefined); + return disposal; + }, + }; + } +} diff --git a/packages/dsh-responses-subagent/tests/live-responses.e2e.ts b/packages/dsh-responses-subagent/tests/live-responses.e2e.ts new file mode 100644 index 0000000..f341790 --- /dev/null +++ b/packages/dsh-responses-subagent/tests/live-responses.e2e.ts @@ -0,0 +1,89 @@ +import { Context } from '@deepseek-ai/cordis'; +import type { ContentBlock } from '@deepseek-ai/dsh-llm'; +import SubagentRuntime, { + type ResolvedSubagentStartRequest, +} from '@deepseek-ai/dsh-subagent'; +import { describe, it } from 'vitest'; +import * as parallelResponses from '../src/index.ts'; + +const apiKey = process.env.PARALLEL_API_KEY; +if (apiKey === undefined || apiKey.length === 0) { + throw new Error( + 'PARALLEL_API_KEY must be present for the single guarded live attempt' + ); +} + +function request(signal: AbortSignal): ResolvedSubagentStartRequest { + return { + prompt: [ + { + type: 'text', + text: + 'Using current primary sources, explain what DeepSeek Harness is and ' + + 'identify its official GitHub repository. Give a concise answer with source links.', + }, + ], + signal, + parent: {} as ResolvedSubagentStartRequest['parent'], + descriptor: {} as ResolvedSubagentStartRequest['descriptor'], + }; +} + +function answerText(blocks: ContentBlock[]): string { + return blocks + .filter( + (block): block is Extract => + block.type === 'text' + ) + .map((block) => block.text) + .join(''); +} + +describe('Parallel Responses provider real API', () => { + it('returns one non-secret research result receipt', async () => { + const startedAt = performance.now(); + const ctx = new Context(); + const runtimeFiber = await ctx.plugin(SubagentRuntime); + const providerFiber = await ctx.plugin(parallelResponses, { apiKey }); + const provider = ctx.subagents.getProvider( + parallelResponses.PARALLEL_RESPONSES_PROVIDER_ID + ); + if (provider === undefined) { + throw new Error('Parallel Responses provider did not register'); + } + + const run = await provider.start(request(new AbortController().signal)); + try { + const result = await run.result; + if (result.stopReason !== 'completed') { + throw new Error( + `Parallel Responses live run ended with ${result.stopReason}` + ); + } + const text = answerText(result.output); + if (text.trim().length === 0) { + throw new Error('Parallel Responses live run returned no answer text'); + } + + const sourceSection = text.split('\n\nSources:\n')[1] ?? ''; + const urls = sourceSection.match(/https?:\/\/\S+/gu) ?? []; + const hostnames = urls.map((url) => new URL(url).hostname); + + process.stdout.write( + `${JSON.stringify({ + timestamp: new Date().toISOString(), + packageVersion: '0.1.0-rc.0', + status: 'passed', + elapsedMs: Math.round(performance.now() - startedAt), + callCount: 1, + citationCount: urls.length, + hostnames, + })}\n` + ); + } finally { + await run.dispose(); + await providerFiber.dispose(); + await runtimeFiber.dispose(); + } + }, 660_000); +}); diff --git a/packages/dsh-responses-subagent/tests/loader-composition.spec.ts b/packages/dsh-responses-subagent/tests/loader-composition.spec.ts new file mode 100644 index 0000000..5a7d824 --- /dev/null +++ b/packages/dsh-responses-subagent/tests/loader-composition.spec.ts @@ -0,0 +1,112 @@ +import { mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { afterEach, describe, expect, it } from 'vitest'; +import { Context } from '@deepseek-ai/cordis'; +import Include from '@deepseek-ai/cordis-plugin-include'; +import Loader from '@deepseek-ai/cordis-plugin-loader'; +import SubagentRuntime from '@deepseek-ai/dsh-subagent'; +import SystemPrompt from '@deepseek-ai/dsh-system-prompt'; +import * as ToolSubagent from '@deepseek-ai/dsh-tool-subagent'; +import ToolRuntime from '@deepseek-ai/dsh-tools'; +import * as ParallelResponses from '../src/index.ts'; + +let temporaryRoot: string | undefined; +let context: Context | undefined; + +afterEach(async () => { + await context?.fiber.dispose(); + context = undefined; + if (temporaryRoot !== undefined) { + await rm(temporaryRoot, { recursive: true, force: true }); + } + temporaryRoot = undefined; +}); + +async function boot(): Promise { + temporaryRoot = await mkdtemp(join(tmpdir(), 'dsh-parallel-responses-')); + const configPath = join(temporaryRoot, 'cordis.yml'); + await writeFile( + configPath, + [ + "- name: '@deepseek-ai/dsh-system-prompt'", + "- name: '@deepseek-ai/dsh-tools'", + "- name: '@deepseek-ai/dsh-subagent'", + "- name: '@parallel-web/dsh-responses-subagent'", + ' config:', + ' apiKey: parallel_test_loader', + "- name: '@deepseek-ai/dsh-tool-subagent'", + ' config:', + ' provider: parallel-responses', + ' toolName: parallel_research', + ' enableRunInBackground: false', + ' backgroundMode: one-shot', + ' maxDepth: provider-managed', + '', + ].join('\n') + ); + + const ctx = new Context(); + context = ctx; + ctx.baseUrl = pathToFileURL(temporaryRoot).href + '/'; + await ctx.plugin(Loader); + ctx.loader.builtins.include = Include; + const modules = new Map([ + ['@deepseek-ai/dsh-system-prompt', SystemPrompt], + ['@deepseek-ai/dsh-tools', ToolRuntime], + ['@deepseek-ai/dsh-subagent', SubagentRuntime], + ['@parallel-web/dsh-responses-subagent', ParallelResponses], + ['@deepseek-ai/dsh-tool-subagent', ToolSubagent], + ]); + ctx.loader.internal = { + version: 'v2', + async import(specifier: string) { + if (!modules.has(specifier)) { + throw new Error(`unexpected Loader import: ${specifier}`); + } + return modules.get(specifier); + }, + } as unknown as NonNullable; + await ctx.loader.create({ + name: 'cordis:include', + config: { path: pathToFileURL(configPath).href }, + }); + await ctx.loader.await(); + return ctx; +} + +describe('Parallel Responses public Loader composition', () => { + it('registers one foreground-only research tool without a network request', async () => { + const ctx = await boot(); + + const provider = ctx.subagents.getProvider('parallel-responses'); + expect(provider).toMatchObject({ + name: 'parallel-responses', + capabilities: { + outputSchema: false, + depthLimit: false, + toolFilter: false, + persona: false, + }, + inheritsParentContext: false, + }); + expect('prepareContinuable' in (provider ?? {})).toBe(false); + + const schema = ctx.tools + .schemas() + .find((candidate) => candidate.name === 'parallel_research'); + expect(schema).toBeDefined(); + if (schema === undefined) { + throw new Error('parallel_research did not register'); + } + const properties = ( + schema.parameters as { properties?: Record } + ).properties; + expect(Object.keys(properties ?? {}).sort()).toEqual([ + 'description', + 'prompt', + ]); + expect(schema.description).not.toContain('job_output'); + }); +}); diff --git a/packages/dsh-responses-subagent/tests/provider.spec.ts b/packages/dsh-responses-subagent/tests/provider.spec.ts new file mode 100644 index 0000000..3fba83e --- /dev/null +++ b/packages/dsh-responses-subagent/tests/provider.spec.ts @@ -0,0 +1,366 @@ +import { Context } from '@deepseek-ai/cordis'; +import { createLaunchEnvironmentSnapshot } from '@deepseek-ai/dsh-launch-environment'; +import type { ContentBlock } from '@deepseek-ai/dsh-llm'; +import SubagentRuntime, { + type ResolvedSubagentStartRequest, +} from '@deepseek-ai/dsh-subagent'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import * as plugin from '../src/index.ts'; +import { + PARALLEL_RESPONSES_TIMEOUT_MS, + PARALLEL_RESPONSES_URL, + ParallelResponsesProvider, + researchPrompt, +} from '../src/provider.ts'; + +function request( + prompt: ContentBlock[] = [{ type: 'text', text: 'research this exactly' }], + signal = new AbortController().signal +): ResolvedSubagentStartRequest { + return { + prompt, + signal, + parent: {} as ResolvedSubagentStartRequest['parent'], + descriptor: {} as ResolvedSubagentStartRequest['descriptor'], + }; +} + +function sseEvent(type: string, response: Record): string { + return `event: ${type}\ndata: ${JSON.stringify({ type, response })}\n\n`; +} + +function completed( + text = 'The researched answer.', + annotations: unknown[] = [] +): string { + return sseEvent('response.completed', { + status: 'completed', + output: [ + { + type: 'message', + content: [{ type: 'output_text', text, annotations }], + }, + ], + }); +} + +function streamResponse(...chunks: string[]): Response { + const encoder = new TextEncoder(); + return new Response( + new ReadableStream({ + start(controller) { + for (const chunk of chunks) controller.enqueue(encoder.encode(chunk)); + controller.close(); + }, + }), + { status: 200, headers: { 'Content-Type': 'text/event-stream' } } + ); +} + +function abortableFetch() { + return vi.fn( + async (_input: string | URL | Request, init?: RequestInit) => + await new Promise((_resolve, reject) => { + init?.signal?.addEventListener( + 'abort', + () => reject(new Error('transport aborted')), + { once: true } + ); + }) + ); +} + +afterEach(() => { + vi.useRealTimers(); + vi.restoreAllMocks(); +}); + +describe('Parallel Responses request and output', () => { + it('sends one fixed request and renders deduplicated title and URL citations', async () => { + const event = completed('Evidence-backed answer.', [ + { + type: 'url_citation', + title: 'Source A', + url: 'https://a.test/report', + }, + { + type: 'url_citation', + title: 'Duplicate title', + url: 'https://a.test/report', + }, + { type: 'url_citation', url: 'https://b.test/data' }, + ]); + const fetch = vi + .fn() + .mockResolvedValue( + streamResponse( + 'event: response.created\r\ndata: {"type":"response.created"}\r\n\r\n', + event.slice(0, 19), + event.slice(19) + ) + ); + const provider = new ParallelResponsesProvider({ + apiKey: 'parallel_test_provider', + fetch, + }); + const run = await provider.start( + request([{ type: 'text', text: ' unchanged prompt ' }]) + ); + + await expect(run.result).resolves.toEqual({ + output: [ + { + type: 'text', + text: + 'Evidence-backed answer.\n\nSources:\n' + + '- Source A — https://a.test/report\n' + + '- https://b.test/data', + }, + ], + stopReason: 'completed', + }); + expect(fetch).toHaveBeenCalledOnce(); + const [url, init] = fetch.mock.calls[0]!; + expect(url).toBe(PARALLEL_RESPONSES_URL); + expect(init).toMatchObject({ method: 'POST', redirect: 'error' }); + expect(JSON.parse(String(init?.body))).toEqual({ + model: 'parallel', + input: ' unchanged prompt ', + reasoning: { effort: 'medium' }, + stream: true, + }); + const headers = new Headers(init?.headers); + expect(headers.get('authorization')).toBe('Bearer parallel_test_provider'); + expect(headers.get('accept')).toBe('text/event-stream'); + expect(run.localAgent).toBeUndefined(); + const firstDispose = run.dispose(); + expect(run.dispose()).toBe(firstDispose); + await firstDispose; + }); + + it('preserves text block order and rejects blank or non-text prompts', () => { + expect( + researchPrompt( + request([ + { type: 'text', text: 'one' }, + { type: 'text', text: ' two' }, + ]) + ) + ).toBe('one two'); + expect(() => researchPrompt(request([]))).toThrow('only text blocks'); + expect(() => + researchPrompt(request([{ type: 'text', text: ' \n ' }])) + ).toThrow('must not be empty'); + expect(() => + researchPrompt( + request([{ type: 'reasoning', text: 'private' } as ContentBlock]) + ) + ).toThrow('only text blocks'); + }); + + it.each([ + new Response('denied', { status: 403 }), + streamResponse(sseEvent('response.failed', { status: 'failed' })), + streamResponse('event: response.completed\ndata: not-json\n\n'), + streamResponse( + 'event: response.created\ndata: {"type":"response.created"}\n\n' + ), + ])( + 'settles HTTP, in-band, and malformed stream failures', + async (response) => { + const fetch = vi.fn().mockResolvedValue(response); + const provider = new ParallelResponsesProvider({ + apiKey: 'parallel_test_provider', + fetch, + }); + const run = await provider.start(request()); + await expect(run.result).resolves.toEqual({ + output: [], + stopReason: 'error', + }); + expect(fetch).toHaveBeenCalledOnce(); + await run.dispose(); + } + ); + + it('reports safe diagnostics without leaking transport details', async () => { + const onError = vi.fn(); + const fetch = vi + .fn() + .mockRejectedValue( + new Error( + 'dsh-responses-subagent: Bearer parallel_secret_value failed with private body' + ) + ); + const provider = new ParallelResponsesProvider({ + apiKey: 'parallel_secret_value', + fetch, + onError, + }); + const run = await provider.start(request()); + + await expect(run.result).resolves.toEqual({ + output: [], + stopReason: 'error', + }); + expect(onError).toHaveBeenCalledOnce(); + expect(onError.mock.calls[0]?.[0]).toMatchObject({ + message: 'dsh-responses-subagent: Parallel Responses transport failed', + }); + expect(String(onError.mock.calls[0]?.[0])).not.toContain( + 'parallel_secret_value' + ); + await run.dispose(); + }); + + it('keeps diagnostic sink failures inside the non-rejecting run seam', async () => { + const provider = new ParallelResponsesProvider({ + apiKey: 'parallel_test_provider', + fetch: vi.fn().mockResolvedValue(new Response('', { status: 429 })), + onError: () => { + throw new Error('diagnostic sink failed'); + }, + }); + const run = await provider.start(request()); + await expect(run.result).resolves.toEqual({ + output: [], + stopReason: 'error', + }); + await run.dispose(); + }); +}); + +describe('Parallel Responses capacity and lifecycle', () => { + it('rejects cancellation before publication without making a request', async () => { + const fetch = vi.fn(); + const provider = new ParallelResponsesProvider({ + apiKey: 'parallel_test_provider', + fetch, + }); + const controller = new AbortController(); + controller.abort(); + await expect( + provider.start(request(undefined, controller.signal)) + ).rejects.toThrow('aborted'); + expect(fetch).not.toHaveBeenCalled(); + }); + + it('aborts an active request, settles, and disposes idempotently', async () => { + const fetch = abortableFetch(); + const provider = new ParallelResponsesProvider({ + apiKey: 'parallel_test_provider', + fetch, + }); + const controller = new AbortController(); + const run = await provider.start(request(undefined, controller.signal)); + controller.abort(); + await expect(run.result).resolves.toEqual({ + output: [], + stopReason: 'aborted', + }); + const disposal = run.dispose(); + expect(run.dispose()).toBe(disposal); + await disposal; + }); + + it('admits only two active requests and removes an aborted queued start', async () => { + const fetch = abortableFetch(); + const provider = new ParallelResponsesProvider({ + apiKey: 'parallel_test_provider', + fetch, + }); + const first = await provider.start(request()); + const second = await provider.start(request()); + const queuedController = new AbortController(); + const queued = provider.start(request(undefined, queuedController.signal)); + await vi.waitFor(() => expect(fetch).toHaveBeenCalledTimes(2)); + + queuedController.abort(); + await expect(queued).rejects.toThrow('waiting for capacity'); + expect(fetch).toHaveBeenCalledTimes(2); + + await first.dispose(); + const replacement = await provider.start(request()); + expect(fetch).toHaveBeenCalledTimes(3); + await Promise.all([second.dispose(), replacement.dispose()]); + }); + + it('times out once with no retry and releases capacity', async () => { + vi.useFakeTimers(); + const fetch = abortableFetch(); + const onError = vi.fn(); + const provider = new ParallelResponsesProvider({ + apiKey: 'parallel_test_provider', + fetch, + onError, + }); + const run = await provider.start(request()); + await vi.advanceTimersByTimeAsync(PARALLEL_RESPONSES_TIMEOUT_MS); + await expect(run.result).resolves.toEqual({ + output: [], + stopReason: 'error', + }); + expect(fetch).toHaveBeenCalledOnce(); + expect(onError).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'dsh-responses-subagent: Parallel Responses request timed out', + }) + ); + await run.dispose(); + }); +}); + +describe('Parallel Responses plugin registration', () => { + it('marks the optional API key as secret configuration', () => { + const schema = plugin.Config as typeof plugin.Config & { + dict: Record; + }; + expect(schema.dict.apiKey?.meta.role).toBe('secret'); + }); + + it('uses the launch environment and unregisters with its Cordis fiber', async () => { + const ctx = new Context(); + ctx.provide( + 'launchEnvironment', + createLaunchEnvironmentSnapshot([ + { + source: 'process', + values: { PARALLEL_API_KEY: 'parallel_test_environment' }, + }, + ]) + ); + await ctx.plugin(SubagentRuntime); + const fiber = await ctx.plugin(plugin, {}); + expect( + ctx.subagents.getProvider(plugin.PARALLEL_RESPONSES_PROVIDER_ID) + ).toMatchObject({ + name: 'parallel-responses', + capabilities: { + outputSchema: false, + depthLimit: false, + toolFilter: false, + persona: false, + }, + inheritsParentContext: false, + }); + await fiber.dispose(); + expect( + ctx.subagents.getProvider(plugin.PARALLEL_RESPONSES_PROVIDER_ID) + ).toBeUndefined(); + }); + + it('fails at load without an explicit or launch-time key', async () => { + const ctx = new Context(); + ctx.provide('launchEnvironment', createLaunchEnvironmentSnapshot([])); + await ctx.plugin(SubagentRuntime); + await expect(ctx.plugin(plugin, {})).rejects.toThrow( + 'apiKey or PARALLEL_API_KEY is required' + ); + }); + + it('has a narrow namespace export surface', () => { + expect('default' in plugin).toBe(false); + expect('ParallelResponsesProvider' in plugin).toBe(false); + expect(plugin.PARALLEL_RESPONSES_PROVIDER_ID).toBe('parallel-responses'); + }); +}); diff --git a/packages/dsh-responses-subagent/tsconfig.json b/packages/dsh-responses-subagent/tsconfig.json new file mode 100644 index 0000000..812215f --- /dev/null +++ b/packages/dsh-responses-subagent/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2024", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2024", "DOM", "DOM.Iterable"], + "types": ["node", "vitest/globals"], + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noImplicitOverride": true, + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "verbatimModuleSyntax": true, + "allowImportingTsExtensions": true, + "skipLibCheck": true + }, + "include": [ + "src/**/*.ts", + "tests/**/*.ts", + "tsdown.config.ts", + "vitest.e2e.config.ts" + ] +} diff --git a/packages/dsh-responses-subagent/tsdown.config.ts b/packages/dsh-responses-subagent/tsdown.config.ts new file mode 100644 index 0000000..beb6608 --- /dev/null +++ b/packages/dsh-responses-subagent/tsdown.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from 'tsdown'; + +export default defineConfig({ + entry: ['src/index.ts'], + outDir: 'lib', + format: ['esm'], + platform: 'node', + target: 'es2024', + dts: true, + sourcemap: false, + clean: true, + fixedExtension: false, + deps: { + neverBundle: [ + '@deepseek-ai/cordis', + '@deepseek-ai/dsh-launch-environment', + '@deepseek-ai/dsh-session', + '@deepseek-ai/dsh-subagent', + '@deepseek-ai/dsh-tool-subagent', + '@deepseek-ai/schemastery', + ], + }, +}); diff --git a/packages/dsh-responses-subagent/vitest.e2e.config.ts b/packages/dsh-responses-subagent/vitest.e2e.config.ts new file mode 100644 index 0000000..ffcbf73 --- /dev/null +++ b/packages/dsh-responses-subagent/vitest.e2e.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['tests/live-responses.e2e.ts'], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a8838fa..965aa03 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -58,6 +58,67 @@ importers: specifier: ^6.0.0 version: 6.0.192(zod@4.3.6) + packages/dsh-responses-subagent: + dependencies: + '@deepseek-ai/schemastery': + specifier: 3.18.1 + version: 3.18.1 + devDependencies: + '@deepseek-ai/cordis': + specifier: 4.0.1 + version: 4.0.1(@deepseek-ai/cordis-plugin-include@1.0.6)(@deepseek-ai/cordis-plugin-loader@1.0.2) + '@deepseek-ai/cordis-plugin-include': + specifier: 1.0.6 + version: 1.0.6(@deepseek-ai/cordis-plugin-loader@1.0.2)(@deepseek-ai/cordis@4.0.1) + '@deepseek-ai/cordis-plugin-loader': + specifier: 1.0.2 + version: 1.0.2(@deepseek-ai/cordis@4.0.1)(node-addon-require-builtin@0.1.5) + '@deepseek-ai/dsh': + specifier: 0.1.0-rc.6 + version: 0.1.0-rc.6(c422946f0a5c0641b2538ac43317dd99) + '@deepseek-ai/dsh-launch-environment': + specifier: 0.1.0-rc.6 + version: 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)) + '@deepseek-ai/dsh-llm': + specifier: 0.1.0-rc.6 + version: 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-attachment@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-timeout@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))) + '@deepseek-ai/dsh-session': + specifier: 0.1.0-rc.6 + version: 0.1.0-rc.6(6fd26f59436a18b115f326d6060415e6) + '@deepseek-ai/dsh-subagent': + specifier: 0.1.0-rc.6 + version: 0.1.0-rc.6(4aacb0222c4facc173b0ce0d58a42120) + '@deepseek-ai/dsh-system-prompt': + specifier: 0.1.0-rc.6 + version: 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-llm@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-attachment@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-timeout@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))))(@deepseek-ai/dsh-scope@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))) + '@deepseek-ai/dsh-tool-subagent': + specifier: 0.1.0-rc.6 + version: 0.1.0-rc.6(927fe785b6705493f00b6337d6e44987) + '@deepseek-ai/dsh-tools': + specifier: 0.1.0-rc.6 + version: 0.1.0-rc.6(f8724372086ccc1457fc84e7becee2e0) + '@types/node': + specifier: 26.2.0 + version: 26.2.0 + esbuild: + specifier: 0.28.2 + version: 0.28.2 + js-yaml: + specifier: 4.2.0 + version: 4.2.0 + oxlint: + specifier: 1.76.0 + version: 1.76.0 + tsdown: + specifier: 0.22.2 + version: 0.22.2(typescript@6.0.3) + typescript: + specifier: 6.0.3 + version: 6.0.3 + vitest: + specifier: 4.1.8 + version: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@26.2.0)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) + packages/dsh-web-search: dependencies: '@deepseek-ai/schemastery': From 1eaf9ee77b962ad43a6bac17cacb65e3a220c904 Mon Sep 17 00:00:00 2001 From: George Pickett Date: Sat, 22 Aug 2026 17:17:27 -0700 Subject: [PATCH 2/6] Simplify experimental DeepSeek Harness Responses subagent --- .github/workflows/ci.yml | 29 ++-- PUBLISHING.md | 10 +- .../dsh-responses-subagent/CONTRIBUTING.md | 125 --------------- packages/dsh-responses-subagent/LICENSE | 19 --- packages/dsh-responses-subagent/README.md | 135 +++------------- packages/dsh-responses-subagent/SECURITY.md | 31 ---- packages/dsh-responses-subagent/package.json | 26 +-- .../scripts/check-manifest.mjs | 120 -------------- .../scripts/check-package.mjs | 34 ---- .../scripts/verify-packed-artifact.mjs | 98 ------------ .../scripts/verify-packed-profile.mjs | 85 ---------- .../scripts/verify-profile-overlay.mjs | 84 ---------- .../dsh-responses-subagent/src/provider.ts | 151 ++++-------------- .../tests/live-responses.e2e.ts | 89 ----------- .../tests/loader-composition.spec.ts | 112 ------------- .../tests/provider.spec.ts | 50 ++---- packages/dsh-responses-subagent/tsconfig.json | 7 +- .../vitest.e2e.config.ts | 7 - pnpm-lock.yaml | 24 --- 19 files changed, 96 insertions(+), 1140 deletions(-) delete mode 100644 packages/dsh-responses-subagent/CONTRIBUTING.md delete mode 100644 packages/dsh-responses-subagent/LICENSE delete mode 100644 packages/dsh-responses-subagent/SECURITY.md delete mode 100644 packages/dsh-responses-subagent/scripts/check-manifest.mjs delete mode 100644 packages/dsh-responses-subagent/scripts/check-package.mjs delete mode 100644 packages/dsh-responses-subagent/scripts/verify-packed-artifact.mjs delete mode 100644 packages/dsh-responses-subagent/scripts/verify-packed-profile.mjs delete mode 100644 packages/dsh-responses-subagent/scripts/verify-profile-overlay.mjs delete mode 100644 packages/dsh-responses-subagent/tests/live-responses.e2e.ts delete mode 100644 packages/dsh-responses-subagent/tests/loader-composition.spec.ts delete mode 100644 packages/dsh-responses-subagent/vitest.e2e.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ad9ffd3..8aba3bf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,13 +85,12 @@ jobs: run: pnpm build dsh-compatibility: - name: DSH Compatibility (${{ matrix.package }}, Node ${{ matrix.node }}) + name: DSH Compatibility (Node ${{ matrix.node }}) runs-on: ubuntu-latest strategy: fail-fast: false matrix: node: [22.19.0, 24, 26] - package: [dsh-web-search, dsh-responses-subagent] steps: - name: Checkout uses: actions/checkout@v4 @@ -106,10 +105,10 @@ jobs: cache: 'pnpm' - name: Install dependencies - run: pnpm install --filter "@parallel-web/${{ matrix.package }}..." --frozen-lockfile + run: pnpm install --filter @parallel-web/dsh-web-search... --frozen-lockfile - name: Run deterministic plugin checks - run: pnpm --filter "@parallel-web/${{ matrix.package }}" check + run: pnpm --filter @parallel-web/dsh-web-search check - name: Verify packed DSH profile lifecycle shell: bash @@ -119,26 +118,26 @@ jobs: dsh_home="$(mktemp -d)" trap 'rm -rf "$pack_dir" "$dsh_home"' EXIT - pnpm --dir "packages/${{ matrix.package }}" pack --pack-destination "$pack_dir" + pnpm --dir packages/dsh-web-search pack --pack-destination "$pack_dir" artifact="$(find "$pack_dir" -name '*.tgz' -print -quit)" - version="$(node -p "require('./packages/${{ matrix.package }}/package.json').version")" + version="$(node -p "require('./packages/dsh-web-search/package.json').version")" - node "packages/${{ matrix.package }}/scripts/verify-packed-artifact.mjs" \ + node packages/dsh-web-search/scripts/verify-packed-artifact.mjs \ --tarball "$artifact" - DSH_HOME="$dsh_home" pnpm --dir "packages/${{ matrix.package }}" exec dsh \ + DSH_HOME="$dsh_home" pnpm --dir packages/dsh-web-search exec dsh \ --profile web --dump-config > "$dsh_home/before.yml" - DSH_HOME="$dsh_home" pnpm --dir "packages/${{ matrix.package }}" exec dsh \ + DSH_HOME="$dsh_home" pnpm --dir packages/dsh-web-search exec dsh \ plugin --profile web add "$artifact" - DSH_HOME="$dsh_home" pnpm --dir "packages/${{ matrix.package }}" exec dsh \ + DSH_HOME="$dsh_home" pnpm --dir packages/dsh-web-search exec dsh \ --profile web --dump-config > "$dsh_home/after.yml" - node "packages/${{ matrix.package }}/scripts/verify-packed-profile.mjs" \ + node packages/dsh-web-search/scripts/verify-packed-profile.mjs \ --dsh-home "$dsh_home" \ --expected-version "$version" - DSH_HOME="$dsh_home" pnpm --dir "packages/${{ matrix.package }}" exec dsh \ - plugin --profile web remove "@parallel-web/${{ matrix.package }}" - DSH_HOME="$dsh_home" pnpm --dir "packages/${{ matrix.package }}" exec dsh \ + DSH_HOME="$dsh_home" pnpm --dir packages/dsh-web-search exec dsh \ + plugin --profile web remove @parallel-web/dsh-web-search + DSH_HOME="$dsh_home" pnpm --dir packages/dsh-web-search exec dsh \ --profile web --dump-config > "$dsh_home/removed.yml" - node "packages/${{ matrix.package }}/scripts/verify-profile-overlay.mjs" \ + node packages/dsh-web-search/scripts/verify-profile-overlay.mjs \ --before "$dsh_home/before.yml" \ --after "$dsh_home/after.yml" \ --removed "$dsh_home/removed.yml" diff --git a/PUBLISHING.md b/PUBLISHING.md index 3580915..4e6e67d 100644 --- a/PUBLISHING.md +++ b/PUBLISHING.md @@ -1,9 +1,8 @@ # Publishing Guide -This monorepo publishes five npm packages, each versioned, tagged, and released **independently**: +This monorepo publishes four npm packages, each versioned, tagged, and released **independently**: - `@parallel-web/ai-sdk-tools` — `packages/ai-sdk-tools` -- `@parallel-web/dsh-responses-subagent` — `packages/dsh-responses-subagent` - `@parallel-web/dsh-web-search` — `packages/dsh-web-search` - `@parallel-web/opencode-plugin` — `packages/opencode-plugin` - `@parallel-web/pi-extension` — `packages/pi-extension` @@ -58,13 +57,12 @@ the reviewed bootstrap release manually from a clean, updated `main` checkout: ```bash pnpm install --frozen-lockfile -PACKAGE=dsh-responses-subagent -pnpm --filter "@parallel-web/$PACKAGE" check +pnpm --filter @parallel-web/dsh-web-search check BOOTSTRAP_DIR="$(mktemp -d)" -pnpm --dir "packages/$PACKAGE" pack --pack-destination "$BOOTSTRAP_DIR" +pnpm --dir packages/dsh-web-search pack --pack-destination "$BOOTSTRAP_DIR" BOOTSTRAP_TARBALL="$(find "$BOOTSTRAP_DIR" -name '*.tgz' -print -quit)" npm publish "$BOOTSTRAP_TARBALL" --access public --tag rc -npm view "@parallel-web/$PACKAGE" dist-tags --json +npm view @parallel-web/dsh-web-search dist-tags --json ``` The npm owner should inspect the tarball listing before the publish and complete npm's 2FA prompt. diff --git a/packages/dsh-responses-subagent/CONTRIBUTING.md b/packages/dsh-responses-subagent/CONTRIBUTING.md deleted file mode 100644 index 642e429..0000000 --- a/packages/dsh-responses-subagent/CONTRIBUTING.md +++ /dev/null @@ -1,125 +0,0 @@ -# Contributing - -Thank you for improving the Parallel Responses research subagent for DeepSeek -Harness. - -## Scope - -Version one is intentionally one remote, answer-producing research worker. A -caller gives it one explicit text prompt; it makes one bounded Responses request -and returns the answer plus source URLs through Harness's existing subagent -contract. - -The following are outside this package's current scope: - -- a general Harness LLM provider or replacement for `web_search`; -- parent conversation, file, workspace, or environment inheritance; -- child tools, structured output, background jobs, continuation, or polling; -- retries, endpoint overrides, arbitrary model selection, or additional knobs; -- changing `@parallel-web/dsh-web-search`; and -- compatibility claims beyond the pinned Harness release. - -Propose broader product or API changes before implementing them. - -## Set up - -Prerequisites are Node.js `^22.19.0 || >=24.0.0` and Corepack. Run these -commands from the monorepo root; the repository pins the pnpm version. - -```sh -corepack enable -pnpm install --frozen-lockfile -``` - -Run the complete keyless package suite: - -```sh -pnpm --filter @parallel-web/dsh-responses-subagent check -``` - -`check` performs strict type checking, linting, deterministic tests, the -ESM/type build, manifest validation, and a package-content allowlist check. - -`pnpm audit --prod` is monorepo-wide even with a workspace filter. Run it -separately and inspect whether a reported dependency path starts with -`packages__dsh-responses-subagent>`; unrelated advisories in another workspace -package do not make this package's keyless suite fail. - -## Verify the packed artifact - -Create one fresh tarball and verify its exact contents: - -```sh -PACK_DIR="$(mktemp -d)" -pnpm --dir packages/dsh-responses-subagent pack --pack-destination "$PACK_DIR" -PACK_TGZ="$(find "$PACK_DIR" -name '*.tgz' -print -quit)" -node packages/dsh-responses-subagent/scripts/verify-packed-artifact.mjs \ - --tarball "$PACK_TGZ" -``` - -Use a disposable DSH home to prove installation, composition, and removal: - -```sh -REVIEW_DSH_HOME="$(mktemp -d)" -DSH_HOME="$REVIEW_DSH_HOME" pnpm --dir packages/dsh-responses-subagent exec \ - dsh --profile web --dump-config > "$REVIEW_DSH_HOME/before.yml" -DSH_HOME="$REVIEW_DSH_HOME" pnpm --dir packages/dsh-responses-subagent exec \ - dsh plugin --profile web add "$PACK_TGZ" -DSH_HOME="$REVIEW_DSH_HOME" pnpm --dir packages/dsh-responses-subagent exec \ - dsh --profile web --dump-config > "$REVIEW_DSH_HOME/after.yml" -PACKAGE_VERSION="$(node -p "require('./packages/dsh-responses-subagent/package.json').version")" -node packages/dsh-responses-subagent/scripts/verify-packed-profile.mjs \ - --dsh-home "$REVIEW_DSH_HOME" \ - --expected-version "$PACKAGE_VERSION" -DSH_HOME="$REVIEW_DSH_HOME" pnpm --dir packages/dsh-responses-subagent exec \ - dsh plugin --profile web remove @parallel-web/dsh-responses-subagent -DSH_HOME="$REVIEW_DSH_HOME" pnpm --dir packages/dsh-responses-subagent exec \ - dsh --profile web --dump-config > "$REVIEW_DSH_HOME/removed.yml" -node packages/dsh-responses-subagent/scripts/verify-profile-overlay.mjs \ - --before "$REVIEW_DSH_HOME/before.yml" \ - --after "$REVIEW_DSH_HOME/after.yml" \ - --removed "$REVIEW_DSH_HOME/removed.yml" -``` - -The packed-profile check verifies the public provider registration and one -shared Cordis/subagent runtime. The overlay check proves that both opt-in rows -appear only while installed and that the existing Search and built-in subagent -rows stay unchanged. - -## Live end-to-end test - -The normal suite is credentialless. `test:e2e` is opt-in because it sends one -real medium-tier Parallel Responses request and may cost $0.05 when successful. -Export `PARALLEL_API_KEY` in the shell first, then run exactly one attempt: - -```sh -pnpm --filter @parallel-web/dsh-responses-subagent test:e2e -``` - -The live receipt contains only package version, status, elapsed time, call -count, citation count, and URL hosts. It must not include the key, request -headers, raw server object, or complete answer. Do not retry a failed live test -automatically; diagnose it first. - -## Secrets and public artifacts - -- Never commit API keys, tokens, `.env` files, request headers, raw live API - responses, or full environment dumps. -- Use obvious sentinel values and `.test` or loopback hosts in deterministic - tests. -- Do not add local absolute paths, private repository facts, planning artifacts, - or evaluation outputs to package contents. -- Inspect `pnpm pack --dry-run` before release. Only the files listed in - `package.json` may ship. -- Preserve exact non-secret request counts and failure statuses in evaluation - receipts; do not log authorization headers or raw response objects. - -## Pull requests - -Keep changes narrow and include focused proof for request shape, citations, -cancellation, disposal, and capacity. Before requesting review, run the keyless -suite and packed install/remove workflow above, then inspect the monorepo-wide -production audit by dependency path. State explicitly whether the single live -test ran. - -For help, contact [support@parallel.ai](mailto:support@parallel.ai). diff --git a/packages/dsh-responses-subagent/LICENSE b/packages/dsh-responses-subagent/LICENSE deleted file mode 100644 index 7d7d9ed..0000000 --- a/packages/dsh-responses-subagent/LICENSE +++ /dev/null @@ -1,19 +0,0 @@ -Copyright 2026 Parallel - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/packages/dsh-responses-subagent/README.md b/packages/dsh-responses-subagent/README.md index 65a5bad..0fed2b5 100644 --- a/packages/dsh-responses-subagent/README.md +++ b/packages/dsh-responses-subagent/README.md @@ -1,137 +1,54 @@ -# Parallel Responses research for DeepSeek Harness +# Parallel Responses for DeepSeek Harness -This opt-in plugin adds a `parallel_research` tool to DeepSeek Harness. The -tool sends one explicit, self-contained research prompt to +This experimental plugin adds an opt-in `parallel_research` tool to DeepSeek +Harness. Each call sends one self-contained text prompt to [Parallel Responses](https://docs.parallel.ai/api-reference/responses-beta/create-a-response) -and returns the final answer with its source URLs visible to the parent model. +and returns the completed answer with its citations. -This is a focused remote research worker, not a general Harness model provider. -It does not receive parent conversation history, files, workspace state, tools, -or ambient environment details. It does not change Harness's built-in -`web_search` tool or the separate `@parallel-web/dsh-web-search` package. +It does not replace `web_search` or change +`@parallel-web/dsh-web-search`. It never sends parent conversation history, +files, workspace state, tools, or ambient environment details. -## Quick start +## Install from this checkout -You will need: - -- Node.js 22.19 or later in the 22.x series, or Node.js 24 or newer; -- pnpm 10 or newer; -- a model API key supported by your Harness setup; and -- a [Parallel API key](https://platform.parallel.ai/). - -Check the supported Harness version without installing it globally: - -```sh -npx --yes @deepseek-ai/dsh@0.1.0-rc.6 --version -``` - -You should see `0.1.0-rc.6`. Harness is still a developer preview, so later -versions may need a plugin update. - -### 1. Install the plugin - -Install the release candidate into the `web` profile: +The package is private and has not been published. Build and pack it from the +repository root, then add the local tarball to Harness's `web` profile: ```sh +pnpm --filter @parallel-web/dsh-responses-subagent build +pnpm --dir packages/dsh-responses-subagent pack --pack-destination /tmp npx --yes @deepseek-ai/dsh@0.1.0-rc.6 \ plugin --profile web add \ - @parallel-web/dsh-responses-subagent@rc + /tmp/parallel-web-dsh-responses-subagent-0.1.0-rc.0.tgz ``` -The `@rc` suffix installs the current release candidate. After version `0.1.0` -is stable, you can leave the suffix off. - -### 2. Start Harness - -Make the Parallel key available in the terminal that starts Harness: +Start Harness from a shell where `PARALLEL_API_KEY` is already set: ```sh -export PARALLEL_API_KEY="your-key" npx --yes @deepseek-ai/dsh@0.1.0-rc.6 web ``` -Open [http://127.0.0.1:3080](http://127.0.0.1:3080). Configure a parent model -and workspace in Harness, then start a session. - -### 3. Delegate a research question +Ask the parent agent to call `parallel_research` with a complete, standalone +research question. The tool waits for the answer and returns its source URLs. -Ask Harness to use the dedicated tool and give it a standalone question, for -example: +## Behavior -> Use `parallel_research` to explain the current DeepSeek Harness plugin model. -> Include useful primary sources and their links. +- One `POST /v1/responses` request per accepted research task. +- Fixed `parallel` model, medium reasoning effort, and no retries. +- At most two concurrent requests, with a 10-minute timeout. +- Parent cancellation or disposal aborts the remote request. +- No parent history, files, background jobs, continuation, or child tools. +- Compatibility is pinned to DeepSeek Harness `0.1.0-rc.6`. -The call runs in the foreground. The parent waits for the remote research -answer, then continues with the returned text and source URLs. - -## Check that it installed - -Inspect the profile: +Run the package checks from the repository root: ```sh -npx --yes @deepseek-ai/dsh@0.1.0-rc.6 \ - --profile web --dump-config | \ - grep -E 'subagent-parallel-responses|tool-subagent-parallel-responses|parallel_research' +pnpm --filter @parallel-web/dsh-responses-subagent check ``` -You should see one provider row and one tool row. The tool row should name -`parallel_research`, select the `parallel-responses` provider, and set -`enableRunInBackground: false`. - -## Fixed v1 behavior - -Version one intentionally has a small contract: - -- one `POST https://api.parallel.ai/v1/responses` request per accepted run; -- fixed `model: "parallel"`, medium reasoning effort, and streaming transport; -- at most two active requests in one plugin instance; -- a fixed 10-minute internal timeout and no automatic retries; -- text-only, self-contained prompts; -- no background jobs, continuation, child tools, structured output, endpoint - override, or arbitrary model selection; and -- caller cancellation and `SubagentRun.dispose()` abort outstanding work and - wait for cleanup. Plugin disposal unregisters the provider; callers retain - ownership of runs they already started. - -Parallel charges Responses by successful request. Review current pricing before -running many calls; the medium reasoning tier is currently documented at $0.05 -per successful request. - -The only package setting is an optional literal `apiKey`. Prefer -`PARALLEL_API_KEY`: profile files and configuration dumps are readable text, -whereas the plugin's schema only protects a literal key from ordinary config -display. - -## If something goes wrong - -- **The provider is unavailable:** confirm `PARALLEL_API_KEY` is set in the same - terminal that starts Harness. -- **The request is rejected immediately:** pass non-empty text only, and make - the prompt fully understandable without the parent conversation. -- **The remote call fails:** inspect the Harness warning. It reports a safe HTTP, - Responses, timeout, or generic transport diagnostic without response bodies, - headers, nested causes, stack traces, or credentials. The plugin deliberately - does not retry. -- **The capacity limit is reached:** wait for an active call to finish or cancel - one before starting more work. -- **Port 3080 is in use:** stop the older Harness process, then start Harness - again. - -## Remove +To remove the experiment: ```sh npx --yes @deepseek-ai/dsh@0.1.0-rc.6 \ plugin --profile web remove @parallel-web/dsh-responses-subagent ``` - -Restart Harness after removing the plugin. - -## Development and security - -See [CONTRIBUTING.md](./CONTRIBUTING.md) for local validation and the packed -install/remove workflow. Report vulnerabilities according to -[SECURITY.md](./SECURITY.md). - -For help, contact [support@parallel.ai](mailto:support@parallel.ai). - -MIT licensed. diff --git a/packages/dsh-responses-subagent/SECURITY.md b/packages/dsh-responses-subagent/SECURITY.md deleted file mode 100644 index 74f4dc2..0000000 --- a/packages/dsh-responses-subagent/SECURITY.md +++ /dev/null @@ -1,31 +0,0 @@ -# Security Policy - -## Reporting a vulnerability - -Please report suspected vulnerabilities privately to -[security@parallel.ai](mailto:security@parallel.ai). Include -`dsh-responses-subagent security` in the subject line. - -Include the affected package and Harness versions, a minimal reproduction, the -expected impact, and any suggested mitigation. Remove API keys, tokens, request -headers, parent prompts, full research answers, personal data, and raw customer -or production data before sending the report. - -Do not open a public issue or publish exploit details before Parallel has had a -reasonable opportunity to investigate and coordinate a fix. We appreciate -responsible disclosure and will use the contact information in your report to -coordinate next steps. - -## Supported versions - -Until the first public release, security fixes are prepared on the repository's -main development line. After release, the latest published version is the -supported line unless a release notice states otherwise. - -## Operational guidance - -Keep `PARALLEL_API_KEY` in the environment that launches Harness rather than in -a profile file. Configuration dumps are readable text. Delegate only prompts -that are safe to send to a remote research service: the plugin intentionally -sends the explicit prompt, but it cannot remove sensitive text that a caller -puts in that prompt. Review logs and evaluation artifacts before sharing them. diff --git a/packages/dsh-responses-subagent/package.json b/packages/dsh-responses-subagent/package.json index ec74f18..50227f0 100644 --- a/packages/dsh-responses-subagent/package.json +++ b/packages/dsh-responses-subagent/package.json @@ -2,6 +2,7 @@ "name": "@parallel-web/dsh-responses-subagent", "description": "Parallel Responses research subagent for DeepSeek Harness", "version": "0.1.0-rc.0", + "private": true, "author": "Parallel Web", "license": "MIT", "type": "module", @@ -22,14 +23,8 @@ "lib/index.js", "lib/index.d.ts", "cordis.patch.yml", - "README.md", - "CONTRIBUTING.md", - "SECURITY.md", - "LICENSE" + "README.md" ], - "publishConfig": { - "access": "public" - }, "repository": { "type": "git", "url": "git+https://github.com/parallel-web/parallel-npm-packages.git", @@ -48,14 +43,11 @@ }, "scripts": { "build": "tsdown", - "check": "pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run build && pnpm run check:manifest && pnpm run check:package", - "check:manifest": "node scripts/check-manifest.mjs", - "check:package": "node scripts/check-package.mjs", + "check": "pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run build", "clean": "rm -rf lib", - "lint": "oxlint src tests scripts tsdown.config.ts vitest.e2e.config.ts", + "lint": "oxlint src tests tsdown.config.ts", "prepare": "pnpm run build", - "test": "vitest run --exclude tests/live-responses.e2e.ts", - "test:e2e": "vitest run --config vitest.e2e.config.ts", + "test": "vitest run", "typecheck": "tsc --noEmit" }, "peerDependencies": { @@ -70,19 +62,11 @@ }, "devDependencies": { "@deepseek-ai/cordis": "4.0.1", - "@deepseek-ai/cordis-plugin-include": "1.0.6", - "@deepseek-ai/cordis-plugin-loader": "1.0.2", - "@deepseek-ai/dsh": "0.1.0-rc.6", "@deepseek-ai/dsh-launch-environment": "0.1.0-rc.6", - "@deepseek-ai/dsh-llm": "0.1.0-rc.6", "@deepseek-ai/dsh-session": "0.1.0-rc.6", "@deepseek-ai/dsh-subagent": "0.1.0-rc.6", - "@deepseek-ai/dsh-system-prompt": "0.1.0-rc.6", "@deepseek-ai/dsh-tool-subagent": "0.1.0-rc.6", - "@deepseek-ai/dsh-tools": "0.1.0-rc.6", "@types/node": "26.2.0", - "esbuild": "0.28.2", - "js-yaml": "4.2.0", "oxlint": "1.76.0", "tsdown": "0.22.2", "typescript": "6.0.3", diff --git a/packages/dsh-responses-subagent/scripts/check-manifest.mjs b/packages/dsh-responses-subagent/scripts/check-manifest.mjs deleted file mode 100644 index 36a0ce4..0000000 --- a/packages/dsh-responses-subagent/scripts/check-manifest.mjs +++ /dev/null @@ -1,120 +0,0 @@ -import assert from 'node:assert/strict'; -import { readFile } from 'node:fs/promises'; -import { entryListSchema } from '@deepseek-ai/cordis-plugin-include'; -import yaml from 'js-yaml'; - -const manifest = JSON.parse( - await readFile(new URL('../package.json', import.meta.url), 'utf8') -); -const rootManifest = JSON.parse( - await readFile(new URL('../../../package.json', import.meta.url), 'utf8') -); - -assert.equal(manifest.name, '@parallel-web/dsh-responses-subagent'); -assert.equal(manifest.version, '0.1.0-rc.0'); -assert.equal(manifest.author, 'Parallel Web'); -assert.equal(manifest.type, 'module'); -assert.equal(manifest.sideEffects, false); -assert.equal(manifest.license, 'MIT'); -assert.deepEqual(manifest.repository, { - type: 'git', - url: 'git+https://github.com/parallel-web/parallel-npm-packages.git', - directory: 'packages/dsh-responses-subagent', -}); -assert.equal(manifest.packageManager, undefined); -assert.equal(rootManifest.packageManager, 'pnpm@11.21.0'); -assert.equal(manifest.engines.node, '^22.19.0 || >=24.0.0'); -assert.equal(manifest.dsh?.bundle?.patch, './cordis.patch.yml'); -assert.deepEqual(manifest.dependencies, { - '@deepseek-ai/schemastery': '3.18.1', -}); -assert.deepEqual(manifest.peerDependencies, { - '@deepseek-ai/cordis': '4.0.1', - '@deepseek-ai/dsh-launch-environment': '0.1.0-rc.6', - '@deepseek-ai/dsh-session': '0.1.0-rc.6', - '@deepseek-ai/dsh-subagent': '0.1.0-rc.6', - '@deepseek-ai/dsh-tool-subagent': '0.1.0-rc.6', -}); -for (const [name, version] of Object.entries(manifest.peerDependencies)) { - assert.equal( - manifest.devDependencies[name], - version, - `${name} must have a matching exact development dependency` - ); -} -assert.equal(manifest.devDependencies['@deepseek-ai/dsh'], '0.1.0-rc.6'); -assert.equal(manifest.devDependencies['@deepseek-ai/dsh-llm'], '0.1.0-rc.6'); -assert.equal( - manifest.devDependencies['@deepseek-ai/cordis-plugin-include'], - '1.0.6' -); -assert.equal( - manifest.devDependencies['@deepseek-ai/cordis-plugin-loader'], - '1.0.2' -); -assert.equal( - manifest.devDependencies['@deepseek-ai/dsh-system-prompt'], - '0.1.0-rc.6' -); -assert.equal(manifest.devDependencies['@deepseek-ai/dsh-tools'], '0.1.0-rc.6'); -assert.equal(manifest.devDependencies['js-yaml'], '4.2.0'); -assert.deepEqual(manifest.files, [ - 'lib/index.js', - 'lib/index.d.ts', - 'cordis.patch.yml', - 'README.md', - 'CONTRIBUTING.md', - 'SECURITY.md', - 'LICENSE', -]); - -const patchText = await readFile( - new URL('../cordis.patch.yml', import.meta.url), - 'utf8' -); -const patch = yaml.load(patchText, { schema: entryListSchema }); -assert.deepEqual(patch, [ - { - insert: [ - { - id: 'subagent-parallel-responses', - name: '@parallel-web/dsh-responses-subagent', - }, - { - id: 'tool-subagent-parallel-responses', - name: '@deepseek-ai/dsh-tool-subagent', - config: { - provider: 'parallel-responses', - toolName: 'parallel_research', - enableRunInBackground: false, - backgroundMode: 'one-shot', - maxDepth: 'provider-managed', - }, - }, - ], - }, -]); - -const built = await import( - new URL(`../lib/index.js?check=${Date.now()}`, import.meta.url) -); -assert.equal( - 'default' in built, - false, - 'the Cordis namespace plugin must not have a default export' -); -assert.deepEqual( - Object.keys(built).sort(), - ['Config', 'PARALLEL_RESPONSES_PROVIDER_ID', 'apply', 'inject', 'name'], - 'the public surface must stay limited to the Cordis plugin contract' -); -assert.equal(built.PARALLEL_RESPONSES_PROVIDER_ID, 'parallel-responses'); -assert.equal(built.name, 'subagent-parallel-responses'); -assert.deepEqual(built.inject, ['subagents']); -assert.equal( - built.Config.dict.apiKey.meta.role, - 'secret', - 'the packaged API key schema must retain secret redaction metadata' -); - -process.stdout.write('manifest, patch, and public export surface are valid\n'); diff --git a/packages/dsh-responses-subagent/scripts/check-package.mjs b/packages/dsh-responses-subagent/scripts/check-package.mjs deleted file mode 100644 index 9ddd7b6..0000000 --- a/packages/dsh-responses-subagent/scripts/check-package.mjs +++ /dev/null @@ -1,34 +0,0 @@ -import assert from 'node:assert/strict'; -import { spawnSync } from 'node:child_process'; - -const pnpmCli = process.env.npm_execpath; -assert.ok(pnpmCli, 'npm_execpath is required to run the pinned pnpm CLI'); - -const result = spawnSync( - process.execPath, - [pnpmCli, 'pack', '--dry-run', '--json'], - { - cwd: new URL('..', import.meta.url), - encoding: 'utf8', - } -); -if (result.status !== 0) { - process.stderr.write(result.stderr); - process.exit(result.status ?? 1); -} - -const jsonStart = result.stdout.lastIndexOf('\n{'); -assert.notEqual(jsonStart, -1, 'pnpm pack did not emit a JSON receipt'); -const pack = JSON.parse(result.stdout.slice(jsonStart + 1)); -const files = pack.files.map((file) => file.path).sort(); -assert.deepEqual(files, [ - 'CONTRIBUTING.md', - 'LICENSE', - 'README.md', - 'SECURITY.md', - 'cordis.patch.yml', - 'lib/index.d.ts', - 'lib/index.js', - 'package.json', -]); -process.stdout.write('package dry-run allowlist is valid\n'); diff --git a/packages/dsh-responses-subagent/scripts/verify-packed-artifact.mjs b/packages/dsh-responses-subagent/scripts/verify-packed-artifact.mjs deleted file mode 100644 index 47632af..0000000 --- a/packages/dsh-responses-subagent/scripts/verify-packed-artifact.mjs +++ /dev/null @@ -1,98 +0,0 @@ -import assert from 'node:assert/strict'; -import { spawnSync } from 'node:child_process'; -import { resolve } from 'node:path'; - -const tarballIndex = process.argv.indexOf('--tarball'); -assert.notEqual( - tarballIndex, - -1, - 'usage: verify-packed-artifact.mjs --tarball ' -); -const tarballArgument = process.argv[tarballIndex + 1]; -assert.ok(tarballArgument, 'missing tarball path'); -const tarball = resolve(tarballArgument); - -function tar(args) { - const result = spawnSync('tar', args, { encoding: 'utf8' }); - if (result.status !== 0) { - process.stderr.write(result.stderr); - process.exit(result.status ?? 1); - } - return result.stdout; -} - -const entries = tar(['-tzf', tarball]) - .split('\n') - .filter(Boolean) - .filter((entry) => !entry.endsWith('/')) - .sort(); - -assert.deepEqual(entries, [ - 'package/CONTRIBUTING.md', - 'package/LICENSE', - 'package/README.md', - 'package/SECURITY.md', - 'package/cordis.patch.yml', - 'package/lib/index.d.ts', - 'package/lib/index.js', - 'package/package.json', -]); - -const manifest = JSON.parse(tar(['-xOzf', tarball, 'package/package.json'])); -assert.equal(manifest.name, '@parallel-web/dsh-responses-subagent'); -assert.equal(manifest.version, '0.1.0-rc.0'); -assert.equal(manifest.author, 'Parallel Web'); -assert.deepEqual(manifest.repository, { - type: 'git', - url: 'git+https://github.com/parallel-web/parallel-npm-packages.git', - directory: 'packages/dsh-responses-subagent', -}); -assert.equal(manifest.dsh?.bundle?.patch, './cordis.patch.yml'); -assert.deepEqual(manifest.dependencies, { - '@deepseek-ai/schemastery': '3.18.1', -}); -for (const name of [ - '@deepseek-ai/cordis', - '@deepseek-ai/dsh-launch-environment', - '@deepseek-ai/dsh-session', - '@deepseek-ai/dsh-subagent', - '@deepseek-ai/dsh-tool-subagent', -]) { - assert.equal( - manifest.peerDependencies[name], - name === '@deepseek-ai/cordis' ? '4.0.1' : '0.1.0-rc.6' - ); -} - -const combinedPublicText = entries - .filter((entry) => /\.(?:js|d\.ts|md|yml|json)$/.test(entry)) - .map((entry) => tar(['-xOzf', tarball, entry])) - .join('\n'); - -for (const forbidden of [ - `/${'Users'}/`, - ['developer', 'parallel', 'code'].join('/'), - `${'.agent'}/`, - 'Authorization: Bearer', -]) { - assert.equal( - combinedPublicText.includes(forbidden), - false, - `packed artifact contains forbidden text: ${forbidden}` - ); -} - -for (const required of [ - 'https://api.parallel.ai/v1/responses', - 'parallel-responses', - 'parallel_research', - 'enableRunInBackground: false', -]) { - assert.equal( - combinedPublicText.includes(required), - true, - `packed artifact must retain contract text: ${required}` - ); -} - -process.stdout.write(`packed artifact valid: ${tarball}\n`); diff --git a/packages/dsh-responses-subagent/scripts/verify-packed-profile.mjs b/packages/dsh-responses-subagent/scripts/verify-packed-profile.mjs deleted file mode 100644 index dc38fd4..0000000 --- a/packages/dsh-responses-subagent/scripts/verify-packed-profile.mjs +++ /dev/null @@ -1,85 +0,0 @@ -import assert from 'node:assert/strict'; -import { realpathSync } from 'node:fs'; -import { createRequire } from 'node:module'; -import { join, resolve } from 'node:path'; -import { pathToFileURL } from 'node:url'; - -function argument(name) { - const index = process.argv.indexOf(name); - assert.notEqual(index, -1, `missing ${name}`); - const value = process.argv[index + 1]; - assert.ok(value, `missing value for ${name}`); - return value; -} - -const dshHome = resolve(argument('--dsh-home')); -const expectedVersion = argument('--expected-version'); -const profileManifest = join(dshHome, 'profiles', 'web', 'package.json'); -const profileRequire = createRequire(profileManifest); -const pluginManifest = profileRequire.resolve( - '@parallel-web/dsh-responses-subagent/package.json' -); -const pluginRequire = createRequire(pluginManifest); - -const pluginPackage = pluginRequire(pluginManifest); -assert.equal(pluginPackage.version, expectedVersion); - -const resolutions = {}; -for (const packageName of [ - '@deepseek-ai/cordis', - '@deepseek-ai/dsh-launch-environment', - '@deepseek-ai/dsh-session', - '@deepseek-ai/dsh-subagent', - '@deepseek-ai/dsh-tool-subagent', -]) { - const fromProfile = realpathSync(profileRequire.resolve(packageName)); - const fromPlugin = realpathSync(pluginRequire.resolve(packageName)); - assert.equal( - fromPlugin, - fromProfile, - `${packageName} must resolve to one shared entrypoint` - ); - resolutions[packageName] = fromProfile; -} - -const pluginEntry = profileRequire.resolve( - '@parallel-web/dsh-responses-subagent' -); -const plugin = await import(pathToFileURL(pluginEntry).href); -const cordis = await import( - pathToFileURL(resolutions['@deepseek-ai/cordis']).href -); -const subagents = await import( - pathToFileURL(resolutions['@deepseek-ai/dsh-subagent']).href -); - -assert.equal('default' in plugin, false); -assert.equal(plugin.PARALLEL_RESPONSES_PROVIDER_ID, 'parallel-responses'); -assert.equal(plugin.Config.dict.apiKey.meta.role, 'secret'); - -const ctx = new cordis.Context(); -const runtimeFiber = await ctx.plugin(subagents.default); -const providerFiber = await ctx.plugin(plugin, { - apiKey: 'parallel_test_packed_profile', -}); -assert.deepEqual(ctx.subagents.list(), ['parallel-responses']); -const provider = ctx.subagents.getProvider('parallel-responses'); -assert.ok(provider); -assert.equal(provider.name, 'parallel-responses'); -assert.deepEqual(provider.capabilities, { - outputSchema: false, - depthLimit: false, - toolFilter: false, - persona: false, -}); -assert.equal(provider.inheritsParentContext, false); -assert.equal(typeof provider.start, 'function'); -assert.equal('prepareContinuable' in provider, false); - -await providerFiber.dispose(); -assert.equal(ctx.subagents.getProvider('parallel-responses'), undefined); -await runtimeFiber.dispose(); - -process.stdout.write( - `packed profile valid: ${JSON.stringify({ version: pluginPackage.version, shared: Object.keys(resolutions) })}\n` -); diff --git a/packages/dsh-responses-subagent/scripts/verify-profile-overlay.mjs b/packages/dsh-responses-subagent/scripts/verify-profile-overlay.mjs deleted file mode 100644 index e77dad5..0000000 --- a/packages/dsh-responses-subagent/scripts/verify-profile-overlay.mjs +++ /dev/null @@ -1,84 +0,0 @@ -import assert from 'node:assert/strict'; -import { readFile } from 'node:fs/promises'; -import { resolve } from 'node:path'; -import { entryListSchema } from '@deepseek-ai/cordis-plugin-include'; -import yaml from 'js-yaml'; - -function argument(name) { - const index = process.argv.indexOf(name); - assert.notEqual(index, -1, `missing ${name}`); - const value = process.argv[index + 1]; - assert.ok(value, `missing value for ${name}`); - return resolve(value); -} - -async function load(path) { - return yaml.load(await readFile(path, 'utf8'), { schema: entryListSchema }); -} - -function flatten(entries) { - const rows = []; - for (const entry of entries) { - rows.push(entry); - if (entry?.group === true && Array.isArray(entry.config)) { - rows.push(...flatten(entry.config)); - } - } - return rows; -} - -function one(rows, id) { - const matches = rows.filter((row) => row?.id === id); - assert.equal(matches.length, 1, `expected exactly one ${id} row`); - return matches[0]; -} - -const before = flatten(await load(argument('--before'))); -const after = flatten(await load(argument('--after'))); -const removed = flatten(await load(argument('--removed'))); - -for (const rows of [before, removed]) { - assert.equal( - rows.some((row) => row?.id === 'subagent-parallel-responses'), - false - ); - assert.equal( - rows.some((row) => row?.id === 'tool-subagent-parallel-responses'), - false - ); -} - -assert.deepEqual(one(after, 'subagent-parallel-responses'), { - id: 'subagent-parallel-responses', - name: '@parallel-web/dsh-responses-subagent', -}); -assert.deepEqual(one(after, 'tool-subagent-parallel-responses'), { - id: 'tool-subagent-parallel-responses', - name: '@deepseek-ai/dsh-tool-subagent', - config: { - provider: 'parallel-responses', - toolName: 'parallel_research', - enableRunInBackground: false, - backgroundMode: 'one-shot', - maxDepth: 'provider-managed', - }, -}); - -for (const id of [ - 'subagent', - 'tool-subagent', - 'web', - 'web-search-deepseek', - 'tool-web', -]) { - assert.deepEqual(one(after, id), one(before, id), `${id} changed on install`); - assert.deepEqual( - one(removed, id), - one(before, id), - `${id} changed after removal` - ); -} - -process.stdout.write( - 'profile overlay valid: provider and foreground-only research tool install/remove cleanly; existing Search and subagent rows preserved\n' -); diff --git a/packages/dsh-responses-subagent/src/provider.ts b/packages/dsh-responses-subagent/src/provider.ts index e9c5f7c..502eaea 100644 --- a/packages/dsh-responses-subagent/src/provider.ts +++ b/packages/dsh-responses-subagent/src/provider.ts @@ -136,37 +136,6 @@ export function researchPrompt(request: ResolvedSubagentStartRequest): string { return prompt; } -function responseRequest(prompt: string): RequestInit { - return { - method: 'POST', - headers: { - Accept: 'text/event-stream', - Authorization: '', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - model: 'parallel', - input: prompt, - reasoning: { effort: 'medium' }, - stream: true, - }), - redirect: 'error', - }; -} - -function eventData(frame: string): { event?: string; data?: string } { - let event: string | undefined; - const data: string[] = []; - for (const line of frame.split(/\r?\n/u)) { - if (line.startsWith('event:')) event = line.slice(6).trimStart(); - if (line.startsWith('data:')) data.push(line.slice(5).replace(/^ /u, '')); - } - return { - ...(event === undefined ? {} : { event }), - ...(data.length === 0 ? {} : { data: data.join('\n') }), - }; -} - function citationLine(title: string | undefined, url: string): string { const compactTitle = title?.replace(/\s+/gu, ' ').trim(); return compactTitle === undefined || compactTitle.length === 0 @@ -174,9 +143,18 @@ function citationLine(title: string | undefined, url: string): string { : `- ${compactTitle} — ${url}`; } -function completedResult(payload: Record): SubagentResult { - const response = payload.response; - if (!isRecord(response) || !Array.isArray(response.output)) { +function completedResult(payload: unknown): SubagentResult { + if (!isRecord(payload) || payload.status !== 'completed') { + if (isRecord(payload) && payload.status === 'failed') { + throw new Error( + 'dsh-responses-subagent: Parallel Responses reported response.failed' + ); + } + throw new TypeError( + 'dsh-responses-subagent: Parallel Responses response was not completed' + ); + } + if (!Array.isArray(payload.output)) { throw new TypeError( 'dsh-responses-subagent: completed response has no output array' ); @@ -184,7 +162,7 @@ function completedResult(payload: Record): SubagentResult { const answerParts: string[] = []; const citations = new Map(); - for (const item of response.output) { + for (const item of payload.output) { if (!isRecord(item) || item.type !== 'message') continue; if (!Array.isArray(item.content)) { throw new TypeError( @@ -250,76 +228,6 @@ function completedResult(payload: Record): SubagentResult { return { output: [{ type: 'text', text }], stopReason: 'completed' }; } -function terminalResult(frame: string): SubagentResult | undefined { - const parsed = eventData(frame); - if (parsed.data === undefined) return undefined; - let value: unknown; - try { - value = JSON.parse(parsed.data); - } catch (error: unknown) { - throw new TypeError('dsh-responses-subagent: malformed SSE data', { - cause: error, - }); - } - if (!isRecord(value) || typeof value.type !== 'string') { - throw new TypeError('dsh-responses-subagent: malformed SSE event'); - } - if (parsed.event !== undefined && parsed.event !== value.type) { - throw new TypeError( - 'dsh-responses-subagent: SSE event name does not match its payload' - ); - } - if (value.type === 'response.completed') return completedResult(value); - if (value.type === 'response.failed') { - throw new Error( - 'dsh-responses-subagent: Parallel Responses reported response.failed' - ); - } - return undefined; -} - -async function readTerminal(response: Response): Promise { - if (!response.ok) { - throw new Error( - `dsh-responses-subagent: Parallel Responses returned HTTP ${response.status}` - ); - } - if (response.body === null) { - throw new Error( - 'dsh-responses-subagent: Parallel Responses returned no body' - ); - } - - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - let buffer = ''; - try { - for (;;) { - const chunk = await reader.read(); - buffer += decoder.decode(chunk.value, { stream: !chunk.done }); - for (;;) { - const delimiter = /\r?\n\r?\n/u.exec(buffer); - if (delimiter === null) break; - const frame = buffer.slice(0, delimiter.index); - buffer = buffer.slice(delimiter.index + delimiter[0].length); - const terminal = terminalResult(frame); - if (terminal !== undefined) return terminal; - } - if (chunk.done) break; - } - if (buffer.trim().length > 0) { - const terminal = terminalResult(buffer); - if (terminal !== undefined) return terminal; - } - throw new Error( - 'dsh-responses-subagent: Parallel Responses stream ended without a terminal event' - ); - } finally { - await reader.cancel(); - reader.releaseLock(); - } -} - /** Construction inputs kept private from Cordis configuration. */ export interface ParallelResponsesProviderOptions { readonly apiKey: string; @@ -364,10 +272,7 @@ export class ParallelResponsesProvider implements SubagentProvider { cancelled = true; controller.abort(cancellationError('during the remote run')); }; - const onAbort = (): void => { - requestCancel(); - }; - request.signal.addEventListener('abort', onAbort, { once: true }); + request.signal.addEventListener('abort', requestCancel, { once: true }); let timedOut = false; const timer = setTimeout(() => { @@ -381,15 +286,27 @@ export class ParallelResponsesProvider implements SubagentProvider { const result: Promise = (async () => { try { - const init = responseRequest(prompt); - const headers = new Headers(init.headers); - headers.set('Authorization', `Bearer ${this.apiKey}`); const response = await this.fetch(PARALLEL_RESPONSES_URL, { - ...init, - headers, + method: 'POST', + headers: { + Authorization: `Bearer ${this.apiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + model: 'parallel', + input: prompt, + reasoning: { effort: 'medium' }, + stream: false, + }), + redirect: 'error', signal: controller.signal, }); - const terminal = await readTerminal(response); + if (!response.ok) { + throw new Error( + `dsh-responses-subagent: Parallel Responses returned HTTP ${response.status}` + ); + } + const terminal = completedResult(await response.json()); if (cancelled) return { output: [], stopReason: 'aborted' }; if (timedOut) { throw new Error( @@ -420,7 +337,7 @@ export class ParallelResponsesProvider implements SubagentProvider { }; } finally { clearTimeout(timer); - request.signal.removeEventListener('abort', onAbort); + request.signal.removeEventListener('abort', requestCancel); release(); } })(); @@ -432,7 +349,7 @@ export class ParallelResponsesProvider implements SubagentProvider { result, dispose: (): Promise => { if (disposal !== undefined) return disposal; - request.signal.removeEventListener('abort', onAbort); + request.signal.removeEventListener('abort', requestCancel); requestCancel(); disposal = result.then(() => undefined); return disposal; diff --git a/packages/dsh-responses-subagent/tests/live-responses.e2e.ts b/packages/dsh-responses-subagent/tests/live-responses.e2e.ts deleted file mode 100644 index f341790..0000000 --- a/packages/dsh-responses-subagent/tests/live-responses.e2e.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { Context } from '@deepseek-ai/cordis'; -import type { ContentBlock } from '@deepseek-ai/dsh-llm'; -import SubagentRuntime, { - type ResolvedSubagentStartRequest, -} from '@deepseek-ai/dsh-subagent'; -import { describe, it } from 'vitest'; -import * as parallelResponses from '../src/index.ts'; - -const apiKey = process.env.PARALLEL_API_KEY; -if (apiKey === undefined || apiKey.length === 0) { - throw new Error( - 'PARALLEL_API_KEY must be present for the single guarded live attempt' - ); -} - -function request(signal: AbortSignal): ResolvedSubagentStartRequest { - return { - prompt: [ - { - type: 'text', - text: - 'Using current primary sources, explain what DeepSeek Harness is and ' + - 'identify its official GitHub repository. Give a concise answer with source links.', - }, - ], - signal, - parent: {} as ResolvedSubagentStartRequest['parent'], - descriptor: {} as ResolvedSubagentStartRequest['descriptor'], - }; -} - -function answerText(blocks: ContentBlock[]): string { - return blocks - .filter( - (block): block is Extract => - block.type === 'text' - ) - .map((block) => block.text) - .join(''); -} - -describe('Parallel Responses provider real API', () => { - it('returns one non-secret research result receipt', async () => { - const startedAt = performance.now(); - const ctx = new Context(); - const runtimeFiber = await ctx.plugin(SubagentRuntime); - const providerFiber = await ctx.plugin(parallelResponses, { apiKey }); - const provider = ctx.subagents.getProvider( - parallelResponses.PARALLEL_RESPONSES_PROVIDER_ID - ); - if (provider === undefined) { - throw new Error('Parallel Responses provider did not register'); - } - - const run = await provider.start(request(new AbortController().signal)); - try { - const result = await run.result; - if (result.stopReason !== 'completed') { - throw new Error( - `Parallel Responses live run ended with ${result.stopReason}` - ); - } - const text = answerText(result.output); - if (text.trim().length === 0) { - throw new Error('Parallel Responses live run returned no answer text'); - } - - const sourceSection = text.split('\n\nSources:\n')[1] ?? ''; - const urls = sourceSection.match(/https?:\/\/\S+/gu) ?? []; - const hostnames = urls.map((url) => new URL(url).hostname); - - process.stdout.write( - `${JSON.stringify({ - timestamp: new Date().toISOString(), - packageVersion: '0.1.0-rc.0', - status: 'passed', - elapsedMs: Math.round(performance.now() - startedAt), - callCount: 1, - citationCount: urls.length, - hostnames, - })}\n` - ); - } finally { - await run.dispose(); - await providerFiber.dispose(); - await runtimeFiber.dispose(); - } - }, 660_000); -}); diff --git a/packages/dsh-responses-subagent/tests/loader-composition.spec.ts b/packages/dsh-responses-subagent/tests/loader-composition.spec.ts deleted file mode 100644 index 5a7d824..0000000 --- a/packages/dsh-responses-subagent/tests/loader-composition.spec.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { mkdtemp, rm, writeFile } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; -import { pathToFileURL } from 'node:url'; -import { afterEach, describe, expect, it } from 'vitest'; -import { Context } from '@deepseek-ai/cordis'; -import Include from '@deepseek-ai/cordis-plugin-include'; -import Loader from '@deepseek-ai/cordis-plugin-loader'; -import SubagentRuntime from '@deepseek-ai/dsh-subagent'; -import SystemPrompt from '@deepseek-ai/dsh-system-prompt'; -import * as ToolSubagent from '@deepseek-ai/dsh-tool-subagent'; -import ToolRuntime from '@deepseek-ai/dsh-tools'; -import * as ParallelResponses from '../src/index.ts'; - -let temporaryRoot: string | undefined; -let context: Context | undefined; - -afterEach(async () => { - await context?.fiber.dispose(); - context = undefined; - if (temporaryRoot !== undefined) { - await rm(temporaryRoot, { recursive: true, force: true }); - } - temporaryRoot = undefined; -}); - -async function boot(): Promise { - temporaryRoot = await mkdtemp(join(tmpdir(), 'dsh-parallel-responses-')); - const configPath = join(temporaryRoot, 'cordis.yml'); - await writeFile( - configPath, - [ - "- name: '@deepseek-ai/dsh-system-prompt'", - "- name: '@deepseek-ai/dsh-tools'", - "- name: '@deepseek-ai/dsh-subagent'", - "- name: '@parallel-web/dsh-responses-subagent'", - ' config:', - ' apiKey: parallel_test_loader', - "- name: '@deepseek-ai/dsh-tool-subagent'", - ' config:', - ' provider: parallel-responses', - ' toolName: parallel_research', - ' enableRunInBackground: false', - ' backgroundMode: one-shot', - ' maxDepth: provider-managed', - '', - ].join('\n') - ); - - const ctx = new Context(); - context = ctx; - ctx.baseUrl = pathToFileURL(temporaryRoot).href + '/'; - await ctx.plugin(Loader); - ctx.loader.builtins.include = Include; - const modules = new Map([ - ['@deepseek-ai/dsh-system-prompt', SystemPrompt], - ['@deepseek-ai/dsh-tools', ToolRuntime], - ['@deepseek-ai/dsh-subagent', SubagentRuntime], - ['@parallel-web/dsh-responses-subagent', ParallelResponses], - ['@deepseek-ai/dsh-tool-subagent', ToolSubagent], - ]); - ctx.loader.internal = { - version: 'v2', - async import(specifier: string) { - if (!modules.has(specifier)) { - throw new Error(`unexpected Loader import: ${specifier}`); - } - return modules.get(specifier); - }, - } as unknown as NonNullable; - await ctx.loader.create({ - name: 'cordis:include', - config: { path: pathToFileURL(configPath).href }, - }); - await ctx.loader.await(); - return ctx; -} - -describe('Parallel Responses public Loader composition', () => { - it('registers one foreground-only research tool without a network request', async () => { - const ctx = await boot(); - - const provider = ctx.subagents.getProvider('parallel-responses'); - expect(provider).toMatchObject({ - name: 'parallel-responses', - capabilities: { - outputSchema: false, - depthLimit: false, - toolFilter: false, - persona: false, - }, - inheritsParentContext: false, - }); - expect('prepareContinuable' in (provider ?? {})).toBe(false); - - const schema = ctx.tools - .schemas() - .find((candidate) => candidate.name === 'parallel_research'); - expect(schema).toBeDefined(); - if (schema === undefined) { - throw new Error('parallel_research did not register'); - } - const properties = ( - schema.parameters as { properties?: Record } - ).properties; - expect(Object.keys(properties ?? {}).sort()).toEqual([ - 'description', - 'prompt', - ]); - expect(schema.description).not.toContain('job_output'); - }); -}); diff --git a/packages/dsh-responses-subagent/tests/provider.spec.ts b/packages/dsh-responses-subagent/tests/provider.spec.ts index 3fba83e..8e29f1c 100644 --- a/packages/dsh-responses-subagent/tests/provider.spec.ts +++ b/packages/dsh-responses-subagent/tests/provider.spec.ts @@ -1,6 +1,5 @@ import { Context } from '@deepseek-ai/cordis'; import { createLaunchEnvironmentSnapshot } from '@deepseek-ai/dsh-launch-environment'; -import type { ContentBlock } from '@deepseek-ai/dsh-llm'; import SubagentRuntime, { type ResolvedSubagentStartRequest, } from '@deepseek-ai/dsh-subagent'; @@ -13,6 +12,8 @@ import { researchPrompt, } from '../src/provider.ts'; +type ContentBlock = ResolvedSubagentStartRequest['prompt'][number]; + function request( prompt: ContentBlock[] = [{ type: 'text', text: 'research this exactly' }], signal = new AbortController().signal @@ -25,15 +26,11 @@ function request( }; } -function sseEvent(type: string, response: Record): string { - return `event: ${type}\ndata: ${JSON.stringify({ type, response })}\n\n`; -} - function completed( text = 'The researched answer.', annotations: unknown[] = [] -): string { - return sseEvent('response.completed', { +): Response { + return Response.json({ status: 'completed', output: [ { @@ -44,19 +41,6 @@ function completed( }); } -function streamResponse(...chunks: string[]): Response { - const encoder = new TextEncoder(); - return new Response( - new ReadableStream({ - start(controller) { - for (const chunk of chunks) controller.enqueue(encoder.encode(chunk)); - controller.close(); - }, - }), - { status: 200, headers: { 'Content-Type': 'text/event-stream' } } - ); -} - function abortableFetch() { return vi.fn( async (_input: string | URL | Request, init?: RequestInit) => @@ -77,7 +61,7 @@ afterEach(() => { describe('Parallel Responses request and output', () => { it('sends one fixed request and renders deduplicated title and URL citations', async () => { - const event = completed('Evidence-backed answer.', [ + const response = completed('Evidence-backed answer.', [ { type: 'url_citation', title: 'Source A', @@ -90,15 +74,7 @@ describe('Parallel Responses request and output', () => { }, { type: 'url_citation', url: 'https://b.test/data' }, ]); - const fetch = vi - .fn() - .mockResolvedValue( - streamResponse( - 'event: response.created\r\ndata: {"type":"response.created"}\r\n\r\n', - event.slice(0, 19), - event.slice(19) - ) - ); + const fetch = vi.fn().mockResolvedValue(response); const provider = new ParallelResponsesProvider({ apiKey: 'parallel_test_provider', fetch, @@ -127,11 +103,11 @@ describe('Parallel Responses request and output', () => { model: 'parallel', input: ' unchanged prompt ', reasoning: { effort: 'medium' }, - stream: true, + stream: false, }); const headers = new Headers(init?.headers); expect(headers.get('authorization')).toBe('Bearer parallel_test_provider'); - expect(headers.get('accept')).toBe('text/event-stream'); + expect(headers.get('content-type')).toBe('application/json'); expect(run.localAgent).toBeUndefined(); const firstDispose = run.dispose(); expect(run.dispose()).toBe(firstDispose); @@ -160,13 +136,11 @@ describe('Parallel Responses request and output', () => { it.each([ new Response('denied', { status: 403 }), - streamResponse(sseEvent('response.failed', { status: 'failed' })), - streamResponse('event: response.completed\ndata: not-json\n\n'), - streamResponse( - 'event: response.created\ndata: {"type":"response.created"}\n\n' - ), + Response.json({ status: 'failed' }), + new Response('not-json'), + Response.json({ status: 'completed', output: [] }), ])( - 'settles HTTP, in-band, and malformed stream failures', + 'settles HTTP, API, malformed JSON, and empty-answer failures', async (response) => { const fetch = vi.fn().mockResolvedValue(response); const provider = new ParallelResponsesProvider({ diff --git a/packages/dsh-responses-subagent/tsconfig.json b/packages/dsh-responses-subagent/tsconfig.json index 812215f..3a6d651 100644 --- a/packages/dsh-responses-subagent/tsconfig.json +++ b/packages/dsh-responses-subagent/tsconfig.json @@ -16,10 +16,5 @@ "allowImportingTsExtensions": true, "skipLibCheck": true }, - "include": [ - "src/**/*.ts", - "tests/**/*.ts", - "tsdown.config.ts", - "vitest.e2e.config.ts" - ] + "include": ["src/**/*.ts", "tests/**/*.ts", "tsdown.config.ts"] } diff --git a/packages/dsh-responses-subagent/vitest.e2e.config.ts b/packages/dsh-responses-subagent/vitest.e2e.config.ts deleted file mode 100644 index ffcbf73..0000000 --- a/packages/dsh-responses-subagent/vitest.e2e.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - test: { - include: ['tests/live-responses.e2e.ts'], - }, -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 965aa03..d58c59c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -67,45 +67,21 @@ importers: '@deepseek-ai/cordis': specifier: 4.0.1 version: 4.0.1(@deepseek-ai/cordis-plugin-include@1.0.6)(@deepseek-ai/cordis-plugin-loader@1.0.2) - '@deepseek-ai/cordis-plugin-include': - specifier: 1.0.6 - version: 1.0.6(@deepseek-ai/cordis-plugin-loader@1.0.2)(@deepseek-ai/cordis@4.0.1) - '@deepseek-ai/cordis-plugin-loader': - specifier: 1.0.2 - version: 1.0.2(@deepseek-ai/cordis@4.0.1)(node-addon-require-builtin@0.1.5) - '@deepseek-ai/dsh': - specifier: 0.1.0-rc.6 - version: 0.1.0-rc.6(c422946f0a5c0641b2538ac43317dd99) '@deepseek-ai/dsh-launch-environment': specifier: 0.1.0-rc.6 version: 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)) - '@deepseek-ai/dsh-llm': - specifier: 0.1.0-rc.6 - version: 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-attachment@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-timeout@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))) '@deepseek-ai/dsh-session': specifier: 0.1.0-rc.6 version: 0.1.0-rc.6(6fd26f59436a18b115f326d6060415e6) '@deepseek-ai/dsh-subagent': specifier: 0.1.0-rc.6 version: 0.1.0-rc.6(4aacb0222c4facc173b0ce0d58a42120) - '@deepseek-ai/dsh-system-prompt': - specifier: 0.1.0-rc.6 - version: 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-llm@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-attachment@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-brand@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)))(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))(@deepseek-ai/dsh-timeout@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))))(@deepseek-ai/dsh-scope@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1))) '@deepseek-ai/dsh-tool-subagent': specifier: 0.1.0-rc.6 version: 0.1.0-rc.6(927fe785b6705493f00b6337d6e44987) - '@deepseek-ai/dsh-tools': - specifier: 0.1.0-rc.6 - version: 0.1.0-rc.6(f8724372086ccc1457fc84e7becee2e0) '@types/node': specifier: 26.2.0 version: 26.2.0 - esbuild: - specifier: 0.28.2 - version: 0.28.2 - js-yaml: - specifier: 4.2.0 - version: 4.2.0 oxlint: specifier: 1.76.0 version: 1.76.0 From 3b2cb6f50461b37293800a718ebe47c5bdd216ed Mon Sep 17 00:00:00 2001 From: George Pickett Date: Sun, 23 Aug 2026 16:03:40 -0700 Subject: [PATCH 3/6] Finish DeepSeek Harness Parallel Responses research plugin --- .github/workflows/ci.yml | 29 ++-- PUBLISHING.md | 17 ++- README.md | 2 +- packages/dsh-responses-subagent/README.md | 65 +++++++-- packages/dsh-responses-subagent/package.json | 13 +- .../scripts/check-manifest.mjs | 105 ++++++++++++++ .../scripts/check-package.mjs | 32 +++++ .../scripts/verify-packed-artifact.mjs | 80 +++++++++++ .../scripts/verify-packed-profile.mjs | 91 +++++++++++++ .../scripts/verify-profile-overlay.mjs | 84 ++++++++++++ packages/dsh-responses-subagent/src/index.ts | 53 +++++++- .../dsh-responses-subagent/src/provider.ts | 55 +++++++- .../tests/provider.spec.ts | 128 ++++++++++++++++++ pnpm-lock.yaml | 9 ++ 14 files changed, 725 insertions(+), 38 deletions(-) create mode 100644 packages/dsh-responses-subagent/scripts/check-manifest.mjs create mode 100644 packages/dsh-responses-subagent/scripts/check-package.mjs create mode 100644 packages/dsh-responses-subagent/scripts/verify-packed-artifact.mjs create mode 100644 packages/dsh-responses-subagent/scripts/verify-packed-profile.mjs create mode 100644 packages/dsh-responses-subagent/scripts/verify-profile-overlay.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8aba3bf..ad9ffd3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,12 +85,13 @@ jobs: run: pnpm build dsh-compatibility: - name: DSH Compatibility (Node ${{ matrix.node }}) + name: DSH Compatibility (${{ matrix.package }}, Node ${{ matrix.node }}) runs-on: ubuntu-latest strategy: fail-fast: false matrix: node: [22.19.0, 24, 26] + package: [dsh-web-search, dsh-responses-subagent] steps: - name: Checkout uses: actions/checkout@v4 @@ -105,10 +106,10 @@ jobs: cache: 'pnpm' - name: Install dependencies - run: pnpm install --filter @parallel-web/dsh-web-search... --frozen-lockfile + run: pnpm install --filter "@parallel-web/${{ matrix.package }}..." --frozen-lockfile - name: Run deterministic plugin checks - run: pnpm --filter @parallel-web/dsh-web-search check + run: pnpm --filter "@parallel-web/${{ matrix.package }}" check - name: Verify packed DSH profile lifecycle shell: bash @@ -118,26 +119,26 @@ jobs: dsh_home="$(mktemp -d)" trap 'rm -rf "$pack_dir" "$dsh_home"' EXIT - pnpm --dir packages/dsh-web-search pack --pack-destination "$pack_dir" + pnpm --dir "packages/${{ matrix.package }}" pack --pack-destination "$pack_dir" artifact="$(find "$pack_dir" -name '*.tgz' -print -quit)" - version="$(node -p "require('./packages/dsh-web-search/package.json').version")" + version="$(node -p "require('./packages/${{ matrix.package }}/package.json').version")" - node packages/dsh-web-search/scripts/verify-packed-artifact.mjs \ + node "packages/${{ matrix.package }}/scripts/verify-packed-artifact.mjs" \ --tarball "$artifact" - DSH_HOME="$dsh_home" pnpm --dir packages/dsh-web-search exec dsh \ + DSH_HOME="$dsh_home" pnpm --dir "packages/${{ matrix.package }}" exec dsh \ --profile web --dump-config > "$dsh_home/before.yml" - DSH_HOME="$dsh_home" pnpm --dir packages/dsh-web-search exec dsh \ + DSH_HOME="$dsh_home" pnpm --dir "packages/${{ matrix.package }}" exec dsh \ plugin --profile web add "$artifact" - DSH_HOME="$dsh_home" pnpm --dir packages/dsh-web-search exec dsh \ + DSH_HOME="$dsh_home" pnpm --dir "packages/${{ matrix.package }}" exec dsh \ --profile web --dump-config > "$dsh_home/after.yml" - node packages/dsh-web-search/scripts/verify-packed-profile.mjs \ + node "packages/${{ matrix.package }}/scripts/verify-packed-profile.mjs" \ --dsh-home "$dsh_home" \ --expected-version "$version" - DSH_HOME="$dsh_home" pnpm --dir packages/dsh-web-search exec dsh \ - plugin --profile web remove @parallel-web/dsh-web-search - DSH_HOME="$dsh_home" pnpm --dir packages/dsh-web-search exec dsh \ + DSH_HOME="$dsh_home" pnpm --dir "packages/${{ matrix.package }}" exec dsh \ + plugin --profile web remove "@parallel-web/${{ matrix.package }}" + DSH_HOME="$dsh_home" pnpm --dir "packages/${{ matrix.package }}" exec dsh \ --profile web --dump-config > "$dsh_home/removed.yml" - node packages/dsh-web-search/scripts/verify-profile-overlay.mjs \ + node "packages/${{ matrix.package }}/scripts/verify-profile-overlay.mjs" \ --before "$dsh_home/before.yml" \ --after "$dsh_home/after.yml" \ --removed "$dsh_home/removed.yml" diff --git a/PUBLISHING.md b/PUBLISHING.md index 4e6e67d..7f70cd8 100644 --- a/PUBLISHING.md +++ b/PUBLISHING.md @@ -1,12 +1,17 @@ # Publishing Guide -This monorepo publishes four npm packages, each versioned, tagged, and released **independently**: +This monorepo tracks five publishable npm packages, each versioned, tagged, and released +**independently**: - `@parallel-web/ai-sdk-tools` — `packages/ai-sdk-tools` +- `@parallel-web/dsh-responses-subagent` — `packages/dsh-responses-subagent` - `@parallel-web/dsh-web-search` — `packages/dsh-web-search` - `@parallel-web/opencode-plugin` — `packages/opencode-plugin` - `@parallel-web/pi-extension` — `packages/pi-extension` +`@parallel-web/dsh-responses-subagent` has not yet been published. It can be installed only +from a local tarball until an npm organization owner completes its reviewed first release. + (`@parallel-web/oauth` in `packages/parallel-oauth` is `private` — it is bundled into the OpenCode plugin and Pi extension at build time and is never published.) @@ -53,16 +58,18 @@ Skipping that upgrade causes a misleading `404 Not Found` on the publish `PUT`. npm requires a package to exist before its trusted publisher can be configured. Adding a package to this repository intentionally does not publish it. An npm organization owner must first publish -the reviewed bootstrap release manually from a clean, updated `main` checkout: +the reviewed bootstrap release manually from a clean, updated `main` checkout. Set `PACKAGE` +to the directory name of the new package: ```bash +PACKAGE=dsh-responses-subagent pnpm install --frozen-lockfile -pnpm --filter @parallel-web/dsh-web-search check +pnpm --filter "@parallel-web/$PACKAGE" check BOOTSTRAP_DIR="$(mktemp -d)" -pnpm --dir packages/dsh-web-search pack --pack-destination "$BOOTSTRAP_DIR" +pnpm --dir "packages/$PACKAGE" pack --pack-destination "$BOOTSTRAP_DIR" BOOTSTRAP_TARBALL="$(find "$BOOTSTRAP_DIR" -name '*.tgz' -print -quit)" npm publish "$BOOTSTRAP_TARBALL" --access public --tag rc -npm view @parallel-web/dsh-web-search dist-tags --json +npm view "@parallel-web/$PACKAGE" dist-tags --json ``` The npm owner should inspect the tarball listing before the publish and complete npm's 2FA prompt. diff --git a/README.md b/README.md index 31442b2..b86a912 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Monorepo for @parallel-web npm packages. ## Packages - [`@parallel-web/ai-sdk-tools`](./packages/ai-sdk-tools) - AI SDK tools for Parallel Web -- [`@parallel-web/dsh-responses-subagent`](./packages/dsh-responses-subagent) - Parallel Responses research subagent for DeepSeek Harness +- [`@parallel-web/dsh-responses-subagent`](./packages/dsh-responses-subagent) - Parallel Responses research subagent for DeepSeek Harness; not yet published, install from a local tarball - [`@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 diff --git a/packages/dsh-responses-subagent/README.md b/packages/dsh-responses-subagent/README.md index 0fed2b5..4fb5858 100644 --- a/packages/dsh-responses-subagent/README.md +++ b/packages/dsh-responses-subagent/README.md @@ -1,8 +1,8 @@ # Parallel Responses for DeepSeek Harness -This experimental plugin adds an opt-in `parallel_research` tool to DeepSeek -Harness. Each call sends one self-contained text prompt to -[Parallel Responses](https://docs.parallel.ai/api-reference/responses-beta/create-a-response) +This DeepSeek Harness plugin adds an opt-in `parallel_research` tool. Each call +sends one self-contained text prompt to +[Parallel Responses](https://docs.parallel.ai/responses-api/examples/research-subagent) and returns the completed answer with its citations. It does not replace `web_search` or change @@ -11,8 +11,9 @@ files, workspace state, tools, or ambient environment details. ## Install from this checkout -The package is private and has not been published. Build and pack it from the -repository root, then add the local tarball to Harness's `web` profile: +This package is ready for a future release but has not been published to npm. +Until an npm organization owner completes its first release, install it from a +local tarball. From the repository root: ```sh pnpm --filter @parallel-web/dsh-responses-subagent build @@ -22,20 +23,66 @@ npx --yes @deepseek-ai/dsh@0.1.0-rc.6 \ /tmp/parallel-web-dsh-responses-subagent-0.1.0-rc.0.tgz ``` -Start Harness from a shell where `PARALLEL_API_KEY` is already set: +Make your [Parallel API key](https://platform.parallel.ai/) available in the +same terminal that starts Harness: ```sh +export PARALLEL_API_KEY="your-key" npx --yes @deepseek-ai/dsh@0.1.0-rc.6 web ``` +For an interactive session, configure a model and its API key in Harness +**Settings > Models**. The plugin works alongside +`@parallel-web/dsh-web-search`: use `web_search` for individual searches and +`parallel_research` when the parent should delegate a complete research task. + Ask the parent agent to call `parallel_research` with a complete, standalone research question. The tool waits for the answer and returns its source URLs. +The parent receives specific guidance to preserve the original question, avoid +inventing JSON schemas, cite primary sources, and make focused follow-up calls +when the first answer leaves an important gap. + +### After the first npm release + +The registry install below will work only after the package has actually been +published with the `rc` dist-tag. It is not currently available: + +```sh +npx --yes @deepseek-ai/dsh@0.1.0-rc.6 \ + plugin --profile web add \ + @parallel-web/dsh-responses-subagent@rc +``` + +## Research depth and concurrency + +Add optional settings to the `subagent-parallel-responses` entry in your Harness +profile, `~/.dsh/profiles/web/cordis.patch.yml`, when a workload needs a +different research strategy: + +```yaml +- id: subagent-parallel-responses + config: + effort: high + maxConcurrentRuns: 4 +``` + +`effort` accepts `low`, `medium`, or `high`; the default remains `medium`. +Use `low` for several independent, focused questions, `medium` for ordinary +multi-hop research, and `high` for difficult research that needs extensive +source comparison or synthesis. `maxConcurrentRuns` defaults to 2 and accepts +values from 1 through 20. Configure it deliberately when the parent should fan +out independent low-effort questions. Harness also applies its own +`maxParallelToolCalls` limit, which defaults to 10. The effective concurrency +is whichever configured limit is lower. To run more than 10 requests together, +raise both `maxConcurrentRuns` and Harness's **Settings > Plugins > Agent +loop > Parallel tool calls** setting. ## Behavior - One `POST /v1/responses` request per accepted research task. -- Fixed `parallel` model, medium reasoning effort, and no retries. -- At most two concurrent requests, with a 10-minute timeout. +- Fixed `parallel` model, configurable reasoning effort, and no retries. +- Configurable bounded concurrency, with a 10-minute timeout per request. +- Research answers include the returned citation titles and source URLs. - Parent cancellation or disposal aborts the remote request. - No parent history, files, background jobs, continuation, or child tools. - Compatibility is pinned to DeepSeek Harness `0.1.0-rc.6`. @@ -46,7 +93,7 @@ Run the package checks from the repository root: pnpm --filter @parallel-web/dsh-responses-subagent check ``` -To remove the experiment: +To remove the plugin: ```sh npx --yes @deepseek-ai/dsh@0.1.0-rc.6 \ diff --git a/packages/dsh-responses-subagent/package.json b/packages/dsh-responses-subagent/package.json index 50227f0..fcbc181 100644 --- a/packages/dsh-responses-subagent/package.json +++ b/packages/dsh-responses-subagent/package.json @@ -2,7 +2,6 @@ "name": "@parallel-web/dsh-responses-subagent", "description": "Parallel Responses research subagent for DeepSeek Harness", "version": "0.1.0-rc.0", - "private": true, "author": "Parallel Web", "license": "MIT", "type": "module", @@ -25,6 +24,9 @@ "cordis.patch.yml", "README.md" ], + "publishConfig": { + "access": "public" + }, "repository": { "type": "git", "url": "git+https://github.com/parallel-web/parallel-npm-packages.git", @@ -43,9 +45,11 @@ }, "scripts": { "build": "tsdown", - "check": "pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run build", + "check": "pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run build && pnpm run check:manifest && pnpm run check:package", + "check:manifest": "node scripts/check-manifest.mjs", + "check:package": "node scripts/check-package.mjs", "clean": "rm -rf lib", - "lint": "oxlint src tests tsdown.config.ts", + "lint": "oxlint src tests scripts tsdown.config.ts", "prepare": "pnpm run build", "test": "vitest run", "typecheck": "tsc --noEmit" @@ -62,11 +66,14 @@ }, "devDependencies": { "@deepseek-ai/cordis": "4.0.1", + "@deepseek-ai/cordis-plugin-include": "1.0.6", + "@deepseek-ai/dsh": "0.1.0-rc.6", "@deepseek-ai/dsh-launch-environment": "0.1.0-rc.6", "@deepseek-ai/dsh-session": "0.1.0-rc.6", "@deepseek-ai/dsh-subagent": "0.1.0-rc.6", "@deepseek-ai/dsh-tool-subagent": "0.1.0-rc.6", "@types/node": "26.2.0", + "js-yaml": "4.2.0", "oxlint": "1.76.0", "tsdown": "0.22.2", "typescript": "6.0.3", diff --git a/packages/dsh-responses-subagent/scripts/check-manifest.mjs b/packages/dsh-responses-subagent/scripts/check-manifest.mjs new file mode 100644 index 0000000..be6b67d --- /dev/null +++ b/packages/dsh-responses-subagent/scripts/check-manifest.mjs @@ -0,0 +1,105 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { entryListSchema } from '@deepseek-ai/cordis-plugin-include'; +import yaml from 'js-yaml'; + +const manifest = JSON.parse( + await readFile(new URL('../package.json', import.meta.url), 'utf8') +); + +assert.equal(manifest.name, '@parallel-web/dsh-responses-subagent'); +assert.match(manifest.version, /^\d+\.\d+\.\d+(?:-rc\.\d+)?$/); +assert.equal(manifest.private, undefined); +assert.equal(manifest.publishConfig?.access, 'public'); +assert.equal(manifest.author, 'Parallel Web'); +assert.equal(manifest.type, 'module'); +assert.equal(manifest.sideEffects, false); +assert.equal(manifest.license, 'MIT'); +assert.deepEqual(manifest.repository, { + type: 'git', + url: 'git+https://github.com/parallel-web/parallel-npm-packages.git', + directory: 'packages/dsh-responses-subagent', +}); +assert.equal(manifest.engines.node, '^22.19.0 || >=24.0.0'); +assert.equal(manifest.dsh?.bundle?.patch, './cordis.patch.yml'); +assert.deepEqual(manifest.dependencies, { + '@deepseek-ai/schemastery': '3.18.1', +}); +assert.deepEqual(manifest.peerDependencies, { + '@deepseek-ai/cordis': '4.0.1', + '@deepseek-ai/dsh-launch-environment': '0.1.0-rc.6', + '@deepseek-ai/dsh-session': '0.1.0-rc.6', + '@deepseek-ai/dsh-subagent': '0.1.0-rc.6', + '@deepseek-ai/dsh-tool-subagent': '0.1.0-rc.6', +}); +for (const [name, version] of Object.entries(manifest.peerDependencies)) { + assert.equal( + manifest.devDependencies[name], + version, + `${name} must have a matching exact development dependency` + ); +} +assert.equal(manifest.devDependencies['@deepseek-ai/dsh'], '0.1.0-rc.6'); +assert.equal( + manifest.devDependencies['@deepseek-ai/cordis-plugin-include'], + '1.0.6' +); +assert.equal(manifest.devDependencies['js-yaml'], '4.2.0'); +assert.deepEqual(manifest.files, [ + 'lib/index.js', + 'lib/index.d.ts', + 'cordis.patch.yml', + 'README.md', +]); + +const patchText = await readFile( + new URL('../cordis.patch.yml', import.meta.url), + 'utf8' +); +assert.deepEqual(yaml.load(patchText, { schema: entryListSchema }), [ + { + insert: [ + { + id: 'subagent-parallel-responses', + name: '@parallel-web/dsh-responses-subagent', + }, + { + id: 'tool-subagent-parallel-responses', + name: '@deepseek-ai/dsh-tool-subagent', + config: { + provider: 'parallel-responses', + toolName: 'parallel_research', + enableRunInBackground: false, + backgroundMode: 'one-shot', + maxDepth: 'provider-managed', + }, + }, + ], + }, +]); + +const built = await import( + new URL(`../lib/index.js?check=${Date.now()}`, import.meta.url) +); +assert.equal( + 'default' in built, + false, + 'the Cordis namespace plugin must not have a default export' +); +assert.deepEqual(Object.keys(built).sort(), [ + 'Config', + 'PARALLEL_RESPONSES_PROVIDER_ID', + 'apply', + 'inject', + 'name', +]); +assert.equal(built.PARALLEL_RESPONSES_PROVIDER_ID, 'parallel-responses'); +assert.equal(built.name, 'subagent-parallel-responses'); +assert.deepEqual(built.inject, ['subagents', 'systemPrompt']); +assert.equal( + built.Config.dict.apiKey.meta.role, + 'secret', + 'the packaged API key schema must retain secret redaction metadata' +); + +process.stdout.write('manifest, patch, and public export surface are valid\n'); diff --git a/packages/dsh-responses-subagent/scripts/check-package.mjs b/packages/dsh-responses-subagent/scripts/check-package.mjs new file mode 100644 index 0000000..a6bec79 --- /dev/null +++ b/packages/dsh-responses-subagent/scripts/check-package.mjs @@ -0,0 +1,32 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; + +const pnpmCli = process.env.npm_execpath; +assert.ok(pnpmCli, 'npm_execpath is required to run the pinned pnpm CLI'); + +const result = spawnSync( + process.execPath, + [pnpmCli, 'pack', '--dry-run', '--json'], + { + cwd: new URL('..', import.meta.url), + encoding: 'utf8', + } +); +if (result.status !== 0) { + process.stderr.write(result.stderr); + process.exit(result.status ?? 1); +} + +const jsonStart = result.stdout.lastIndexOf('\n{'); +assert.notEqual(jsonStart, -1, 'pnpm pack did not emit a JSON receipt'); +const pack = JSON.parse(result.stdout.slice(jsonStart + 1)); +assert.deepEqual(pack.files.map((file) => file.path).sort(), [ + 'LICENSE', + 'README.md', + 'cordis.patch.yml', + 'lib/index.d.ts', + 'lib/index.js', + 'package.json', +]); + +process.stdout.write('package dry-run allowlist is valid\n'); diff --git a/packages/dsh-responses-subagent/scripts/verify-packed-artifact.mjs b/packages/dsh-responses-subagent/scripts/verify-packed-artifact.mjs new file mode 100644 index 0000000..e7d3924 --- /dev/null +++ b/packages/dsh-responses-subagent/scripts/verify-packed-artifact.mjs @@ -0,0 +1,80 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { resolve } from 'node:path'; + +const tarballIndex = process.argv.indexOf('--tarball'); +assert.notEqual( + tarballIndex, + -1, + 'usage: verify-packed-artifact.mjs --tarball ' +); +const tarballArgument = process.argv[tarballIndex + 1]; +assert.ok(tarballArgument, 'missing tarball path'); +const tarball = resolve(tarballArgument); + +function tar(args) { + const result = spawnSync('tar', args, { encoding: 'utf8' }); + if (result.status !== 0) { + process.stderr.write(result.stderr); + process.exit(result.status ?? 1); + } + return result.stdout; +} + +const entries = tar(['-tzf', tarball]) + .split('\n') + .filter(Boolean) + .filter((entry) => !entry.endsWith('/')) + .sort(); + +assert.deepEqual(entries, [ + 'package/LICENSE', + 'package/README.md', + 'package/cordis.patch.yml', + 'package/lib/index.d.ts', + 'package/lib/index.js', + 'package/package.json', +]); + +const manifest = JSON.parse(tar(['-xOzf', tarball, 'package/package.json'])); +assert.equal(manifest.name, '@parallel-web/dsh-responses-subagent'); +assert.match(manifest.version, /^\d+\.\d+\.\d+(?:-rc\.\d+)?$/); +assert.equal(manifest.private, undefined); +assert.equal(manifest.publishConfig?.access, 'public'); +assert.equal(manifest.dsh?.bundle?.patch, './cordis.patch.yml'); +assert.deepEqual(manifest.dependencies, { + '@deepseek-ai/schemastery': '3.18.1', +}); + +const combinedPublicText = entries + .filter((entry) => /\.(?:js|d\.ts|md|yml|json)$/.test(entry)) + .map((entry) => tar(['-xOzf', tarball, entry])) + .join('\n'); + +for (const forbidden of [ + `/${'Users'}/`, + ['developer', 'parallel', 'code'].join('/'), + `${'.agent'}/`, + 'Authorization: Bearer', +]) { + assert.equal( + combinedPublicText.includes(forbidden), + false, + `packed artifact contains forbidden text: ${forbidden}` + ); +} + +for (const required of [ + 'https://api.parallel.ai/v1/responses', + 'parallel-responses', + 'parallel_research', + 'enableRunInBackground: false', +]) { + assert.equal( + combinedPublicText.includes(required), + true, + `packed artifact must retain contract text: ${required}` + ); +} + +process.stdout.write(`packed artifact valid: ${tarball}\n`); diff --git a/packages/dsh-responses-subagent/scripts/verify-packed-profile.mjs b/packages/dsh-responses-subagent/scripts/verify-packed-profile.mjs new file mode 100644 index 0000000..290c92b --- /dev/null +++ b/packages/dsh-responses-subagent/scripts/verify-packed-profile.mjs @@ -0,0 +1,91 @@ +import assert from 'node:assert/strict'; +import { realpathSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import { join, resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +function argument(name) { + const index = process.argv.indexOf(name); + assert.notEqual(index, -1, `missing ${name}`); + const value = process.argv[index + 1]; + assert.ok(value, `missing value for ${name}`); + return value; +} + +const dshHome = resolve(argument('--dsh-home')); +const expectedVersion = argument('--expected-version'); +const profileManifest = join(dshHome, 'profiles', 'web', 'package.json'); +const profileRequire = createRequire(profileManifest); +const pluginManifest = profileRequire.resolve( + '@parallel-web/dsh-responses-subagent/package.json' +); +const pluginRequire = createRequire(pluginManifest); + +const pluginPackage = pluginRequire(pluginManifest); +assert.equal(pluginPackage.version, expectedVersion); + +const resolutions = {}; +for (const packageName of [ + '@deepseek-ai/cordis', + '@deepseek-ai/dsh-launch-environment', + '@deepseek-ai/dsh-session', + '@deepseek-ai/dsh-subagent', + '@deepseek-ai/dsh-tool-subagent', +]) { + const fromProfile = realpathSync(profileRequire.resolve(packageName)); + const fromPlugin = realpathSync(pluginRequire.resolve(packageName)); + assert.equal( + fromPlugin, + fromProfile, + `${packageName} must resolve to one shared entrypoint` + ); + resolutions[packageName] = fromProfile; +} + +const pluginEntry = profileRequire.resolve( + '@parallel-web/dsh-responses-subagent' +); +const plugin = await import(pathToFileURL(pluginEntry).href); +const cordis = await import( + pathToFileURL(resolutions['@deepseek-ai/cordis']).href +); +const subagents = await import( + pathToFileURL(resolutions['@deepseek-ai/dsh-subagent']).href +); + +assert.equal('default' in plugin, false); +assert.equal(plugin.PARALLEL_RESPONSES_PROVIDER_ID, 'parallel-responses'); +assert.equal(plugin.Config.dict.apiKey.meta.role, 'secret'); + +const sections = []; +const ctx = new cordis.Context(); +ctx.provide('systemPrompt', { + section: (section) => sections.push(section), +}); +const runtimeFiber = await ctx.plugin(subagents.default); +const providerFiber = await ctx.plugin(plugin, { + apiKey: 'parallel_test_packed_profile', +}); +assert.deepEqual(ctx.subagents.list(), ['parallel-responses']); +const provider = ctx.subagents.getProvider('parallel-responses'); +assert.ok(provider); +assert.equal(provider.name, 'parallel-responses'); +assert.deepEqual(provider.capabilities, { + outputSchema: false, + depthLimit: false, + toolFilter: false, + persona: false, +}); +assert.equal(provider.inheritsParentContext, false); +assert.equal(typeof provider.start, 'function'); +assert.equal('prepareContinuable' in provider, false); +assert.equal(sections.length, 1); +assert.equal(sections[0].name, 'tool:parallel_research'); + +await providerFiber.dispose(); +assert.equal(ctx.subagents.getProvider('parallel-responses'), undefined); +await runtimeFiber.dispose(); + +process.stdout.write( + `packed profile valid: ${JSON.stringify({ version: pluginPackage.version, shared: Object.keys(resolutions) })}\n` +); diff --git a/packages/dsh-responses-subagent/scripts/verify-profile-overlay.mjs b/packages/dsh-responses-subagent/scripts/verify-profile-overlay.mjs new file mode 100644 index 0000000..e77dad5 --- /dev/null +++ b/packages/dsh-responses-subagent/scripts/verify-profile-overlay.mjs @@ -0,0 +1,84 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import { entryListSchema } from '@deepseek-ai/cordis-plugin-include'; +import yaml from 'js-yaml'; + +function argument(name) { + const index = process.argv.indexOf(name); + assert.notEqual(index, -1, `missing ${name}`); + const value = process.argv[index + 1]; + assert.ok(value, `missing value for ${name}`); + return resolve(value); +} + +async function load(path) { + return yaml.load(await readFile(path, 'utf8'), { schema: entryListSchema }); +} + +function flatten(entries) { + const rows = []; + for (const entry of entries) { + rows.push(entry); + if (entry?.group === true && Array.isArray(entry.config)) { + rows.push(...flatten(entry.config)); + } + } + return rows; +} + +function one(rows, id) { + const matches = rows.filter((row) => row?.id === id); + assert.equal(matches.length, 1, `expected exactly one ${id} row`); + return matches[0]; +} + +const before = flatten(await load(argument('--before'))); +const after = flatten(await load(argument('--after'))); +const removed = flatten(await load(argument('--removed'))); + +for (const rows of [before, removed]) { + assert.equal( + rows.some((row) => row?.id === 'subagent-parallel-responses'), + false + ); + assert.equal( + rows.some((row) => row?.id === 'tool-subagent-parallel-responses'), + false + ); +} + +assert.deepEqual(one(after, 'subagent-parallel-responses'), { + id: 'subagent-parallel-responses', + name: '@parallel-web/dsh-responses-subagent', +}); +assert.deepEqual(one(after, 'tool-subagent-parallel-responses'), { + id: 'tool-subagent-parallel-responses', + name: '@deepseek-ai/dsh-tool-subagent', + config: { + provider: 'parallel-responses', + toolName: 'parallel_research', + enableRunInBackground: false, + backgroundMode: 'one-shot', + maxDepth: 'provider-managed', + }, +}); + +for (const id of [ + 'subagent', + 'tool-subagent', + 'web', + 'web-search-deepseek', + 'tool-web', +]) { + assert.deepEqual(one(after, id), one(before, id), `${id} changed on install`); + assert.deepEqual( + one(removed, id), + one(before, id), + `${id} changed after removal` + ); +} + +process.stdout.write( + 'profile overlay valid: provider and foreground-only research tool install/remove cleanly; existing Search and subagent rows preserved\n' +); diff --git a/packages/dsh-responses-subagent/src/index.ts b/packages/dsh-responses-subagent/src/index.ts index 45fe5f2..592a045 100644 --- a/packages/dsh-responses-subagent/src/index.ts +++ b/packages/dsh-responses-subagent/src/index.ts @@ -4,21 +4,58 @@ import type { Context } from '@deepseek-ai/cordis'; import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment'; import z from '@deepseek-ai/schemastery'; import type {} from '@deepseek-ai/dsh-subagent'; -import { ParallelResponsesProvider } from './provider.ts'; +import { + DEFAULT_MAX_ACTIVE_RUNS, + MAX_ACTIVE_RUN_LIMIT, + PARALLEL_RESPONSES_EFFORTS, + ParallelResponsesProvider, + type ParallelResponsesEffort, +} from './provider.ts'; export { PARALLEL_RESPONSES_PROVIDER_ID } from './provider.ts'; +export type { ParallelResponsesEffort } from './provider.ts'; export const name = 'subagent-parallel-responses'; -export const inject = ['subagents']; +export const inject = ['subagents', 'systemPrompt']; + +const RESEARCH_TOOL_GUIDANCE = + 'Use parallel_research for current web research and primary-source ' + + 'verification. It delegates to an autonomous web-research specialist and ' + + 'returns a synthesized, citation-backed answer, not raw search results. ' + + 'Pass a complete, self-contained natural-language research question that ' + + 'preserves every requested entity, date, unit, and constraint. Do not ' + + 'invent JSON schemas, machine-output requirements, or keyword fragments. '; + +function researchToolGuidance(effort: ParallelResponsesEffort): string { + const strategy = + effort === 'low' + ? 'Break multi-part research into focused, independent questions and ' + + 'start those calls together when possible. Reconcile their answers ' + + 'and make focused follow-up calls only for missing evidence.' + : 'For a connected question, prefer one complete handoff and use ' + + 'focused follow-up calls only for material unresolved facts.'; + return `${RESEARCH_TOOL_GUIDANCE}${strategy} Preserve and cite the returned source URLs.`; +} /** Deployment configuration for the fixed Parallel Responses provider. */ export interface Config { /** Explicit Parallel API key; omission uses the Harness launch snapshot. */ apiKey?: string; + /** Research tier; higher effort trades cost for deeper investigation. */ + effort?: ParallelResponsesEffort; + /** Maximum simultaneous research calls, including parallel fan-out. */ + maxConcurrentRuns?: number; } export const Config: z = z.object({ apiKey: z.string().role('secret'), + effort: z.union(PARALLEL_RESPONSES_EFFORTS), + maxConcurrentRuns: z + .number() + .step(1) + .min(1) + .max(MAX_ACTIVE_RUN_LIMIT) + .default(DEFAULT_MAX_ACTIVE_RUNS), }); /** @@ -39,6 +76,10 @@ export function apply(ctx: Context, config: Config): void { ctx.subagents.registerProvider( new ParallelResponsesProvider({ apiKey, + ...(config.effort === undefined ? {} : { effort: config.effort }), + ...(config.maxConcurrentRuns === undefined + ? {} + : { maxConcurrentRuns: config.maxConcurrentRuns }), onError: (error) => { ctx.logger.warn( `subagent-parallel-responses: remote run failed: ${error.message}` @@ -46,4 +87,12 @@ export function apply(ctx: Context, config: Config): void { }, }) ); + ctx.systemPrompt.section({ + name: 'tool:parallel_research', + order: 110.5, + text: (context) => + ctx.get('tools')?.get('parallel_research', context.scope) === undefined + ? '' + : researchToolGuidance(config.effort ?? 'medium'), + }); } diff --git a/packages/dsh-responses-subagent/src/provider.ts b/packages/dsh-responses-subagent/src/provider.ts index 502eaea..28aabc3 100644 --- a/packages/dsh-responses-subagent/src/provider.ts +++ b/packages/dsh-responses-subagent/src/provider.ts @@ -17,8 +17,22 @@ import { export const PARALLEL_RESPONSES_PROVIDER_ID = 'parallel-responses'; export const PARALLEL_RESPONSES_URL = 'https://api.parallel.ai/v1/responses'; export const PARALLEL_RESPONSES_TIMEOUT_MS = 10 * 60_000; +export const PARALLEL_RESPONSES_MAX_INPUT_CHARS = 20_000; +export const PARALLEL_RESPONSES_EFFORTS = ['low', 'medium', 'high'] as const; +export const DEFAULT_MAX_ACTIVE_RUNS = 2; +export const MAX_ACTIVE_RUN_LIMIT = 20; +export const PARALLEL_RESEARCH_INSTRUCTIONS = + 'You are an autonomous live-web research specialist. Answer the actual ' + + 'research question with current evidence, prioritizing official ' + + 'documentation, original announcements, direct repository evidence, and ' + + 'other primary sources. Cover every requested entity and constraint. Verify ' + + 'dates, versions, prices, and units, distinguish confirmed facts from ' + + 'uncertainty, and cite direct supporting source URLs. Return a complete ' + + 'synthesized answer for the parent agent. Do not reject a valid research ' + + 'question merely because its wording mentions JSON or an output schema.'; -const MAX_ACTIVE_RUNS = 2; +export type ParallelResponsesEffort = + (typeof PARALLEL_RESPONSES_EFFORTS)[number]; type Fetch = ( input: string | URL | Request, @@ -36,9 +50,11 @@ class Capacity { private active = 0; private readonly waiters: CapacityWaiter[] = []; + constructor(private readonly limit: number) {} + async acquire(signal: AbortSignal): Promise<() => void> { if (signal.aborted) throw cancellationError('before capacity admission'); - if (this.active < MAX_ACTIVE_RUNS) { + if (this.active < this.limit) { this.active += 1; return this.releaseOnce(); } @@ -133,6 +149,14 @@ export function researchPrompt(request: ResolvedSubagentStartRequest): string { 'dsh-responses-subagent: the research prompt must not be empty' ); } + if ( + prompt.length + PARALLEL_RESEARCH_INSTRUCTIONS.length + 1 > + PARALLEL_RESPONSES_MAX_INPUT_CHARS + ) { + throw new Error( + `dsh-responses-subagent: the research prompt and instructions exceed ${PARALLEL_RESPONSES_MAX_INPUT_CHARS.toLocaleString('en-US')} characters` + ); + } return prompt; } @@ -231,6 +255,8 @@ function completedResult(payload: unknown): SubagentResult { /** Construction inputs kept private from Cordis configuration. */ export interface ParallelResponsesProviderOptions { readonly apiKey: string; + readonly effort?: ParallelResponsesEffort; + readonly maxConcurrentRuns?: number; readonly fetch?: Fetch; /** Safe diagnostic sink for failures flattened into an error result. */ readonly onError?: (error: Error) => void; @@ -243,14 +269,34 @@ export class ParallelResponsesProvider implements SubagentProvider { readonly inheritsParentContext = false; private readonly apiKey: string; + private readonly effort: ParallelResponsesEffort; private readonly fetch: Fetch; private readonly onError: ((error: Error) => void) | undefined; - private readonly capacity = new Capacity(); + private readonly capacity: Capacity; constructor(options: ParallelResponsesProviderOptions) { + const effort = options.effort === undefined ? 'medium' : options.effort; + if (!PARALLEL_RESPONSES_EFFORTS.some((candidate) => candidate === effort)) { + throw new Error( + 'dsh-responses-subagent: effort must be low, medium, or high' + ); + } + const maxConcurrentRuns = + options.maxConcurrentRuns ?? DEFAULT_MAX_ACTIVE_RUNS; + if ( + !Number.isSafeInteger(maxConcurrentRuns) || + maxConcurrentRuns < 1 || + maxConcurrentRuns > MAX_ACTIVE_RUN_LIMIT + ) { + throw new Error( + `dsh-responses-subagent: maxConcurrentRuns must be an integer from 1 to ${MAX_ACTIVE_RUN_LIMIT}` + ); + } this.apiKey = options.apiKey; + this.effort = effort; this.fetch = options.fetch ?? globalThis.fetch; this.onError = options.onError; + this.capacity = new Capacity(maxConcurrentRuns); } async start(request: ResolvedSubagentStartRequest): Promise { @@ -295,7 +341,8 @@ export class ParallelResponsesProvider implements SubagentProvider { body: JSON.stringify({ model: 'parallel', input: prompt, - reasoning: { effort: 'medium' }, + instructions: PARALLEL_RESEARCH_INSTRUCTIONS, + reasoning: { effort: this.effort }, stream: false, }), redirect: 'error', diff --git a/packages/dsh-responses-subagent/tests/provider.spec.ts b/packages/dsh-responses-subagent/tests/provider.spec.ts index 8e29f1c..b9062e1 100644 --- a/packages/dsh-responses-subagent/tests/provider.spec.ts +++ b/packages/dsh-responses-subagent/tests/provider.spec.ts @@ -6,9 +6,12 @@ import SubagentRuntime, { import { afterEach, describe, expect, it, vi } from 'vitest'; import * as plugin from '../src/index.ts'; import { + PARALLEL_RESPONSES_MAX_INPUT_CHARS, PARALLEL_RESPONSES_TIMEOUT_MS, PARALLEL_RESPONSES_URL, + PARALLEL_RESEARCH_INSTRUCTIONS, ParallelResponsesProvider, + type ParallelResponsesEffort, researchPrompt, } from '../src/provider.ts'; @@ -102,6 +105,7 @@ describe('Parallel Responses request and output', () => { expect(JSON.parse(String(init?.body))).toEqual({ model: 'parallel', input: ' unchanged prompt ', + instructions: PARALLEL_RESEARCH_INSTRUCTIONS, reasoning: { effort: 'medium' }, stream: false, }); @@ -114,6 +118,41 @@ describe('Parallel Responses request and output', () => { await firstDispose; }); + it.each(['low', 'medium', 'high'] as const)( + 'sends the configured %s research tier', + async (effort) => { + const fetch = vi.fn().mockResolvedValue(completed()); + const provider = new ParallelResponsesProvider({ + apiKey: 'parallel_test_provider', + effort, + fetch, + }); + const run = await provider.start(request()); + + await expect(run.result).resolves.toMatchObject({ + stopReason: 'completed', + }); + expect(JSON.parse(String(fetch.mock.calls[0]?.[1]?.body))).toMatchObject({ + instructions: PARALLEL_RESEARCH_INSTRUCTIONS, + reasoning: { effort }, + }); + await run.dispose(); + } + ); + + it.each(['unsupported', '', 1, null])( + 'rejects invalid direct-provider effort %s', + (effort) => { + expect( + () => + new ParallelResponsesProvider({ + apiKey: 'parallel_test_provider', + effort: effort as unknown as ParallelResponsesEffort, + }) + ).toThrow('effort must be low, medium, or high'); + } + ); + it('preserves text block order and rejects blank or non-text prompts', () => { expect( researchPrompt( @@ -134,6 +173,22 @@ describe('Parallel Responses request and output', () => { ).toThrow('only text blocks'); }); + it('rejects prompts that exceed the API limit with research instructions', () => { + const available = + PARALLEL_RESPONSES_MAX_INPUT_CHARS - + PARALLEL_RESEARCH_INSTRUCTIONS.length - + 1; + + expect( + researchPrompt(request([{ type: 'text', text: 'x'.repeat(available) }])) + ).toHaveLength(available); + expect(() => + researchPrompt( + request([{ type: 'text', text: 'x'.repeat(available + 1) }]) + ) + ).toThrow('exceed 20,000 characters'); + }); + it.each([ new Response('denied', { status: 403 }), Response.json({ status: 'failed' }), @@ -259,6 +314,34 @@ describe('Parallel Responses capacity and lifecycle', () => { await Promise.all([second.dispose(), replacement.dispose()]); }); + it('admits the configured number of simultaneous research calls', async () => { + const fetch = abortableFetch(); + const provider = new ParallelResponsesProvider({ + apiKey: 'parallel_test_provider', + maxConcurrentRuns: 4, + fetch, + }); + const runs = await Promise.all( + Array.from({ length: 4 }, async () => await provider.start(request())) + ); + + expect(fetch).toHaveBeenCalledTimes(4); + await Promise.all(runs.map(async (run) => await run.dispose())); + }); + + it.each([0, 1.5, 21, Number.NaN])( + 'rejects an invalid concurrency limit %s', + (maxConcurrentRuns) => { + expect( + () => + new ParallelResponsesProvider({ + apiKey: 'parallel_test_provider', + maxConcurrentRuns, + }) + ).toThrow('maxConcurrentRuns must be an integer from 1 to 20'); + } + ); + it('times out once with no retry and releases capacity', async () => { vi.useFakeTimers(); const fetch = abortableFetch(); @@ -294,6 +377,12 @@ describe('Parallel Responses plugin registration', () => { it('uses the launch environment and unregisters with its Cordis fiber', async () => { const ctx = new Context(); + const section = vi.fn(); + const get = vi.fn((): unknown => ({})); + ctx.provide('systemPrompt', { + section, + } as unknown as Context['systemPrompt']); + ctx.provide('tools', { get } as unknown as Context['tools']); ctx.provide( 'launchEnvironment', createLaunchEnvironmentSnapshot([ @@ -317,14 +406,53 @@ describe('Parallel Responses plugin registration', () => { }, inheritsParentContext: false, }); + expect(section).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'tool:parallel_research', + text: expect.any(Function), + }) + ); + const registration = section.mock.calls[0]?.[0] as { + text: (context: { scope?: unknown }) => string; + }; + expect(registration.text({})).toContain('complete, self-contained'); + get.mockReturnValue(undefined); + expect(registration.text({})).toBe(''); await fiber.dispose(); expect( ctx.subagents.getProvider(plugin.PARALLEL_RESPONSES_PROVIDER_ID) ).toBeUndefined(); }); + it('teaches low-effort parents to fan out independent research', async () => { + const ctx = new Context(); + const section = vi.fn(); + ctx.provide('systemPrompt', { + section, + } as unknown as Context['systemPrompt']); + ctx.provide('tools', { + get: vi.fn(() => ({})), + } as unknown as Context['tools']); + ctx.provide('launchEnvironment', createLaunchEnvironmentSnapshot([])); + await ctx.plugin(SubagentRuntime); + const fiber = await ctx.plugin(plugin, { + apiKey: 'parallel_test_explicit', + effort: 'low', + maxConcurrentRuns: 8, + }); + + const registration = section.mock.calls[0]?.[0] as { + text: (context: { scope?: unknown }) => string; + }; + expect(registration.text({})).toContain('focused, independent questions'); + await fiber.dispose(); + }); + it('fails at load without an explicit or launch-time key', async () => { const ctx = new Context(); + ctx.provide('systemPrompt', { + section: vi.fn(), + } as unknown as Context['systemPrompt']); ctx.provide('launchEnvironment', createLaunchEnvironmentSnapshot([])); await ctx.plugin(SubagentRuntime); await expect(ctx.plugin(plugin, {})).rejects.toThrow( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d58c59c..f68b40b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -67,6 +67,12 @@ importers: '@deepseek-ai/cordis': specifier: 4.0.1 version: 4.0.1(@deepseek-ai/cordis-plugin-include@1.0.6)(@deepseek-ai/cordis-plugin-loader@1.0.2) + '@deepseek-ai/cordis-plugin-include': + specifier: 1.0.6 + version: 1.0.6(@deepseek-ai/cordis-plugin-loader@1.0.2)(@deepseek-ai/cordis@4.0.1) + '@deepseek-ai/dsh': + specifier: 0.1.0-rc.6 + version: 0.1.0-rc.6(c422946f0a5c0641b2538ac43317dd99) '@deepseek-ai/dsh-launch-environment': specifier: 0.1.0-rc.6 version: 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)) @@ -82,6 +88,9 @@ importers: '@types/node': specifier: 26.2.0 version: 26.2.0 + js-yaml: + specifier: 4.2.0 + version: 4.2.0 oxlint: specifier: 1.76.0 version: 1.76.0 From 4125755ee36472ac0bcdb64e7fb1483681dc3833 Mon Sep 17 00:00:00 2001 From: George Pickett Date: Sun, 23 Aug 2026 16:22:21 -0700 Subject: [PATCH 4/6] Simplify DeepSeek Harness Responses subagent --- .github/workflows/ci.yml | 70 +++- PUBLISHING.md | 10 +- README.md | 2 +- packages/dsh-responses-subagent/README.md | 90 ++--- .../dsh-responses-subagent/cordis.patch.yml | 1 - packages/dsh-responses-subagent/package.json | 9 +- .../scripts/check-manifest.mjs | 105 ------ .../scripts/check-package.mjs | 32 -- .../scripts/verify-packed-artifact.mjs | 80 ----- .../scripts/verify-packed-profile.mjs | 91 ----- .../scripts/verify-profile-overlay.mjs | 84 ----- packages/dsh-responses-subagent/src/index.ts | 23 +- .../dsh-responses-subagent/src/provider.ts | 313 +++++------------- .../tests/provider.spec.ts | 78 ++--- .../dsh-responses-subagent/tsdown.config.ts | 1 - pnpm-lock.yaml | 9 - 16 files changed, 197 insertions(+), 801 deletions(-) delete mode 100644 packages/dsh-responses-subagent/scripts/check-manifest.mjs delete mode 100644 packages/dsh-responses-subagent/scripts/check-package.mjs delete mode 100644 packages/dsh-responses-subagent/scripts/verify-packed-artifact.mjs delete mode 100644 packages/dsh-responses-subagent/scripts/verify-packed-profile.mjs delete mode 100644 packages/dsh-responses-subagent/scripts/verify-profile-overlay.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ad9ffd3..7479b66 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,13 +85,12 @@ jobs: run: pnpm build dsh-compatibility: - name: DSH Compatibility (${{ matrix.package }}, Node ${{ matrix.node }}) + name: DSH Compatibility (Node ${{ matrix.node }}) runs-on: ubuntu-latest strategy: fail-fast: false matrix: node: [22.19.0, 24, 26] - package: [dsh-web-search, dsh-responses-subagent] steps: - name: Checkout uses: actions/checkout@v4 @@ -106,10 +105,10 @@ jobs: cache: 'pnpm' - name: Install dependencies - run: pnpm install --filter "@parallel-web/${{ matrix.package }}..." --frozen-lockfile + run: pnpm install --filter @parallel-web/dsh-web-search... --filter @parallel-web/dsh-responses-subagent... --frozen-lockfile - name: Run deterministic plugin checks - run: pnpm --filter "@parallel-web/${{ matrix.package }}" check + run: pnpm --filter @parallel-web/dsh-web-search --filter @parallel-web/dsh-responses-subagent run check - name: Verify packed DSH profile lifecycle shell: bash @@ -119,26 +118,67 @@ jobs: dsh_home="$(mktemp -d)" trap 'rm -rf "$pack_dir" "$dsh_home"' EXIT - pnpm --dir "packages/${{ matrix.package }}" pack --pack-destination "$pack_dir" + pnpm --dir packages/dsh-web-search pack --pack-destination "$pack_dir" artifact="$(find "$pack_dir" -name '*.tgz' -print -quit)" - version="$(node -p "require('./packages/${{ matrix.package }}/package.json').version")" + version="$(node -p "require('./packages/dsh-web-search/package.json').version")" - node "packages/${{ matrix.package }}/scripts/verify-packed-artifact.mjs" \ + node packages/dsh-web-search/scripts/verify-packed-artifact.mjs \ --tarball "$artifact" - DSH_HOME="$dsh_home" pnpm --dir "packages/${{ matrix.package }}" exec dsh \ + DSH_HOME="$dsh_home" pnpm --dir packages/dsh-web-search exec dsh \ --profile web --dump-config > "$dsh_home/before.yml" - DSH_HOME="$dsh_home" pnpm --dir "packages/${{ matrix.package }}" exec dsh \ + DSH_HOME="$dsh_home" pnpm --dir packages/dsh-web-search exec dsh \ plugin --profile web add "$artifact" - DSH_HOME="$dsh_home" pnpm --dir "packages/${{ matrix.package }}" exec dsh \ + DSH_HOME="$dsh_home" pnpm --dir packages/dsh-web-search exec dsh \ --profile web --dump-config > "$dsh_home/after.yml" - node "packages/${{ matrix.package }}/scripts/verify-packed-profile.mjs" \ + node packages/dsh-web-search/scripts/verify-packed-profile.mjs \ --dsh-home "$dsh_home" \ --expected-version "$version" - DSH_HOME="$dsh_home" pnpm --dir "packages/${{ matrix.package }}" exec dsh \ - plugin --profile web remove "@parallel-web/${{ matrix.package }}" - DSH_HOME="$dsh_home" pnpm --dir "packages/${{ matrix.package }}" exec dsh \ + + pnpm --dir packages/dsh-responses-subagent pack --pack-destination "$pack_dir" + responses_artifact="$(find "$pack_dir" -name '*dsh-responses-subagent*.tgz' -print -quit)" + tar -xOzf "$responses_artifact" package/package.json | node -e ' + const manifest = JSON.parse(require("node:fs").readFileSync(0, "utf8")); + if (manifest.private || manifest.publishConfig?.access !== "public" || + manifest.dsh?.bundle?.patch !== "./cordis.patch.yml") { + throw new Error("invalid packed Responses plugin manifest"); + } + ' + DSH_HOME="$dsh_home" pnpm --dir packages/dsh-web-search exec dsh \ + plugin --profile web add "$responses_artifact" + DSH_HOME="$dsh_home" pnpm --dir packages/dsh-web-search exec dsh \ + --profile web --dump-config > "$dsh_home/combined.yml" + grep -Fq 'id: subagent-parallel-responses' "$dsh_home/combined.yml" + grep -Fq 'toolName: parallel_research' "$dsh_home/combined.yml" + grep -Fq 'id: web-search-parallel' "$dsh_home/combined.yml" + + node --input-type=module -e ' + import assert from "node:assert/strict"; + import { createRequire } from "node:module"; + import { join } from "node:path"; + + const require = createRequire(join(process.argv[1], "profiles/web/package.json")); + const plugin = await import(require.resolve("@parallel-web/dsh-responses-subagent")); + const { Context } = await import(require.resolve("@deepseek-ai/cordis")); + const subagents = await import(require.resolve("@deepseek-ai/dsh-subagent")); + const context = new Context(); + context.provide("systemPrompt", { section() {} }); + await context.plugin(subagents.default); + await context.plugin(plugin, { apiKey: "parallel_test_packed_profile" }); + assert.equal(plugin.Config.dict.apiKey.meta.role, "secret"); + assert.equal(context.subagents.getProvider("parallel-responses")?.inheritsParentContext, false); + ' "$dsh_home" + + DSH_HOME="$dsh_home" pnpm --dir packages/dsh-web-search exec dsh \ + plugin --profile web remove @parallel-web/dsh-responses-subagent + DSH_HOME="$dsh_home" pnpm --dir packages/dsh-web-search exec dsh \ + --profile web --dump-config > "$dsh_home/responses-removed.yml" + cmp "$dsh_home/after.yml" "$dsh_home/responses-removed.yml" + + DSH_HOME="$dsh_home" pnpm --dir packages/dsh-web-search exec dsh \ + plugin --profile web remove @parallel-web/dsh-web-search + DSH_HOME="$dsh_home" pnpm --dir packages/dsh-web-search exec dsh \ --profile web --dump-config > "$dsh_home/removed.yml" - node "packages/${{ matrix.package }}/scripts/verify-profile-overlay.mjs" \ + node packages/dsh-web-search/scripts/verify-profile-overlay.mjs \ --before "$dsh_home/before.yml" \ --after "$dsh_home/after.yml" \ --removed "$dsh_home/removed.yml" diff --git a/PUBLISHING.md b/PUBLISHING.md index 7f70cd8..80223d4 100644 --- a/PUBLISHING.md +++ b/PUBLISHING.md @@ -58,18 +58,16 @@ Skipping that upgrade causes a misleading `404 Not Found` on the publish `PUT`. npm requires a package to exist before its trusted publisher can be configured. Adding a package to this repository intentionally does not publish it. An npm organization owner must first publish -the reviewed bootstrap release manually from a clean, updated `main` checkout. Set `PACKAGE` -to the directory name of the new package: +the reviewed bootstrap release manually from a clean, updated `main` checkout: ```bash -PACKAGE=dsh-responses-subagent pnpm install --frozen-lockfile -pnpm --filter "@parallel-web/$PACKAGE" check +pnpm --filter @parallel-web/dsh-responses-subagent check BOOTSTRAP_DIR="$(mktemp -d)" -pnpm --dir "packages/$PACKAGE" pack --pack-destination "$BOOTSTRAP_DIR" +pnpm --dir packages/dsh-responses-subagent pack --pack-destination "$BOOTSTRAP_DIR" BOOTSTRAP_TARBALL="$(find "$BOOTSTRAP_DIR" -name '*.tgz' -print -quit)" npm publish "$BOOTSTRAP_TARBALL" --access public --tag rc -npm view "@parallel-web/$PACKAGE" dist-tags --json +npm view @parallel-web/dsh-responses-subagent dist-tags --json ``` The npm owner should inspect the tarball listing before the publish and complete npm's 2FA prompt. diff --git a/README.md b/README.md index b86a912..31442b2 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Monorepo for @parallel-web npm packages. ## Packages - [`@parallel-web/ai-sdk-tools`](./packages/ai-sdk-tools) - AI SDK tools for Parallel Web -- [`@parallel-web/dsh-responses-subagent`](./packages/dsh-responses-subagent) - Parallel Responses research subagent for DeepSeek Harness; not yet published, install from a local tarball +- [`@parallel-web/dsh-responses-subagent`](./packages/dsh-responses-subagent) - Parallel Responses research subagent for DeepSeek Harness - [`@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 diff --git a/packages/dsh-responses-subagent/README.md b/packages/dsh-responses-subagent/README.md index 4fb5858..16ad877 100644 --- a/packages/dsh-responses-subagent/README.md +++ b/packages/dsh-responses-subagent/README.md @@ -1,19 +1,17 @@ # Parallel Responses for DeepSeek Harness -This DeepSeek Harness plugin adds an opt-in `parallel_research` tool. Each call -sends one self-contained text prompt to +This DeepSeek Harness plugin adds `parallel_research`, an opt-in [Parallel Responses](https://docs.parallel.ai/responses-api/examples/research-subagent) -and returns the completed answer with its citations. +subagent that returns complete, cited web research. It works alongside +`web_search` without changing the existing Parallel Search plugin. -It does not replace `web_search` or change -`@parallel-web/dsh-web-search`. It never sends parent conversation history, -files, workspace state, tools, or ambient environment details. +Only the explicitly delegated question is forwarded as research input. Parent +history, files, workspace state, tools, and environment details are never +gathered automatically. -## Install from this checkout +## Install -This package is ready for a future release but has not been published to npm. -Until an npm organization owner completes its first release, install it from a -local tarball. From the repository root: +This package has not been published to npm. Install from the repository root: ```sh pnpm --filter @parallel-web/dsh-responses-subagent build @@ -23,77 +21,41 @@ npx --yes @deepseek-ai/dsh@0.1.0-rc.6 \ /tmp/parallel-web-dsh-responses-subagent-0.1.0-rc.0.tgz ``` -Make your [Parallel API key](https://platform.parallel.ai/) available in the -same terminal that starts Harness: +Provide your [Parallel API key](https://platform.parallel.ai/) in the terminal +that starts Harness, then configure a parent model in **Settings > Models**: ```sh export PARALLEL_API_KEY="your-key" npx --yes @deepseek-ai/dsh@0.1.0-rc.6 web ``` -For an interactive session, configure a model and its API key in Harness -**Settings > Models**. The plugin works alongside -`@parallel-web/dsh-web-search`: use `web_search` for individual searches and -`parallel_research` when the parent should delegate a complete research task. +After the first npm release, replace the tarball path in the install command +with `@parallel-web/dsh-responses-subagent@rc`. -Ask the parent agent to call `parallel_research` with a complete, standalone -research question. The tool waits for the answer and returns its source URLs. -The parent receives specific guidance to preserve the original question, avoid -inventing JSON schemas, cite primary sources, and make focused follow-up calls -when the first answer leaves an important gap. +## Research depth and parallelism -### After the first npm release +Ask the parent agent to delegate a complete, standalone research question. It +receives guidance to preserve constraints, prioritize primary sources, and +cite the returned URLs. -The registry install below will work only after the package has actually been -published with the `rc` dist-tag. It is not currently available: - -```sh -npx --yes @deepseek-ai/dsh@0.1.0-rc.6 \ - plugin --profile web add \ - @parallel-web/dsh-responses-subagent@rc -``` - -## Research depth and concurrency - -Add optional settings to the `subagent-parallel-responses` entry in your Harness -profile, `~/.dsh/profiles/web/cordis.patch.yml`, when a workload needs a -different research strategy: +To choose the research depth, edit the `subagent-parallel-responses` entry in +`~/.dsh/profiles/web/cordis.patch.yml`: ```yaml - id: subagent-parallel-responses config: - effort: high - maxConcurrentRuns: 4 + effort: low ``` -`effort` accepts `low`, `medium`, or `high`; the default remains `medium`. -Use `low` for several independent, focused questions, `medium` for ordinary -multi-hop research, and `high` for difficult research that needs extensive -source comparison or synthesis. `maxConcurrentRuns` defaults to 2 and accepts -values from 1 through 20. Configure it deliberately when the parent should fan -out independent low-effort questions. Harness also applies its own -`maxParallelToolCalls` limit, which defaults to 10. The effective concurrency -is whichever configured limit is lower. To run more than 10 requests together, -raise both `maxConcurrentRuns` and Harness's **Settings > Plugins > Agent -loop > Parallel tool calls** setting. - -## Behavior - -- One `POST /v1/responses` request per accepted research task. -- Fixed `parallel` model, configurable reasoning effort, and no retries. -- Configurable bounded concurrency, with a 10-minute timeout per request. -- Research answers include the returned citation titles and source URLs. -- Parent cancellation or disposal aborts the remote request. -- No parent history, files, background jobs, continuation, or child tools. -- Compatibility is pinned to DeepSeek Harness `0.1.0-rc.6`. - -Run the package checks from the repository root: +Choose `low` for focused questions, `medium` (the default) for ordinary +research, or `high` for deeper synthesis. At low effort, the parent is +encouraged to dispatch independent questions together. Harness schedules +parallel tool calls itself, allowing 10 by default. To allow more, increase +**Settings > Plugins > Agent loop > Parallel tool calls**. -```sh -pnpm --filter @parallel-web/dsh-responses-subagent check -``` +Keep `PARALLEL_API_KEY` in the launch environment, not in this profile. -To remove the plugin: +## Remove ```sh npx --yes @deepseek-ai/dsh@0.1.0-rc.6 \ diff --git a/packages/dsh-responses-subagent/cordis.patch.yml b/packages/dsh-responses-subagent/cordis.patch.yml index ea6bf32..42205c7 100644 --- a/packages/dsh-responses-subagent/cordis.patch.yml +++ b/packages/dsh-responses-subagent/cordis.patch.yml @@ -8,5 +8,4 @@ provider: parallel-responses toolName: parallel_research enableRunInBackground: false - backgroundMode: one-shot maxDepth: provider-managed diff --git a/packages/dsh-responses-subagent/package.json b/packages/dsh-responses-subagent/package.json index fcbc181..f3f0df3 100644 --- a/packages/dsh-responses-subagent/package.json +++ b/packages/dsh-responses-subagent/package.json @@ -45,11 +45,9 @@ }, "scripts": { "build": "tsdown", - "check": "pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run build && pnpm run check:manifest && pnpm run check:package", - "check:manifest": "node scripts/check-manifest.mjs", - "check:package": "node scripts/check-package.mjs", + "check": "pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run build", "clean": "rm -rf lib", - "lint": "oxlint src tests scripts tsdown.config.ts", + "lint": "oxlint src tests tsdown.config.ts", "prepare": "pnpm run build", "test": "vitest run", "typecheck": "tsc --noEmit" @@ -66,14 +64,11 @@ }, "devDependencies": { "@deepseek-ai/cordis": "4.0.1", - "@deepseek-ai/cordis-plugin-include": "1.0.6", - "@deepseek-ai/dsh": "0.1.0-rc.6", "@deepseek-ai/dsh-launch-environment": "0.1.0-rc.6", "@deepseek-ai/dsh-session": "0.1.0-rc.6", "@deepseek-ai/dsh-subagent": "0.1.0-rc.6", "@deepseek-ai/dsh-tool-subagent": "0.1.0-rc.6", "@types/node": "26.2.0", - "js-yaml": "4.2.0", "oxlint": "1.76.0", "tsdown": "0.22.2", "typescript": "6.0.3", diff --git a/packages/dsh-responses-subagent/scripts/check-manifest.mjs b/packages/dsh-responses-subagent/scripts/check-manifest.mjs deleted file mode 100644 index be6b67d..0000000 --- a/packages/dsh-responses-subagent/scripts/check-manifest.mjs +++ /dev/null @@ -1,105 +0,0 @@ -import assert from 'node:assert/strict'; -import { readFile } from 'node:fs/promises'; -import { entryListSchema } from '@deepseek-ai/cordis-plugin-include'; -import yaml from 'js-yaml'; - -const manifest = JSON.parse( - await readFile(new URL('../package.json', import.meta.url), 'utf8') -); - -assert.equal(manifest.name, '@parallel-web/dsh-responses-subagent'); -assert.match(manifest.version, /^\d+\.\d+\.\d+(?:-rc\.\d+)?$/); -assert.equal(manifest.private, undefined); -assert.equal(manifest.publishConfig?.access, 'public'); -assert.equal(manifest.author, 'Parallel Web'); -assert.equal(manifest.type, 'module'); -assert.equal(manifest.sideEffects, false); -assert.equal(manifest.license, 'MIT'); -assert.deepEqual(manifest.repository, { - type: 'git', - url: 'git+https://github.com/parallel-web/parallel-npm-packages.git', - directory: 'packages/dsh-responses-subagent', -}); -assert.equal(manifest.engines.node, '^22.19.0 || >=24.0.0'); -assert.equal(manifest.dsh?.bundle?.patch, './cordis.patch.yml'); -assert.deepEqual(manifest.dependencies, { - '@deepseek-ai/schemastery': '3.18.1', -}); -assert.deepEqual(manifest.peerDependencies, { - '@deepseek-ai/cordis': '4.0.1', - '@deepseek-ai/dsh-launch-environment': '0.1.0-rc.6', - '@deepseek-ai/dsh-session': '0.1.0-rc.6', - '@deepseek-ai/dsh-subagent': '0.1.0-rc.6', - '@deepseek-ai/dsh-tool-subagent': '0.1.0-rc.6', -}); -for (const [name, version] of Object.entries(manifest.peerDependencies)) { - assert.equal( - manifest.devDependencies[name], - version, - `${name} must have a matching exact development dependency` - ); -} -assert.equal(manifest.devDependencies['@deepseek-ai/dsh'], '0.1.0-rc.6'); -assert.equal( - manifest.devDependencies['@deepseek-ai/cordis-plugin-include'], - '1.0.6' -); -assert.equal(manifest.devDependencies['js-yaml'], '4.2.0'); -assert.deepEqual(manifest.files, [ - 'lib/index.js', - 'lib/index.d.ts', - 'cordis.patch.yml', - 'README.md', -]); - -const patchText = await readFile( - new URL('../cordis.patch.yml', import.meta.url), - 'utf8' -); -assert.deepEqual(yaml.load(patchText, { schema: entryListSchema }), [ - { - insert: [ - { - id: 'subagent-parallel-responses', - name: '@parallel-web/dsh-responses-subagent', - }, - { - id: 'tool-subagent-parallel-responses', - name: '@deepseek-ai/dsh-tool-subagent', - config: { - provider: 'parallel-responses', - toolName: 'parallel_research', - enableRunInBackground: false, - backgroundMode: 'one-shot', - maxDepth: 'provider-managed', - }, - }, - ], - }, -]); - -const built = await import( - new URL(`../lib/index.js?check=${Date.now()}`, import.meta.url) -); -assert.equal( - 'default' in built, - false, - 'the Cordis namespace plugin must not have a default export' -); -assert.deepEqual(Object.keys(built).sort(), [ - 'Config', - 'PARALLEL_RESPONSES_PROVIDER_ID', - 'apply', - 'inject', - 'name', -]); -assert.equal(built.PARALLEL_RESPONSES_PROVIDER_ID, 'parallel-responses'); -assert.equal(built.name, 'subagent-parallel-responses'); -assert.deepEqual(built.inject, ['subagents', 'systemPrompt']); -assert.equal( - built.Config.dict.apiKey.meta.role, - 'secret', - 'the packaged API key schema must retain secret redaction metadata' -); - -process.stdout.write('manifest, patch, and public export surface are valid\n'); diff --git a/packages/dsh-responses-subagent/scripts/check-package.mjs b/packages/dsh-responses-subagent/scripts/check-package.mjs deleted file mode 100644 index a6bec79..0000000 --- a/packages/dsh-responses-subagent/scripts/check-package.mjs +++ /dev/null @@ -1,32 +0,0 @@ -import assert from 'node:assert/strict'; -import { spawnSync } from 'node:child_process'; - -const pnpmCli = process.env.npm_execpath; -assert.ok(pnpmCli, 'npm_execpath is required to run the pinned pnpm CLI'); - -const result = spawnSync( - process.execPath, - [pnpmCli, 'pack', '--dry-run', '--json'], - { - cwd: new URL('..', import.meta.url), - encoding: 'utf8', - } -); -if (result.status !== 0) { - process.stderr.write(result.stderr); - process.exit(result.status ?? 1); -} - -const jsonStart = result.stdout.lastIndexOf('\n{'); -assert.notEqual(jsonStart, -1, 'pnpm pack did not emit a JSON receipt'); -const pack = JSON.parse(result.stdout.slice(jsonStart + 1)); -assert.deepEqual(pack.files.map((file) => file.path).sort(), [ - 'LICENSE', - 'README.md', - 'cordis.patch.yml', - 'lib/index.d.ts', - 'lib/index.js', - 'package.json', -]); - -process.stdout.write('package dry-run allowlist is valid\n'); diff --git a/packages/dsh-responses-subagent/scripts/verify-packed-artifact.mjs b/packages/dsh-responses-subagent/scripts/verify-packed-artifact.mjs deleted file mode 100644 index e7d3924..0000000 --- a/packages/dsh-responses-subagent/scripts/verify-packed-artifact.mjs +++ /dev/null @@ -1,80 +0,0 @@ -import assert from 'node:assert/strict'; -import { spawnSync } from 'node:child_process'; -import { resolve } from 'node:path'; - -const tarballIndex = process.argv.indexOf('--tarball'); -assert.notEqual( - tarballIndex, - -1, - 'usage: verify-packed-artifact.mjs --tarball ' -); -const tarballArgument = process.argv[tarballIndex + 1]; -assert.ok(tarballArgument, 'missing tarball path'); -const tarball = resolve(tarballArgument); - -function tar(args) { - const result = spawnSync('tar', args, { encoding: 'utf8' }); - if (result.status !== 0) { - process.stderr.write(result.stderr); - process.exit(result.status ?? 1); - } - return result.stdout; -} - -const entries = tar(['-tzf', tarball]) - .split('\n') - .filter(Boolean) - .filter((entry) => !entry.endsWith('/')) - .sort(); - -assert.deepEqual(entries, [ - 'package/LICENSE', - 'package/README.md', - 'package/cordis.patch.yml', - 'package/lib/index.d.ts', - 'package/lib/index.js', - 'package/package.json', -]); - -const manifest = JSON.parse(tar(['-xOzf', tarball, 'package/package.json'])); -assert.equal(manifest.name, '@parallel-web/dsh-responses-subagent'); -assert.match(manifest.version, /^\d+\.\d+\.\d+(?:-rc\.\d+)?$/); -assert.equal(manifest.private, undefined); -assert.equal(manifest.publishConfig?.access, 'public'); -assert.equal(manifest.dsh?.bundle?.patch, './cordis.patch.yml'); -assert.deepEqual(manifest.dependencies, { - '@deepseek-ai/schemastery': '3.18.1', -}); - -const combinedPublicText = entries - .filter((entry) => /\.(?:js|d\.ts|md|yml|json)$/.test(entry)) - .map((entry) => tar(['-xOzf', tarball, entry])) - .join('\n'); - -for (const forbidden of [ - `/${'Users'}/`, - ['developer', 'parallel', 'code'].join('/'), - `${'.agent'}/`, - 'Authorization: Bearer', -]) { - assert.equal( - combinedPublicText.includes(forbidden), - false, - `packed artifact contains forbidden text: ${forbidden}` - ); -} - -for (const required of [ - 'https://api.parallel.ai/v1/responses', - 'parallel-responses', - 'parallel_research', - 'enableRunInBackground: false', -]) { - assert.equal( - combinedPublicText.includes(required), - true, - `packed artifact must retain contract text: ${required}` - ); -} - -process.stdout.write(`packed artifact valid: ${tarball}\n`); diff --git a/packages/dsh-responses-subagent/scripts/verify-packed-profile.mjs b/packages/dsh-responses-subagent/scripts/verify-packed-profile.mjs deleted file mode 100644 index 290c92b..0000000 --- a/packages/dsh-responses-subagent/scripts/verify-packed-profile.mjs +++ /dev/null @@ -1,91 +0,0 @@ -import assert from 'node:assert/strict'; -import { realpathSync } from 'node:fs'; -import { createRequire } from 'node:module'; -import { join, resolve } from 'node:path'; -import { pathToFileURL } from 'node:url'; - -function argument(name) { - const index = process.argv.indexOf(name); - assert.notEqual(index, -1, `missing ${name}`); - const value = process.argv[index + 1]; - assert.ok(value, `missing value for ${name}`); - return value; -} - -const dshHome = resolve(argument('--dsh-home')); -const expectedVersion = argument('--expected-version'); -const profileManifest = join(dshHome, 'profiles', 'web', 'package.json'); -const profileRequire = createRequire(profileManifest); -const pluginManifest = profileRequire.resolve( - '@parallel-web/dsh-responses-subagent/package.json' -); -const pluginRequire = createRequire(pluginManifest); - -const pluginPackage = pluginRequire(pluginManifest); -assert.equal(pluginPackage.version, expectedVersion); - -const resolutions = {}; -for (const packageName of [ - '@deepseek-ai/cordis', - '@deepseek-ai/dsh-launch-environment', - '@deepseek-ai/dsh-session', - '@deepseek-ai/dsh-subagent', - '@deepseek-ai/dsh-tool-subagent', -]) { - const fromProfile = realpathSync(profileRequire.resolve(packageName)); - const fromPlugin = realpathSync(pluginRequire.resolve(packageName)); - assert.equal( - fromPlugin, - fromProfile, - `${packageName} must resolve to one shared entrypoint` - ); - resolutions[packageName] = fromProfile; -} - -const pluginEntry = profileRequire.resolve( - '@parallel-web/dsh-responses-subagent' -); -const plugin = await import(pathToFileURL(pluginEntry).href); -const cordis = await import( - pathToFileURL(resolutions['@deepseek-ai/cordis']).href -); -const subagents = await import( - pathToFileURL(resolutions['@deepseek-ai/dsh-subagent']).href -); - -assert.equal('default' in plugin, false); -assert.equal(plugin.PARALLEL_RESPONSES_PROVIDER_ID, 'parallel-responses'); -assert.equal(plugin.Config.dict.apiKey.meta.role, 'secret'); - -const sections = []; -const ctx = new cordis.Context(); -ctx.provide('systemPrompt', { - section: (section) => sections.push(section), -}); -const runtimeFiber = await ctx.plugin(subagents.default); -const providerFiber = await ctx.plugin(plugin, { - apiKey: 'parallel_test_packed_profile', -}); -assert.deepEqual(ctx.subagents.list(), ['parallel-responses']); -const provider = ctx.subagents.getProvider('parallel-responses'); -assert.ok(provider); -assert.equal(provider.name, 'parallel-responses'); -assert.deepEqual(provider.capabilities, { - outputSchema: false, - depthLimit: false, - toolFilter: false, - persona: false, -}); -assert.equal(provider.inheritsParentContext, false); -assert.equal(typeof provider.start, 'function'); -assert.equal('prepareContinuable' in provider, false); -assert.equal(sections.length, 1); -assert.equal(sections[0].name, 'tool:parallel_research'); - -await providerFiber.dispose(); -assert.equal(ctx.subagents.getProvider('parallel-responses'), undefined); -await runtimeFiber.dispose(); - -process.stdout.write( - `packed profile valid: ${JSON.stringify({ version: pluginPackage.version, shared: Object.keys(resolutions) })}\n` -); diff --git a/packages/dsh-responses-subagent/scripts/verify-profile-overlay.mjs b/packages/dsh-responses-subagent/scripts/verify-profile-overlay.mjs deleted file mode 100644 index e77dad5..0000000 --- a/packages/dsh-responses-subagent/scripts/verify-profile-overlay.mjs +++ /dev/null @@ -1,84 +0,0 @@ -import assert from 'node:assert/strict'; -import { readFile } from 'node:fs/promises'; -import { resolve } from 'node:path'; -import { entryListSchema } from '@deepseek-ai/cordis-plugin-include'; -import yaml from 'js-yaml'; - -function argument(name) { - const index = process.argv.indexOf(name); - assert.notEqual(index, -1, `missing ${name}`); - const value = process.argv[index + 1]; - assert.ok(value, `missing value for ${name}`); - return resolve(value); -} - -async function load(path) { - return yaml.load(await readFile(path, 'utf8'), { schema: entryListSchema }); -} - -function flatten(entries) { - const rows = []; - for (const entry of entries) { - rows.push(entry); - if (entry?.group === true && Array.isArray(entry.config)) { - rows.push(...flatten(entry.config)); - } - } - return rows; -} - -function one(rows, id) { - const matches = rows.filter((row) => row?.id === id); - assert.equal(matches.length, 1, `expected exactly one ${id} row`); - return matches[0]; -} - -const before = flatten(await load(argument('--before'))); -const after = flatten(await load(argument('--after'))); -const removed = flatten(await load(argument('--removed'))); - -for (const rows of [before, removed]) { - assert.equal( - rows.some((row) => row?.id === 'subagent-parallel-responses'), - false - ); - assert.equal( - rows.some((row) => row?.id === 'tool-subagent-parallel-responses'), - false - ); -} - -assert.deepEqual(one(after, 'subagent-parallel-responses'), { - id: 'subagent-parallel-responses', - name: '@parallel-web/dsh-responses-subagent', -}); -assert.deepEqual(one(after, 'tool-subagent-parallel-responses'), { - id: 'tool-subagent-parallel-responses', - name: '@deepseek-ai/dsh-tool-subagent', - config: { - provider: 'parallel-responses', - toolName: 'parallel_research', - enableRunInBackground: false, - backgroundMode: 'one-shot', - maxDepth: 'provider-managed', - }, -}); - -for (const id of [ - 'subagent', - 'tool-subagent', - 'web', - 'web-search-deepseek', - 'tool-web', -]) { - assert.deepEqual(one(after, id), one(before, id), `${id} changed on install`); - assert.deepEqual( - one(removed, id), - one(before, id), - `${id} changed after removal` - ); -} - -process.stdout.write( - 'profile overlay valid: provider and foreground-only research tool install/remove cleanly; existing Search and subagent rows preserved\n' -); diff --git a/packages/dsh-responses-subagent/src/index.ts b/packages/dsh-responses-subagent/src/index.ts index 592a045..d2a73c8 100644 --- a/packages/dsh-responses-subagent/src/index.ts +++ b/packages/dsh-responses-subagent/src/index.ts @@ -5,8 +5,6 @@ import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment'; import z from '@deepseek-ai/schemastery'; import type {} from '@deepseek-ai/dsh-subagent'; import { - DEFAULT_MAX_ACTIVE_RUNS, - MAX_ACTIVE_RUN_LIMIT, PARALLEL_RESPONSES_EFFORTS, ParallelResponsesProvider, type ParallelResponsesEffort, @@ -19,12 +17,10 @@ export const name = 'subagent-parallel-responses'; export const inject = ['subagents', 'systemPrompt']; const RESEARCH_TOOL_GUIDANCE = - 'Use parallel_research for current web research and primary-source ' + - 'verification. It delegates to an autonomous web-research specialist and ' + - 'returns a synthesized, citation-backed answer, not raw search results. ' + - 'Pass a complete, self-contained natural-language research question that ' + - 'preserves every requested entity, date, unit, and constraint. Do not ' + - 'invent JSON schemas, machine-output requirements, or keyword fragments. '; + 'parallel_research delegates to an autonomous web-research specialist and ' + + 'returns a synthesized, citation-backed answer. Pass a complete, ' + + 'self-contained research question with all requested constraints. Do not ' + + 'invent JSON schemas or machine-output requirements. '; function researchToolGuidance(effort: ParallelResponsesEffort): string { const strategy = @@ -43,19 +39,11 @@ export interface Config { apiKey?: string; /** Research tier; higher effort trades cost for deeper investigation. */ effort?: ParallelResponsesEffort; - /** Maximum simultaneous research calls, including parallel fan-out. */ - maxConcurrentRuns?: number; } export const Config: z = z.object({ apiKey: z.string().role('secret'), effort: z.union(PARALLEL_RESPONSES_EFFORTS), - maxConcurrentRuns: z - .number() - .step(1) - .min(1) - .max(MAX_ACTIVE_RUN_LIMIT) - .default(DEFAULT_MAX_ACTIVE_RUNS), }); /** @@ -77,9 +65,6 @@ export function apply(ctx: Context, config: Config): void { new ParallelResponsesProvider({ apiKey, ...(config.effort === undefined ? {} : { effort: config.effort }), - ...(config.maxConcurrentRuns === undefined - ? {} - : { maxConcurrentRuns: config.maxConcurrentRuns }), onError: (error) => { ctx.logger.warn( `subagent-parallel-responses: remote run failed: ${error.message}` diff --git a/packages/dsh-responses-subagent/src/provider.ts b/packages/dsh-responses-subagent/src/provider.ts index 28aabc3..9043a0f 100644 --- a/packages/dsh-responses-subagent/src/provider.ts +++ b/packages/dsh-responses-subagent/src/provider.ts @@ -8,6 +8,8 @@ import { randomUUID } from 'node:crypto'; import { SessionId } from '@deepseek-ai/dsh-session'; import { NO_START_CAPABILITIES, + settleRunResult, + subprocessRunHandle, type ResolvedSubagentStartRequest, type SubagentProvider, type SubagentResult, @@ -19,8 +21,6 @@ export const PARALLEL_RESPONSES_URL = 'https://api.parallel.ai/v1/responses'; export const PARALLEL_RESPONSES_TIMEOUT_MS = 10 * 60_000; export const PARALLEL_RESPONSES_MAX_INPUT_CHARS = 20_000; export const PARALLEL_RESPONSES_EFFORTS = ['low', 'medium', 'high'] as const; -export const DEFAULT_MAX_ACTIVE_RUNS = 2; -export const MAX_ACTIVE_RUN_LIMIT = 20; export const PARALLEL_RESEARCH_INSTRUCTIONS = 'You are an autonomous live-web research specialist. Answer the actual ' + 'research question with current evidence, prioritizing official ' + @@ -39,78 +39,12 @@ type Fetch = ( init?: RequestInit ) => Promise; -interface CapacityWaiter { - readonly signal: AbortSignal; - readonly resolve: (release: () => void) => void; - readonly reject: (error: Error) => void; - readonly onAbort: () => void; -} - -class Capacity { - private active = 0; - private readonly waiters: CapacityWaiter[] = []; - - constructor(private readonly limit: number) {} - - async acquire(signal: AbortSignal): Promise<() => void> { - if (signal.aborted) throw cancellationError('before capacity admission'); - if (this.active < this.limit) { - this.active += 1; - return this.releaseOnce(); - } - - return await new Promise<() => void>((resolve, reject) => { - const onAbort = (): void => { - const index = this.waiters.indexOf(waiter); - if (index >= 0) this.waiters.splice(index, 1); - reject(cancellationError('while waiting for capacity')); - }; - const waiter: CapacityWaiter = { signal, resolve, reject, onAbort }; - this.waiters.push(waiter); - signal.addEventListener('abort', onAbort, { once: true }); - }); - } - - private releaseOnce(): () => void { - let released = false; - return (): void => { - if (released) return; - released = true; - this.active -= 1; - this.admitNext(); - }; - } - - private admitNext(): void { - for (;;) { - const waiter = this.waiters.shift(); - if (waiter === undefined) return; - waiter.signal.removeEventListener('abort', waiter.onAbort); - if (waiter.signal.aborted) { - waiter.reject(cancellationError('while waiting for capacity')); - continue; - } - this.active += 1; - waiter.resolve(this.releaseOnce()); - return; - } - } -} - -function cancellationError(when: string): Error { - return new Error(`dsh-responses-subagent: request was aborted ${when}`); -} - function diagnosticError(error: unknown): Error { if ( error instanceof Error && - (/^dsh-responses-subagent: Parallel Responses returned HTTP \d{3}$/u.test( + /^dsh-responses-subagent: Parallel Responses (?:returned HTTP \d{3}|reported response\.failed|request timed out)$/u.test( error.message - ) || - error.message === - 'dsh-responses-subagent: Parallel Responses reported response.failed' || - error.message === - 'dsh-responses-subagent: Parallel Responses request timed out') + ) ) { return new Error(error.message); } @@ -129,21 +63,15 @@ function isRecord(value: unknown): value is Record { * @returns the exact concatenated text supplied to Parallel Responses. */ export function researchPrompt(request: ResolvedSubagentStartRequest): string { - if (request.prompt.length === 0) { + if ( + request.prompt.length === 0 || + !request.prompt.every((block) => block.type === 'text') + ) { throw new Error( 'dsh-responses-subagent: the research prompt must contain only text blocks' ); } - const texts: string[] = []; - for (const block of request.prompt) { - if (block.type !== 'text') { - throw new Error( - 'dsh-responses-subagent: the research prompt must contain only text blocks' - ); - } - texts.push(block.text); - } - const prompt = texts.join(''); + const prompt = request.prompt.map((block) => block.text).join(''); if (prompt.trim().length === 0) { throw new Error( 'dsh-responses-subagent: the research prompt must not be empty' @@ -160,13 +88,6 @@ export function researchPrompt(request: ResolvedSubagentStartRequest): string { return prompt; } -function citationLine(title: string | undefined, url: string): string { - const compactTitle = title?.replace(/\s+/gu, ' ').trim(); - return compactTitle === undefined || compactTitle.length === 0 - ? `- ${url}` - : `- ${compactTitle} — ${url}`; -} - function completedResult(payload: unknown): SubagentResult { if (!isRecord(payload) || payload.status !== 'completed') { if (isRecord(payload) && payload.status === 'failed') { @@ -187,50 +108,37 @@ function completedResult(payload: unknown): SubagentResult { const answerParts: string[] = []; const citations = new Map(); for (const item of payload.output) { - if (!isRecord(item) || item.type !== 'message') continue; - if (!Array.isArray(item.content)) { - throw new TypeError( - 'dsh-responses-subagent: completed message has no content array' - ); + if ( + !isRecord(item) || + item.type !== 'message' || + !Array.isArray(item.content) + ) { + continue; } for (const content of item.content) { - if (!isRecord(content) || content.type !== 'output_text') continue; - if (typeof content.text !== 'string') { - throw new TypeError( - 'dsh-responses-subagent: completed output text is malformed' - ); + if ( + !isRecord(content) || + content.type !== 'output_text' || + typeof content.text !== 'string' + ) { + continue; } answerParts.push(content.text); - if (content.annotations === undefined) continue; - if (!Array.isArray(content.annotations)) { - throw new TypeError( - 'dsh-responses-subagent: completed annotations are malformed' - ); - } + if (!Array.isArray(content.annotations)) continue; for (const annotation of content.annotations) { - if (!isRecord(annotation) || annotation.type !== 'url_citation') { - continue; - } - if (typeof annotation.url !== 'string' || annotation.url.length === 0) { - throw new TypeError( - 'dsh-responses-subagent: URL citation is malformed' - ); - } if ( - annotation.title !== undefined && - typeof annotation.title !== 'string' + !isRecord(annotation) || + annotation.type !== 'url_citation' || + typeof annotation.url !== 'string' || + annotation.url.length === 0 ) { - throw new TypeError( - 'dsh-responses-subagent: citation title is malformed' - ); + continue; } - const priorTitle = citations.get(annotation.url); - if ( - !citations.has(annotation.url) || - priorTitle === undefined || - priorTitle.trim().length === 0 - ) { - citations.set(annotation.url, annotation.title); + if (!citations.get(annotation.url)?.trim()) { + citations.set( + annotation.url, + typeof annotation.title === 'string' ? annotation.title : undefined + ); } } } @@ -242,9 +150,10 @@ function completedResult(payload: unknown): SubagentResult { 'dsh-responses-subagent: completed response contains no answer text' ); } - const sourceLines = [...citations].map(([url, title]) => - citationLine(title, url) - ); + const sourceLines = [...citations].map(([url, title]) => { + const compactTitle = title?.replace(/\s+/gu, ' ').trim(); + return compactTitle ? `- ${compactTitle} — ${url}` : `- ${url}`; + }); const text = sourceLines.length === 0 ? answer @@ -256,7 +165,6 @@ function completedResult(payload: unknown): SubagentResult { export interface ParallelResponsesProviderOptions { readonly apiKey: string; readonly effort?: ParallelResponsesEffort; - readonly maxConcurrentRuns?: number; readonly fetch?: Fetch; /** Safe diagnostic sink for failures flattened into an error result. */ readonly onError?: (error: Error) => void; @@ -272,135 +180,84 @@ export class ParallelResponsesProvider implements SubagentProvider { private readonly effort: ParallelResponsesEffort; private readonly fetch: Fetch; private readonly onError: ((error: Error) => void) | undefined; - private readonly capacity: Capacity; constructor(options: ParallelResponsesProviderOptions) { - const effort = options.effort === undefined ? 'medium' : options.effort; - if (!PARALLEL_RESPONSES_EFFORTS.some((candidate) => candidate === effort)) { - throw new Error( - 'dsh-responses-subagent: effort must be low, medium, or high' - ); - } - const maxConcurrentRuns = - options.maxConcurrentRuns ?? DEFAULT_MAX_ACTIVE_RUNS; - if ( - !Number.isSafeInteger(maxConcurrentRuns) || - maxConcurrentRuns < 1 || - maxConcurrentRuns > MAX_ACTIVE_RUN_LIMIT - ) { - throw new Error( - `dsh-responses-subagent: maxConcurrentRuns must be an integer from 1 to ${MAX_ACTIVE_RUN_LIMIT}` - ); - } this.apiKey = options.apiKey; - this.effort = effort; + this.effort = options.effort ?? 'medium'; this.fetch = options.fetch ?? globalThis.fetch; this.onError = options.onError; - this.capacity = new Capacity(maxConcurrentRuns); } async start(request: ResolvedSubagentStartRequest): Promise { const prompt = researchPrompt(request); - if (this.apiKey.trim().length === 0) { - throw new Error('dsh-responses-subagent: PARALLEL_API_KEY is required'); - } - - const release = await this.capacity.acquire(request.signal); if (request.signal.aborted) { - release(); - throw cancellationError('before run publication'); + throw new Error('dsh-responses-subagent: request was aborted'); } const controller = new AbortController(); let cancelled = false; const requestCancel = (): void => { - if (cancelled) return; cancelled = true; - controller.abort(cancellationError('during the remote run')); + controller.abort(); }; request.signal.addEventListener('abort', requestCancel, { once: true }); let timedOut = false; + const timeoutError = new Error( + 'dsh-responses-subagent: Parallel Responses request timed out' + ); const timer = setTimeout(() => { timedOut = true; - controller.abort( - new Error( - 'dsh-responses-subagent: Parallel Responses request timed out' - ) - ); + controller.abort(timeoutError); }, PARALLEL_RESPONSES_TIMEOUT_MS); - const result: Promise = (async () => { - try { - const response = await this.fetch(PARALLEL_RESPONSES_URL, { - method: 'POST', - headers: { - Authorization: `Bearer ${this.apiKey}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - model: 'parallel', - input: prompt, - instructions: PARALLEL_RESEARCH_INSTRUCTIONS, - reasoning: { effort: this.effort }, - stream: false, - }), - redirect: 'error', - signal: controller.signal, - }); - if (!response.ok) { - throw new Error( - `dsh-responses-subagent: Parallel Responses returned HTTP ${response.status}` - ); - } - const terminal = completedResult(await response.json()); - if (cancelled) return { output: [], stopReason: 'aborted' }; - if (timedOut) { - throw new Error( - 'dsh-responses-subagent: Parallel Responses request timed out' - ); - } - return terminal; - } catch (error: unknown) { - if (!cancelled) { - try { - this.onError?.( - diagnosticError( - timedOut - ? new Error( - 'dsh-responses-subagent: Parallel Responses request timed out' - ) - : error - ) + const result = settleRunResult({ + attempt: async () => { + try { + const response = await this.fetch(PARALLEL_RESPONSES_URL, { + method: 'POST', + headers: { + Authorization: `Bearer ${this.apiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + model: 'parallel', + input: prompt, + instructions: PARALLEL_RESEARCH_INSTRUCTIONS, + reasoning: { effort: this.effort }, + stream: false, + }), + redirect: 'error', + signal: controller.signal, + }); + if (!response.ok) { + throw new Error( + `dsh-responses-subagent: Parallel Responses returned HTTP ${response.status}` ); - } catch { - // A diagnostic sink must not violate the non-rejecting run seam. - return { output: [], stopReason: 'error' }; } + const terminal = completedResult(await response.json()); + if (timedOut) throw timeoutError; + return terminal; + } finally { + clearTimeout(timer); } - return { - output: [], - stopReason: cancelled ? 'aborted' : 'error', - }; - } finally { - clearTimeout(timer); - request.signal.removeEventListener('abort', requestCancel); - release(); - } - })(); + }, + collectOutput: () => [], + cancelled: () => cancelled, + signal: request.signal, + onAbort: requestCancel, + onError: (error) => { + this.onError?.(diagnosticError(timedOut ? timeoutError : error)); + }, + }); - let disposal: Promise | undefined; - return { + return subprocessRunHandle({ id: SessionId(randomUUID()), - localAgent: undefined, result, - dispose: (): Promise => { - if (disposal !== undefined) return disposal; - request.signal.removeEventListener('abort', requestCancel); - requestCancel(); - disposal = result.then(() => undefined); - return disposal; - }, - }; + signal: request.signal, + onAbort: requestCancel, + requestCancel, + teardown: () => result.then(() => undefined), + }); } } diff --git a/packages/dsh-responses-subagent/tests/provider.spec.ts b/packages/dsh-responses-subagent/tests/provider.spec.ts index b9062e1..e57b205 100644 --- a/packages/dsh-responses-subagent/tests/provider.spec.ts +++ b/packages/dsh-responses-subagent/tests/provider.spec.ts @@ -11,7 +11,6 @@ import { PARALLEL_RESPONSES_URL, PARALLEL_RESEARCH_INSTRUCTIONS, ParallelResponsesProvider, - type ParallelResponsesEffort, researchPrompt, } from '../src/provider.ts'; @@ -63,7 +62,7 @@ afterEach(() => { }); describe('Parallel Responses request and output', () => { - it('sends one fixed request and renders deduplicated title and URL citations', async () => { + it('sends one fixed request and tolerates imperfect, duplicate citations', async () => { const response = completed('Evidence-backed answer.', [ { type: 'url_citation', @@ -76,6 +75,18 @@ describe('Parallel Responses request and output', () => { url: 'https://a.test/report', }, { type: 'url_citation', url: 'https://b.test/data' }, + { type: 'url_citation', url: '' }, + { type: 'url_citation', url: 42 }, + { + type: 'url_citation', + title: 42, + url: 'https://c.test/report', + }, + { + type: 'url_citation', + title: 'Recovered title', + url: 'https://c.test/report', + }, ]); const fetch = vi.fn().mockResolvedValue(response); const provider = new ParallelResponsesProvider({ @@ -93,7 +104,8 @@ describe('Parallel Responses request and output', () => { text: 'Evidence-backed answer.\n\nSources:\n' + '- Source A — https://a.test/report\n' + - '- https://b.test/data', + '- https://b.test/data\n' + + '- Recovered title — https://c.test/report', }, ], stopReason: 'completed', @@ -140,19 +152,6 @@ describe('Parallel Responses request and output', () => { } ); - it.each(['unsupported', '', 1, null])( - 'rejects invalid direct-provider effort %s', - (effort) => { - expect( - () => - new ParallelResponsesProvider({ - apiKey: 'parallel_test_provider', - effort: effort as unknown as ParallelResponsesEffort, - }) - ).toThrow('effort must be low, medium, or high'); - } - ); - it('preserves text block order and rejects blank or non-text prompts', () => { expect( researchPrompt( @@ -259,7 +258,7 @@ describe('Parallel Responses request and output', () => { }); }); -describe('Parallel Responses capacity and lifecycle', () => { +describe('Parallel Responses lifecycle', () => { it('rejects cancellation before publication without making a request', async () => { const fetch = vi.fn(); const provider = new ParallelResponsesProvider({ @@ -292,57 +291,21 @@ describe('Parallel Responses capacity and lifecycle', () => { await disposal; }); - it('admits only two active requests and removes an aborted queued start', async () => { + it('does not throttle simultaneous research calls', async () => { const fetch = abortableFetch(); const provider = new ParallelResponsesProvider({ apiKey: 'parallel_test_provider', fetch, }); - const first = await provider.start(request()); - const second = await provider.start(request()); - const queuedController = new AbortController(); - const queued = provider.start(request(undefined, queuedController.signal)); - await vi.waitFor(() => expect(fetch).toHaveBeenCalledTimes(2)); - - queuedController.abort(); - await expect(queued).rejects.toThrow('waiting for capacity'); - expect(fetch).toHaveBeenCalledTimes(2); - - await first.dispose(); - const replacement = await provider.start(request()); - expect(fetch).toHaveBeenCalledTimes(3); - await Promise.all([second.dispose(), replacement.dispose()]); - }); - - it('admits the configured number of simultaneous research calls', async () => { - const fetch = abortableFetch(); - const provider = new ParallelResponsesProvider({ - apiKey: 'parallel_test_provider', - maxConcurrentRuns: 4, - fetch, - }); const runs = await Promise.all( - Array.from({ length: 4 }, async () => await provider.start(request())) + Array.from({ length: 20 }, async () => await provider.start(request())) ); - expect(fetch).toHaveBeenCalledTimes(4); + expect(fetch).toHaveBeenCalledTimes(20); await Promise.all(runs.map(async (run) => await run.dispose())); }); - it.each([0, 1.5, 21, Number.NaN])( - 'rejects an invalid concurrency limit %s', - (maxConcurrentRuns) => { - expect( - () => - new ParallelResponsesProvider({ - apiKey: 'parallel_test_provider', - maxConcurrentRuns, - }) - ).toThrow('maxConcurrentRuns must be an integer from 1 to 20'); - } - ); - - it('times out once with no retry and releases capacity', async () => { + it('times out once with no retry', async () => { vi.useFakeTimers(); const fetch = abortableFetch(); const onError = vi.fn(); @@ -438,7 +401,6 @@ describe('Parallel Responses plugin registration', () => { const fiber = await ctx.plugin(plugin, { apiKey: 'parallel_test_explicit', effort: 'low', - maxConcurrentRuns: 8, }); const registration = section.mock.calls[0]?.[0] as { diff --git a/packages/dsh-responses-subagent/tsdown.config.ts b/packages/dsh-responses-subagent/tsdown.config.ts index beb6608..9433656 100644 --- a/packages/dsh-responses-subagent/tsdown.config.ts +++ b/packages/dsh-responses-subagent/tsdown.config.ts @@ -16,7 +16,6 @@ export default defineConfig({ '@deepseek-ai/dsh-launch-environment', '@deepseek-ai/dsh-session', '@deepseek-ai/dsh-subagent', - '@deepseek-ai/dsh-tool-subagent', '@deepseek-ai/schemastery', ], }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f68b40b..d58c59c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -67,12 +67,6 @@ importers: '@deepseek-ai/cordis': specifier: 4.0.1 version: 4.0.1(@deepseek-ai/cordis-plugin-include@1.0.6)(@deepseek-ai/cordis-plugin-loader@1.0.2) - '@deepseek-ai/cordis-plugin-include': - specifier: 1.0.6 - version: 1.0.6(@deepseek-ai/cordis-plugin-loader@1.0.2)(@deepseek-ai/cordis@4.0.1) - '@deepseek-ai/dsh': - specifier: 0.1.0-rc.6 - version: 0.1.0-rc.6(c422946f0a5c0641b2538ac43317dd99) '@deepseek-ai/dsh-launch-environment': specifier: 0.1.0-rc.6 version: 0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)(@deepseek-ai/dsh-invariants@0.1.0-rc.6(@deepseek-ai/cordis@4.0.1)) @@ -88,9 +82,6 @@ importers: '@types/node': specifier: 26.2.0 version: 26.2.0 - js-yaml: - specifier: 4.2.0 - version: 4.2.0 oxlint: specifier: 1.76.0 version: 1.76.0 From 72e565ca0ce8f8fd0208b193e7ab29a6ef2ca1b8 Mon Sep 17 00:00:00 2001 From: George Pickett Date: Sun, 23 Aug 2026 16:51:01 -0700 Subject: [PATCH 5/6] Support the latest DeepSeek Harness release --- packages/dsh-responses-subagent/README.md | 6 +++--- packages/dsh-responses-subagent/package.json | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/dsh-responses-subagent/README.md b/packages/dsh-responses-subagent/README.md index 16ad877..3b9e719 100644 --- a/packages/dsh-responses-subagent/README.md +++ b/packages/dsh-responses-subagent/README.md @@ -16,7 +16,7 @@ This package has not been published to npm. Install from the repository root: ```sh pnpm --filter @parallel-web/dsh-responses-subagent build pnpm --dir packages/dsh-responses-subagent pack --pack-destination /tmp -npx --yes @deepseek-ai/dsh@0.1.0-rc.6 \ +npx --yes @deepseek-ai/dsh@0.1.1-rc.2 \ plugin --profile web add \ /tmp/parallel-web-dsh-responses-subagent-0.1.0-rc.0.tgz ``` @@ -26,7 +26,7 @@ that starts Harness, then configure a parent model in **Settings > Models**: ```sh export PARALLEL_API_KEY="your-key" -npx --yes @deepseek-ai/dsh@0.1.0-rc.6 web +npx --yes @deepseek-ai/dsh@0.1.1-rc.2 web ``` After the first npm release, replace the tarball path in the install command @@ -58,6 +58,6 @@ Keep `PARALLEL_API_KEY` in the launch environment, not in this profile. ## Remove ```sh -npx --yes @deepseek-ai/dsh@0.1.0-rc.6 \ +npx --yes @deepseek-ai/dsh@0.1.1-rc.2 \ plugin --profile web remove @parallel-web/dsh-responses-subagent ``` diff --git a/packages/dsh-responses-subagent/package.json b/packages/dsh-responses-subagent/package.json index f3f0df3..e60b02f 100644 --- a/packages/dsh-responses-subagent/package.json +++ b/packages/dsh-responses-subagent/package.json @@ -54,10 +54,10 @@ }, "peerDependencies": { "@deepseek-ai/cordis": "4.0.1", - "@deepseek-ai/dsh-launch-environment": "0.1.0-rc.6", - "@deepseek-ai/dsh-session": "0.1.0-rc.6", - "@deepseek-ai/dsh-subagent": "0.1.0-rc.6", - "@deepseek-ai/dsh-tool-subagent": "0.1.0-rc.6" + "@deepseek-ai/dsh-launch-environment": "^0.1.0-rc.6 || ^0.1.1-rc.2", + "@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.1.1-rc.2", + "@deepseek-ai/dsh-subagent": "^0.1.0-rc.6 || ^0.1.1-rc.2", + "@deepseek-ai/dsh-tool-subagent": "^0.1.0-rc.6 || ^0.1.1-rc.2" }, "dependencies": { "@deepseek-ai/schemastery": "3.18.1" From f9e09f46cf08951287521503a076e6126a2c7e2e Mon Sep 17 00:00:00 2001 From: George Pickett Date: Fri, 28 Aug 2026 11:10:06 -0700 Subject: [PATCH 6/6] Clarify Responses plugin preview setup --- packages/dsh-responses-subagent/README.md | 81 ++++++++++++++++++++--- 1 file changed, 71 insertions(+), 10 deletions(-) diff --git a/packages/dsh-responses-subagent/README.md b/packages/dsh-responses-subagent/README.md index 3b9e719..554bed1 100644 --- a/packages/dsh-responses-subagent/README.md +++ b/packages/dsh-responses-subagent/README.md @@ -9,28 +9,64 @@ Only the explicitly delegated question is forwarded as research input. Parent history, files, workspace state, tools, and environment details are never gathered automatically. -## Install +## Availability -This package has not been published to npm. Install from the repository root: +This plugin is an unreleased preview. It is not published to npm, and there is +no release tarball to download yet. The setup below requires a locally built +preview tarball. If you want to build one yourself, see [Development](#development). + +## Set up a preview + +You will need: + +- Node.js 22.19 or later in the 22.x series, or Node.js 24 or newer; +- pnpm 10 or newer, available in your terminal; +- a [Parallel API key](https://platform.parallel.ai/); and +- a preview `.tgz` file for this plugin. + +### 1. Install the plugin + +Install into Harness's `web` profile. Replace `/absolute/path/to/plugin.tgz` +with the path to your preview tarball: ```sh -pnpm --filter @parallel-web/dsh-responses-subagent build -pnpm --dir packages/dsh-responses-subagent pack --pack-destination /tmp npx --yes @deepseek-ai/dsh@0.1.1-rc.2 \ - plugin --profile web add \ - /tmp/parallel-web-dsh-responses-subagent-0.1.0-rc.0.tgz + plugin --profile web add /absolute/path/to/plugin.tgz ``` -Provide your [Parallel API key](https://platform.parallel.ai/) in the terminal -that starts Harness, then configure a parent model in **Settings > Models**: +### 2. Start Harness + +Set your Parallel API key in the same terminal that starts Harness: ```sh export PARALLEL_API_KEY="your-key" npx --yes @deepseek-ai/dsh@0.1.1-rc.2 web ``` -After the first npm release, replace the tarball path in the install command -with `@parallel-web/dsh-responses-subagent@rc`. +Open [http://127.0.0.1:3080](http://127.0.0.1:3080), configure your parent +model and its credentials in **Settings > Models**, choose a workspace, and +start a new session. Restart Harness if it was already running when you +installed the plugin or set the key. + +### 3. Try a research question + +Ask Harness to use the plugin, for example: + +> Use parallel_research to compare Node.js 22 and 24 support schedules. Include +> links to the official sources. + +The agent calls `parallel_research` and receives a researched answer with +source URLs. It can still use `web_search` separately. + +To check that the plugin is installed in the profile: + +```sh +npx --yes @deepseek-ai/dsh@0.1.1-rc.2 --profile web --dump-config +``` + +Look for `subagent-parallel-responses` and `toolName: parallel_research`. +If the plugin reports that `PARALLEL_API_KEY` is required, set the key in the +terminal that starts Harness and restart it. ## Research depth and parallelism @@ -61,3 +97,28 @@ Keep `PARALLEL_API_KEY` in the launch environment, not in this profile. npx --yes @deepseek-ai/dsh@0.1.1-rc.2 \ plugin --profile web remove @parallel-web/dsh-responses-subagent ``` + +Restart Harness after removing the plugin. + +## Development + +These steps are for building a preview from source. Use a checkout containing +this package, available in [PR #42](https://github.com/parallel-web/parallel-npm-packages/pull/42). +From the repository root, enable Corepack to use the pinned pnpm version, +install dependencies, and run the package checks: + +```sh +corepack enable +pnpm install --frozen-lockfile +pnpm --filter @parallel-web/dsh-responses-subagent check +``` + +The checks include the build. Pack it into a local tarball: + +```sh +PREVIEW_PACK_DIR="$(mktemp -d)" +pnpm --dir packages/dsh-responses-subagent pack --pack-destination "$PREVIEW_PACK_DIR" +``` + +Use the tarball path printed by `pack` in the [preview setup](#1-install-the-plugin). +This does not publish the package.