@hackerrank/astra-cli 0.1.23 → 0.1.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/model.js +178 -11
- package/src/prompts.js +24 -0
- package/src/repl.js +198 -0
package/README.md
CHANGED
|
@@ -80,7 +80,7 @@ astra -m claude-sonnet-5 # start chatting
|
|
|
80
80
|
astra -m claude-sonnet-5 -y # auto-run commands (no prompts)
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
-
In-REPL commands: `/help /exit /clear /history /tokens /yolo`.
|
|
83
|
+
In-REPL commands: `/help /plan <task> /exit /clear /history /tokens /yolo`.
|
|
84
84
|
|
|
85
85
|
### Autonomous task run
|
|
86
86
|
|
package/package.json
CHANGED
package/src/model.js
CHANGED
|
@@ -42,7 +42,7 @@ export class GatewayModel {
|
|
|
42
42
|
* @param {number} [opts.maxRetries]
|
|
43
43
|
* @param {(info:object)=>void} [opts.onRetry] called before each retry sleep
|
|
44
44
|
*/
|
|
45
|
-
constructor({ model, baseUrl, apiKey, modelKwargs = {}, maxRetries =
|
|
45
|
+
constructor({ model, baseUrl, apiKey, modelKwargs = {}, maxRetries = 8, maxTokens = 8192, requestTimeoutMs = 0, onRetry } = {}) {
|
|
46
46
|
if (!model) throw new Error("GatewayModel: `model` is required");
|
|
47
47
|
this.model = model;
|
|
48
48
|
this.maxTokens = maxTokens;
|
|
@@ -124,14 +124,15 @@ export class GatewayModel {
|
|
|
124
124
|
// Retry on transient server / rate-limit errors with exponential
|
|
125
125
|
// backoff (honoring Retry-After when the server provides it).
|
|
126
126
|
if ((res.status === 429 || res.status >= 500) && attempt < this.maxRetries) {
|
|
127
|
-
const
|
|
127
|
+
const serverWait = retryAfterMs(res.headers, text);
|
|
128
|
+
const wait = serverWait != null ? Math.max(serverWait + 250, 1000) : backoffMs(attempt);
|
|
128
129
|
this.nRetries++;
|
|
129
130
|
this.onRetry({
|
|
130
131
|
attempt: attempt + 1,
|
|
131
132
|
maxRetries: this.maxRetries,
|
|
132
133
|
status: res.status,
|
|
133
134
|
waitMs: wait,
|
|
134
|
-
reason: `HTTP ${res.status}`,
|
|
135
|
+
reason: res.status === 429 ? "HTTP 429 (rate limited)" : `HTTP ${res.status}`,
|
|
135
136
|
});
|
|
136
137
|
await sleep(wait);
|
|
137
138
|
continue;
|
|
@@ -282,14 +283,180 @@ function hintForStatus(status) {
|
|
|
282
283
|
}
|
|
283
284
|
}
|
|
284
285
|
|
|
285
|
-
/**
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
286
|
+
/**
|
|
287
|
+
* Parse a duration string, number, or date into milliseconds.
|
|
288
|
+
* Supports numbers (seconds or timestamps), units (ms, s, m, h, d),
|
|
289
|
+
* composite durations ("1m 30s"), and HTTP/ISO dates.
|
|
290
|
+
*/
|
|
291
|
+
export function parseDurationMs(val) {
|
|
292
|
+
if (typeof val === "number" && Number.isFinite(val) && val >= 0) {
|
|
293
|
+
if (val > 1e11) return Math.max(0, val - Date.now());
|
|
294
|
+
if (val > 1e9) return Math.max(0, val * 1000 - Date.now());
|
|
295
|
+
return Math.max(0, Math.round(val * 1000));
|
|
296
|
+
}
|
|
297
|
+
if (!val || typeof val !== "string") return null;
|
|
298
|
+
const s = val.trim();
|
|
299
|
+
|
|
300
|
+
const rawNum = Number(s);
|
|
301
|
+
if (Number.isFinite(rawNum) && rawNum >= 0) {
|
|
302
|
+
if (rawNum > 1e11) return Math.max(0, rawNum - Date.now());
|
|
303
|
+
if (rawNum > 1e9) return Math.max(0, rawNum * 1000 - Date.now());
|
|
304
|
+
return Math.max(0, Math.round(rawNum * 1000));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const when = Date.parse(s);
|
|
308
|
+
if (Number.isFinite(when) && when > Date.now()) {
|
|
309
|
+
return Math.max(0, when - Date.now());
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const pattern = /([0-9]+(?:\.[0-9]+)?)\s*(milliseconds?|millis?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d)\b/gi;
|
|
313
|
+
let match;
|
|
314
|
+
let totalMs = 0;
|
|
315
|
+
let matched = false;
|
|
316
|
+
while ((match = pattern.exec(s)) !== null) {
|
|
317
|
+
matched = true;
|
|
318
|
+
const n = parseFloat(match[1]);
|
|
319
|
+
const unit = match[2].toLowerCase();
|
|
320
|
+
if (!Number.isFinite(n)) continue;
|
|
321
|
+
if (unit.startsWith("ms") || unit.startsWith("milli")) {
|
|
322
|
+
totalMs += n;
|
|
323
|
+
} else if (unit.startsWith("s")) {
|
|
324
|
+
totalMs += n * 1000;
|
|
325
|
+
} else if (unit.startsWith("m")) {
|
|
326
|
+
totalMs += n * 60 * 1000;
|
|
327
|
+
} else if (unit.startsWith("h")) {
|
|
328
|
+
totalMs += n * 3600 * 1000;
|
|
329
|
+
} else if (unit.startsWith("d")) {
|
|
330
|
+
totalMs += n * 86400 * 1000;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return matched ? Math.round(totalMs) : null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Extract retry duration from unstructured error message text.
|
|
338
|
+
* Matches phrases like "retry after 12.5s", "try again in 5 seconds", "resets in 30s", "wait 10s".
|
|
339
|
+
*/
|
|
340
|
+
export function extractDurationFromText(text) {
|
|
341
|
+
if (!text || typeof text !== "string") return null;
|
|
342
|
+
|
|
343
|
+
const p1 = /(?:retry(?:ing)?|try\s+again|wait(?:ing)?|resets?|available|back\s*off)\s+(?:again\s+)?(?:after|in|for)\s+([0-9]+(?:\.[0-9]+)?\s*(?:milliseconds?|millis?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d)\b(?:\s+[0-9]+(?:\.[0-9]+)?\s*(?:milliseconds?|millis?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d)\b)*|[0-9]+(?:\.[0-9]+)?)/i;
|
|
344
|
+
const m1 = text.match(p1);
|
|
345
|
+
if (m1) {
|
|
346
|
+
const parsed = parseDurationMs(m1[1]);
|
|
347
|
+
if (parsed != null) return parsed;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const p2 = /(?:wait(?:ing)?)\s+([0-9]+(?:\.[0-9]+)?\s*(?:milliseconds?|millis?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d)\b)/i;
|
|
351
|
+
const m2 = text.match(p2);
|
|
352
|
+
if (m2) {
|
|
353
|
+
const parsed = parseDurationMs(m2[1]);
|
|
354
|
+
if (parsed != null) return parsed;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const p3 = /in\s+([0-9]+(?:\.[0-9]+)?\s*(?:milliseconds?|millis?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d)\b)\s*[,.]?\s*(?:please\s+)?(?:retry|try)/i;
|
|
358
|
+
const m3 = text.match(p3);
|
|
359
|
+
if (m3) {
|
|
360
|
+
const parsed = parseDurationMs(m3[1]);
|
|
361
|
+
if (parsed != null) return parsed;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Parse retry delay from response headers or error response body into milliseconds.
|
|
369
|
+
* Checks standard and provider-specific rate-limit headers as well as JSON fields
|
|
370
|
+
* and error messages returned by AI gateways (HackerRank, OpenAI, Anthropic, LiteLLM, Gemini, etc.).
|
|
371
|
+
*/
|
|
372
|
+
export function retryAfterMs(headers, bodyText) {
|
|
373
|
+
const msHeader = headers?.get?.("retry-after-ms");
|
|
374
|
+
if (msHeader) {
|
|
375
|
+
const ms = Number(msHeader);
|
|
376
|
+
if (Number.isFinite(ms) && ms >= 0) return Math.round(ms);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const headerKeys = [
|
|
380
|
+
"retry-after",
|
|
381
|
+
"x-retry-after",
|
|
382
|
+
"x-ratelimit-reset-requests",
|
|
383
|
+
"x-ratelimit-reset-tokens",
|
|
384
|
+
"x-ratelimit-reset",
|
|
385
|
+
];
|
|
386
|
+
for (const k of headerKeys) {
|
|
387
|
+
const val = headers?.get?.(k);
|
|
388
|
+
if (val) {
|
|
389
|
+
const parsed = parseDurationMs(val);
|
|
390
|
+
if (parsed != null) return parsed;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (bodyText) {
|
|
395
|
+
let json = null;
|
|
396
|
+
if (typeof bodyText === "object") {
|
|
397
|
+
json = bodyText;
|
|
398
|
+
} else if (typeof bodyText === "string") {
|
|
399
|
+
try {
|
|
400
|
+
json = JSON.parse(bodyText);
|
|
401
|
+
} catch {}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (json && typeof json === "object") {
|
|
405
|
+
const msCandidates = [json.retry_after_ms, json.error?.retry_after_ms];
|
|
406
|
+
for (const c of msCandidates) {
|
|
407
|
+
if (Number.isFinite(Number(c)) && Number(c) >= 0) {
|
|
408
|
+
return Math.round(Number(c));
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const durationCandidates = [
|
|
413
|
+
json.retry_after,
|
|
414
|
+
json.retryAfter,
|
|
415
|
+
json.retry_after_seconds,
|
|
416
|
+
json.retry_after_sec,
|
|
417
|
+
json.reset_in,
|
|
418
|
+
json.reset_after,
|
|
419
|
+
json.reset_at,
|
|
420
|
+
json.wait_seconds,
|
|
421
|
+
json.wait_time,
|
|
422
|
+
json.error?.retry_after,
|
|
423
|
+
json.error?.retryAfter,
|
|
424
|
+
json.error?.retry_after_seconds,
|
|
425
|
+
json.error?.retry_after_sec,
|
|
426
|
+
json.error?.reset_in,
|
|
427
|
+
json.error?.reset_after,
|
|
428
|
+
json.error?.reset_at,
|
|
429
|
+
json.error?.wait_seconds,
|
|
430
|
+
json.error?.wait_time,
|
|
431
|
+
];
|
|
432
|
+
for (const c of durationCandidates) {
|
|
433
|
+
if (c != null) {
|
|
434
|
+
const parsed = parseDurationMs(c);
|
|
435
|
+
if (parsed != null) return parsed;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const msgCandidates = [
|
|
440
|
+
json.error?.message,
|
|
441
|
+
typeof json.error === "string" ? json.error : null,
|
|
442
|
+
json.message,
|
|
443
|
+
json.detail,
|
|
444
|
+
typeof json.details === "string" ? json.details : null,
|
|
445
|
+
];
|
|
446
|
+
for (const msg of msgCandidates) {
|
|
447
|
+
if (msg) {
|
|
448
|
+
const extracted = extractDurationFromText(msg);
|
|
449
|
+
if (extracted != null) return extracted;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (typeof bodyText === "string") {
|
|
455
|
+
const extracted = extractDurationFromText(bodyText);
|
|
456
|
+
if (extracted != null) return extracted;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
293
460
|
return null;
|
|
294
461
|
}
|
|
295
462
|
|
package/src/prompts.js
CHANGED
|
@@ -102,3 +102,27 @@ export function render(template, vars) {
|
|
|
102
102
|
key in vars && vars[key] != null ? String(vars[key]) : ""
|
|
103
103
|
);
|
|
104
104
|
}
|
|
105
|
+
|
|
106
|
+
export const PLAN_PROMPT_TEMPLATE = `Please explore the workspace and create a clear, actionable plan for this task:
|
|
107
|
+
|
|
108
|
+
Task:
|
|
109
|
+
{{task}}
|
|
110
|
+
|
|
111
|
+
Instructions for planning:
|
|
112
|
+
1. Run read-only commands (e.g. ls, find, grep, cat) to explore and understand the relevant files.
|
|
113
|
+
2. Present a structured plan with:
|
|
114
|
+
- **Objective & Scope**: Summary of what needs to be done.
|
|
115
|
+
- **Files to Modify / Create**: List of target files.
|
|
116
|
+
- **Self-Testing & Verification Strategy**: Exact test commands or verification scripts to write/run.
|
|
117
|
+
- **Self-Review Checklist**: Code quality, edge cases, and cleanliness checks.
|
|
118
|
+
3. Finish with a chat response presenting the complete plan so the user can review it before execution.`;
|
|
119
|
+
|
|
120
|
+
export const AUTONOMOUS_EXECUTION_PROMPT = `The plan has been approved. Execute the plan end-to-end now in autonomous mode without asking further questions.
|
|
121
|
+
|
|
122
|
+
Workflow requirements:
|
|
123
|
+
1. Implement the changes with clean, focused edits.
|
|
124
|
+
2. Self-verify by writing or running test scripts to prove the solution works.
|
|
125
|
+
3. Review your changes with \`git diff\` (or inspect modified files) to ensure no regressions, unintended files, or debug prints remain.
|
|
126
|
+
4. If any tests or checks fail, diagnose and fix the errors.
|
|
127
|
+
5. When completely finished, verified, and reviewed, run:
|
|
128
|
+
\`echo COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT\``;
|
package/src/repl.js
CHANGED
|
@@ -17,6 +17,7 @@ import readline from "node:readline";
|
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
18
|
import { ask, saveAgentPrefs } from "./config.js";
|
|
19
19
|
import { AVAILABLE_MODELS, REASONING_LEVELS } from "./models.js";
|
|
20
|
+
import { PLAN_PROMPT_TEMPLATE, AUTONOMOUS_EXECUTION_PROMPT, render } from "./prompts.js";
|
|
20
21
|
|
|
21
22
|
const C = {
|
|
22
23
|
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
@@ -517,6 +518,7 @@ Interactive commands:
|
|
|
517
518
|
/help show this help
|
|
518
519
|
/exit, /quit leave (session is saved)
|
|
519
520
|
/clear start a fresh conversation (new context)
|
|
521
|
+
/plan <task> plan a task, review it, then execute autonomously with self-review
|
|
520
522
|
/history print the message history
|
|
521
523
|
/tokens show token usage so far
|
|
522
524
|
/yolo toggle auto-run of commands (no confirmation)
|
|
@@ -643,6 +645,25 @@ async function runStickyRepl(agent, model, yolo, intro = [], { runBench = null }
|
|
|
643
645
|
if (cmd === "history") { printHistory(agent, log); continue; }
|
|
644
646
|
if (cmd === "tokens") { log(tokensLine(model)); continue; }
|
|
645
647
|
if (cmd === "yolo") { yolo = !yolo; log(C.dim(`[astra] auto-run ${yolo ? "ON" : "OFF"}.`)); refresh(); continue; }
|
|
648
|
+
if (cmd === "plan") {
|
|
649
|
+
const taskText = line.slice(line.indexOf("plan") + 4).trim();
|
|
650
|
+
const prevYolo = yolo;
|
|
651
|
+
await handlePlanWorkflow({
|
|
652
|
+
taskText,
|
|
653
|
+
agent,
|
|
654
|
+
model,
|
|
655
|
+
log,
|
|
656
|
+
readInput: (promptText) => screen.readLine(promptText, { guardEnter: true, echo: true }),
|
|
657
|
+
startBusy: (label) => screen.startBusy(label),
|
|
658
|
+
stopBusy: () => screen.stopBusy(),
|
|
659
|
+
interrupt,
|
|
660
|
+
setYolo: (v) => { yolo = v; },
|
|
661
|
+
onProgress: () => refresh(),
|
|
662
|
+
});
|
|
663
|
+
yolo = prevYolo;
|
|
664
|
+
refresh();
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
646
667
|
log(C.dim(`[astra] unknown command: /${cmd} (try /help)`));
|
|
647
668
|
continue;
|
|
648
669
|
}
|
|
@@ -742,6 +763,25 @@ async function runPlainRepl(agent, model, yolo) {
|
|
|
742
763
|
if (cmd === "history") { printHistory(agent, log); continue; }
|
|
743
764
|
if (cmd === "tokens") { console.error(tokensLine(model)); continue; }
|
|
744
765
|
if (cmd === "yolo") { yolo = !yolo; console.error(C.dim(`[astra] auto-run ${yolo ? "ON" : "OFF"}.`)); continue; }
|
|
766
|
+
if (cmd === "plan") {
|
|
767
|
+
const taskText = line.slice(line.indexOf("plan") + 4).trim();
|
|
768
|
+
const prevYolo = yolo;
|
|
769
|
+
await handlePlanWorkflow({
|
|
770
|
+
taskText,
|
|
771
|
+
agent,
|
|
772
|
+
model,
|
|
773
|
+
log: (t) => console.error(t),
|
|
774
|
+
readInput: (promptText) => new Promise((res) => rl.question(promptText, res)),
|
|
775
|
+
startBusy: () => {},
|
|
776
|
+
stopBusy: () => {},
|
|
777
|
+
interrupt: { aborted: false, quit: false },
|
|
778
|
+
setYolo: (v) => { yolo = v; },
|
|
779
|
+
onProgress: () => printFooterInline(agent, model, yolo),
|
|
780
|
+
});
|
|
781
|
+
yolo = prevYolo;
|
|
782
|
+
printFooterInline(agent, model, yolo);
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
745
785
|
console.error(C.dim(`[astra] unknown command: /${cmd} (try /help)`));
|
|
746
786
|
continue;
|
|
747
787
|
}
|
|
@@ -859,3 +899,161 @@ function tokensLine(model) {
|
|
|
859
899
|
const dollars = usd < 0.01 ? "$" + usd.toFixed(5) : "$" + usd.toFixed(4);
|
|
860
900
|
return C.dim(`[astra] tokens: prompt=${pt} completion=${ct} total=${pt + ct} · cost=${dollars} (${src})`);
|
|
861
901
|
}
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Run the plan-and-execute workflow:
|
|
906
|
+
* 1. Prompt model to explore workspace and draft a structured plan.
|
|
907
|
+
* 2. Pause and prompt user for approval / feedback.
|
|
908
|
+
* 3. On approval, execute autonomously to completion with auto-review and auto-fix.
|
|
909
|
+
*/
|
|
910
|
+
export async function handlePlanWorkflow({
|
|
911
|
+
taskText,
|
|
912
|
+
agent,
|
|
913
|
+
model,
|
|
914
|
+
log,
|
|
915
|
+
readInput,
|
|
916
|
+
startBusy,
|
|
917
|
+
stopBusy,
|
|
918
|
+
interrupt,
|
|
919
|
+
setYolo = () => {},
|
|
920
|
+
onProgress = () => {},
|
|
921
|
+
}) {
|
|
922
|
+
let task = taskText;
|
|
923
|
+
if (!task) {
|
|
924
|
+
task = (await readInput(C.yellow("Enter task to plan: "))).trim();
|
|
925
|
+
}
|
|
926
|
+
if (!task) {
|
|
927
|
+
log(C.dim("[astra] plan cancelled (empty task)."));
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
log(C.cyan(`[astra] exploring workspace and drafting plan for: "${task}"...`));
|
|
932
|
+
agent.addUserMessage(render(PLAN_PROMPT_TEMPLATE, { task }));
|
|
933
|
+
|
|
934
|
+
model._abort = new AbortController();
|
|
935
|
+
model.signal = model._abort.signal;
|
|
936
|
+
startBusy("planning");
|
|
937
|
+
try {
|
|
938
|
+
await driveUntilChat(agent, log, interrupt);
|
|
939
|
+
} finally {
|
|
940
|
+
stopBusy();
|
|
941
|
+
model.signal = null;
|
|
942
|
+
model._abort = null;
|
|
943
|
+
}
|
|
944
|
+
if (interrupt?.quit || interrupt?.aborted) return;
|
|
945
|
+
|
|
946
|
+
// Review gate loop
|
|
947
|
+
let approved = false;
|
|
948
|
+
while (!approved) {
|
|
949
|
+
const ans = (await readInput(
|
|
950
|
+
C.yellow("\nApprove plan and begin autonomous execution? [Y/n/feedback] ")
|
|
951
|
+
)).trim();
|
|
952
|
+
const lower = ans.toLowerCase();
|
|
953
|
+
|
|
954
|
+
if (lower === "n" || lower === "no" || lower === "cancel" || lower === "abort") {
|
|
955
|
+
log(C.dim("[astra] plan cancelled. Returning to interactive mode."));
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
if (ans === "" || lower === "y" || lower === "yes") {
|
|
959
|
+
approved = true;
|
|
960
|
+
break;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// Feedback provided: refine plan
|
|
964
|
+
log(C.cyan(`[astra] updating plan with feedback: "${ans}"...`));
|
|
965
|
+
agent.addUserMessage(
|
|
966
|
+
`Here is feedback on the plan: "${ans}". Please refine the plan accordingly and present the updated plan.`
|
|
967
|
+
);
|
|
968
|
+
model._abort = new AbortController();
|
|
969
|
+
model.signal = model._abort.signal;
|
|
970
|
+
startBusy("planning");
|
|
971
|
+
try {
|
|
972
|
+
await driveUntilChat(agent, log, interrupt);
|
|
973
|
+
} finally {
|
|
974
|
+
stopBusy();
|
|
975
|
+
model.signal = null;
|
|
976
|
+
model._abort = null;
|
|
977
|
+
}
|
|
978
|
+
if (interrupt?.quit || interrupt?.aborted) return;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// Execute autonomously
|
|
982
|
+
log(C.green("\n[astra] plan approved! Starting unattended autonomous execution with auto-review & auto-fix..."));
|
|
983
|
+
const prevMode = agent.mode;
|
|
984
|
+
setYolo(true);
|
|
985
|
+
agent.mode = "autonomous";
|
|
986
|
+
agent.addUserMessage(AUTONOMOUS_EXECUTION_PROMPT);
|
|
987
|
+
|
|
988
|
+
model._abort = new AbortController();
|
|
989
|
+
model.signal = model._abort.signal;
|
|
990
|
+
startBusy("executing plan");
|
|
991
|
+
let status = "Completed";
|
|
992
|
+
try {
|
|
993
|
+
status = await driveAutonomous(agent, log, interrupt);
|
|
994
|
+
} catch (err) {
|
|
995
|
+
log(C.red(`[astra] error during autonomous execution: ${err?.message || err}`));
|
|
996
|
+
status = "Error";
|
|
997
|
+
} finally {
|
|
998
|
+
stopBusy();
|
|
999
|
+
model.signal = null;
|
|
1000
|
+
model._abort = null;
|
|
1001
|
+
agent.mode = prevMode;
|
|
1002
|
+
agent.exitStatus = null;
|
|
1003
|
+
onProgress();
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
log(C.green(`\n[astra] plan execution finished (${status}). You are now back in interactive mode.`));
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* Drive turns autonomously until the model submits with the completion sentinel.
|
|
1011
|
+
*/
|
|
1012
|
+
export async function driveAutonomous(agent, log, interrupt = null) {
|
|
1013
|
+
while (true) {
|
|
1014
|
+
if (interrupt && interrupt.aborted) {
|
|
1015
|
+
log(C.yellow("[astra] autonomous execution interrupted — returning to prompt."));
|
|
1016
|
+
return "Interrupted";
|
|
1017
|
+
}
|
|
1018
|
+
let turn;
|
|
1019
|
+
try {
|
|
1020
|
+
turn = await agent.runTurn();
|
|
1021
|
+
} catch (err) {
|
|
1022
|
+
if (err && (err.name === "GatewayError" || err.name === "ContextWindowError")) {
|
|
1023
|
+
log(C.red(`[astra] ${err.message}`));
|
|
1024
|
+
return err.name;
|
|
1025
|
+
}
|
|
1026
|
+
throw err;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
if (interrupt && interrupt.aborted) {
|
|
1030
|
+
log(C.yellow("[astra] autonomous execution interrupted — returning to prompt."));
|
|
1031
|
+
return "Interrupted";
|
|
1032
|
+
}
|
|
1033
|
+
if (turn.kind === "exit") {
|
|
1034
|
+
log(C.dim(`[astra] autonomous run ended: ${turn.exit_status}`));
|
|
1035
|
+
return turn.exit_status;
|
|
1036
|
+
}
|
|
1037
|
+
if (turn.kind === "command") {
|
|
1038
|
+
const rc = turn.returncode;
|
|
1039
|
+
const tag = rc === 0 ? C.dim(`[rc ${rc}]`) : C.red(`[rc ${rc}]`);
|
|
1040
|
+
log(tag + "\n" + indent(turn.output));
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
1043
|
+
if (turn.kind === "declined") {
|
|
1044
|
+
log(C.dim("[astra] command skipped."));
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
1047
|
+
if (turn.kind === "format_error") {
|
|
1048
|
+
log(C.dim(`[astra] (reprompting: ${turn.error})`));
|
|
1049
|
+
continue;
|
|
1050
|
+
}
|
|
1051
|
+
if (turn.kind === "chat") {
|
|
1052
|
+
log("\n" + C.cyan("astra › ") + turn.content.trim());
|
|
1053
|
+
agent.addUserMessage(
|
|
1054
|
+
"Continue executing the approved plan in autonomous mode. When all changes are implemented, tested, and self-reviewed, run:\necho COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT"
|
|
1055
|
+
);
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|