ask-pro 0.1.58 → 0.1.63
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 +21 -0
- package/dist/bin/ask-pro-cli.js +47 -3
- package/dist/src/ask-pro/browserRunner.js +37 -3
- package/dist/src/ask-pro/questions.js +40 -0
- package/dist/src/browser/actions/assistantResponse.js +12 -3
- package/dist/src/browser/actions/promptComposer.js +8 -2
- package/dist/src/browser/actions/questions.js +195 -0
- package/dist/src/browser/actions/thinkingStatus.js +5 -3
- package/dist/src/browser/constants.js +2 -2
- package/dist/src/browser/index.js +7 -0
- package/dist/src/browser/reattach.js +24 -5
- package/package.json +1 -1
- package/skills/ask-pro/SKILL.md +10 -0
package/README.md
CHANGED
|
@@ -88,6 +88,27 @@ Use `ask-pro --help` for the full CLI. For multiline prompts, use
|
|
|
88
88
|
`--prompt-file`. Prefer `--no-temporary` when recovery matters; `--temporary`
|
|
89
89
|
requires Temporary Chat. The [skill](skills/ask-pro/SKILL.md) has agent guidance.
|
|
90
90
|
|
|
91
|
+
When Pro asks native questions, ask-pro returns `awaiting_answers`, prints each
|
|
92
|
+
question and its numbered choices, and saves `QUESTIONS.json`. The calling agent
|
|
93
|
+
can answer directly, or ask its user when a decision needs their input. Write a
|
|
94
|
+
UTF-8 JSON file using the emitted `request_id`, with one answer per question:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{ "requestId": "<request_id>", "answers": [2, 3, "A workshop on incident response"] }
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Numbers select 1-based options; strings enter free text. Then continue the same chat:
|
|
101
|
+
|
|
102
|
+
```powershell
|
|
103
|
+
ask-pro --resume <session-id> --answers answers.json
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`--answers -` accepts piped JSON. ask-pro requests a native pause; if ChatGPT expires
|
|
107
|
+
the cards anyway, it sends your answers as a reply in the same conversation.
|
|
108
|
+
Keep the session's original Temporary Chat mode when answering.
|
|
109
|
+
Further question rounds use the same
|
|
110
|
+
flow; `--status` repeats pending questions and `--harvest` waits for a final answer.
|
|
111
|
+
|
|
91
112
|
## Optional GitHub context
|
|
92
113
|
|
|
93
114
|
Use `--github` to check the GitHub connection in ask-pro's signed-in ChatGPT
|
package/dist/bin/ask-pro-cli.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import "dotenv/config";
|
|
3
3
|
import { AssistantStoppedError } from "../src/browser/errors.js";
|
|
4
|
+
import { AssistantQuestionsError } from "../src/browser/actions/questions.js";
|
|
5
|
+
import { readAskProQuestions } from "../src/ask-pro/questions.js";
|
|
4
6
|
import { GITHUB_SETUP_URL, GitHubConnectionRequiredError } from "../src/browser/actions/github.js";
|
|
5
7
|
import fs from "node:fs/promises";
|
|
6
8
|
import path from "node:path";
|
|
@@ -28,6 +30,9 @@ program
|
|
|
28
30
|
"copy",
|
|
29
31
|
]))
|
|
30
32
|
.option("--resume [session-id]", "resume a prepared or waiting ask-pro session")
|
|
33
|
+
.addOption(new Option("--answers <path>", "answer native questions from JSON; use - for stdin")
|
|
34
|
+
.implies({ resume: true })
|
|
35
|
+
.conflicts(["status", "harvest", "copy", "dryRun", "promptFile"]))
|
|
31
36
|
.option("--status [session-id]", "show ask-pro session status")
|
|
32
37
|
.option("--harvest [session-id]", "print harvested ANSWER.md for a session")
|
|
33
38
|
.option("--copy [session-id]", "print the copy target for a session")
|
|
@@ -52,6 +57,8 @@ program
|
|
|
52
57
|
await program.parseAsync(process.argv);
|
|
53
58
|
async function runAskPro(question, options) {
|
|
54
59
|
const cwd = resolveProjectCwd(options);
|
|
60
|
+
if (options.answers && question.trim())
|
|
61
|
+
throw new Error("Use --answers with --resume, without a question argument.");
|
|
55
62
|
await pruneExpiredAskProSessions({ cwd });
|
|
56
63
|
if (options.status !== undefined) {
|
|
57
64
|
const { status } = await readAskProStatus({ cwd, sessionId: optionSessionId(options.status) });
|
|
@@ -115,6 +122,12 @@ async function runAskPro(question, options) {
|
|
|
115
122
|
}
|
|
116
123
|
if (options.resume !== undefined) {
|
|
117
124
|
const { status } = await readAskProStatus({ cwd, sessionId: optionSessionId(options.resume) });
|
|
125
|
+
if (options.answers !== undefined && options.temporary !== undefined) {
|
|
126
|
+
const metadata = await readBrowserMetadata(cwd, status.sessionId);
|
|
127
|
+
const savedTemporary = metadata?.temporary ?? metadata?.url?.includes("temporary-chat=true");
|
|
128
|
+
if (savedTemporary !== options.temporary)
|
|
129
|
+
throw new Error("Cannot change Temporary Chat mode while answering questions; resume with the saved mode.");
|
|
130
|
+
}
|
|
118
131
|
const effectiveOptions = mergeStatusOptions(options, status);
|
|
119
132
|
const resumeCommand = buildResumeCommand(status.sessionId, effectiveOptions, cwd);
|
|
120
133
|
const harvestCommand = buildHarvestCommand(status.sessionId, cwd);
|
|
@@ -192,6 +205,10 @@ function optionSessionId(value) {
|
|
|
192
205
|
}
|
|
193
206
|
async function submitOrResumeBrowserSession(cwd, sessionId, options) {
|
|
194
207
|
const { status } = await readAskProStatus({ cwd, sessionId });
|
|
208
|
+
if (options.answers !== undefined && status.status !== "AWAITING_ANSWERS")
|
|
209
|
+
throw new Error("This session is not waiting for answers. Resume without --answers to inspect it.");
|
|
210
|
+
if (options.answers === "-" && process.stdin.isTTY)
|
|
211
|
+
throw new Error("--answers - requires piped stdin.");
|
|
195
212
|
if (status.status === "COMPLETED" || status.status === "HARVESTED") {
|
|
196
213
|
printStatusRecord(status, {
|
|
197
214
|
...(await readBrowserPreflight(cwd, status)),
|
|
@@ -200,6 +217,7 @@ async function submitOrResumeBrowserSession(cwd, sessionId, options) {
|
|
|
200
217
|
return;
|
|
201
218
|
}
|
|
202
219
|
if (status.status === "WAITING" ||
|
|
220
|
+
status.status === "AWAITING_ANSWERS" ||
|
|
203
221
|
status.status === "WAIT_TIMED_OUT" ||
|
|
204
222
|
status.status === "INCOMPLETE_ANSWER" ||
|
|
205
223
|
status.status === "NEEDS_USER_AUTH" ||
|
|
@@ -210,6 +228,11 @@ async function submitOrResumeBrowserSession(cwd, sessionId, options) {
|
|
|
210
228
|
sessionId,
|
|
211
229
|
temporary: options.temporary,
|
|
212
230
|
verbose: options.verbose,
|
|
231
|
+
answers: options.answers === undefined
|
|
232
|
+
? undefined
|
|
233
|
+
: options.answers === "-"
|
|
234
|
+
? await readStdin()
|
|
235
|
+
: await fs.readFile(path.resolve(cwd, options.answers), "utf8"),
|
|
213
236
|
});
|
|
214
237
|
}
|
|
215
238
|
catch (error) {
|
|
@@ -218,7 +241,7 @@ async function submitOrResumeBrowserSession(cwd, sessionId, options) {
|
|
|
218
241
|
printStatusRecord(paused);
|
|
219
242
|
return;
|
|
220
243
|
}
|
|
221
|
-
if (error instanceof AssistantStoppedError) {
|
|
244
|
+
if (error instanceof AssistantStoppedError || error instanceof AssistantQuestionsError) {
|
|
222
245
|
const { status: stopped } = await readAskProStatus({ cwd, sessionId });
|
|
223
246
|
printStatusRecord(stopped, await readBrowserPreflight(cwd, stopped));
|
|
224
247
|
return;
|
|
@@ -255,7 +278,7 @@ async function submitOrResumeBrowserSession(cwd, sessionId, options) {
|
|
|
255
278
|
printStatusRecord(paused);
|
|
256
279
|
return;
|
|
257
280
|
}
|
|
258
|
-
if (error instanceof AssistantStoppedError) {
|
|
281
|
+
if (error instanceof AssistantStoppedError || error instanceof AssistantQuestionsError) {
|
|
259
282
|
const { status: stopped } = await readAskProStatus({ cwd, sessionId });
|
|
260
283
|
printStatusRecord(stopped, await readBrowserPreflight(cwd, stopped));
|
|
261
284
|
return;
|
|
@@ -388,6 +411,8 @@ function actionForStatus(status) {
|
|
|
388
411
|
switch (status.status) {
|
|
389
412
|
case "NEEDS_GITHUB_CONNECTION":
|
|
390
413
|
return "connect_github_then_resume";
|
|
414
|
+
case "AWAITING_ANSWERS":
|
|
415
|
+
return "answer_questions";
|
|
391
416
|
case "DRY_RUN_COMPLETE":
|
|
392
417
|
case "INCOMPLETE_ANSWER":
|
|
393
418
|
case "READY_TO_SUBMIT":
|
|
@@ -411,6 +436,7 @@ function shouldPrintResume(status) {
|
|
|
411
436
|
"DRY_RUN_COMPLETE",
|
|
412
437
|
"INCOMPLETE_ANSWER",
|
|
413
438
|
"WAITING",
|
|
439
|
+
"AWAITING_ANSWERS",
|
|
414
440
|
"READY_TO_SUBMIT",
|
|
415
441
|
"NEEDS_USER_AUTH",
|
|
416
442
|
"NEEDS_GITHUB_CONNECTION",
|
|
@@ -431,7 +457,7 @@ function isAnswerBearingStatus(status) {
|
|
|
431
457
|
return ["COMPLETED", "HARVESTED"].includes(status.status);
|
|
432
458
|
}
|
|
433
459
|
async function readRecoverableCapturedAnswer(cwd, status) {
|
|
434
|
-
if (status.status === "INCOMPLETE_ANSWER") {
|
|
460
|
+
if (status.status === "INCOMPLETE_ANSWER" || status.status === "AWAITING_ANSWERS") {
|
|
435
461
|
return null;
|
|
436
462
|
}
|
|
437
463
|
try {
|
|
@@ -457,7 +483,25 @@ async function readBrowserPreflightForSession(cwd, sessionId) {
|
|
|
457
483
|
return {};
|
|
458
484
|
const profileDir = typeof metadata.profileDir === "string" ? metadata.profileDir : undefined;
|
|
459
485
|
const profile = profileMode(profileDir);
|
|
486
|
+
const questions = {};
|
|
487
|
+
if (metadata.status === "awaiting_answers") {
|
|
488
|
+
const request = await readAskProQuestions(getAskProSessionPaths(cwd, sessionId).dir);
|
|
489
|
+
questions.request_id = request.requestId;
|
|
490
|
+
questions.questions = request.questions.length;
|
|
491
|
+
questions.questions_file = `.ask-pro/sessions/${sessionId}/QUESTIONS.json`;
|
|
492
|
+
questions.reply = `${buildSessionCommand(cwd, ["--resume", sessionId, "--answers", "<answers.json>"])}`;
|
|
493
|
+
questions.answers_format =
|
|
494
|
+
'{"requestId":"<request_id>","answers":[1,"free text"]} (one answer per question; choices are 1-based)';
|
|
495
|
+
for (const [index, question] of request.questions.entries()) {
|
|
496
|
+
questions[`question_${index + 1}`] = question.question;
|
|
497
|
+
questions[`options_${index + 1}`] = question.options
|
|
498
|
+
.map((option, choice) => `${choice + 1}: ${option}`)
|
|
499
|
+
.join("; ");
|
|
500
|
+
questions[`free_text_${index + 1}`] = question.freeText;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
460
503
|
return compactFields({
|
|
504
|
+
...questions,
|
|
461
505
|
profile,
|
|
462
506
|
profile_path: profile && profileDir ? collapseHome(profileDir) : undefined,
|
|
463
507
|
chrome: chromeMode(metadata),
|
|
@@ -2,6 +2,8 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { DEFAULT_CHATGPT_BROWSER_MODEL_LABEL } from "../browser/chatgptModelCatalog.js";
|
|
4
4
|
import { AssistantStoppedError, BrowserAutomationError } from "../browser/errors.js";
|
|
5
|
+
import { AssistantQuestionsError } from "../browser/actions/questions.js";
|
|
6
|
+
import { readAskProQuestions, parseQuestionAnswers, writeAskProQuestions } from "./questions.js";
|
|
5
7
|
import { GitHubConnectionRequiredError } from "../browser/actions/github.js";
|
|
6
8
|
import { askProAgentIdForLegacyBrowserProfileDir, askProAgentIdForManagedBrowserProfileDir, askProBrowserProfileDirForAgentId, ensureAskProBrowserProfileDir, isLegacyAskProManagedBrowserProfileDir, isAskProManagedBrowserProfileDir, resolveAskProAgentId, } from "../browser/profilePaths.js";
|
|
7
9
|
import { runBrowserMode } from "../browserMode.js";
|
|
@@ -245,6 +247,12 @@ async function runAskProBrowserSessionWithLease({ cwd, sessionId, temporary: req
|
|
|
245
247
|
});
|
|
246
248
|
throw new AskProNeedsAuthError(sessionId, browserProfile, classifyBrowserError(error));
|
|
247
249
|
}
|
|
250
|
+
if (error instanceof AssistantQuestionsError) {
|
|
251
|
+
await writeAskProQuestions(paths.dir, error.questions);
|
|
252
|
+
await writeTerminalBrowserMetadata(cwd, sessionId, "awaiting_answers", "questions");
|
|
253
|
+
await updateAskProStatus({ cwd, sessionId, status: "AWAITING_ANSWERS", reason: "questions" });
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
248
256
|
if (error instanceof AssistantStoppedError) {
|
|
249
257
|
await writeTerminalBrowserMetadata(cwd, sessionId, "incomplete_answer", "stopped_without_answer");
|
|
250
258
|
await updateAskProStatus({
|
|
@@ -281,13 +289,28 @@ export async function resumeAskProBrowserSession(options) {
|
|
|
281
289
|
const { dir } = getAskProSessionPaths(options.cwd, options.sessionId);
|
|
282
290
|
return withSessionControllerLease(dir, () => resumeAskProBrowserSessionWithLease(options));
|
|
283
291
|
}
|
|
284
|
-
async function resumeAskProBrowserSessionWithLease({ cwd, sessionId, temporary, verbose, }) {
|
|
292
|
+
async function resumeAskProBrowserSessionWithLease({ cwd, sessionId, temporary, verbose, answers, }) {
|
|
285
293
|
const paths = getAskProSessionPaths(cwd, sessionId);
|
|
286
294
|
const prompt = await readAskProPrompt({ cwd, sessionId });
|
|
287
295
|
const { status: sessionStatus } = await readAskProStatus({ cwd, sessionId });
|
|
296
|
+
const request = answers !== undefined ? await readAskProQuestions(paths.dir) : undefined;
|
|
297
|
+
if (answers !== undefined && sessionStatus.status !== "AWAITING_ANSWERS")
|
|
298
|
+
throw new Error("This session is not waiting for answers. Resume without --answers to inspect it.");
|
|
299
|
+
const questionAnswers = request && answers !== undefined
|
|
300
|
+
? {
|
|
301
|
+
questions: request.questions,
|
|
302
|
+
answers: parseQuestionAnswers(answers, request),
|
|
303
|
+
submissionState: { started: false },
|
|
304
|
+
}
|
|
305
|
+
: undefined;
|
|
288
306
|
const artifactsRequested = sessionStatus.artifacts === true;
|
|
289
307
|
const logger = buildAskProBrowserLogger(cwd, sessionId, verbose);
|
|
290
308
|
const metadata = await readBrowserMetadata(paths.browser);
|
|
309
|
+
if (questionAnswers &&
|
|
310
|
+
temporary !== undefined &&
|
|
311
|
+
temporary !== (metadata.temporary ?? isTemporaryAskProUrl(metadata.url ?? ""))) {
|
|
312
|
+
throw new Error("Cannot change Temporary Chat mode while answering questions; resume with the saved mode.");
|
|
313
|
+
}
|
|
291
314
|
const effectiveTemporary = temporary ?? metadata.temporary;
|
|
292
315
|
const chatgptUrl = effectiveTemporary === true
|
|
293
316
|
? ASK_PRO_TEMPORARY_CHATGPT_URL
|
|
@@ -377,6 +400,10 @@ async function resumeAskProBrowserSessionWithLease({ cwd, sessionId, temporary,
|
|
|
377
400
|
thinkingTime: "pro",
|
|
378
401
|
startMinimized: false,
|
|
379
402
|
}, logger, {
|
|
403
|
+
questionAnswers,
|
|
404
|
+
afterQuestionsAnswered: async () => {
|
|
405
|
+
await fs.rm(path.join(paths.dir, "QUESTIONS.json"), { force: true });
|
|
406
|
+
},
|
|
380
407
|
promptPreview: prompt,
|
|
381
408
|
chromeModeCb: async (chromeMode) => {
|
|
382
409
|
const current = await readBrowserMetadata(paths.browser).catch(() => metadata);
|
|
@@ -473,6 +500,12 @@ async function resumeAskProBrowserSessionWithLease({ cwd, sessionId, temporary,
|
|
|
473
500
|
});
|
|
474
501
|
throw new AskProNeedsAuthError(sessionId, fallbackProfile, classifyBrowserError(error));
|
|
475
502
|
}
|
|
503
|
+
if (error instanceof AssistantQuestionsError) {
|
|
504
|
+
await writeAskProQuestions(paths.dir, error.questions);
|
|
505
|
+
await writeTerminalBrowserMetadata(cwd, sessionId, "awaiting_answers", "questions");
|
|
506
|
+
await updateAskProStatus({ cwd, sessionId, status: "AWAITING_ANSWERS", reason: "questions" });
|
|
507
|
+
throw error;
|
|
508
|
+
}
|
|
476
509
|
if (error instanceof AssistantStoppedError) {
|
|
477
510
|
await writeTerminalBrowserMetadata(cwd, sessionId, "incomplete_answer", "stopped_without_answer");
|
|
478
511
|
await updateAskProStatus({
|
|
@@ -494,11 +527,12 @@ async function resumeAskProBrowserSessionWithLease({ cwd, sessionId, temporary,
|
|
|
494
527
|
throw error;
|
|
495
528
|
}
|
|
496
529
|
const reason = error instanceof Error ? error.message : String(error);
|
|
497
|
-
|
|
530
|
+
const answersPending = questionAnswers && !questionAnswers.submissionState.started;
|
|
531
|
+
await writeTerminalBrowserMetadata(cwd, sessionId, answersPending ? "awaiting_answers" : questionAnswers ? "wait_timed_out" : "failed", reason);
|
|
498
532
|
await updateAskProStatus({
|
|
499
533
|
cwd,
|
|
500
534
|
sessionId,
|
|
501
|
-
status: "FAILED",
|
|
535
|
+
status: answersPending ? "AWAITING_ANSWERS" : questionAnswers ? "WAIT_TIMED_OUT" : "FAILED",
|
|
502
536
|
reason,
|
|
503
537
|
});
|
|
504
538
|
throw error;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { atomicWriteFile } from "./atomicWrite.js";
|
|
5
|
+
export async function readAskProQuestions(sessionDir) {
|
|
6
|
+
return JSON.parse(await fs.readFile(path.join(sessionDir, "QUESTIONS.json"), "utf8"));
|
|
7
|
+
}
|
|
8
|
+
export async function writeAskProQuestions(sessionDir, questions) {
|
|
9
|
+
const previous = await readAskProQuestions(sessionDir).catch(() => null);
|
|
10
|
+
const request = {
|
|
11
|
+
requestId: previous && JSON.stringify(previous.questions) === JSON.stringify(questions)
|
|
12
|
+
? previous.requestId
|
|
13
|
+
: randomUUID(),
|
|
14
|
+
questions,
|
|
15
|
+
};
|
|
16
|
+
await atomicWriteFile(path.join(sessionDir, "QUESTIONS.json"), `${JSON.stringify(request, null, 2)}\n`);
|
|
17
|
+
}
|
|
18
|
+
export function parseQuestionAnswers(raw, request) {
|
|
19
|
+
const value = JSON.parse(raw);
|
|
20
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
21
|
+
throw new Error("Answers must be a JSON object with requestId and answers.");
|
|
22
|
+
}
|
|
23
|
+
if (value?.requestId !== request.requestId)
|
|
24
|
+
throw new Error("Answers belong to a different question request; read the current QUESTIONS.json.");
|
|
25
|
+
if (!Array.isArray(value.answers) || value.answers.length !== request.questions.length) {
|
|
26
|
+
throw new Error("Provide one answer per question in answers, in question order.");
|
|
27
|
+
}
|
|
28
|
+
for (const [index, answer] of value.answers.entries()) {
|
|
29
|
+
const question = request.questions[index];
|
|
30
|
+
if (typeof answer === "number" &&
|
|
31
|
+
Number.isInteger(answer) &&
|
|
32
|
+
answer >= 1 &&
|
|
33
|
+
answer <= question.options.length)
|
|
34
|
+
continue;
|
|
35
|
+
if (typeof answer === "string" && answer.trim() && question.freeText)
|
|
36
|
+
continue;
|
|
37
|
+
throw new Error(`Question ${index + 1}: choose a numbered option or provide nonempty free text.`);
|
|
38
|
+
}
|
|
39
|
+
return value.answers;
|
|
40
|
+
}
|
|
@@ -4,6 +4,7 @@ import { logDomFailure } from "../domDebug.js";
|
|
|
4
4
|
import { buildClickDispatcher } from "./domEvents.js";
|
|
5
5
|
import { AssistantStoppedError } from "../errors.js";
|
|
6
6
|
import { readComposerSnapshot, submitPrompt } from "./promptComposer.js";
|
|
7
|
+
import { AssistantQuestionsError, readAssistantQuestions } from "./questions.js";
|
|
7
8
|
const ASSISTANT_POLL_TIMEOUT_ERROR = "assistant-response-watchdog-timeout";
|
|
8
9
|
export function createAssistantContinuation(Runtime, Input, logger, inputGuard, state = { used: false }) {
|
|
9
10
|
return async (turnIndex) => {
|
|
@@ -60,14 +61,18 @@ export async function waitForAssistantResponse(Runtime, timeoutMs, logger, minTu
|
|
|
60
61
|
const deadline = Date.now() + timeoutMs;
|
|
61
62
|
while (true) {
|
|
62
63
|
try {
|
|
63
|
-
|
|
64
|
+
const answer = await waitForAssistantResponseOnce(Runtime, Math.max(1, deadline - Date.now()), logger, minTurnIndex, expectedConversationId);
|
|
65
|
+
const questions = await readAssistantQuestions(Runtime);
|
|
66
|
+
if (questions)
|
|
67
|
+
throw new AssistantQuestionsError(questions);
|
|
68
|
+
return answer;
|
|
64
69
|
}
|
|
65
70
|
catch (error) {
|
|
66
|
-
if (!(error instanceof AssistantStoppedError))
|
|
67
|
-
throw error;
|
|
68
71
|
await Runtime.evaluate({
|
|
69
72
|
expression: "window.__askProCancelResponseObserver?.()",
|
|
70
73
|
}).catch(() => undefined);
|
|
74
|
+
if (!(error instanceof AssistantStoppedError))
|
|
75
|
+
throw error;
|
|
71
76
|
if (!continueResponse || Date.now() >= deadline)
|
|
72
77
|
throw error;
|
|
73
78
|
minTurnIndex = await continueResponse(error.turnIndex);
|
|
@@ -126,6 +131,7 @@ async function waitForAssistantResponseOnce(Runtime, timeoutMs, logger, minTurnI
|
|
|
126
131
|
}
|
|
127
132
|
else if (source === "poll") {
|
|
128
133
|
pollerAbort.abort();
|
|
134
|
+
evaluationPromise.catch(() => undefined);
|
|
129
135
|
throw error;
|
|
130
136
|
}
|
|
131
137
|
else if (source === "evaluation") {
|
|
@@ -341,6 +347,9 @@ async function pollAssistantCompletion(Runtime, timeoutMs, minTurnIndex, expecte
|
|
|
341
347
|
if (abortSignal?.aborted) {
|
|
342
348
|
return null;
|
|
343
349
|
}
|
|
350
|
+
const questions = await readAssistantQuestions(Runtime);
|
|
351
|
+
if (questions)
|
|
352
|
+
throw new AssistantQuestionsError(questions);
|
|
344
353
|
const snapshot = await readAssistantSnapshot(Runtime, minTurnIndex, expectedConversationId);
|
|
345
354
|
const normalized = normalizeAssistantSnapshot(snapshot);
|
|
346
355
|
if (normalized) {
|
|
@@ -332,7 +332,7 @@ export async function submitPrompt(deps, prompt, logger) {
|
|
|
332
332
|
composerState: postVerification,
|
|
333
333
|
});
|
|
334
334
|
}
|
|
335
|
-
const clicked = await attemptSendButton(runtime, logger, deps?.attachmentNames, deps.inputTimeoutMs ?? undefined);
|
|
335
|
+
const clicked = await attemptSendButton(runtime, logger, deps?.attachmentNames, deps.inputTimeoutMs ?? undefined, deps.submissionState);
|
|
336
336
|
if (!clicked) {
|
|
337
337
|
const canSubmitViaEnter = await canSubmitPromptViaEnter(runtime);
|
|
338
338
|
if (!canSubmitViaEnter) {
|
|
@@ -342,6 +342,8 @@ export async function submitPrompt(deps, prompt, logger) {
|
|
|
342
342
|
code: "unsafe-enter-submit",
|
|
343
343
|
});
|
|
344
344
|
}
|
|
345
|
+
if (deps.submissionState)
|
|
346
|
+
deps.submissionState.started = true;
|
|
345
347
|
await input.dispatchKeyEvent({
|
|
346
348
|
type: "keyDown",
|
|
347
349
|
...ENTER_KEY_EVENT,
|
|
@@ -539,7 +541,7 @@ async function waitForDomReady(Runtime, logger, timeoutMs = 10_000) {
|
|
|
539
541
|
export function buildComposerSendReadinessExpressionForTest() {
|
|
540
542
|
return buildComposerSendReadinessExpression();
|
|
541
543
|
}
|
|
542
|
-
async function attemptSendButton(Runtime, logger, attachmentNames, inputTimeoutMs) {
|
|
544
|
+
async function attemptSendButton(Runtime, logger, attachmentNames, inputTimeoutMs, submissionState) {
|
|
543
545
|
const needAttachment = Array.isArray(attachmentNames) && attachmentNames.length > 0;
|
|
544
546
|
const deadline = Date.now() +
|
|
545
547
|
(needAttachment ? Math.max(15_000, Math.min(inputTimeoutMs ?? 30_000, 30_000)) : 8_000);
|
|
@@ -572,6 +574,8 @@ async function attemptSendButton(Runtime, logger, attachmentNames, inputTimeoutM
|
|
|
572
574
|
continue;
|
|
573
575
|
}
|
|
574
576
|
}
|
|
577
|
+
if (submissionState)
|
|
578
|
+
submissionState.started = true;
|
|
575
579
|
const { result } = await Runtime.evaluate({
|
|
576
580
|
expression: buildComposerSendClickExpression(),
|
|
577
581
|
returnByValue: true,
|
|
@@ -579,6 +583,8 @@ async function attemptSendButton(Runtime, logger, attachmentNames, inputTimeoutM
|
|
|
579
583
|
if (result.value === "clicked") {
|
|
580
584
|
return true;
|
|
581
585
|
}
|
|
586
|
+
if (submissionState)
|
|
587
|
+
submissionState.started = false;
|
|
582
588
|
if (result.value === "stop-button") {
|
|
583
589
|
throw new BrowserAutomationError("Refusing to click ChatGPT stop control as a send button.", {
|
|
584
590
|
stage: "submit-prompt",
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { readComposerSnapshot, submitPrompt } from "./promptComposer.js";
|
|
2
|
+
import { CONVERSATION_TURN_SELECTOR } from "../constants.js";
|
|
3
|
+
export class AssistantQuestionsError extends Error {
|
|
4
|
+
questions;
|
|
5
|
+
constructor(questions) {
|
|
6
|
+
super("ChatGPT is waiting for answers to its questions.");
|
|
7
|
+
this.questions = questions;
|
|
8
|
+
this.name = "AssistantQuestionsError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
// This function also runs in Chrome; keep it independent of module state.
|
|
12
|
+
async function questionCards(answers, expected, submission = { started: false }) {
|
|
13
|
+
const selector = '[data-codex-composer-request-navigation="true"]';
|
|
14
|
+
const visible = (node) => {
|
|
15
|
+
const rect = node.getBoundingClientRect();
|
|
16
|
+
return rect.width > 0 && rect.height > 0 && !node.closest('[inert], [aria-hidden="true"]');
|
|
17
|
+
};
|
|
18
|
+
const card = () => Array.from(document.querySelectorAll(selector)).find(visible);
|
|
19
|
+
let root = card();
|
|
20
|
+
if (!root)
|
|
21
|
+
return null;
|
|
22
|
+
// Native pointer interaction cancels ChatGPT's automatic question dismissal.
|
|
23
|
+
root.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true, pointerType: "mouse" }));
|
|
24
|
+
const pause = () => new Promise((resolve) => setTimeout(resolve, 100));
|
|
25
|
+
const read = () => {
|
|
26
|
+
root = card();
|
|
27
|
+
if (!root)
|
|
28
|
+
throw new Error("ChatGPT question card disappeared.");
|
|
29
|
+
return {
|
|
30
|
+
question: root.querySelector(".text-size-chat")?.innerText.trim() ?? "",
|
|
31
|
+
options: Array.from(root.querySelectorAll('[role="radio"]')).map((option) => option.getAttribute("aria-label") ?? option.innerText.replace(/^\d+\s*/, "").trim()),
|
|
32
|
+
freeText: Boolean(root.querySelector('[role="textbox"], textarea, input:not([type="file"])')),
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
const position = () => {
|
|
36
|
+
const match = root?.innerText.match(/(?:^|\n)(\d+) of (\d+)(?:\n|$)/);
|
|
37
|
+
return match ? { index: Number(match[1]), count: Number(match[2]) } : { index: 1, count: 1 };
|
|
38
|
+
};
|
|
39
|
+
const move = async (direction) => {
|
|
40
|
+
const before = position().index;
|
|
41
|
+
const button = root?.querySelector(`button[aria-label="${direction} question"]`);
|
|
42
|
+
if (!button || button.disabled)
|
|
43
|
+
throw new Error("Cannot browse ChatGPT's questions.");
|
|
44
|
+
button.click();
|
|
45
|
+
const deadline = Date.now() + 5_000;
|
|
46
|
+
do {
|
|
47
|
+
await pause();
|
|
48
|
+
read();
|
|
49
|
+
if (position().index !== before)
|
|
50
|
+
return;
|
|
51
|
+
} while (Date.now() < deadline);
|
|
52
|
+
throw new Error("ChatGPT did not change questions.");
|
|
53
|
+
};
|
|
54
|
+
while (position().index > 1)
|
|
55
|
+
await move("Previous");
|
|
56
|
+
const questions = [];
|
|
57
|
+
const count = position().count;
|
|
58
|
+
for (let index = 0; index < count; index++) {
|
|
59
|
+
const current = read();
|
|
60
|
+
if (!current.question)
|
|
61
|
+
throw new Error("Cannot read ChatGPT's question.");
|
|
62
|
+
questions.push(current);
|
|
63
|
+
if (index + 1 < count)
|
|
64
|
+
await move("Next");
|
|
65
|
+
}
|
|
66
|
+
while (position().index > 1)
|
|
67
|
+
await move("Previous");
|
|
68
|
+
if (!answers)
|
|
69
|
+
return questions;
|
|
70
|
+
if (JSON.stringify(questions) !== JSON.stringify(expected)) {
|
|
71
|
+
throw new Error("ChatGPT's questions changed; read the current questions before answering.");
|
|
72
|
+
}
|
|
73
|
+
if (answers.length !== questions.length)
|
|
74
|
+
throw new Error("Provide one answer per question.");
|
|
75
|
+
for (const [index, answer] of answers.entries()) {
|
|
76
|
+
const question = questions[index];
|
|
77
|
+
if (typeof answer === "number") {
|
|
78
|
+
if (!Number.isInteger(answer) || answer < 1 || answer > question.options.length) {
|
|
79
|
+
throw new Error(`Question ${index + 1}: choose 1-${question.options.length} or provide free text.`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else if (typeof answer !== "string" || !answer.trim() || !question.freeText) {
|
|
83
|
+
throw new Error(`Question ${index + 1}: a nonempty free-text answer is required.`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
for (const [index, answer] of answers.entries()) {
|
|
87
|
+
read();
|
|
88
|
+
if (typeof answer === "number") {
|
|
89
|
+
const options = Array.from(root.querySelectorAll('[role="radio"]'));
|
|
90
|
+
if (index + 1 === count)
|
|
91
|
+
submission.started = true;
|
|
92
|
+
options[answer - 1].click();
|
|
93
|
+
if (index + 1 < count) {
|
|
94
|
+
const deadline = Date.now() + 5_000;
|
|
95
|
+
do {
|
|
96
|
+
await pause();
|
|
97
|
+
read();
|
|
98
|
+
} while (position().index === index + 1 && Date.now() < deadline);
|
|
99
|
+
if (position().index !== index + 2)
|
|
100
|
+
throw new Error(`ChatGPT did not accept answer for question ${index + 1}.`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const input = root.querySelector('[role="textbox"], textarea, input:not([type="file"])');
|
|
105
|
+
if (!input)
|
|
106
|
+
throw new Error("ChatGPT's free-text input is missing.");
|
|
107
|
+
input.focus();
|
|
108
|
+
if (input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement) {
|
|
109
|
+
const prototype = input instanceof HTMLInputElement
|
|
110
|
+
? HTMLInputElement.prototype
|
|
111
|
+
: HTMLTextAreaElement.prototype;
|
|
112
|
+
Object.getOwnPropertyDescriptor(prototype, "value").set.call(input, answer);
|
|
113
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
const selection = window.getSelection();
|
|
117
|
+
const range = document.createRange();
|
|
118
|
+
range.selectNodeContents(input);
|
|
119
|
+
selection?.removeAllRanges();
|
|
120
|
+
selection?.addRange(range);
|
|
121
|
+
document.execCommand("insertText", false, answer);
|
|
122
|
+
}
|
|
123
|
+
await pause();
|
|
124
|
+
if ((input instanceof HTMLInputElement || input instanceof HTMLTextAreaElement
|
|
125
|
+
? input.value
|
|
126
|
+
: Array.from(input.children)
|
|
127
|
+
.map((child) => (child.textContent ? child.innerText : ""))
|
|
128
|
+
.join("\n")) !== answer) {
|
|
129
|
+
throw new Error(`Could not enter answer for question ${index + 1}.`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (typeof answer === "string" && index + 1 < count)
|
|
133
|
+
await move("Next");
|
|
134
|
+
}
|
|
135
|
+
// Choice clicks advance/submit natively; a custom answer uses the card's submit button.
|
|
136
|
+
if (typeof answers.at(-1) === "string") {
|
|
137
|
+
const submit = root.querySelector("[data-request-input-skip]");
|
|
138
|
+
if (!submit || submit.disabled || !/^(submit|send|continue)$/i.test(submit.innerText.trim()))
|
|
139
|
+
throw new Error("ChatGPT's answer submit button is not ready.");
|
|
140
|
+
submission.started = true;
|
|
141
|
+
submit.click();
|
|
142
|
+
}
|
|
143
|
+
const deadline = Date.now() + 5_000;
|
|
144
|
+
while (card() && Date.now() < deadline)
|
|
145
|
+
await pause();
|
|
146
|
+
if (card())
|
|
147
|
+
throw new Error("ChatGPT did not accept the answers; inspect the question card before retrying.");
|
|
148
|
+
return questions;
|
|
149
|
+
}
|
|
150
|
+
export function buildQuestionCardsExpression(answers, expected) {
|
|
151
|
+
// tsx preserves function names with __name; Chrome only needs the function itself.
|
|
152
|
+
const call = `(${questionCards.toString()})(${JSON.stringify(answers) ?? "undefined"}, ${JSON.stringify(expected) ?? "undefined"}, submission)`;
|
|
153
|
+
return `(() => { const __name = (fn) => fn; const submission = { started: false }; return ${answers ? `${call}.then(questions => ({ questions, started: submission.started }), error => ({ error: String(error), started: submission.started }))` : call}; })()`;
|
|
154
|
+
}
|
|
155
|
+
export async function readAssistantQuestions(Runtime) {
|
|
156
|
+
const evaluation = await Runtime.evaluate({
|
|
157
|
+
expression: buildQuestionCardsExpression(),
|
|
158
|
+
awaitPromise: true,
|
|
159
|
+
returnByValue: true,
|
|
160
|
+
});
|
|
161
|
+
if (evaluation.exceptionDetails)
|
|
162
|
+
throw new Error(evaluation.exceptionDetails.exception?.description ?? "Cannot read ChatGPT's questions.");
|
|
163
|
+
return Array.isArray(evaluation.result.value) && evaluation.result.value.length > 0
|
|
164
|
+
? evaluation.result.value
|
|
165
|
+
: null;
|
|
166
|
+
}
|
|
167
|
+
export async function answerAssistantQuestions(Runtime, Input, logger, answers, expected, submissionState = { started: false }) {
|
|
168
|
+
// A lost transport may have submitted successfully; never replay that attempt automatically.
|
|
169
|
+
submissionState.started = true;
|
|
170
|
+
const evaluation = await Runtime.evaluate({
|
|
171
|
+
expression: buildQuestionCardsExpression(answers, expected),
|
|
172
|
+
awaitPromise: true,
|
|
173
|
+
returnByValue: true,
|
|
174
|
+
});
|
|
175
|
+
if (evaluation.exceptionDetails)
|
|
176
|
+
throw new Error(evaluation.exceptionDetails.exception?.description ?? "Cannot answer ChatGPT's questions.");
|
|
177
|
+
const value = evaluation.result.value;
|
|
178
|
+
submissionState.started = value.started;
|
|
179
|
+
if (value.error)
|
|
180
|
+
throw new Error(value.error);
|
|
181
|
+
if (!value.questions) {
|
|
182
|
+
// Native snooze returned 409 in the canary; expired cards need a reply in the same chat.
|
|
183
|
+
const composer = await readComposerSnapshot(Runtime);
|
|
184
|
+
if (composer.activeValue.trim())
|
|
185
|
+
throw new Error("The ChatGPT composer has a draft; clear it before answering expired questions.");
|
|
186
|
+
const prompt = `Answers to your clarification questions:\n\n${expected.map((question, index) => `${index + 1}. ${question.question}\n${typeof answers[index] === "number" ? question.options[answers[index] - 1] : answers[index]}`).join("\n\n")}\n\nContinue the original request using these answers.`;
|
|
187
|
+
const { result } = await Runtime.evaluate({
|
|
188
|
+
expression: `document.querySelectorAll(${JSON.stringify(CONVERSATION_TURN_SELECTOR)}).length`,
|
|
189
|
+
returnByValue: true,
|
|
190
|
+
});
|
|
191
|
+
const baselineTurns = result.value;
|
|
192
|
+
await submitPrompt({ runtime: Runtime, input: Input, baselineTurns, submissionState }, prompt, logger);
|
|
193
|
+
return baselineTurns;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { formatElapsed } from "../format.js";
|
|
2
|
-
import { ASSISTANT_ROLE_SELECTOR, CONVERSATION_TURN_SELECTOR } from "../constants.js";
|
|
2
|
+
import { ASSISTANT_ROLE_SELECTOR, CONVERSATION_TURN_SELECTOR, STOP_BUTTON_SELECTOR, } from "../constants.js";
|
|
3
3
|
const THINKING_STALE_HINT_MS = 10 * 60_000;
|
|
4
4
|
export function startThinkingStatusMonitor(Runtime, logger, options = {}) {
|
|
5
5
|
const intervalMs = resolveThinkingStatusInterval(options.intervalMs);
|
|
@@ -200,13 +200,15 @@ function buildThinkingStatusExpression() {
|
|
|
200
200
|
[node.textContent, node.getAttribute?.('aria-label'), node.getAttribute?.('title')]
|
|
201
201
|
.some((label) => normalize(label) === expected),
|
|
202
202
|
);
|
|
203
|
-
|
|
203
|
+
const stopActive = hasControl('stop answering') ||
|
|
204
|
+
Array.from(document.querySelectorAll(${JSON.stringify(STOP_BUTTON_SELECTOR)})).some(isVisible);
|
|
205
|
+
if (hasControl('answer now') && stopActive) {
|
|
204
206
|
return {
|
|
205
207
|
message: 'pro gate active',
|
|
206
208
|
source: 'inline',
|
|
207
209
|
};
|
|
208
210
|
}
|
|
209
|
-
if (
|
|
211
|
+
if (stopActive) {
|
|
210
212
|
return { message: 'active', source: 'inline' };
|
|
211
213
|
}
|
|
212
214
|
const looksLikeThinking = (node) => {
|
|
@@ -32,8 +32,8 @@ export const ANSWER_SELECTORS = [
|
|
|
32
32
|
];
|
|
33
33
|
export const CONVERSATION_TURN_SELECTOR = 'article[data-testid^="conversation-turn"], div[data-testid^="conversation-turn"], section[data-testid^="conversation-turn"], ' +
|
|
34
34
|
"article[data-message-author-role], div[data-message-author-role], section[data-message-author-role], " +
|
|
35
|
-
|
|
36
|
-
export const ASSISTANT_ROLE_SELECTOR = '[data-message-author-role="assistant"], [data-turn="assistant"]';
|
|
35
|
+
'article[data-turn], div[data-turn], section[data-turn], [data-content-search-unit-key$=":assistant"], [data-content-search-unit-key$=":user"]';
|
|
36
|
+
export const ASSISTANT_ROLE_SELECTOR = '[data-message-author-role="assistant"], [data-turn="assistant"], [data-markdown-text-style="assistant-message"]';
|
|
37
37
|
export const CLOUDFLARE_TITLE = "just a moment";
|
|
38
38
|
export const PROMPT_PRIMARY_SELECTOR = "#prompt-textarea";
|
|
39
39
|
export const PROMPT_FALLBACK_SELECTOR = 'textarea[name="prompt-textarea"]';
|
|
@@ -19,6 +19,7 @@ import { estimateTokenCount, withRetries, delay } from "./utils.js";
|
|
|
19
19
|
import { formatElapsed } from "./format.js";
|
|
20
20
|
import { CHATGPT_URL, CONVERSATION_TURN_SELECTOR, DEFAULT_MODEL_STRATEGY } from "./constants.js";
|
|
21
21
|
import { AssistantStoppedError, BrowserAutomationError } from "./errors.js";
|
|
22
|
+
import { AssistantQuestionsError } from "./actions/questions.js";
|
|
22
23
|
import { defaultAskProBrowserProfileDir } from "./profilePaths.js";
|
|
23
24
|
import { applyPageLanguageOverrides, seedChromeProfileLanguage } from "./language.js";
|
|
24
25
|
import { alignPromptEchoPair, buildPromptEchoMatcher, extractConversationIdFromUrl, withTimeout, } from "./reattachHelpers.js";
|
|
@@ -1292,6 +1293,12 @@ export async function runBrowserMode(options) {
|
|
|
1292
1293
|
}
|
|
1293
1294
|
catch (error) {
|
|
1294
1295
|
let normalizedError = error instanceof Error ? error : new Error(String(error));
|
|
1296
|
+
if (normalizedError instanceof AssistantQuestionsError) {
|
|
1297
|
+
preserveBrowserOnError = true;
|
|
1298
|
+
preserveWindowStateOnError = true;
|
|
1299
|
+
await emitRuntimeHint();
|
|
1300
|
+
throw normalizedError;
|
|
1301
|
+
}
|
|
1295
1302
|
if (normalizedError instanceof GitHubConnectionRequiredError) {
|
|
1296
1303
|
preserveBrowserOnError = true;
|
|
1297
1304
|
await revealAuthenticatedWindow("github-connection-required");
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AssistantStoppedError } from "./errors.js";
|
|
2
|
+
import { AssistantQuestionsError, answerAssistantQuestions, } from "./actions/questions.js";
|
|
2
3
|
import { createAssistantContinuation } from "./actions/assistantResponse.js";
|
|
3
4
|
import CDP from "chrome-remote-interface";
|
|
4
5
|
import os from "node:os";
|
|
@@ -113,6 +114,7 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
|
113
114
|
? await acquireReattachRunLease(directUserDataDir, resolvedConfig.profileLockTimeoutMs, logger)
|
|
114
115
|
: null;
|
|
115
116
|
let closeConnection = null;
|
|
117
|
+
let answeringQuestions = false;
|
|
116
118
|
try {
|
|
117
119
|
const liveRuntime = await refreshAttachRuntime(runtime, logger).catch(() => runtime);
|
|
118
120
|
if (!liveRuntime.chromePort && !liveRuntime.chromeBrowserWSEndpoint) {
|
|
@@ -152,7 +154,8 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
|
152
154
|
})),
|
|
153
155
|
close: async () => undefined,
|
|
154
156
|
};
|
|
155
|
-
closeConnection =
|
|
157
|
+
closeConnection =
|
|
158
|
+
browserWSEndpoint && !deps.connect ? connection.close : () => connection.client.close();
|
|
156
159
|
const connectedTargetId = "targetId" in connection ? connection.targetId : target?.targetId;
|
|
157
160
|
const ownsConnectedTarget = Boolean(!resolvedConfig.browserTabRef &&
|
|
158
161
|
liveRuntime.chromeTargetId &&
|
|
@@ -213,7 +216,14 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
|
213
216
|
const pingTimeoutMs = Math.min(5_000, Math.max(1_500, Math.floor(timeoutMs * 0.05)));
|
|
214
217
|
await withTimeout(Runtime.evaluate({ expression: "1+1", returnByValue: true }), pingTimeoutMs, "Reattach target did not respond");
|
|
215
218
|
await ensureConversationOpen();
|
|
216
|
-
|
|
219
|
+
let minTurnIndex = await readConversationTurnIndex(Runtime, logger);
|
|
220
|
+
if (deps.questionAnswers) {
|
|
221
|
+
answeringQuestions = true;
|
|
222
|
+
const replyBoundary = await answerAssistantQuestions(Runtime, Input, logger, deps.questionAnswers.answers, deps.questionAnswers.questions, deps.questionAnswers.submissionState);
|
|
223
|
+
if (replyBoundary !== undefined)
|
|
224
|
+
minTurnIndex = replyBoundary;
|
|
225
|
+
await deps.afterQuestionsAnswered?.();
|
|
226
|
+
}
|
|
217
227
|
const promptEcho = buildPromptEchoMatcher(deps.promptPreview);
|
|
218
228
|
const answer = await withTimeout(waitForResponse(Runtime, timeoutMs, logger, minTurnIndex ?? undefined, runtime.conversationId, createAssistantContinuation(Runtime, Input, logger, undefined, deps.continuationState)), timeoutMs + 5_000, "Reattach response timed out");
|
|
219
229
|
const recovered = await recoverPromptEcho(Runtime, answer, promptEcho, logger, minTurnIndex, timeoutMs);
|
|
@@ -228,7 +238,7 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
|
228
238
|
markdown: aligned.answerMarkdown,
|
|
229
239
|
},
|
|
230
240
|
});
|
|
231
|
-
await
|
|
241
|
+
await closeConnection().catch(() => undefined);
|
|
232
242
|
closeConnection = null;
|
|
233
243
|
const keepBrowserOpen = Boolean(resolvedConfig.keepBrowser ||
|
|
234
244
|
resolvedConfig.browserTabRef ||
|
|
@@ -261,7 +271,10 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
|
261
271
|
await closeConnection?.().catch(() => undefined);
|
|
262
272
|
await releaseReattachRunLease(reattachRunLease, logger);
|
|
263
273
|
reattachRunLease = null;
|
|
264
|
-
if (error instanceof AssistantStoppedError
|
|
274
|
+
if (error instanceof AssistantStoppedError ||
|
|
275
|
+
error instanceof AssistantQuestionsError ||
|
|
276
|
+
answeringQuestions ||
|
|
277
|
+
deps.questionAnswers?.submissionState.started)
|
|
265
278
|
throw error;
|
|
266
279
|
const message = error instanceof Error ? error.message : String(error);
|
|
267
280
|
logger(`Existing Chrome reattach failed (${message}); reopening browser to locate the session.`);
|
|
@@ -482,7 +495,13 @@ async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
|
|
|
482
495
|
const waitForResponse = deps.waitForAssistantResponse ?? waitForAssistantResponse;
|
|
483
496
|
const captureMarkdown = deps.captureAssistantMarkdown ?? captureAssistantMarkdown;
|
|
484
497
|
const timeoutMs = resolved.timeoutMs ?? 120_000;
|
|
485
|
-
|
|
498
|
+
let minTurnIndex = await readConversationTurnIndex(Runtime, logger);
|
|
499
|
+
if (deps.questionAnswers) {
|
|
500
|
+
const replyBoundary = await answerAssistantQuestions(Runtime, client.Input, logger, deps.questionAnswers.answers, deps.questionAnswers.questions, deps.questionAnswers.submissionState);
|
|
501
|
+
if (replyBoundary !== undefined)
|
|
502
|
+
minTurnIndex = replyBoundary;
|
|
503
|
+
await deps.afterQuestionsAnswered?.();
|
|
504
|
+
}
|
|
486
505
|
const promptEcho = buildPromptEchoMatcher(deps.promptPreview);
|
|
487
506
|
const answer = await waitForResponse(Runtime, timeoutMs, logger, minTurnIndex ?? undefined, runtime.conversationId, createAssistantContinuation(Runtime, client.Input, logger, undefined, deps.continuationState));
|
|
488
507
|
const recovered = await recoverPromptEcho(Runtime, answer, promptEcho, logger, minTurnIndex, timeoutMs);
|
package/package.json
CHANGED
package/skills/ask-pro/SKILL.md
CHANGED
|
@@ -162,6 +162,16 @@ Normal `ask-pro` stdout is compact TOON-style telemetry. Use `state`, `action`,
|
|
|
162
162
|
`resume`, and `harvest` to decide the next command. Browser progress may appear
|
|
163
163
|
on stderr and can be ignored unless diagnosing a stuck run.
|
|
164
164
|
|
|
165
|
+
`AWAITING_ANSWERS` / `answer_questions` means Pro is asking native interactive
|
|
166
|
+
questions. Read the printed questions/options or `QUESTIONS.json`, answer from
|
|
167
|
+
the caller's context, and ask the human only for decisions you cannot infer.
|
|
168
|
+
Write UTF-8 JSON `{"requestId":"<request_id>","answers":[2,3,"free text"]}`
|
|
169
|
+
with one answer per question in order, then run the emitted
|
|
170
|
+
`ask-pro --resume <session-id> --answers <answers.json>` command. Numbers select
|
|
171
|
+
1-based choices; strings enter the native free-text field. `--answers -` accepts
|
|
172
|
+
piped JSON. Do not submit defaults or treat the question preamble as the answer.
|
|
173
|
+
Repeat for further question rounds, then harvest the final answer normally.
|
|
174
|
+
|
|
165
175
|
When present, use `profile`, `profile_path`, `chrome`, and `language` only as
|
|
166
176
|
diagnostic hints. They tell you whether the run used the shared profile, an
|
|
167
177
|
isolated agent profile, saved DevTools state, and English browser steering.
|