@a-t-h-i/bot-lobby 0.6.36 → 0.6.38
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 +32 -7
- package/package.json +1 -1
- package/prompts/master.md +32 -0
- package/prompts/worker.md +11 -0
- package/src/classifier/effort.ts +14 -2
- package/src/classifier/instance.ts +10 -2
- package/src/classifier/knowledge.ts +5 -2
- package/src/classifier/scripts.ts +76 -0
- package/src/execution/agent-runner.ts +75 -14
- package/src/execution/delegation.ts +25 -7
- package/src/execution/pi-runner.ts +9 -3
- package/src/knowledge/selector.ts +2 -0
- package/src/lobby/quickfix.ts +10 -2
- package/src/lobby/runtime.ts +2 -1
- package/src/lobby/service.ts +8 -1
- package/src/lobby/sessions.ts +1 -1
- package/src/master/master.ts +19 -6
- package/src/pi/activity.ts +2 -0
- package/src/pi/auto-compaction.ts +4 -4
- package/src/pi/events.ts +4 -1
- package/src/pi/plan-checklist.ts +4 -2
- package/src/pi/start-task.ts +15 -9
- package/src/pi/task-title.ts +46 -0
- package/src/pi/tools.ts +6 -4
- package/src/prompts/compiler.ts +4 -1
- package/src/roles/worker.ts +15 -1
- package/src/schemas/configuration.ts +5 -2
- package/src/schemas/findings.ts +14 -0
- package/src/scripts/library.ts +168 -0
- package/src/scripts/pick.ts +79 -0
- package/src/state/step-limits.ts +41 -0
- package/src/state/work-time.ts +16 -11
- package/src/webui/api/lobby.ts +1 -1
- package/src/webui/api/plan-facts.ts +3 -2
- package/src/webui/protocol.ts +1 -1
- package/src/workflow/workflow.ts +122 -40
- package/webui/dist/assets/index-BFsMFIr5.js +110 -0
- package/webui/dist/assets/index-Ccc6rsPD.css +2 -0
- package/webui/dist/build.json +99 -79
- package/webui/dist/index.html +2 -2
- package/webui/dist/assets/index-4Zm1DbOM.css +0 -2
- package/webui/dist/assets/index-CjhlNym4.js +0 -109
package/src/workflow/workflow.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, realpathSync } from "node:fs";
|
|
2
|
-
import { isAbsolute, join, relative } from "node:path";
|
|
3
|
-
import type
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { profileFor, type BotLobbyConfig, type ProfileResolver } from "../schemas/configuration.ts";
|
|
4
4
|
import type { AgentRun, Pushback, ResearchResult, ReviewResult, WorkerResult } from "../schemas/findings.ts";
|
|
5
5
|
import {
|
|
6
6
|
MAX_RUN_LOG,
|
|
@@ -24,6 +24,7 @@ import { ExcalidrawBook } from "../excalidraw/sessions.ts";
|
|
|
24
24
|
import { knowledgeDir, type KnowledgeAgent } from "../knowledge/paths.ts";
|
|
25
25
|
import { lobbyTopics } from "../lobby/topics.ts";
|
|
26
26
|
import { writeScratchpad } from "../state/persistence.ts";
|
|
27
|
+
import { saveScript } from "../scripts/library.ts";
|
|
27
28
|
import { spawnPiProcess, type ProcessRunner, type RelayAsk } from "../execution/pi-runner.ts";
|
|
28
29
|
import { previewDir } from "../ask/relay.ts";
|
|
29
30
|
import type { AskQuestion, AskResult } from "../ask/types.ts";
|
|
@@ -55,6 +56,8 @@ import { assertNoPendingApprovals, pendingApprovals, requestApproval, resolveApp
|
|
|
55
56
|
import { withBlockingRequest } from "../state/blocking-requests.ts";
|
|
56
57
|
import { pingApproval } from "../pi/notify.ts";
|
|
57
58
|
import { describeRun, runLogEntry } from "../pi/run-summary.ts";
|
|
59
|
+
import { planChecklist, planSteps, runSteps } from "../pi/plan-checklist.ts";
|
|
60
|
+
import { STEP_LIMIT_MS, grantStepTime, reserveStepTime, stepTimeLeft } from "../state/step-limits.ts";
|
|
58
61
|
import { noteWorkerRun } from "../state/work-time.ts";
|
|
59
62
|
import { nextStates } from "./transitions.ts";
|
|
60
63
|
import type { FileHinter } from "../classifier/files.ts";
|
|
@@ -88,6 +91,7 @@ export const ORCHESTRATE_ACTIONS = [
|
|
|
88
91
|
"qa",
|
|
89
92
|
"knowledge",
|
|
90
93
|
"compact",
|
|
94
|
+
"script",
|
|
91
95
|
"whiteboard",
|
|
92
96
|
"complete",
|
|
93
97
|
"block",
|
|
@@ -124,10 +128,14 @@ export interface OrchestrateParams {
|
|
|
124
128
|
minutes?: number;
|
|
125
129
|
/** knowledge: which persistent file the text belongs to. */
|
|
126
130
|
kind?: KnowledgeKind;
|
|
127
|
-
/** compact: the knowledge file being rewritten
|
|
131
|
+
/** compact: the knowledge file being rewritten; script: a drafted script to take instead of `text`. */
|
|
128
132
|
file?: string;
|
|
129
|
-
/** whiteboard: name for the new Excalidraw session. */
|
|
133
|
+
/** whiteboard: name for the new Excalidraw session; script: the script's file name (`seed_public_holidays.py`). */
|
|
130
134
|
name?: string;
|
|
135
|
+
/** script: one line on what the script does. */
|
|
136
|
+
purpose?: string;
|
|
137
|
+
/** script: how to run it, with its arguments. */
|
|
138
|
+
usage?: string;
|
|
131
139
|
approvalId?: string;
|
|
132
140
|
decision?: "approved" | "rejected";
|
|
133
141
|
note?: string;
|
|
@@ -633,6 +641,9 @@ function workerReport(outcome: WorkerOutcome, approvals: Approval[], pushback?:
|
|
|
633
641
|
result.knowledgeProposals.length > 0
|
|
634
642
|
? `Knowledge proposals (accept with action=knowledge, or ignore): ${result.knowledgeProposals.map((proposal) => `${proposal.kind}: ${truncate(proposal.content, 160)}`).join("; ")}`
|
|
635
643
|
: "",
|
|
644
|
+
result.scriptProposals?.length
|
|
645
|
+
? `Script proposals (save with action=script name purpose usage file=<draft>, when the job will come back; or ignore): ${result.scriptProposals.map((proposal) => `${proposal.name}${proposal.purpose ? ` — ${truncate(proposal.purpose, 120)}` : ""}${proposal.draft ? ` (draft: ${proposal.draft})` : ""}`).join("; ")}`
|
|
646
|
+
: "",
|
|
636
647
|
issues.length > 0 ? `Issues: ${issues.join("; ")}` : "",
|
|
637
648
|
pushback && objection
|
|
638
649
|
? `Pushback recorded (${pushback.id}): ${truncate(objection.reason, 240)}. Resolve with action=resolve_approval before re-delegating ${result.domain}.`
|
|
@@ -993,7 +1004,6 @@ async function handleImplement(task: Task, params: OrchestrateParams, deps: Work
|
|
|
993
1004
|
for (const { domain } of assignments) assertNoPendingApprovals(task, domain);
|
|
994
1005
|
for (const { domain } of assignments) if (!task.domains.includes(domain)) task.domains.push(domain);
|
|
995
1006
|
// Under a time budget every step is given its share before any starts; a spent budget starts none.
|
|
996
|
-
const times = workerTimes(task, deps, assignments);
|
|
997
1007
|
if (task.track && onFastTrack(task)) task.track.roster = parseRoster([...task.track.roster, ...assignments.map((entry) => entry.domain)]);
|
|
998
1008
|
if (starting) startFast(task);
|
|
999
1009
|
if (task.track?.autoPlan) addFastSteps(task, deps, assignments);
|
|
@@ -1001,6 +1011,7 @@ async function handleImplement(task: Task, params: OrchestrateParams, deps: Work
|
|
|
1001
1011
|
await takeBaseline(task, deps);
|
|
1002
1012
|
// A QA step after work is in verifies it at its risk: read the change first.
|
|
1003
1013
|
if (assignments.some((entry) => entry.domain === "qa") && builtSomething(task)) await assessBuilt(task, deps);
|
|
1014
|
+
const times = await workerTimes(task, deps, assignments);
|
|
1004
1015
|
let report: string;
|
|
1005
1016
|
try {
|
|
1006
1017
|
if (assignments.length === 1) {
|
|
@@ -1010,7 +1021,10 @@ async function handleImplement(task: Task, params: OrchestrateParams, deps: Work
|
|
|
1010
1021
|
} else report = await runParallelWorkers(task, deps, assignments, times);
|
|
1011
1022
|
} finally {
|
|
1012
1023
|
// A step that never reported (the call failed) does not stay "running" in the budget.
|
|
1013
|
-
for (const time of times.values())
|
|
1024
|
+
for (const time of times.values()) {
|
|
1025
|
+
settleAllotment(task, deps, time.id, "stopped");
|
|
1026
|
+
time.finish?.();
|
|
1027
|
+
}
|
|
1014
1028
|
}
|
|
1015
1029
|
const tree = await changeState(deps, task, task.baseline?.head);
|
|
1016
1030
|
const lint = lintNote(await lintTask(task, deps, tree), deps.config.lint.mode, ownersOf(tree?.files));
|
|
@@ -1364,7 +1378,7 @@ async function handleQa(task: Task, params: OrchestrateParams, deps: WorkflowDep
|
|
|
1364
1378
|
return qaReport(header, result, decision, provenance, lintLine);
|
|
1365
1379
|
}
|
|
1366
1380
|
const time = qaTime(task, deps);
|
|
1367
|
-
const outcome = await runReviewer({ ...qaRequest(deps, task, diff, risk, params.task, provenance, lint), ...(time ? { time } : {}) }, deps.runProcess ?? spawnPiProcess);
|
|
1381
|
+
const outcome = await runReviewer({ ...qaRequest(deps, task, diff, risk, params.task, provenance, lint), effort: deps.effort, ...(time ? { time } : {}) }, deps.runProcess ?? spawnPiProcess);
|
|
1368
1382
|
if (time) settleAllotment(task, deps, time.id, "finished");
|
|
1369
1383
|
const { result, run } = outcome;
|
|
1370
1384
|
recordAdvisoryPushbacks(task, [{ pushback: result.pushback, who: "qa" }]);
|
|
@@ -1476,6 +1490,34 @@ function handleKnowledge(task: Task | undefined, params: OrchestrateParams, deps
|
|
|
1476
1490
|
return `Recorded ${kind} for ${agent} in ${outcome.path}.`;
|
|
1477
1491
|
}
|
|
1478
1492
|
|
|
1493
|
+
/**
|
|
1494
|
+
* The oracle adds a script to the shared library, or replaces one, so every
|
|
1495
|
+
* agent can run it the next time the job comes up. Its body is `text`, or a
|
|
1496
|
+
* file a worker drafted (`file`, inside the project or the task's directory).
|
|
1497
|
+
*/
|
|
1498
|
+
function handleScript(task: Task | undefined, params: OrchestrateParams, deps: WorkflowDeps): string {
|
|
1499
|
+
const name = params.name?.trim();
|
|
1500
|
+
if (!name) throw new Error("script requires name: what it does in snake_case, with its extension (seed_public_holidays.py)");
|
|
1501
|
+
const purpose = params.purpose?.trim();
|
|
1502
|
+
if (!purpose) throw new Error("script requires purpose: one line on what it does");
|
|
1503
|
+
const body = params.file?.trim() ? readDraft(params.file.trim(), task, deps) : params.text;
|
|
1504
|
+
if (!body?.trim()) throw new Error("script requires text (the script) or file (a drafted script)");
|
|
1505
|
+
const saved = saveScript({ dataRoot: dataRoot(deps.root, deps.configDir), name, purpose, body, ...(params.usage?.trim() ? { usage: params.usage.trim() } : {}) });
|
|
1506
|
+
const { entry } = saved;
|
|
1507
|
+
if (task) recordDecision(task, `${saved.archived ? "Replaced" : "Saved"} the shared script ${entry.name}: ${truncate(entry.purpose, 160)}`);
|
|
1508
|
+
lobbyTopics.bump("knowledge");
|
|
1509
|
+
return [`${saved.archived ? "Replaced" : "Saved"} ${entry.name} in the shared scripts library (${entry.path}); run: ${entry.usage}.`, saved.archived ? `The previous version is kept at ${saved.archived}.` : "", "Agents whose steps it fits are told to run it."].filter(Boolean).join(" ");
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
/** A drafted script, read only from inside the project, its worktree or the task's directory. */
|
|
1513
|
+
function readDraft(file: string, task: Task | undefined, deps: WorkflowDeps): string {
|
|
1514
|
+
const roots = [deps.cwd, deps.root, ...(task ? [taskDirFor(deps.root, deps.configDir, task.id)] : [])].map((dir) => resolve(dir));
|
|
1515
|
+
const path = isAbsolute(file) ? resolve(file) : resolve(deps.cwd, file);
|
|
1516
|
+
if (!roots.some((root) => path === root || path.startsWith(`${root}${sep}`))) throw new Error(`the drafted script must be inside the project: ${file}`);
|
|
1517
|
+
if (!existsSync(path)) throw new Error(`no drafted script at ${file}`);
|
|
1518
|
+
return readFileSync(path, "utf8");
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1479
1521
|
/**
|
|
1480
1522
|
* §24: the Master rewrites the file (asking the user about ambiguity), the
|
|
1481
1523
|
* engine archives the previous version and writes the compacted text.
|
|
@@ -1705,7 +1747,7 @@ const RESEARCH_SHARE = 0.15;
|
|
|
1705
1747
|
const DEFAULT_MORE_MINUTES = 10;
|
|
1706
1748
|
|
|
1707
1749
|
/** A delegation's time, with the allotment it is recorded under. */
|
|
1708
|
-
type WorkerTime = AgentTime & { id: string };
|
|
1750
|
+
type WorkerTime = AgentTime & { id: string; finish?: () => void };
|
|
1709
1751
|
|
|
1710
1752
|
/** The task's budget and where it stands, when it has one. */
|
|
1711
1753
|
function budgetFor(task: Task, deps: WorkflowDeps): { budget: TaskBudget; state: BudgetState } | undefined {
|
|
@@ -1726,7 +1768,7 @@ function timeNote(allotMs: number, state: BudgetState, worker: boolean): string
|
|
|
1726
1768
|
"## Time",
|
|
1727
1769
|
`You have ${formatMinutes(allotMs)} for this ${worker ? "step" : "work"}; the task has ${formatMinutes(state.leftMs)} of its ${formatMinutes(state.totalMs)} left.`,
|
|
1728
1770
|
worker
|
|
1729
|
-
? "At about 75% you get a heads-up.
|
|
1771
|
+
? "This is the total allowance shared across startup, retries, children, tools and reporting. At about 75% you get a heads-up. Before the hard deadline, stop and report where you left off (`## Left Off`) and how much more you need (`## More Time`); only the user can grant it. Land the most important part first, and keep every file consistent as you go."
|
|
1730
1772
|
: "When the time is up you are asked to stop and report what you have, so cover the most important questions first.",
|
|
1731
1773
|
].join("\n");
|
|
1732
1774
|
}
|
|
@@ -1752,7 +1794,7 @@ function oneLine(text: string, max: number): string {
|
|
|
1752
1794
|
function readerTime(task: Task, deps: WorkflowDeps, who: string, what: string, share: number, limitMs: number, label: string): WorkerTime | undefined {
|
|
1753
1795
|
const now = budgetFor(task, deps);
|
|
1754
1796
|
if (!now) return undefined;
|
|
1755
|
-
const ms = readerAllotment(now.state, share, limitMs);
|
|
1797
|
+
const ms = readerAllotment(now.state, share, Math.min(STEP_LIMIT_MS, limitMs));
|
|
1756
1798
|
if (ms === undefined) throw budgetSpent(now.state, label);
|
|
1757
1799
|
const id = `${who.toLowerCase()}-${Date.now().toString(36)}`;
|
|
1758
1800
|
recordAllotment(task, deps, { id, who, what: oneLine(what, 120), minutes: Math.round(ms / 60_000) });
|
|
@@ -1763,7 +1805,7 @@ function readerTime(task: Task, deps: WorkflowDeps, who: string, what: string, s
|
|
|
1763
1805
|
function qaTime(task: Task, deps: WorkflowDeps): WorkerTime | undefined {
|
|
1764
1806
|
const now = budgetFor(task, deps);
|
|
1765
1807
|
if (!now) return undefined;
|
|
1766
|
-
const ms = qaAllotment(now.state, deps.config.agents.qa.timeoutMs ?? deps.config.workflow.agentTimeoutMs);
|
|
1808
|
+
const ms = qaAllotment(now.state, Math.min(STEP_LIMIT_MS, deps.config.agents.qa.timeoutMs ?? deps.config.workflow.agentTimeoutMs));
|
|
1767
1809
|
if (ms === undefined) throw budgetSpent(now.state, "the QA gate");
|
|
1768
1810
|
const id = `qa-${Date.now().toString(36)}`;
|
|
1769
1811
|
recordAllotment(task, deps, { id, who: "QA gate", what: "review the task's work", minutes: Math.round(ms / 60_000) });
|
|
@@ -1781,18 +1823,58 @@ function openSlots(task: Task, batch: readonly Domain[]): number {
|
|
|
1781
1823
|
* for (it divides by scope) or an even share, never past what is left before
|
|
1782
1824
|
* the QA gate's reserve. A spent budget starts nothing.
|
|
1783
1825
|
*/
|
|
1784
|
-
function workerTimes(task: Task, deps: WorkflowDeps, assignments: readonly Assignment[]): Map<Domain, WorkerTime
|
|
1826
|
+
async function workerTimes(task: Task, deps: WorkflowDeps, assignments: readonly Assignment[]): Promise<Map<Domain, WorkerTime>> {
|
|
1785
1827
|
const times = new Map<Domain, WorkerTime>();
|
|
1786
1828
|
const now = budgetFor(task, deps);
|
|
1787
|
-
if (!now) return times;
|
|
1788
1829
|
const slots = openSlots(task, assignments.map((entry) => entry.domain));
|
|
1830
|
+
const steps = planSteps(task.plan ?? "");
|
|
1831
|
+
const completed = new Set(planChecklist(task.plan ?? "", (task.workerRuns ?? []).map((record) => ({ ...record, taskId: task.id, role: "worker" as const, output: "", attempts: 1 }))).flatMap((step, index) => step.status === "done" ? [index] : []));
|
|
1832
|
+
const prepared = [];
|
|
1789
1833
|
for (const { domain, instruction, minutes } of assignments) {
|
|
1790
|
-
const
|
|
1791
|
-
|
|
1792
|
-
const
|
|
1793
|
-
|
|
1794
|
-
if (
|
|
1795
|
-
|
|
1834
|
+
const covered = runSteps(steps, instruction, completed);
|
|
1835
|
+
const keys = covered.length ? covered.map((index) => `${domain}:step-${index + 1}`) : [`${domain}:unplanned`];
|
|
1836
|
+
const limitMs = Math.min(STEP_LIMIT_MS, profileFor(deps.config, deps.profile, domain, "worker").timeoutMs);
|
|
1837
|
+
let left = stepTimeLeft(deps.root, deps.configDir, task.id, keys, limitMs);
|
|
1838
|
+
if (left <= 0) {
|
|
1839
|
+
const wanted = DEFAULT_MORE_MINUTES;
|
|
1840
|
+
const granted = await decideMoreTime(task, deps, {
|
|
1841
|
+
label: AGENT_LABELS[domain], instruction, wanted,
|
|
1842
|
+
result: parseWorkerResult(domain, scratchpadSummary(deps, task, domain)),
|
|
1843
|
+
run: { runId: "", taskId: task.id, domain, role: "worker", status: "timeout", output: "", attempts: 1, startedAt: new Date().toISOString(), allotMs: limitMs },
|
|
1844
|
+
state: now?.state,
|
|
1845
|
+
});
|
|
1846
|
+
if (granted <= 0) throw new Error(`${AGENT_LABELS[domain]}'s step allowance is exhausted. Return the unfinished report; continuing requires user-approved time.`);
|
|
1847
|
+
grantStepTime(deps.root, deps.configDir, task.id, keys, limitMs, granted * 60_000);
|
|
1848
|
+
recordDecision(task, `The user gave ${AGENT_LABELS[domain]}'s exhausted step ${granted} more minutes.`);
|
|
1849
|
+
left = stepTimeLeft(deps.root, deps.configDir, task.id, keys, limitMs);
|
|
1850
|
+
}
|
|
1851
|
+
const given = now ? workerAllotment(now.state, minutes, slots) : { ms: left };
|
|
1852
|
+
if (!given) throw budgetSpent(now!.state, `the ${domain} step`);
|
|
1853
|
+
const ms = Math.min(left, given.ms);
|
|
1854
|
+
prepared.push({ domain, instruction, keys, limitMs, ms });
|
|
1855
|
+
}
|
|
1856
|
+
try {
|
|
1857
|
+
for (const { domain, instruction, keys, limitMs, ms } of prepared) {
|
|
1858
|
+
const reservation = reserveStepTime(deps.root, deps.configDir, task.id, keys, limitMs, ms);
|
|
1859
|
+
const id = `${domain}-${Date.now().toString(36)}`;
|
|
1860
|
+
let exhausted = false;
|
|
1861
|
+
const askMore = moreTimeFor(task, deps, domain, instruction, id, (extraMs) => reservation.grant(extraMs));
|
|
1862
|
+
recordAllotment(task, deps, { id, who: AGENT_LABELS[domain], what: oneLine(instruction, 120), minutes: Math.round(ms / 60_000) });
|
|
1863
|
+
const time: WorkerTime = { id, endsAt: Date.now() + ms, allotMs: ms, strict: true,
|
|
1864
|
+
note: now ? timeNote(ms, now.state, true) : `## Time\nYou have ${formatMinutes(ms)} total for this step, shared across retries and children. Report unfinished work before the deadline; only the user can grant more time.`,
|
|
1865
|
+
onTimeUp: async (run, report) => {
|
|
1866
|
+
const extraMs = await askMore(run, report);
|
|
1867
|
+
const result = parseWorkerResult(domain, report);
|
|
1868
|
+
if (extraMs <= 0 && (result.leftOff || result.moreTime)) exhausted = true;
|
|
1869
|
+
return extraMs;
|
|
1870
|
+
},
|
|
1871
|
+
};
|
|
1872
|
+
time.finish = () => reservation.finish(time.usedMs ?? ms, exhausted);
|
|
1873
|
+
times.set(domain, time);
|
|
1874
|
+
}
|
|
1875
|
+
} catch (error) {
|
|
1876
|
+
for (const time of times.values()) { time.usedMs = 0; time.finish?.(); }
|
|
1877
|
+
throw error;
|
|
1796
1878
|
}
|
|
1797
1879
|
return times;
|
|
1798
1880
|
}
|
|
@@ -1809,30 +1891,27 @@ function oneAtATime<T>(ask: () => Promise<T>): Promise<T> {
|
|
|
1809
1891
|
|
|
1810
1892
|
/**
|
|
1811
1893
|
* A worker out of time has reported what it did, where it left off and how
|
|
1812
|
-
* much more it needs.
|
|
1813
|
-
* is left before the QA gate's reserve); a grant past that grows the budget,
|
|
1894
|
+
* much more it needs. Only the user grants more time; a grant past the task budget grows it,
|
|
1814
1895
|
* and the same agent carries on. Resolves with the ms granted, 0 to stop it.
|
|
1815
1896
|
*/
|
|
1816
|
-
function moreTimeFor(task: Task, deps: WorkflowDeps, domain: Domain, instruction: string, id: string): NonNullable<AgentTime["onTimeUp"]> {
|
|
1817
|
-
let asked = 0;
|
|
1897
|
+
function moreTimeFor(task: Task, deps: WorkflowDeps, domain: Domain, instruction: string, id: string, onGrant?: (ms: number) => void): NonNullable<AgentTime["onTimeUp"]> {
|
|
1818
1898
|
return (run, report) => oneAtATime(async () => {
|
|
1819
1899
|
const result = parseWorkerResult(domain, report);
|
|
1820
1900
|
// A report with nothing left to do finished in time.
|
|
1821
1901
|
if (!result.leftOff && !result.moreTime) return 0;
|
|
1822
|
-
asked += 1;
|
|
1823
1902
|
const label = AGENT_LABELS[domain];
|
|
1824
1903
|
const wanted = result.moreTime?.minutes ?? DEFAULT_MORE_MINUTES;
|
|
1825
1904
|
const now = budgetFor(task, deps);
|
|
1826
|
-
|
|
1827
|
-
const minutes = await decideMoreTime(task, deps, { label, instruction, result, wanted, run, asked, state: now.state });
|
|
1905
|
+
const minutes = await decideMoreTime(task, deps, { label, instruction, result, wanted, run, state: now?.state });
|
|
1828
1906
|
if (minutes <= 0) return 0;
|
|
1829
|
-
const over = Math.max(0, Math.ceil((minutes * 60_000 - now
|
|
1907
|
+
const over = Math.max(0, Math.ceil((minutes * 60_000 - (now?.state.windowMs ?? minutes * 60_000)) / 60_000));
|
|
1830
1908
|
updateBudget(deps.root, deps.configDir, task.id, (budget) => {
|
|
1831
1909
|
budget.granted += over;
|
|
1832
1910
|
const entry = budget.allotments.find((allotment) => allotment.id === id);
|
|
1833
1911
|
if (entry) Object.assign(entry, { minutes: entry.minutes + minutes, granted: entry.granted + minutes });
|
|
1834
1912
|
});
|
|
1835
|
-
recordDecision(task, `${label} ran out of time;
|
|
1913
|
+
recordDecision(task, `${label} ran out of time; the user gave it ${minutes} more minutes${over > 0 ? `, ${over} of them added to the task's budget` : ""}. Left off: ${oneLine(result.leftOff ?? result.moreTime?.reason ?? "", 200)}`);
|
|
1914
|
+
onGrant?.(minutes * 60_000);
|
|
1836
1915
|
return minutes * 60_000;
|
|
1837
1916
|
});
|
|
1838
1917
|
}
|
|
@@ -1843,22 +1922,19 @@ interface MoreTimeAsk {
|
|
|
1843
1922
|
result: WorkerResult;
|
|
1844
1923
|
wanted: number;
|
|
1845
1924
|
run: AgentRun;
|
|
1846
|
-
|
|
1847
|
-
state: BudgetState;
|
|
1925
|
+
state?: BudgetState;
|
|
1848
1926
|
}
|
|
1849
1927
|
|
|
1850
1928
|
const MORE_TIME = "Give it the time it asks for";
|
|
1851
1929
|
const OTHER_TIME = "Give a different amount";
|
|
1852
1930
|
|
|
1853
1931
|
async function decideMoreTime(task: Task, deps: WorkflowDeps, ask: MoreTimeAsk): Promise<number> {
|
|
1854
|
-
const { label, instruction, result, wanted, run,
|
|
1932
|
+
const { label, instruction, result, wanted, run, state } = ask;
|
|
1855
1933
|
if (isAutoMode(deps.root, deps.configDir, task.id)) {
|
|
1856
|
-
|
|
1857
|
-
if (asked === 1 && wanted * 60_000 <= state.windowMs) return wanted;
|
|
1858
|
-
recordDecision(task, `Auto mode: ${label} ran out of time and was not given more (${asked > 1 ? "it already had more once" : `it asked for ${wanted} minutes, ${formatMinutes(state.windowMs)} were left`}).`);
|
|
1934
|
+
recordDecision(task, `Auto mode: ${label}'s step allowance is spent. More time requires the user; return the unfinished report.`);
|
|
1859
1935
|
return 0;
|
|
1860
1936
|
}
|
|
1861
|
-
const over = Math.max(0, Math.ceil((wanted * 60_000 - state
|
|
1937
|
+
const over = Math.max(0, Math.ceil((wanted * 60_000 - (state?.windowMs ?? wanted * 60_000)) / 60_000));
|
|
1862
1938
|
const had = run.allotMs ?? 0;
|
|
1863
1939
|
const choice = await deps.choose(
|
|
1864
1940
|
[
|
|
@@ -1866,7 +1942,7 @@ async function decideMoreTime(task: Task, deps: WorkflowDeps, ask: MoreTimeAsk):
|
|
|
1866
1942
|
result.completed ? `Done so far: ${oneLine(result.completed, 400)}` : "",
|
|
1867
1943
|
result.leftOff ? `Left to do: ${oneLine(result.leftOff, 400)}` : "",
|
|
1868
1944
|
`It needs about ${wanted} more minutes${result.moreTime?.reason ? `: ${oneLine(result.moreTime.reason, 200)}` : ""}.`,
|
|
1869
|
-
`The task has used ${formatMinutes(state.usedMs)} of ${formatMinutes(state.totalMs)} (${formatMinutes(state.leftMs)} left)${over > 0 ? `; ${wanted} more minutes adds ${over} to its budget` : ""}
|
|
1945
|
+
state ? `The task has used ${formatMinutes(state.usedMs)} of ${formatMinutes(state.totalMs)} (${formatMinutes(state.leftMs)} left)${over > 0 ? `; ${wanted} more minutes adds ${over} to its budget` : ""}.` : "This step has a shared allowance even though the task has no overall time budget.",
|
|
1870
1946
|
`Give ${label} ${wanted} more minutes to finish?`,
|
|
1871
1947
|
].filter(Boolean).join("\n\n"),
|
|
1872
1948
|
[MORE_TIME, OTHER_TIME, `Stop ${label} here`],
|
|
@@ -1879,7 +1955,7 @@ async function decideMoreTime(task: Task, deps: WorkflowDeps, ask: MoreTimeAsk):
|
|
|
1879
1955
|
|
|
1880
1956
|
/** Stopped at its time with work left: a report that finished everything as time ran out is not. */
|
|
1881
1957
|
function stoppedForTime(outcome: WorkerOutcome): boolean {
|
|
1882
|
-
return Boolean(outcome.run.timeUp && (outcome.result.leftOff || outcome.result.moreTime));
|
|
1958
|
+
return Boolean(outcome.run.status === "timeout" || (outcome.run.timeUp && (outcome.result.leftOff || outcome.result.moreTime)));
|
|
1883
1959
|
}
|
|
1884
1960
|
|
|
1885
1961
|
/** How a step's time went, for the oracle: out of time and stopped, or given more. */
|
|
@@ -1951,6 +2027,7 @@ const HANDLERS: Record<OrchestrateAction, (task: Task, params: OrchestrateParams
|
|
|
1951
2027
|
resolve_approval: handleResolveApproval,
|
|
1952
2028
|
qa: handleQa,
|
|
1953
2029
|
knowledge: handleKnowledge,
|
|
2030
|
+
script: handleScript,
|
|
1954
2031
|
compact: handleCompact,
|
|
1955
2032
|
whiteboard: handleWhiteboard,
|
|
1956
2033
|
complete: handleComplete,
|
|
@@ -1969,9 +2046,9 @@ const HANDLERS: Record<OrchestrateAction, (task: Task, params: OrchestrateParams
|
|
|
1969
2046
|
*/
|
|
1970
2047
|
export async function runWorkflowAction(params: OrchestrateParams, deps: WorkflowDeps): Promise<WorkflowResult> {
|
|
1971
2048
|
// Project knowledge can be learned without starting or changing a task.
|
|
1972
|
-
if ((params.action === "knowledge" || params.action === "compact") && !params.taskId) {
|
|
2049
|
+
if ((params.action === "knowledge" || params.action === "compact" || params.action === "script") && !params.taskId) {
|
|
1973
2050
|
try {
|
|
1974
|
-
const message = params.action === "knowledge" ? handleKnowledge(undefined, params, deps) : handleCompact(undefined, params, deps);
|
|
2051
|
+
const message = params.action === "knowledge" ? handleKnowledge(undefined, params, deps) : params.action === "script" ? handleScript(undefined, params, deps) : handleCompact(undefined, params, deps);
|
|
1975
2052
|
return { ok: true, taskId: "", state: "created", message };
|
|
1976
2053
|
} catch (error) {
|
|
1977
2054
|
return { ok: false, taskId: "", state: "created", message: `Rejected: ${(error as Error).message}` };
|
|
@@ -2011,6 +2088,11 @@ export async function runWorkflowAction(params: OrchestrateParams, deps: Workflo
|
|
|
2011
2088
|
...(deps.askQuestions ? { askQuestions: (...args: Parameters<NonNullable<WorkflowDeps["askQuestions"]>>) => wait(() => deps.askQuestions!(...args)) } : {}),
|
|
2012
2089
|
// Every agent, diff and desk of a task with its own worktree works there.
|
|
2013
2090
|
cwd: taskCwd(task, deps.cwd),
|
|
2091
|
+
// An agent can work for an hour: what the action has done so far (implementing, reviewing) is saved before it starts, so every window reads it.
|
|
2092
|
+
runProcess: (args, options) => {
|
|
2093
|
+
saveTask(deps.root, deps.configDir, task);
|
|
2094
|
+
return (deps.runProcess ?? spawnPiProcess)(args, options);
|
|
2095
|
+
},
|
|
2014
2096
|
onUpdate: (run) => {
|
|
2015
2097
|
if (run.status !== "running") {
|
|
2016
2098
|
finished.set(run.runId, run);
|