@a-t-h-i/bot-lobby 0.6.54 → 0.6.56
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/THIRD_PARTY_NOTICES.md +12 -1
- package/package.json +4 -1
- package/prompts/master.md +10 -30
- package/prompts/worker.md +8 -12
- package/src/desk/ipc.ts +4 -3
- package/src/execution/agent-runner.ts +20 -129
- package/src/execution/delegation.ts +10 -32
- package/src/execution/live-run.ts +3 -14
- package/src/execution/pi-runner.ts +36 -166
- package/src/harness/acp.ts +272 -0
- package/src/harness/catalog.ts +257 -0
- package/src/harness/claude-sdk.ts +169 -0
- package/src/harness/claude.ts +188 -0
- package/src/harness/cli-args.ts +62 -0
- package/src/harness/codex.ts +155 -0
- package/src/harness/instructions.ts +45 -0
- package/src/harness/model-manifest.json +829 -0
- package/src/harness/process.ts +175 -0
- package/src/harness/runner.ts +188 -0
- package/src/harness/status.ts +115 -0
- package/src/index.ts +3 -0
- package/src/lobby/feed.ts +1 -1
- package/src/lobby/host.ts +4 -0
- package/src/lobby/planner.ts +4 -3
- package/src/lobby/pr-review.ts +4 -3
- package/src/lobby/quickfix.ts +8 -4
- package/src/lobby/recovery.ts +2 -10
- package/src/lobby/runtime.ts +1 -1
- package/src/lobby/service.ts +3 -1
- package/src/master/goal-check.ts +1 -6
- package/src/master/master.ts +12 -24
- package/src/master/research.ts +4 -6
- package/src/pi/commands.ts +5 -45
- package/src/pi/delegation.ts +4 -4
- package/src/pi/events.ts +6 -19
- package/src/pi/grants.ts +89 -0
- package/src/pi/model-settings.ts +2 -4
- package/src/pi/model-support.ts +10 -6
- package/src/pi/route.ts +1 -1
- package/src/pi/run-summary.ts +3 -7
- package/src/pi/start-flags.ts +3 -3
- package/src/pi/start-task.ts +2 -8
- package/src/pi/tools.ts +8 -11
- package/src/roles/worker.ts +0 -18
- package/src/schemas/configuration.ts +73 -45
- package/src/schemas/findings.ts +0 -12
- package/src/schemas/harness.ts +122 -0
- package/src/schemas/task.ts +0 -1
- package/src/state/metrics.ts +1 -1
- package/src/state/work-clock.ts +140 -0
- package/src/state/work-time.ts +4 -7
- package/src/webui/api/index.ts +3 -1
- package/src/webui/api/settings.ts +12 -18
- package/src/webui/dev/fake-service.ts +27 -0
- package/src/webui/protocol.ts +32 -0
- package/src/workflow/workflow.ts +23 -318
- package/webui/dist/assets/{TerminalView-CbRc77C8.js → TerminalView-BVehQ7yB.js} +1 -1
- package/webui/dist/assets/index-BKiKxUUG.js +114 -0
- package/webui/dist/assets/index-DOLQ4FDd.css +2 -0
- package/webui/dist/build.json +20 -12
- package/webui/dist/index.html +2 -2
- package/src/state/budget.ts +0 -301
- package/src/state/step-limits.ts +0 -41
- package/webui/dist/assets/index-Cca5uEVs.js +0 -115
- package/webui/dist/assets/index-DsVdXmh5.css +0 -2
package/README.md
CHANGED
|
@@ -73,4 +73,4 @@ Settings live in `~/.pi/bot-lobby/config.json` (`BOT_LOBBY_CONFIG_DIR` moves the
|
|
|
73
73
|
project knowledge stay in `.pi/bot-lobby/` inside your project.
|
|
74
74
|
|
|
75
75
|
The [guide](docs/guide.md) covers the rest: delivery and commits, crash recovery, knowledge and
|
|
76
|
-
shared scripts, the classifier, delegation,
|
|
76
|
+
shared scripts, the classifier, delegation, compaction, and MCP tools and skills.
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
The web UI's look (colours, radii, shadows, control and dialog styles, the
|
|
6
6
|
sidebar rail, the chat rows, the work log, the composer (its surface, footer controls and send button), the select
|
|
7
7
|
fields and drop-down lists, the loading and status animations and the
|
|
8
|
-
built-in colour themes)
|
|
8
|
+
built-in colour themes), and the third-party harness support (the model
|
|
9
|
+
manifest, CLI flags, prompt injection and ACP handling behind Settings >
|
|
10
|
+
Harnesses), is adapted from T3 Code,
|
|
9
11
|
https://github.com/pingdotgg/t3code. No T3 Code package is a
|
|
10
12
|
dependency; the styles and markup below were ported into this repository and
|
|
11
13
|
rewired to bot-lobby's own data layer. Each adapted file starts with a comment
|
|
@@ -37,6 +39,15 @@ naming the file it came from.
|
|
|
37
39
|
| `webui/src/app/TerminalDrawer.tsx` | `apps/web/src/components/ThreadTerminalDrawer.tsx` (the drawer, its floating toolbar and terminal list) |
|
|
38
40
|
| `webui/src/tabs/lobby/InlineActivity.tsx` | `apps/web/src/components/chat/WorkLog.tsx` |
|
|
39
41
|
| `src/webui/palette-core.ts` (the built-in themes) | `packages/shared/src/themePalettes.ts` (Blossom is their T3 Chat theme, renamed) |
|
|
42
|
+
| `src/harness/model-manifest.json` (copied as is) | `apps/server/src/provider/model-manifest.json` |
|
|
43
|
+
| `src/harness/catalog.ts` | `apps/server/src/provider/ClaudeModelCatalog.ts` (effort descriptors and maps), `CodexProvider.ts` (effort levels), `antigravityAuthSupport.ts` (`buildAntigravityAcpSpawnInput`) |
|
|
44
|
+
| `src/harness/cli-args.ts` | `packages/shared/src/cliArgs.ts` (`tokenizeCliArgs`) |
|
|
45
|
+
| `src/harness/instructions.ts` | `packages/provider-core/src/server/runtimeInstructions.ts`, `orchestrationInstructions.ts` (`t3AcpPromptWithInstructions`) |
|
|
46
|
+
| `src/harness/codex.ts` | `apps/server/src/textGeneration/CodexTextGeneration.ts`, `apps/server/src/provider/codexLaunchArgs.ts` |
|
|
47
|
+
| `src/harness/claude.ts` | `apps/server/src/textGeneration/ClaudeTextGeneration.ts`, `apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts` (auto-compact window) |
|
|
48
|
+
| `src/harness/claude-sdk.ts` | `apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts` (Agent SDK query options) |
|
|
49
|
+
| `src/harness/acp.ts` | `apps/server/src/provider/acp/AntigravityProtocol.ts` (permission answers), `packages/provider-acp/src/server/sessionConfig.ts` (session option categories) |
|
|
50
|
+
| `src/schemas/harness.ts`, `webui/src/tabs/settings/HarnessesGroup.tsx` | `packages/contracts/src/settings.ts` (provider settings fields), `apps/web/src/components/settings/ProviderInstanceCard.tsx` |
|
|
40
51
|
|
|
41
52
|
```
|
|
42
53
|
MIT License
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@a-t-h-i/bot-lobby",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.56",
|
|
4
4
|
"description": "Structured multi-agent software engineering orchestrator for Pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -96,5 +96,8 @@
|
|
|
96
96
|
"typebox": "1.3.27",
|
|
97
97
|
"typescript": "^5.7.0",
|
|
98
98
|
"vite": "8.3.2"
|
|
99
|
+
},
|
|
100
|
+
"optionalDependencies": {
|
|
101
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.306"
|
|
99
102
|
}
|
|
100
103
|
}
|
package/prompts/master.md
CHANGED
|
@@ -339,36 +339,16 @@ agents share facts without you:
|
|
|
339
339
|
`reports/` folder and the report gives its path; long findings go in the
|
|
340
340
|
task's `notes/` folder. Read the file when the detail matters rather than
|
|
341
341
|
asking for it again.
|
|
342
|
-
- A report flagged as
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
##
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
repeating failed approaches. Waiting for the user pauses clocks.
|
|
353
|
-
|
|
354
|
-
When the task has a time budget (your context says how much is used and
|
|
355
|
-
left), it covers everyone: you, scouts, workers and the QA gate. The clock
|
|
356
|
-
runs while you work and stops while you wait on the user.
|
|
357
|
-
|
|
358
|
-
- Size the plan to fit it, and say so in the proposal when it does not.
|
|
359
|
-
- Divide what is left by scope: give each `implement` its `minutes` (per
|
|
360
|
-
assignment in a parallel batch). Bigger steps get more; keep the QA gate's
|
|
361
|
-
reserve (the engine holds it back). Without `minutes` a step gets an even
|
|
362
|
-
share.
|
|
363
|
-
- Every agent is told its minutes. One that runs out stops, reports what it
|
|
364
|
-
did, where it left off and how much more it needs, and the user decides; if
|
|
365
|
-
they give it more, the same agent carries on where it stopped. A step that
|
|
366
|
-
was not given more comes back unfinished: trim the scope, or ask for task
|
|
367
|
-
time.
|
|
368
|
-
- When the budget is spent the engine starts no new work. Ask the user with
|
|
369
|
-
`action=budget` (`minutes` and a `reason`: what is left and why it is worth
|
|
370
|
-
it), or wrap up with what is done. `action=budget` with no minutes shows
|
|
371
|
-
where it stands.
|
|
342
|
+
- A report flagged as stalled may be partial: check what is missing and
|
|
343
|
+
delegate only the remainder.
|
|
344
|
+
|
|
345
|
+
## No time limits
|
|
346
|
+
|
|
347
|
+
Agents have no time limit. An assignment runs until its agent reports, the
|
|
348
|
+
user stops it, or it goes silent so long that the stall watchdog stops it.
|
|
349
|
+
Size steps by scope, not by the clock, and never cut a step short or split it
|
|
350
|
+
to save time. In auto mode keep going until the task is complete or truly
|
|
351
|
+
blocked: nobody needs to grant more time.
|
|
372
352
|
|
|
373
353
|
## Git branch or worktree
|
|
374
354
|
|
package/prompts/worker.md
CHANGED
|
@@ -58,18 +58,14 @@ no code deleted or bent just to make a rule pass. Every one of them is listed
|
|
|
58
58
|
for QA, who reads it as a finding unless it carries a reason that holds. When
|
|
59
59
|
a rule is wrong for your change, leave it failing and say why under Notes.
|
|
60
60
|
|
|
61
|
-
##
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
time is up you are told to stop: finish or revert the edit in progress, then
|
|
70
|
-
report with `## Left Off` (what you were doing, what is still to do) and
|
|
71
|
-
`## More Time` (`N minutes — why`), honestly sized. If the user gives you more,
|
|
72
|
-
you carry on from where you stopped.
|
|
61
|
+
## Pace
|
|
62
|
+
|
|
63
|
+
There is no time limit: finish the step properly and verify it rather than
|
|
64
|
+
cutting it short. Work efficiently all the same: read what you need, make the
|
|
65
|
+
change, verify, report, and do not explore past what the step needs. Keep
|
|
66
|
+
every file consistent as you go. A long silent command (one that prints
|
|
67
|
+
nothing for many minutes) can be stopped as stalled, so give such commands
|
|
68
|
+
output or a bash `timeout`.
|
|
73
69
|
|
|
74
70
|
## Working alongside other workers (file desk)
|
|
75
71
|
|
package/src/desk/ipc.ts
CHANGED
|
@@ -132,7 +132,7 @@ export interface DeskClient {
|
|
|
132
132
|
export function createDeskClient(address: string, worker: string): DeskClient {
|
|
133
133
|
let socket: Socket | undefined;
|
|
134
134
|
let nextId = 1;
|
|
135
|
-
const pending = new Map<number, { done: (response: DeskResponse) => void; timer: ReturnType<typeof setTimeout> }>();
|
|
135
|
+
const pending = new Map<number, { done: (response: DeskResponse) => void; timer: ReturnType<typeof setTimeout> | undefined }>();
|
|
136
136
|
const failAll = (text: string) => {
|
|
137
137
|
for (const [id, entry] of pending) {
|
|
138
138
|
clearTimeout(entry.timer);
|
|
@@ -170,10 +170,11 @@ export function createDeskClient(address: string, worker: string): DeskClient {
|
|
|
170
170
|
request(request, timeoutMs = 10_000) {
|
|
171
171
|
const id = nextId++;
|
|
172
172
|
return new Promise((done) => {
|
|
173
|
-
|
|
173
|
+
// 0 waits for as long as the answer takes (a delegation, a consult): no time limit.
|
|
174
|
+
const timer = timeoutMs > 0 ? setTimeout(() => {
|
|
174
175
|
pending.delete(id);
|
|
175
176
|
done({ id, ok: false, text: "the file desk did not answer" });
|
|
176
|
-
}, timeoutMs);
|
|
177
|
+
}, timeoutMs) : undefined;
|
|
177
178
|
pending.set(id, { done, timer });
|
|
178
179
|
try {
|
|
179
180
|
connect().write(`${JSON.stringify({ ...request, id, worker })}\n`);
|
|
@@ -5,12 +5,11 @@ import type { AgentRun } from "../schemas/findings.ts";
|
|
|
5
5
|
import { roleSpec } from "../roles/registry.ts";
|
|
6
6
|
import { compilePrompt } from "../prompts/compiler.ts";
|
|
7
7
|
import { EditLog } from "../state/changes.ts";
|
|
8
|
-
import { formatMinutes, REPORT_GRACE_MS } from "../state/budget.ts";
|
|
9
8
|
import { runPiAgent, spawnPiProcess, type ProcessRunner, type RelayAsk } from "./pi-runner.ts";
|
|
10
9
|
import { ASK_ENV } from "../ask/relay.ts";
|
|
11
|
-
import { STEP_LIMIT_MS } from "../state/step-limits.ts";
|
|
12
10
|
import type { EffortRouter, RunProfileLike } from "../classifier/effort.ts";
|
|
13
11
|
import { ASK_TOOL } from "../ask/types.ts";
|
|
12
|
+
import type { HarnessRun } from "../schemas/harness.ts";
|
|
14
13
|
|
|
15
14
|
export interface AgentContext {
|
|
16
15
|
task: string;
|
|
@@ -54,7 +53,8 @@ export interface AgentRequest {
|
|
|
54
53
|
/** See PiRunOptions.expert. */
|
|
55
54
|
expert?: { model?: string; thinking: string };
|
|
56
55
|
thinking?: string;
|
|
57
|
-
|
|
56
|
+
/** Skill files this agent may load; absent keeps pi's own discovery. */
|
|
57
|
+
skills?: string[];
|
|
58
58
|
cwd: string;
|
|
59
59
|
signal?: AbortSignal;
|
|
60
60
|
onUpdate?: (run: AgentRun) => void;
|
|
@@ -64,8 +64,6 @@ export interface AgentRequest {
|
|
|
64
64
|
stallTimeoutMs?: number;
|
|
65
65
|
/** Silence allowed while one tool call runs. */
|
|
66
66
|
toolStallTimeoutMs?: number;
|
|
67
|
-
/** Fraction of `timeoutMs` at which the agent is asked to wrap up; 0 disables. */
|
|
68
|
-
wrapUpAt?: number;
|
|
69
67
|
/** Extra tools on top of the role allowlist (the file desk's tools). */
|
|
70
68
|
extraTools?: readonly string[];
|
|
71
69
|
env?: Record<string, string>;
|
|
@@ -76,56 +74,10 @@ export interface AgentRequest {
|
|
|
76
74
|
/** The classifier routed this run down from this configured profile (receipts and metrics show it). */
|
|
77
75
|
routedFrom?: string;
|
|
78
76
|
route?: string;
|
|
79
|
-
/**
|
|
80
|
-
time?: AgentTime;
|
|
81
|
-
/** Lets the agent ask the user (ask_user_question, relayed through the master); its clocks wait for the answers. */
|
|
77
|
+
/** Lets the agent ask the user (ask_user_question, relayed through the master). */
|
|
82
78
|
onAsk?: RelayAsk;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* A run under a task time budget. The agent is told its minutes up front,
|
|
87
|
-
* warned at 75%, and asked to stop and report when they run out. A worker
|
|
88
|
-
* then reports where it left off and how much more it needs; `onTimeUp`
|
|
89
|
-
* (the engine asking the user) grants more, and the same agent carries on.
|
|
90
|
-
*/
|
|
91
|
-
export interface AgentTime {
|
|
92
|
-
/** Active time shared across attempts and model fallbacks. */
|
|
93
|
-
usedMs?: number;
|
|
94
|
-
/** Include reporting in the hard allowance. */
|
|
95
|
-
strict?: boolean;
|
|
96
|
-
/** When the delegation's time runs out (epoch ms); moved out when more is granted, shared by retries and re-runs. */
|
|
97
|
-
endsAt: number;
|
|
98
|
-
/** The allotment, as the agent and the lobby see it. */
|
|
99
|
-
allotMs: number;
|
|
100
|
-
/** What the agent is told about its time, in its context. */
|
|
101
|
-
note?: string;
|
|
102
|
-
/** Workers only: decide on more time once it has reported where it left off; resolve with ms, 0 to stop it. */
|
|
103
|
-
onTimeUp?: (run: AgentRun, report: string) => Promise<number>;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** The warning at 75% of an allotment. */
|
|
107
|
-
export function headsUpMessage(leftMs: number, allotMs: number): string {
|
|
108
|
-
return `bot-lobby: about ${formatMinutes(leftMs)} of your ${formatMinutes(allotMs)} for this step are left. Finish what you have started and make sure it works; leave nice-to-haves.`;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** The steer when an allotment is spent: a worker says where it left off and what more it needs; any other role reports what it has. */
|
|
112
|
-
export function timeUpMessage(allotMs: number, canAskForMore: boolean): string {
|
|
113
|
-
return canAskForMore
|
|
114
|
-
? `bot-lobby: your ${formatMinutes(allotMs)} for this step are up. Stop here: finish or revert the edit in progress so every file is consistent, start nothing new, and write your report now in the required format, adding \`## Left Off\` (what you were doing and what is still to do) and \`## More Time\` (how many more minutes you need, and why). The user decides whether you get more time to finish.`
|
|
115
|
-
: `bot-lobby: your ${formatMinutes(allotMs)} are up. Stop now and write your report in the required format with what you have.`;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** The follow-up that sends an agent on once the user gave it more time. */
|
|
119
|
-
export function carryOnMessage(extraMs: number): string {
|
|
120
|
-
return `bot-lobby: the user gave you ${formatMinutes(extraMs)} more. Carry on from where you left off and finish the step, then write your final report in the required format (leave out Left Off and More Time if you finish).`;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** Least time an attempt starts with; a retry with less than this left is not started. */
|
|
124
|
-
const MIN_ATTEMPT_MS = 60_000;
|
|
125
|
-
|
|
126
|
-
export function boundedAgentTime(timeoutMs: number): AgentTime {
|
|
127
|
-
const allotMs = Math.min(STEP_LIMIT_MS, timeoutMs);
|
|
128
|
-
return { endsAt: Date.now() + allotMs, allotMs, strict: true };
|
|
79
|
+
/** Run on this third-party harness instead of pi. */
|
|
80
|
+
harness?: HarnessRun;
|
|
129
81
|
}
|
|
130
82
|
|
|
131
83
|
/** The watchdog fields of an AgentRequest, taken from the workflow config. */
|
|
@@ -133,19 +85,17 @@ export function watchdogOptions(workflow: {
|
|
|
133
85
|
maxAgentRetries: number;
|
|
134
86
|
stallTimeoutMs: number;
|
|
135
87
|
toolStallTimeoutMs: number;
|
|
136
|
-
|
|
137
|
-
}): Pick<AgentRequest, "retries" | "stallTimeoutMs" | "toolStallTimeoutMs" | "wrapUpAt"> {
|
|
88
|
+
}): Pick<AgentRequest, "retries" | "stallTimeoutMs" | "toolStallTimeoutMs"> {
|
|
138
89
|
return {
|
|
139
90
|
retries: workflow.maxAgentRetries,
|
|
140
91
|
stallTimeoutMs: workflow.stallTimeoutMs,
|
|
141
92
|
toolStallTimeoutMs: workflow.toolStallTimeoutMs,
|
|
142
|
-
wrapUpAt: workflow.wrapUpAt,
|
|
143
93
|
};
|
|
144
94
|
}
|
|
145
95
|
|
|
146
96
|
const activeControllers = new Set<AbortController>();
|
|
147
97
|
|
|
148
|
-
/** A stall or crash is worth one more try; a
|
|
98
|
+
/** A stall or crash is worth one more try; a cancel is not. */
|
|
149
99
|
function retryable(run: AgentRun): boolean {
|
|
150
100
|
if (run.status === "failed") return true;
|
|
151
101
|
return run.status === "timeout" && run.stalled === true;
|
|
@@ -153,23 +103,11 @@ function retryable(run: AgentRun): boolean {
|
|
|
153
103
|
|
|
154
104
|
/**
|
|
155
105
|
* Run one domain/role agent, retrying transient failures a bounded number of
|
|
156
|
-
* times. Cancellation
|
|
157
|
-
*
|
|
106
|
+
* times. Cancellation never retries, so Esc stays responsive. There is no
|
|
107
|
+
* time limit: an agent works until it reports, is cancelled, or stalls.
|
|
158
108
|
*/
|
|
159
109
|
export async function runAgent(request: AgentRequest, run: ProcessRunner = spawnPiProcess): Promise<AgentRun> {
|
|
160
|
-
request.time ??= boundedAgentTime(request.timeoutMs);
|
|
161
|
-
if (!request.time.strict) {
|
|
162
|
-
request.time.endsAt = Math.min(request.time.endsAt, Date.now() + Math.min(STEP_LIMIT_MS, request.timeoutMs));
|
|
163
|
-
request.time.allotMs = Math.min(request.time.allotMs, STEP_LIMIT_MS, request.timeoutMs);
|
|
164
|
-
request.time.strict = true;
|
|
165
|
-
}
|
|
166
|
-
request.time.usedMs ??= Math.max(0, request.time.allotMs - (request.time.endsAt - Date.now()));
|
|
167
110
|
const startedAt = new Date().toISOString();
|
|
168
|
-
if (request.time.endsAt <= Date.now()) {
|
|
169
|
-
const stopped: AgentRun = { ...baseRun(request, `${request.taskId}:${randomUUID()}`, startedAt), status: "timeout", error: "The assignment's shared time limit is spent; request user-approved time before continuing.", finishedAt: startedAt, timeUp: true };
|
|
170
|
-
request.onUpdate?.(stopped);
|
|
171
|
-
return stopped;
|
|
172
|
-
}
|
|
173
111
|
const attempts = Math.max(1, (request.retries ?? 0) + 1);
|
|
174
112
|
// A failed attempt may have edited files before the retry: the run owns every edit.
|
|
175
113
|
const edits = new EditLog(request.cwd);
|
|
@@ -179,8 +117,6 @@ export async function runAgent(request: AgentRequest, run: ProcessRunner = spawn
|
|
|
179
117
|
last = await runAgentOnce(request, run, attempt, startedAt, edits, quota);
|
|
180
118
|
request.onAttemptEnd?.(last);
|
|
181
119
|
if (!retryable(last)) break;
|
|
182
|
-
// Under a budget a retry only uses what is left of the allotment.
|
|
183
|
-
if (request.time && request.time.endsAt - Date.now() < MIN_ATTEMPT_MS) break;
|
|
184
120
|
}
|
|
185
121
|
const edited = edits.list();
|
|
186
122
|
return edited.length > 0 ? { ...last!, edited } : last!;
|
|
@@ -206,7 +142,6 @@ function baseRun(request: AgentRequest, runId: string, startedAt: string, attemp
|
|
|
206
142
|
...(request.thinking ? { thinking: request.thinking } : {}),
|
|
207
143
|
...(request.routedFrom ? { routedFrom: request.routedFrom } : {}),
|
|
208
144
|
...(request.route ? { route: request.route } : {}),
|
|
209
|
-
...(request.time ? { allotMs: request.time.allotMs, endsAt: request.time.endsAt } : {}),
|
|
210
145
|
...(attempts > 1 ? { note: `retry ${attempts - 1} of ${(request.retries ?? 0)}`, noteKind: "warning" as const } : {}),
|
|
211
146
|
};
|
|
212
147
|
}
|
|
@@ -225,30 +160,18 @@ function toolsFor(request: AgentRequest): readonly string[] | undefined {
|
|
|
225
160
|
async function runAgentOnce(request: AgentRequest, run: ProcessRunner, attempt: number, startedAt: string, edits: EditLog, quota: { used: number }): Promise<AgentRun> {
|
|
226
161
|
const base = baseRun(request, `${request.taskId}:${request.domain}:${request.role}:${randomUUID()}`, startedAt, attempt);
|
|
227
162
|
request.onUpdate?.(base);
|
|
228
|
-
const time = request.time!;
|
|
229
163
|
const attemptStart = Date.now();
|
|
230
164
|
let pausedMs = 0;
|
|
231
165
|
let asking = 0;
|
|
232
166
|
let askStarted = 0;
|
|
233
|
-
let expired = false;
|
|
234
167
|
const elapsed = () => Math.max(0, Date.now() - attemptStart - pausedMs - (asking ? Date.now() - askStarted : 0));
|
|
235
|
-
const live = createLiveRun(base, {
|
|
168
|
+
const live = createLiveRun(base, { onUpdate: (update) => request.onUpdate?.({ ...update, activeMs: elapsed() }) }, edits);
|
|
236
169
|
|
|
237
170
|
const controller = new AbortController();
|
|
238
171
|
activeControllers.add(controller);
|
|
239
172
|
const signal = request.signal ? AbortSignal.any([request.signal, controller.signal]) : controller.signal;
|
|
240
|
-
let deadline: ReturnType<typeof setTimeout>;
|
|
241
|
-
const arm = () => {
|
|
242
|
-
clearTimeout(deadline);
|
|
243
|
-
deadline = setTimeout(() => { expired = true; controller.abort(new Error("Assignment time limit reached")); }, Math.max(0, time.endsAt - Date.now()));
|
|
244
|
-
};
|
|
245
|
-
arm();
|
|
246
173
|
try {
|
|
247
174
|
const systemPrompt = request.systemPrompt ?? compilePrompt({ domain: request.domain, role: request.role, ...request.context });
|
|
248
|
-
const left = Math.max(1, time.endsAt - Date.now());
|
|
249
|
-
const reportMs = Math.min(REPORT_GRACE_MS, Math.floor(left / 4));
|
|
250
|
-
const wrapUp = Math.floor(left * (request.wrapUpAt || 0.75));
|
|
251
|
-
const onTimeUp = time?.onTimeUp;
|
|
252
175
|
const result = await runPiAgent({
|
|
253
176
|
cwd: request.cwd,
|
|
254
177
|
agent: { runId: base.runId, taskId: request.taskId, domain: request.domain, role: request.role },
|
|
@@ -261,88 +184,56 @@ async function runAgentOnce(request: AgentRequest, run: ProcessRunner, attempt:
|
|
|
261
184
|
request.onUpdate?.(child);
|
|
262
185
|
},
|
|
263
186
|
mcpTools: request.mcpTools,
|
|
187
|
+
...(request.skills ? { skills: request.skills } : {}),
|
|
264
188
|
...(request.autoCompact === false ? { autoCompact: false } : {}),
|
|
265
189
|
task: request.instruction,
|
|
266
190
|
systemPrompt,
|
|
267
191
|
tools: toolsFor(request),
|
|
268
192
|
model: request.model,
|
|
269
193
|
thinking: request.thinking,
|
|
270
|
-
timeoutMs: left,
|
|
271
|
-
...(time ? {
|
|
272
|
-
time: {
|
|
273
|
-
hardLimit: true,
|
|
274
|
-
upAtMs: Math.max(1, left - reportMs),
|
|
275
|
-
headsUpAtMs: left >= 4 * 60_000 ? Math.floor(left * 0.75) : 0,
|
|
276
|
-
headsUpMessage: headsUpMessage(left - Math.floor(left * 0.75), time.allotMs),
|
|
277
|
-
upMessage: `bot-lobby: your shared step allowance is nearly spent. Start nothing new; finish or revert the edit in progress and report completed work, verification, failed approaches, blockers, and the next action now.${onTimeUp ? " Include ## Left Off and ## More Time (minutes and reason). Only the user can grant more time." : ""}`,
|
|
278
|
-
graceMs: reportMs,
|
|
279
|
-
...(onTimeUp ? {
|
|
280
|
-
onTimeUp: async (report: string) => {
|
|
281
|
-
clearTimeout(deadline);
|
|
282
|
-
const waiting = Date.now();
|
|
283
|
-
let extraMs: number;
|
|
284
|
-
try { extraMs = await onTimeUp(live.current(), report); }
|
|
285
|
-
finally { pausedMs += Date.now() - waiting; }
|
|
286
|
-
time.endsAt += Date.now() - waiting;
|
|
287
|
-
if (extraMs > 0) {
|
|
288
|
-
time.endsAt = Date.now() + extraMs;
|
|
289
|
-
live.annotate({ note: `given ${formatMinutes(extraMs)} more`, noteKind: "info" });
|
|
290
|
-
}
|
|
291
|
-
arm();
|
|
292
|
-
return { extraMs, message: carryOnMessage(extraMs) };
|
|
293
|
-
},
|
|
294
|
-
} : {}),
|
|
295
|
-
},
|
|
296
|
-
} : {}),
|
|
297
194
|
signal,
|
|
298
195
|
...(request.onAsk ? { onAsk: request.onAsk } : {}),
|
|
196
|
+
...(request.harness ? { harness: request.harness } : {}),
|
|
299
197
|
env: request.onAsk ? { ...request.env, [ASK_ENV]: "1" } : request.env,
|
|
300
198
|
stallTimeoutMs: request.stallTimeoutMs,
|
|
301
199
|
toolStallTimeoutMs: request.toolStallTimeoutMs,
|
|
302
|
-
wrapUpAtMs: wrapUp,
|
|
303
200
|
onEvent: (event) => {
|
|
304
|
-
if (event.type === "asking") { if (!asking) askStarted = Date.now(); asking += 1;
|
|
201
|
+
if (event.type === "asking") { if (!asking) askStarted = Date.now(); asking += 1; }
|
|
305
202
|
if (event.type === "answered") { asking = 0; pausedMs += event.waitedMs; }
|
|
306
203
|
live.onEvent(event);
|
|
307
|
-
if (event.type === "answered" && asking === 0) arm();
|
|
308
204
|
},
|
|
309
205
|
onStart: (handle) => request.onStart?.({ runId: base.runId, steer: handle.steer, annotate: live.annotate }),
|
|
310
206
|
}, run);
|
|
311
207
|
const current = live.current();
|
|
312
208
|
const final: AgentRun = {
|
|
313
209
|
...base,
|
|
314
|
-
status:
|
|
210
|
+
status: result.status,
|
|
315
211
|
output: result.output,
|
|
316
|
-
error:
|
|
212
|
+
error: result.error ? describeError(result.error, current, result.stalled === true) : undefined,
|
|
317
213
|
usage: result.usage,
|
|
318
214
|
finishedAt: new Date().toISOString(),
|
|
319
|
-
activeMs:
|
|
215
|
+
activeMs: elapsed(),
|
|
320
216
|
turns: current.turns ?? (result.usage.turns || undefined),
|
|
321
217
|
tools: current.tools,
|
|
322
218
|
model: result.model ?? current.model,
|
|
323
219
|
...(result.stalled ? { stalled: true } : {}),
|
|
324
|
-
...(result.wrappedUp ? { wrappedUp: true } : {}),
|
|
325
|
-
...(result.timeUp || expired ? { timeUp: true } : {}),
|
|
326
|
-
...(result.extendedMs ? { extendedMs: result.extendedMs } : {}),
|
|
327
220
|
note: undefined,
|
|
328
221
|
noteKind: undefined,
|
|
329
222
|
};
|
|
330
223
|
request.onUpdate?.(final);
|
|
331
224
|
return final;
|
|
332
225
|
} catch (error) {
|
|
333
|
-
const final: AgentRun = { ...live.current(), status:
|
|
226
|
+
const final: AgentRun = { ...live.current(), status: signal.aborted ? "cancelled" : "failed", error: (error as Error).message, activeMs: elapsed(), edited: edits.list(), finishedAt: new Date().toISOString(), note: undefined, noteKind: undefined };
|
|
334
227
|
request.onUpdate?.(final);
|
|
335
228
|
return final;
|
|
336
229
|
} finally {
|
|
337
|
-
clearTimeout(deadline!);
|
|
338
|
-
time.usedMs = (time.usedMs ?? 0) + elapsed();
|
|
339
230
|
activeControllers.delete(controller);
|
|
340
231
|
}
|
|
341
232
|
}
|
|
342
233
|
|
|
343
|
-
/** Name what the agent was doing when a stall
|
|
234
|
+
/** Name what the agent was doing when a stall hit. */
|
|
344
235
|
function describeError(error: string, current: AgentRun, stalled: boolean): string {
|
|
345
|
-
if (!stalled
|
|
236
|
+
if (!stalled) return error;
|
|
346
237
|
const doing = current.activity ? ` while ${current.activity}${current.detail ? ` ${current.detail}` : ""}` : "";
|
|
347
238
|
return `${error}${doing}`;
|
|
348
239
|
}
|
|
@@ -23,7 +23,6 @@ export const EXPERT_TOOL = "ask_expert";
|
|
|
23
23
|
/** Consults one step may make: advice on the hard part, not a second worker. */
|
|
24
24
|
export const MAX_CONSULTS = 2;
|
|
25
25
|
/** Longest an expert may take to answer. */
|
|
26
|
-
export const EXPERT_TIMEOUT_MS = 5 * 60 * 1000;
|
|
27
26
|
/** Marks an expert's run, so its host gives it an adviser's note rather than a child's. */
|
|
28
27
|
export const EXPERT_ENV = "BOT_LOBBY_EXPERT";
|
|
29
28
|
|
|
@@ -85,7 +84,7 @@ export interface AgentPolicy {
|
|
|
85
84
|
depth: number;
|
|
86
85
|
}
|
|
87
86
|
|
|
88
|
-
/** The host owns quotas,
|
|
87
|
+
/** The host owns quotas, file identities and child lifetimes, outside model context. */
|
|
89
88
|
export async function delegationHost(options: PiRunOptions, run: ProcessRunner) {
|
|
90
89
|
const config = loadConfig();
|
|
91
90
|
const id = options.agent?.runId ?? randomUUID();
|
|
@@ -112,14 +111,11 @@ export async function delegationHost(options: PiRunOptions, run: ProcessRunner)
|
|
|
112
111
|
desk ??= ownedDesk;
|
|
113
112
|
const controller = new AbortController();
|
|
114
113
|
const lifetime = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
|
|
115
|
-
|
|
116
|
-
const childSignal = () => AbortSignal.any([lifetime, timeController.signal]);
|
|
114
|
+
const childSignal = () => lifetime;
|
|
117
115
|
const key = `${options.cwd}:${agent.taskId}`;
|
|
118
116
|
const pool = pools.get(key) ?? { active: 0, users: 0, waiters: [] };
|
|
119
117
|
pool.users += 1;
|
|
120
118
|
pools.set(key, pool);
|
|
121
|
-
let deadlineAt = Date.now() + (options.time?.upAtMs ?? options.timeoutMs);
|
|
122
|
-
let deadline = setTimeout(() => timeController.abort(), Math.max(1, deadlineAt - Date.now()));
|
|
123
119
|
const children = new Map<string, AgentRun>();
|
|
124
120
|
const pending = new Set<Promise<AgentRun>>();
|
|
125
121
|
const parentDeskId = options.env?.[DESK_ENV.worker] ?? id;
|
|
@@ -137,18 +133,16 @@ export async function delegationHost(options: PiRunOptions, run: ProcessRunner)
|
|
|
137
133
|
|
|
138
134
|
const childRun = async (brief: string, signal: AbortSignal): Promise<AgentRun> => {
|
|
139
135
|
const childId = randomUUID();
|
|
140
|
-
const base: AgentRun = { runId: childId, parentRunId: id, depth: 2, taskId: agent.taskId, domain: agent.domain, role: agent.role, instruction: brief, stepInstruction: options.task, status: "running",
|
|
136
|
+
const base: AgentRun = { runId: childId, parentRunId: id, depth: 2, taskId: agent.taskId, domain: agent.domain, role: agent.role, instruction: brief, stepInstruction: options.task, status: "running", output: "", attempts: 1, startedAt: new Date().toISOString(), ...(options.thinking ? { thinking: options.thinking } : {}) };
|
|
141
137
|
const edits = new EditLog(options.cwd);
|
|
142
|
-
const live = createLiveRun(base, { onUpdate: update
|
|
138
|
+
const live = createLiveRun(base, { onUpdate: update }, edits);
|
|
143
139
|
update(base);
|
|
144
140
|
try {
|
|
145
141
|
const configured = options.effortProfile ?? { model: options.model, thinking: options.thinking ?? "medium" };
|
|
146
142
|
const effort = options.effort ?? effortFor();
|
|
147
143
|
const route = await effort.childRoute?.(brief, configured, { context: options.task, signal }).catch(() => undefined);
|
|
148
144
|
const attempt = async (model: string | undefined, thinking: string) => {
|
|
149
|
-
const left = deadlineAt - Date.now();
|
|
150
145
|
signal.throwIfAborted();
|
|
151
|
-
if (left <= 0) throw new Error("The parent's time is up.");
|
|
152
146
|
const routed = route && model === route.model && thinking === route.thinking;
|
|
153
147
|
live.annotate({ model, thinking, route: routed ? routeLabel(route) : undefined, routedFrom: routed ? profileLabel(route.from) : undefined, note: routed ? routeLabel(route) : undefined, noteKind: "info" });
|
|
154
148
|
return runPiAgent({
|
|
@@ -159,10 +153,7 @@ export async function delegationHost(options: PiRunOptions, run: ProcessRunner)
|
|
|
159
153
|
thinking,
|
|
160
154
|
tools: [...new Set([...tools.filter((tool) => tool !== DELEGATE_TOOL && tool !== EXPERT_TOOL), ...(childDesk ? childDesk.tools().filter((tool) => !PEER_TOOLS.includes(tool)) : [])])],
|
|
161
155
|
mcpTools,
|
|
162
|
-
timeoutMs: left,
|
|
163
156
|
signal,
|
|
164
|
-
time: undefined,
|
|
165
|
-
wrapUpAtMs: Math.floor(left * 0.85),
|
|
166
157
|
delegationQuota: undefined,
|
|
167
158
|
onChildRun: undefined,
|
|
168
159
|
onEvent: live.onEvent,
|
|
@@ -172,7 +163,7 @@ export async function delegationHost(options: PiRunOptions, run: ProcessRunner)
|
|
|
172
163
|
};
|
|
173
164
|
let result = await attempt(route?.model ?? configured.model, route?.thinking ?? configured.thinking);
|
|
174
165
|
let retried = false;
|
|
175
|
-
if (route && (route.level === "simple" || route.level === "trivial") && fellShort(result) && !signal.aborted
|
|
166
|
+
if (route && (route.level === "simple" || route.level === "trivial") && fellShort(result) && !signal.aborted) {
|
|
176
167
|
retried = true;
|
|
177
168
|
result = await attempt(configured.model, configured.thinking);
|
|
178
169
|
}
|
|
@@ -180,7 +171,7 @@ export async function delegationHost(options: PiRunOptions, run: ProcessRunner)
|
|
|
180
171
|
update(final);
|
|
181
172
|
return final;
|
|
182
173
|
} catch (error) {
|
|
183
|
-
const final: AgentRun = { ...live.current(), edited: edits.list(), status:
|
|
174
|
+
const final: AgentRun = { ...live.current(), edited: edits.list(), status: signal.aborted ? "cancelled" : "failed", error: (error as Error).message, finishedAt: new Date().toISOString(), note: undefined, noteKind: undefined };
|
|
184
175
|
update(final);
|
|
185
176
|
return final;
|
|
186
177
|
} finally {
|
|
@@ -192,14 +183,12 @@ export async function delegationHost(options: PiRunOptions, run: ProcessRunner)
|
|
|
192
183
|
quota.consults ??= 0;
|
|
193
184
|
const consultations = new Map<string, AbortController>();
|
|
194
185
|
const consult = async (question: string, signal: AbortSignal): Promise<{ ok: boolean; text: string }> => {
|
|
195
|
-
const left = Math.min(EXPERT_TIMEOUT_MS, deadlineAt - Date.now());
|
|
196
|
-
if (left < 30_000) return { ok: false, text: "Too little of your time is left to consult; carry on with your best reading." };
|
|
197
186
|
// The oracle resolves the expert (its session model when settings name none); settings alone, then the worker's own, without it.
|
|
198
187
|
const settled = options.expert ?? expertProfile(config);
|
|
199
188
|
const expert = { model: settled.model ?? options.model, thinking: settled.thinking };
|
|
200
189
|
const expertId = randomUUID();
|
|
201
|
-
const base: AgentRun = { runId: expertId, parentRunId: id, depth: 2, taskId: agent.taskId, domain: agent.domain, role: "reviewer", instruction: question, stepInstruction: options.task, status: "running",
|
|
202
|
-
const live = createLiveRun(base, { onUpdate: update
|
|
190
|
+
const base: AgentRun = { runId: expertId, parentRunId: id, depth: 2, taskId: agent.taskId, domain: agent.domain, role: "reviewer", instruction: question, stepInstruction: options.task, status: "running", output: "", attempts: 1, startedAt: new Date().toISOString(), thinking: expert.thinking, ...(expert.model ? { model: expert.model } : {}), note: "expert consult", noteKind: "info" };
|
|
191
|
+
const live = createLiveRun(base, { onUpdate: update }, new EditLog(options.cwd));
|
|
203
192
|
update(base);
|
|
204
193
|
let final: AgentRun;
|
|
205
194
|
try {
|
|
@@ -212,8 +201,6 @@ export async function delegationHost(options: PiRunOptions, run: ProcessRunner)
|
|
|
212
201
|
mcpTools: [],
|
|
213
202
|
...(expert.model ? { model: expert.model } : {}),
|
|
214
203
|
thinking: expert.thinking,
|
|
215
|
-
timeoutMs: left,
|
|
216
|
-
wrapUpAtMs: Math.floor(left * 0.8),
|
|
217
204
|
...(options.stallTimeoutMs !== undefined ? { stallTimeoutMs: options.stallTimeoutMs } : {}),
|
|
218
205
|
...(options.toolStallTimeoutMs !== undefined ? { toolStallTimeoutMs: options.toolStallTimeoutMs } : {}),
|
|
219
206
|
signal,
|
|
@@ -275,7 +262,6 @@ export async function delegationHost(options: PiRunOptions, run: ProcessRunner)
|
|
|
275
262
|
const results = await Promise.all(runs);
|
|
276
263
|
return { ok: true, text: JSON.stringify(results.map((child) => ({ runId: child.runId, status: child.status, report: child.output.slice(0, 12_000), reportPath: join(dataRoot(detectProjectRoot(options.cwd, CONFIG_DIR_NAME), CONFIG_DIR_NAME), "delegations", `${child.runId}.json`), error: child.error, usage: child.usage }))) };
|
|
277
264
|
}, () => controller.abort()).catch(async (error) => {
|
|
278
|
-
clearTimeout(deadline);
|
|
279
265
|
if (ownedDesk) await ownedDesk.close();
|
|
280
266
|
pool.users -= 1;
|
|
281
267
|
if (pool.users === 0) pools.delete(key);
|
|
@@ -286,25 +272,17 @@ export async function delegationHost(options: PiRunOptions, run: ProcessRunner)
|
|
|
286
272
|
return {
|
|
287
273
|
tools: desk ? [...new Set([...tools, ...desk.tools()])] : tools,
|
|
288
274
|
mcpTools,
|
|
289
|
-
env: { ...options.env, ...(desk ? desk.env(parentDeskId) : {}), [AGENT_ENV]: JSON.stringify({ ...policy, tools: desk ? [...new Set([...tools, ...desk.tools()])] : tools }), [DELEGATION_ENV]: JSON.stringify({ address: server.address, token
|
|
290
|
-
note: `${consults ? `When you are stuck on a hard step after a real attempt, or about to make a choice you are unsure of (a tricky algorithm, a subtle bug, an unfamiliar API), ${EXPERT_TOOL} asks a stronger model for advice, at most ${MAX_CONSULTS} times; it reads the repository and answers, you still do the work. ` : ""}${canDelegate(agent, quota.used) ? `Work directly when you can finish the brief yourself. Use ${DELEGATE_TOOL} only for useful, separable work with disjoint files, at most five children total; children
|
|
275
|
+
env: { ...options.env, ...(desk ? desk.env(parentDeskId) : {}), [AGENT_ENV]: JSON.stringify({ ...policy, tools: desk ? [...new Set([...tools, ...desk.tools()])] : tools }), [DELEGATION_ENV]: JSON.stringify({ address: server.address, token }) },
|
|
276
|
+
note: `${consults ? `When you are stuck on a hard step after a real attempt, or about to make a choice you are unsure of (a tricky algorithm, a subtle bug, an unfamiliar API), ${EXPERT_TOOL} asks a stronger model for advice, at most ${MAX_CONSULTS} times; it reads the repository and answers, you still do the work. ` : ""}${canDelegate(agent, quota.used) ? `Work directly when you can finish the brief yourself. Use ${DELEGATE_TOOL} only for useful, separable work with disjoint files, at most five children total; children cannot delegate further. Integrate and verify their work yourself. ` : ""}Use codemode for concise parallel calls, inspect failures, and return only useful evidence.${desk?.usesClaims ? " Before edit/write, claim_file; handover_file when finished. Release files before delegating or waiting." : ""}`,
|
|
291
277
|
onStart: (handle: Parameters<NonNullable<PiRunOptions["onStart"]>>[0]) => {
|
|
292
278
|
desk?.attach(parentDeskId, { runId: id, steer: handle.steer, annotate: () => {} });
|
|
293
279
|
options.onStart?.(handle);
|
|
294
280
|
},
|
|
295
281
|
get children() { return [...children.values()]; },
|
|
296
|
-
pause() { clearTimeout(deadline); },
|
|
297
|
-
extend(ms: number, reset = false) {
|
|
298
|
-
deadlineAt = reset ? Date.now() + ms : deadlineAt + ms;
|
|
299
|
-
clearTimeout(deadline);
|
|
300
|
-
if (timeController.signal.aborted) timeController = new AbortController();
|
|
301
|
-
deadline = setTimeout(() => timeController.abort(), Math.max(1, deadlineAt - Date.now()));
|
|
302
|
-
},
|
|
303
282
|
async close() {
|
|
304
283
|
if (closed) return;
|
|
305
284
|
closed = true;
|
|
306
285
|
controller.abort();
|
|
307
|
-
clearTimeout(deadline);
|
|
308
286
|
await Promise.allSettled([...pending]);
|
|
309
287
|
await server.close();
|
|
310
288
|
if (ownedDesk) { ownedDesk.release(parentDeskId, () => "Parent finished; re-read before editing."); await ownedDesk.close(); }
|
|
@@ -3,7 +3,7 @@ import type { AgentRun } from "../schemas/findings.ts";
|
|
|
3
3
|
import type { PiStreamEvent } from "./pi-runner.ts";
|
|
4
4
|
import { EditLog } from "../state/changes.ts";
|
|
5
5
|
import { activityDetail, activityWord, describeToolCall } from "../pi/activity.ts";
|
|
6
|
-
import {
|
|
6
|
+
import { truncate } from "../text.ts";
|
|
7
7
|
|
|
8
8
|
const THROTTLE_MS = 2000;
|
|
9
9
|
|
|
@@ -12,7 +12,7 @@ const THROTTLE_MS = 2000;
|
|
|
12
12
|
* `onUpdate`: activity changes, retries and notes immediately, counters and
|
|
13
13
|
* heartbeats at most every couple of seconds.
|
|
14
14
|
*/
|
|
15
|
-
export function createLiveRun(base: AgentRun, request: Pick<AgentRequest, "onUpdate"
|
|
15
|
+
export function createLiveRun(base: AgentRun, request: Pick<AgentRequest, "onUpdate">, edits: EditLog) {
|
|
16
16
|
let state: AgentRun = base;
|
|
17
17
|
let lastEmit = 0;
|
|
18
18
|
const emit = (patch: Partial<AgentRun>, force: boolean) => {
|
|
@@ -55,22 +55,11 @@ export function createLiveRun(base: AgentRun, request: Pick<AgentRequest, "onUpd
|
|
|
55
55
|
case "compaction_end":
|
|
56
56
|
emit({ note: event.error, noteKind: event.error ? "warning" : undefined }, true);
|
|
57
57
|
return;
|
|
58
|
-
case "wrap_up":
|
|
59
|
-
emit({ note: `asked to wrap up (${shortDuration(request.timeoutMs)} limit)`, noteKind: "warning", wrappedUp: true }, true);
|
|
60
|
-
return;
|
|
61
|
-
case "time_up":
|
|
62
|
-
emit({ note: request.time?.onTimeUp ? "out of time: reporting where it left off" : "out of time: reporting", noteKind: "warning" }, true);
|
|
63
|
-
return;
|
|
64
|
-
case "extended":
|
|
65
|
-
emit({ endsAt: request.time?.endsAt, extendedMs: (state.extendedMs ?? 0) + event.ms }, true);
|
|
66
|
-
return;
|
|
67
58
|
case "asking":
|
|
68
59
|
emit({ note: `waiting on you: ${event.questions} question${event.questions === 1 ? "" : "s"}`, noteKind: "info" }, true);
|
|
69
60
|
return;
|
|
70
61
|
case "answered":
|
|
71
|
-
|
|
72
|
-
if (request.time) request.time.endsAt += event.waitedMs;
|
|
73
|
-
emit({ note: "you answered; carrying on", noteKind: "info", ...(request.time ? { endsAt: request.time.endsAt } : {}) }, true);
|
|
62
|
+
emit({ note: "you answered; carrying on", noteKind: "info" }, true);
|
|
74
63
|
return;
|
|
75
64
|
case "usage": {
|
|
76
65
|
const usage = state.usage ?? { input: 0, output: 0, cost: 0, turns: 0 };
|