@lasso-ai/cli 2.1.15 → 2.1.16
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/dist/cli/bridge.d.ts +11 -0
- package/dist/cli/bridge.js +40 -0
- package/dist/cli/runtime-client.d.ts +8 -0
- package/dist/cli/runtime-client.js +34 -1
- package/dist/overlay.js +135 -10
- package/dist/runtime/agent-runtime.d.ts +3 -1
- package/dist/runtime/verification-engine.d.ts +10 -0
- package/dist/runtime/verification-engine.js +83 -35
- package/package.json +1 -1
package/dist/cli/bridge.d.ts
CHANGED
|
@@ -96,6 +96,11 @@ export type BridgeMessage = {
|
|
|
96
96
|
type: "agent_prompt_response";
|
|
97
97
|
taskId: string;
|
|
98
98
|
response: string;
|
|
99
|
+
} | {
|
|
100
|
+
type: "steer";
|
|
101
|
+
taskId: string;
|
|
102
|
+
queueId: string;
|
|
103
|
+
prompt: string;
|
|
99
104
|
} | {
|
|
100
105
|
type: "list_page_folders";
|
|
101
106
|
path?: string;
|
|
@@ -221,6 +226,12 @@ export type ServerBridgeMessage = {
|
|
|
221
226
|
kind: "permission" | "input" | "multiselect";
|
|
222
227
|
options?: string[];
|
|
223
228
|
};
|
|
229
|
+
} | {
|
|
230
|
+
type: "steer_result";
|
|
231
|
+
taskId: string;
|
|
232
|
+
queueId: string;
|
|
233
|
+
success: boolean;
|
|
234
|
+
error?: string;
|
|
224
235
|
} | {
|
|
225
236
|
type: "page_folders";
|
|
226
237
|
path: string;
|
package/dist/cli/bridge.js
CHANGED
|
@@ -900,6 +900,40 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
900
900
|
message: "Agent stopped",
|
|
901
901
|
}));
|
|
902
902
|
}
|
|
903
|
+
/** Forward an overlay answer to the runtime's pending user question. */
|
|
904
|
+
async function handleAgentPromptResponse(message, socket) {
|
|
905
|
+
const session = activeSessions.get(message.taskId);
|
|
906
|
+
const facade = taskFacades.get(message.taskId);
|
|
907
|
+
if (!session || !facade) {
|
|
908
|
+
throw new Error("No active agent session is waiting for a response.");
|
|
909
|
+
}
|
|
910
|
+
if (!facade.respondToPrompt) {
|
|
911
|
+
throw new Error("This agent backend does not support interactive prompts.");
|
|
912
|
+
}
|
|
913
|
+
await facade.respondToPrompt(session.id, message.response);
|
|
914
|
+
socket.send(JSON.stringify({
|
|
915
|
+
type: "agent_status",
|
|
916
|
+
taskId: message.taskId,
|
|
917
|
+
status: "working",
|
|
918
|
+
message: "Continuing with your response…",
|
|
919
|
+
}));
|
|
920
|
+
}
|
|
921
|
+
/** Queue a prompt for the active runtime's next model turn. */
|
|
922
|
+
async function handleSteer(message, socket) {
|
|
923
|
+
const session = activeSessions.get(message.taskId);
|
|
924
|
+
const facade = taskFacades.get(message.taskId);
|
|
925
|
+
if (!session || !facade?.steerSession) {
|
|
926
|
+
socket.send(JSON.stringify({ type: "steer_result", taskId: message.taskId, queueId: message.queueId, success: false, error: "This agent backend does not support live steering." }));
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
try {
|
|
930
|
+
await facade.steerSession(session.id, message.prompt);
|
|
931
|
+
socket.send(JSON.stringify({ type: "steer_result", taskId: message.taskId, queueId: message.queueId, success: true }));
|
|
932
|
+
}
|
|
933
|
+
catch (error) {
|
|
934
|
+
socket.send(JSON.stringify({ type: "steer_result", taskId: message.taskId, queueId: message.queueId, success: false, error: error instanceof Error ? error.message : "Failed to steer the agent." }));
|
|
935
|
+
}
|
|
936
|
+
}
|
|
903
937
|
// Handler for switch_model
|
|
904
938
|
async function handleSwitchModel(message, socket) {
|
|
905
939
|
const { taskId, provider, modelId } = message;
|
|
@@ -1138,6 +1172,12 @@ async function startBridge(cwd = process.cwd(), collabConfig = null, bridgePort
|
|
|
1138
1172
|
case "stop":
|
|
1139
1173
|
await handleStop(msg, socket);
|
|
1140
1174
|
break;
|
|
1175
|
+
case "agent_prompt_response":
|
|
1176
|
+
await handleAgentPromptResponse(msg, socket);
|
|
1177
|
+
break;
|
|
1178
|
+
case "steer":
|
|
1179
|
+
await handleSteer(msg, socket);
|
|
1180
|
+
break;
|
|
1141
1181
|
case "switch_model":
|
|
1142
1182
|
await handleSwitchModel(msg, socket);
|
|
1143
1183
|
break;
|
|
@@ -157,6 +157,7 @@ export interface RuntimeFacade {
|
|
|
157
157
|
runSession(sessionId: string, projectPath: string, onEvent: RuntimeEventHandler, signal?: AbortSignal, opts?: {
|
|
158
158
|
providerApiKey?: string;
|
|
159
159
|
browserContextProvider?: BrowserContextProvider;
|
|
160
|
+
devServerUrl?: string;
|
|
160
161
|
}): Promise<void>;
|
|
161
162
|
getSession(sessionId: string): Promise<RuntimeSession>;
|
|
162
163
|
verifySession(sessionId: string, projectPath: string): Promise<SessionVerificationResult>;
|
|
@@ -166,6 +167,8 @@ export interface RuntimeFacade {
|
|
|
166
167
|
name?: string;
|
|
167
168
|
}, apiKey?: string, backendConfig?: RuntimeSessionInput["backendConfig"]): Promise<void>;
|
|
168
169
|
cancelSession(sessionId: string): Promise<void>;
|
|
170
|
+
respondToPrompt?(sessionId: string, response: string): Promise<void>;
|
|
171
|
+
steerSession?(sessionId: string, prompt: string): Promise<void>;
|
|
169
172
|
}
|
|
170
173
|
export type RuntimeEvent = {
|
|
171
174
|
type: "agent_started";
|
|
@@ -315,6 +318,7 @@ export declare class RuntimeClient {
|
|
|
315
318
|
runSession(sessionId: string, projectPath: string, onEvent: RuntimeEventHandler, signal?: AbortSignal, opts?: {
|
|
316
319
|
providerApiKey?: string;
|
|
317
320
|
browserContextProvider?: BrowserContextProvider;
|
|
321
|
+
devServerUrl?: string;
|
|
318
322
|
}): Promise<void>;
|
|
319
323
|
/** Parse and dispatch one complete SSE frame. */
|
|
320
324
|
private processSseFrame;
|
|
@@ -326,6 +330,10 @@ export declare class RuntimeClient {
|
|
|
326
330
|
* Cancel a running session
|
|
327
331
|
*/
|
|
328
332
|
cancelSession(sessionId: string): Promise<void>;
|
|
333
|
+
/** Resume a hosted runtime that is waiting for an answer from the user. */
|
|
334
|
+
respondToPrompt(sessionId: string, response: string): Promise<void>;
|
|
335
|
+
/** Queue a prompt for the next turn of an active hosted runtime. */
|
|
336
|
+
steerSession(sessionId: string, prompt: string): Promise<void>;
|
|
329
337
|
/**
|
|
330
338
|
* Switch model/backend for a session
|
|
331
339
|
*/
|
|
@@ -83,7 +83,10 @@ class RuntimeClient {
|
|
|
83
83
|
// pre-run workspace capture could not be established.
|
|
84
84
|
const guard = await index_js_1.WorkspaceGuard.capture(projectPath);
|
|
85
85
|
const executor = new index_js_1.ActionExecutor(projectPath, opts?.browserContextProvider);
|
|
86
|
-
const verifier = new index_js_1.VerificationEngine(projectPath
|
|
86
|
+
const verifier = new index_js_1.VerificationEngine(projectPath, {
|
|
87
|
+
devServerUrl: opts?.devServerUrl || process.env.LASSO_DEV_SERVER_URL,
|
|
88
|
+
browserContextProvider: opts?.browserContextProvider,
|
|
89
|
+
});
|
|
87
90
|
// Idempotent: called on agent_completed AND on stream failure, so the
|
|
88
91
|
// diff is always cached before the safety-net restore wipes the edits.
|
|
89
92
|
let finalized = false;
|
|
@@ -268,6 +271,36 @@ class RuntimeClient {
|
|
|
268
271
|
throw new Error(`Failed to cancel session: ${response.statusText}`);
|
|
269
272
|
}
|
|
270
273
|
}
|
|
274
|
+
/** Resume a hosted runtime that is waiting for an answer from the user. */
|
|
275
|
+
async respondToPrompt(sessionId, response) {
|
|
276
|
+
const result = await fetch(`${this.serverUrl}/api/v1/agent-runtime/sessions/${sessionId}/user-response`, {
|
|
277
|
+
method: "POST",
|
|
278
|
+
headers: {
|
|
279
|
+
"Content-Type": "application/json",
|
|
280
|
+
...(this.apiKey && { Authorization: `Bearer ${this.apiKey}` }),
|
|
281
|
+
},
|
|
282
|
+
body: JSON.stringify({ response }),
|
|
283
|
+
});
|
|
284
|
+
if (!result.ok) {
|
|
285
|
+
const body = (await result.json().catch(() => ({})));
|
|
286
|
+
throw new Error(body.error || `Failed to submit agent response: ${result.statusText}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/** Queue a prompt for the next turn of an active hosted runtime. */
|
|
290
|
+
async steerSession(sessionId, prompt) {
|
|
291
|
+
const result = await fetch(`${this.serverUrl}/api/v1/agent-runtime/sessions/${sessionId}/steer`, {
|
|
292
|
+
method: "POST",
|
|
293
|
+
headers: {
|
|
294
|
+
"Content-Type": "application/json",
|
|
295
|
+
...(this.apiKey && { Authorization: `Bearer ${this.apiKey}` }),
|
|
296
|
+
},
|
|
297
|
+
body: JSON.stringify({ prompt }),
|
|
298
|
+
});
|
|
299
|
+
if (!result.ok) {
|
|
300
|
+
const body = (await result.json().catch(() => ({})));
|
|
301
|
+
throw new Error(body.error || `Failed to queue steering prompt: ${result.statusText}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
271
304
|
/**
|
|
272
305
|
* Switch model/backend for a session
|
|
273
306
|
*/
|
package/dist/overlay.js
CHANGED
|
@@ -11522,7 +11522,6 @@
|
|
|
11522
11522
|
align-items: center;
|
|
11523
11523
|
justify-content: space-between;
|
|
11524
11524
|
padding: 6px 8px 8px;
|
|
11525
|
-
border-bottom: 1px solid var(--lo-border-subtle);
|
|
11526
11525
|
font-size: 11px;
|
|
11527
11526
|
font-weight: 600;
|
|
11528
11527
|
color: var(--lo-text-3);
|
|
@@ -11553,6 +11552,48 @@
|
|
|
11553
11552
|
width: 100%;
|
|
11554
11553
|
}
|
|
11555
11554
|
|
|
11555
|
+
.lasso-command-history-load {
|
|
11556
|
+
display: flex;
|
|
11557
|
+
align-items: center;
|
|
11558
|
+
justify-content: space-between;
|
|
11559
|
+
gap: 8px;
|
|
11560
|
+
min-width: 0;
|
|
11561
|
+
flex: 1;
|
|
11562
|
+
padding: 0;
|
|
11563
|
+
border: 0;
|
|
11564
|
+
background: transparent;
|
|
11565
|
+
color: inherit;
|
|
11566
|
+
cursor: pointer;
|
|
11567
|
+
text-align: left;
|
|
11568
|
+
}
|
|
11569
|
+
|
|
11570
|
+
.lasso-command-history-delete {
|
|
11571
|
+
display: inline-flex;
|
|
11572
|
+
align-items: center;
|
|
11573
|
+
justify-content: center;
|
|
11574
|
+
flex: 0 0 auto;
|
|
11575
|
+
width: 24px;
|
|
11576
|
+
height: 24px;
|
|
11577
|
+
padding: 0;
|
|
11578
|
+
border: 0;
|
|
11579
|
+
border-radius: 5px;
|
|
11580
|
+
background: transparent;
|
|
11581
|
+
color: var(--lo-text-3);
|
|
11582
|
+
cursor: pointer;
|
|
11583
|
+
opacity: 0;
|
|
11584
|
+
transition: opacity 120ms ease, color 120ms ease, background 120ms ease;
|
|
11585
|
+
}
|
|
11586
|
+
|
|
11587
|
+
.lasso-command-history-item:hover .lasso-command-history-delete,
|
|
11588
|
+
.lasso-command-history-delete:focus-visible {
|
|
11589
|
+
opacity: 1;
|
|
11590
|
+
}
|
|
11591
|
+
|
|
11592
|
+
.lasso-command-history-delete:hover {
|
|
11593
|
+
color: #fca5a5;
|
|
11594
|
+
background: rgba(239, 68, 68, 0.14);
|
|
11595
|
+
}
|
|
11596
|
+
|
|
11556
11597
|
.lasso-command-history-item:hover {
|
|
11557
11598
|
background: var(--lo-surface-hover);
|
|
11558
11599
|
}
|
|
@@ -19700,17 +19741,44 @@
|
|
|
19700
19741
|
</div>
|
|
19701
19742
|
${conversationList.map(
|
|
19702
19743
|
(conv) => `
|
|
19703
|
-
<
|
|
19704
|
-
<
|
|
19705
|
-
|
|
19706
|
-
<
|
|
19707
|
-
|
|
19708
|
-
|
|
19709
|
-
|
|
19744
|
+
<div class="lasso-command-history-item ${conv.id === activeConversationId ? "active" : ""}" data-conv-id="${escapeHtml4(conv.id)}">
|
|
19745
|
+
<button class="lasso-command-history-load" type="button" title="Open session">
|
|
19746
|
+
<span class="lasso-command-history-title" title="${escapeHtml4(conv.title || "Untitled Session")}">${escapeHtml4(conv.title || "Untitled Session")}</span>
|
|
19747
|
+
<div class="lasso-command-history-meta">
|
|
19748
|
+
<span class="lasso-command-history-scope ${conv.scope || "private"}">${conv.scope || "private"}</span>
|
|
19749
|
+
<span>${formatDateAgo(conv.updatedAt)}</span>
|
|
19750
|
+
</div>
|
|
19751
|
+
</button>
|
|
19752
|
+
<button class="lasso-command-history-delete" type="button" title="Delete session" aria-label="Delete session">
|
|
19753
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v5M14 11v5"/></svg>
|
|
19754
|
+
</button>
|
|
19755
|
+
</div>
|
|
19710
19756
|
`
|
|
19711
19757
|
).join("")}
|
|
19712
19758
|
`;
|
|
19713
19759
|
}
|
|
19760
|
+
async function deleteConversation(convId) {
|
|
19761
|
+
const conversation = conversationList.find((item) => item.id === convId);
|
|
19762
|
+
if (!conversation) return;
|
|
19763
|
+
if (!convId.startsWith("local-session-")) {
|
|
19764
|
+
const deleted = await apiFetch(`/agent/conversations/${encodeURIComponent(convId)}`, { method: "DELETE" });
|
|
19765
|
+
if (!deleted?.success) return;
|
|
19766
|
+
}
|
|
19767
|
+
try {
|
|
19768
|
+
localStorage.removeItem(`lasso:session:${convId}`);
|
|
19769
|
+
const key = getStorageProjectKey();
|
|
19770
|
+
const list2 = JSON.parse(localStorage.getItem(key) || "[]");
|
|
19771
|
+
localStorage.setItem(key, JSON.stringify(list2.filter((item) => item.id !== convId)));
|
|
19772
|
+
} catch {
|
|
19773
|
+
}
|
|
19774
|
+
conversationList = conversationList.filter((item) => item.id !== convId);
|
|
19775
|
+
if (activeConversationId === convId) {
|
|
19776
|
+
activeConversationId = null;
|
|
19777
|
+
commandMessages = [];
|
|
19778
|
+
renderMessages();
|
|
19779
|
+
}
|
|
19780
|
+
renderHistoryPopover();
|
|
19781
|
+
}
|
|
19714
19782
|
async function switchConversation(convId) {
|
|
19715
19783
|
if (activeThinkingInterval) {
|
|
19716
19784
|
clearInterval(activeThinkingInterval);
|
|
@@ -20038,9 +20106,16 @@
|
|
|
20038
20106
|
});
|
|
20039
20107
|
commandHistoryPopover.addEventListener("click", async (event) => {
|
|
20040
20108
|
event.stopPropagation();
|
|
20041
|
-
const
|
|
20109
|
+
const target = event.target;
|
|
20110
|
+
const item = target.closest(".lasso-command-history-item");
|
|
20042
20111
|
if (!item?.dataset.convId) return;
|
|
20043
|
-
|
|
20112
|
+
if (target.closest(".lasso-command-history-delete")) {
|
|
20113
|
+
await deleteConversation(item.dataset.convId);
|
|
20114
|
+
return;
|
|
20115
|
+
}
|
|
20116
|
+
if (target.closest(".lasso-command-history-load")) {
|
|
20117
|
+
await switchConversation(item.dataset.convId);
|
|
20118
|
+
}
|
|
20044
20119
|
});
|
|
20045
20120
|
const setCommandModelMenuOpen = (open) => {
|
|
20046
20121
|
if (!commandModelMenu || !commandModelBtn) return;
|
|
@@ -20194,6 +20269,19 @@
|
|
|
20194
20269
|
}
|
|
20195
20270
|
return;
|
|
20196
20271
|
}
|
|
20272
|
+
const steerBtn = target.closest(".lasso-command-steer-btn");
|
|
20273
|
+
if (steerBtn) {
|
|
20274
|
+
const msgId = steerBtn.closest(".lasso-command-msg")?.dataset.msgId;
|
|
20275
|
+
const msg = commandMessages.find((m) => m.id === msgId);
|
|
20276
|
+
if (msg?.queued && !msg.steering && activeTaskId && state.bridgeSocket?.readyState === WebSocket.OPEN) {
|
|
20277
|
+
steerBtn.disabled = true;
|
|
20278
|
+
steerBtn.textContent = "Steering\u2026";
|
|
20279
|
+
msg.steering = true;
|
|
20280
|
+
state.bridgeSocket.send(JSON.stringify({ type: "steer", taskId: activeTaskId, queueId: msg.id, prompt: msg.content }));
|
|
20281
|
+
renderMessages();
|
|
20282
|
+
}
|
|
20283
|
+
return;
|
|
20284
|
+
}
|
|
20197
20285
|
const fileCheck = target.closest(".lasso-command-file-check");
|
|
20198
20286
|
if (fileCheck) {
|
|
20199
20287
|
const msgId = fileCheck.closest(".lasso-command-msg")?.dataset.msgId;
|
|
@@ -20405,6 +20493,21 @@ Please proceed directly with the implementation and code changes from this point
|
|
|
20405
20493
|
async function handleSubmit() {
|
|
20406
20494
|
if (!commandInput) return;
|
|
20407
20495
|
if (activeTaskId) {
|
|
20496
|
+
const queuedPrompt = commandInput.value.trim();
|
|
20497
|
+
if (queuedPrompt) {
|
|
20498
|
+
commandMessages.push({
|
|
20499
|
+
id: `queued-${Date.now()}`,
|
|
20500
|
+
role: "user",
|
|
20501
|
+
content: queuedPrompt,
|
|
20502
|
+
timestamp: Date.now(),
|
|
20503
|
+
queued: true
|
|
20504
|
+
});
|
|
20505
|
+
commandInput.value = "";
|
|
20506
|
+
autoResizeTextarea(commandInput);
|
|
20507
|
+
renderMessages();
|
|
20508
|
+
scrollToBottom();
|
|
20509
|
+
return;
|
|
20510
|
+
}
|
|
20408
20511
|
if (state.bridgeSocket?.readyState === WebSocket.OPEN) {
|
|
20409
20512
|
state.bridgeSocket.send(
|
|
20410
20513
|
JSON.stringify({
|
|
@@ -20663,6 +20766,18 @@ Please proceed directly with the implementation and code changes from this point
|
|
|
20663
20766
|
renderMessages();
|
|
20664
20767
|
}
|
|
20665
20768
|
}
|
|
20769
|
+
function handleCommandBarSteerResult(message) {
|
|
20770
|
+
const queued = commandMessages.find((msg) => msg.id === message.queueId);
|
|
20771
|
+
if (!queued) return;
|
|
20772
|
+
queued.steering = false;
|
|
20773
|
+
if (message.success) {
|
|
20774
|
+
queued.queued = false;
|
|
20775
|
+
queued.steered = true;
|
|
20776
|
+
} else {
|
|
20777
|
+
queued.error = message.error || "The agent could not accept this steering prompt.";
|
|
20778
|
+
}
|
|
20779
|
+
renderMessages();
|
|
20780
|
+
}
|
|
20666
20781
|
function handleCommandBarPrompt(message) {
|
|
20667
20782
|
const taskId2 = message.taskId;
|
|
20668
20783
|
let targetMsg = commandMessages.find((m) => m.id === taskId2);
|
|
@@ -20716,6 +20831,7 @@ Please proceed directly with the implementation and code changes from this point
|
|
|
20716
20831
|
html += `
|
|
20717
20832
|
<div class="lasso-command-msg user" data-msg-id="${msg.id}">
|
|
20718
20833
|
<div class="lasso-command-msg-bubble">${escapeHtml4(msg.content)}</div>
|
|
20834
|
+
${msg.queued ? `<button class="lasso-command-steer-btn" type="button"${msg.steering ? " disabled" : ""} style="margin-top: 6px; padding: 4px 9px; border: 1px solid rgba(96, 165, 250, 0.45); border-radius: 6px; background: rgba(59, 130, 246, 0.14); color: #bfdbfe; cursor: pointer;">${msg.steering ? "Steering\u2026" : "Steer"}</button>` : msg.steered ? `<span class="lasso-command-steered-label" style="display: inline-block; margin-top: 6px; color: #93c5fd; font-size: 12px;">Steered into next turn</span>` : ""}
|
|
20719
20835
|
</div>
|
|
20720
20836
|
`;
|
|
20721
20837
|
} else {
|
|
@@ -21821,6 +21937,15 @@ Please proceed directly with the implementation and code changes from this point
|
|
|
21821
21937
|
window.dispatchEvent(new CustomEvent("lasso-page-folder-created", { detail: { path: message.path || "", error: message.error } }));
|
|
21822
21938
|
return;
|
|
21823
21939
|
}
|
|
21940
|
+
if (message.type === "steer_result" && message.taskId && message.queueId) {
|
|
21941
|
+
handleCommandBarSteerResult({
|
|
21942
|
+
taskId: message.taskId,
|
|
21943
|
+
queueId: message.queueId,
|
|
21944
|
+
success: Boolean(message.success),
|
|
21945
|
+
error: message.error
|
|
21946
|
+
});
|
|
21947
|
+
return;
|
|
21948
|
+
}
|
|
21824
21949
|
if (message.type === "agent_status" && message.status && message.message) {
|
|
21825
21950
|
const taskId2 = message.taskId;
|
|
21826
21951
|
if (isCommandBarTaskId(taskId2)) {
|
|
@@ -195,10 +195,12 @@ export interface LassoModelResponse {
|
|
|
195
195
|
raw?: string;
|
|
196
196
|
};
|
|
197
197
|
decision: {
|
|
198
|
-
type: "inspect" | "plan" | "execute" | "verify" | "recover" | "ask_user";
|
|
198
|
+
type: "respond" | "inspect" | "plan" | "execute" | "verify" | "recover" | "ask_user";
|
|
199
199
|
goal: string;
|
|
200
200
|
};
|
|
201
201
|
actions: AgentAction[];
|
|
202
|
+
/** Direct conversational answer when no project work is requested. */
|
|
203
|
+
reply?: string;
|
|
202
204
|
expectedOutcome?: string;
|
|
203
205
|
}
|
|
204
206
|
export type AgentEventType = "agent_started" | "agent_reasoning" | "agent_completed" | "agent_failed" | "agent_cancelled" | "phase_changed" | "tool_started" | "tool_completed" | "tool_call" | "observation" | "plan_created" | "file_changed" | "verification_started" | "verification_completed" | "error" | "model_changed" | "waiting_for_user";
|
|
@@ -10,6 +10,14 @@ export interface VerificationConfig {
|
|
|
10
10
|
buildTimeout: number;
|
|
11
11
|
devServerTimeout: number;
|
|
12
12
|
browserTimeout: number;
|
|
13
|
+
/** URL of the project's already-running dev server, if available. */
|
|
14
|
+
devServerUrl?: string;
|
|
15
|
+
/** Optional browser bridge used for DOM/runtime verification. */
|
|
16
|
+
browserContextProvider?: (action: {
|
|
17
|
+
id: string;
|
|
18
|
+
type: "browser_check";
|
|
19
|
+
data?: Record<string, unknown>;
|
|
20
|
+
}) => Promise<unknown>;
|
|
13
21
|
}
|
|
14
22
|
export interface VerificationResult {
|
|
15
23
|
type: "typecheck" | "build" | "dev_server" | "browser";
|
|
@@ -68,4 +76,6 @@ export declare class VerificationEngine {
|
|
|
68
76
|
private hasTypeScriptConfig;
|
|
69
77
|
/** package.json with a non-empty build script. */
|
|
70
78
|
private hasBuildScript;
|
|
79
|
+
/** Fetch a health URL with a portable timeout for supported Node versions. */
|
|
80
|
+
private fetchWithTimeout;
|
|
71
81
|
}
|
|
@@ -19,6 +19,8 @@ class VerificationEngine {
|
|
|
19
19
|
buildTimeout: config?.buildTimeout || 120000,
|
|
20
20
|
devServerTimeout: config?.devServerTimeout || 30000,
|
|
21
21
|
browserTimeout: config?.browserTimeout || 60000,
|
|
22
|
+
devServerUrl: config?.devServerUrl,
|
|
23
|
+
browserContextProvider: config?.browserContextProvider,
|
|
22
24
|
};
|
|
23
25
|
}
|
|
24
26
|
/**
|
|
@@ -43,7 +45,6 @@ class VerificationEngine {
|
|
|
43
45
|
// Dev server health
|
|
44
46
|
const devServerResult = await this.verifyDevServer(phase);
|
|
45
47
|
results.push(devServerResult);
|
|
46
|
-
// Browser check (placeholder)
|
|
47
48
|
const browserResult = await this.verifyBrowser(phase);
|
|
48
49
|
results.push(browserResult);
|
|
49
50
|
return results;
|
|
@@ -283,23 +284,48 @@ class VerificationEngine {
|
|
|
283
284
|
const startTime = Date.now();
|
|
284
285
|
const observations = [];
|
|
285
286
|
const errors = [];
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
287
|
+
const url = this.config.devServerUrl;
|
|
288
|
+
if (!url) {
|
|
289
|
+
observations.push({
|
|
290
|
+
id: this.generateId(),
|
|
291
|
+
type: "build",
|
|
292
|
+
summary: "Dev server health check skipped (no devServerUrl configured)",
|
|
293
|
+
data: { type: "dev_server", success: true, skipped: true },
|
|
294
|
+
timestamp: Date.now(),
|
|
295
|
+
phase,
|
|
296
|
+
});
|
|
297
|
+
return { type: "dev_server", success: true, skipped: true, observations, errors, duration: Date.now() - startTime };
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
const response = await this.fetchWithTimeout(url, this.config.devServerTimeout);
|
|
301
|
+
const success = response.status >= 200 && response.status < 400;
|
|
302
|
+
const summary = success ? `Dev server responded with HTTP ${response.status}` : `Dev server returned HTTP ${response.status}`;
|
|
303
|
+
observations.push({
|
|
304
|
+
id: this.generateId(),
|
|
305
|
+
type: "build",
|
|
306
|
+
summary,
|
|
307
|
+
data: { type: "dev_server", success, url, status: response.status },
|
|
308
|
+
timestamp: Date.now(),
|
|
309
|
+
phase,
|
|
310
|
+
});
|
|
311
|
+
if (!success) {
|
|
312
|
+
errors.push({
|
|
313
|
+
id: this.generateId(),
|
|
314
|
+
type: "verification",
|
|
315
|
+
message: summary,
|
|
316
|
+
phase,
|
|
317
|
+
timestamp: Date.now(),
|
|
318
|
+
recoverable: true,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
return { type: "dev_server", success, command: `GET ${url}`, error: success ? undefined : summary, observations, errors, duration: Date.now() - startTime };
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
const message = `Dev server health check failed: ${error?.message || String(error)}`;
|
|
325
|
+
observations.push({ id: this.generateId(), type: "build", summary: message, data: { type: "dev_server", success: false, url }, timestamp: Date.now(), phase });
|
|
326
|
+
errors.push({ id: this.generateId(), type: "verification", message, phase, timestamp: Date.now(), recoverable: true });
|
|
327
|
+
return { type: "dev_server", success: false, command: `GET ${url}`, error: message, observations, errors, duration: Date.now() - startTime };
|
|
328
|
+
}
|
|
303
329
|
}
|
|
304
330
|
/**
|
|
305
331
|
* Verify browser/runtime state
|
|
@@ -308,23 +334,34 @@ class VerificationEngine {
|
|
|
308
334
|
const startTime = Date.now();
|
|
309
335
|
const observations = [];
|
|
310
336
|
const errors = [];
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
337
|
+
const provider = this.config.browserContextProvider;
|
|
338
|
+
if (!provider) {
|
|
339
|
+
observations.push({
|
|
340
|
+
id: this.generateId(),
|
|
341
|
+
type: "browser",
|
|
342
|
+
summary: "Browser verification skipped (no browser bridge configured)",
|
|
343
|
+
data: { type: "browser", success: true, skipped: true },
|
|
344
|
+
timestamp: Date.now(),
|
|
345
|
+
phase,
|
|
346
|
+
});
|
|
347
|
+
return { type: "browser", success: true, skipped: true, observations, errors, duration: Date.now() - startTime };
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
const result = await Promise.race([provider({ id: this.generateId(), type: "browser_check", data: { phase } }), new Promise((_, reject) => setTimeout(() => reject(new Error("Browser verification timed out")), this.config.browserTimeout))]);
|
|
351
|
+
const details = result && typeof result === "object" ? result : {};
|
|
352
|
+
const success = details.success !== false;
|
|
353
|
+
const summary = typeof details.summary === "string" ? details.summary : success ? "Browser check passed" : "Browser check failed";
|
|
354
|
+
observations.push({ id: this.generateId(), type: "browser", summary, data: { type: "browser", success, result }, timestamp: Date.now(), phase });
|
|
355
|
+
if (!success)
|
|
356
|
+
errors.push({ id: this.generateId(), type: "verification", message: summary, phase, timestamp: Date.now(), recoverable: true });
|
|
357
|
+
return { type: "browser", success, error: success ? undefined : summary, observations, errors, duration: Date.now() - startTime };
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
const message = `Browser verification failed: ${error?.message || String(error)}`;
|
|
361
|
+
observations.push({ id: this.generateId(), type: "browser", summary: message, data: { type: "browser", success: false }, timestamp: Date.now(), phase });
|
|
362
|
+
errors.push({ id: this.generateId(), type: "verification", message, phase, timestamp: Date.now(), recoverable: true });
|
|
363
|
+
return { type: "browser", success: false, error: message, observations, errors, duration: Date.now() - startTime };
|
|
364
|
+
}
|
|
328
365
|
}
|
|
329
366
|
/**
|
|
330
367
|
* Check if verification passed overall
|
|
@@ -370,5 +407,16 @@ class VerificationEngine {
|
|
|
370
407
|
return false;
|
|
371
408
|
}
|
|
372
409
|
}
|
|
410
|
+
/** Fetch a health URL with a portable timeout for supported Node versions. */
|
|
411
|
+
async fetchWithTimeout(url, timeoutMs) {
|
|
412
|
+
const controller = new AbortController();
|
|
413
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
414
|
+
try {
|
|
415
|
+
return await fetch(url, { signal: controller.signal, redirect: "manual" });
|
|
416
|
+
}
|
|
417
|
+
finally {
|
|
418
|
+
clearTimeout(timer);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
373
421
|
}
|
|
374
422
|
exports.VerificationEngine = VerificationEngine;
|
package/package.json
CHANGED