@tea-agent/loop-agent 0.39.0-beta.2 → 0.39.0-beta.3
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/AGENTS.md +4 -1
- package/CHANGELOG.md +187 -92
- package/README.md +9 -5
- package/bin/loop-agent.js +7 -3
- package/dist/application/task-lifecycle/recommendations.js +13 -2
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +16 -5
- package/dist/cli/program.js +16 -2
- package/dist/cli/update/notifier.js +2 -2
- package/dist/cli.js +2 -1
- package/dist/commands/dag-rerun.js +55 -1
- package/dist/commands/init-model-catalog.js +464 -0
- package/dist/commands/init-upgrade.js +265 -97
- package/dist/commands/init.js +429 -28
- package/dist/commands/inspect-next.js +8 -0
- package/dist/executors/dag-pi-executor.js +1645 -29
- package/dist/executors/pi-extension-resolver.js +233 -0
- package/dist/executors/pi-playwright-cli-tool.js +51 -12
- package/dist/executors/pi-sdk-executor.js +180 -33
- package/dist/executors/shell-executor.js +1150 -171
- package/dist/executors/shell-write-guard.js +14 -0
- package/dist/shared/dag-prompt-override.js +28 -0
- package/dist/shared/openspec-spec.js +70 -4
- package/dist/shared/operator/capabilities.js +32 -0
- package/dist/shared/playwright-cli-command-policy.js +15 -0
- package/dist/shared/update/console-notifier.js +100 -0
- package/dist/{cli → shared}/update/npm-client.js +41 -8
- package/dist/{cli → shared}/update/state.js +41 -13
- package/dist/task/config-types.js +19 -5
- package/dist/task/contract/project.js +3 -0
- package/dist/task/contract/schema.js +2 -1
- package/dist/task/frontend-project-capability.js +42 -16
- package/dist/task/source-prepare/parse-intent.js +24 -4
- package/dist/task/source-prepare/prepare.js +36 -0
- package/dist/worker/cli.js +31 -1
- package/dist/worker/console/chat/pi-console-config.js +81 -10
- package/dist/worker/console/chat/routes.js +1 -0
- package/dist/worker/console/dag-execution-receipt.js +33 -0
- package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
- package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
- package/dist/worker/console/index.js +3 -0
- package/dist/worker/console/operation-store.js +226 -6
- package/dist/worker/console/operator-actions.js +263 -0
- package/dist/worker/console/operator-user-error.js +2 -2
- package/dist/worker/console/routes.js +43 -2
- package/dist/worker/console/security.js +63 -9
- package/dist/worker/console/server.js +151 -6
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-zsNyvGaH.js} +1 -1
- package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-BDjZ5kE1.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-BkgaujnN.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-CqxOJi0j.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-C2JHLdRi.js} +1 -1
- package/dist/worker/console/static/assets/channel-BuqaGKAT.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-DF_YSvOS.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-D3Ort5Vo.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-XfAfZeUC.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-SLBibhf5.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-Dz29Ye_f.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-DB2G4H8V.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-CIoGkxdu.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-CG78FGkn.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-ZDHAvXPd.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-BKkkC4VA.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DNdSHjH9.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DNdSHjH9.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-SposUZsO.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-BSCtQpD7.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-DgFHrw9l.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-BVBasYbj.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-6r_4rN_c.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-JmuaB41P.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-xXvAY3sk.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-D2B3lY2o.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-tK54tMEX.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-CNtELzV-.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-C1xKMKcW.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-CZ7dlPMz.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-BqGb42Xe.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-D_BlXEtt.js} +1 -1
- package/dist/worker/console/static/assets/{index-DTZOKgAn.js → index-D5zGX6fG.js} +68 -68
- package/dist/worker/console/static/assets/index-DBbhESQ_.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-Vce2vmIe.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-DDwVBAvI.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-y_Ew-wv4.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-CjXJFp1K.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-1itMst1Q.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-C-D3ZRUa.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-BiYywmZc.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-l2fydNWA.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-Dz0hNbYW.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-BujI_mLZ.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-pWe84beb.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-ByYu2frg.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-CoPnilFP.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-1Md2LnS0.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-DqqqAoSH.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-C4sGkf--.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8rmop_9.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-ByxJrUzW.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B0ky-kBM.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-D8MLgM0O.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-DnBis-wZ.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-uuyYT7UO.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-BjgabhwK.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/active-run-badge.js +17 -0
- package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
- package/dist/worker/loop-agent/loop-agent-client.js +7 -3
- package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
- package/dist/worker/observe/node-input.js +74 -3
- package/dist/worker/observe/node-process.js +377 -0
- package/dist/worker/observe/routes.js +36 -3
- package/dist/worker/observe/shell-handler-keys.js +34 -0
- package/dist/worker/observe/static/api.js +66 -0
- package/dist/worker/observe/static/constants.js +8 -1
- package/dist/worker/observe/static/dag-history-labels.js +4 -0
- package/dist/worker/observe/static/format.js +23 -0
- package/dist/worker/observe/static/markdown-render.js +20 -1
- package/dist/worker/observe/static/operator-chrome.js +4 -0
- package/dist/worker/observe/static/state.js +13 -2
- package/dist/worker/observe/static/styles.css +73 -0
- package/dist/worker/observe/static/views/dag-inspector.js +612 -8
- package/dist/worker/observe/static/views/dag.js +8 -1
- package/dist/worker/observe/static/views/failures.js +5 -2
- package/dist/worker/observe/static/views/session-timeline.js +78 -9
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +20 -2
- package/dist/workflows/dag/contract-output-registry.js +15 -0
- package/dist/workflows/dag/contract-validator-registrations.js +1 -2
- package/dist/workflows/dag/failure-category.js +12 -0
- package/dist/workflows/dag/frontend-closeout.js +221 -0
- package/dist/workflows/dag/frontend-design-policy.js +347 -0
- package/dist/workflows/dag/frontend-human-decision.js +182 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +378 -160
- package/dist/workflows/dag/frontend-prewrite-gate.js +258 -393
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
- package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
- package/dist/workflows/dag/frontend-recovery-controller.js +201 -0
- package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
- package/dist/workflows/dag/frontend-recovery-run.js +33 -20
- package/dist/workflows/dag/frontend-repair.js +7 -424
- package/dist/workflows/dag/frontend-review-context.js +195 -8
- package/dist/workflows/dag/frontend-risk.js +15 -2
- package/dist/workflows/dag/frontend-shadow-dual-write.js +665 -0
- package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
- package/dist/workflows/dag/frontend-shape-facts.js +409 -0
- package/dist/workflows/dag/frontend-shape.js +425 -0
- package/dist/workflows/dag/frontend-split-application-service.js +203 -0
- package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
- package/dist/workflows/dag/frontend-test-case-checklist.js +26 -2
- package/dist/workflows/dag/frontend-test-case-manifest.js +5 -0
- package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
- package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
- package/dist/workflows/dag/frontend-test-markdown.js +61 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +160 -28
- package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +438 -0
- package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
- package/dist/workflows/dag/frontend-verification-trace.js +52 -17
- package/dist/workflows/dag/frontend-worktree-diff.js +97 -18
- package/dist/workflows/dag/frontend-writer-admission.js +260 -0
- package/dist/workflows/dag/frontend-writer-status.js +244 -0
- package/dist/workflows/dag/init-hybrid.js +572 -500
- package/dist/workflows/dag/node-execution.js +307 -70
- package/dist/workflows/dag/prompt.js +10 -18
- package/dist/workflows/dag/rerun-plan.js +144 -5
- package/dist/workflows/dag/rerun-run.js +58 -6
- package/dist/workflows/dag/retry-policy.js +228 -4
- package/dist/workflows/dag/runner.js +314 -119
- package/dist/workflows/dag/scheduler.js +114 -16
- package/dist/workflows/dag/structured-output-repair.js +712 -0
- package/dist/workflows/dag/types.js +235 -23
- package/dist/workflows/dag/upstream-artifacts.js +0 -4
- package/dist/workflows/dag/validate.js +60 -10
- package/docs/init-surface.manifest.json +5 -0
- package/docs/operations/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +44 -0
- package/docs/templates/frontend-test-dag.json +8 -10
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
- package/docs/templates/init-managed-agents.md +13 -3
- package/harness.json +3 -3
- package/package.json +5 -1
- package/scripts/next-info.mjs +356 -0
- package/scripts/next-publish-gate.mjs +238 -0
- package/scripts/release-source-binding.mjs +251 -0
- package/skills/codebase-scout/SKILL.md +1 -1
- package/skills/fe-test-ui-scout/SKILL.md +65 -0
- package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
- package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
- package/skills/frontend-implementation/SKILL.md +4 -4
- package/skills/frontend-implementation/references/design-spec.md +11 -7
- package/skills/frontend-implementation/references/node-contracts.md +20 -11
- package/skills/loop-agent/references/command-reference.md +11 -4
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
- package/skills/playwright-cli/SKILL.md +22 -0
- package/skills/playwright-cli-case-generator/SKILL.md +20 -1
- package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
- package/dist/worker/console/static/assets/index-Ya5FE7cD.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* Pi extension allowlist for backend implementation DAGs (plan D2/D3).
|
|
6
|
+
* DAG specs carry these frozen short ids; the executor resolves them against
|
|
7
|
+
* the Pi agent settings package inventory at run time.
|
|
8
|
+
*/
|
|
9
|
+
export const DAG_PI_EXTENSION_IDS = ["pi-codegraph", "pi-lens"];
|
|
10
|
+
/**
|
|
11
|
+
* Alias table: one DAG short id may resolve to multiple published package
|
|
12
|
+
* names. The first settings spec whose de-`npm:`-prefixed package name is in
|
|
13
|
+
* the alias set wins (project settings override user settings).
|
|
14
|
+
*/
|
|
15
|
+
export const DAG_PI_EXTENSION_PACKAGE_ALIASES = {
|
|
16
|
+
"pi-codegraph": [
|
|
17
|
+
"@estebanforge/pi-codegraph-enhanced",
|
|
18
|
+
"@vndv/pi-codegraph",
|
|
19
|
+
"pi-codegraph",
|
|
20
|
+
],
|
|
21
|
+
"pi-lens": ["pi-lens"],
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Extension tools that must never be activated as extras on any node (D5):
|
|
25
|
+
* they mutate files without the writer path policy. Writer nodes keep only
|
|
26
|
+
* policy-wrapped edit/write; read nodes keep only navigation tools.
|
|
27
|
+
*/
|
|
28
|
+
export const DAG_PI_EXTENSION_WRITE_TOOL_DENY = new Set([
|
|
29
|
+
"ast_grep_replace",
|
|
30
|
+
]);
|
|
31
|
+
/** Builtin tool names extensions must never replace (D5). */
|
|
32
|
+
export const DAG_PI_EXTENSION_PROTECTED_BUILTIN_TOOLS = new Set(["read", "edit", "write", "bash", "grep", "find", "ls"]);
|
|
33
|
+
/** Deny surface extensions must never activate (D5). */
|
|
34
|
+
export const DAG_PI_EXTENSION_DENIED_TOOL_IDS = new Set([
|
|
35
|
+
"apply_patch",
|
|
36
|
+
"apply-patch",
|
|
37
|
+
"full-tools",
|
|
38
|
+
"full_tools",
|
|
39
|
+
"coding-chat",
|
|
40
|
+
"coding_chat",
|
|
41
|
+
"shell",
|
|
42
|
+
]);
|
|
43
|
+
export function isDagPiExtensionId(value) {
|
|
44
|
+
return DAG_PI_EXTENSION_IDS.includes(value);
|
|
45
|
+
}
|
|
46
|
+
function getPiAgentDir() {
|
|
47
|
+
const envDir = process.env.PI_CODING_AGENT_DIR;
|
|
48
|
+
if (envDir)
|
|
49
|
+
return envDir;
|
|
50
|
+
return path.join(os.homedir(), ".pi", "agent");
|
|
51
|
+
}
|
|
52
|
+
function readSettingsPackages(settingsPath) {
|
|
53
|
+
try {
|
|
54
|
+
if (!existsSync(settingsPath))
|
|
55
|
+
return undefined;
|
|
56
|
+
const parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
57
|
+
if (!Array.isArray(parsed.packages))
|
|
58
|
+
return undefined;
|
|
59
|
+
return parsed.packages.filter((spec) => typeof spec === "string");
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Strip an `npm:` prefix from a settings package spec, e.g. `npm:pi-lens`. */
|
|
66
|
+
export function packageNameFromSpec(spec) {
|
|
67
|
+
return spec.startsWith("npm:") ? spec.slice(4) : spec;
|
|
68
|
+
}
|
|
69
|
+
function loadPackageInventories(repoRoot) {
|
|
70
|
+
const agentDir = getPiAgentDir();
|
|
71
|
+
const userSpecs = readSettingsPackages(path.join(agentDir, "settings.json"));
|
|
72
|
+
const projectSpecs = readSettingsPackages(path.join(repoRoot, ".pi", "settings.json"));
|
|
73
|
+
const inventories = [];
|
|
74
|
+
if (userSpecs) {
|
|
75
|
+
inventories.push({
|
|
76
|
+
specs: userSpecs,
|
|
77
|
+
nodeModulesRoot: path.join(agentDir, "npm", "node_modules"),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (projectSpecs) {
|
|
81
|
+
inventories.push({
|
|
82
|
+
specs: projectSpecs,
|
|
83
|
+
nodeModulesRoot: path.join(repoRoot, "node_modules"),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return inventories;
|
|
87
|
+
}
|
|
88
|
+
const EXTENSION_FILE_RE = /\.(?:ts|js|mjs|cjs)$/;
|
|
89
|
+
/**
|
|
90
|
+
* Resolve a package's `pi.extensions` entries to absolute entry paths.
|
|
91
|
+
* - A file entry maps to itself.
|
|
92
|
+
* - A directory entry expands to every extension file directly inside it
|
|
93
|
+
* (e.g. `@estebanforge/pi-codegraph-enhanced` ships `./extensions` holding
|
|
94
|
+
* `codegraph.ts`).
|
|
95
|
+
* No `pi.extensions` declaration, or a declaration whose paths do not exist,
|
|
96
|
+
* yields no entries (caller records `no-entry`) — no heuristic scanning
|
|
97
|
+
* outside declared paths.
|
|
98
|
+
*/
|
|
99
|
+
export function resolvePackageEntryPaths(packageDir) {
|
|
100
|
+
let manifest;
|
|
101
|
+
try {
|
|
102
|
+
manifest = JSON.parse(readFileSync(path.join(packageDir, "package.json"), "utf8"));
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return { entryPaths: [], reason: "no-entry" };
|
|
106
|
+
}
|
|
107
|
+
const declared = manifest.pi?.extensions;
|
|
108
|
+
if (!Array.isArray(declared)) {
|
|
109
|
+
return { entryPaths: [], reason: "no-entry" };
|
|
110
|
+
}
|
|
111
|
+
const entryPaths = [];
|
|
112
|
+
for (const entry of declared) {
|
|
113
|
+
if (typeof entry !== "string" || entry.length === 0)
|
|
114
|
+
continue;
|
|
115
|
+
const resolved = path.resolve(packageDir, entry);
|
|
116
|
+
if (!existsSync(resolved))
|
|
117
|
+
continue;
|
|
118
|
+
let isDirectory = false;
|
|
119
|
+
try {
|
|
120
|
+
isDirectory = statSync(resolved).isDirectory();
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (!isDirectory) {
|
|
126
|
+
entryPaths.push(resolved);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
let names;
|
|
130
|
+
try {
|
|
131
|
+
names = readdirSync(resolved);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
for (const name of names.sort()) {
|
|
137
|
+
if (!EXTENSION_FILE_RE.test(name))
|
|
138
|
+
continue;
|
|
139
|
+
entryPaths.push(path.join(resolved, name));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (entryPaths.length === 0) {
|
|
143
|
+
return { entryPaths: [], reason: "no-entry" };
|
|
144
|
+
}
|
|
145
|
+
return { entryPaths };
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Resolve frozen DAG short ids against the Pi settings package inventory
|
|
149
|
+
* (user `~/.pi/agent/settings.json`, then project `.pi/settings.json` — a
|
|
150
|
+
* later match on the same alias wins). Pure synchronous read; never throws.
|
|
151
|
+
*/
|
|
152
|
+
export function resolveDagPiExtensions(input) {
|
|
153
|
+
const inventories = loadPackageInventories(input.repoRoot);
|
|
154
|
+
const resolved = [];
|
|
155
|
+
const missing = [];
|
|
156
|
+
for (const id of input.ids) {
|
|
157
|
+
if (!isDagPiExtensionId(id)) {
|
|
158
|
+
missing.push({
|
|
159
|
+
id,
|
|
160
|
+
reason: "package-unreadable",
|
|
161
|
+
detail: `unknown pi extension id: ${id}`,
|
|
162
|
+
});
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const aliases = DAG_PI_EXTENSION_PACKAGE_ALIASES[id];
|
|
166
|
+
let matched;
|
|
167
|
+
// Later inventories (project) override earlier (user).
|
|
168
|
+
for (const inventory of inventories) {
|
|
169
|
+
for (const spec of inventory.specs) {
|
|
170
|
+
const packageName = packageNameFromSpec(spec);
|
|
171
|
+
if (!aliases.includes(packageName))
|
|
172
|
+
continue;
|
|
173
|
+
const packageDir = path.join(inventory.nodeModulesRoot, ...packageName.split("/"));
|
|
174
|
+
if (!existsSync(packageDir))
|
|
175
|
+
continue;
|
|
176
|
+
try {
|
|
177
|
+
const manifest = JSON.parse(readFileSync(path.join(packageDir, "package.json"), "utf8"));
|
|
178
|
+
if (typeof manifest.version !== "string")
|
|
179
|
+
continue;
|
|
180
|
+
matched = { packageDir, packageName, version: manifest.version };
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
// unreadable manifest: skip this candidate
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (!matched) {
|
|
188
|
+
missing.push({ id, reason: "not-installed" });
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const entries = resolvePackageEntryPaths(matched.packageDir);
|
|
192
|
+
if (entries.entryPaths.length === 0) {
|
|
193
|
+
missing.push({
|
|
194
|
+
id,
|
|
195
|
+
reason: entries.reason === "no-entry" ? "no-entry" : "package-unreadable",
|
|
196
|
+
});
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
resolved.push({
|
|
200
|
+
id,
|
|
201
|
+
packageName: matched.packageName,
|
|
202
|
+
version: matched.version,
|
|
203
|
+
entryPaths: entries.entryPaths,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return { resolved, missing };
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Compute extension tool names that may join a node's active tool allowlist
|
|
210
|
+
* after session creation (D5): new tool names registered by the loaded
|
|
211
|
+
* extensions, minus protected builtins, the deny surface, and write-capable
|
|
212
|
+
* extension tools (both buckets — writer writes stay policy-wrapped only).
|
|
213
|
+
*/
|
|
214
|
+
export function computeExtensionToolAllowlistExtras(sessionTools, baseline) {
|
|
215
|
+
const baselineLower = new Set(baseline.map((name) => name.toLowerCase()));
|
|
216
|
+
const extras = [];
|
|
217
|
+
for (const tool of sessionTools) {
|
|
218
|
+
const name = tool && typeof tool.name === "string" ? tool.name : undefined;
|
|
219
|
+
if (!name)
|
|
220
|
+
continue;
|
|
221
|
+
const lower = name.toLowerCase();
|
|
222
|
+
if (baselineLower.has(lower))
|
|
223
|
+
continue;
|
|
224
|
+
if (DAG_PI_EXTENSION_PROTECTED_BUILTIN_TOOLS.has(lower))
|
|
225
|
+
continue;
|
|
226
|
+
if (DAG_PI_EXTENSION_DENIED_TOOL_IDS.has(lower))
|
|
227
|
+
continue;
|
|
228
|
+
if (DAG_PI_EXTENSION_WRITE_TOOL_DENY.has(lower))
|
|
229
|
+
continue;
|
|
230
|
+
extras.push(name);
|
|
231
|
+
}
|
|
232
|
+
return extras;
|
|
233
|
+
}
|
|
@@ -4,7 +4,7 @@ import { appendFile, lstat, mkdir, readFile, realpath, stat, } from "node:fs/pro
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { processTreeSpawnOptions, terminateProcessTree, } from "./process-tree.js";
|
|
6
6
|
import { resolvePlaywrightCliLauncher } from "./playwright-cli-launcher.js";
|
|
7
|
-
import { PLAYWRIGHT_CLI_ALLOWED_COMMANDS, isPlaywrightCliCommand, } from "../shared/playwright-cli-command-policy.js";
|
|
7
|
+
import { PLAYWRIGHT_CLI_ALLOWED_COMMANDS, isPlaywrightCliCommand, parsePlaywrightCliFindMatchCount, } from "../shared/playwright-cli-command-policy.js";
|
|
8
8
|
export { PLAYWRIGHT_CLI_ALLOWED_COMMANDS, isPlaywrightCliCommand, };
|
|
9
9
|
/** Logical capability name only; execution always uses the verified JS launcher. */
|
|
10
10
|
export const PLAYWRIGHT_CLI_EXECUTABLE = "playwright-cli";
|
|
@@ -111,6 +111,13 @@ export async function readPlaywrightCliReceipts(runDir, nodeId) {
|
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
export function summarizePlaywrightCliReceipts(receipts) {
|
|
114
|
+
// Sequence numbers are assigned at dispatch time, which is the controller's
|
|
115
|
+
// causal order. Receipt lines are appended when each command COMPLETES, so
|
|
116
|
+
// concurrent commands (e.g. parallel find calls issued in one turn) persist
|
|
117
|
+
// out of sequence order. Judge chain contiguity on the dispatch order by
|
|
118
|
+
// sorting on `sequence` first; a genuinely missing receipt (gap) or a
|
|
119
|
+
// duplicated sequence still fails the contiguity check below.
|
|
120
|
+
const orderedReceipts = [...receipts].sort((a, b) => a.sequence - b.sequence);
|
|
114
121
|
let hasSuccessfulOpen = false;
|
|
115
122
|
let hasInteractionOrAssertion = false;
|
|
116
123
|
let hasMeaningfulAssertion = false;
|
|
@@ -122,7 +129,7 @@ export function summarizePlaywrightCliReceipts(receipts) {
|
|
|
122
129
|
let hasContiguousControllerSequence = true;
|
|
123
130
|
let previousSequence = 0;
|
|
124
131
|
const caseIds = new Set();
|
|
125
|
-
for (const receipt of
|
|
132
|
+
for (const receipt of orderedReceipts) {
|
|
126
133
|
caseIds.add(receipt.caseId);
|
|
127
134
|
const succeeded = receipt.exitCode === 0 && !receipt.timedOut;
|
|
128
135
|
const ordered = receipt.sequence === previousSequence + 1;
|
|
@@ -137,8 +144,10 @@ export function summarizePlaywrightCliReceipts(receipts) {
|
|
|
137
144
|
if (PLAYWRIGHT_CLI_INTERACTION_COMMANDS.has(receipt.command) && succeeded) {
|
|
138
145
|
hasInteractionOrAssertion = true;
|
|
139
146
|
}
|
|
140
|
-
// A successful find
|
|
141
|
-
|
|
147
|
+
// A successful find (or find-unique, its semantic wrapper) is the only
|
|
148
|
+
// model-visible semantic assertion in v1.
|
|
149
|
+
if ((receipt.command === "find" || receipt.command === "find-unique") &&
|
|
150
|
+
succeeded) {
|
|
142
151
|
hasMeaningfulAssertion = true;
|
|
143
152
|
if (ordered && sawOpen)
|
|
144
153
|
sawMeaningfulAssertionAfterOpen = true;
|
|
@@ -523,6 +532,17 @@ export function preparePlaywrightCliArgv(input, ctx) {
|
|
|
523
532
|
else if (command === "pdf" || command === "snapshot") {
|
|
524
533
|
args = normalizePdfOrSnapshotArgs(command, rawArgs, ctx);
|
|
525
534
|
}
|
|
535
|
+
else if (command === "find-unique") {
|
|
536
|
+
// find-unique is a controller-side semantic wrapper: the underlying CLI
|
|
537
|
+
// only knows `find`; uniqueness (matchCount === 1) is judged by the
|
|
538
|
+
// executor after parsing the find output. Require exactly one text
|
|
539
|
+
// literal — a unique-control assertion is meaningless with multiple.
|
|
540
|
+
const literals = rawArgs.filter((arg) => !arg.startsWith("-"));
|
|
541
|
+
if (rawArgs.length !== 1 || literals.length !== 1) {
|
|
542
|
+
throw new PlaywrightCliPolicyError("find-unique-args", "find-unique requires exactly one text literal and no flags");
|
|
543
|
+
}
|
|
544
|
+
args = literals;
|
|
545
|
+
}
|
|
526
546
|
else {
|
|
527
547
|
args = rewriteArgsForPaths(command, rawArgs, ctx);
|
|
528
548
|
}
|
|
@@ -818,7 +838,10 @@ export async function executePlaywrightCliCommand(input, ctx, signal) {
|
|
|
818
838
|
};
|
|
819
839
|
}
|
|
820
840
|
let executable = PLAYWRIGHT_CLI_EXECUTABLE;
|
|
821
|
-
|
|
841
|
+
// find-unique executes as the underlying CLI `find` command; uniqueness is
|
|
842
|
+
// a controller-side judgment on the parsed match count.
|
|
843
|
+
const cliCommand = command === "find-unique" ? "find" : command;
|
|
844
|
+
let argv = [cliCommand, ...args];
|
|
822
845
|
try {
|
|
823
846
|
await validatePlaywrightCliOutputPath(command, args, ctx);
|
|
824
847
|
await validatePlaywrightCliInputFiles(command, args, ctx);
|
|
@@ -827,7 +850,7 @@ export async function executePlaywrightCliCommand(input, ctx, signal) {
|
|
|
827
850
|
if (!ctx.runCommand) {
|
|
828
851
|
const launcher = resolvePlaywrightCliLauncher(ctx.repoRoot);
|
|
829
852
|
executable = launcher.executable;
|
|
830
|
-
argv = [...launcher.argvPrefix,
|
|
853
|
+
argv = [...launcher.argvPrefix, cliCommand, ...args];
|
|
831
854
|
}
|
|
832
855
|
}
|
|
833
856
|
catch (error) {
|
|
@@ -887,6 +910,19 @@ export async function executePlaywrightCliCommand(input, ctx, signal) {
|
|
|
887
910
|
}
|
|
888
911
|
const finishedAt = new Date();
|
|
889
912
|
const combined = `${runResult.stdout}\n${runResult.stderr}`;
|
|
913
|
+
// find-family commands report their match count in stdout with exit code
|
|
914
|
+
// 0 even on zero matches — parse the count so receipts carry the real
|
|
915
|
+
// assertion signal for finalize-side audits.
|
|
916
|
+
const matchCount = command === "find" || command === "find-unique"
|
|
917
|
+
? parsePlaywrightCliFindMatchCount(runResult.stdout || "")
|
|
918
|
+
: undefined;
|
|
919
|
+
// find-unique fails closed unless exactly one match was found (including
|
|
920
|
+
// zero and unparseable outputs).
|
|
921
|
+
const findUniqueFailed = command === "find-unique" && matchCount !== 1 && !runResult.timedOut;
|
|
922
|
+
const effectiveExitCode = findUniqueFailed && runResult.exitCode === 0 ? 1 : runResult.exitCode;
|
|
923
|
+
const effectiveStderr = findUniqueFailed
|
|
924
|
+
? `${runResult.stderr}${runResult.stderr ? "\n" : ""}find-unique: expected exactly 1 match, got ${matchCount === undefined ? "unparseable output" : `${matchCount}`}`
|
|
925
|
+
: runResult.stderr;
|
|
890
926
|
const receipt = {
|
|
891
927
|
schemaVersion: 1,
|
|
892
928
|
caseId: ctx.caseId,
|
|
@@ -895,25 +931,28 @@ export async function executePlaywrightCliCommand(input, ctx, signal) {
|
|
|
895
931
|
argsRedacted: redactPlaywrightCliArgs(command, args),
|
|
896
932
|
startedAt: startedAt.toISOString(),
|
|
897
933
|
finishedAt: finishedAt.toISOString(),
|
|
898
|
-
exitCode:
|
|
934
|
+
exitCode: effectiveExitCode,
|
|
899
935
|
durationMs: finishedAt.getTime() - startedAt.getTime(),
|
|
900
936
|
outputSha256: createHash("sha256").update(combined).digest("hex"),
|
|
901
937
|
toolVersion: ctx.toolVersion ?? "unknown",
|
|
902
938
|
timedOut: runResult.timedOut || undefined,
|
|
939
|
+
...(matchCount !== undefined ? { matchCount } : {}),
|
|
903
940
|
errorClass: runResult.timedOut
|
|
904
941
|
? "timeout"
|
|
905
942
|
: runResult.signal === "abort"
|
|
906
943
|
? "abort"
|
|
907
|
-
:
|
|
944
|
+
: effectiveExitCode === 0
|
|
908
945
|
? undefined
|
|
909
|
-
:
|
|
946
|
+
: findUniqueFailed
|
|
947
|
+
? "find-unique-count-mismatch"
|
|
948
|
+
: "nonzero-exit",
|
|
910
949
|
};
|
|
911
950
|
await appendReceipt(ctx, receipt);
|
|
912
951
|
return {
|
|
913
|
-
ok:
|
|
914
|
-
exitCode:
|
|
952
|
+
ok: effectiveExitCode === 0 && !runResult.timedOut,
|
|
953
|
+
exitCode: effectiveExitCode,
|
|
915
954
|
stdout: runResult.stdout,
|
|
916
|
-
stderr:
|
|
955
|
+
stderr: effectiveStderr,
|
|
917
956
|
timedOut: runResult.timedOut,
|
|
918
957
|
receipt,
|
|
919
958
|
errorClass: receipt.errorClass,
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { appendFile, mkdir } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { computeExtensionToolAllowlistExtras } from "./pi-extension-resolver.js";
|
|
3
4
|
import { BoundedTextPreview, classifyPiFailure, createPiJsonlStreamCollector, DEFAULT_ABORT_GRACE_MS, DEFAULT_STALL_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, extractPiEventStopReason, extractAssistantTextFromPiJson, } from "./pi-executor.js";
|
|
4
5
|
import { serializeSessionEvent } from "./pi-event-serializer.js";
|
|
5
6
|
import { PI_RECOMMENDED_COMPACTION, PI_RECOMMENDED_RETRY, } from "../shared/pi-retry-settings.js";
|
|
7
|
+
/** Same-session recovery after a transport stall. Exactly one continue per SDK attempt. */
|
|
8
|
+
export const PI_SDK_STALL_CONTINUE_MAX = 1;
|
|
9
|
+
/**
|
|
10
|
+
* Fixed follow-up sent after aborting a stalled prompt.
|
|
11
|
+
* Kept short so the model can resume without repeating the original task payload.
|
|
12
|
+
*/
|
|
13
|
+
export const PI_SDK_STALL_CONTINUE_PROMPT = "继续。上一轮因长时间无 provider 活动已被中止;请从中断处接着完成,不要重复已完成的步骤。若此前没有有效输出,请重新执行原任务。";
|
|
6
14
|
let sdkSessionFactoryOverride;
|
|
7
15
|
let sdkImportOverrideForTests;
|
|
8
16
|
let sdkModuleOverrideForTests;
|
|
@@ -222,6 +230,9 @@ async function createSdkSession(sdk, input, shared) {
|
|
|
222
230
|
noContextFiles: true,
|
|
223
231
|
noSkills: true,
|
|
224
232
|
noExtensions: true,
|
|
233
|
+
...(input.additionalExtensionPaths && input.additionalExtensionPaths.length > 0
|
|
234
|
+
? { additionalExtensionPaths: input.additionalExtensionPaths }
|
|
235
|
+
: {}),
|
|
225
236
|
...(settingsManager ? { settingsManager } : {}),
|
|
226
237
|
appendSystemPromptOverride: (base) => [
|
|
227
238
|
...base,
|
|
@@ -229,6 +240,19 @@ async function createSdkSession(sdk, input, shared) {
|
|
|
229
240
|
],
|
|
230
241
|
});
|
|
231
242
|
await loader.reload();
|
|
243
|
+
// Discover extension-registered tool names BEFORE session creation (plan
|
|
244
|
+
// 2026-08-21 D5): verified against the real SDK — passing extension tool
|
|
245
|
+
// names in createAgentSession `tools` activates them, while a post-create
|
|
246
|
+
// setActiveToolsByName pin leaves the active set unchanged. Merge filtered
|
|
247
|
+
// extras into the create-time allowlist; protected builtins, the deny
|
|
248
|
+
// surface, and write-capable extension tools never join.
|
|
249
|
+
const extensionToolExtras = [];
|
|
250
|
+
if (input.additionalExtensionPaths &&
|
|
251
|
+
input.additionalExtensionPaths.length > 0) {
|
|
252
|
+
const registered = collectLoaderExtensionToolNames(loader);
|
|
253
|
+
extensionToolExtras.push(...computeExtensionToolAllowlistExtras(registered.map((name) => ({ name })), input.toolNames));
|
|
254
|
+
}
|
|
255
|
+
const sessionToolNames = [...input.toolNames, ...extensionToolExtras];
|
|
232
256
|
if (input.requireWriterCustomTools) {
|
|
233
257
|
if (!Array.isArray(input.customTools) || input.customTools.length === 0) {
|
|
234
258
|
throw new Error("pi writer tool policy missing customTools; refusing to create uncontrolled writer session");
|
|
@@ -238,7 +262,7 @@ async function createSdkSession(sdk, input, shared) {
|
|
|
238
262
|
cwd: input.cwd,
|
|
239
263
|
sessionManager: SessionManager.inMemory(input.cwd),
|
|
240
264
|
resourceLoader: loader,
|
|
241
|
-
tools:
|
|
265
|
+
tools: sessionToolNames,
|
|
242
266
|
modelRuntime,
|
|
243
267
|
...(settingsManager ? { settingsManager } : {}),
|
|
244
268
|
...(model ? { model } : {}),
|
|
@@ -247,6 +271,22 @@ async function createSdkSession(sdk, input, shared) {
|
|
|
247
271
|
? { customTools: input.customTools }
|
|
248
272
|
: {}),
|
|
249
273
|
});
|
|
274
|
+
if (extensionToolExtras.length > 0) {
|
|
275
|
+
// Fail closed when merged extension tools did not activate: a silent
|
|
276
|
+
// pin failure must never let the node run without its declared tools.
|
|
277
|
+
const activeTools = created.session.agent?.state?.tools;
|
|
278
|
+
if (!Array.isArray(activeTools)) {
|
|
279
|
+
throw new Error("pi SDK session does not expose agent.state.tools; refusing unverifiable extension-tool activation");
|
|
280
|
+
}
|
|
281
|
+
const activeNames = new Set(activeTools
|
|
282
|
+
.map((tool) => typeof tool?.name === "string" ? tool.name : undefined)
|
|
283
|
+
.filter((name) => Boolean(name)));
|
|
284
|
+
for (const extensionToolName of extensionToolExtras) {
|
|
285
|
+
if (!activeNames.has(extensionToolName)) {
|
|
286
|
+
throw new Error(`pi SDK extension-tool activation mismatch for ${extensionToolName}: tool did not activate at session creation`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
250
290
|
const customToolNames = (input.customTools ?? [])
|
|
251
291
|
.map((tool) => isRecord(tool) && typeof tool.name === "string" ? tool.name : undefined)
|
|
252
292
|
.filter((name) => Boolean(name));
|
|
@@ -259,7 +299,7 @@ async function createSdkSession(sdk, input, shared) {
|
|
|
259
299
|
.map((tool) => typeof tool?.name === "string" ? tool.name : undefined)
|
|
260
300
|
.filter((name) => Boolean(name)));
|
|
261
301
|
for (const customToolName of customToolNames) {
|
|
262
|
-
const expectedActive =
|
|
302
|
+
const expectedActive = sessionToolNames.includes(customToolName);
|
|
263
303
|
if (activeNames.has(customToolName) !== expectedActive) {
|
|
264
304
|
throw new Error(`pi SDK custom-tool activation mismatch for ${customToolName}: expectedActive=${expectedActive}`);
|
|
265
305
|
}
|
|
@@ -267,6 +307,47 @@ async function createSdkSession(sdk, input, shared) {
|
|
|
267
307
|
}
|
|
268
308
|
return created.session;
|
|
269
309
|
}
|
|
310
|
+
/**
|
|
311
|
+
* Collect tool names registered by loader-resolved extensions. The SDK exposes
|
|
312
|
+
* each extension's `tools` as a Record (name → definition) or Map; both shapes
|
|
313
|
+
* are handled. Missing getExtensions (older SDK) yields no extras — the node
|
|
314
|
+
* then runs with the baseline allowlist only.
|
|
315
|
+
*/
|
|
316
|
+
function collectLoaderExtensionToolNames(loader) {
|
|
317
|
+
const getExtensions = loader?.getExtensions;
|
|
318
|
+
if (typeof getExtensions !== "function")
|
|
319
|
+
return [];
|
|
320
|
+
let parsed;
|
|
321
|
+
try {
|
|
322
|
+
parsed = getExtensions.call(loader);
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
return [];
|
|
326
|
+
}
|
|
327
|
+
if (!parsed || typeof parsed !== "object")
|
|
328
|
+
return [];
|
|
329
|
+
const extensions = parsed.extensions;
|
|
330
|
+
if (!Array.isArray(extensions))
|
|
331
|
+
return [];
|
|
332
|
+
const names = [];
|
|
333
|
+
for (const extension of extensions) {
|
|
334
|
+
if (!extension || typeof extension !== "object")
|
|
335
|
+
continue;
|
|
336
|
+
const tools = extension.tools;
|
|
337
|
+
if (tools instanceof Map) {
|
|
338
|
+
for (const key of tools.keys()) {
|
|
339
|
+
if (typeof key === "string")
|
|
340
|
+
names.push(key);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
else if (tools && typeof tools === "object") {
|
|
344
|
+
for (const key of Object.keys(tools)) {
|
|
345
|
+
names.push(key);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return names;
|
|
350
|
+
}
|
|
270
351
|
/**
|
|
271
352
|
* Throttle high-noise Pi SDK events from session-events.jsonl.
|
|
272
353
|
* Always keeps tool lifecycle, turn/agent end, assistant messages, and errors.
|
|
@@ -425,6 +506,8 @@ function aggregateSdkTokenUsage(samples) {
|
|
|
425
506
|
* When reuseScope is active, only shared auth/model resources are reused; each attempt still
|
|
426
507
|
* creates and disposes its own session and resource loader. Session reuse across steps is
|
|
427
508
|
* deferred until a proven reset/isolation strategy exists.
|
|
509
|
+
* A transport stall may abort the current prompt and send one continue on the
|
|
510
|
+
* same session; that is in-attempt recovery, not cross-step session reuse.
|
|
428
511
|
*/
|
|
429
512
|
export async function executeSingleSdkAttempt(options) {
|
|
430
513
|
const modelConfig = options.modelConfig;
|
|
@@ -536,20 +619,37 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
536
619
|
? { customTools: writerCustomTools }
|
|
537
620
|
: {}),
|
|
538
621
|
...(requireWriterCustomTools ? { requireWriterCustomTools: true } : {}),
|
|
622
|
+
...(options.piExtensionPaths && options.piExtensionPaths.length > 0
|
|
623
|
+
? { additionalExtensionPaths: options.piExtensionPaths }
|
|
624
|
+
: {}),
|
|
539
625
|
});
|
|
540
626
|
let resolveStall;
|
|
541
627
|
let resolveSettlement;
|
|
542
628
|
let settled = false;
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
629
|
+
let abortConfirmed = true;
|
|
630
|
+
const persistRuntimeNote = (event) => {
|
|
631
|
+
const line = serializeSessionEvent(event);
|
|
632
|
+
stdoutPreview.append(`${line}\n`);
|
|
633
|
+
stdoutCollector.append(`${line}\n`);
|
|
634
|
+
sessionEventAppender?.append(line, event);
|
|
635
|
+
};
|
|
636
|
+
const resetSettlement = () => {
|
|
637
|
+
settled = false;
|
|
638
|
+
return new Promise((resolve) => {
|
|
639
|
+
resolveSettlement = resolve;
|
|
640
|
+
});
|
|
641
|
+
};
|
|
642
|
+
const resetStallPromise = () => {
|
|
643
|
+
if (stallTimeoutMs <= 0) {
|
|
644
|
+
resolveStall = undefined;
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
return new Promise((resolve) => {
|
|
548
648
|
resolveStall = resolve;
|
|
549
|
-
})
|
|
550
|
-
|
|
649
|
+
});
|
|
650
|
+
};
|
|
551
651
|
const armStallWatchdog = () => {
|
|
552
|
-
if (
|
|
652
|
+
if (stallTimeoutMs <= 0 || !resolveStall)
|
|
553
653
|
return;
|
|
554
654
|
if (stallHandle)
|
|
555
655
|
clearTimeout(stallHandle);
|
|
@@ -590,17 +690,6 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
590
690
|
stdoutCollector.append(`${line}\n`);
|
|
591
691
|
sessionEventAppender?.append(line, event);
|
|
592
692
|
});
|
|
593
|
-
const filePrefix = options.attachedFiles
|
|
594
|
-
.map((file) => `@${file}`)
|
|
595
|
-
.join(" ");
|
|
596
|
-
const promptMessage = filePrefix
|
|
597
|
-
? `${filePrefix}\n${options.userMessage}`
|
|
598
|
-
: options.userMessage;
|
|
599
|
-
const promptPromise = session.prompt(promptMessage);
|
|
600
|
-
// A settlement event is permitted to win the race while prompt() remains
|
|
601
|
-
// pending. Observe a later rejection so it never becomes unhandled.
|
|
602
|
-
void promptPromise.catch(() => { });
|
|
603
|
-
armStallWatchdog();
|
|
604
693
|
const timeoutPromise = timeoutMs > 0
|
|
605
694
|
? new Promise((resolve) => {
|
|
606
695
|
timeoutHandle = setTimeout(() => {
|
|
@@ -608,21 +697,79 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
608
697
|
}, timeoutMs);
|
|
609
698
|
})
|
|
610
699
|
: null;
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
700
|
+
const runTurn = async (userMessage) => {
|
|
701
|
+
const settlementPromise = resetSettlement();
|
|
702
|
+
const stallPromise = resetStallPromise();
|
|
703
|
+
const promptPromise = session.prompt(userMessage);
|
|
704
|
+
// A settlement event is permitted to win the race while prompt() remains
|
|
705
|
+
// pending. Observe a later rejection so it never becomes unhandled.
|
|
706
|
+
void promptPromise.catch(() => { });
|
|
707
|
+
armStallWatchdog();
|
|
708
|
+
return Promise.race([
|
|
709
|
+
promptPromise.then(() => "done"),
|
|
710
|
+
settlementPromise,
|
|
711
|
+
...(timeoutPromise ? [timeoutPromise] : []),
|
|
712
|
+
...(stallPromise ? [stallPromise] : []),
|
|
713
|
+
]);
|
|
714
|
+
};
|
|
715
|
+
const disposeSession = async () => {
|
|
623
716
|
disposeAttempted = true;
|
|
624
717
|
const disposeConfirmed = await runSessionActionWithGrace("dispose", () => session.dispose());
|
|
625
718
|
terminationConfirmed = abortConfirmed && disposeConfirmed;
|
|
719
|
+
};
|
|
720
|
+
const failClosedAfterWait = async (reason, extraStderr, abortFirst = true) => {
|
|
721
|
+
timedOut = true;
|
|
722
|
+
appendStderr(reason === "stall"
|
|
723
|
+
? `pi SDK step stalled after ${stallTimeoutMs}ms with no provider activity`
|
|
724
|
+
: `pi SDK step timed out after ${timeoutMs}ms`);
|
|
725
|
+
if (extraStderr)
|
|
726
|
+
appendStderr(extraStderr);
|
|
727
|
+
if (abortFirst) {
|
|
728
|
+
abortConfirmed = await runSessionActionWithGrace("abort", () => session.abort());
|
|
729
|
+
}
|
|
730
|
+
await disposeSession();
|
|
731
|
+
};
|
|
732
|
+
const filePrefix = options.attachedFiles
|
|
733
|
+
.map((file) => `@${file}`)
|
|
734
|
+
.join(" ");
|
|
735
|
+
const promptMessage = filePrefix
|
|
736
|
+
? `${filePrefix}\n${options.userMessage}`
|
|
737
|
+
: options.userMessage;
|
|
738
|
+
let continuesSent = 0;
|
|
739
|
+
let raced = await runTurn(promptMessage);
|
|
740
|
+
if (raced === "stall") {
|
|
741
|
+
abortConfirmed = await runSessionActionWithGrace("abort", () => session.abort());
|
|
742
|
+
const withinAbsoluteTimeout = timeoutMs <= 0 || Date.now() - startedAt < timeoutMs;
|
|
743
|
+
const canContinue = abortConfirmed &&
|
|
744
|
+
writeToolCallCount === 0 &&
|
|
745
|
+
withinAbsoluteTimeout &&
|
|
746
|
+
continuesSent < PI_SDK_STALL_CONTINUE_MAX;
|
|
747
|
+
if (canContinue) {
|
|
748
|
+
// The aborted turn's stopReason must not leak onto a recovered continue.
|
|
749
|
+
stopReason = undefined;
|
|
750
|
+
continuesSent += 1;
|
|
751
|
+
persistRuntimeNote({
|
|
752
|
+
type: "loop-agent-stall-continue",
|
|
753
|
+
prompt: PI_SDK_STALL_CONTINUE_PROMPT,
|
|
754
|
+
maxContinues: PI_SDK_STALL_CONTINUE_MAX,
|
|
755
|
+
});
|
|
756
|
+
raced = await runTurn(PI_SDK_STALL_CONTINUE_PROMPT);
|
|
757
|
+
if (raced !== "done" && raced !== "settled") {
|
|
758
|
+
await failClosedAfterWait(raced === "stall" ? "stall" : "absolute-timeout", raced === "stall"
|
|
759
|
+
? "pi SDK stall continue did not recover; treating as timeout"
|
|
760
|
+
: "pi SDK stall continue hit the absolute timeout", true);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
else {
|
|
764
|
+
await failClosedAfterWait("stall", !abortConfirmed
|
|
765
|
+
? undefined
|
|
766
|
+
: writeToolCallCount > 0
|
|
767
|
+
? "pi SDK stall continue skipped because write tools already ran"
|
|
768
|
+
: "pi SDK stall continue skipped because absolute timeout elapsed", false);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
else if (raced === "absolute-timeout") {
|
|
772
|
+
await failClosedAfterWait("absolute-timeout");
|
|
626
773
|
}
|
|
627
774
|
}
|
|
628
775
|
catch (error) {
|