@tea-agent/loop-agent 0.38.1 → 0.38.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/CHANGELOG.md +126 -12
- package/README.md +2 -0
- package/bin/loop-agent.js +7 -3
- package/dist/application/dag/generate-task-dag.js +9 -0
- package/dist/application/task-lifecycle/advance.js +4 -0
- package/dist/build-stamp.json +3 -3
- package/dist/commands/init.js +2 -14
- package/dist/commands/task-advance.js +31 -0
- package/dist/executors/dag-pi-executor.js +99 -5
- package/dist/executors/pi-event-serializer.js +42 -1
- package/dist/executors/pi-executor.js +51 -0
- package/dist/executors/pi-extension-resolver.js +233 -0
- package/dist/executors/pi-sdk-executor.js +187 -59
- package/dist/executors/shell-executor.js +324 -16
- package/dist/executors/shell-write-guard.js +7 -0
- package/dist/governance/manifest-types.js +0 -2
- package/dist/shared/operator/capabilities.js +87 -22
- package/dist/task/config-types.js +22 -0
- package/dist/task/contract/constants.js +1 -0
- package/dist/task/contract/diff.js +26 -0
- package/dist/task/contract/project.js +5 -0
- package/dist/task/contract/schema.js +6 -1
- package/dist/task/source-prepare/build-draft.js +15 -0
- package/dist/worker/console/chat/deferred-turn.js +91 -0
- package/dist/worker/console/chat/pi-runtime.js +10 -5
- package/dist/worker/console/chat/routes.js +458 -47
- package/dist/worker/console/chat/semantic-activity.js +13 -13
- package/dist/worker/console/chat/session-store.js +146 -2
- package/dist/worker/console/harness-pi-model-matrix.js +2 -11
- package/dist/worker/console/operator-actions.js +5 -4
- package/dist/worker/console/pi-readiness.js +5 -4
- package/dist/worker/console/recovery-cta.js +4 -4
- package/dist/worker/console/recovery-error-copy.js +4 -4
- package/dist/worker/console/server.js +6 -1
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Hfsot6rP.js → abnfDiagram-N423BO3Z-C4kVYweA.js} +1 -1
- package/dist/worker/console/static/assets/{arc-YGwEASin.js → arc-Bj2M8iO5.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CmOM0F92.js → architectureDiagram-T3A2C74G-CLlXe4-6.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-DDLuaHC5.js → blockDiagram-VBNYF7ZC-psEp5xH0.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B0Um43W4.js → c4Diagram-5PPSVZJV-DeKcOCGJ.js} +1 -1
- package/dist/worker/console/static/assets/channel-CPF4N7pf.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DvBN4JZc.js → chunk-2GRJ4B5K-K0C744p1.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B1UVgW4E.js → chunk-2Q5K7J3B-xbNw4J9-.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-Cf0GgXzX.js → chunk-5RXB4S5H-DjLaSDUO.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-DbNA24sj.js → chunk-5VM5RSS4-CtiPlKel.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Bi_0Be06.js → chunk-6Q2QTUOP-DZtolir7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-C8pPGvRd.js → chunk-GF5L2VYU-CPcdNeew.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BitQY8Z-.js → chunk-JWPE2WC7-NgHc6V37.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-U67GCiEo.js → chunk-KBJHAD2P-DQ_T-hg8.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-IoJwXPBA.js → chunk-RYQCIY6F-DgLsxcVP.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-YDZ_UHgE.js → chunk-XXDRQBXY-UrVoM9z1.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CUZgsiRC.js → cose-bilkent-JH36ORCC-CLkYvTb3.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-COmCh4pT.js → cynefin-VYW2F7L2-BwT_xKrE.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Ta6FSm68.js → cynefinDiagram-MW4NZA55-Bi9tuzif.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-UdcR-VW9.js → dagre-VZM6K2ZE-BCIqkWBV.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DybcO3Et.js → diagram-7IWD3JNH-Dip6l9_Z.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-wErDFWLw.js → diagram-B4RE2ZJO-B-xkh_wK.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DkVHZWqz.js → diagram-LBJQPF4R-DZO0kTt_.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DdqWs7QN.js → diagram-Q27KOJAE-D6ZplNbZ.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-C9dMVmDo.js → diagram-UB23O5K3-BPekbhoS.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-B2T2WcBK.js → ebnfDiagram-BXEA7PRR-BtmaJpK5.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Bt9rjDFq.js → erDiagram-JOGREHBK-6DS0Xc44.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-U8ic_tvi.js → flowDiagram-UKHOOZJN-CihnROSm.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BfQJdQoO.js → ganttDiagram-PKOTCBZU-CZC4zOE2.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CpMftH4O.js → gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js} +1 -1
- package/dist/worker/console/static/assets/index-DTZOKgAn.js +404 -0
- package/dist/worker/console/static/assets/index-Ya5FE7cD.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DzqWXNO_.js → infoDiagram-6WML65LV-DlSl4BGx.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-CJmGQ5Yp.js → ishikawaDiagram-WSZJBQD7-snnzFl_U.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-ChPtKFLz.js → journeyDiagram-NVQOT4AX-BDE7qpMx.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-PRe7wB5u.js → kanban-definition-27J2QSJJ-CD2Ci04G.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DpKX6QNp.js → linear-DdnapKIH.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Dnl76b7Y.js → mermaid.core-BVjAT9b8.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-S9PCXc3J.js → mindmap-definition-FAOFIHXS-D0yJk-zA.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Du8e-FSX.js → pegDiagram-VL7TDLO6-BVo9tFP-.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CY6OmHTc.js → pieDiagram-7S7Q4E2Y-DnOV-mZ_.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-C65GPhpj.js → quadrantDiagram-CIZ2JOQS-CzUJo58i.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-U6IUiTCL.js → railroadDiagram-AXF67PYL-Bvikrolh.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D-I_9Qhi.js → requirementDiagram-LRYGKXZP-DtaSVCap.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B57Fhed0.js → sankeyDiagram-W5VNT64P-C2c9A0wW.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CJOtiw21.js → sequenceDiagram-SI44F4Z6-DmXJcU7r.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-FyX8zxSh.js → sizeCapture-X5ZJPWSS-B4GUFW92.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-eF-pbs9t.js → stateDiagram-OKZ733FA-Dsad2MXf.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CcfkA-Dq.js → swimlanes-SLNWSIFB-DGq48Fbi.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DH8FjnOK.js → timeline-definition-Z64GVDOM-CSSi6OFf.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-beoTagJg.js → vennDiagram-T6HMQDX7-BqyzPrWv.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CQSfw4cK.js → wardleyDiagram-T6FBY63Y-DlznVb6S.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CQgzQ6jn.js → xychartDiagram-ELKLHX3M-CfBcgJ_K.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +0 -12
- package/dist/worker/console/static-src/app/useOperatorActions.js +1 -1
- package/dist/worker/console/static-src/app/useRecoveryActions.js +2 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +38 -1
- package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +8 -1
- package/dist/worker/console/static-src/operator-chat/turn-submission.js +42 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +3 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +204 -11
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +23 -1
- package/dist/worker/observe/node-input.js +72 -3
- package/dist/worker/observe/static/dag-history-labels.js +4 -0
- package/dist/worker/observe/static/state.js +2 -2
- package/dist/worker/observe/static/views/dag-inspector.js +51 -0
- package/dist/worker/observe/static/views/session-timeline.js +18 -5
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +809 -53
- package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
- package/dist/workflows/dag/backend-test-execution-contract.js +18 -9
- package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
- package/dist/workflows/dag/backend-test-intake-context.js +32 -0
- package/dist/workflows/dag/backend-test-layout.js +133 -0
- package/dist/workflows/dag/backend-test-markdown-workflow.js +167 -32
- package/dist/workflows/dag/backend-test-module-stem.js +5 -0
- package/dist/workflows/dag/backend-test-pytest-collection.js +368 -33
- package/dist/workflows/dag/backend-test-result-contract.js +1 -1
- package/dist/workflows/dag/backend-test-scenario-param.js +884 -140
- package/dist/workflows/dag/backend-test-scenario-partitions.js +321 -0
- package/dist/workflows/dag/backend-test-writer-completeness.js +100 -56
- package/dist/workflows/dag/contract-output-registry.js +15 -0
- package/dist/workflows/dag/contract-validator-registrations.js +2 -1
- package/dist/workflows/dag/convergence/controller.js +141 -0
- package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
- package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
- package/dist/workflows/dag/failure-category.js +4 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +355 -26
- package/dist/workflows/dag/frontend-prewrite-gate.js +96 -84
- 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 +10 -18
- package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
- package/dist/workflows/dag/init-hybrid.js +720 -177
- package/dist/workflows/dag/node-execution.js +164 -26
- package/dist/workflows/dag/prompt.js +60 -1
- package/dist/workflows/dag/rerun-feedback.js +212 -0
- package/dist/workflows/dag/rerun-plan.js +112 -27
- package/dist/workflows/dag/rerun-run.js +3 -0
- package/dist/workflows/dag/rerun-task.js +61 -2
- package/dist/workflows/dag/retry-policy.js +18 -0
- package/dist/workflows/dag/structured-output-repair.js +712 -0
- package/dist/workflows/dag/types.js +121 -0
- package/dist/workflows/dag/validate.js +41 -1
- package/docs/operations/local-development-environment.md +1 -5
- package/docs/skills/vetted-skill-registry.md +14 -0
- package/docs/templates/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +44 -0
- package/docs/templates/backend-test-dag.json +60 -35
- package/docs/templates/frontend-test-dag.json +8 -10
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
- package/docs/templates/harness.schema.json +0 -8
- package/harness.json +1 -8
- package/package.json +8 -4
- package/skills/analyze-product-requirements/SKILL.md +8 -5
- package/skills/analyze-product-requirements/references/acceptance-criteria.md +1 -1
- package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +10 -6
- package/skills/analyze-product-requirements/references/example.md +50 -0
- package/skills/analyze-product-requirements/references/forward-test-cases.md +11 -7
- package/skills/analyze-product-requirements/references/kb-integration.md +5 -5
- package/skills/analyze-product-requirements/references/product-analysis-schema.md +8 -4
- package/skills/analyze-product-requirements/references/product-requirement-schema.md +2 -2
- package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +1 -1
- package/skills/analyze-product-requirements/scripts/test-validators.mjs +11 -1
- package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +20 -1
- package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +9 -0
- package/skills/codebase-scout/SKILL.md +1 -1
- package/skills/frontend-implementation/references/node-contracts.md +2 -2
- package/skills/improve-codebase-architecture/SKILL.md +81 -0
- package/skills/improve-codebase-architecture/deepening.md +37 -0
- package/skills/improve-codebase-architecture/html-report.md +123 -0
- package/skills/improve-codebase-architecture/interface-design.md +44 -0
- package/skills/improve-codebase-architecture/language.md +53 -0
- package/dist/worker/console/static/assets/channel-Dp_2Aqf1.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL--CODy37j.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE--CODy37j.js +0 -1
- package/dist/worker/console/static/assets/index-BA5-N5ER.css +0 -1
- package/dist/worker/console/static/assets/index-DKPXBrdc.js +0 -325
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B-mvnLEc.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-JtpBmIv4.js +0 -8
|
@@ -408,6 +408,50 @@ export class BoundedTextPreview {
|
|
|
408
408
|
].join("\n");
|
|
409
409
|
}
|
|
410
410
|
}
|
|
411
|
+
/**
|
|
412
|
+
* Normalize provider-specific completion-limit labels to the DAG's canonical
|
|
413
|
+
* `length` signal. Other terminal reasons remain observable without being
|
|
414
|
+
* treated as truncation.
|
|
415
|
+
*/
|
|
416
|
+
export function normalizeProviderStopReason(value) {
|
|
417
|
+
if (typeof value !== "string" || value.trim().length === 0)
|
|
418
|
+
return undefined;
|
|
419
|
+
const trimmed = value.trim();
|
|
420
|
+
const compact = trimmed.toLowerCase().replace(/[\s_-]/g, "");
|
|
421
|
+
if ([
|
|
422
|
+
"length",
|
|
423
|
+
"maxtokens",
|
|
424
|
+
"maxoutputtokens",
|
|
425
|
+
"outputlimit",
|
|
426
|
+
"tokenlimit",
|
|
427
|
+
"tokenlimitreached",
|
|
428
|
+
].includes(compact)) {
|
|
429
|
+
return "length";
|
|
430
|
+
}
|
|
431
|
+
return trimmed;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Extract a terminal stop reason from the provider event shapes used by Pi
|
|
435
|
+
* SDK/CLI bridges. Providers disagree on `stop*` versus `finish*`; keep this
|
|
436
|
+
* boundary centralized so downstream DAG recovery sees one canonical signal.
|
|
437
|
+
*/
|
|
438
|
+
export function extractPiEventStopReason(event) {
|
|
439
|
+
const nested = [event.message, event.response, event.completion].filter(isRecord);
|
|
440
|
+
const containers = [...nested, event];
|
|
441
|
+
for (const container of containers) {
|
|
442
|
+
for (const key of [
|
|
443
|
+
"stopReason",
|
|
444
|
+
"stop_reason",
|
|
445
|
+
"finishReason",
|
|
446
|
+
"finish_reason",
|
|
447
|
+
]) {
|
|
448
|
+
const normalized = normalizeProviderStopReason(container[key]);
|
|
449
|
+
if (normalized)
|
|
450
|
+
return normalized;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return undefined;
|
|
454
|
+
}
|
|
411
455
|
export class PiJsonlStreamCollector {
|
|
412
456
|
lineBuffer = "";
|
|
413
457
|
discardUntilNewline = false;
|
|
@@ -415,6 +459,7 @@ export class PiJsonlStreamCollector {
|
|
|
415
459
|
parsedEvents = 0;
|
|
416
460
|
tokensUsed = 0;
|
|
417
461
|
outputTooLarge = false;
|
|
462
|
+
stopReason;
|
|
418
463
|
subagentTotalCalls = 0;
|
|
419
464
|
subagentFailedCalls = 0;
|
|
420
465
|
subagentModes = new Set();
|
|
@@ -454,6 +499,7 @@ export class PiJsonlStreamCollector {
|
|
|
454
499
|
assistantText: this.assistantText.trim(),
|
|
455
500
|
outputTooLarge: this.outputTooLarge,
|
|
456
501
|
parsedEvents: this.parsedEvents,
|
|
502
|
+
stopReason: this.stopReason,
|
|
457
503
|
subagentStats: this.formatSubagentStats(),
|
|
458
504
|
tokensUsed: this.tokensUsed,
|
|
459
505
|
};
|
|
@@ -472,6 +518,10 @@ export class PiJsonlStreamCollector {
|
|
|
472
518
|
}
|
|
473
519
|
}
|
|
474
520
|
consumeEvent(event) {
|
|
521
|
+
const stopReason = extractPiEventStopReason(event);
|
|
522
|
+
if (stopReason === "length" || !this.stopReason) {
|
|
523
|
+
this.stopReason = stopReason ?? this.stopReason;
|
|
524
|
+
}
|
|
475
525
|
const candidate = extractAssistantTextFromEvent(event);
|
|
476
526
|
if (candidate !== undefined) {
|
|
477
527
|
if (candidate.length > MAX_ASSISTANT_TEXT_CHARS) {
|
|
@@ -865,6 +915,7 @@ async function executeSingleCliAttempt(options, modelConfig, promptFilePath) {
|
|
|
865
915
|
stdoutTruncated: stdoutPreview.truncated,
|
|
866
916
|
stderrTruncated: stderrPreview.truncated,
|
|
867
917
|
outputTooLarge: collected.outputTooLarge,
|
|
918
|
+
stopReason: collected.stopReason,
|
|
868
919
|
timedOut,
|
|
869
920
|
tokensUsed: collected.tokensUsed,
|
|
870
921
|
subagentStats: collected.subagentStats,
|
|
@@ -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
|
+
}
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { appendFile, mkdir } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { computeExtensionToolAllowlistExtras } from "./pi-extension-resolver.js";
|
|
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;
|
|
@@ -44,15 +52,15 @@ export async function resetPiSdkReuseScopeForTests() {
|
|
|
44
52
|
export function setPiSdkSessionFactoryForTests(factory) {
|
|
45
53
|
sdkSessionFactoryOverride = factory;
|
|
46
54
|
}
|
|
47
|
-
/** Test hook: simulate SDK import success/failure without relying on
|
|
55
|
+
/** Test hook: simulate SDK import success/failure without relying on installed SDK state. */
|
|
48
56
|
export function setPiSdkImportOverrideForTests(fn) {
|
|
49
57
|
sdkImportOverrideForTests = fn;
|
|
50
58
|
}
|
|
51
|
-
/** Test hook: inject a mock Pi SDK module to exercise resolveSdkSessionFactory without
|
|
59
|
+
/** Test hook: inject a mock Pi SDK module to exercise resolveSdkSessionFactory without loading the real SDK. */
|
|
52
60
|
export function setPiSdkModuleOverrideForTests(fn) {
|
|
53
61
|
sdkModuleOverrideForTests = fn;
|
|
54
62
|
}
|
|
55
|
-
/** Check whether the Pi SDK
|
|
63
|
+
/** Check whether the Pi SDK runtime dependency satisfies the 0.83.0 runtime contract. */
|
|
56
64
|
export async function checkPiSdkAvailability(_repoRoot) {
|
|
57
65
|
if (sdkSessionFactoryOverride) {
|
|
58
66
|
return { ok: true, detail: "pi SDK session factory override active" };
|
|
@@ -68,10 +76,10 @@ export async function checkPiSdkAvailability(_repoRoot) {
|
|
|
68
76
|
typeof ModelRuntime?.create !== "function") {
|
|
69
77
|
return {
|
|
70
78
|
ok: false,
|
|
71
|
-
detail: "pi SDK incompatible: requires createAgentSession, getAgentDir, and ModelRuntime.create (0.
|
|
79
|
+
detail: "pi SDK incompatible: requires createAgentSession, getAgentDir, and ModelRuntime.create (0.83.0 contract)",
|
|
72
80
|
};
|
|
73
81
|
}
|
|
74
|
-
return { ok: true, detail: "pi SDK 0.
|
|
82
|
+
return { ok: true, detail: "pi SDK 0.83.0 contract available" };
|
|
75
83
|
}
|
|
76
84
|
catch (error) {
|
|
77
85
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -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.
|
|
@@ -404,25 +485,6 @@ function extractSdkUsageSample(event) {
|
|
|
404
485
|
const responseKey = responseKeyCandidates.find((value) => typeof value === "string" && value.length > 0);
|
|
405
486
|
return responseKey ? { responseKey, tokens } : { tokens };
|
|
406
487
|
}
|
|
407
|
-
/** Extract a non-empty stop reason from a terminal SDK event.
|
|
408
|
-
* Probes multiple field shapes across SDK versions so a missing field fails
|
|
409
|
-
* open (undefined) rather than misclassifying an attempt. */
|
|
410
|
-
function readStopReason(event) {
|
|
411
|
-
const message = isRecord(event.message) ? event.message : undefined;
|
|
412
|
-
const candidates = [
|
|
413
|
-
message?.stop_reason,
|
|
414
|
-
message?.stopReason,
|
|
415
|
-
event.stopReason,
|
|
416
|
-
event.stop_reason,
|
|
417
|
-
];
|
|
418
|
-
for (const candidate of candidates) {
|
|
419
|
-
if (typeof candidate === "string" &&
|
|
420
|
-
candidate.trim().length > 0) {
|
|
421
|
-
return candidate.trim();
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
return undefined;
|
|
425
|
-
}
|
|
426
488
|
function aggregateSdkTokenUsage(samples) {
|
|
427
489
|
const identified = new Map();
|
|
428
490
|
let anonymousMaximum = 0;
|
|
@@ -444,6 +506,8 @@ function aggregateSdkTokenUsage(samples) {
|
|
|
444
506
|
* When reuseScope is active, only shared auth/model resources are reused; each attempt still
|
|
445
507
|
* creates and disposes its own session and resource loader. Session reuse across steps is
|
|
446
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.
|
|
447
511
|
*/
|
|
448
512
|
export async function executeSingleSdkAttempt(options) {
|
|
449
513
|
const modelConfig = options.modelConfig;
|
|
@@ -494,7 +558,7 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
494
558
|
type === "message_end" ||
|
|
495
559
|
type === "agent_end" ||
|
|
496
560
|
type === "agent_settled") {
|
|
497
|
-
const candidate =
|
|
561
|
+
const candidate = extractPiEventStopReason(event);
|
|
498
562
|
if (candidate)
|
|
499
563
|
stopReason = candidate;
|
|
500
564
|
}
|
|
@@ -555,20 +619,37 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
555
619
|
? { customTools: writerCustomTools }
|
|
556
620
|
: {}),
|
|
557
621
|
...(requireWriterCustomTools ? { requireWriterCustomTools: true } : {}),
|
|
622
|
+
...(options.piExtensionPaths && options.piExtensionPaths.length > 0
|
|
623
|
+
? { additionalExtensionPaths: options.piExtensionPaths }
|
|
624
|
+
: {}),
|
|
558
625
|
});
|
|
559
626
|
let resolveStall;
|
|
560
627
|
let resolveSettlement;
|
|
561
628
|
let settled = false;
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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) => {
|
|
567
648
|
resolveStall = resolve;
|
|
568
|
-
})
|
|
569
|
-
|
|
649
|
+
});
|
|
650
|
+
};
|
|
570
651
|
const armStallWatchdog = () => {
|
|
571
|
-
if (
|
|
652
|
+
if (stallTimeoutMs <= 0 || !resolveStall)
|
|
572
653
|
return;
|
|
573
654
|
if (stallHandle)
|
|
574
655
|
clearTimeout(stallHandle);
|
|
@@ -609,17 +690,6 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
609
690
|
stdoutCollector.append(`${line}\n`);
|
|
610
691
|
sessionEventAppender?.append(line, event);
|
|
611
692
|
});
|
|
612
|
-
const filePrefix = options.attachedFiles
|
|
613
|
-
.map((file) => `@${file}`)
|
|
614
|
-
.join(" ");
|
|
615
|
-
const promptMessage = filePrefix
|
|
616
|
-
? `${filePrefix}\n${options.userMessage}`
|
|
617
|
-
: options.userMessage;
|
|
618
|
-
const promptPromise = session.prompt(promptMessage);
|
|
619
|
-
// A settlement event is permitted to win the race while prompt() remains
|
|
620
|
-
// pending. Observe a later rejection so it never becomes unhandled.
|
|
621
|
-
void promptPromise.catch(() => { });
|
|
622
|
-
armStallWatchdog();
|
|
623
693
|
const timeoutPromise = timeoutMs > 0
|
|
624
694
|
? new Promise((resolve) => {
|
|
625
695
|
timeoutHandle = setTimeout(() => {
|
|
@@ -627,21 +697,79 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
627
697
|
}, timeoutMs);
|
|
628
698
|
})
|
|
629
699
|
: null;
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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 () => {
|
|
642
716
|
disposeAttempted = true;
|
|
643
717
|
const disposeConfirmed = await runSessionActionWithGrace("dispose", () => session.dispose());
|
|
644
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");
|
|
645
773
|
}
|
|
646
774
|
}
|
|
647
775
|
catch (error) {
|