@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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useCallback, useEffect } from "react";
|
|
2
2
|
import { formatOperatorUserError } from "../../operator-user-error.js";
|
|
3
3
|
import { confirmationToken, requestId, sleep } from "./format.js";
|
|
4
|
-
import { classifyCreateTurnNetworkFailure, classifyCreateTurnResponse, createPendingSubmission, isRuntimeBusyUnion, isSubmissionCurrent, isTerminalTurnState, keepAmbiguousAfterRetry, reconcilePendingSubmissionFromState, resolveAmbiguousFromState, } from "./turn-submission.js";
|
|
5
|
-
import { applyAbortDecision, applyPendingReconcileDecision, classifyAbortResponse, markStopRequested, operatorChatAbortUrl, operatorChatEventsUrl, operatorChatStateUrl, operatorChatTurnsUrl, releaseOwnershipOnTerminalEvidence, releaseSubmitOwnership as releaseOwnershipBag, requestStopOnOwnership, settleCreateTurnOutcome, } from "./turn-stream-controller.js";
|
|
4
|
+
import { classifyCreateTurnNetworkFailure, classifyCreateTurnResponse, createPendingSubmission, decideSubmissionRoute, isRuntimeBusyUnion, isSubmissionCurrent, isTerminalTurnState, keepAmbiguousAfterRetry, reconcileDeferredSubmissionFromState, reconcilePendingSubmissionFromState, resolveAmbiguousFromState, } from "./turn-submission.js";
|
|
5
|
+
import { applyAbortDecision, applyPendingReconcileDecision, classifyAbortResponse, markStopRequested, operatorChatAbortUrl, operatorChatEventsUrl, operatorChatStateUrl, operatorChatTurnsUrl, releaseOwnershipOnTerminalEvidence, releaseSubmitOwnership as releaseOwnershipBag, requestStopOnOwnership, settleCreateTurnOutcome, turnPhaseFromOwnership, } from "./turn-stream-controller.js";
|
|
6
6
|
/** Project ChatRefs into the executable ownership bag used by the controller. */
|
|
7
7
|
function ownershipBagFromRefs(refs) {
|
|
8
8
|
return {
|
|
@@ -57,7 +57,7 @@ function clearEmptyAssistant(setMessages, assistantId) {
|
|
|
57
57
|
* detached Turn resource only; it never opens a second inline SSE consumer. */
|
|
58
58
|
export function useChatStream(params) {
|
|
59
59
|
const { origin, refs, session, thread, composer, setError, setAutoScroll, setNotice, recoveryVersion = 0, watchSessionTitle, } = params;
|
|
60
|
-
const { applySseBlock, setMessages, setStreaming, setStreamingAssistantId, streaming, } = thread;
|
|
60
|
+
const { applySseBlock, setMessages, setStreaming, setStreamingAssistantId, streaming, setTurnPhase, setDeferredTurn, } = thread;
|
|
61
61
|
const { setInput, setPendingImages } = composer;
|
|
62
62
|
const showNotice = useCallback((message) => {
|
|
63
63
|
if (!setNotice)
|
|
@@ -78,9 +78,10 @@ export function useChatStream(params) {
|
|
|
78
78
|
refs.assistantIdRef.current = null;
|
|
79
79
|
refs.stoppingTurnIdRef.current = null;
|
|
80
80
|
setStreaming(false);
|
|
81
|
+
setTurnPhase("idle");
|
|
81
82
|
setStreamingAssistantId(null);
|
|
82
83
|
clearEmptyAssistant(setMessages, assistantId);
|
|
83
|
-
}, [refs, setStreaming, setStreamingAssistantId, setMessages]);
|
|
84
|
+
}, [refs, setStreaming, setTurnPhase, setStreamingAssistantId, setMessages]);
|
|
84
85
|
/** Follow one session's event stream until it is explicitly aborted. The
|
|
85
86
|
* server's `stream_ready` marker separates replay from live events; this
|
|
86
87
|
* means replay cannot render duplicate messages while live text remains
|
|
@@ -242,13 +243,12 @@ export function useChatStream(params) {
|
|
|
242
243
|
/** Issue real POST /abort for a known turnId (Stop never disposes Session). */
|
|
243
244
|
const postAbortForTurn = useCallback(async (sessionId, generation, turnId) => {
|
|
244
245
|
refs.stoppingTurnIdRef.current = turnId;
|
|
245
|
-
// Optimistic stop
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
// server later reports the turn still running (TURN_ABORT_PENDING
|
|
249
|
-
// or owner-lost reconcile), applyAbortDecision restores busy state.
|
|
246
|
+
// Optimistic visual stop: remove the running spinner immediately, but
|
|
247
|
+
// retain ownership as `stopping` until the server proves a terminal state.
|
|
248
|
+
// This keeps the composer responsive without falsely advertising idle.
|
|
250
249
|
refs.streamingRef.current = false;
|
|
251
250
|
setStreaming(false);
|
|
251
|
+
setTurnPhase("stopping");
|
|
252
252
|
setStreamingAssistantId(null);
|
|
253
253
|
try {
|
|
254
254
|
const res = await fetch(operatorChatAbortUrl(origin, sessionId, turnId), {
|
|
@@ -283,6 +283,7 @@ export function useChatStream(params) {
|
|
|
283
283
|
markIdle: () => markIdleIfCurrent(sessionId, generation, refs.assistantIdRef.current),
|
|
284
284
|
});
|
|
285
285
|
applyOwnershipBagToRefs(refs, bag);
|
|
286
|
+
setTurnPhase(turnPhaseFromOwnership(bag));
|
|
286
287
|
return;
|
|
287
288
|
}
|
|
288
289
|
applyAbortDecision(bag, decision, noticeSink(), {
|
|
@@ -290,6 +291,7 @@ export function useChatStream(params) {
|
|
|
290
291
|
markIdle: () => markIdleIfCurrent(sessionId, generation, refs.assistantIdRef.current),
|
|
291
292
|
});
|
|
292
293
|
applyOwnershipBagToRefs(refs, bag);
|
|
294
|
+
setTurnPhase(turnPhaseFromOwnership(bag));
|
|
293
295
|
// Sync the post-decision busy state back to React: optimistic stop
|
|
294
296
|
// flipped it idle up front; pending/keep-busy outcomes restore it.
|
|
295
297
|
if (bag.streaming)
|
|
@@ -310,11 +312,13 @@ export function useChatStream(params) {
|
|
|
310
312
|
// so the turn is not silently dropped; /state reconcile converges.
|
|
311
313
|
refs.streamingRef.current = true;
|
|
312
314
|
setStreaming(true);
|
|
315
|
+
setTurnPhase("stopping");
|
|
313
316
|
}
|
|
314
317
|
}, [
|
|
315
318
|
origin,
|
|
316
319
|
refs,
|
|
317
320
|
setStreaming,
|
|
321
|
+
setTurnPhase,
|
|
318
322
|
markIdleIfCurrent,
|
|
319
323
|
fetchSessionState,
|
|
320
324
|
noticeSink,
|
|
@@ -364,10 +368,12 @@ export function useChatStream(params) {
|
|
|
364
368
|
},
|
|
365
369
|
});
|
|
366
370
|
applyOwnershipBagToRefs(refs, bag);
|
|
371
|
+
setTurnPhase(turnPhaseFromOwnership(bag));
|
|
367
372
|
}, [
|
|
368
373
|
refs,
|
|
369
374
|
noticeSink,
|
|
370
375
|
setStreaming,
|
|
376
|
+
setTurnPhase,
|
|
371
377
|
setStreamingAssistantId,
|
|
372
378
|
setPendingImages,
|
|
373
379
|
watchSessionTitle,
|
|
@@ -376,10 +382,190 @@ export function useChatStream(params) {
|
|
|
376
382
|
setError,
|
|
377
383
|
postAbortForTurn,
|
|
378
384
|
]);
|
|
385
|
+
const queueDeferredTurn = useCallback(async (input) => {
|
|
386
|
+
const activeSession = refs.sessionRef.current;
|
|
387
|
+
if (!activeSession || (!input.text.trim() && input.images.length === 0))
|
|
388
|
+
return { action: "handled" };
|
|
389
|
+
const existing = thread.deferredTurn;
|
|
390
|
+
showNotice("正在排队…");
|
|
391
|
+
const acceptDeferred = (deferredTurn) => {
|
|
392
|
+
setDeferredTurn(deferredTurn);
|
|
393
|
+
setInput("");
|
|
394
|
+
setPendingImages([]);
|
|
395
|
+
setError(null);
|
|
396
|
+
showNotice(existing ? "已更新停止后发送的消息" : "已排队,将在上一轮停止后自动发送");
|
|
397
|
+
};
|
|
398
|
+
const acceptDispatched = () => {
|
|
399
|
+
setDeferredTurn(null);
|
|
400
|
+
setInput("");
|
|
401
|
+
setPendingImages([]);
|
|
402
|
+
setError(null);
|
|
403
|
+
showNotice("消息已进入发送");
|
|
404
|
+
};
|
|
405
|
+
const putOnce = async () => {
|
|
406
|
+
const response = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(activeSession.sessionId)}/deferred-turn`, {
|
|
407
|
+
method: "PUT",
|
|
408
|
+
headers: {
|
|
409
|
+
"Content-Type": "application/json",
|
|
410
|
+
"x-loop-console-confirmation": confirmationToken(),
|
|
411
|
+
},
|
|
412
|
+
credentials: "include",
|
|
413
|
+
body: JSON.stringify({
|
|
414
|
+
clientRequestId: input.clientRequestId,
|
|
415
|
+
blockedByTurnId: input.blockedByTurnId,
|
|
416
|
+
text: input.text,
|
|
417
|
+
images: input.images,
|
|
418
|
+
...(existing ? { expectedRevision: existing.revision } : {}),
|
|
419
|
+
}),
|
|
420
|
+
});
|
|
421
|
+
const body = (await response.json().catch(() => ({})));
|
|
422
|
+
return { response, body };
|
|
423
|
+
};
|
|
424
|
+
try {
|
|
425
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
426
|
+
const { response, body } = await putOnce();
|
|
427
|
+
if (response.ok && body.accepted && body.deferredTurn) {
|
|
428
|
+
acceptDeferred(body.deferredTurn);
|
|
429
|
+
return { action: "handled" };
|
|
430
|
+
}
|
|
431
|
+
if (body.deferredTurn)
|
|
432
|
+
setDeferredTurn(body.deferredTurn);
|
|
433
|
+
if (body.error?.code !== "TURN_NOT_STOPPING") {
|
|
434
|
+
setError(body.error?.message ??
|
|
435
|
+
`待发送消息未被服务端接受(HTTP ${response.status})`);
|
|
436
|
+
return { action: "handled" };
|
|
437
|
+
}
|
|
438
|
+
const canonical = await fetchSessionState(activeSession.sessionId);
|
|
439
|
+
const decision = reconcileDeferredSubmissionFromState({
|
|
440
|
+
clientRequestId: input.clientRequestId,
|
|
441
|
+
blockedByTurnId: input.blockedByTurnId,
|
|
442
|
+
state: canonical ?? undefined,
|
|
443
|
+
});
|
|
444
|
+
if (decision.action === "deferred") {
|
|
445
|
+
acceptDeferred(decision.deferredTurn);
|
|
446
|
+
return { action: "handled" };
|
|
447
|
+
}
|
|
448
|
+
if (decision.action === "accepted") {
|
|
449
|
+
acceptDispatched();
|
|
450
|
+
return { action: "handled" };
|
|
451
|
+
}
|
|
452
|
+
if (decision.action === "retry-deferred" && attempt === 0)
|
|
453
|
+
continue;
|
|
454
|
+
if (decision.action === "send-now" && !existing) {
|
|
455
|
+
showNotice("上一轮已停止,正在发送当前消息…");
|
|
456
|
+
return { action: "send-now" };
|
|
457
|
+
}
|
|
458
|
+
setError(body.error?.message ?? "停止状态已变化,当前消息仍保留在输入框");
|
|
459
|
+
return { action: "handled" };
|
|
460
|
+
}
|
|
461
|
+
return { action: "handled" };
|
|
462
|
+
}
|
|
463
|
+
catch (error) {
|
|
464
|
+
const canonical = await fetchSessionState(activeSession.sessionId);
|
|
465
|
+
const decision = reconcileDeferredSubmissionFromState({
|
|
466
|
+
clientRequestId: input.clientRequestId,
|
|
467
|
+
blockedByTurnId: input.blockedByTurnId,
|
|
468
|
+
state: canonical ?? undefined,
|
|
469
|
+
});
|
|
470
|
+
if (decision.action === "deferred") {
|
|
471
|
+
acceptDeferred(decision.deferredTurn);
|
|
472
|
+
return { action: "handled" };
|
|
473
|
+
}
|
|
474
|
+
if (decision.action === "accepted") {
|
|
475
|
+
acceptDispatched();
|
|
476
|
+
return { action: "handled" };
|
|
477
|
+
}
|
|
478
|
+
if (decision.action === "send-now" && !existing) {
|
|
479
|
+
showNotice("上一轮已停止,正在发送当前消息…");
|
|
480
|
+
return { action: "send-now" };
|
|
481
|
+
}
|
|
482
|
+
setError(error instanceof Error
|
|
483
|
+
? `无法确认消息是否已排队:${error.message}`
|
|
484
|
+
: "无法确认消息是否已排队,请刷新后查看服务端状态");
|
|
485
|
+
return { action: "handled" };
|
|
486
|
+
}
|
|
487
|
+
}, [
|
|
488
|
+
origin,
|
|
489
|
+
refs,
|
|
490
|
+
thread.deferredTurn,
|
|
491
|
+
setDeferredTurn,
|
|
492
|
+
setInput,
|
|
493
|
+
setPendingImages,
|
|
494
|
+
setError,
|
|
495
|
+
showNotice,
|
|
496
|
+
fetchSessionState,
|
|
497
|
+
]);
|
|
498
|
+
const cancelDeferredTurn = useCallback(async () => {
|
|
499
|
+
const activeSession = refs.sessionRef.current;
|
|
500
|
+
const deferred = thread.deferredTurn;
|
|
501
|
+
if (!activeSession || !deferred)
|
|
502
|
+
return;
|
|
503
|
+
try {
|
|
504
|
+
const response = await fetch(`${origin}/api/operator/v1/chat/sessions/${encodeURIComponent(activeSession.sessionId)}/deferred-turn`, {
|
|
505
|
+
method: "DELETE",
|
|
506
|
+
headers: {
|
|
507
|
+
"Content-Type": "application/json",
|
|
508
|
+
"x-loop-console-confirmation": confirmationToken(),
|
|
509
|
+
},
|
|
510
|
+
credentials: "include",
|
|
511
|
+
body: JSON.stringify({
|
|
512
|
+
clientRequestId: deferred.clientRequestId,
|
|
513
|
+
expectedRevision: deferred.revision,
|
|
514
|
+
}),
|
|
515
|
+
});
|
|
516
|
+
const body = (await response.json().catch(() => ({})));
|
|
517
|
+
if (!response.ok || !body.cancelled) {
|
|
518
|
+
setError(body.error?.message ?? "待发送消息撤回失败");
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
setDeferredTurn(null);
|
|
522
|
+
setInput(body.deferredTurn?.text ?? "");
|
|
523
|
+
setPendingImages(body.deferredTurn?.images ?? []);
|
|
524
|
+
setError(null);
|
|
525
|
+
showNotice("待发送消息已移回输入框");
|
|
526
|
+
}
|
|
527
|
+
catch (error) {
|
|
528
|
+
setError(error instanceof Error ? error.message : String(error));
|
|
529
|
+
}
|
|
530
|
+
}, [
|
|
531
|
+
origin,
|
|
532
|
+
refs,
|
|
533
|
+
thread.deferredTurn,
|
|
534
|
+
setDeferredTurn,
|
|
535
|
+
setInput,
|
|
536
|
+
setPendingImages,
|
|
537
|
+
setError,
|
|
538
|
+
showNotice,
|
|
539
|
+
]);
|
|
379
540
|
const sendPrompt = useCallback(async (text, images) => {
|
|
541
|
+
const initialSession = refs.sessionRef.current;
|
|
542
|
+
if (!initialSession || (!text.trim() && images.length === 0))
|
|
543
|
+
return;
|
|
544
|
+
const ownership = ownershipBagFromRefs(refs);
|
|
545
|
+
const route = decideSubmissionRoute({
|
|
546
|
+
turnPhase: turnPhaseFromOwnership(ownership),
|
|
547
|
+
blockedByTurnId: ownership.stoppingTurnId ?? ownership.activeTurnId,
|
|
548
|
+
});
|
|
549
|
+
const clientRequestId = requestId("chat-submit");
|
|
550
|
+
if (route.action === "hold") {
|
|
551
|
+
if (turnPhaseFromOwnership(ownership) === "stopping")
|
|
552
|
+
showNotice("正在确认上一轮停止状态,当前消息已保留");
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
if (route.action === "queue-deferred") {
|
|
556
|
+
const deferred = await queueDeferredTurn({
|
|
557
|
+
text,
|
|
558
|
+
images,
|
|
559
|
+
clientRequestId,
|
|
560
|
+
blockedByTurnId: route.blockedByTurnId,
|
|
561
|
+
});
|
|
562
|
+
if (deferred.action !== "send-now")
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
380
565
|
const activeSession = refs.sessionRef.current;
|
|
381
566
|
const generation = refs.sessionGenerationRef.current;
|
|
382
567
|
if (!activeSession ||
|
|
568
|
+
activeSession.sessionId !== initialSession.sessionId ||
|
|
383
569
|
(!text.trim() && images.length === 0) ||
|
|
384
570
|
refs.streamingRef.current ||
|
|
385
571
|
refs.submitInFlightRef.current ||
|
|
@@ -389,7 +575,6 @@ export function useChatStream(params) {
|
|
|
389
575
|
// AC-001: acquire synchronous submit ownership before the first await.
|
|
390
576
|
refs.submitInFlightRef.current = true;
|
|
391
577
|
const submitGeneration = ++refs.submitGenerationRef.current;
|
|
392
|
-
const clientRequestId = requestId("chat-submit");
|
|
393
578
|
const userMessageId = requestId("u");
|
|
394
579
|
const assistantId = requestId("a");
|
|
395
580
|
const draftText = text.trim();
|
|
@@ -437,6 +622,7 @@ export function useChatStream(params) {
|
|
|
437
622
|
setAutoScroll(true);
|
|
438
623
|
setError(null);
|
|
439
624
|
refs.streamingRef.current = true;
|
|
625
|
+
setTurnPhase("running");
|
|
440
626
|
refs.assistantIdRef.current = assistantId;
|
|
441
627
|
setStreaming(true);
|
|
442
628
|
setStreamingAssistantId(assistantId);
|
|
@@ -532,7 +718,9 @@ export function useChatStream(params) {
|
|
|
532
718
|
setAutoScroll,
|
|
533
719
|
setError,
|
|
534
720
|
setStreaming,
|
|
721
|
+
setTurnPhase,
|
|
535
722
|
setStreamingAssistantId,
|
|
723
|
+
queueDeferredTurn,
|
|
536
724
|
]);
|
|
537
725
|
const handleStop = useCallback(async () => {
|
|
538
726
|
// AC-R5-001: durable stopRequested is set synchronously before any await so
|
|
@@ -548,6 +736,7 @@ export function useChatStream(params) {
|
|
|
548
736
|
notice: noticeSink(),
|
|
549
737
|
});
|
|
550
738
|
applyOwnershipBagToRefs(refs, bag);
|
|
739
|
+
setTurnPhase("stopping");
|
|
551
740
|
// Immediate abort already scheduled for a known turnId — coalesce via
|
|
552
741
|
// abortPostedTurnIds; stopInFlight only gates concurrent in-flight POSTs.
|
|
553
742
|
if (stopResult.abortedTurnId) {
|
|
@@ -656,6 +845,9 @@ export function useChatStream(params) {
|
|
|
656
845
|
},
|
|
657
846
|
});
|
|
658
847
|
applyOwnershipBagToRefs(refs, bag);
|
|
848
|
+
setTurnPhase(turnPhaseFromOwnership(bag));
|
|
849
|
+
if (state && "deferredTurn" in state)
|
|
850
|
+
setDeferredTurn(state.deferredTurn ?? null);
|
|
659
851
|
if (result === "bound") {
|
|
660
852
|
// US-BUSY-ACTIVE-BOUND: keep streaming/busy with same id.
|
|
661
853
|
setStreaming(true);
|
|
@@ -755,6 +947,7 @@ export function useChatStream(params) {
|
|
|
755
947
|
refs,
|
|
756
948
|
session,
|
|
757
949
|
streaming,
|
|
950
|
+
setTurnPhase,
|
|
758
951
|
markIdleIfCurrent,
|
|
759
952
|
releaseSubmitOwnership,
|
|
760
953
|
postAbortForTurn,
|
|
@@ -776,5 +969,5 @@ export function useChatStream(params) {
|
|
|
776
969
|
}
|
|
777
970
|
};
|
|
778
971
|
}, [session, recoveryVersion, refs, startEventsStream]);
|
|
779
|
-
return { sendPrompt, handleStop, openEventsStream };
|
|
972
|
+
return { sendPrompt, handleStop, cancelDeferredTurn, openEventsStream };
|
|
780
973
|
}
|
|
@@ -31,6 +31,8 @@ export function useChatThread(params) {
|
|
|
31
31
|
const [contextLoading, setContextLoading] = useState(false);
|
|
32
32
|
const [usage, setUsage] = useState(EMPTY_USAGE);
|
|
33
33
|
const [streaming, setStreaming] = useState(false);
|
|
34
|
+
const [turnPhase, setTurnPhase] = useState("idle");
|
|
35
|
+
const [deferredTurn, setDeferredTurn] = useState(null);
|
|
34
36
|
/** Assistant bubble id for the in-flight turn; only this row shows the streaming cursor. */
|
|
35
37
|
const [streamingAssistantId, setStreamingAssistantId] = useState(null);
|
|
36
38
|
const [showSystemInMessages] = useState(false);
|
|
@@ -44,6 +46,8 @@ export function useChatThread(params) {
|
|
|
44
46
|
setHumanGateCards,
|
|
45
47
|
setArtifactCards,
|
|
46
48
|
setStreaming,
|
|
49
|
+
setTurnPhase,
|
|
50
|
+
setDeferredTurn,
|
|
47
51
|
setStreamingAssistantId,
|
|
48
52
|
setUnread,
|
|
49
53
|
setOperationCards,
|
|
@@ -190,6 +194,8 @@ export function useChatThread(params) {
|
|
|
190
194
|
setTaskContext(null);
|
|
191
195
|
setUsage(EMPTY_USAGE);
|
|
192
196
|
setStreaming(false);
|
|
197
|
+
setTurnPhase("idle");
|
|
198
|
+
setDeferredTurn(null);
|
|
193
199
|
setStreamingAssistantId(null);
|
|
194
200
|
setUnread(0);
|
|
195
201
|
setHistoryLimit(50);
|
|
@@ -207,6 +213,8 @@ export function useChatThread(params) {
|
|
|
207
213
|
setUsage(EMPTY_USAGE);
|
|
208
214
|
refs.usageResponseKeysRef.current = new Set();
|
|
209
215
|
setStreaming(next.streaming);
|
|
216
|
+
setTurnPhase(next.turnPhase ?? (next.streaming ? "running" : "idle"));
|
|
217
|
+
setDeferredTurn(next.deferredTurn ?? null);
|
|
210
218
|
setStreamingAssistantId(null);
|
|
211
219
|
setUnread(0);
|
|
212
220
|
setHistoryLimit(50);
|
|
@@ -228,6 +236,8 @@ export function useChatThread(params) {
|
|
|
228
236
|
const activeTurnId = body.session.activeTurnId ?? null;
|
|
229
237
|
refs.activeTurnIdRef.current = activeTurnId;
|
|
230
238
|
refs.streamingRef.current = Boolean(activeTurnId);
|
|
239
|
+
setTurnPhase(body.session.turnPhase ?? (activeTurnId ? "running" : "idle"));
|
|
240
|
+
setDeferredTurn(body.session.deferredTurn ?? null);
|
|
231
241
|
// Resume the events stream from the snapshot tail so the next
|
|
232
242
|
// reconnect never immediately re-expires (AC-003). The refs below
|
|
233
243
|
// are reset so any follow-up tail replay re-applies cleanly.
|
|
@@ -246,7 +256,15 @@ export function useChatThread(params) {
|
|
|
246
256
|
// trigger another reconcile and retry (best-effort recovery).
|
|
247
257
|
return false;
|
|
248
258
|
}
|
|
249
|
-
}, [
|
|
259
|
+
}, [
|
|
260
|
+
origin,
|
|
261
|
+
refs,
|
|
262
|
+
setMessages,
|
|
263
|
+
setToolCalls,
|
|
264
|
+
setStreamingAssistantId,
|
|
265
|
+
setTurnPhase,
|
|
266
|
+
setDeferredTurn,
|
|
267
|
+
]);
|
|
250
268
|
return {
|
|
251
269
|
messages,
|
|
252
270
|
setMessages,
|
|
@@ -265,6 +283,10 @@ export function useChatThread(params) {
|
|
|
265
283
|
usage,
|
|
266
284
|
streaming,
|
|
267
285
|
setStreaming,
|
|
286
|
+
turnPhase,
|
|
287
|
+
setTurnPhase,
|
|
288
|
+
deferredTurn,
|
|
289
|
+
setDeferredTurn,
|
|
268
290
|
streamingAssistantId,
|
|
269
291
|
setStreamingAssistantId,
|
|
270
292
|
visibleMessages,
|
|
@@ -2,7 +2,7 @@ import { readFile } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { redactSecrets, truncateUtf8Preview } from "../../shared/preview.js";
|
|
4
4
|
import { resolveDagNodeSkills } from "../../workflows/dag/skills.js";
|
|
5
|
-
import { resolveDagNodePromptRedacted, resolveDagRunJson, } from "./dag-run-artifacts.js";
|
|
5
|
+
import { resolveDagNodePromptRedacted, resolveDagRunArtifact, resolveDagRunJson, } from "./dag-run-artifacts.js";
|
|
6
6
|
export const NODE_INPUT_SCHEMA_VERSION = 1;
|
|
7
7
|
export const NODE_INPUT_PREVIEW_MAX_BYTES = 64 * 1024;
|
|
8
8
|
const SHELL_GATE_KEYS = [
|
|
@@ -33,6 +33,7 @@ function unavailableBody(dagRunId, nodeId, reason, warnings = []) {
|
|
|
33
33
|
executorInput: null,
|
|
34
34
|
boundaries: null,
|
|
35
35
|
promptFingerprint: null,
|
|
36
|
+
piExtensionRunFacts: null,
|
|
36
37
|
warnings,
|
|
37
38
|
};
|
|
38
39
|
}
|
|
@@ -137,7 +138,7 @@ function resolveModelHint(spec, task) {
|
|
|
137
138
|
}
|
|
138
139
|
return null;
|
|
139
140
|
}
|
|
140
|
-
function projectTask(dagRunId, nodeId, lifecycle, spec, task, fingerprint, warnings) {
|
|
141
|
+
function projectTask(dagRunId, nodeId, lifecycle, spec, task, fingerprint, piExtensionRunFacts, warnings) {
|
|
141
142
|
const defaults = spec.defaults && typeof spec.defaults === "object"
|
|
142
143
|
? spec.defaults
|
|
143
144
|
: undefined;
|
|
@@ -268,6 +269,7 @@ function projectTask(dagRunId, nodeId, lifecycle, spec, task, fingerprint, warni
|
|
|
268
269
|
kind,
|
|
269
270
|
skills,
|
|
270
271
|
modelHint: resolveModelHint(spec, task),
|
|
272
|
+
piExtensions: redactStringList(task.piExtensions),
|
|
271
273
|
shell,
|
|
272
274
|
static: staticBlock,
|
|
273
275
|
},
|
|
@@ -284,9 +286,72 @@ function projectTask(dagRunId, nodeId, lifecycle, spec, task, fingerprint, warni
|
|
|
284
286
|
: null,
|
|
285
287
|
},
|
|
286
288
|
promptFingerprint: fingerprint,
|
|
289
|
+
piExtensionRunFacts,
|
|
287
290
|
warnings,
|
|
288
291
|
};
|
|
289
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* Load run-time Pi extension facts from <node>/pi-extensions.json (plan
|
|
295
|
+
* 2026-08-22 R2). Returns null when the node declares no piExtensions
|
|
296
|
+
* (no expected artifact). Never throws: unreadable/corrupt artifacts degrade
|
|
297
|
+
* to parseError stubs so the node-input endpoint stays 200.
|
|
298
|
+
*/
|
|
299
|
+
async function loadPiExtensionRunFacts(repoRoot, dagRunId, nodeId, declared, warnings) {
|
|
300
|
+
if (declared.length === 0)
|
|
301
|
+
return null;
|
|
302
|
+
const artifact = path.posix.join(nodeId, "pi-extensions.json");
|
|
303
|
+
const resolved = resolveDagRunArtifact(repoRoot, dagRunId, [
|
|
304
|
+
nodeId,
|
|
305
|
+
"pi-extensions.json",
|
|
306
|
+
]);
|
|
307
|
+
if (!resolved.ok) {
|
|
308
|
+
if (resolved.reason === "ambiguous") {
|
|
309
|
+
warnings.push("Ambiguous pi-extensions.json lifecycle copies.");
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
artifact,
|
|
313
|
+
exists: false,
|
|
314
|
+
requested: [...declared],
|
|
315
|
+
resolved: [],
|
|
316
|
+
missing: [],
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
const empty = {
|
|
320
|
+
artifact,
|
|
321
|
+
exists: false,
|
|
322
|
+
requested: [...declared],
|
|
323
|
+
resolved: [],
|
|
324
|
+
missing: [],
|
|
325
|
+
};
|
|
326
|
+
try {
|
|
327
|
+
const raw = JSON.parse(await readFile(resolved.result.absolutePath, "utf-8"));
|
|
328
|
+
const resolvedList = Array.isArray(raw.resolved) ? raw.resolved : [];
|
|
329
|
+
const missingList = Array.isArray(raw.missing) ? raw.missing : [];
|
|
330
|
+
const projected = resolvedList
|
|
331
|
+
.filter((entry) => Boolean(entry) && typeof entry === "object" && !Array.isArray(entry))
|
|
332
|
+
.map((entry) => ({
|
|
333
|
+
id: redactSecrets(String(entry.id ?? "")),
|
|
334
|
+
packageName: redactSecrets(String(entry.packageName ?? "")),
|
|
335
|
+
version: redactSecrets(String(entry.version ?? "")),
|
|
336
|
+
}));
|
|
337
|
+
const missingProjected = missingList
|
|
338
|
+
.filter((entry) => Boolean(entry) && typeof entry === "object" && !Array.isArray(entry))
|
|
339
|
+
.map((entry) => ({
|
|
340
|
+
id: redactSecrets(String(entry.id ?? "")),
|
|
341
|
+
reason: redactSecrets(String(entry.reason ?? "")),
|
|
342
|
+
}));
|
|
343
|
+
return {
|
|
344
|
+
artifact,
|
|
345
|
+
exists: true,
|
|
346
|
+
requested: redactStringList(raw.requested ?? declared),
|
|
347
|
+
resolved: projected,
|
|
348
|
+
missing: missingProjected,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
return { ...empty, parseError: "read-failed" };
|
|
353
|
+
}
|
|
354
|
+
}
|
|
290
355
|
async function loadPromptFingerprint(repoRoot, dagRunId, nodeId, warnings) {
|
|
291
356
|
const resolved = resolveDagNodePromptRedacted(repoRoot, dagRunId, nodeId);
|
|
292
357
|
if (!resolved.ok) {
|
|
@@ -415,8 +480,12 @@ export async function buildDagNodeInput(repoRoot, dagRunId, nodeId) {
|
|
|
415
480
|
}
|
|
416
481
|
const warnings = [];
|
|
417
482
|
const fingerprint = await loadPromptFingerprint(repoRoot, dagRunId, nodeId, warnings);
|
|
483
|
+
const declaredPiExtensions = Array.isArray(task.piExtensions)
|
|
484
|
+
? task.piExtensions.filter((item) => typeof item === "string")
|
|
485
|
+
: [];
|
|
486
|
+
const piExtensionRunFacts = await loadPiExtensionRunFacts(repoRoot, dagRunId, nodeId, declaredPiExtensions, warnings);
|
|
418
487
|
// Reject unknown nested leakage by projecting only through allowlist helpers.
|
|
419
|
-
const body = projectTask(dagRunId, nodeId, runResolved.result.lifecycle, spec, task, fingerprint, warnings);
|
|
488
|
+
const body = projectTask(dagRunId, nodeId, runResolved.result.lifecycle, spec, task, fingerprint, piExtensionRunFacts, warnings);
|
|
420
489
|
// Defense-in-depth: ensure raw task never sneaks in.
|
|
421
490
|
if ("task" in body) {
|
|
422
491
|
return {
|
|
@@ -14,6 +14,10 @@ const ATTEMPT_FAILURE_LABELS = {
|
|
|
14
14
|
timeout: "执行超时",
|
|
15
15
|
"nonzero-exit": "命令执行失败",
|
|
16
16
|
"invalid-output": "输出格式无效",
|
|
17
|
+
"structured-output-truncated": "结构化输出被截断",
|
|
18
|
+
"output-too-large": "输出超出体积上限",
|
|
19
|
+
"structured-repair-exhausted": "结构化修复耗尽",
|
|
20
|
+
"governance-blocked": "治理规则阻断",
|
|
17
21
|
"empty-output": "输出为空",
|
|
18
22
|
auth: "认证失败",
|
|
19
23
|
quota: "额度或限流",
|
|
@@ -76,8 +76,8 @@ export const uiState = {
|
|
|
76
76
|
dagNodeExecutionOutputLoading: false,
|
|
77
77
|
dagNodeExecutionOutputError: null,
|
|
78
78
|
dagNodeExecutionOutputKey: null,
|
|
79
|
-
/** When true,「执行过程」展开 message_start/end
|
|
80
|
-
sessionTimelineShowProtocol:
|
|
79
|
+
/** When true,「执行过程」展开 message_start/end 原始协议事件。默认展开,用户可点选隐藏。 */
|
|
80
|
+
sessionTimelineShowProtocol: true,
|
|
81
81
|
/** Open <details> keys restored across timeline re-renders. */
|
|
82
82
|
sessionTimelineOpenDetails: null,
|
|
83
83
|
lastSnapshot: null,
|
|
@@ -725,6 +725,11 @@ function renderDagNodeInputDetail(content, data) {
|
|
|
725
725
|
}
|
|
726
726
|
section.appendChild(dl);
|
|
727
727
|
appendCodeList(section, "skills", data.executorInput.skills);
|
|
728
|
+
appendCodeList(
|
|
729
|
+
section,
|
|
730
|
+
"piExtensions(声明)",
|
|
731
|
+
data.executorInput.piExtensions,
|
|
732
|
+
);
|
|
728
733
|
if (data.executorInput.shell) {
|
|
729
734
|
const shell = data.executorInput.shell;
|
|
730
735
|
const shellDl = el("dl", "node-input-summary");
|
|
@@ -799,6 +804,52 @@ function renderDagNodeInputDetail(content, data) {
|
|
|
799
804
|
wrap.appendChild(section);
|
|
800
805
|
}
|
|
801
806
|
|
|
807
|
+
if (data.piExtensionRunFacts) {
|
|
808
|
+
const facts = data.piExtensionRunFacts;
|
|
809
|
+
const section = el("section", "node-input-section");
|
|
810
|
+
section.appendChild(el("h4", null, "Pi 扩展运行事实"));
|
|
811
|
+
if (!facts.exists) {
|
|
812
|
+
section.appendChild(
|
|
813
|
+
el(
|
|
814
|
+
"p",
|
|
815
|
+
"muted",
|
|
816
|
+
facts.parseError
|
|
817
|
+
? `产物 ${facts.artifact} 读取失败(${facts.parseError})。`
|
|
818
|
+
: `产物 ${facts.artifact} 尚未生成(节点未执行或缺失)。`,
|
|
819
|
+
),
|
|
820
|
+
);
|
|
821
|
+
} else {
|
|
822
|
+
const dl = el("dl", "node-input-summary");
|
|
823
|
+
appendMetaRow(
|
|
824
|
+
dl,
|
|
825
|
+
"resolved",
|
|
826
|
+
facts.resolved
|
|
827
|
+
.map((entry) => `${entry.id} → ${entry.packageName}@${entry.version}`)
|
|
828
|
+
.join(", ") || "(none)",
|
|
829
|
+
null,
|
|
830
|
+
);
|
|
831
|
+
section.appendChild(dl);
|
|
832
|
+
appendCodeList(
|
|
833
|
+
section,
|
|
834
|
+
"requested",
|
|
835
|
+
facts.requested,
|
|
836
|
+
);
|
|
837
|
+
for (const missingEntry of facts.missing ?? []) {
|
|
838
|
+
section.appendChild(
|
|
839
|
+
el(
|
|
840
|
+
"p",
|
|
841
|
+
"node-input-warning",
|
|
842
|
+
`降级:${missingEntry.id} 未加载(${missingEntry.reason || "unknown"})`,
|
|
843
|
+
),
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
if ((facts.missing ?? []).length === 0) {
|
|
847
|
+
section.appendChild(el("p", "muted", "全部声明扩展均已解析加载。"));
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
wrap.appendChild(section);
|
|
851
|
+
}
|
|
852
|
+
|
|
802
853
|
if (data.promptFingerprint?.exists) {
|
|
803
854
|
const section = el("section", "node-input-section");
|
|
804
855
|
section.appendChild(el("h4", null, "Assembled prompt 指纹"));
|
|
@@ -439,12 +439,25 @@ function ensureOpenDetailsSet() {
|
|
|
439
439
|
return uiState.sessionTimelineOpenDetails;
|
|
440
440
|
}
|
|
441
441
|
|
|
442
|
-
function
|
|
442
|
+
function closedDetailKey(detailKey) {
|
|
443
|
+
return `${detailKey}::user-closed`;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function bindDetailsToggle(details, detailKey, defaultOpen = false) {
|
|
443
447
|
const openSet = ensureOpenDetailsSet();
|
|
444
|
-
if (
|
|
448
|
+
if (defaultOpen) {
|
|
449
|
+
if (!openSet.has(closedDetailKey(detailKey))) details.open = true;
|
|
450
|
+
} else if (openSet.has(detailKey)) {
|
|
451
|
+
details.open = true;
|
|
452
|
+
}
|
|
445
453
|
details.addEventListener("toggle", () => {
|
|
446
|
-
if (details.open)
|
|
447
|
-
|
|
454
|
+
if (details.open) {
|
|
455
|
+
openSet.add(detailKey);
|
|
456
|
+
openSet.delete(closedDetailKey(detailKey));
|
|
457
|
+
} else {
|
|
458
|
+
openSet.delete(detailKey);
|
|
459
|
+
openSet.add(closedDetailKey(detailKey));
|
|
460
|
+
}
|
|
448
461
|
});
|
|
449
462
|
}
|
|
450
463
|
|
|
@@ -599,7 +612,7 @@ function renderProtocolCard(item) {
|
|
|
599
612
|
const details = document.createElement("details");
|
|
600
613
|
details.className = "process-timeline-details";
|
|
601
614
|
details.dataset.detailKey = item.detailKey;
|
|
602
|
-
bindDetailsToggle(details, item.detailKey);
|
|
615
|
+
bindDetailsToggle(details, item.detailKey, true);
|
|
603
616
|
const summary = document.createElement("summary");
|
|
604
617
|
summary.className = "process-timeline-summary";
|
|
605
618
|
summary.textContent = `消息收发 · ${item.protocolCount} 条协议事件`;
|