@engineeros/connector 0.14.2 → 0.14.5
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 +178 -169
- package/package.json +1 -1
- package/src/runner.mjs +117 -111
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.5` runs up to three independent assessment stages for every supported coding-agent protocol. ACP agents such as OpenCode use isolated stage sessions within one persistent coding-agent process, avoiding the CPU and memory spike from launching three full agent runtimes. 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} stages.`,
|
|
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,21 @@ function pump() {
|
|
|
491
504
|
kind: "assessment",
|
|
492
505
|
key: assessmentKey,
|
|
493
506
|
runId: assessment.assessment_id,
|
|
494
|
-
stage: assessment.stage,
|
|
495
|
-
child: null,
|
|
496
|
-
|
|
497
|
-
|
|
507
|
+
stage: assessment.stage,
|
|
508
|
+
child: null,
|
|
509
|
+
controller: null,
|
|
510
|
+
resumeAssignment: null,
|
|
511
|
+
phase: "starting",
|
|
512
|
+
progressPercent: 5,
|
|
513
|
+
lastMessage: `Starting ${assessment.stage} assessment stage`,
|
|
514
|
+
startedAt: Date.now(),
|
|
515
|
+
lastActivityAt: Date.now(),
|
|
516
|
+
eventCount: 0,
|
|
517
|
+
};
|
|
498
518
|
activeAssessments.set(assessmentKey, assessmentState);
|
|
499
519
|
void executeAssessment(assessment, assessmentState);
|
|
500
520
|
}
|
|
521
|
+
if (wave.length > 0) sendPing();
|
|
501
522
|
if (activeAssessments.size > 0 || assessments.length > 0) return;
|
|
502
523
|
const runId = available.shift();
|
|
503
524
|
if (!runId) return;
|
|
@@ -606,16 +627,21 @@ async function cancelPrompt(promptState) {
|
|
|
606
627
|
await stopProcess(promptState.child);
|
|
607
628
|
}
|
|
608
629
|
|
|
630
|
+
async function cancelAssessment(assessmentState) {
|
|
631
|
+
if (typeof assessmentState.controller?.cancel === "function") {
|
|
632
|
+
await assessmentState.controller.cancel();
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
await stopProcess(assessmentState.child);
|
|
636
|
+
}
|
|
637
|
+
|
|
609
638
|
async function executeAssessment(assignment, assessmentState) {
|
|
610
639
|
const assessmentId = assignment.assessment_id;
|
|
611
640
|
const stage = assignment.stage;
|
|
612
641
|
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;
|
|
642
|
+
const stageStartedAt = assessmentState.startedAt;
|
|
643
|
+
let agentProcessStarted = false;
|
|
644
|
+
let agentCompleted = false;
|
|
619
645
|
let statusTicks = 0;
|
|
620
646
|
let progress = 5;
|
|
621
647
|
let outputBuffer = "";
|
|
@@ -623,31 +649,28 @@ async function executeAssessment(assignment, assessmentState) {
|
|
|
623
649
|
let outputTimer;
|
|
624
650
|
let accepted = false;
|
|
625
651
|
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 } = {}) => {
|
|
652
|
+
let inactivityFailure = null;
|
|
653
|
+
let lastReportedMilestone = null;
|
|
654
|
+
const creditedMilestones = new Set();
|
|
655
|
+
const isActiveAssessment = () =>
|
|
656
|
+
activeAssessments.get(assessmentState.key) === assessmentState;
|
|
657
|
+
const sendAssessmentMessage = (payload) => {
|
|
658
|
+
if (socket.readyState !== WebSocket.OPEN) return false;
|
|
659
|
+
try {
|
|
660
|
+
socket.send(JSON.stringify(payload));
|
|
661
|
+
return true;
|
|
662
|
+
} catch (error) {
|
|
663
|
+
lastConnectionError = protocolFailureMessage(error);
|
|
664
|
+
try {
|
|
665
|
+
socket.close();
|
|
666
|
+
} catch {
|
|
667
|
+
// The reconnect timer below owns recovery.
|
|
668
|
+
}
|
|
669
|
+
scheduleReconnect();
|
|
670
|
+
return false;
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
const reportProgress = (message, { milestone = true } = {}) => {
|
|
651
674
|
if (!isActiveAssessment()) return;
|
|
652
675
|
if (milestone && lastReportedMilestone === message) return;
|
|
653
676
|
if (milestone) {
|
|
@@ -656,38 +679,11 @@ async function executeAssessment(assignment, assessmentState) {
|
|
|
656
679
|
creditedMilestones.add(message);
|
|
657
680
|
progress = Math.min(90, progress + 10);
|
|
658
681
|
}
|
|
659
|
-
lastAgentStatus = message;
|
|
660
682
|
console.log(`[assessment:${stage}] ${message}.`);
|
|
661
683
|
}
|
|
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
|
-
}
|
|
684
|
+
const boundedMessage = String(message).slice(0, 500);
|
|
685
|
+
assessmentState.progressPercent = progress;
|
|
686
|
+
assessmentState.lastMessage = boundedMessage;
|
|
691
687
|
};
|
|
692
688
|
const flushOutput = () => {
|
|
693
689
|
if (outputTimer) clearTimeout(outputTimer);
|
|
@@ -700,14 +696,14 @@ async function executeAssessment(assignment, assessmentState) {
|
|
|
700
696
|
!isActiveAssessment()
|
|
701
697
|
) {
|
|
702
698
|
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
|
-
});
|
|
699
|
+
}
|
|
700
|
+
for (let offset = 0; offset < output.length; offset += 50_000) {
|
|
701
|
+
sendAssessmentMessage({
|
|
702
|
+
type: "workspace.assessment.output",
|
|
703
|
+
assessment_id: assessmentId,
|
|
704
|
+
stage,
|
|
705
|
+
delta: output.slice(offset, offset + 50_000),
|
|
706
|
+
});
|
|
711
707
|
}
|
|
712
708
|
};
|
|
713
709
|
const reportOutput = (delta) => {
|
|
@@ -719,59 +715,68 @@ async function executeAssessment(assignment, assessmentState) {
|
|
|
719
715
|
if (outputBuffer.length >= 50_000) {
|
|
720
716
|
flushOutput();
|
|
721
717
|
} else if (!outputTimer) {
|
|
722
|
-
outputTimer = setTimeout(
|
|
718
|
+
outputTimer = setTimeout(
|
|
719
|
+
flushOutput,
|
|
720
|
+
ASSESSMENT_OUTPUT_FLUSH_INTERVAL_MS,
|
|
721
|
+
);
|
|
723
722
|
}
|
|
724
723
|
};
|
|
725
724
|
reportProgress(`Starting ${stage} assessment stage`);
|
|
726
725
|
const heartbeat = setInterval(() => {
|
|
727
726
|
const now = Date.now();
|
|
728
|
-
const inactiveMs = now -
|
|
729
|
-
const nextInactivityFailure = agentProcessStarted && !agentCompleted
|
|
730
|
-
? assessmentInactivityFailure(stage, inactiveMs)
|
|
731
|
-
: null;
|
|
727
|
+
const inactiveMs = now - assessmentState.lastActivityAt;
|
|
728
|
+
const nextInactivityFailure = agentProcessStarted && !agentCompleted
|
|
729
|
+
? assessmentInactivityFailure(stage, inactiveMs)
|
|
730
|
+
: null;
|
|
732
731
|
if (!inactivityFailure && nextInactivityFailure) {
|
|
733
732
|
inactivityFailure = nextInactivityFailure;
|
|
734
733
|
console.error(`[assessment:${stage}] ${inactivityFailure}`);
|
|
735
|
-
void
|
|
734
|
+
void cancelAssessment(assessmentState);
|
|
736
735
|
return;
|
|
737
736
|
}
|
|
738
737
|
if (inactivityFailure) return;
|
|
739
|
-
reportProgress("Assessment in progress", { milestone: false });
|
|
740
738
|
statusTicks += 1;
|
|
741
739
|
if (statusTicks % 2 === 0) {
|
|
742
740
|
console.log(
|
|
743
741
|
`[assessment:${stage}] ${formatAssessmentDuration(now - stageStartedAt)} elapsed · ` +
|
|
744
|
-
`${
|
|
745
|
-
`(${
|
|
742
|
+
`${assessmentState.lastMessage} · last agent activity ${formatAssessmentDuration(inactiveMs)} ago ` +
|
|
743
|
+
`(${assessmentState.eventCount.toLocaleString()} events).`,
|
|
746
744
|
);
|
|
747
745
|
}
|
|
748
746
|
}, 15_000);
|
|
749
747
|
try {
|
|
750
|
-
if (assignment.assessment_mode === "incremental") {
|
|
751
|
-
progress = 15;
|
|
752
|
-
reportProgress("Calculating changed files and affected behavior");
|
|
748
|
+
if (assignment.assessment_mode === "incremental") {
|
|
749
|
+
progress = 15;
|
|
750
|
+
reportProgress("Calculating changed files and affected behavior");
|
|
753
751
|
}
|
|
754
|
-
let result = await executeWorkspaceAssessment(assignment, config, {
|
|
752
|
+
let result = await executeWorkspaceAssessment(assignment, config, {
|
|
753
|
+
onController: (controller) => {
|
|
754
|
+
if (isActiveAssessment()) assessmentState.controller = controller;
|
|
755
|
+
},
|
|
755
756
|
onProcess: (child) => {
|
|
756
757
|
if (isActiveAssessment()) {
|
|
757
758
|
assessmentState.child = child;
|
|
758
759
|
agentProcessStarted = true;
|
|
759
|
-
|
|
760
|
+
assessmentState.phase = "running";
|
|
761
|
+
assessmentState.lastActivityAt = Date.now();
|
|
760
762
|
reportProgress("Connected agent process started");
|
|
761
763
|
}
|
|
762
764
|
},
|
|
763
765
|
onEvent: (event) => {
|
|
764
|
-
|
|
765
|
-
|
|
766
|
+
assessmentState.lastActivityAt = Date.now();
|
|
767
|
+
assessmentState.eventCount += 1;
|
|
766
768
|
reportOutput(assessmentStreamDelta(event));
|
|
767
769
|
const message = assessmentProgressMessage(event);
|
|
768
|
-
if (message) reportProgress(message);
|
|
769
|
-
},
|
|
770
|
-
});
|
|
771
|
-
agentCompleted = true;
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
770
|
+
if (message) reportProgress(message);
|
|
771
|
+
},
|
|
772
|
+
});
|
|
773
|
+
agentCompleted = true;
|
|
774
|
+
assessmentState.phase = "delivering";
|
|
775
|
+
assessmentState.progressPercent = 95;
|
|
776
|
+
assessmentState.lastMessage = "Delivering completed stage result";
|
|
777
|
+
flushOutput();
|
|
778
|
+
const submitResult = (payload) =>
|
|
779
|
+
fetch(
|
|
775
780
|
assessmentResultUrl(
|
|
776
781
|
config.server_url,
|
|
777
782
|
config.connector_id,
|
|
@@ -779,38 +784,42 @@ async function executeAssessment(assignment, assessmentState) {
|
|
|
779
784
|
),
|
|
780
785
|
{
|
|
781
786
|
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
|
-
|
|
787
|
+
headers: {
|
|
788
|
+
"Content-Type": "application/json",
|
|
789
|
+
Authorization: `Bearer ${config.token}`,
|
|
790
|
+
},
|
|
791
|
+
body: JSON.stringify(payload),
|
|
792
|
+
signal: AbortSignal.timeout(ASSESSMENT_RESULT_TIMEOUT_MS),
|
|
793
|
+
},
|
|
794
|
+
);
|
|
795
|
+
const deliverResult = (payload) =>
|
|
796
|
+
submitAssessmentResultWithRetry(payload, submitResult, {
|
|
797
|
+
isActive: () => isActiveAssessment() && !stopped,
|
|
798
|
+
onRetry: (error, delayMs) => {
|
|
799
|
+
reportProgress("Completed result is waiting for EngineerOS", {
|
|
800
|
+
milestone: false,
|
|
801
|
+
});
|
|
802
|
+
console.warn(
|
|
803
|
+
`[assessment:${stage}] Completed result delivery failed: ${protocolFailureMessage(error)} ` +
|
|
804
|
+
`Retrying in ${Math.round(delayMs / 1_000)}s without rerunning the agent.`,
|
|
805
|
+
);
|
|
806
|
+
},
|
|
807
|
+
});
|
|
808
|
+
let response = await deliverResult(result);
|
|
809
|
+
if (response.status === 422 && result.correctAfterRejection) {
|
|
810
|
+
const rejection = await describeRejectedResponse(
|
|
811
|
+
response,
|
|
812
|
+
"the assessment",
|
|
813
|
+
);
|
|
814
|
+
agentCompleted = false;
|
|
815
|
+
assessmentState.phase = "correcting";
|
|
816
|
+
assessmentState.lastMessage = "Correcting a rejected stage result";
|
|
817
|
+
result = await result.correctAfterRejection(rejection);
|
|
818
|
+
agentCompleted = true;
|
|
819
|
+
assessmentState.phase = "delivering";
|
|
820
|
+
assessmentState.lastMessage = "Delivering corrected stage result";
|
|
821
|
+
flushOutput();
|
|
822
|
+
response = await deliverResult(result);
|
|
814
823
|
}
|
|
815
824
|
if (!response.ok) {
|
|
816
825
|
throw new Error(
|
|
@@ -822,14 +831,14 @@ async function executeAssessment(assignment, assessmentState) {
|
|
|
822
831
|
} catch (error) {
|
|
823
832
|
flushOutput();
|
|
824
833
|
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
|
-
}
|
|
834
|
+
if (isActiveAssessment()) {
|
|
835
|
+
failureReported = sendAssessmentMessage({
|
|
836
|
+
type: "workspace.assessment.failed",
|
|
837
|
+
assessment_id: assessmentId,
|
|
838
|
+
stage,
|
|
839
|
+
message,
|
|
840
|
+
});
|
|
841
|
+
}
|
|
833
842
|
console.error(message);
|
|
834
843
|
} finally {
|
|
835
844
|
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",
|
|
@@ -354,6 +357,28 @@ export async function executeWorkspaceAssessment(
|
|
|
354
357
|
callbacks,
|
|
355
358
|
) {
|
|
356
359
|
const execution = workspaceAssessmentExecution(assignment);
|
|
360
|
+
const assessmentAcpContext =
|
|
361
|
+
config.agent_protocol === "acp"
|
|
362
|
+
? {
|
|
363
|
+
persistentAcp: true,
|
|
364
|
+
sessionKey: `assessment:${assignment.assessment_id}:${assignment.stage}`,
|
|
365
|
+
}
|
|
366
|
+
: undefined;
|
|
367
|
+
const launchAssessmentAgent = (turnPrompt, previousSessionId) => {
|
|
368
|
+
const controller = launchAgentProcess(
|
|
369
|
+
config.workspace,
|
|
370
|
+
turnPrompt,
|
|
371
|
+
execution.sandboxMode,
|
|
372
|
+
config,
|
|
373
|
+
callbacks,
|
|
374
|
+
execution.profile,
|
|
375
|
+
previousSessionId,
|
|
376
|
+
assessmentAcpContext,
|
|
377
|
+
);
|
|
378
|
+
callbacks.onController?.(controller);
|
|
379
|
+
callbacks.onProcess?.(controller.child);
|
|
380
|
+
return controller;
|
|
381
|
+
};
|
|
357
382
|
const startingRevision = await run(
|
|
358
383
|
"git",
|
|
359
384
|
["rev-parse", "HEAD"],
|
|
@@ -386,15 +411,7 @@ export async function executeWorkspaceAssessment(
|
|
|
386
411
|
changeImpact.markdown,
|
|
387
412
|
)
|
|
388
413
|
: execution.prompt;
|
|
389
|
-
const controller =
|
|
390
|
-
config.workspace,
|
|
391
|
-
prompt,
|
|
392
|
-
execution.sandboxMode,
|
|
393
|
-
config,
|
|
394
|
-
callbacks,
|
|
395
|
-
execution.profile,
|
|
396
|
-
);
|
|
397
|
-
callbacks.onProcess?.(controller.child);
|
|
414
|
+
const controller = launchAssessmentAgent(prompt);
|
|
398
415
|
let completed = await controller.completed;
|
|
399
416
|
const recovered = await recoverAssessmentStageOutput({
|
|
400
417
|
completed,
|
|
@@ -405,16 +422,10 @@ export async function executeWorkspaceAssessment(
|
|
|
405
422
|
type: "assessment.output_correction",
|
|
406
423
|
message: "The Agent is completing the required stage report structure",
|
|
407
424
|
});
|
|
408
|
-
const correction =
|
|
409
|
-
config.workspace,
|
|
425
|
+
const correction = launchAssessmentAgent(
|
|
410
426
|
correctionPrompt,
|
|
411
|
-
execution.sandboxMode,
|
|
412
|
-
config,
|
|
413
|
-
callbacks,
|
|
414
|
-
execution.profile,
|
|
415
427
|
previousSessionId,
|
|
416
428
|
);
|
|
417
|
-
callbacks.onProcess?.(correction.child);
|
|
418
429
|
return correction.completed;
|
|
419
430
|
},
|
|
420
431
|
});
|
|
@@ -454,19 +465,13 @@ export async function executeWorkspaceAssessment(
|
|
|
454
465
|
type: "assessment.output_correction",
|
|
455
466
|
message: "The Agent is correcting the rejected stage report",
|
|
456
467
|
});
|
|
457
|
-
const correction =
|
|
458
|
-
config.workspace,
|
|
468
|
+
const correction = launchAssessmentAgent(
|
|
459
469
|
assessmentRejectionCorrectionPrompt(
|
|
460
470
|
validationMessage,
|
|
461
471
|
execution.requiredOutputHeading,
|
|
462
472
|
),
|
|
463
|
-
execution.sandboxMode,
|
|
464
|
-
config,
|
|
465
|
-
callbacks,
|
|
466
|
-
execution.profile,
|
|
467
473
|
completed.sessionId,
|
|
468
474
|
);
|
|
469
|
-
callbacks.onProcess?.(correction.child);
|
|
470
475
|
const corrected = await correction.completed;
|
|
471
476
|
const correctedReport = normalizeAgentStructuredOutput(
|
|
472
477
|
corrected.finalMessage,
|
|
@@ -697,16 +702,16 @@ export function codexFailureMessage(output, code) {
|
|
|
697
702
|
|
|
698
703
|
export function assessmentProgressMessage(event) {
|
|
699
704
|
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";
|
|
705
|
+
if (event.type === "assessment.output_correction")
|
|
706
|
+
return "Completing the required stage report structure";
|
|
707
|
+
if (event.type === "agent.connected")
|
|
708
|
+
return "Connected agent is ready to inspect the workspace";
|
|
704
709
|
if (event.type === "acp.plan")
|
|
705
710
|
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";
|
|
711
|
+
if (event.type === "acp.agent_thought_chunk")
|
|
712
|
+
return "Reasoning through the current implementation";
|
|
713
|
+
if (event.type === "acp.agent_message_chunk")
|
|
714
|
+
return "Drafting the stage report";
|
|
710
715
|
if (event.type === "acp.tool_call" || event.type === "acp.tool_call_update") {
|
|
711
716
|
return assessmentCommandMilestone(event.update?.title);
|
|
712
717
|
}
|
|
@@ -824,7 +829,7 @@ function assessmentCommandMilestone(command) {
|
|
|
824
829
|
? command.join(" ")
|
|
825
830
|
: String(command || "");
|
|
826
831
|
const normalized = value.replace(/\s+/g, " ").trim().toLowerCase();
|
|
827
|
-
if (!normalized) return "Inspecting workspace source";
|
|
832
|
+
if (!normalized) return "Inspecting workspace source";
|
|
828
833
|
if (/\bgit\s+(status|log|diff|show|rev-parse)\b/.test(normalized)) {
|
|
829
834
|
return "Comparing Git history and workspace changes";
|
|
830
835
|
}
|
|
@@ -930,9 +935,9 @@ export function assessmentStageCorrectionPrompt(
|
|
|
930
935
|
"## Incomplete Stage Output Recovery",
|
|
931
936
|
"",
|
|
932
937
|
"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.",
|
|
938
|
+
"Use repository context already inspected in the previous turn when it is available; inspect only what remains necessary.",
|
|
934
939
|
`The first non-whitespace line of the final answer must be exactly: ${requiredOutputHeading}`,
|
|
935
|
-
"Follow every section, inspection, and completeness rule in the Assignment.",
|
|
940
|
+
"Follow every section, inspection, and completeness rule in the Assignment.",
|
|
936
941
|
"Do not return progress commentary, an explanation of the correction, or a code fence.",
|
|
937
942
|
].join("\n");
|
|
938
943
|
}
|
|
@@ -992,7 +997,7 @@ export function enqueueWorkspaceAssessment(
|
|
|
992
997
|
return "queued";
|
|
993
998
|
}
|
|
994
999
|
|
|
995
|
-
export function takeWorkspaceAssessmentWave(queue, activeCount, limit = 3) {
|
|
1000
|
+
export function takeWorkspaceAssessmentWave(queue, activeCount, limit = 3) {
|
|
996
1001
|
const available = Math.max(0, limit - activeCount);
|
|
997
1002
|
if (!available || !queue.length) return [];
|
|
998
1003
|
if (queue[0]?.parallelizable !== true) {
|
|
@@ -1005,36 +1010,37 @@ export function takeWorkspaceAssessmentWave(queue, activeCount, limit = 3) {
|
|
|
1005
1010
|
) {
|
|
1006
1011
|
wave.push(queue.shift());
|
|
1007
1012
|
}
|
|
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
|
-
|
|
1013
|
+
return wave;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
export function workspaceAssessmentWorkerLimit() {
|
|
1017
|
+
return MAX_ASSESSMENT_WORKERS;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
export function assessmentWorkerSnapshots(activeAssessments) {
|
|
1021
|
+
return [...activeAssessments.values()]
|
|
1022
|
+
.slice(0, MAX_ASSESSMENT_WORKERS)
|
|
1023
|
+
.map((state) => ({
|
|
1024
|
+
assessment_id: String(state.runId),
|
|
1025
|
+
stage: String(state.stage).slice(0, 96),
|
|
1026
|
+
phase: state.phase,
|
|
1027
|
+
progress_percent: Math.max(
|
|
1028
|
+
0,
|
|
1029
|
+
Math.min(95, Number(state.progressPercent) || 0),
|
|
1030
|
+
),
|
|
1031
|
+
message: String(state.lastMessage || "Assessment worker is starting").slice(
|
|
1032
|
+
0,
|
|
1033
|
+
500,
|
|
1034
|
+
),
|
|
1035
|
+
started_at: new Date(state.startedAt).toISOString(),
|
|
1036
|
+
last_activity_at: new Date(state.lastActivityAt).toISOString(),
|
|
1037
|
+
event_count: Math.max(0, Number(state.eventCount) || 0),
|
|
1038
|
+
}));
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
export function requeueInterruptedAssessment(
|
|
1042
|
+
queue,
|
|
1043
|
+
assessmentState,
|
|
1038
1044
|
{ accepted, failureReported },
|
|
1039
1045
|
) {
|
|
1040
1046
|
if (accepted || failureReported || !assessmentState?.resumeAssignment) {
|
|
@@ -1047,52 +1053,52 @@ export function requeueInterruptedAssessment(
|
|
|
1047
1053
|
assessmentState.resumeAssignment,
|
|
1048
1054
|
{ front: true },
|
|
1049
1055
|
) === "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 = {}) {
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
export async function submitAssessmentResultWithRetry(
|
|
1060
|
+
payload,
|
|
1061
|
+
submit,
|
|
1062
|
+
{
|
|
1063
|
+
isActive = () => true,
|
|
1064
|
+
onRetry = () => {},
|
|
1065
|
+
wait = (milliseconds) =>
|
|
1066
|
+
new Promise((resolve) => setTimeout(resolve, milliseconds)),
|
|
1067
|
+
initialDelayMs = 1_000,
|
|
1068
|
+
maxDelayMs = 30_000,
|
|
1069
|
+
} = {},
|
|
1070
|
+
) {
|
|
1071
|
+
let retryDelayMs = initialDelayMs;
|
|
1072
|
+
while (isActive()) {
|
|
1073
|
+
let retryFailure;
|
|
1074
|
+
try {
|
|
1075
|
+
const response = await submit(payload);
|
|
1076
|
+
if (!isTransientAssessmentResultResponse(response)) return response;
|
|
1077
|
+
await response.body?.cancel?.();
|
|
1078
|
+
retryFailure = new Error(
|
|
1079
|
+
`EngineerOS temporarily rejected the completed assessment result (${response.status}).`,
|
|
1080
|
+
);
|
|
1081
|
+
} catch (error) {
|
|
1082
|
+
retryFailure = error;
|
|
1083
|
+
}
|
|
1084
|
+
if (!isActive()) break;
|
|
1085
|
+
onRetry(retryFailure, retryDelayMs);
|
|
1086
|
+
await wait(retryDelayMs);
|
|
1087
|
+
retryDelayMs = Math.min(maxDelayMs, retryDelayMs * 2);
|
|
1088
|
+
}
|
|
1089
|
+
throw new Error("Assessment result delivery stopped before EngineerOS accepted it.");
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function isTransientAssessmentResultResponse(response) {
|
|
1093
|
+
return (
|
|
1094
|
+
response.status === 408 ||
|
|
1095
|
+
response.status === 425 ||
|
|
1096
|
+
response.status === 429 ||
|
|
1097
|
+
response.status >= 500
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
export function connectorExecution(assignment, options = {}) {
|
|
1096
1102
|
const rawPrompt = assignment?.prompt_markdown;
|
|
1097
1103
|
if (typeof rawPrompt !== "string" || !rawPrompt.trim()) {
|
|
1098
1104
|
throw new Error("EngineerOS assignment is missing prompt_markdown.");
|