@companion-ai/feynman 0.5.14 → 0.5.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.feynman/agents/researcher.md +1 -2
- package/.feynman/agents/reviewer.md +1 -2
- package/.feynman/agents/verifier.md +1 -2
- package/.feynman/agents/writer.md +1 -2
- package/RELEASES.md +16 -0
- package/bin/feynman.js +11 -0
- package/dist/bootstrap/home.js +8 -2
- package/dist/cli.js +31 -10
- package/dist/model/commands.js +40 -2
- package/dist/pi/packages.js +3 -0
- package/dist/pi/settings.js +11 -1
- package/extensions/research-tools/request-pacer.ts +17 -0
- package/extensions/research-tools/science-database-openalex.ts +41 -8
- package/extensions/research-tools/science-databases.ts +3 -11
- package/metadata/commands.mjs +2 -1
- package/package.json +7 -8
- package/prompts/deepresearch.md +4 -4
- package/prompts/lit.md +1 -1
- package/prompts/summarize.md +1 -1
|
@@ -3,7 +3,6 @@ name: researcher
|
|
|
3
3
|
description: Gather primary evidence across papers, web sources, repos, docs, and local artifacts.
|
|
4
4
|
thinking: high
|
|
5
5
|
tools: read, write, edit, bash, grep, find, ls, web_search, fetch_content, get_search_content, feynman_science_database_search, hf_dataset_info, hf_repo_files, hf_repo_read_file
|
|
6
|
-
output: research.md
|
|
7
6
|
defaultProgress: true
|
|
8
7
|
async: true
|
|
9
8
|
---
|
|
@@ -94,7 +93,7 @@ Numbered list matching the evidence table:
|
|
|
94
93
|
- If you were assigned multiple questions, track them explicitly in the file and mark each as `done`, `blocked`, or `needs follow-up`. Do not silently skip questions.
|
|
95
94
|
|
|
96
95
|
## Output contract
|
|
97
|
-
- Save to the
|
|
96
|
+
- Save to the path the parent specifies, relative to the workspace. If none is given, return the complete artifact in your final response.
|
|
98
97
|
- Minimum viable output: evidence table with ≥5 numbered entries, findings with inline references, and a numbered Sources section.
|
|
99
98
|
- Include a short `Coverage Status` section listing what you checked directly, what remains uncertain, and any tasks you could not complete.
|
|
100
99
|
- Write to the file and pass a lightweight reference back — do not dump full content into the parent context.
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
name: reviewer
|
|
3
3
|
description: Run tough but constructive internal research critique of an AI research artifact.
|
|
4
4
|
thinking: high
|
|
5
|
-
output: review.md
|
|
6
5
|
defaultProgress: true
|
|
7
6
|
---
|
|
8
7
|
|
|
@@ -88,5 +87,5 @@ Reference the weakness/question IDs from Part 1 so annotations link back to the
|
|
|
88
87
|
- End with a `Sources` section containing direct URLs for anything additionally inspected during review.
|
|
89
88
|
|
|
90
89
|
## Output contract
|
|
91
|
-
- Save the
|
|
90
|
+
- Save to the path the parent specifies, relative to the workspace. If none is given, return the complete artifact in your final response.
|
|
92
91
|
- The review must contain both the structured review AND inline annotations.
|
|
@@ -3,7 +3,6 @@ name: verifier
|
|
|
3
3
|
description: Post-process a draft to add inline citations and verify every source URL.
|
|
4
4
|
thinking: medium
|
|
5
5
|
tools: read, bash, grep, find, ls, write, edit, web_search, fetch_content, get_search_content, feynman_science_database_search
|
|
6
|
-
output: cited.md
|
|
7
6
|
defaultProgress: true
|
|
8
7
|
async: true
|
|
9
8
|
---
|
|
@@ -57,6 +56,6 @@ Before saving the final document, scan for:
|
|
|
57
56
|
For each item, verify that it maps to a source URL, research note, raw artifact path, or script path. If not, remove it or replace it with a TODO. Add a short `Removed Unsupported Claims` section only when you remove material.
|
|
58
57
|
|
|
59
58
|
## Output contract
|
|
60
|
-
- Save to the
|
|
59
|
+
- Save to the path the parent specifies, relative to the workspace. If none is given, return the complete artifact in your final response.
|
|
61
60
|
- The output is the complete final document — same structure as the input draft, but with inline citations added throughout and a verified Sources section.
|
|
62
61
|
- Do not change the intended structure of the draft, but you may delete or soften unsupported factual claims when necessary to maintain integrity.
|
|
@@ -3,7 +3,6 @@ name: writer
|
|
|
3
3
|
description: Turn research notes into clear, structured briefs and drafts.
|
|
4
4
|
thinking: medium
|
|
5
5
|
tools: read, bash, grep, find, ls, write, edit
|
|
6
|
-
output: draft.md
|
|
7
6
|
defaultProgress: true
|
|
8
7
|
---
|
|
9
8
|
|
|
@@ -52,5 +51,5 @@ Unresolved issues, disagreements between sources, gaps in evidence.
|
|
|
52
51
|
- Before finishing, do a result-provenance sweep for numeric results, figures, charts, benchmarks, tables, and images.
|
|
53
52
|
|
|
54
53
|
## Output contract
|
|
55
|
-
- Save the
|
|
54
|
+
- Save to the path the parent specifies, relative to the workspace. If none is given, return the complete artifact in your final response.
|
|
56
55
|
- Focus on clarity, structure, and evidence traceability.
|
package/RELEASES.md
CHANGED
|
@@ -6,6 +6,22 @@ GitHub release notes are generated from the matching `## vX.Y.Z` section in this
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## v0.5.16 - 2026-09-30
|
|
10
|
+
|
|
11
|
+
- **Subagent research files land in the workspace.** When a workflow gave a subagent a relative output path such as `outputs/.drafts/<slug>-research-web.md`, pi-subagents saved it under `~/.feynman/sessions/subagent-artifacts/outputs/<run>/` instead, so the lead agent could not find the research notes, citations, or verification it asked for (about one in seven installs running research workflows hit this in the last week). Relative subagent outputs now resolve against the workspace, the researcher, reviewer, verifier, and writer no longer default to generic file names such as `research.md`, and `/deepresearch` and `/lit` name every subagent file under `outputs/.drafts/`. If you set `singleRunOutputBaseDir` in `~/.feynman/agent/extensions/subagent/config.json`, Feynman keeps your value.
|
|
12
|
+
- `feynman packages remove <preset>` (also `rm` and `uninstall`) removes an optional package such as `memory`.
|
|
13
|
+
- On Windows, Feynman no longer fails to start when another Feynman start is copying `keybindings.json` (`EBUSY: resource busy or locked`).
|
|
14
|
+
- pi-web-access 0.34.0: `fetch_content` falls back to your other fetch providers on Cloudflare "Just a moment..." pages, web tools start available on models such as DeepSeek without costing a prompt-cache miss, and its undici moves to 8.11.2.
|
|
15
|
+
- Security: three brace-expansion advisories published on 2026-09-29 affect the 5.0.9 that Pi 0.99.1's shrinkwrap locks, which Feynman cannot override. Pi uses it only for package filters and model patterns from your own settings and flags; the next Pi release that ships 5.0.12 replaces it. pi-subagents has still not released its undici fix.
|
|
16
|
+
|
|
17
|
+
## v0.5.15 - 2026-09-29
|
|
18
|
+
|
|
19
|
+
- **Pi 0.99.1, pinned.** Feynman accepted any Pi version, so npm installs picked up Pi 0.99.1 the day it was released, untested, and Pi 0.99 warns on every launch about how Feynman declared it. Feynman now runs on Pi 0.99.1, which adds MCP server support (`/mcp`), declared the way Pi asks, and pinned so new Pi releases arrive only after they pass Feynman's checks. If npm has `legacy-peer-deps` on, it skips Pi; `feynman` now says to reinstall with `--legacy-peer-deps=false` instead of failing on a missing package.
|
|
20
|
+
- **npm installs use a patched undici.** pi-subagents pins undici 8.10.0, which ten advisories published on 2026-09-29 affect (plus GHSA-3wwx-pv8p-q78v), and npm hoisted that copy for every package in an npm install. Feynman now depends on a patched undici directly, so only pi-subagents keeps 8.10.0 until it releases its bump (nicobailon/pi-subagents#2548); the standalone installers were already patched. Correction to the 0.5.14 notes: pi-subagents' background runner installs its undici as the process-wide fetch and WebSocket, so the advisories do apply to background subagent runs.
|
|
21
|
+
- **`feynman model set` explains a model it cannot use.** "Model not available in Pi auth storage" gave no reason. It now says which applies: the provider has no credentials (with the `feynman model login` command to run), a local provider in `models.json` has no `apiKey` (Pi hides providers without one; a placeholder such as `"local"` is enough), or the ID is unknown (with close matches, for example `gpt-5.6` suggests `openai/gpt-5.6-terra`).
|
|
22
|
+
- **Parallel OpenAlex semantic searches no longer fail.** OpenAlex allows one semantic search per second, and parallel searches got `429 Too Many Requests`: in a live test of six at once, five failed. They now run one after another at that pace, and a 429 that asks for a short wait is retried once. Six parallel searches now all succeed.
|
|
23
|
+
- `feynman model set` with no model opens a picker of the available models instead of failing. `feynman model help` lists the model commands, an unknown model subcommand points to it, and `-h` works like `--help`.
|
|
24
|
+
|
|
9
25
|
## v0.5.14 - 2026-09-28
|
|
10
26
|
|
|
11
27
|
- **The Windows installer no longer fails on slow networks (#309).** When usage telemetry could not reach PostHog quickly, the PostHog library printed a timeout to stderr, and Windows PowerShell treats stderr from the installer's `feynman --help` check as a failure. Each telemetry send now has a 1.5 s budget and ends quietly, so no command waits on telemetry (on a stalled network `feynman --help` took 11 s and printed two errors; it now takes about 2 s and prints nothing). The installers also run their checks with telemetry off and judge them by exit code.
|
package/bin/feynman.js
CHANGED
|
@@ -40,4 +40,15 @@ if (process.argv.length === 3 && process.argv[2] === "--version") {
|
|
|
40
40
|
process.exit(0);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
// Pi is a peer dependency, as Pi requires of extension packages, and npm skips
|
|
44
|
+
// peers when legacy-peer-deps is on.
|
|
45
|
+
try {
|
|
46
|
+
import.meta.resolve("@earendil-works/pi-coding-agent");
|
|
47
|
+
import.meta.resolve("@earendil-works/pi-ai");
|
|
48
|
+
} catch {
|
|
49
|
+
console.error("feynman could not find its Pi runtime (@earendil-works/pi-coding-agent), which npm skips when legacy-peer-deps is on.");
|
|
50
|
+
console.error("Reinstall with: npm install -g @companion-ai/feynman --legacy-peer-deps=false");
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
43
54
|
await import(pathToFileURL(resolve(here, "..", "dist", "index.js")).href);
|
package/dist/bootstrap/home.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { copyFileSync, existsSync, readFileSync, renameSync, rmSync } from "node:fs";
|
|
2
|
+
import { constants, copyFileSync, existsSync, readFileSync, renameSync, rmSync } from "node:fs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { getBootstrapStatePath } from "../config/paths.js";
|
|
5
5
|
const LEGACY_SYNCED_SCOPES = { themes: "themes", agents: "agents", skills: "skills" };
|
|
@@ -41,6 +41,12 @@ export function ensureFeynmanAgentDir(appRoot, home, agentDir) {
|
|
|
41
41
|
}
|
|
42
42
|
const keybindingsPath = resolve(agentDir, "keybindings.json");
|
|
43
43
|
if (!existsSync(keybindingsPath)) {
|
|
44
|
-
|
|
44
|
+
try {
|
|
45
|
+
copyFileSync(resolve(appRoot, ".feynman", "config", "keybindings.json"), keybindingsPath, constants.COPYFILE_EXCL);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Another Feynman start may be writing it (Windows reports EBUSY). Pi
|
|
49
|
+
// uses its default keys without the file, and the next start copies it.
|
|
50
|
+
}
|
|
45
51
|
}
|
|
46
52
|
}
|
package/dist/cli.js
CHANGED
|
@@ -18,11 +18,11 @@ import { verifyAlphaAuthStatus } from "./alpha-auth-status.js";
|
|
|
18
18
|
import { ensureFeynmanAgentDir } from "./bootstrap/home.js";
|
|
19
19
|
import { ensureFeynmanHome, getDefaultSessionDir, getFeynmanAgentDir, getFeynmanHome } from "./config/paths.js";
|
|
20
20
|
import { getLastPiStderr, launchPiChat, runPi } from "./pi/launch.js";
|
|
21
|
-
import { installPiPackage, listOptionalPackagePresets, normalizeOptionalPackagePresetName, resolvePackageSource, updatePiPackages, } from "./pi/packages.js";
|
|
21
|
+
import { installPiPackage, removePiPackage, listOptionalPackagePresets, normalizeOptionalPackagePresetName, resolvePackageSource, updatePiPackages, } from "./pi/packages.js";
|
|
22
22
|
import { canonicalizeModelSpec, ensureFeynmanSettings, normalizeThinkingLevel, readJson, } from "./pi/settings.js";
|
|
23
23
|
import { BUNDLED_PI_PACKAGES, buildPiEnv } from "./pi/runtime.js";
|
|
24
24
|
import { getConfiguredServiceTier, normalizeServiceTier, setConfiguredServiceTier } from "./model/service-tier.js";
|
|
25
|
-
import { authenticateModelProvider, getCurrentModelSpec, isLocalModelProvider, loginModelProvider, logoutModelProvider, printModelList, setDefaultModelSpec, } from "./model/commands.js";
|
|
25
|
+
import { authenticateModelProvider, getCurrentModelSpec, isLocalModelProvider, loginModelProvider, logoutModelProvider, printModelList, selectDefaultModel, setDefaultModelSpec, } from "./model/commands.js";
|
|
26
26
|
import { buildModelStatusSnapshotFromRecords, getAuthenticatedModelRecords, isProClassModelSpec, getSupportedModelRecords, } from "./model/catalog.js";
|
|
27
27
|
import { clearSearchConfig, printSearchStatus, setSearchProvider } from "./search/commands.js";
|
|
28
28
|
import { fetchLatestFeynmanVersion, getFeynmanUpgradeLines, isNewerVersion } from "./system/self-update.js";
|
|
@@ -189,10 +189,15 @@ async function handleModelCommand(subcommand, args, feynmanSettingsPath, feynman
|
|
|
189
189
|
}
|
|
190
190
|
if (subcommand === "set") {
|
|
191
191
|
const spec = args[0];
|
|
192
|
-
if (
|
|
192
|
+
if (spec) {
|
|
193
|
+
await setDefaultModelSpec(feynmanSettingsPath, feynmanAuthPath, spec);
|
|
194
|
+
}
|
|
195
|
+
else if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
196
|
+
await selectDefaultModel(feynmanSettingsPath, feynmanAuthPath);
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
193
199
|
throw new Error("Usage: feynman model set <provider/model|provider:model>");
|
|
194
200
|
}
|
|
195
|
-
await setDefaultModelSpec(feynmanSettingsPath, feynmanAuthPath, spec);
|
|
196
201
|
return;
|
|
197
202
|
}
|
|
198
203
|
if (subcommand === "tier") {
|
|
@@ -214,7 +219,14 @@ async function handleModelCommand(subcommand, args, feynmanSettingsPath, feynman
|
|
|
214
219
|
console.log(`Service tier set to ${tier}`);
|
|
215
220
|
return;
|
|
216
221
|
}
|
|
217
|
-
|
|
222
|
+
if (subcommand === "help") {
|
|
223
|
+
printSection("Model Management");
|
|
224
|
+
for (const command of cliCommandSections.find((section) => section.title === "Model Management")?.commands ?? []) {
|
|
225
|
+
printHelpLine(command.usage, command.description);
|
|
226
|
+
}
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
throw new Error(`Unknown model command: ${subcommand}\nRun \`feynman model help\` to see model commands.`);
|
|
218
230
|
}
|
|
219
231
|
async function handleUpdateCommand(piOptions, feynmanVersion, source) {
|
|
220
232
|
const latestFeynmanVersionPromise = fetchLatestFeynmanVersion();
|
|
@@ -255,18 +267,27 @@ async function handlePackagesCommand(subcommand, args, piOptions) {
|
|
|
255
267
|
printInfo(`Install with: feynman packages install <${optionalPresets.map((preset) => preset.name).join("|")}>`);
|
|
256
268
|
return;
|
|
257
269
|
}
|
|
258
|
-
|
|
259
|
-
|
|
270
|
+
const remove = subcommand === "remove" || subcommand === "rm" || subcommand === "uninstall";
|
|
271
|
+
if (subcommand !== "install" && !remove) {
|
|
272
|
+
throw new Error(`Unknown packages command: ${subcommand}\nUse: feynman packages list, install <preset>, or remove <preset>.`);
|
|
260
273
|
}
|
|
261
274
|
const target = args[0];
|
|
262
275
|
if (!target) {
|
|
263
|
-
throw new Error(`Usage: feynman packages install <${listOptionalPackagePresets().map((preset) => preset.name).join("|")}>`);
|
|
276
|
+
throw new Error(`Usage: feynman packages ${remove ? "remove" : "install"} <${listOptionalPackagePresets().map((preset) => preset.name).join("|")}>`);
|
|
264
277
|
}
|
|
265
278
|
const presetName = normalizeOptionalPackagePresetName(target);
|
|
266
279
|
if (!presetName) {
|
|
267
280
|
throw new Error(`Unknown package preset: ${target}`);
|
|
268
281
|
}
|
|
269
282
|
const source = resolvePackageSource(presetName);
|
|
283
|
+
if (remove) {
|
|
284
|
+
if (!configuredSources.has(source)) {
|
|
285
|
+
console.log(`${source} is not installed`);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
process.exitCode = await removePiPackage(piOptions, source);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
270
291
|
if (configuredSources.has(source)) {
|
|
271
292
|
console.log(`${source} already installed`);
|
|
272
293
|
return;
|
|
@@ -436,7 +457,7 @@ async function runMain(input) {
|
|
|
436
457
|
doctor: { type: "boolean" },
|
|
437
458
|
export: { type: "string" },
|
|
438
459
|
fork: { type: "string" },
|
|
439
|
-
help: { type: "boolean" },
|
|
460
|
+
help: { type: "boolean", short: "h" },
|
|
440
461
|
version: { type: "boolean" },
|
|
441
462
|
"alpha-login": { type: "boolean" },
|
|
442
463
|
"alpha-logout": { type: "boolean" },
|
|
@@ -614,7 +635,7 @@ async function runMain(input) {
|
|
|
614
635
|
const modelRuntime = await createModelRuntime(feynmanAuthPath);
|
|
615
636
|
const canonicalModelSpec = canonicalizeModelSpec(requestedExplicitModelSpec, modelRuntime);
|
|
616
637
|
if (!canonicalModelSpec) {
|
|
617
|
-
throw new Error(`Unknown model: ${requestedExplicitModelSpec}
|
|
638
|
+
throw new Error(`Unknown model: ${requestedExplicitModelSpec}. Run \`feynman model list\` to see available models.`);
|
|
618
639
|
}
|
|
619
640
|
explicitModelSpec = canonicalModelSpec;
|
|
620
641
|
}
|
package/dist/model/commands.js
CHANGED
|
@@ -5,7 +5,7 @@ import { readJson } from "../pi/settings.js";
|
|
|
5
5
|
import { promptChoice, promptSelect, promptText } from "../setup/prompts.js";
|
|
6
6
|
import { openUrl } from "../system/open-url.js";
|
|
7
7
|
import { printInfo, printSection, printSuccess, printWarning } from "../ui/terminal.js";
|
|
8
|
-
import { buildModelStatusSnapshotFromRecords, chooseRecommendedModel, getAuthenticatedModelRecords, getAvailableModelRecords, getSupportedModelRecords, } from "./catalog.js";
|
|
8
|
+
import { buildModelStatusSnapshotFromRecords, chooseRecommendedModel, getAuthenticatedModelRecords, getAvailableModelRecords, getSupportedModelRecords, isProClassModelSpec, } from "./catalog.js";
|
|
9
9
|
import { MODEL_API_KEY_PROVIDERS } from "./api-key-providers.js";
|
|
10
10
|
import { createModelRegistry, createModelRuntime, getModelsJsonPath } from "./registry.js";
|
|
11
11
|
import { upsertProviderBaseUrl, upsertProviderConfig } from "./models-json.js";
|
|
@@ -821,10 +821,32 @@ export async function logoutModelProvider(authPath, providerId) {
|
|
|
821
821
|
await modelRuntime.logout(provider.id);
|
|
822
822
|
printSuccess(`Model provider logout complete: ${provider.id}`);
|
|
823
823
|
}
|
|
824
|
+
// Explains why `feynman model set <spec>` found no usable model.
|
|
825
|
+
async function describeUnavailableModelSpec(authPath, spec) {
|
|
826
|
+
const input = spec.trim().replace(/^([^/:]+):(.+)$/, "$1/$2").toLowerCase();
|
|
827
|
+
const known = (await getSupportedModelRecords(authPath)).find((model) => `${model.provider}/${model.id}`.toLowerCase() === input || model.id.toLowerCase() === input);
|
|
828
|
+
if (known) {
|
|
829
|
+
const knownSpec = `${known.provider}/${known.id}`;
|
|
830
|
+
if (isProClassModelSpec(knownSpec)) {
|
|
831
|
+
return `Pro-class model disabled: ${knownSpec}. Choose an approved research model.`;
|
|
832
|
+
}
|
|
833
|
+
if (isLocalModelProvider(authPath, known.provider)) {
|
|
834
|
+
return `${knownSpec} has no API key. Pi only lists a local provider when it has one: add "apiKey": "local" to the ${known.provider} provider in ${getModelsJsonPath(authPath)}.`;
|
|
835
|
+
}
|
|
836
|
+
return `${knownSpec} has no credentials. Run \`feynman model login ${known.provider}\` first.`;
|
|
837
|
+
}
|
|
838
|
+
const modelId = input.slice(input.lastIndexOf("/") + 1);
|
|
839
|
+
const matches = (await getAvailableModelRecords(authPath))
|
|
840
|
+
.map((model) => `${model.provider}/${model.id}`)
|
|
841
|
+
.filter((candidate) => candidate.toLowerCase().includes(modelId))
|
|
842
|
+
.slice(0, 5);
|
|
843
|
+
const suggestion = matches.length > 0 ? ` Did you mean ${matches.join(", ")}?` : "";
|
|
844
|
+
return `Unknown model: ${spec}.${suggestion} Run \`feynman model list\` to see available models.`;
|
|
845
|
+
}
|
|
824
846
|
export async function setDefaultModelSpec(settingsPath, authPath, spec) {
|
|
825
847
|
const resolvedSpec = await resolveAvailableModelSpec(authPath, spec);
|
|
826
848
|
if (!resolvedSpec) {
|
|
827
|
-
throw new Error(
|
|
849
|
+
throw new Error(await describeUnavailableModelSpec(authPath, spec));
|
|
828
850
|
}
|
|
829
851
|
const [provider, ...rest] = resolvedSpec.split("/");
|
|
830
852
|
const modelId = rest.join("/");
|
|
@@ -834,6 +856,22 @@ export async function setDefaultModelSpec(settingsPath, authPath, spec) {
|
|
|
834
856
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
835
857
|
printSuccess(`Research default model set to ${resolvedSpec}`);
|
|
836
858
|
}
|
|
859
|
+
export async function selectDefaultModel(settingsPath, authPath) {
|
|
860
|
+
const status = await collectModelStatus(settingsPath, authPath);
|
|
861
|
+
if (status.availableModels.length === 0) {
|
|
862
|
+
printWarning("No authenticated Pi models are currently available.");
|
|
863
|
+
for (const line of status.guidance) {
|
|
864
|
+
printInfo(line);
|
|
865
|
+
}
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
const spec = await promptSelect("Choose the default research model:", status.availableModels.map((value) => ({
|
|
869
|
+
value,
|
|
870
|
+
label: value,
|
|
871
|
+
hint: value === status.current ? "current" : value === status.recommended ? "recommended" : undefined,
|
|
872
|
+
})), status.current ?? status.recommended);
|
|
873
|
+
await setDefaultModelSpec(settingsPath, authPath, spec);
|
|
874
|
+
}
|
|
837
875
|
export async function runModelSetup(settingsPath, authPath) {
|
|
838
876
|
let status = await collectModelStatus(settingsPath, authPath);
|
|
839
877
|
while (status.availableModels.length === 0) {
|
package/dist/pi/packages.js
CHANGED
|
@@ -27,6 +27,9 @@ export function resolvePackageSource(name) {
|
|
|
27
27
|
export async function installPiPackage(options, source) {
|
|
28
28
|
return runPi(options, ["install", source], buildPiEnv(options));
|
|
29
29
|
}
|
|
30
|
+
export async function removePiPackage(options, source) {
|
|
31
|
+
return runPi(options, ["remove", source], buildPiEnv(options));
|
|
32
|
+
}
|
|
30
33
|
export async function updatePiPackages(options, source) {
|
|
31
34
|
return runPi(options, source ? ["update", source] : ["update", "--extensions"], buildPiEnv(options));
|
|
32
35
|
}
|
package/dist/pi/settings.js
CHANGED
|
@@ -95,7 +95,13 @@ function prepareSubagentDefaults(settingsPath) {
|
|
|
95
95
|
throw new Error(`Invalid subagent config at ${path}: ${key} must be a boolean. The file was not changed.`);
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
|
-
if (
|
|
98
|
+
if (config.singleRunOutputBaseDir !== undefined && typeof config.singleRunOutputBaseDir !== "string") {
|
|
99
|
+
throw new Error(`Invalid subagent config at ${path}: singleRunOutputBaseDir must be a string. The file was not changed.`);
|
|
100
|
+
}
|
|
101
|
+
if (missions.enabled !== undefined &&
|
|
102
|
+
config.fleetView !== undefined &&
|
|
103
|
+
config.asyncByDefault !== undefined &&
|
|
104
|
+
config.singleRunOutputBaseDir !== undefined) {
|
|
99
105
|
return undefined;
|
|
100
106
|
}
|
|
101
107
|
const next = {
|
|
@@ -103,6 +109,10 @@ function prepareSubagentDefaults(settingsPath) {
|
|
|
103
109
|
missions: { ...missions, enabled: missions.enabled ?? false },
|
|
104
110
|
fleetView: config.fleetView ?? false,
|
|
105
111
|
asyncByDefault: config.asyncByDefault ?? true,
|
|
112
|
+
// pi-subagents otherwise saves a relative `output` such as
|
|
113
|
+
// outputs/.drafts/x.md under the session's subagent-artifacts folder, where
|
|
114
|
+
// the lead agent never looks. "." resolves against Pi's cwd, the workspace.
|
|
115
|
+
singleRunOutputBaseDir: config.singleRunOutputBaseDir ?? ".",
|
|
106
116
|
};
|
|
107
117
|
return { path, original: existing?.source, content: `${JSON.stringify(next, null, 2)}\n` };
|
|
108
118
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Runs requests one after another, each starting at least `minGapMs` after the
|
|
2
|
+
// previous one started. Pi runs parallel tool calls in one process, so this keeps
|
|
3
|
+
// them under a service's per-second limit.
|
|
4
|
+
export function createRequestPacer(): <T>(minGapMs: number, run: () => Promise<T>) => Promise<T> {
|
|
5
|
+
let queue: Promise<unknown> = Promise.resolve();
|
|
6
|
+
let lastStartedAt = Number.NEGATIVE_INFINITY;
|
|
7
|
+
return <T>(minGapMs: number, run: () => Promise<T>): Promise<T> => {
|
|
8
|
+
const next = queue.then(async () => {
|
|
9
|
+
const wait = lastStartedAt + minGapMs - Date.now();
|
|
10
|
+
if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait));
|
|
11
|
+
lastStartedAt = Date.now();
|
|
12
|
+
return run();
|
|
13
|
+
});
|
|
14
|
+
queue = next.catch(() => {});
|
|
15
|
+
return next;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { OPENALEX_API_KEY_HINT, openAlexRequestFailure, searchExactOpenAlex } from "./science-database-openalex-exact.js";
|
|
2
|
+
import { createRequestPacer } from "./request-pacer.js";
|
|
2
3
|
|
|
3
4
|
export type OpenAlexScienceDatabaseSource = "openalex";
|
|
4
5
|
|
|
@@ -96,9 +97,18 @@ function scrubOpenAlexText(text: string, url: URL): string {
|
|
|
96
97
|
return scrubbed;
|
|
97
98
|
}
|
|
98
99
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
type OpenAlexResponse =
|
|
101
|
+
| { ok: true; payload: unknown }
|
|
102
|
+
| { ok: false; status: number; statusText: string; retryAfterMs?: number; body: string };
|
|
103
|
+
|
|
104
|
+
// Semantic search allows one request per second (429 with Retry-After: 1), so
|
|
105
|
+
// semantic requests are paced. A 429 that asks for a short wait, such as one
|
|
106
|
+
// caused by a subagent in another process, is retried once.
|
|
107
|
+
const semanticPacer = createRequestPacer();
|
|
108
|
+
const SEMANTIC_MIN_GAP_MS = 1100;
|
|
109
|
+
const MAX_RETRY_WAIT_MS = 5000;
|
|
110
|
+
|
|
111
|
+
async function requestOnce(url: URL): Promise<OpenAlexResponse> {
|
|
102
112
|
const controller = new AbortController();
|
|
103
113
|
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
104
114
|
try {
|
|
@@ -109,16 +119,39 @@ async function fetchJson(url: URL): Promise<{ credentialStatus: string; endpoint
|
|
|
109
119
|
},
|
|
110
120
|
signal: controller.signal,
|
|
111
121
|
});
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
122
|
+
if (response.ok) return { ok: true, payload: await response.json() };
|
|
123
|
+
const retryAfter = response.headers.get("retry-after");
|
|
124
|
+
const retryAfterSeconds = retryAfter === null ? Number.NaN : Number(retryAfter);
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
status: response.status,
|
|
128
|
+
statusText: response.statusText,
|
|
129
|
+
retryAfterMs: Number.isFinite(retryAfterSeconds) && retryAfterSeconds >= 0 ? retryAfterSeconds * 1000 : undefined,
|
|
130
|
+
body: (await response.text()).slice(0, 4096),
|
|
131
|
+
};
|
|
117
132
|
} finally {
|
|
118
133
|
clearTimeout(timeout);
|
|
119
134
|
}
|
|
120
135
|
}
|
|
121
136
|
|
|
137
|
+
async function fetchJson(url: URL): Promise<{ credentialStatus: string; endpoint: string; payload: unknown; usingApiKey: boolean }> {
|
|
138
|
+
const auth = addAuth(url);
|
|
139
|
+
const endpoint = scrubOpenAlexEndpoint(url);
|
|
140
|
+
const request = () =>
|
|
141
|
+
url.searchParams.has("search.semantic") ? semanticPacer(SEMANTIC_MIN_GAP_MS, () => requestOnce(url)) : requestOnce(url);
|
|
142
|
+
let response = await request();
|
|
143
|
+
if (!response.ok && response.status === 429 && response.retryAfterMs !== undefined && response.retryAfterMs <= MAX_RETRY_WAIT_MS) {
|
|
144
|
+
const waitMs = response.retryAfterMs + Math.random() * 500;
|
|
145
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
146
|
+
response = await request();
|
|
147
|
+
}
|
|
148
|
+
if (!response.ok) {
|
|
149
|
+
const snippet = scrubOpenAlexText(response.body, url).slice(0, 240);
|
|
150
|
+
throw openAlexRequestFailure(response.status, response.statusText, snippet, auth.usingApiKey);
|
|
151
|
+
}
|
|
152
|
+
return { ...auth, endpoint, payload: response.payload };
|
|
153
|
+
}
|
|
154
|
+
|
|
122
155
|
function shortOpenAlexId(value: unknown): string | undefined {
|
|
123
156
|
const raw = stringValue(value);
|
|
124
157
|
if (!raw) return undefined;
|
|
@@ -5,6 +5,7 @@ import { searchOpenAlex } from "./science-database-openalex.js";
|
|
|
5
5
|
import { searchPreprints } from "./science-database-preprints.js";
|
|
6
6
|
import { searchPubMed } from "./science-database-pubmed.js";
|
|
7
7
|
import { isEuropePmcFullTextQuery, searchEuropePmcFullText } from "./science-database-europepmc-fulltext.js";
|
|
8
|
+
import { createRequestPacer } from "./request-pacer.js";
|
|
8
9
|
|
|
9
10
|
type ScienceDatabaseSource = "arxiv" | "biorxiv" | "crossref" | "europepmc" | "medrxiv" | "openalex" | "pubmed" | "semanticscholar";
|
|
10
11
|
|
|
@@ -141,21 +142,12 @@ function crossrefAuthors(value: unknown): string[] {
|
|
|
141
142
|
// (x-concurrency-limit: 1); the polite pool, used when a mailto is sent, allows
|
|
142
143
|
// three per second. Parallel DOI checks got 429 on most calls, so run Crossref
|
|
143
144
|
// requests one after another at the pool's pace.
|
|
144
|
-
|
|
145
|
-
let crossrefLastStartedAt = Number.NEGATIVE_INFINITY;
|
|
145
|
+
const crossrefPacer = createRequestPacer();
|
|
146
146
|
|
|
147
147
|
export function withCrossrefPacing<T>(polite: boolean, run: () => Promise<T>): Promise<T> {
|
|
148
148
|
// A little over the pool's limit: network jitter can land two requests paced
|
|
149
149
|
// exactly one second apart inside the same window.
|
|
150
|
-
|
|
151
|
-
const next = crossrefQueue.then(async () => {
|
|
152
|
-
const wait = crossrefLastStartedAt + minGapMs - Date.now();
|
|
153
|
-
if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait));
|
|
154
|
-
crossrefLastStartedAt = Date.now();
|
|
155
|
-
return run();
|
|
156
|
-
});
|
|
157
|
-
crossrefQueue = next.catch(() => {});
|
|
158
|
-
return next;
|
|
150
|
+
return crossrefPacer(polite ? 400 : 1200, run);
|
|
159
151
|
}
|
|
160
152
|
|
|
161
153
|
async function searchCrossref(params: ScienceDatabaseSearchParams): Promise<Record<string, unknown>> {
|
package/metadata/commands.mjs
CHANGED
|
@@ -115,7 +115,7 @@ export const cliCommandSections = [
|
|
|
115
115
|
{ usage: "feynman model list", description: "List available models in Pi auth storage." },
|
|
116
116
|
{ usage: "feynman model login [id]", description: "Authenticate a model provider with OAuth or API-key setup." },
|
|
117
117
|
{ usage: "feynman model logout [id]", description: "Clear stored auth for a model provider." },
|
|
118
|
-
{ usage: "feynman model set
|
|
118
|
+
{ usage: "feynman model set [provider/model]", description: "Set the default approved research model (also accepts provider:model); without one, pick from a list." },
|
|
119
119
|
{ usage: "feynman model tier [value]", description: "View or set the request service tier override." },
|
|
120
120
|
],
|
|
121
121
|
},
|
|
@@ -137,6 +137,7 @@ export const cliCommandSections = [
|
|
|
137
137
|
commands: [
|
|
138
138
|
{ usage: "feynman packages list", description: "Show core and optional Pi package presets." },
|
|
139
139
|
{ usage: "feynman packages install <preset>", description: "Install optional package presets on demand." },
|
|
140
|
+
{ usage: "feynman packages remove <preset>", description: "Remove an installed optional package preset." },
|
|
140
141
|
{ usage: "feynman search status", description: "Show Pi web-access status and config path." },
|
|
141
142
|
{ usage: "feynman search set <provider> [api-key]", description: "Set the web search provider and optionally save its API key." },
|
|
142
143
|
{ usage: "feynman search clear", description: "Reset web search provider to auto while preserving API keys." },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@companion-ai/feynman",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.16",
|
|
4
4
|
"description": "Research-first CLI agent built on Pi and alphaXiv",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -79,18 +79,17 @@
|
|
|
79
79
|
"dependencies": {
|
|
80
80
|
"@clack/prompts": "^1.8.1",
|
|
81
81
|
"@companion-ai/alpha-hub": "0.1.6",
|
|
82
|
-
"@earendil-works/pi-ai": "*",
|
|
83
|
-
"@earendil-works/pi-coding-agent": "*",
|
|
84
82
|
"fast-xml-parser": "5.11.1",
|
|
85
83
|
"pi-btw": "0.6.1",
|
|
86
84
|
"pi-docparser": "4.0.0",
|
|
87
85
|
"pi-subagents": "0.73.1",
|
|
88
|
-
"pi-web-access": "0.
|
|
89
|
-
"posthog-node": "^5.53.0"
|
|
86
|
+
"pi-web-access": "0.34.0",
|
|
87
|
+
"posthog-node": "^5.53.0",
|
|
88
|
+
"undici": "^8.11.2"
|
|
90
89
|
},
|
|
91
90
|
"peerDependencies": {
|
|
92
|
-
"@earendil-works/pi-ai": "
|
|
93
|
-
"@earendil-works/pi-coding-agent": "
|
|
91
|
+
"@earendil-works/pi-ai": "0.99.1",
|
|
92
|
+
"@earendil-works/pi-coding-agent": "0.99.1",
|
|
94
93
|
"@earendil-works/pi-tui": "*",
|
|
95
94
|
"typebox": "*"
|
|
96
95
|
},
|
|
@@ -117,6 +116,6 @@
|
|
|
117
116
|
"access": "public"
|
|
118
117
|
},
|
|
119
118
|
"overrides": {
|
|
120
|
-
"undici": "
|
|
119
|
+
"undici": "$undici"
|
|
121
120
|
}
|
|
122
121
|
}
|
package/prompts/deepresearch.md
CHANGED
|
@@ -76,13 +76,13 @@ Example shape:
|
|
|
76
76
|
|
|
77
77
|
```json
|
|
78
78
|
{
|
|
79
|
-
"workflowScript": "return await runs.all([{key:'web',agent:'researcher',task:'Read outputs/.plans/<slug>-T1.md and write
|
|
79
|
+
"workflowScript": "return await runs.all([{key:'web',agent:'researcher',task:'Read outputs/.plans/<slug>-T1.md and write outputs/.drafts/<slug>-research-web.md.',output:'outputs/.drafts/<slug>-research-web.md'},{key:'papers',agent:'researcher',task:'Read outputs/.plans/<slug>-T2.md and write outputs/.drafts/<slug>-research-papers.md.',output:'outputs/.drafts/<slug>-research-papers.md'}]);",
|
|
80
80
|
"async": true,
|
|
81
81
|
"globalConcurrencyLimit": 4
|
|
82
82
|
}
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
-
Continue independent work after launch, then consume completion results before synthesis.
|
|
85
|
+
Continue independent work after launch, then consume completion results before synthesis. Relative output paths resolve against the workspace, so each child writes its declared path; verify each file on disk, and use the returned output reference for any that is missing. After evidence gathering, update the plan ledger and verification log. If research failed, record exactly what failed and proceed with a blocked or partial draft.
|
|
86
86
|
|
|
87
87
|
## Step 4: Draft
|
|
88
88
|
|
|
@@ -123,7 +123,7 @@ Use this shape:
|
|
|
123
123
|
}
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
-
Wait for the verifier's completion result before review, not merely the async launch receipt. Verify on disk that `outputs/.drafts/<slug>-cited.md` exists. If
|
|
126
|
+
Wait for the verifier's completion result before review, not merely the async launch receipt. Verify on disk that `outputs/.drafts/<slug>-cited.md` exists. If it is missing, use the returned output reference to find the cited file and copy it there.
|
|
127
127
|
|
|
128
128
|
## Step 6: Review
|
|
129
129
|
|
|
@@ -142,7 +142,7 @@ Use this shape:
|
|
|
142
142
|
"agent": "reviewer",
|
|
143
143
|
"async": true,
|
|
144
144
|
"task": "Verify outputs/.drafts/<slug>-cited.md. Flag unsupported claims, logical gaps, single-source critical claims, and overstated confidence. This is a verification pass, not a peer review.",
|
|
145
|
-
"output": "
|
|
145
|
+
"output": "outputs/.drafts/<slug>-verification.md"
|
|
146
146
|
}
|
|
147
147
|
```
|
|
148
148
|
|
package/prompts/lit.md
CHANGED
|
@@ -12,7 +12,7 @@ Derive a short slug from the topic (lowercase, hyphens, no filler words, ≤5 wo
|
|
|
12
12
|
|
|
13
13
|
1. **Plan** — Outline the scope: key questions, source types to search (papers, web, repos), time period, expected sections, and a small task ledger plus verification log. When the input appears to name a lab, PI, author, institution lab page, or author profile, run the review as a publication-corpus review: find the lab/author identity first, collect the reachable publication list, then map the research trajectory across that corpus. Write the plan to `outputs/.plans/<slug>.md`. Briefly summarize the plan to the user and continue immediately in the same turn with the next tool call; never end a turn with a message that only announces what you will do next. Do not ask for confirmation or wait for a proceed response unless the user explicitly requested plan review.
|
|
14
14
|
- When updating the plan ledger later, keep edits small and valid. If an `edit` tool call fails with a JSON parse error or the replacement would require embedding a large markdown block, rewrite the full corrected plan file with the file-writing tool instead, then continue to final artifact/provenance verification.
|
|
15
|
-
2. **Gather** — Use the `researcher` subagent when the sweep is wide enough to benefit from delegated paper triage before synthesis. For narrow topics, search directly. Researcher outputs go to
|
|
15
|
+
2. **Gather** — Use the `researcher` subagent when the sweep is wide enough to benefit from delegated paper triage before synthesis. For narrow topics, search directly. Researcher outputs go to `outputs/.drafts/<slug>-research-*.md`; pass that path as each researcher's `output`. For publication-corpus reviews, the lead agent owns identity resolution and writes `notes/<slug>-publications.md` with reachable titles, years, venues, URLs/DOIs, and gaps before delegating trajectory synthesis. Prefer lab publication pages, author profiles, arXiv/OpenReview/Semantic Scholar pages, and paper search results that expose stable source URLs. Do not silently skip assigned questions; mark them `done`, `blocked`, or `superseded`.
|
|
16
16
|
3. **Synthesize** — Separate consensus, disagreements, and open questions. For publication-corpus reviews, also identify 3-5 research trajectories and the 3-5 papers that most changed the corpus direction; rank them by contrastive originality, methodology strength, and relationship to prior art rather than by author prestige alone. When useful, propose concrete next experiments or follow-up reading. Use Mermaid diagrams for taxonomies, method pipelines, or lab trajectory maps when the structure is source-supported and changes the reader's research decision. Keep the output to research evidence, source coverage, and next research decisions; do not create non-research operational artifacts from a literature review run.
|
|
17
17
|
4. **Cite** — Spawn the `verifier` agent to add inline citations and verify every source URL in the draft.
|
|
18
18
|
5. **Verify** — Spawn the `reviewer` agent to check the cited draft for unsupported claims, logical gaps, zombie sections, and single-source critical findings. Fix FATAL issues before delivering. Note MAJOR issues in Open Questions. If FATAL issues were found, run one more verification pass after the fixes.
|
package/prompts/summarize.md
CHANGED
|
@@ -126,7 +126,7 @@ Use one async workflow with a stable key for each chunk and a concurrency limit
|
|
|
126
126
|
}
|
|
127
127
|
```
|
|
128
128
|
|
|
129
|
-
Consume completion results before aggregation. `runs.all` returns an ordered array including ordinary child failures; inspect each `ok` and error. A validation or infrastructure failure can still fail the workflow.
|
|
129
|
+
Consume completion results before aggregation. `runs.all` returns an ordered array including ordinary child failures; inspect each `ok` and error. A validation or infrastructure failure can still fail the workflow. Relative output paths resolve against the workspace; verify each chunk summary on disk, and use the returned output reference for any that is missing. Record missing or failed chunks rather than treating an async launch receipt as a finished summary.
|
|
130
130
|
|
|
131
131
|
### 3d. Aggregate
|
|
132
132
|
|