@engineeros/connector 0.14.2 → 0.14.4
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 +1 -1
- package/bin/engineeros-connector.mjs +165 -168
- package/package.json +1 -1
- package/src/runner.mjs +92 -88
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ npx --yes @engineeros/connector@latest pair PAIRING-CODE --url https://your-engi
|
|
|
47
47
|
|
|
48
48
|
The connector uploads a bounded ZIP snapshot for a safe file inventory, then stays online for deep assessments, rescans, and Goal Runs. Inventory never executes repository code. It excludes known secrets, dependency directories, build output, compiled binaries, files larger than 5 MB, agent-tool caches, Git metadata, and connector state before upload.
|
|
49
49
|
|
|
50
|
-
From **Project steering -> Workspace**, run the workspace assessment to use the connected agent subscription already authenticated on that computer. Select any combination of fifteen brownfield assessment domains, collectively covering 133 baseline checks from product and architecture through security, privacy, project-specific compliance, delivery, reliability, governance, and modernization planning. Every domain uses a detailed structured-Markdown contract. EngineerOS stores each completed stage, resumes unfinished work after a connector restart, retry, or temporary connection loss, and then synthesizes the selected results into System State. Connector `0.14.
|
|
50
|
+
From **Project steering -> Workspace**, run the workspace assessment to use the connected agent subscription already authenticated on that computer. Select any combination of fifteen brownfield assessment domains, collectively covering 133 baseline checks from product and architecture through security, privacy, project-specific compliance, delivery, reliability, governance, and modernization planning. Every domain uses a detailed structured-Markdown contract. EngineerOS stores each completed stage, resumes unfinished work after a connector restart, retry, or temporary connection loss, and then synthesizes the selected results into System State. Connector `0.14.4` runs up to three independent assessment stages for every supported coding-agent protocol, including ACP agents such as OpenCode. The connector owns the worker pool and sends one aggregate heartbeat containing every active worker's phase, progress, last activity, and event count; workers no longer compete to send individual WebSocket progress heartbeats. Live output remains coalesced separately because it is durable report content. Capability detail and project-specific compliance controls fan out after their catalogs, and final synthesis waits for every selected result. Compliance findings describe repository-verifiable engineering readiness and missing external context, never legal compliance or certification. Each result, live output, validation correction, and retry remains independently durable. The connector prints each worker's safe activity and elapsed time every 30 seconds, stops a worker that produces no agent activity for ten minutes instead of waiting indefinitely, and gives an incomplete report one bounded correction turn instead of repeatedly restarting it. Assessment cannot modify the workspace.
|
|
51
51
|
|
|
52
52
|
After onboarding, every project prompt is routed to this connection. Copilot, shaping, planning, architecture, and experience generation use the connected agent subscription and workspace context. Interactive prompts run independently from assessments and Goal scheduling. Prompt runs are read-only; only an explicitly registered Goal Run receives workspace-write access. If the connector is offline, EngineerOS asks the user to reconnect instead of silently switching models.
|
|
53
53
|
|
|
@@ -12,6 +12,9 @@ import {
|
|
|
12
12
|
workspaceUrl,
|
|
13
13
|
} from "../src/config.mjs";
|
|
14
14
|
import {
|
|
15
|
+
ASSESSMENT_OUTPUT_FLUSH_INTERVAL_MS,
|
|
16
|
+
ASSESSMENT_RESULT_TIMEOUT_MS,
|
|
17
|
+
assessmentWorkerSnapshots,
|
|
15
18
|
assessmentInactivityFailure,
|
|
16
19
|
assessmentProgressMessage,
|
|
17
20
|
assessmentStreamDelta,
|
|
@@ -20,15 +23,14 @@ import {
|
|
|
20
23
|
executeAssignment,
|
|
21
24
|
executeConnectedPrompt,
|
|
22
25
|
executeWorkspaceAssessment,
|
|
23
|
-
inspectCodingAgent,
|
|
24
|
-
promptStreamEvent,
|
|
25
|
-
requeueInterruptedAssessment,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
workspaceSnapshot,
|
|
26
|
+
inspectCodingAgent,
|
|
27
|
+
promptStreamEvent,
|
|
28
|
+
requeueInterruptedAssessment,
|
|
29
|
+
stopProcess,
|
|
30
|
+
submitAssessmentResultWithRetry,
|
|
31
|
+
takeWorkspaceAssessmentWave,
|
|
32
|
+
workspaceAssessmentWorkerLimit,
|
|
33
|
+
workspaceSnapshot,
|
|
32
34
|
} from "../src/runner.mjs";
|
|
33
35
|
import { disposeAcpRuntimes } from "../src/acp-client.mjs";
|
|
34
36
|
import {
|
|
@@ -199,15 +201,13 @@ firstMessage.capabilities = capabilities;
|
|
|
199
201
|
|
|
200
202
|
let stopped = false;
|
|
201
203
|
let active = null;
|
|
202
|
-
const activeAssessments = new Map();
|
|
203
|
-
const ASSESSMENT_WORKER_LIMIT = workspaceAssessmentWorkerLimit(
|
|
204
|
-
codingAgent.protocol,
|
|
205
|
-
);
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
);
|
|
210
|
-
}
|
|
204
|
+
const activeAssessments = new Map();
|
|
205
|
+
const ASSESSMENT_WORKER_LIMIT = workspaceAssessmentWorkerLimit(
|
|
206
|
+
codingAgent.protocol,
|
|
207
|
+
);
|
|
208
|
+
console.log(
|
|
209
|
+
`Assessment supervisor can run up to ${ASSESSMENT_WORKER_LIMIT} independent ${codingAgent.name} workers.`,
|
|
210
|
+
);
|
|
211
211
|
const available = [];
|
|
212
212
|
const assessments = [];
|
|
213
213
|
const activePrompts = new Map();
|
|
@@ -354,11 +354,11 @@ async function connect() {
|
|
|
354
354
|
socket.addEventListener("close", (event) => {
|
|
355
355
|
clearConnectionWatchdog?.();
|
|
356
356
|
clearInterval(pingTimer);
|
|
357
|
-
for (const promptState of activePrompts.values())
|
|
358
|
-
void cancelPrompt(promptState);
|
|
359
|
-
for (const assessmentState of activeAssessments.values()) {
|
|
360
|
-
if (event.code === 4001) void stopProcess(assessmentState.child);
|
|
361
|
-
}
|
|
357
|
+
for (const promptState of activePrompts.values())
|
|
358
|
+
void cancelPrompt(promptState);
|
|
359
|
+
for (const assessmentState of activeAssessments.values()) {
|
|
360
|
+
if (event.code === 4001) void stopProcess(assessmentState.child);
|
|
361
|
+
}
|
|
362
362
|
if (connectionRejected) {
|
|
363
363
|
stopped = true;
|
|
364
364
|
console.error(
|
|
@@ -421,7 +421,7 @@ async function submitWorkspaceSnapshot() {
|
|
|
421
421
|
`Inventoried ${snapshot.total_file_count.toLocaleString()} safe file(s).`,
|
|
422
422
|
);
|
|
423
423
|
console.log(
|
|
424
|
-
`Uploaded ${snapshot.evidence_file_count.toLocaleString()} prioritized source file(s).`,
|
|
424
|
+
`Uploaded ${snapshot.evidence_file_count.toLocaleString()} prioritized source file(s).`,
|
|
425
425
|
);
|
|
426
426
|
console.log(
|
|
427
427
|
`Excluded ${snapshot.excluded_file_count.toLocaleString()} sensitive or generated file(s).`,
|
|
@@ -453,16 +453,29 @@ async function submitWorkspaceSnapshot() {
|
|
|
453
453
|
|
|
454
454
|
function startPings() {
|
|
455
455
|
clearInterval(pingTimer);
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
456
|
+
sendPing();
|
|
457
|
+
pingTimer = setInterval(sendPing, 10_000);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function sendPing() {
|
|
461
|
+
if (socket.readyState !== WebSocket.OPEN) return;
|
|
462
|
+
try {
|
|
463
|
+
socket.send(
|
|
464
|
+
JSON.stringify({
|
|
465
|
+
type: "ping",
|
|
466
|
+
active_run_id: active?.kind === "goal" ? active.runId : null,
|
|
467
|
+
assessment_workers: assessmentWorkerSnapshots(activeAssessments),
|
|
468
|
+
}),
|
|
469
|
+
);
|
|
470
|
+
} catch (error) {
|
|
471
|
+
lastConnectionError = protocolFailureMessage(error);
|
|
472
|
+
try {
|
|
473
|
+
socket.close();
|
|
474
|
+
} catch {
|
|
475
|
+
// The reconnect timer below owns recovery.
|
|
464
476
|
}
|
|
465
|
-
|
|
477
|
+
scheduleReconnect();
|
|
478
|
+
}
|
|
466
479
|
}
|
|
467
480
|
|
|
468
481
|
function sendPromptEvent(promptId, event) {
|
|
@@ -491,13 +504,20 @@ function pump() {
|
|
|
491
504
|
kind: "assessment",
|
|
492
505
|
key: assessmentKey,
|
|
493
506
|
runId: assessment.assessment_id,
|
|
494
|
-
stage: assessment.stage,
|
|
495
|
-
child: null,
|
|
496
|
-
resumeAssignment: null,
|
|
497
|
-
|
|
507
|
+
stage: assessment.stage,
|
|
508
|
+
child: null,
|
|
509
|
+
resumeAssignment: null,
|
|
510
|
+
phase: "starting",
|
|
511
|
+
progressPercent: 5,
|
|
512
|
+
lastMessage: `Starting ${assessment.stage} assessment stage`,
|
|
513
|
+
startedAt: Date.now(),
|
|
514
|
+
lastActivityAt: Date.now(),
|
|
515
|
+
eventCount: 0,
|
|
516
|
+
};
|
|
498
517
|
activeAssessments.set(assessmentKey, assessmentState);
|
|
499
518
|
void executeAssessment(assessment, assessmentState);
|
|
500
519
|
}
|
|
520
|
+
if (wave.length > 0) sendPing();
|
|
501
521
|
if (activeAssessments.size > 0 || assessments.length > 0) return;
|
|
502
522
|
const runId = available.shift();
|
|
503
523
|
if (!runId) return;
|
|
@@ -610,12 +630,9 @@ async function executeAssessment(assignment, assessmentState) {
|
|
|
610
630
|
const assessmentId = assignment.assessment_id;
|
|
611
631
|
const stage = assignment.stage;
|
|
612
632
|
console.log(`Agent is running assessment stage ${stage} (${assessmentId}).`);
|
|
613
|
-
const stageStartedAt =
|
|
614
|
-
let
|
|
615
|
-
let
|
|
616
|
-
let agentEventCount = 0;
|
|
617
|
-
let agentProcessStarted = false;
|
|
618
|
-
let agentCompleted = false;
|
|
633
|
+
const stageStartedAt = assessmentState.startedAt;
|
|
634
|
+
let agentProcessStarted = false;
|
|
635
|
+
let agentCompleted = false;
|
|
619
636
|
let statusTicks = 0;
|
|
620
637
|
let progress = 5;
|
|
621
638
|
let outputBuffer = "";
|
|
@@ -623,31 +640,28 @@ async function executeAssessment(assignment, assessmentState) {
|
|
|
623
640
|
let outputTimer;
|
|
624
641
|
let accepted = false;
|
|
625
642
|
let failureReported = false;
|
|
626
|
-
let inactivityFailure = null;
|
|
627
|
-
let lastReportedMilestone = null;
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
const
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
}
|
|
649
|
-
};
|
|
650
|
-
const reportProgress = (message, { milestone = true } = {}) => {
|
|
643
|
+
let inactivityFailure = null;
|
|
644
|
+
let lastReportedMilestone = null;
|
|
645
|
+
const creditedMilestones = new Set();
|
|
646
|
+
const isActiveAssessment = () =>
|
|
647
|
+
activeAssessments.get(assessmentState.key) === assessmentState;
|
|
648
|
+
const sendAssessmentMessage = (payload) => {
|
|
649
|
+
if (socket.readyState !== WebSocket.OPEN) return false;
|
|
650
|
+
try {
|
|
651
|
+
socket.send(JSON.stringify(payload));
|
|
652
|
+
return true;
|
|
653
|
+
} catch (error) {
|
|
654
|
+
lastConnectionError = protocolFailureMessage(error);
|
|
655
|
+
try {
|
|
656
|
+
socket.close();
|
|
657
|
+
} catch {
|
|
658
|
+
// The reconnect timer below owns recovery.
|
|
659
|
+
}
|
|
660
|
+
scheduleReconnect();
|
|
661
|
+
return false;
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
const reportProgress = (message, { milestone = true } = {}) => {
|
|
651
665
|
if (!isActiveAssessment()) return;
|
|
652
666
|
if (milestone && lastReportedMilestone === message) return;
|
|
653
667
|
if (milestone) {
|
|
@@ -656,38 +670,11 @@ async function executeAssessment(assignment, assessmentState) {
|
|
|
656
670
|
creditedMilestones.add(message);
|
|
657
671
|
progress = Math.min(90, progress + 10);
|
|
658
672
|
}
|
|
659
|
-
lastAgentStatus = message;
|
|
660
673
|
console.log(`[assessment:${stage}] ${message}.`);
|
|
661
674
|
}
|
|
662
|
-
const boundedMessage = String(message).slice(0, 500);
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
!shouldReportAssessmentProgress(
|
|
666
|
-
{
|
|
667
|
-
message: boundedMessage,
|
|
668
|
-
progressPercent: progress,
|
|
669
|
-
lastMessage: lastProgressMessage,
|
|
670
|
-
lastProgressPercent,
|
|
671
|
-
lastSentAt: lastProgressSentAt,
|
|
672
|
-
},
|
|
673
|
-
{ now },
|
|
674
|
-
)
|
|
675
|
-
) {
|
|
676
|
-
return;
|
|
677
|
-
}
|
|
678
|
-
if (
|
|
679
|
-
sendAssessmentMessage({
|
|
680
|
-
type: "workspace.assessment.progress",
|
|
681
|
-
assessment_id: assessmentId,
|
|
682
|
-
stage,
|
|
683
|
-
progress_percent: progress,
|
|
684
|
-
message: boundedMessage,
|
|
685
|
-
})
|
|
686
|
-
) {
|
|
687
|
-
lastProgressMessage = boundedMessage;
|
|
688
|
-
lastProgressPercent = progress;
|
|
689
|
-
lastProgressSentAt = now;
|
|
690
|
-
}
|
|
675
|
+
const boundedMessage = String(message).slice(0, 500);
|
|
676
|
+
assessmentState.progressPercent = progress;
|
|
677
|
+
assessmentState.lastMessage = boundedMessage;
|
|
691
678
|
};
|
|
692
679
|
const flushOutput = () => {
|
|
693
680
|
if (outputTimer) clearTimeout(outputTimer);
|
|
@@ -700,14 +687,14 @@ async function executeAssessment(assignment, assessmentState) {
|
|
|
700
687
|
!isActiveAssessment()
|
|
701
688
|
) {
|
|
702
689
|
return;
|
|
703
|
-
}
|
|
704
|
-
for (let offset = 0; offset < output.length; offset += 50_000) {
|
|
705
|
-
sendAssessmentMessage({
|
|
706
|
-
type: "workspace.assessment.output",
|
|
707
|
-
assessment_id: assessmentId,
|
|
708
|
-
stage,
|
|
709
|
-
delta: output.slice(offset, offset + 50_000),
|
|
710
|
-
});
|
|
690
|
+
}
|
|
691
|
+
for (let offset = 0; offset < output.length; offset += 50_000) {
|
|
692
|
+
sendAssessmentMessage({
|
|
693
|
+
type: "workspace.assessment.output",
|
|
694
|
+
assessment_id: assessmentId,
|
|
695
|
+
stage,
|
|
696
|
+
delta: output.slice(offset, offset + 50_000),
|
|
697
|
+
});
|
|
711
698
|
}
|
|
712
699
|
};
|
|
713
700
|
const reportOutput = (delta) => {
|
|
@@ -719,16 +706,19 @@ async function executeAssessment(assignment, assessmentState) {
|
|
|
719
706
|
if (outputBuffer.length >= 50_000) {
|
|
720
707
|
flushOutput();
|
|
721
708
|
} else if (!outputTimer) {
|
|
722
|
-
outputTimer = setTimeout(
|
|
709
|
+
outputTimer = setTimeout(
|
|
710
|
+
flushOutput,
|
|
711
|
+
ASSESSMENT_OUTPUT_FLUSH_INTERVAL_MS,
|
|
712
|
+
);
|
|
723
713
|
}
|
|
724
714
|
};
|
|
725
715
|
reportProgress(`Starting ${stage} assessment stage`);
|
|
726
716
|
const heartbeat = setInterval(() => {
|
|
727
717
|
const now = Date.now();
|
|
728
|
-
const inactiveMs = now -
|
|
729
|
-
const nextInactivityFailure = agentProcessStarted && !agentCompleted
|
|
730
|
-
? assessmentInactivityFailure(stage, inactiveMs)
|
|
731
|
-
: null;
|
|
718
|
+
const inactiveMs = now - assessmentState.lastActivityAt;
|
|
719
|
+
const nextInactivityFailure = agentProcessStarted && !agentCompleted
|
|
720
|
+
? assessmentInactivityFailure(stage, inactiveMs)
|
|
721
|
+
: null;
|
|
732
722
|
if (!inactivityFailure && nextInactivityFailure) {
|
|
733
723
|
inactivityFailure = nextInactivityFailure;
|
|
734
724
|
console.error(`[assessment:${stage}] ${inactivityFailure}`);
|
|
@@ -736,42 +726,45 @@ async function executeAssessment(assignment, assessmentState) {
|
|
|
736
726
|
return;
|
|
737
727
|
}
|
|
738
728
|
if (inactivityFailure) return;
|
|
739
|
-
reportProgress("Assessment in progress", { milestone: false });
|
|
740
729
|
statusTicks += 1;
|
|
741
730
|
if (statusTicks % 2 === 0) {
|
|
742
731
|
console.log(
|
|
743
732
|
`[assessment:${stage}] ${formatAssessmentDuration(now - stageStartedAt)} elapsed · ` +
|
|
744
|
-
`${
|
|
745
|
-
`(${
|
|
733
|
+
`${assessmentState.lastMessage} · last agent activity ${formatAssessmentDuration(inactiveMs)} ago ` +
|
|
734
|
+
`(${assessmentState.eventCount.toLocaleString()} events).`,
|
|
746
735
|
);
|
|
747
736
|
}
|
|
748
737
|
}, 15_000);
|
|
749
738
|
try {
|
|
750
|
-
if (assignment.assessment_mode === "incremental") {
|
|
751
|
-
progress = 15;
|
|
752
|
-
reportProgress("Calculating changed files and affected behavior");
|
|
739
|
+
if (assignment.assessment_mode === "incremental") {
|
|
740
|
+
progress = 15;
|
|
741
|
+
reportProgress("Calculating changed files and affected behavior");
|
|
753
742
|
}
|
|
754
|
-
let result = await executeWorkspaceAssessment(assignment, config, {
|
|
743
|
+
let result = await executeWorkspaceAssessment(assignment, config, {
|
|
755
744
|
onProcess: (child) => {
|
|
756
745
|
if (isActiveAssessment()) {
|
|
757
746
|
assessmentState.child = child;
|
|
758
747
|
agentProcessStarted = true;
|
|
759
|
-
|
|
748
|
+
assessmentState.phase = "running";
|
|
749
|
+
assessmentState.lastActivityAt = Date.now();
|
|
760
750
|
reportProgress("Connected agent process started");
|
|
761
751
|
}
|
|
762
752
|
},
|
|
763
753
|
onEvent: (event) => {
|
|
764
|
-
|
|
765
|
-
|
|
754
|
+
assessmentState.lastActivityAt = Date.now();
|
|
755
|
+
assessmentState.eventCount += 1;
|
|
766
756
|
reportOutput(assessmentStreamDelta(event));
|
|
767
757
|
const message = assessmentProgressMessage(event);
|
|
768
|
-
if (message) reportProgress(message);
|
|
769
|
-
},
|
|
770
|
-
});
|
|
771
|
-
agentCompleted = true;
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
758
|
+
if (message) reportProgress(message);
|
|
759
|
+
},
|
|
760
|
+
});
|
|
761
|
+
agentCompleted = true;
|
|
762
|
+
assessmentState.phase = "delivering";
|
|
763
|
+
assessmentState.progressPercent = 95;
|
|
764
|
+
assessmentState.lastMessage = "Delivering completed stage result";
|
|
765
|
+
flushOutput();
|
|
766
|
+
const submitResult = (payload) =>
|
|
767
|
+
fetch(
|
|
775
768
|
assessmentResultUrl(
|
|
776
769
|
config.server_url,
|
|
777
770
|
config.connector_id,
|
|
@@ -779,38 +772,42 @@ async function executeAssessment(assignment, assessmentState) {
|
|
|
779
772
|
),
|
|
780
773
|
{
|
|
781
774
|
method: "POST",
|
|
782
|
-
headers: {
|
|
783
|
-
"Content-Type": "application/json",
|
|
784
|
-
Authorization: `Bearer ${config.token}`,
|
|
785
|
-
},
|
|
786
|
-
body: JSON.stringify(payload),
|
|
787
|
-
signal: AbortSignal.timeout(
|
|
788
|
-
},
|
|
789
|
-
);
|
|
790
|
-
const deliverResult = (payload) =>
|
|
791
|
-
submitAssessmentResultWithRetry(payload, submitResult, {
|
|
792
|
-
isActive: () => isActiveAssessment() && !stopped,
|
|
793
|
-
onRetry: (error, delayMs) => {
|
|
794
|
-
reportProgress("Completed result is waiting for EngineerOS", {
|
|
795
|
-
milestone: false,
|
|
796
|
-
});
|
|
797
|
-
console.warn(
|
|
798
|
-
`[assessment:${stage}] Completed result delivery failed: ${protocolFailureMessage(error)} ` +
|
|
799
|
-
`Retrying in ${Math.round(delayMs / 1_000)}s without rerunning the agent.`,
|
|
800
|
-
);
|
|
801
|
-
},
|
|
802
|
-
});
|
|
803
|
-
let response = await deliverResult(result);
|
|
804
|
-
if (response.status === 422 && result.correctAfterRejection) {
|
|
805
|
-
const rejection = await describeRejectedResponse(
|
|
806
|
-
response,
|
|
807
|
-
"the assessment",
|
|
808
|
-
);
|
|
809
|
-
agentCompleted = false;
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
775
|
+
headers: {
|
|
776
|
+
"Content-Type": "application/json",
|
|
777
|
+
Authorization: `Bearer ${config.token}`,
|
|
778
|
+
},
|
|
779
|
+
body: JSON.stringify(payload),
|
|
780
|
+
signal: AbortSignal.timeout(ASSESSMENT_RESULT_TIMEOUT_MS),
|
|
781
|
+
},
|
|
782
|
+
);
|
|
783
|
+
const deliverResult = (payload) =>
|
|
784
|
+
submitAssessmentResultWithRetry(payload, submitResult, {
|
|
785
|
+
isActive: () => isActiveAssessment() && !stopped,
|
|
786
|
+
onRetry: (error, delayMs) => {
|
|
787
|
+
reportProgress("Completed result is waiting for EngineerOS", {
|
|
788
|
+
milestone: false,
|
|
789
|
+
});
|
|
790
|
+
console.warn(
|
|
791
|
+
`[assessment:${stage}] Completed result delivery failed: ${protocolFailureMessage(error)} ` +
|
|
792
|
+
`Retrying in ${Math.round(delayMs / 1_000)}s without rerunning the agent.`,
|
|
793
|
+
);
|
|
794
|
+
},
|
|
795
|
+
});
|
|
796
|
+
let response = await deliverResult(result);
|
|
797
|
+
if (response.status === 422 && result.correctAfterRejection) {
|
|
798
|
+
const rejection = await describeRejectedResponse(
|
|
799
|
+
response,
|
|
800
|
+
"the assessment",
|
|
801
|
+
);
|
|
802
|
+
agentCompleted = false;
|
|
803
|
+
assessmentState.phase = "correcting";
|
|
804
|
+
assessmentState.lastMessage = "Correcting a rejected stage result";
|
|
805
|
+
result = await result.correctAfterRejection(rejection);
|
|
806
|
+
agentCompleted = true;
|
|
807
|
+
assessmentState.phase = "delivering";
|
|
808
|
+
assessmentState.lastMessage = "Delivering corrected stage result";
|
|
809
|
+
flushOutput();
|
|
810
|
+
response = await deliverResult(result);
|
|
814
811
|
}
|
|
815
812
|
if (!response.ok) {
|
|
816
813
|
throw new Error(
|
|
@@ -822,14 +819,14 @@ async function executeAssessment(assignment, assessmentState) {
|
|
|
822
819
|
} catch (error) {
|
|
823
820
|
flushOutput();
|
|
824
821
|
const message = inactivityFailure || protocolFailureMessage(error);
|
|
825
|
-
if (isActiveAssessment()) {
|
|
826
|
-
failureReported = sendAssessmentMessage({
|
|
827
|
-
type: "workspace.assessment.failed",
|
|
828
|
-
assessment_id: assessmentId,
|
|
829
|
-
stage,
|
|
830
|
-
message,
|
|
831
|
-
});
|
|
832
|
-
}
|
|
822
|
+
if (isActiveAssessment()) {
|
|
823
|
+
failureReported = sendAssessmentMessage({
|
|
824
|
+
type: "workspace.assessment.failed",
|
|
825
|
+
assessment_id: assessmentId,
|
|
826
|
+
stage,
|
|
827
|
+
message,
|
|
828
|
+
});
|
|
829
|
+
}
|
|
833
830
|
console.error(message);
|
|
834
831
|
} finally {
|
|
835
832
|
if (outputTimer) clearTimeout(outputTimer);
|
package/package.json
CHANGED
package/src/runner.mjs
CHANGED
|
@@ -32,6 +32,9 @@ const MAX_INVENTORY_FILES = 100_000;
|
|
|
32
32
|
const MAX_COMPRESSED_INVENTORY_BYTES = 10_000_000;
|
|
33
33
|
const EVIDENCE_POLICY_VERSION = "workspace-evidence-v1";
|
|
34
34
|
export const ASSESSMENT_INACTIVITY_TIMEOUT_MS = 10 * 60 * 1_000;
|
|
35
|
+
export const ASSESSMENT_OUTPUT_FLUSH_INTERVAL_MS = 15_000;
|
|
36
|
+
export const ASSESSMENT_RESULT_TIMEOUT_MS = 120_000;
|
|
37
|
+
export const MAX_ASSESSMENT_WORKERS = 3;
|
|
35
38
|
const EXCLUDED_DIRECTORIES = new Set([
|
|
36
39
|
".agents",
|
|
37
40
|
".claude",
|
|
@@ -697,16 +700,16 @@ export function codexFailureMessage(output, code) {
|
|
|
697
700
|
|
|
698
701
|
export function assessmentProgressMessage(event) {
|
|
699
702
|
if (!event || typeof event !== "object") return null;
|
|
700
|
-
if (event.type === "assessment.output_correction")
|
|
701
|
-
return "Completing the required stage report structure";
|
|
702
|
-
if (event.type === "agent.connected")
|
|
703
|
-
return "Connected agent is ready to inspect the workspace";
|
|
703
|
+
if (event.type === "assessment.output_correction")
|
|
704
|
+
return "Completing the required stage report structure";
|
|
705
|
+
if (event.type === "agent.connected")
|
|
706
|
+
return "Connected agent is ready to inspect the workspace";
|
|
704
707
|
if (event.type === "acp.plan")
|
|
705
708
|
return "Organizing the repository assessment plan";
|
|
706
|
-
if (event.type === "acp.agent_thought_chunk")
|
|
707
|
-
return "Reasoning through the current implementation";
|
|
708
|
-
if (event.type === "acp.agent_message_chunk")
|
|
709
|
-
return "Drafting the stage report";
|
|
709
|
+
if (event.type === "acp.agent_thought_chunk")
|
|
710
|
+
return "Reasoning through the current implementation";
|
|
711
|
+
if (event.type === "acp.agent_message_chunk")
|
|
712
|
+
return "Drafting the stage report";
|
|
710
713
|
if (event.type === "acp.tool_call" || event.type === "acp.tool_call_update") {
|
|
711
714
|
return assessmentCommandMilestone(event.update?.title);
|
|
712
715
|
}
|
|
@@ -824,7 +827,7 @@ function assessmentCommandMilestone(command) {
|
|
|
824
827
|
? command.join(" ")
|
|
825
828
|
: String(command || "");
|
|
826
829
|
const normalized = value.replace(/\s+/g, " ").trim().toLowerCase();
|
|
827
|
-
if (!normalized) return "Inspecting workspace source";
|
|
830
|
+
if (!normalized) return "Inspecting workspace source";
|
|
828
831
|
if (/\bgit\s+(status|log|diff|show|rev-parse)\b/.test(normalized)) {
|
|
829
832
|
return "Comparing Git history and workspace changes";
|
|
830
833
|
}
|
|
@@ -930,9 +933,9 @@ export function assessmentStageCorrectionPrompt(
|
|
|
930
933
|
"## Incomplete Stage Output Recovery",
|
|
931
934
|
"",
|
|
932
935
|
"The previous turn ended without a complete stage deliverable. Return the entire structured Markdown stage report now.",
|
|
933
|
-
"Use repository context already inspected in the previous turn when it is available; inspect only what remains necessary.",
|
|
936
|
+
"Use repository context already inspected in the previous turn when it is available; inspect only what remains necessary.",
|
|
934
937
|
`The first non-whitespace line of the final answer must be exactly: ${requiredOutputHeading}`,
|
|
935
|
-
"Follow every section, inspection, and completeness rule in the Assignment.",
|
|
938
|
+
"Follow every section, inspection, and completeness rule in the Assignment.",
|
|
936
939
|
"Do not return progress commentary, an explanation of the correction, or a code fence.",
|
|
937
940
|
].join("\n");
|
|
938
941
|
}
|
|
@@ -992,7 +995,7 @@ export function enqueueWorkspaceAssessment(
|
|
|
992
995
|
return "queued";
|
|
993
996
|
}
|
|
994
997
|
|
|
995
|
-
export function takeWorkspaceAssessmentWave(queue, activeCount, limit = 3) {
|
|
998
|
+
export function takeWorkspaceAssessmentWave(queue, activeCount, limit = 3) {
|
|
996
999
|
const available = Math.max(0, limit - activeCount);
|
|
997
1000
|
if (!available || !queue.length) return [];
|
|
998
1001
|
if (queue[0]?.parallelizable !== true) {
|
|
@@ -1005,36 +1008,37 @@ export function takeWorkspaceAssessmentWave(queue, activeCount, limit = 3) {
|
|
|
1005
1008
|
) {
|
|
1006
1009
|
wave.push(queue.shift());
|
|
1007
1010
|
}
|
|
1008
|
-
return wave;
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
export function workspaceAssessmentWorkerLimit(
|
|
1012
|
-
return
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
export function
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
)
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1011
|
+
return wave;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
export function workspaceAssessmentWorkerLimit() {
|
|
1015
|
+
return MAX_ASSESSMENT_WORKERS;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
export function assessmentWorkerSnapshots(activeAssessments) {
|
|
1019
|
+
return [...activeAssessments.values()]
|
|
1020
|
+
.slice(0, MAX_ASSESSMENT_WORKERS)
|
|
1021
|
+
.map((state) => ({
|
|
1022
|
+
assessment_id: String(state.runId),
|
|
1023
|
+
stage: String(state.stage).slice(0, 96),
|
|
1024
|
+
phase: state.phase,
|
|
1025
|
+
progress_percent: Math.max(
|
|
1026
|
+
0,
|
|
1027
|
+
Math.min(95, Number(state.progressPercent) || 0),
|
|
1028
|
+
),
|
|
1029
|
+
message: String(state.lastMessage || "Assessment worker is starting").slice(
|
|
1030
|
+
0,
|
|
1031
|
+
500,
|
|
1032
|
+
),
|
|
1033
|
+
started_at: new Date(state.startedAt).toISOString(),
|
|
1034
|
+
last_activity_at: new Date(state.lastActivityAt).toISOString(),
|
|
1035
|
+
event_count: Math.max(0, Number(state.eventCount) || 0),
|
|
1036
|
+
}));
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
export function requeueInterruptedAssessment(
|
|
1040
|
+
queue,
|
|
1041
|
+
assessmentState,
|
|
1038
1042
|
{ accepted, failureReported },
|
|
1039
1043
|
) {
|
|
1040
1044
|
if (accepted || failureReported || !assessmentState?.resumeAssignment) {
|
|
@@ -1047,52 +1051,52 @@ export function requeueInterruptedAssessment(
|
|
|
1047
1051
|
assessmentState.resumeAssignment,
|
|
1048
1052
|
{ front: true },
|
|
1049
1053
|
) === "queued"
|
|
1050
|
-
);
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
export async function submitAssessmentResultWithRetry(
|
|
1054
|
-
payload,
|
|
1055
|
-
submit,
|
|
1056
|
-
{
|
|
1057
|
-
isActive = () => true,
|
|
1058
|
-
onRetry = () => {},
|
|
1059
|
-
wait = (milliseconds) =>
|
|
1060
|
-
new Promise((resolve) => setTimeout(resolve, milliseconds)),
|
|
1061
|
-
initialDelayMs = 1_000,
|
|
1062
|
-
maxDelayMs = 30_000,
|
|
1063
|
-
} = {},
|
|
1064
|
-
) {
|
|
1065
|
-
let retryDelayMs = initialDelayMs;
|
|
1066
|
-
while (isActive()) {
|
|
1067
|
-
let retryFailure;
|
|
1068
|
-
try {
|
|
1069
|
-
const response = await submit(payload);
|
|
1070
|
-
if (!isTransientAssessmentResultResponse(response)) return response;
|
|
1071
|
-
await response.body?.cancel?.();
|
|
1072
|
-
retryFailure = new Error(
|
|
1073
|
-
`EngineerOS temporarily rejected the completed assessment result (${response.status}).`,
|
|
1074
|
-
);
|
|
1075
|
-
} catch (error) {
|
|
1076
|
-
retryFailure = error;
|
|
1077
|
-
}
|
|
1078
|
-
if (!isActive()) break;
|
|
1079
|
-
onRetry(retryFailure, retryDelayMs);
|
|
1080
|
-
await wait(retryDelayMs);
|
|
1081
|
-
retryDelayMs = Math.min(maxDelayMs, retryDelayMs * 2);
|
|
1082
|
-
}
|
|
1083
|
-
throw new Error("Assessment result delivery stopped before EngineerOS accepted it.");
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
function isTransientAssessmentResultResponse(response) {
|
|
1087
|
-
return (
|
|
1088
|
-
response.status === 408 ||
|
|
1089
|
-
response.status === 425 ||
|
|
1090
|
-
response.status === 429 ||
|
|
1091
|
-
response.status >= 500
|
|
1092
|
-
);
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
export function connectorExecution(assignment, options = {}) {
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
export async function submitAssessmentResultWithRetry(
|
|
1058
|
+
payload,
|
|
1059
|
+
submit,
|
|
1060
|
+
{
|
|
1061
|
+
isActive = () => true,
|
|
1062
|
+
onRetry = () => {},
|
|
1063
|
+
wait = (milliseconds) =>
|
|
1064
|
+
new Promise((resolve) => setTimeout(resolve, milliseconds)),
|
|
1065
|
+
initialDelayMs = 1_000,
|
|
1066
|
+
maxDelayMs = 30_000,
|
|
1067
|
+
} = {},
|
|
1068
|
+
) {
|
|
1069
|
+
let retryDelayMs = initialDelayMs;
|
|
1070
|
+
while (isActive()) {
|
|
1071
|
+
let retryFailure;
|
|
1072
|
+
try {
|
|
1073
|
+
const response = await submit(payload);
|
|
1074
|
+
if (!isTransientAssessmentResultResponse(response)) return response;
|
|
1075
|
+
await response.body?.cancel?.();
|
|
1076
|
+
retryFailure = new Error(
|
|
1077
|
+
`EngineerOS temporarily rejected the completed assessment result (${response.status}).`,
|
|
1078
|
+
);
|
|
1079
|
+
} catch (error) {
|
|
1080
|
+
retryFailure = error;
|
|
1081
|
+
}
|
|
1082
|
+
if (!isActive()) break;
|
|
1083
|
+
onRetry(retryFailure, retryDelayMs);
|
|
1084
|
+
await wait(retryDelayMs);
|
|
1085
|
+
retryDelayMs = Math.min(maxDelayMs, retryDelayMs * 2);
|
|
1086
|
+
}
|
|
1087
|
+
throw new Error("Assessment result delivery stopped before EngineerOS accepted it.");
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function isTransientAssessmentResultResponse(response) {
|
|
1091
|
+
return (
|
|
1092
|
+
response.status === 408 ||
|
|
1093
|
+
response.status === 425 ||
|
|
1094
|
+
response.status === 429 ||
|
|
1095
|
+
response.status >= 500
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
export function connectorExecution(assignment, options = {}) {
|
|
1096
1100
|
const rawPrompt = assignment?.prompt_markdown;
|
|
1097
1101
|
if (typeof rawPrompt !== "string" || !rawPrompt.trim()) {
|
|
1098
1102
|
throw new Error("EngineerOS assignment is missing prompt_markdown.");
|