setup-agent 0.1.0 → 0.1.2
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 +5 -1
- package/dist/agents.d.ts.map +1 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +68 -14
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/host.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/needs.d.ts +1 -1
- package/dist/needs.d.ts.map +1 -1
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +1 -1
- package/dist/presets.js.map +1 -1
- package/dist/protocol.d.ts +1 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +1 -1
- package/dist/sdk.js.map +1 -1
- package/dist/stream.d.ts +2 -2
- package/dist/stream.d.ts.map +1 -1
- package/dist/stream.js +9 -5
- package/dist/stream.js.map +1 -1
- package/package.json +4 -4
- package/src/cli.test.ts +140 -0
- package/src/cli.ts +114 -17
- package/src/presets.ts +1 -1
- package/src/sdk.test.ts +2 -0
- package/src/sdk.ts +1 -1
- package/src/stream.test.ts +3 -4
- package/src/stream.ts +26 -5
package/src/cli.ts
CHANGED
|
@@ -41,6 +41,15 @@ export { diffEvents } from "./stream";
|
|
|
41
41
|
|
|
42
42
|
const DEFAULT_COMMAND = "npx setup-agent";
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* How long "wait <input-id>", "wait plan" and "--wait" block before handing
|
|
46
|
+
* control back with a "waiting" result, in seconds. Agent shells kill a
|
|
47
|
+
* command after a limit of their own (two minutes by default in Claude
|
|
48
|
+
* Code); a wait that returns first lets the agent run it again instead of
|
|
49
|
+
* reading the kill as the end of the setup.
|
|
50
|
+
*/
|
|
51
|
+
const DEFAULT_WAIT_SECONDS = 100;
|
|
52
|
+
|
|
44
53
|
export const HELP = `setup-agent <url> [command]
|
|
45
54
|
|
|
46
55
|
Commands (the url always comes first):
|
|
@@ -65,10 +74,13 @@ Commands (the url always comes first):
|
|
|
65
74
|
ask <prompt> --entry-points '<JSON array>' [--default <id>] [--optional] [--step <step-id>] [--wait]
|
|
66
75
|
Ask where users should access the assistant. Each of 1–3 options has id, label, description and entryPoint {formFactor: modal|sidebar|full-page, placement, trigger, recommended?}. Answers one option id.
|
|
67
76
|
wait start Block until the user clicks Begin plan in their browser; after a finished or cancelled setup, until the next one begins.
|
|
68
|
-
wait <input-id>
|
|
69
|
-
|
|
77
|
+
wait <input-id> [--timeout <seconds>]
|
|
78
|
+
Block until the user answers or dismisses an input. A multi-select answer prints as an array.
|
|
79
|
+
plan (<markdown> | --file <path>) [--wait] [--timeout <seconds>]
|
|
70
80
|
Propose the plan for the user to approve. --wait blocks until they decide.
|
|
71
|
-
wait plan
|
|
81
|
+
wait plan [--timeout <seconds>] Block until the user approves the plan or asks for changes.
|
|
82
|
+
wait <input-id>, wait plan and every --wait give up after ${DEFAULT_WAIT_SECONDS}s (--timeout, 0 for no limit) and print
|
|
83
|
+
{"ok":false,"status":"waiting","next":"<command>"}: run "next" again to keep waiting.
|
|
72
84
|
env <input-id> <ENV_VAR> [--file <path>]
|
|
73
85
|
Write the API key behind an answered llm-provider input to an env file (default .env.local) as ENV_VAR=<key>, without printing it.
|
|
74
86
|
add-step <title> [--detail <text>] [--product <slug>] [--active]
|
|
@@ -166,9 +178,9 @@ const streamSection = (
|
|
|
166
178
|
noOpen: boolean,
|
|
167
179
|
) =>
|
|
168
180
|
streaming
|
|
169
|
-
? `The command that printed this is the stream. Keep it running in the background until the setup ends: the browser shows you as connected only while it runs
|
|
170
|
-
- "browser.opened" ({id, message}): setup opened the setup page in the user's browser for a question. Tell the user once, in your own conversation, that it waits there.
|
|
171
|
-
- "input.needed" ({id, message, question, options?, answer}): the user may not see the browser right now. Do what its message says: wait for
|
|
181
|
+
? `The command that printed this is the stream. Keep it running in the background until the setup ends: the browser shows you as connected only while it runs. New output does not wake you: read its new lines between your other commands. Each new line is a JSON array of events: ${EVENT_LIST}, plus:
|
|
182
|
+
- "browser.opened" ({id, message}): setup opened the setup page in the user's browser for a question. Tell the user once, in your own conversation, that it waits there, and keep working in the same turn.
|
|
183
|
+
- "input.needed" ({id, message, question, options?, answer}): the user may not see the browser right now. Do what its message says: run "wait <id>" for their answer in the browser, or ask the user in your own conversation with that question and options, then run its "answer" command with their reply. Answered either way, the setup goes on.
|
|
172
184
|
If this command stops, start it again in the background with
|
|
173
185
|
${cmd} ${url} stream${noOpen ? " --no-open" : ""}
|
|
174
186
|
The URL is the user's link to you from their browser, not one setup: it outlives this one, and their next setup arrives on the same stream.`
|
|
@@ -176,6 +188,12 @@ The URL is the user's link to you from their browser, not one setup: it outlives
|
|
|
176
188
|
${cmd} ${url} stream
|
|
177
189
|
and keep it running: the browser shows you as connected only while it runs, and it is how answers reach you. The URL is the user's link to you from their browser, not one setup: it outlives this one, and their next setup arrives on the same stream. Each line is a JSON array of events: ${EVENT_LIST}.`;
|
|
178
190
|
|
|
191
|
+
/** Why the agent must not end its turn mid-setup, and how it waits instead. */
|
|
192
|
+
const keepGoing = (cmd: string, url: string) =>
|
|
193
|
+
`Run the setup to the end in one go. Do not count on the stream or the browser to wake you: if you end your turn before the install is done, the setup can stay paused until the user types to you, even after they answer in the browser. So never end your turn to wait for an answer, and do not stop after telling the user something. To get an answer, block on it:
|
|
194
|
+
${cmd} ${url} wait <input-id>
|
|
195
|
+
("plan --wait" and "ask --wait" block the same way). A wait that prints {"ok":false,"status":"waiting"} has not failed and the setup is not stuck: run its "next" command, right away. "next" only waits; never repeat an "ask" or "plan" to keep waiting, since that asks or submits again. End your turn only to ask the user in your own conversation (an "input.needed" you relay, or a value only they can give), and go on as soon as they reply; or once the install is done and proposed with "done".`;
|
|
196
|
+
|
|
179
197
|
export const agentInstructions = (
|
|
180
198
|
setupUrl: string,
|
|
181
199
|
options: AgentInstructionsOptions = {},
|
|
@@ -188,6 +206,8 @@ export const agentInstructions = (
|
|
|
188
206
|
|
|
189
207
|
${streamSection(cmd, url, streaming, started?.opened === false)}
|
|
190
208
|
|
|
209
|
+
${keepGoing(cmd, url)}
|
|
210
|
+
|
|
191
211
|
The user can message you at any time, including while a question is waiting. Read "message.created" events, acknowledge each with "${cmd} ${url} ack <message-id>", and reply using "log" with what you will do. Pending messages are replayed when the stream starts and listed in "status". A message is not an answer or plan approval; keep those decisions explicit.
|
|
192
212
|
|
|
193
213
|
"log" is a message to the user, and the user reads every one: send it only to reply to their message, to tell them something no question or step conveys (a blocker, a deviation from the plan, a decision you made for them), or once as a wrap-up when the install is finished. Findings go in the plan, progress in step notes, questions in "ask"; do not narrate.
|
|
@@ -205,10 +225,10 @@ The setup's products are starting names and guide references. If the setup lists
|
|
|
205
225
|
When the setup puts an assistant in the app, ask how users should access it after discovering the actual app and before proposing the plan. Skip this question when no product's guide installs an assistant (a standalone library or service setup, for example). Use an entry-point question with only 1–3 sensible alternatives. Describe the actual app's pages, workflow and component placement, not generic placeholder copy. Recommend one clear fit when there is one; two options may use the same form factor in different places. Each option needs a stable id, label, contextual description and entryPoint {"formFactor":"modal"|"sidebar"|"full-page","placement":"where it appears","trigger":"how users open it or navigate to it","recommended":true?}. Write the JSON array for this app and send it with:
|
|
206
226
|
${cmd} ${url} ask "Where should users access the assistant?" --entry-points '<JSON array>' --wait
|
|
207
227
|
Mark a clear fit with "recommended":true rather than --default, so the user makes the pick. When you ask it, do not submit the plan before the user picks an entry point. Respect the selected placement and trigger in the plan and implementation. Do not ask for a new-versus-existing app setting; discover the app from its files and ask only for an install path when needed.
|
|
208
|
-
Skip a preset the code already answers (a chat route on the AI SDK, a provider key in .env.local) and log why. Ask everything at once, keep investigating, and
|
|
228
|
+
Skip a preset the code already answers (a chat route on the AI SDK, a provider key in .env.local) and log why. Ask everything at once, keep investigating, and then run "wait <input-id>" for each open question before you write the plan; do not end your turn while they are open. A framework answer is "<framework>:<language>", for example "ai-sdk:typescript". A model answer is JSON {"provider","model","reasoningEffort"?}. An answer may carry a "note" from the user; follow it. A choice answer that matches none of the options is the user's own text.
|
|
209
229
|
Then write the plan as markdown and submit it:
|
|
210
230
|
${cmd} ${url} plan --file <path> --wait
|
|
211
|
-
Use these headings, each with bullets: "## What I found" (facts as \`- **Label:** value\`: app framework, package manager, agent framework, model provider and model, where components live), "## What I will install" (packages and files, one per bullet), "## Steps" (a numbered list, one line each), and "## Open questions" only if any remain. Keep it under 30 lines; no preamble. "plan --wait" prints {"decision":"approve"} or {"decision":"revise","feedback":"..."}. On revise, address the feedback (ask again if you need to) and submit a new plan; repeat until approved. Do not change the project before approval: the setup refuses steps until then.
|
|
231
|
+
Use these headings, each with bullets: "## What I found" (facts as \`- **Label:** value\`: app framework, package manager, agent framework, model provider and model, where components live), "## What I will install" (packages and files, one per bullet), "## Steps" (a numbered list, one line each), and "## Open questions" only if any remain. Keep it under 30 lines; no preamble. "plan --wait" prints {"decision":"approve"} or {"decision":"revise","feedback":"..."}; when it prints {"ok":false,"status":"waiting"}, the plan is submitted and still with the user, so keep waiting with "wait plan" and do not submit it again. On revise, address the feedback (ask again if you need to) and submit a new plan; repeat until approved. Do not change the project before approval: the setup refuses steps until then.
|
|
212
232
|
|
|
213
233
|
2. Install. Declare the steps from your plan up front, in order:
|
|
214
234
|
${cmd} ${url} add-step "<title>" --detail "<one line>"
|
|
@@ -416,6 +436,47 @@ const waitForPlan = (client: CheckoutClient, revision: number) =>
|
|
|
416
436
|
check();
|
|
417
437
|
});
|
|
418
438
|
|
|
439
|
+
/** The seconds `--timeout` asks a wait to block for; 0 is no limit. */
|
|
440
|
+
const waitSeconds = (flags: Parsed["flags"], fallback: number) => {
|
|
441
|
+
const value = flagText(flags, "timeout");
|
|
442
|
+
if (value === undefined) return fallback;
|
|
443
|
+
const seconds = Number(value);
|
|
444
|
+
if (!/^\d+(?:\.\d+)?$/.test(value) || !Number.isFinite(seconds)) {
|
|
445
|
+
return fail("--timeout takes a number of seconds, 0 for no limit");
|
|
446
|
+
}
|
|
447
|
+
return seconds;
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
const TIMED_OUT = Symbol("timed out");
|
|
451
|
+
|
|
452
|
+
/** `promise`, or `TIMED_OUT` once `seconds` pass first; 0 waits as long as it takes. */
|
|
453
|
+
const within = async <T>(
|
|
454
|
+
promise: Promise<T>,
|
|
455
|
+
seconds: number,
|
|
456
|
+
): Promise<T | typeof TIMED_OUT> => {
|
|
457
|
+
if (seconds === 0) return promise;
|
|
458
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
459
|
+
try {
|
|
460
|
+
return await Promise.race([
|
|
461
|
+
promise,
|
|
462
|
+
new Promise<typeof TIMED_OUT>((resolve) => {
|
|
463
|
+
timer = setTimeout(() => resolve(TIMED_OUT), seconds * 1000);
|
|
464
|
+
}),
|
|
465
|
+
]);
|
|
466
|
+
} finally {
|
|
467
|
+
clearTimeout(timer);
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
/** What a wait prints when the user has not decided yet: the command to run again, now. */
|
|
472
|
+
const stillWaiting = (target: string, next: string) => ({
|
|
473
|
+
ok: false,
|
|
474
|
+
status: "waiting",
|
|
475
|
+
waitingFor: target,
|
|
476
|
+
next,
|
|
477
|
+
message: `The user has not answered yet. Run "next" now to keep waiting (it only waits; do not repeat the command you ran, which would ask or submit again), or go on with other work and run it when you need the answer. Do not end your turn: nothing resumes you until you run it.`,
|
|
478
|
+
});
|
|
479
|
+
|
|
419
480
|
const decided = (plan: Checkout.Plan | undefined, state: Checkout.State) => {
|
|
420
481
|
if (state.status === "cancelled") return { ok: false, status: "cancelled" };
|
|
421
482
|
if (plan === undefined) return fail("the plan disappeared");
|
|
@@ -840,6 +901,8 @@ export const runSetupCommand = async (
|
|
|
840
901
|
fail(error instanceof Error ? error.message : String(error)),
|
|
841
902
|
);
|
|
842
903
|
const commands = client.commands;
|
|
904
|
+
const waitCommand = (target: string) =>
|
|
905
|
+
`${options.command ?? DEFAULT_COMMAND} ${shellWord(url)} wait ${target}`;
|
|
843
906
|
|
|
844
907
|
try {
|
|
845
908
|
switch (command) {
|
|
@@ -921,8 +984,15 @@ export const runSetupCommand = async (
|
|
|
921
984
|
print({ ok: true, revision });
|
|
922
985
|
break;
|
|
923
986
|
}
|
|
924
|
-
const plan = await
|
|
925
|
-
|
|
987
|
+
const plan = await within(
|
|
988
|
+
waitForPlan(client, revision),
|
|
989
|
+
waitSeconds(flags, DEFAULT_WAIT_SECONDS),
|
|
990
|
+
);
|
|
991
|
+
print(
|
|
992
|
+
plan === TIMED_OUT
|
|
993
|
+
? stillWaiting("plan", waitCommand("plan"))
|
|
994
|
+
: decided(plan, requireState(client)),
|
|
995
|
+
);
|
|
926
996
|
break;
|
|
927
997
|
}
|
|
928
998
|
case "ask": {
|
|
@@ -956,7 +1026,15 @@ export const runSetupCommand = async (
|
|
|
956
1026
|
print({ ok: true, inputId });
|
|
957
1027
|
break;
|
|
958
1028
|
}
|
|
959
|
-
|
|
1029
|
+
const input = await within(
|
|
1030
|
+
waitForInput(client, inputId),
|
|
1031
|
+
waitSeconds(flags, DEFAULT_WAIT_SECONDS),
|
|
1032
|
+
);
|
|
1033
|
+
print(
|
|
1034
|
+
input === TIMED_OUT
|
|
1035
|
+
? stillWaiting(inputId, waitCommand(inputId))
|
|
1036
|
+
: answered(input),
|
|
1037
|
+
);
|
|
960
1038
|
break;
|
|
961
1039
|
}
|
|
962
1040
|
case "wait": {
|
|
@@ -964,7 +1042,15 @@ export const runSetupCommand = async (
|
|
|
964
1042
|
rest[0] ?? fail("usage: wait start | wait <input-id> | wait plan");
|
|
965
1043
|
const state = requireState(client);
|
|
966
1044
|
if (target === "start") {
|
|
967
|
-
|
|
1045
|
+
// Waiting for the user's next setup is idle time, so it has no limit by default.
|
|
1046
|
+
const status = await within(
|
|
1047
|
+
waitForStart(client),
|
|
1048
|
+
waitSeconds(flags, 0),
|
|
1049
|
+
);
|
|
1050
|
+
if (status === TIMED_OUT) {
|
|
1051
|
+
print(stillWaiting("start", waitCommand("start")));
|
|
1052
|
+
break;
|
|
1053
|
+
}
|
|
968
1054
|
print({
|
|
969
1055
|
ok: status === "planning" || status === "installing",
|
|
970
1056
|
status,
|
|
@@ -973,18 +1059,29 @@ export const runSetupCommand = async (
|
|
|
973
1059
|
}
|
|
974
1060
|
if (target === "plan") {
|
|
975
1061
|
const plan = currentPlan(state) ?? fail("no plan has been proposed");
|
|
1062
|
+
const decision = await within(
|
|
1063
|
+
waitForPlan(client, plan.revision),
|
|
1064
|
+
waitSeconds(flags, DEFAULT_WAIT_SECONDS),
|
|
1065
|
+
);
|
|
976
1066
|
print(
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
requireState(client),
|
|
980
|
-
),
|
|
1067
|
+
decision === TIMED_OUT
|
|
1068
|
+
? stillWaiting("plan", waitCommand("plan"))
|
|
1069
|
+
: decided(decision, requireState(client)),
|
|
981
1070
|
);
|
|
982
1071
|
break;
|
|
983
1072
|
}
|
|
984
1073
|
if (!state.inputs.some((input) => input.id === target)) {
|
|
985
1074
|
fail(`no input "${target}"`);
|
|
986
1075
|
}
|
|
987
|
-
|
|
1076
|
+
const input = await within(
|
|
1077
|
+
waitForInput(client, target),
|
|
1078
|
+
waitSeconds(flags, DEFAULT_WAIT_SECONDS),
|
|
1079
|
+
);
|
|
1080
|
+
print(
|
|
1081
|
+
input === TIMED_OUT
|
|
1082
|
+
? stillWaiting(target, waitCommand(target))
|
|
1083
|
+
: answered(input),
|
|
1084
|
+
);
|
|
988
1085
|
break;
|
|
989
1086
|
}
|
|
990
1087
|
case "env": {
|
package/src/presets.ts
CHANGED
|
@@ -134,7 +134,7 @@ export const INPUT_PRESETS: Record<PresetId, Preset> = {
|
|
|
134
134
|
help: {
|
|
135
135
|
summary:
|
|
136
136
|
"Pick the app the assistant should live in. For a new project, Next.js is the shortest path: the assistant-ui CLI scaffolds the chat route with it. Pick Vite, React Router or TanStack Start when the rest of your stack already uses them, and Expo for a native app.",
|
|
137
|
-
href: `${DOCS}/docs/installation`,
|
|
137
|
+
href: `${DOCS}/docs/manual-installation`,
|
|
138
138
|
},
|
|
139
139
|
},
|
|
140
140
|
};
|
package/src/sdk.test.ts
CHANGED
|
@@ -67,6 +67,8 @@ describe("setup-agent/sdk", () => {
|
|
|
67
67
|
`npx my-tool setup https://checkout.test/${session.linkId} stream`,
|
|
68
68
|
);
|
|
69
69
|
expect(out).toContain("run_in_background");
|
|
70
|
+
expect(out).toContain("never end your turn to wait for it");
|
|
71
|
+
expect(out).not.toContain("Read its new output as it arrives");
|
|
70
72
|
expect(out).toContain('"input.needed"');
|
|
71
73
|
expect(out).toContain('"browser.opened"');
|
|
72
74
|
expect(out).not.toContain("start a persistent Monitor");
|
package/src/sdk.ts
CHANGED
|
@@ -68,7 +68,7 @@ const setupBriefing = (
|
|
|
68
68
|
|
|
69
69
|
1. Keep it running in the background until the setup ends. If you started it in the foreground, stop it and resume it in the background (Claude Code: Bash with run_in_background; Codex and other agents: add > "\${TMPDIR:-/tmp}/setup-${session.linkId}.log" 2>&1 & and read that file):
|
|
70
70
|
${command} ${url} stream${canOpen ? "" : " --no-open"}
|
|
71
|
-
2.
|
|
71
|
+
2. Its new output does not wake you, so never end your turn to wait for it. Read the new lines between your other commands; each line after this briefing is a JSON array of events. Answers reach you through the blocking "wait" commands below.
|
|
72
72
|
3. Run every other command in another shell: ${command} ${url} <command>.
|
|
73
73
|
4. ${
|
|
74
74
|
canOpen
|
package/src/stream.test.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { StreamEvent } from "./stream";
|
|
|
7
7
|
|
|
8
8
|
const PAGE = "https://setup.test/#link=abc&setup=s1";
|
|
9
9
|
const ANSWER = "npx assistant-ui@latest setup https://checkout.test/abc answer";
|
|
10
|
+
const WAIT = "npx assistant-ui@latest setup https://checkout.test/abc wait";
|
|
10
11
|
|
|
11
12
|
/** A client whose state the test changes, notifying like a statewire client. */
|
|
12
13
|
const fakeClient = () => {
|
|
@@ -129,8 +130,7 @@ describe("followSetup", () => {
|
|
|
129
130
|
event: "browser.opened",
|
|
130
131
|
id: "q1",
|
|
131
132
|
url: PAGE,
|
|
132
|
-
message:
|
|
133
|
-
'Opened the setup in the user\'s browser for "Which package manager?". Tell the user once that it is waiting there, and give them https://setup.test/#link=abc&setup=s1 in case it did not open. The answer arrives on this stream.',
|
|
133
|
+
message: `Opened the setup in the user's browser for "Which package manager?". Tell the user once that it is waiting there, and give them https://setup.test/#link=abc&setup=s1 in case it did not open. Then keep working in the same turn; when you need the answer, block on it with "${WAIT} q1". Do not end your turn to wait: nothing on this stream wakes you.`,
|
|
134
134
|
},
|
|
135
135
|
]);
|
|
136
136
|
|
|
@@ -160,8 +160,7 @@ describe("followSetup", () => {
|
|
|
160
160
|
event: "input.needed",
|
|
161
161
|
id: "q3",
|
|
162
162
|
reason: "recently-opened",
|
|
163
|
-
message:
|
|
164
|
-
"A browser tab for setup was opened 10s ago. Wait for the user there, or ask the user directly in the terminal: Which registry? Options: pnpm (pnpm), npm (npm).",
|
|
163
|
+
message: `A browser tab for setup was opened 10s ago. Wait for the user there with "${WAIT} q3", or ask the user directly in the terminal: Which registry? Options: pnpm (pnpm), npm (npm).`,
|
|
165
164
|
question: "Which registry?",
|
|
166
165
|
options: [
|
|
167
166
|
{ id: "pnpm", label: "pnpm" },
|
package/src/stream.ts
CHANGED
|
@@ -167,29 +167,44 @@ export const shellWord = (value: string) =>
|
|
|
167
167
|
|
|
168
168
|
const ASK_MESSAGES: Record<
|
|
169
169
|
AskReason,
|
|
170
|
-
(
|
|
170
|
+
(
|
|
171
|
+
question: string,
|
|
172
|
+
context: { agoSeconds: number; page: string; wait: string },
|
|
173
|
+
) => string
|
|
171
174
|
> = {
|
|
172
|
-
"recently-opened": (question, { agoSeconds }) =>
|
|
173
|
-
`A browser tab for setup was opened ${agoSeconds}s ago. Wait for the user there, or ask the user directly in the terminal: ${question}`,
|
|
175
|
+
"recently-opened": (question, { agoSeconds, wait }) =>
|
|
176
|
+
`A browser tab for setup was opened ${agoSeconds}s ago. Wait for the user there${wait}, or ask the user directly in the terminal: ${question}`,
|
|
174
177
|
"browser-closed": (question) =>
|
|
175
178
|
`The user closed the setup tab. Ask the user directly in the terminal: ${question}`,
|
|
176
179
|
"no-browser": (question, { page }) =>
|
|
177
180
|
`No browser tab is open for this setup. Ask the user directly in the terminal, or give them ${page}: ${question}`,
|
|
178
181
|
};
|
|
179
182
|
|
|
183
|
+
/** The command that blocks on a need, beside the one that answers it: `<cmd> <url> wait`. */
|
|
184
|
+
const waitCommandFor = (answerCommand: string) =>
|
|
185
|
+
answerCommand.endsWith(" answer")
|
|
186
|
+
? `${answerCommand.slice(0, -" answer".length)} wait`
|
|
187
|
+
: undefined;
|
|
188
|
+
|
|
180
189
|
/** What the agent reads when setup opened the browser, or when it should ask the user itself. */
|
|
181
190
|
const gateEvent = (
|
|
182
191
|
action: BrowserGateAction,
|
|
183
192
|
answerCommand: string,
|
|
184
193
|
page: string,
|
|
194
|
+
waitCommand?: string,
|
|
185
195
|
): StreamEvent => {
|
|
186
196
|
const { need } = action;
|
|
197
|
+
// Closing the setup is the user's call after "done"; there is nothing to block on.
|
|
198
|
+
const wait =
|
|
199
|
+
waitCommand !== undefined && need.kind !== "finish"
|
|
200
|
+
? ` with "${waitCommand} ${need.id}"`
|
|
201
|
+
: "";
|
|
187
202
|
if (action.type === "open")
|
|
188
203
|
return {
|
|
189
204
|
event: "browser.opened",
|
|
190
205
|
id: need.id,
|
|
191
206
|
url: action.url,
|
|
192
|
-
message: `Opened the setup in the user's browser for ${need.kind === "input" ? `"${need.input.prompt.trim() || "Your agent needs an answer."}"` : need.kind === "plan" ? "the plan review" : "closing the setup"}. Tell the user once that it is waiting there, and give them ${action.url} in case it did not open. The answer arrives on this stream
|
|
207
|
+
message: `Opened the setup in the user's browser for ${need.kind === "input" ? `"${need.input.prompt.trim() || "Your agent needs an answer."}"` : need.kind === "plan" ? "the plan review" : "closing the setup"}. Tell the user once that it is waiting there, and give them ${action.url} in case it did not open.${need.kind === "finish" ? " The answer arrives on this stream." : ` Then keep working in the same turn; when you need the answer, block on it${wait || ' with "wait"'}. Do not end your turn to wait: nothing on this stream wakes you.`}`,
|
|
193
208
|
};
|
|
194
209
|
const question = terminalQuestion(need);
|
|
195
210
|
return {
|
|
@@ -199,6 +214,7 @@ const gateEvent = (
|
|
|
199
214
|
message: ASK_MESSAGES[action.reason](formatTerminalQuestion(question), {
|
|
200
215
|
agoSeconds: Math.round((action.openedAgoMs ?? 0) / 1000),
|
|
201
216
|
page,
|
|
217
|
+
wait,
|
|
202
218
|
}),
|
|
203
219
|
...question,
|
|
204
220
|
answer: `${answerCommand} ${need.id} <${question.answerFormat}>`,
|
|
@@ -263,7 +279,12 @@ export const followSetup = async (
|
|
|
263
279
|
for (const action of gate?.update(state) ?? []) {
|
|
264
280
|
if (action.type === "open") (browser!.open ?? openUrl)(action.url);
|
|
265
281
|
pending.push(
|
|
266
|
-
gateEvent(
|
|
282
|
+
gateEvent(
|
|
283
|
+
action,
|
|
284
|
+
browser!.answerCommand,
|
|
285
|
+
page(state) ?? "",
|
|
286
|
+
waitCommandFor(browser!.answerCommand),
|
|
287
|
+
),
|
|
267
288
|
);
|
|
268
289
|
}
|
|
269
290
|
};
|