@tea-agent/loop-agent 0.43.0-next.1 → 0.43.0-next.2
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 +18 -0
- package/dist/application/evaluation/budget.js +19 -1
- package/dist/build-stamp.json +3 -3
- package/dist/executors/dag-pi-executor.js +762 -252
- package/dist/executors/pi-executor.js +20 -1
- package/dist/executors/pi-sdk-executor.js +64 -1
- package/dist/executors/shell-executor.js +5 -2
- package/dist/shared/frontend-execution-policy.js +22 -0
- package/dist/task/config-types.js +4 -0
- package/dist/task/source-prepare/ledger-reconciliation.js +2 -2
- package/dist/task/source-prepare/ledger-review.js +6 -9
- package/dist/task/source-prepare/semantic-intake.js +16 -26
- package/dist/task/source-prepare/source-fidelity-pi.js +26 -7
- package/dist/worker/observe/node-transparency.js +81 -72
- package/dist/worker/observe/routes.js +20 -1
- package/dist/worker/observe/static/dag-inspector-humanize.js +3 -0
- package/dist/worker/observe/static/dom.js +20 -1
- package/dist/worker/observe/static/format-pool.d.ts +2 -0
- package/dist/worker/observe/static/format-pool.js +6 -0
- package/dist/worker/observe/static/format.js +7 -0
- package/dist/worker/observe/static/inspect-workspace.js +34 -7
- package/dist/worker/observe/static/inspector-submission.js +32 -0
- package/dist/worker/observe/static/kpi.js +1 -0
- package/dist/worker/observe/static/relations.js +2 -0
- package/dist/worker/observe/static/router.js +13 -0
- package/dist/worker/observe/static/run-processing.js +2 -0
- package/dist/worker/observe/static/shell-chrome.js +36 -3
- package/dist/worker/observe/static/state.js +35 -2
- package/dist/worker/observe/static/styles.css +260 -39
- package/dist/worker/observe/static/task-failure-labels.d.ts +4 -0
- package/dist/worker/observe/static/task-failure-labels.js +67 -0
- package/dist/worker/observe/static/task-history.js +12 -0
- package/dist/worker/observe/static/views/batch.js +6 -13
- package/dist/worker/observe/static/views/dag-graph.js +50 -3
- package/dist/worker/observe/static/views/dag-inspector.js +746 -265
- package/dist/worker/observe/static/views/dag-trajectory.js +3 -0
- package/dist/worker/observe/static/views/dag.d.ts +6 -0
- package/dist/worker/observe/static/views/dag.js +48 -10
- package/dist/worker/observe/static/views/dags.js +2 -0
- package/dist/worker/observe/static/views/dashboard.js +21 -12
- package/dist/worker/observe/static/views/failures.js +21 -11
- package/dist/worker/observe/static/views/feature.js +11 -29
- package/dist/worker/observe/static/views/pool.js +37 -28
- package/dist/worker/observe/static/views/run.js +48 -5
- package/dist/worker/observe/static/views/session-timeline.js +189 -240
- package/dist/worker/observe/static/views/task.js +81 -62
- package/dist/workflows/dag/budget-enforcement.js +53 -3
- package/dist/workflows/dag/frontend-durable-tools.js +193 -0
- package/dist/workflows/dag/frontend-execution-groups.js +24 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +9 -0
- package/dist/workflows/dag/frontend-input-projection.js +76 -0
- package/dist/workflows/dag/frontend-plan-render.js +10 -3
- package/dist/workflows/dag/frontend-recovery-controller.js +7 -7
- package/dist/workflows/dag/frontend-recovery-lineage.js +13 -0
- package/dist/workflows/dag/frontend-recovery-run.js +4 -0
- package/dist/workflows/dag/frontend-review-scopes.js +117 -0
- package/dist/workflows/dag/frontend-session-budget.js +249 -0
- package/dist/workflows/dag/frontend-shadow-dual-write.js +20 -2
- package/dist/workflows/dag/frontend-test-execution-evidence.js +3 -2
- package/dist/workflows/dag/frontend-typed-event-store.js +11 -0
- package/dist/workflows/dag/init-hybrid.js +16 -10
- package/dist/workflows/dag/node-execution.js +32 -155
- package/dist/workflows/dag/prompt.js +4 -0
- package/dist/workflows/dag/rerun-plan.js +7 -1
- package/dist/workflows/dag/runner.js +26 -1
- package/dist/workflows/dag/types.js +6 -0
- package/docs/operations/README.md +1 -0
- package/docs/templates/frontend-design-contract.md +4 -4
- package/docs/templates/frontend-implementation-contract.schema.json +34 -2
- package/docs/templates/frontend-implementation-dag.json +5 -5
- package/package.json +1 -1
- package/skills/frontend-contract/SKILL.md +2 -1
- package/skills/frontend-contract/references/contract-protocol.md +19 -3
- package/skills/frontend-design-review/SKILL.md +12 -11
- package/skills/frontend-plan/SKILL.md +2 -2
- package/skills/frontend-plan/references/decision-contract.md +18 -5
- package/skills/frontend-review/SKILL.md +10 -11
- package/skills/frontend-scout/references/scout-evidence.md +4 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { collectFrontendUsage, createFrontendReadObservation, observeFrontendAttempt } from "../workflows/dag/frontend-session-budget.js";
|
|
1
2
|
import { spawn, spawnSync } from "node:child_process";
|
|
2
3
|
import { existsSync } from "node:fs";
|
|
3
4
|
import { homedir } from "node:os";
|
|
@@ -453,6 +454,9 @@ export function extractPiEventStopReason(event) {
|
|
|
453
454
|
return undefined;
|
|
454
455
|
}
|
|
455
456
|
export class PiJsonlStreamCollector {
|
|
457
|
+
frontendReads = createFrontendReadObservation();
|
|
458
|
+
frontendUsageEvents = [];
|
|
459
|
+
frontendObservation() { return { readObservation: this.frontendReads.snapshot(), usageObservation: collectFrontendUsage(this.frontendUsageEvents) }; }
|
|
456
460
|
lineBuffer = "";
|
|
457
461
|
discardUntilNewline = false;
|
|
458
462
|
assistantText = "";
|
|
@@ -518,6 +522,13 @@ export class PiJsonlStreamCollector {
|
|
|
518
522
|
}
|
|
519
523
|
}
|
|
520
524
|
consumeEvent(event) {
|
|
525
|
+
try {
|
|
526
|
+
this.frontendReads.observe(event);
|
|
527
|
+
const message = isRecord(event.message) ? event.message : undefined;
|
|
528
|
+
if (event.usage || message?.usage || event.tokenUsage)
|
|
529
|
+
this.frontendUsageEvents.push({ responseId: event.responseId, messageId: event.messageId, usage: event.usage, tokenUsage: event.tokenUsage, message: message ? { role: message.role, id: message.id, responseId: message.responseId, usage: message.usage } : undefined });
|
|
530
|
+
}
|
|
531
|
+
catch { /* observation only */ }
|
|
521
532
|
const stopReason = extractPiEventStopReason(event);
|
|
522
533
|
if (stopReason === "length" || !this.stopReason) {
|
|
523
534
|
this.stopReason = stopReason ?? this.stopReason;
|
|
@@ -730,6 +741,8 @@ function writerPolicyFailureResult(options, message, extras) {
|
|
|
730
741
|
export async function executePiStep(options) {
|
|
731
742
|
const backendMode = resolvePiBackend();
|
|
732
743
|
const writerRequiresSdk = options.writerToolPolicy?.requireSdk === true;
|
|
744
|
+
if (options.reserveProviderRequest && backendMode === "cli-only")
|
|
745
|
+
return { ...writerPolicyFailureResult(options, "FRONTEND_PROVIDER_BUDGET_UNSUPPORTED: actual request admission requires SDK", { sdkAttempted: false }), failureCategory: "budget_breach" };
|
|
733
746
|
if (backendMode === "cli-only") {
|
|
734
747
|
if (writerRequiresSdk) {
|
|
735
748
|
return writerPolicyFailureResult(options, "pi writer tool policy requires SDK backend; CODE_AGENT_PI_BACKEND=cli-only is fail-closed for writers", { sdkAttempted: false, backend: "cli" });
|
|
@@ -745,6 +758,8 @@ export async function executePiStep(options) {
|
|
|
745
758
|
const { checkPiSdkAvailability, executeSingleSdkAttempt, getActivePiSdkReuseScope, } = await import("./pi-sdk-executor.js");
|
|
746
759
|
const sdkCheck = await checkPiSdkAvailability(options.repoRoot);
|
|
747
760
|
if (!sdkCheck.ok) {
|
|
761
|
+
if (options.reserveProviderRequest)
|
|
762
|
+
return { ...writerPolicyFailureResult(options, `FRONTEND_PROVIDER_BUDGET_UNSUPPORTED: ${sdkCheck.detail}`, { sdkAttempted: false }), failureCategory: "budget_breach" };
|
|
748
763
|
if (writerRequiresSdk) {
|
|
749
764
|
return writerPolicyFailureResult(options, `pi writer tool policy requires SDK; ${sdkCheck.detail}`, { sdkAttempted: false });
|
|
750
765
|
}
|
|
@@ -773,7 +788,7 @@ export async function executePiStep(options) {
|
|
|
773
788
|
reuseRuntimeActive,
|
|
774
789
|
});
|
|
775
790
|
}
|
|
776
|
-
if (!shouldFallbackSdkToCli(sdkResult, validationIssues, options)) {
|
|
791
|
+
if (options.reserveProviderRequest || !shouldFallbackSdkToCli(sdkResult, validationIssues, options)) {
|
|
777
792
|
const failureCategory = validationIssues.length > 0
|
|
778
793
|
? "invalid-output"
|
|
779
794
|
: sdkResult.failureCategory;
|
|
@@ -798,6 +813,9 @@ export async function executePiStep(options) {
|
|
|
798
813
|
return { ...cliResult, reuseRuntimeActive };
|
|
799
814
|
}
|
|
800
815
|
async function executeSingleCliAttempt(options, modelConfig, promptFilePath) {
|
|
816
|
+
return observeFrontendAttempt({ backend: "cli", model: [modelConfig.provider, modelConfig.model].filter(Boolean).join("/"), observer: options.onAttemptObservation }, () => executeSingleCliAttemptInternal(options, modelConfig, promptFilePath));
|
|
817
|
+
}
|
|
818
|
+
async function executeSingleCliAttemptInternal(options, modelConfig, promptFilePath) {
|
|
801
819
|
const modelDisplay = modelConfig.provider && modelConfig.model
|
|
802
820
|
? `${modelConfig.provider}/${modelConfig.model}`
|
|
803
821
|
: (modelConfig.model ?? "default");
|
|
@@ -900,6 +918,7 @@ async function executeSingleCliAttempt(options, modelConfig, promptFilePath) {
|
|
|
900
918
|
});
|
|
901
919
|
resolve({
|
|
902
920
|
assistantText: collected.assistantText,
|
|
921
|
+
...stdoutCollector.frontendObservation(),
|
|
903
922
|
command: ["pi", ...recordedPiArgs],
|
|
904
923
|
durationMs,
|
|
905
924
|
exitCode: code,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { collectFrontendUsage, createFrontendReadObservation, observeFrontendAttempt } from "../workflows/dag/frontend-session-budget.js";
|
|
2
|
+
import { frontendModelCapabilities, measureFrontendRequestEnvelope } from "../shared/frontend-execution-policy.js";
|
|
1
3
|
import { appendFile, mkdir } from "node:fs/promises";
|
|
2
4
|
import os from "node:os";
|
|
3
5
|
import path from "node:path";
|
|
@@ -7,6 +9,39 @@ import { BoundedTextPreview, classifyPiFailure, createPiJsonlStreamCollector, DE
|
|
|
7
9
|
import { serializeSessionEvent } from "./pi-event-serializer.js";
|
|
8
10
|
import { PI_RECOMMENDED_COMPACTION, PI_RECOMMENDED_RETRY, } from "../shared/pi-retry-settings.js";
|
|
9
11
|
import { createCompactionController, createContextPressureExtension, createSiftBridgeState, inMemoryCompactionSettings, isDagSiftEnabled, parseCompactionPolicy, parseSiftBridgeConfig, SIFT_RETRIEVE_TOOL_NAME, } from "../shared/pi-context-pressure/index.js";
|
|
12
|
+
/** Wrap the actual stream seam: tool turns, retries and compaction all await it. */
|
|
13
|
+
export async function installPiProviderRequestBudget(session, reserve, onFailure, capacity) {
|
|
14
|
+
const agent = session.agent;
|
|
15
|
+
const original = agent?.streamFunction;
|
|
16
|
+
if (!agent || typeof original !== "function")
|
|
17
|
+
throw Error("FRONTEND_PROVIDER_BUDGET_UNSUPPORTED: SDK actual request stream unavailable");
|
|
18
|
+
const { createAssistantMessageEventStream } = await import("@earendil-works/pi-ai");
|
|
19
|
+
agent.streamFunction = async (...args) => {
|
|
20
|
+
try {
|
|
21
|
+
if (args[2]?.signal?.aborted)
|
|
22
|
+
throw Error("FRONTEND_PROVIDER_REQUEST_ABORTED");
|
|
23
|
+
if (capacity) {
|
|
24
|
+
const measurement = measureFrontendRequestEnvelope(args[1], args[0], capacity.policy);
|
|
25
|
+
capacity.observe(measurement);
|
|
26
|
+
if (measurement.exceedsEstimate)
|
|
27
|
+
throw Error(`FRONTEND_CONTEXT_ESTIMATE_EXCEEDED: estimated=${measurement.estimatedTokens} available=${measurement.availableInputTokens}; estimator=${measurement.estimatorVersion}; registry capability is not a measured gateway limit`);
|
|
28
|
+
}
|
|
29
|
+
await reserve();
|
|
30
|
+
if (args[2]?.signal?.aborted)
|
|
31
|
+
throw Error("FRONTEND_PROVIDER_REQUEST_ABORTED");
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
35
|
+
onFailure(message);
|
|
36
|
+
const stream = createAssistantMessageEventStream();
|
|
37
|
+
const model = args[0] ?? {};
|
|
38
|
+
stream.push({ type: "error", reason: "error", error: { role: "assistant", content: [], api: model.api ?? "unknown", provider: model.provider ?? "unknown", model: model.id ?? "unknown", usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } }, stopReason: "error", errorMessage: message, timestamp: Date.now() } });
|
|
39
|
+
stream.end();
|
|
40
|
+
return stream;
|
|
41
|
+
}
|
|
42
|
+
return original.apply(agent, args);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
10
45
|
/** Same-session recovery after a transport stall. Exactly one continue per SDK attempt. */
|
|
11
46
|
export const PI_SDK_STALL_CONTINUE_MAX = 1;
|
|
12
47
|
/**
|
|
@@ -544,6 +579,9 @@ function aggregateSdkTokenUsage(samples) {
|
|
|
544
579
|
* same session; that is in-attempt recovery, not cross-step session reuse.
|
|
545
580
|
*/
|
|
546
581
|
export async function executeSingleSdkAttempt(options) {
|
|
582
|
+
return observeFrontendAttempt({ backend: "sdk", model: [options.modelConfig.provider, options.modelConfig.model].filter(Boolean).join("/"), observer: options.onAttemptObservation }, () => executeSingleSdkAttemptInternal(options));
|
|
583
|
+
}
|
|
584
|
+
async function executeSingleSdkAttemptInternal(options) {
|
|
547
585
|
const modelConfig = options.modelConfig;
|
|
548
586
|
const modelDisplay = modelConfig.provider && modelConfig.model
|
|
549
587
|
? `${modelConfig.provider}/${modelConfig.model}`
|
|
@@ -568,9 +606,14 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
568
606
|
let timedOut = false;
|
|
569
607
|
let terminationConfirmed = true;
|
|
570
608
|
let stderr = "";
|
|
609
|
+
let providerBudgetFailure;
|
|
610
|
+
let capabilitiesObservation = frontendModelCapabilities(undefined);
|
|
611
|
+
const requestEnvelopeObservation = { peakBytes: 0, peakEstimatedTokens: 0, estimatorVersion: "utf8-bytes-div3-v1-estimate" };
|
|
571
612
|
const stdoutPreview = new BoundedTextPreview("stdout");
|
|
572
613
|
const stdoutCollector = createPiJsonlStreamCollector();
|
|
573
614
|
const usageSamples = [];
|
|
615
|
+
const observationEvents = [];
|
|
616
|
+
const readObservation = createFrontendReadObservation();
|
|
574
617
|
let observedTurns = 0;
|
|
575
618
|
let contextBudgetExhausted = false;
|
|
576
619
|
let contextBudgetDetail = "";
|
|
@@ -660,6 +703,13 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
660
703
|
? { additionalExtensionPaths: options.piExtensionPaths }
|
|
661
704
|
: {}),
|
|
662
705
|
});
|
|
706
|
+
capabilitiesObservation = frontendModelCapabilities(session.agent?.state?.model);
|
|
707
|
+
if (options.reserveProviderRequest || options.frontendExecutionPolicy)
|
|
708
|
+
await installPiProviderRequestBudget(session, options.reserveProviderRequest ?? (async () => { }), message => { providerBudgetFailure = message; appendStderr(message); }, options.frontendExecutionPolicy ? { policy: options.frontendExecutionPolicy, observe: measurement => {
|
|
709
|
+
capabilitiesObservation = measurement.capabilities;
|
|
710
|
+
requestEnvelopeObservation.peakBytes = Math.max(requestEnvelopeObservation.peakBytes, measurement.bytes);
|
|
711
|
+
requestEnvelopeObservation.peakEstimatedTokens = Math.max(requestEnvelopeObservation.peakEstimatedTokens, measurement.estimatedTokens);
|
|
712
|
+
} } : undefined);
|
|
663
713
|
let resolveStall;
|
|
664
714
|
let resolveSettlement;
|
|
665
715
|
let resolveContextBudget;
|
|
@@ -737,6 +787,13 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
737
787
|
// best-effort: activity must never change Pi result
|
|
738
788
|
}
|
|
739
789
|
}
|
|
790
|
+
const observationMessage = isRecord(event.message) ? event.message : undefined;
|
|
791
|
+
try {
|
|
792
|
+
readObservation.observe(event);
|
|
793
|
+
}
|
|
794
|
+
catch { /* observation must not change execution */ }
|
|
795
|
+
if (!(typeof observationMessage?.errorMessage === "string" && observationMessage.errorMessage.startsWith("FRONTEND_PROVIDER_")) && (event.usage || observationMessage?.usage || event.tokenUsage))
|
|
796
|
+
observationEvents.push({ responseId: event.responseId, messageId: event.messageId, usage: event.usage, tokenUsage: event.tokenUsage, message: observationMessage ? { role: observationMessage.role, id: observationMessage.id, responseId: observationMessage.responseId, usage: observationMessage.usage } : undefined });
|
|
740
797
|
const usageSample = extractSdkUsageSample(event);
|
|
741
798
|
if (usageSample)
|
|
742
799
|
usageSamples.push(usageSample);
|
|
@@ -850,6 +907,8 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
850
907
|
}
|
|
851
908
|
catch (error) {
|
|
852
909
|
const message = error instanceof Error ? error.message : String(error);
|
|
910
|
+
if (options.reserveProviderRequest && message.startsWith("FRONTEND_PROVIDER_"))
|
|
911
|
+
providerBudgetFailure = message;
|
|
853
912
|
appendStderr(message);
|
|
854
913
|
}
|
|
855
914
|
finally {
|
|
@@ -885,7 +944,7 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
885
944
|
? collected.parsedEvents
|
|
886
945
|
: (fallbackParsed?.parsedEvents ?? 0);
|
|
887
946
|
const tokensUsed = aggregateSdkTokenUsage(usageSamples);
|
|
888
|
-
const failureCategory = terminationConfirmed
|
|
947
|
+
const failureCategory = !terminationConfirmed ? "termination-unconfirmed" : providerBudgetFailure ? (providerBudgetFailure.includes("REQUEST_ABORTED") ? "interrupted" : providerBudgetFailure.startsWith("FRONTEND_CONTEXT_ESTIMATE_EXCEEDED") ? "context-budget-exhausted" : "budget_breach") : terminationConfirmed
|
|
889
948
|
? classifyPiFailure({
|
|
890
949
|
assistantText,
|
|
891
950
|
exitCode: timedOut ? 1 : stderr ? 1 : 0,
|
|
@@ -915,6 +974,10 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
915
974
|
outputTooLarge: collected.outputTooLarge,
|
|
916
975
|
timedOut,
|
|
917
976
|
tokensUsed,
|
|
977
|
+
usageObservation: collectFrontendUsage(observationEvents),
|
|
978
|
+
readObservation: readObservation.snapshot(),
|
|
979
|
+
capabilitiesObservation,
|
|
980
|
+
...(options.frontendExecutionPolicy ? { requestEnvelopeObservation } : {}),
|
|
918
981
|
subagentStats: collected.subagentStats,
|
|
919
982
|
// Writer-thinking-exhausted diagnosis evidence (AC-002). Carried through
|
|
920
983
|
// the object spread in executePiStep so executeDagPiNode can read them
|
|
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
import { withFrontendTestReporter, parseFrontendTestExecutionReport } from "../workflows/dag/frontend-test-execution-evidence.js";
|
|
4
4
|
import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
5
|
+
import { mkdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { writeDagNodeJsonArtifact, writeDagNodeTextArtifact, writeDagRunJsonArtifact, } from "../infrastructure/harness/artifact-store.js";
|
|
8
8
|
import { captureWorkspaceCheckpoint } from "../workflows/dag/workspace-checkpoint.js";
|
|
@@ -2065,7 +2065,10 @@ async function executeFrontendVerificationBundle(input, meta) {
|
|
|
2065
2065
|
let reportError;
|
|
2066
2066
|
if (observedCommand && result.ok) {
|
|
2067
2067
|
try {
|
|
2068
|
-
|
|
2068
|
+
// Vitest reports physical paths (e.g. /private/var on macOS).
|
|
2069
|
+
// Match those in workspace coordinates even when shell.cwd is
|
|
2070
|
+
// a subdirectory or the caller used a directory alias.
|
|
2071
|
+
commandExecutionEvidence.set(key, parseFrontendTestExecutionReport(JSON.parse(await readFile(reportPath, "utf8")), await realpath(cwd), await realpath(input.cwd)));
|
|
2069
2072
|
}
|
|
2070
2073
|
catch (error) {
|
|
2071
2074
|
reportError = `frontend test execution report unavailable: ${error instanceof Error ? error.message : String(error)}`;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** Execution hints and estimation reserves, never model-name capacity defaults. */
|
|
3
|
+
export const frontendExecutionPolicySchema = z.object({
|
|
4
|
+
scopeTargetBytes: z.number().int().positive().default(24_000),
|
|
5
|
+
maxScopeUnits: z.number().int().positive().default(4),
|
|
6
|
+
outputReserveTokens: z.number().int().positive().default(4096),
|
|
7
|
+
readGrowthReserveTokens: z.number().int().nonnegative().default(2048),
|
|
8
|
+
safetyRatio: z.number().min(0).max(0.5).default(0.15),
|
|
9
|
+
}).strict();
|
|
10
|
+
export function frontendModelCapabilities(model) {
|
|
11
|
+
const value = model && typeof model === "object" ? model : {};
|
|
12
|
+
const positive = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : null;
|
|
13
|
+
return { model: typeof value.id === "string" ? `${value.provider ?? "unknown"}/${value.id}` : null, source: Object.keys(value).length ? "model-registry" : "unknown", inputLimit: positive(value.inputLimit), totalContextLimit: positive(value.contextWindow), outputLimit: positive(value.maxTokens), reasoningControllability: "unknown" };
|
|
14
|
+
}
|
|
15
|
+
export function measureFrontendRequestEnvelope(context, model, policy) {
|
|
16
|
+
const bytes = Buffer.byteLength(JSON.stringify(context));
|
|
17
|
+
const estimatedTokens = Math.ceil(bytes / 3);
|
|
18
|
+
const capabilities = frontendModelCapabilities(model);
|
|
19
|
+
const outputReserve = Math.min(policy.outputReserveTokens, capabilities.outputLimit ?? policy.outputReserveTokens);
|
|
20
|
+
const availableInputTokens = Math.min(capabilities.inputLimit ?? Infinity, capabilities.totalContextLimit === null ? Infinity : Math.floor(capabilities.totalContextLimit * (1 - policy.safetyRatio)) - outputReserve - policy.readGrowthReserveTokens);
|
|
21
|
+
return { bytes, estimatedTokens, estimatorVersion: "utf8-bytes-div3-v1-estimate", capabilities, availableInputTokens: Number.isFinite(availableInputTokens) ? availableInputTokens : null, exceedsEstimate: estimatedTokens > availableInputTokens };
|
|
22
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { frontendExecutionPolicySchema } from "../shared/frontend-execution-policy.js";
|
|
2
3
|
import { DEFAULT_FRONTEND_SPEC_ROOTS, isFrontendSpecFilePath, isValidFrontendSpecRoot, } from "../shared/openspec-spec.js";
|
|
3
4
|
export const taskFlowSchema = z.enum([
|
|
4
5
|
"auto",
|
|
@@ -300,6 +301,8 @@ const taskConfigObjectSchema = z.object({
|
|
|
300
301
|
* default; raise to 2-3 for more tolerance of transient verify failures). */
|
|
301
302
|
frontendRecovery: z
|
|
302
303
|
.object({
|
|
304
|
+
/** Frozen request quota across the original run and recovery lineage; not a model capacity. */
|
|
305
|
+
maxProviderRequests: z.number().int().positive().optional(),
|
|
303
306
|
maxContinuations: z
|
|
304
307
|
.number()
|
|
305
308
|
.int()
|
|
@@ -310,6 +313,7 @@ const taskConfigObjectSchema = z.object({
|
|
|
310
313
|
})
|
|
311
314
|
.strict()
|
|
312
315
|
.optional(),
|
|
316
|
+
frontendExecutionPolicy: frontendExecutionPolicySchema.optional(),
|
|
313
317
|
/** Outer loop auto mode never writes by default; DAG execute requires this policy plus path/writeSet gates. */
|
|
314
318
|
loopAutoExecutionPolicy: loopAutoExecutionPolicySchema
|
|
315
319
|
.optional()
|
|
@@ -3,7 +3,7 @@ import { normalizeFragmentText } from "./fragment-inventory.js";
|
|
|
3
3
|
// 顺序 reconciliation(AC-006 / AC-FIDELITY-REPAIR-006)
|
|
4
4
|
// 结构化确定性片段走代码映射(显式 AC ID / 文本精确匹配),绝不调用 executor;
|
|
5
5
|
// 仅叙述性/歧义 chunk 调用可注入 executor,按标题顺序串行、逐块有界处理
|
|
6
|
-
// (budgetChars
|
|
6
|
+
// (budgetChars 仅为调用提示,不截断判定所需的原文)。executor 超时/异常/无效
|
|
7
7
|
// 返回/identity 越界一律 fail closed(SOURCE_FIDELITY_RECONCILIATION_FAILED),
|
|
8
8
|
// 不回退 heuristics。
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
@@ -65,7 +65,7 @@ export async function reconcileLedgerFragments(input) {
|
|
|
65
65
|
path: fragment.path,
|
|
66
66
|
headingPath: fragment.headingPath,
|
|
67
67
|
lineRange: fragment.lineRange,
|
|
68
|
-
text: fragment.text
|
|
68
|
+
text: fragment.text,
|
|
69
69
|
},
|
|
70
70
|
canonicalRequirements,
|
|
71
71
|
budgetChars,
|
|
@@ -7,8 +7,7 @@ import { mergeInventoryItems } from "./ledger-reconciliation.js";
|
|
|
7
7
|
// context-only 仅 reviewer pass 放行,高风险 context-only 始终阻断;duplicate
|
|
8
8
|
// 保留全部来源但审核失败(含修订后仍拒绝)以稳定码 fail closed。
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
|
-
/**
|
|
11
|
-
* prompt;沿用 MAX_SOURCE_EXCERPT_CHARS 边界语义。 */
|
|
10
|
+
/** Historical caller hint; it must never clip the fragment used for a decision. */
|
|
12
11
|
export const LEDGER_REVIEW_DEFAULT_BUDGET_CHARS = 2000;
|
|
13
12
|
export const LEDGER_REVIEW_MAX_REVISIONS = 1;
|
|
14
13
|
/** review 失败(reviewer 异常/第二次不通过)统一稳定错误码。 */
|
|
@@ -180,12 +179,9 @@ export const defaultLedgerReviewer = async (request) => {
|
|
|
180
179
|
: "conflict 无有效 human resolution 覆盖,保持阻断",
|
|
181
180
|
};
|
|
182
181
|
};
|
|
183
|
-
/**
|
|
184
|
-
*
|
|
185
|
-
* (AC-004 / AC-HARD-004):请求携带非空有界原文与精确 fragment id,完整文档
|
|
186
|
-
* 绝不进入下游 prompt;原文不可得时退化为 reason / id 保证非空。 */
|
|
182
|
+
/** Resolve complete source fragments for independent review; missing original text
|
|
183
|
+
* cannot be replaced with its reason or identity. */
|
|
187
184
|
export function ledgerReviewItemsFromRecords(records, input) {
|
|
188
|
-
const budgetChars = input?.budgetChars ?? LEDGER_REVIEW_DEFAULT_BUDGET_CHARS;
|
|
189
185
|
const merged = input?.fragments
|
|
190
186
|
? mergeInventoryItems({ records, fragments: input.fragments })
|
|
191
187
|
: records.map((record) => ({
|
|
@@ -200,8 +196,9 @@ export function ledgerReviewItemsFromRecords(records, input) {
|
|
|
200
196
|
canonicalRequirementIds: record.canonicalRequirementIds,
|
|
201
197
|
}));
|
|
202
198
|
return merged.map((item) => {
|
|
203
|
-
const
|
|
204
|
-
|
|
199
|
+
const text = item.text;
|
|
200
|
+
if (!text.trim() && ["context-only", "duplicate", "conflict"].includes(item.disposition))
|
|
201
|
+
throw new LedgerReviewError(`SOURCE_FIDELITY_ORIGINAL_MISSING: ${item.id}; independent review needs the full source text`);
|
|
205
202
|
return {
|
|
206
203
|
id: item.id,
|
|
207
204
|
text,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { observeFrontendSession } from "../../workflows/dag/frontend-session-budget.js";
|
|
1
3
|
/**
|
|
2
4
|
* P2 Semantic Intake: one-shot, read-only Pi structuring of imported PRDs
|
|
3
5
|
* into TaskContractDraftV1. Does not write source/需求.md directly — caller
|
|
@@ -411,24 +413,6 @@ export async function buildRepoHint(repoRoot) {
|
|
|
411
413
|
}
|
|
412
414
|
return lines.join("\n");
|
|
413
415
|
}
|
|
414
|
-
/**
|
|
415
|
-
* 在 maxChars 附近回退到最近的标题边界再截断(AC-005 / AC-FIDELITY-REPAIR-005
|
|
416
|
-
* 回归点移除):避免把可执行项/列表项从中间切开吞掉。仅用于模型提示词预算;
|
|
417
|
-
* ledger fragment inventory 始终消费完整原文,不经过此裁剪。
|
|
418
|
-
*/
|
|
419
|
-
function clipAtSectionBoundary(content, maxChars) {
|
|
420
|
-
if (content.length <= maxChars)
|
|
421
|
-
return content;
|
|
422
|
-
const prefix = content.slice(0, maxChars);
|
|
423
|
-
const headingRe = /^(?:#{1,6})\s+.+$/gm;
|
|
424
|
-
let boundary = prefix.length;
|
|
425
|
-
let match;
|
|
426
|
-
while ((match = headingRe.exec(prefix)) !== null) {
|
|
427
|
-
if (match.index >= maxChars * 0.4)
|
|
428
|
-
boundary = match.index;
|
|
429
|
-
}
|
|
430
|
-
return `${content.slice(0, boundary)}\n\n[truncated at section boundary]`;
|
|
431
|
-
}
|
|
432
416
|
async function buildPrompt(input) {
|
|
433
417
|
const bodies = input.documents
|
|
434
418
|
.map((doc, index) => {
|
|
@@ -436,14 +420,14 @@ async function buildPrompt(input) {
|
|
|
436
420
|
const metaLine = meta.recognized
|
|
437
421
|
? `artifact_type=${meta.artifactType} status=${meta.requirementStatus ?? meta.analysisStatus ?? "n/a"}`
|
|
438
422
|
: "artifact_type=unknown";
|
|
439
|
-
const
|
|
423
|
+
const complete = doc.content;
|
|
440
424
|
return [
|
|
441
425
|
`### Document ${index + 1}`,
|
|
442
426
|
`role: ${doc.role}`,
|
|
443
427
|
`path: ${doc.materializedPath}`,
|
|
444
428
|
metaLine,
|
|
445
429
|
"```markdown",
|
|
446
|
-
|
|
430
|
+
complete,
|
|
447
431
|
"```",
|
|
448
432
|
].join("\n");
|
|
449
433
|
})
|
|
@@ -609,9 +593,7 @@ export async function runSemanticIntake(input) {
|
|
|
609
593
|
message: "semantic intake has no MED/HIGH/LOW or Pi-supported models to try",
|
|
610
594
|
};
|
|
611
595
|
}
|
|
612
|
-
|
|
613
|
-
.map((d) => d.absolutePath)
|
|
614
|
-
.filter((p) => Boolean(p));
|
|
596
|
+
// Complete text is already inline; attaching the same documents duplicates it.
|
|
615
597
|
const prompt = await buildPrompt({
|
|
616
598
|
taskId: input.taskId,
|
|
617
599
|
title: input.title,
|
|
@@ -623,8 +605,12 @@ export async function runSemanticIntake(input) {
|
|
|
623
605
|
let chosenText;
|
|
624
606
|
for (const modelRef of queue) {
|
|
625
607
|
try {
|
|
626
|
-
const result = await
|
|
627
|
-
|
|
608
|
+
const result = await observeFrontendSession({
|
|
609
|
+
phase: "semantic-intake", userMessage: "Structure the imported requirement documents into the required JSON only.", prompt, model: modelRef, taskId: input.taskId,
|
|
610
|
+
artifactPath: path.join(getTaskPaths(input.repoRoot, input.taskId).taskDir, "artifacts", "session-budget", `${randomUUID()}.json`),
|
|
611
|
+
}, observer => execute({
|
|
612
|
+
onAttemptObservation: observer,
|
|
613
|
+
attachedFiles: [],
|
|
628
614
|
modelConfig: resolveDagPiModelConfig(modelRef),
|
|
629
615
|
prompt,
|
|
630
616
|
repoRoot: input.repoRoot,
|
|
@@ -643,7 +629,7 @@ export async function runSemanticIntake(input) {
|
|
|
643
629
|
return [error instanceof Error ? error.message : String(error)];
|
|
644
630
|
}
|
|
645
631
|
},
|
|
646
|
-
});
|
|
632
|
+
}));
|
|
647
633
|
attempted.push({
|
|
648
634
|
model: result.modelDisplay || modelRef,
|
|
649
635
|
ok: result.ok,
|
|
@@ -656,6 +642,10 @@ export async function runSemanticIntake(input) {
|
|
|
656
642
|
}
|
|
657
643
|
lastFailure =
|
|
658
644
|
`${result.failureCategory}: ${result.stderr || result.assistantText}`.slice(0, 2000);
|
|
645
|
+
if (result.stopReason === "length" || ["context-overflow", "context-budget-exhausted"].includes(result.failureCategory)) {
|
|
646
|
+
lastFailure = `SEMANTIC_INTAKE_CAPACITY_EXHAUSTED: refine the complete source into independently reviewable documents; ${lastFailure}`;
|
|
647
|
+
break;
|
|
648
|
+
}
|
|
659
649
|
}
|
|
660
650
|
catch (error) {
|
|
661
651
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { observeFrontendSession } from "../../workflows/dag/frontend-session-budget.js";
|
|
1
4
|
/**
|
|
2
5
|
* 生产 Source Fidelity Pi 适配器(AC-PROD-001..006 / AC-001..006)
|
|
3
6
|
*
|
|
@@ -22,7 +25,7 @@ import { resolveDagPiModelConfig } from "../../executors/dag-pi-executor.js";
|
|
|
22
25
|
import { resolveExecutorModelMatrices } from "../../executors/model-routing.js";
|
|
23
26
|
import { listPiSdkAvailableModels } from "../../executors/pi-sdk-executor.js";
|
|
24
27
|
import { loadHarnessManifest } from "../../governance/harness.js";
|
|
25
|
-
import { loadTaskConfig } from "../runtime.js";
|
|
28
|
+
import { getTaskPaths, loadTaskConfig } from "../runtime.js";
|
|
26
29
|
import { buildSemanticIntakeModelQueue, extractJsonObject, } from "./semantic-intake.js";
|
|
27
30
|
import { LEDGER_RECONCILIATION_DEFAULT_BUDGET_CHARS, LedgerReconciliationError, } from "./ledger-reconciliation.js";
|
|
28
31
|
import { LedgerReviewError, } from "./ledger-review.js";
|
|
@@ -112,7 +115,7 @@ function buildReconciliationPrompt(request) {
|
|
|
112
115
|
`fragment path: ${request.fragment.path}`,
|
|
113
116
|
`fragment headingPath: ${request.fragment.headingPath}`,
|
|
114
117
|
`fragment lineRange: ${JSON.stringify(request.fragment.lineRange)}`,
|
|
115
|
-
`
|
|
118
|
+
`packing hint: ${request.budgetChars} chars; the fragment below is complete and must not be truncated or summarized before deciding`,
|
|
116
119
|
"fragment text:",
|
|
117
120
|
"```markdown",
|
|
118
121
|
request.fragment.text,
|
|
@@ -247,7 +250,11 @@ export async function runProductionLedgerReconciliation(request, options) {
|
|
|
247
250
|
let lastFailure = "";
|
|
248
251
|
for (const modelRef of queue) {
|
|
249
252
|
try {
|
|
250
|
-
const result = await
|
|
253
|
+
const result = await observeFrontendSession({
|
|
254
|
+
phase: "source-reconciliation", userMessage: "Reconcile the source fragment into the required JSON only.", sourceDigest: createHash("sha256").update(JSON.stringify(request)).digest("hex"), scopeIds: [request.fragment.id], prompt, model: modelRef, taskId: options.taskId, sink: options.observationSink,
|
|
255
|
+
artifactPath: options.taskId ? path.join(getTaskPaths(options.repoRoot, options.taskId).taskDir, "artifacts", "session-budget", `${randomUUID()}.json`) : undefined,
|
|
256
|
+
}, observer => execute({
|
|
257
|
+
onAttemptObservation: observer,
|
|
251
258
|
attachedFiles: [],
|
|
252
259
|
modelConfig: resolveDagPiModelConfig(modelRef),
|
|
253
260
|
prompt,
|
|
@@ -266,10 +273,14 @@ export async function runProductionLedgerReconciliation(request, options) {
|
|
|
266
273
|
return [error instanceof Error ? error.message : String(error)];
|
|
267
274
|
}
|
|
268
275
|
},
|
|
269
|
-
});
|
|
276
|
+
}));
|
|
270
277
|
if (!result.ok) {
|
|
271
278
|
lastFailure =
|
|
272
279
|
`${result.failureCategory}: ${(result.stderr || result.assistantText).slice(0, 2000)}`;
|
|
280
|
+
if (result.stopReason === "length" || ["context-overflow", "context-budget-exhausted"].includes(result.failureCategory)) {
|
|
281
|
+
lastFailure = `SOURCE_FIDELITY_CAPACITY_EXHAUSTED: refine this complete source fragment without losing conditions; ${lastFailure}`;
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
273
284
|
continue;
|
|
274
285
|
}
|
|
275
286
|
try {
|
|
@@ -306,7 +317,11 @@ export async function runProductionLedgerReview(request, options) {
|
|
|
306
317
|
let lastFailure = "";
|
|
307
318
|
for (const modelRef of queue) {
|
|
308
319
|
try {
|
|
309
|
-
const result = await
|
|
320
|
+
const result = await observeFrontendSession({
|
|
321
|
+
phase: "source-review", userMessage: "Review the fragment disposition into the required JSON only.", sourceDigest: createHash("sha256").update(JSON.stringify(request)).digest("hex"), scopeIds: [request.fragment.id], prompt, model: modelRef, taskId: options.taskId, sink: options.observationSink,
|
|
322
|
+
artifactPath: options.taskId ? path.join(getTaskPaths(options.repoRoot, options.taskId).taskDir, "artifacts", "session-budget", `${randomUUID()}.json`) : undefined,
|
|
323
|
+
}, observer => execute({
|
|
324
|
+
onAttemptObservation: observer,
|
|
310
325
|
attachedFiles: [],
|
|
311
326
|
modelConfig: resolveDagPiModelConfig(modelRef),
|
|
312
327
|
prompt,
|
|
@@ -325,10 +340,14 @@ export async function runProductionLedgerReview(request, options) {
|
|
|
325
340
|
return [error instanceof Error ? error.message : String(error)];
|
|
326
341
|
}
|
|
327
342
|
},
|
|
328
|
-
});
|
|
343
|
+
}));
|
|
329
344
|
if (!result.ok) {
|
|
330
345
|
lastFailure =
|
|
331
346
|
`${result.failureCategory}: ${(result.stderr || result.assistantText).slice(0, 2000)}`;
|
|
347
|
+
if (result.stopReason === "length" || ["context-overflow", "context-budget-exhausted"].includes(result.failureCategory)) {
|
|
348
|
+
lastFailure = `SOURCE_FIDELITY_CAPACITY_EXHAUSTED: refine this complete source fragment without losing conditions; ${lastFailure}`;
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
332
351
|
continue;
|
|
333
352
|
}
|
|
334
353
|
try {
|
|
@@ -380,5 +399,5 @@ export async function resolveProductionSourceFidelityPi(input) {
|
|
|
380
399
|
}
|
|
381
400
|
if (effectiveTaskKind !== SOURCE_FIDELITY_TASK_KIND)
|
|
382
401
|
return undefined;
|
|
383
|
-
return createProductionSourceFidelityPi({ repoRoot: input.repoRoot });
|
|
402
|
+
return createProductionSourceFidelityPi({ repoRoot: input.repoRoot, taskId: input.taskId });
|
|
384
403
|
}
|