harness-engineering-agent 0.3.32 → 0.3.34
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/README.md +7 -6
- package/dist/agent/harness-role-capability-directory-data.d.ts.map +1 -1
- package/dist/agent/harness-role-capability-directory-data.js +2 -3
- package/dist/agent/harness-role-capability-directory-data.js.map +1 -1
- package/dist/controller/failure-factory.d.ts.map +1 -1
- package/dist/controller/failure-factory.js +27 -0
- package/dist/controller/failure-factory.js.map +1 -1
- package/dist/controller/index.d.ts +2 -0
- package/dist/controller/index.d.ts.map +1 -1
- package/dist/controller/index.js +1 -0
- package/dist/controller/index.js.map +1 -1
- package/dist/controller/parallel-task-delta.d.ts +24 -0
- package/dist/controller/parallel-task-delta.d.ts.map +1 -0
- package/dist/controller/parallel-task-delta.js +34 -0
- package/dist/controller/parallel-task-delta.js.map +1 -0
- package/dist/controller/repair-loop.d.ts.map +1 -1
- package/dist/controller/repair-loop.js +3 -4
- package/dist/controller/repair-loop.js.map +1 -1
- package/dist/controller/route-resolver.d.ts +1 -0
- package/dist/controller/route-resolver.d.ts.map +1 -1
- package/dist/controller/route-resolver.js +6 -2
- package/dist/controller/route-resolver.js.map +1 -1
- package/dist/controller/run-controller.d.ts.map +1 -1
- package/dist/controller/run-controller.js +1 -0
- package/dist/controller/run-controller.js.map +1 -1
- package/dist/controller/run-state.d.ts.map +1 -1
- package/dist/controller/run-state.js +3 -1
- package/dist/controller/run-state.js.map +1 -1
- package/dist/controller/task-executor.d.ts.map +1 -1
- package/dist/controller/task-executor.js +278 -100
- package/dist/controller/task-executor.js.map +1 -1
- package/dist/domain/failure.d.ts +221 -0
- package/dist/domain/failure.d.ts.map +1 -1
- package/dist/domain/failure.js +5 -0
- package/dist/domain/failure.js.map +1 -1
- package/dist/domain/report.d.ts +470 -0
- package/dist/domain/report.d.ts.map +1 -1
- package/dist/domain/run.d.ts +481 -0
- package/dist/domain/run.d.ts.map +1 -1
- package/dist/domain/run.js +9 -1
- package/dist/domain/run.js.map +1 -1
- package/dist/plugin/harness-chat-hook.d.ts +1 -0
- package/dist/plugin/harness-chat-hook.d.ts.map +1 -1
- package/dist/plugin/harness-chat-hook.js +18 -2
- package/dist/plugin/harness-chat-hook.js.map +1 -1
- package/dist/plugin/harness-chat-protocol.d.ts.map +1 -1
- package/dist/plugin/harness-chat-protocol.js +1 -1
- package/dist/plugin/harness-chat-protocol.js.map +1 -1
- package/dist/plugin/harness-coordinator.d.ts.map +1 -1
- package/dist/plugin/harness-coordinator.js +14 -3
- package/dist/plugin/harness-coordinator.js.map +1 -1
- package/dist/plugin/harness-escalate-tool.d.ts.map +1 -1
- package/dist/plugin/harness-escalate-tool.js +16 -2
- package/dist/plugin/harness-escalate-tool.js.map +1 -1
- package/dist/plugin/harness-request-router.d.ts +13 -0
- package/dist/plugin/harness-request-router.d.ts.map +1 -1
- package/dist/plugin/harness-request-router.js +26 -0
- package/dist/plugin/harness-request-router.js.map +1 -1
- package/dist/store/run-event.d.ts +470 -0
- package/dist/store/run-event.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { AgentResultSchema } from "../domain/index.js";
|
|
2
2
|
import { normalizeVerificationCommand } from "../verification/command-policy.js";
|
|
3
3
|
import { VerificationExecutionStatus } from "../verification/index.js";
|
|
4
|
-
import { createGitWorkspaceAdapter, createGitWorktreeAdapter } from "../workspace/index.js";
|
|
4
|
+
import { createGitWorkspaceAdapter, createGitWorktreeAdapter, } from "../workspace/index.js";
|
|
5
5
|
import { createEvidence } from "./evidence-factory.js";
|
|
6
6
|
import { resolveFailureAnalysis } from "./failure-analysis.js";
|
|
7
7
|
import { classifyFailureScope } from "./failure-classification.js";
|
|
8
8
|
import { createContextGapFailure, createImplementationFailure, createMergeConflictFailure, createModelFailure, createScopeDriftFailure, createVerificationEnvironmentFailure, } from "./failure-factory.js";
|
|
9
9
|
import { orderTasks } from "./graph-validator.js";
|
|
10
|
+
import { createParallelTaskDelta } from "./parallel-task-delta.js";
|
|
10
11
|
import { reassessRemainingTasks } from "./reassessor.js";
|
|
11
12
|
import { runRepairLoop } from "./repair-loop.js";
|
|
13
|
+
import { resolveRoute } from "./route-resolver.js";
|
|
12
14
|
import { finalizeRunAcceptance } from "./run-acceptance.js";
|
|
13
15
|
import { consumeBudget, parsePlannedRun, RunBudgetExceededError, withParts, withPlannedStatus, withTask, } from "./run-state.js";
|
|
14
16
|
import { readFailureAnalyzerOutput, readTaskReviewOutput, readWorkerOutput, } from "./task-agent-output.js";
|
|
@@ -712,11 +714,56 @@ export const createTaskExecutor = (input) => {
|
|
|
712
714
|
const runTasks = async (run) => {
|
|
713
715
|
let currentRun = run;
|
|
714
716
|
const orderedTasks = orderTasks(currentRun.taskGraph);
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
717
|
+
let schedulingMode = currentRun.routingDecision?.scheduling ?? "serial";
|
|
718
|
+
const refreshBatchRouting = async (candidate, completedTaskIds) => {
|
|
719
|
+
const assessment = candidate.complexityAssessment ?? {
|
|
720
|
+
tier: "complex",
|
|
721
|
+
rationale: "No Intake assessment was persisted.",
|
|
722
|
+
estimatedFileCount: Math.max(1, candidate.taskGraph.tasks.length),
|
|
723
|
+
estimatedCrossModuleDeps: 0,
|
|
724
|
+
suggestedRoute: "full",
|
|
725
|
+
confidence: "low",
|
|
726
|
+
};
|
|
727
|
+
const resolution = resolveRoute(assessment, {
|
|
728
|
+
taskGraph: candidate.taskGraph,
|
|
729
|
+
workspaceMode: candidate.workspace.git.mode,
|
|
730
|
+
completedTaskIds,
|
|
731
|
+
});
|
|
732
|
+
const currentScheduling = candidate.routingDecision?.scheduling ?? "serial";
|
|
733
|
+
const currentGovernance = candidate.routingDecision?.governance ??
|
|
734
|
+
(candidate.resolvedRoute === "parallel" ? "full" : candidate.resolvedRoute);
|
|
735
|
+
const currentRoute = candidate.resolvedRoute;
|
|
736
|
+
const nextRoute = resolution.resolvedRoute;
|
|
737
|
+
if (currentScheduling === resolution.routingDecision.scheduling &&
|
|
738
|
+
currentGovernance === resolution.routingDecision.governance &&
|
|
739
|
+
candidate.resolvedTier === resolution.resolvedTier &&
|
|
740
|
+
currentRoute === nextRoute) {
|
|
741
|
+
schedulingMode = resolution.routingDecision.scheduling;
|
|
742
|
+
return candidate;
|
|
743
|
+
}
|
|
744
|
+
const routed = parsePlannedRun({
|
|
745
|
+
...candidate,
|
|
746
|
+
resolvedTier: resolution.resolvedTier,
|
|
747
|
+
resolvedRoute: nextRoute,
|
|
748
|
+
routingDecision: { ...resolution.routingDecision, source: "task-graph" },
|
|
749
|
+
});
|
|
750
|
+
schedulingMode = routed.routingDecision?.scheduling ?? "serial";
|
|
751
|
+
await input.writer.persist(routed, input.writer.createEvent(candidate.runId, "run.scheduling.recalculated", {
|
|
752
|
+
previousScheduling: currentScheduling,
|
|
753
|
+
scheduling: schedulingMode,
|
|
754
|
+
previousRoute: currentRoute,
|
|
755
|
+
route: nextRoute,
|
|
756
|
+
completedTaskIds: [...completedTaskIds].sort(),
|
|
757
|
+
reasons: resolution.routingDecision.reasons,
|
|
758
|
+
}));
|
|
759
|
+
return routed;
|
|
760
|
+
};
|
|
761
|
+
while (true) {
|
|
762
|
+
const completedTaskIds = new Set(currentRun.taskGraph.tasks
|
|
763
|
+
.filter((task) => task.status === "completed")
|
|
764
|
+
.map((task) => task.taskId));
|
|
765
|
+
const remainingTaskIds = new Set(orderedTasks.map((task) => task.taskId).filter((taskId) => !completedTaskIds.has(taskId)));
|
|
766
|
+
currentRun = await refreshBatchRouting(currentRun, completedTaskIds);
|
|
720
767
|
while (remainingTaskIds.size > 0) {
|
|
721
768
|
const readyTasks = orderedTasks.filter((task) => {
|
|
722
769
|
if (completedTaskIds.has(task.taskId))
|
|
@@ -726,10 +773,10 @@ export const createTaskExecutor = (input) => {
|
|
|
726
773
|
if (readyTasks.length === 0) {
|
|
727
774
|
break;
|
|
728
775
|
}
|
|
729
|
-
if (readyTasks.length >= 2) {
|
|
730
|
-
const batchResult = await runParallelReadyBatch(currentRun,
|
|
776
|
+
if (readyTasks.length >= 2 && schedulingMode === "parallel") {
|
|
777
|
+
const batchResult = await runParallelReadyBatch(currentRun, orderedTasks, completedTaskIds);
|
|
731
778
|
if (batchResult.kind === "paused") {
|
|
732
|
-
return batchResult.run;
|
|
779
|
+
return reassessAfterFailure(batchResult.run);
|
|
733
780
|
}
|
|
734
781
|
if (batchResult.kind === "completed") {
|
|
735
782
|
currentRun = batchResult.run;
|
|
@@ -786,7 +833,7 @@ export const createTaskExecutor = (input) => {
|
|
|
786
833
|
break;
|
|
787
834
|
currentRun = await runTask(currentRun, task);
|
|
788
835
|
if (currentRun.status !== "executing") {
|
|
789
|
-
return currentRun;
|
|
836
|
+
return reassessAfterFailure(currentRun);
|
|
790
837
|
}
|
|
791
838
|
const completedTask = currentRun.taskGraph.tasks.find((candidate) => candidate.taskId === task.taskId);
|
|
792
839
|
if (completedTask?.status === "completed") {
|
|
@@ -830,62 +877,19 @@ export const createTaskExecutor = (input) => {
|
|
|
830
877
|
reason: event.reason,
|
|
831
878
|
}));
|
|
832
879
|
}
|
|
880
|
+
const nextCompletedTaskIds = new Set(currentRun.taskGraph.tasks
|
|
881
|
+
.filter((candidate) => candidate.status === "completed")
|
|
882
|
+
.map((candidate) => candidate.taskId));
|
|
883
|
+
currentRun = await refreshBatchRouting(currentRun, nextCompletedTaskIds);
|
|
884
|
+
if (schedulingMode === "parallel")
|
|
885
|
+
break;
|
|
833
886
|
}
|
|
834
887
|
}
|
|
835
888
|
}
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
for (const task of orderedTasks) {
|
|
839
|
-
const currentTask = currentRun.taskGraph.tasks.find((candidate) => candidate.taskId === task.taskId);
|
|
840
|
-
if (currentTask?.status === "completed") {
|
|
841
|
-
continue;
|
|
842
|
-
}
|
|
843
|
-
currentRun = await runTask(currentRun, task);
|
|
844
|
-
if (currentRun.status !== "executing") {
|
|
845
|
-
return currentRun;
|
|
846
|
-
}
|
|
847
|
-
const completedTask = currentRun.taskGraph.tasks.find((candidate) => candidate.taskId === task.taskId);
|
|
848
|
-
if (completedTask?.status === "completed") {
|
|
849
|
-
const reassessment = reassessRemainingTasks({
|
|
850
|
-
run: currentRun,
|
|
851
|
-
completedTask,
|
|
852
|
-
allTasks: currentRun.taskGraph.tasks,
|
|
853
|
-
});
|
|
854
|
-
if (reassessment.shouldReassess && reassessment.event && reassessment.newTier) {
|
|
855
|
-
const event = reassessment.event;
|
|
856
|
-
const newTier = reassessment.newTier;
|
|
857
|
-
const updatedTasks = currentRun.taskGraph.tasks.map((t) => {
|
|
858
|
-
if (t.taskId === completedTask.taskId)
|
|
859
|
-
return t;
|
|
860
|
-
if (!event.affectedRemainingTasks.includes(t.taskId))
|
|
861
|
-
return t;
|
|
862
|
-
return { ...t, complexityTier: newTier };
|
|
863
|
-
});
|
|
864
|
-
currentRun = parsePlannedRun({
|
|
865
|
-
...currentRun,
|
|
866
|
-
resolvedTier: newTier,
|
|
867
|
-
routingDecision: {
|
|
868
|
-
...currentRun.routingDecision,
|
|
869
|
-
tier: newTier,
|
|
870
|
-
governance: newTier === "complex"
|
|
871
|
-
? "full"
|
|
872
|
-
: (currentRun.routingDecision?.governance ?? "streamlined"),
|
|
873
|
-
scheduling: currentRun.routingDecision?.scheduling ?? "serial",
|
|
874
|
-
source: "reassessment",
|
|
875
|
-
reasons: [event.reason],
|
|
876
|
-
},
|
|
877
|
-
reassessmentHistory: [...currentRun.reassessmentHistory, event],
|
|
878
|
-
taskGraph: { ...currentRun.taskGraph, tasks: updatedTasks },
|
|
879
|
-
});
|
|
880
|
-
await input.writer.persist(currentRun, input.writer.createEvent(currentRun.runId, "task.reassessment", {
|
|
881
|
-
triggerTaskId: event.triggerTaskId,
|
|
882
|
-
previousTier: event.previousTier,
|
|
883
|
-
newTier: event.newTier,
|
|
884
|
-
reason: event.reason,
|
|
885
|
-
}));
|
|
886
|
-
}
|
|
887
|
-
}
|
|
889
|
+
if (remainingTaskIds.size > 0 && schedulingMode === "parallel") {
|
|
890
|
+
continue;
|
|
888
891
|
}
|
|
892
|
+
break;
|
|
889
893
|
}
|
|
890
894
|
currentRun = await runIntegrationVerification(currentRun);
|
|
891
895
|
const runAcceptanceResult = await finalizeRunAcceptance({
|
|
@@ -893,9 +897,27 @@ export const createTaskExecutor = (input) => {
|
|
|
893
897
|
runtime: input.runtime,
|
|
894
898
|
run: currentRun,
|
|
895
899
|
});
|
|
900
|
+
const acceptanceBlockingDetails = runAcceptanceResult.status === "blocked"
|
|
901
|
+
? {
|
|
902
|
+
kind: "blocked",
|
|
903
|
+
missingContext: runAcceptanceResult.runAcceptance.criterionDecisions
|
|
904
|
+
.filter((decision) => decision.status === "blocked" || decision.status === "unknown")
|
|
905
|
+
.map((decision) => decision.criterionId),
|
|
906
|
+
discoveredEvidence: runAcceptanceResult.runAcceptance.criterionDecisions.flatMap((decision) => decision.evidenceIds),
|
|
907
|
+
nextActions: ["补充缺失证据后执行 resume", "检查阻塞 criterion 后重新验收"],
|
|
908
|
+
failureIds: runAcceptanceResult.runAcceptance.criterionDecisions.flatMap((decision) => decision.failureIds),
|
|
909
|
+
criterionIds: runAcceptanceResult.runAcceptance.criterionDecisions
|
|
910
|
+
.filter((decision) => decision.status === "blocked" || decision.status === "unknown")
|
|
911
|
+
.map((decision) => decision.criterionId),
|
|
912
|
+
allowedOperations: ["resume", "report", "cancel"],
|
|
913
|
+
}
|
|
914
|
+
: undefined;
|
|
896
915
|
const completed = withPlannedStatus(withParts(parsePlannedRun({
|
|
897
916
|
...currentRun,
|
|
898
917
|
runAcceptance: runAcceptanceResult.runAcceptance,
|
|
918
|
+
...(acceptanceBlockingDetails === undefined
|
|
919
|
+
? {}
|
|
920
|
+
: { blockingDetails: acceptanceBlockingDetails }),
|
|
899
921
|
}), {
|
|
900
922
|
evidence: currentRun.evidence,
|
|
901
923
|
failures: runAcceptanceResult.failure === undefined
|
|
@@ -921,10 +943,6 @@ export const createTaskExecutor = (input) => {
|
|
|
921
943
|
verificationCommands: commands,
|
|
922
944
|
defaultWorkingDirectory: runWorkingDirectory(run),
|
|
923
945
|
});
|
|
924
|
-
if (result.records.length === 0 ||
|
|
925
|
-
result.records.some((record) => record.status !== VerificationExecutionStatus.passed)) {
|
|
926
|
-
return persistIntegrationVerificationFailure(run, task, "integration verification failed");
|
|
927
|
-
}
|
|
928
946
|
const evidence = result.records.map((record, index) => createEvidence({
|
|
929
947
|
ids: input.options.ids,
|
|
930
948
|
runId: run.runId,
|
|
@@ -940,6 +958,63 @@ export const createTaskExecutor = (input) => {
|
|
|
940
958
|
agentResults: run.agentResults,
|
|
941
959
|
workspace: run.workspace,
|
|
942
960
|
});
|
|
961
|
+
if (result.records.length === 0) {
|
|
962
|
+
const failure = createContextGapFailure({
|
|
963
|
+
failureId: input.options.ids.createFailureId(`${run.runId}-integration-empty`),
|
|
964
|
+
runId: run.runId,
|
|
965
|
+
taskId: task.taskId,
|
|
966
|
+
relatedEvidenceIds: [],
|
|
967
|
+
summary: "Integration verification produced no evidence records.",
|
|
968
|
+
detail: "At least one integration verification record is required before acceptance.",
|
|
969
|
+
recoveryPlan: "Rerun integration verification and capture a real result.",
|
|
970
|
+
missingContext: ["integration-verification-record"],
|
|
971
|
+
});
|
|
972
|
+
return (await analyzeFailure({
|
|
973
|
+
run: withParts(verified, {
|
|
974
|
+
evidence: verified.evidence,
|
|
975
|
+
failures: [...verified.failures, failure],
|
|
976
|
+
agentResults: verified.agentResults,
|
|
977
|
+
workspace: verified.workspace,
|
|
978
|
+
}),
|
|
979
|
+
task,
|
|
980
|
+
attempt: 1,
|
|
981
|
+
failure,
|
|
982
|
+
evidence,
|
|
983
|
+
})).run;
|
|
984
|
+
}
|
|
985
|
+
if (result.records.some((record) => record.status !== VerificationExecutionStatus.passed)) {
|
|
986
|
+
const failure = createImplementationFailure({
|
|
987
|
+
failureId: input.options.ids.createFailureId(`${run.runId}-integration-failed`),
|
|
988
|
+
runId: run.runId,
|
|
989
|
+
taskId: task.taskId,
|
|
990
|
+
...(evidence[0] === undefined ? {} : { failingEvidenceId: evidence[0].evidenceId }),
|
|
991
|
+
summary: "Integration verification failed after task execution.",
|
|
992
|
+
detail: "A run-level integration verification command returned a failed result.",
|
|
993
|
+
recoveryPlan: "Analyze the integration failure before authorizing a bounded repair.",
|
|
994
|
+
});
|
|
995
|
+
const analyzed = await analyzeFailure({
|
|
996
|
+
run: withParts(verified, {
|
|
997
|
+
evidence: verified.evidence,
|
|
998
|
+
failures: [...verified.failures, failure],
|
|
999
|
+
agentResults: verified.agentResults,
|
|
1000
|
+
workspace: verified.workspace,
|
|
1001
|
+
}),
|
|
1002
|
+
task,
|
|
1003
|
+
attempt: 1,
|
|
1004
|
+
failure,
|
|
1005
|
+
evidence,
|
|
1006
|
+
});
|
|
1007
|
+
if (analyzed.repairRequested && analyzed.run.status === "executing" && task.repairEnabled) {
|
|
1008
|
+
return handleRepairRequest({
|
|
1009
|
+
run: analyzed.run,
|
|
1010
|
+
task,
|
|
1011
|
+
attempt: 1,
|
|
1012
|
+
failure: analyzed.failure,
|
|
1013
|
+
evidence,
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
return analyzed.run;
|
|
1017
|
+
}
|
|
943
1018
|
await input.writer.persist(verified, input.writer.createEvent(run.runId, "run.integration.verified", {
|
|
944
1019
|
evidenceIds: evidence.map((item) => item.evidenceId),
|
|
945
1020
|
}));
|
|
@@ -948,34 +1023,42 @@ export const createTaskExecutor = (input) => {
|
|
|
948
1023
|
catch (error) {
|
|
949
1024
|
if (!(error instanceof Error))
|
|
950
1025
|
throw error;
|
|
951
|
-
|
|
1026
|
+
const failure = createVerificationEnvironmentFailure({
|
|
1027
|
+
failureId: input.options.ids.createFailureId(`${run.runId}-integration-environment`),
|
|
1028
|
+
runId: run.runId,
|
|
1029
|
+
taskId: task.taskId,
|
|
1030
|
+
commandName: commands[0]?.name ?? "integration verification",
|
|
1031
|
+
detail: error.message,
|
|
1032
|
+
});
|
|
1033
|
+
const blocked = withPlannedStatus(withParts(run, {
|
|
1034
|
+
evidence: run.evidence,
|
|
1035
|
+
failures: [...run.failures, failure],
|
|
1036
|
+
agentResults: run.agentResults,
|
|
1037
|
+
workspace: run.workspace,
|
|
1038
|
+
}), "blocked");
|
|
1039
|
+
const withDetails = parsePlannedRun({
|
|
1040
|
+
...blocked,
|
|
1041
|
+
blockingDetails: {
|
|
1042
|
+
kind: "blocked",
|
|
1043
|
+
missingContext: ["integration-environment"],
|
|
1044
|
+
discoveredEvidence: [],
|
|
1045
|
+
nextActions: ["修复集成验证环境后执行 resume"],
|
|
1046
|
+
failureIds: [failure.failureId],
|
|
1047
|
+
criterionIds: [],
|
|
1048
|
+
allowedOperations: ["resume", "report", "cancel"],
|
|
1049
|
+
},
|
|
1050
|
+
});
|
|
1051
|
+
await input.writer.persist(withDetails, input.writer.createEvent(run.runId, "run.integration.blocked", {
|
|
1052
|
+
failureId: failure.failureId,
|
|
1053
|
+
}));
|
|
1054
|
+
return withDetails;
|
|
952
1055
|
}
|
|
953
1056
|
};
|
|
954
|
-
const persistIntegrationVerificationFailure = async (run, task, detail) => {
|
|
955
|
-
const failure = createVerificationEnvironmentFailure({
|
|
956
|
-
failureId: input.options.ids.createFailureId(`${run.runId}-integration`),
|
|
957
|
-
runId: run.runId,
|
|
958
|
-
taskId: task.taskId,
|
|
959
|
-
commandName: "integration verification",
|
|
960
|
-
detail,
|
|
961
|
-
});
|
|
962
|
-
const nextRun = withParts(run, {
|
|
963
|
-
evidence: run.evidence,
|
|
964
|
-
failures: [...run.failures, failure],
|
|
965
|
-
agentResults: run.agentResults,
|
|
966
|
-
workspace: run.workspace,
|
|
967
|
-
});
|
|
968
|
-
await input.writer.persist(nextRun, input.writer.createEvent(run.runId, "run.integration.failed", {
|
|
969
|
-
failureId: failure.failureId,
|
|
970
|
-
}));
|
|
971
|
-
return nextRun;
|
|
972
|
-
};
|
|
973
1057
|
return { runTasks };
|
|
974
|
-
async function runParallelReadyBatch(run, orderedTasks) {
|
|
1058
|
+
async function runParallelReadyBatch(run, orderedTasks, completedTaskIds) {
|
|
975
1059
|
if (run.workspace.git.mode !== "git") {
|
|
976
1060
|
return { kind: "skipped" };
|
|
977
1061
|
}
|
|
978
|
-
const completedTaskIds = new Set(orderedTasks.filter((task) => task.status === "completed").map((task) => task.taskId));
|
|
979
1062
|
const readyTasks = orderedTasks.filter((task) => task.status !== "completed" &&
|
|
980
1063
|
task.dependencies.every((dependency) => completedTaskIds.has(dependency)));
|
|
981
1064
|
if (readyTasks.length < 2) {
|
|
@@ -1046,7 +1129,10 @@ export const createTaskExecutor = (input) => {
|
|
|
1046
1129
|
return { kind: "completed", run: serialRun };
|
|
1047
1130
|
}
|
|
1048
1131
|
case "parallel-completed":
|
|
1049
|
-
return {
|
|
1132
|
+
return {
|
|
1133
|
+
kind: "completed",
|
|
1134
|
+
run: mergeChildRuns(run, childRuns, new Map(result.results.map((item) => [item.task.taskId, item.lease]))),
|
|
1135
|
+
};
|
|
1050
1136
|
case "merge-conflict":
|
|
1051
1137
|
return { kind: "paused", run: await persistMergeConflict(run, result.conflict.detail) };
|
|
1052
1138
|
case "execution-failed":
|
|
@@ -1061,24 +1147,68 @@ export const createTaskExecutor = (input) => {
|
|
|
1061
1147
|
const result = run.agentResults.findLast((candidate) => candidate.role === "worker" && candidate.taskId === task.taskId);
|
|
1062
1148
|
return result?.status === "completed" ? result.changedPaths : [];
|
|
1063
1149
|
}
|
|
1064
|
-
function mergeChildRuns(parentRun, childRuns) {
|
|
1150
|
+
function mergeChildRuns(parentRun, childRuns, leasesByTaskId) {
|
|
1065
1151
|
let merged = parentRun;
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1152
|
+
const appliedDeltaIds = new Set();
|
|
1153
|
+
const deltas = childRuns
|
|
1154
|
+
.map((childRun) => {
|
|
1155
|
+
const task = childRun.taskGraph.tasks.find((candidate) => candidate.status === "completed" && candidate.taskId);
|
|
1156
|
+
if (task === undefined)
|
|
1157
|
+
return undefined;
|
|
1158
|
+
const lease = leasesByTaskId.get(task.taskId);
|
|
1159
|
+
if (lease === undefined)
|
|
1160
|
+
return undefined;
|
|
1161
|
+
return createParallelTaskDelta({
|
|
1162
|
+
parentRun,
|
|
1163
|
+
childRun,
|
|
1164
|
+
task,
|
|
1165
|
+
lease,
|
|
1166
|
+
});
|
|
1167
|
+
})
|
|
1168
|
+
.filter((delta) => delta !== undefined)
|
|
1169
|
+
.sort((left, right) => left.taskId.localeCompare(right.taskId));
|
|
1170
|
+
for (const delta of deltas) {
|
|
1171
|
+
if (delta.runId !== merged.runId)
|
|
1172
|
+
continue;
|
|
1173
|
+
if (appliedDeltaIds.has(delta.deltaId))
|
|
1174
|
+
continue;
|
|
1175
|
+
const parentTask = merged.taskGraph.tasks.find((candidate) => candidate.taskId === delta.taskId);
|
|
1176
|
+
if (parentTask === undefined)
|
|
1177
|
+
continue;
|
|
1178
|
+
if (delta.baselineDigest !== parentRun.workspace.git.baselineDiffDigest) {
|
|
1179
|
+
throw new Error(`stale parallel task delta baseline for ${delta.taskId}`);
|
|
1072
1180
|
}
|
|
1181
|
+
appliedDeltaIds.add(delta.deltaId);
|
|
1182
|
+
merged = withTask(merged, delta.task);
|
|
1073
1183
|
merged = withParts(merged, {
|
|
1074
|
-
evidence: uniqueById([...merged.evidence, ...
|
|
1075
|
-
failures: uniqueById([...merged.failures, ...
|
|
1076
|
-
agentResults: uniqueById([...merged.agentResults, ...
|
|
1077
|
-
workspace:
|
|
1184
|
+
evidence: uniqueById([...merged.evidence, ...delta.evidence], (item) => item.evidenceId),
|
|
1185
|
+
failures: uniqueById([...merged.failures, ...delta.failures], (item) => item.failureId),
|
|
1186
|
+
agentResults: uniqueById([...merged.agentResults, ...delta.agentResults], (item) => item.resultId),
|
|
1187
|
+
workspace: merged.workspace,
|
|
1078
1188
|
});
|
|
1189
|
+
merged = applyBudgetDelta(merged, delta.budgetDelta);
|
|
1079
1190
|
}
|
|
1080
1191
|
return merged;
|
|
1081
1192
|
}
|
|
1193
|
+
function applyBudgetDelta(run, delta) {
|
|
1194
|
+
return parsePlannedRun({
|
|
1195
|
+
...run,
|
|
1196
|
+
budgets: Object.fromEntries(Object.entries(run.budgets).map(([key, counter]) => [
|
|
1197
|
+
key,
|
|
1198
|
+
{
|
|
1199
|
+
...counter,
|
|
1200
|
+
used: checkedBudgetUsage(counter.used, delta[key] ?? 0, counter.limit, key, run.runId),
|
|
1201
|
+
},
|
|
1202
|
+
])),
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
function checkedBudgetUsage(used, delta, limit, budget, runId) {
|
|
1206
|
+
const next = used + delta;
|
|
1207
|
+
if (next > limit) {
|
|
1208
|
+
throw new RunBudgetExceededError(runId, budget, limit);
|
|
1209
|
+
}
|
|
1210
|
+
return next;
|
|
1211
|
+
}
|
|
1082
1212
|
async function persistMergeConflict(run, detail) {
|
|
1083
1213
|
const task = firstIncompleteTask(run);
|
|
1084
1214
|
const failure = createMergeConflictFailure({
|
|
@@ -1149,6 +1279,54 @@ export const createTaskExecutor = (input) => {
|
|
|
1149
1279
|
}));
|
|
1150
1280
|
return paused;
|
|
1151
1281
|
}
|
|
1282
|
+
async function reassessAfterFailure(run) {
|
|
1283
|
+
const failure = run.failures.at(-1);
|
|
1284
|
+
if (failure === undefined ||
|
|
1285
|
+
!["implementation-defect", "scope-drift", "merge-conflict"].includes(failure.category)) {
|
|
1286
|
+
return run;
|
|
1287
|
+
}
|
|
1288
|
+
const affectedRemainingTasks = run.taskGraph.tasks
|
|
1289
|
+
.filter((task) => task.status !== "completed")
|
|
1290
|
+
.map((task) => task.taskId);
|
|
1291
|
+
if (affectedRemainingTasks.length === 0 || run.resolvedTier === "complex") {
|
|
1292
|
+
return run;
|
|
1293
|
+
}
|
|
1294
|
+
const event = {
|
|
1295
|
+
triggerTaskId: failure.taskId,
|
|
1296
|
+
previousTier: run.resolvedTier,
|
|
1297
|
+
newTier: "complex",
|
|
1298
|
+
reason: `Failure ${failure.failureId} requires conservative complexity escalation`,
|
|
1299
|
+
affectedRemainingTasks,
|
|
1300
|
+
};
|
|
1301
|
+
const updated = parsePlannedRun({
|
|
1302
|
+
...run,
|
|
1303
|
+
resolvedTier: "complex",
|
|
1304
|
+
resolvedRoute: "full",
|
|
1305
|
+
routingDecision: {
|
|
1306
|
+
...run.routingDecision,
|
|
1307
|
+
tier: "complex",
|
|
1308
|
+
governance: "full",
|
|
1309
|
+
scheduling: run.routingDecision?.scheduling ?? "serial",
|
|
1310
|
+
source: "reassessment",
|
|
1311
|
+
reasons: [event.reason],
|
|
1312
|
+
},
|
|
1313
|
+
reassessmentHistory: [...run.reassessmentHistory, event],
|
|
1314
|
+
taskGraph: {
|
|
1315
|
+
...run.taskGraph,
|
|
1316
|
+
tasks: run.taskGraph.tasks.map((task) => affectedRemainingTasks.includes(task.taskId)
|
|
1317
|
+
? { ...task, complexityTier: "complex" }
|
|
1318
|
+
: task),
|
|
1319
|
+
},
|
|
1320
|
+
});
|
|
1321
|
+
await input.writer.persist(updated, input.writer.createEvent(run.runId, "task.reassessment.after_failure", {
|
|
1322
|
+
triggerTaskId: event.triggerTaskId,
|
|
1323
|
+
failureId: failure.failureId,
|
|
1324
|
+
previousTier: event.previousTier,
|
|
1325
|
+
newTier: event.newTier,
|
|
1326
|
+
affectedRemainingTasks,
|
|
1327
|
+
}));
|
|
1328
|
+
return updated;
|
|
1329
|
+
}
|
|
1152
1330
|
};
|
|
1153
1331
|
const findWorkerScopeViolations = (task, output) => {
|
|
1154
1332
|
const violations = [];
|