@oharness/server 0.7.0 → 0.7.1
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/dist/i18n.js +12 -0
- package/dist/i18n.js.map +1 -1
- package/dist/page.js +311 -15
- package/dist/page.js.map +1 -1
- package/dist/server.js +163 -4
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
package/dist/server.js
CHANGED
|
@@ -204,7 +204,7 @@ export async function startWebServer(opts) {
|
|
|
204
204
|
const makeRunner = (session, harness) => {
|
|
205
205
|
const ledger = new ChangeLedger(session.cwd);
|
|
206
206
|
ledger.attach(harness.hooks);
|
|
207
|
-
return { session, agent: harness.agent(session), harness, ledger };
|
|
207
|
+
return { session, agent: harness.agent(session), harness, ledger, queue: [] };
|
|
208
208
|
};
|
|
209
209
|
/** The runner a request names, or the most recently focused one. */
|
|
210
210
|
let focused = opts.session.id;
|
|
@@ -2403,9 +2403,6 @@ export async function startWebServer(opts) {
|
|
|
2403
2403
|
if (!prompt && !ids.length && !references.length) {
|
|
2404
2404
|
return send(res, 400, { error: "prompt is required" });
|
|
2405
2405
|
}
|
|
2406
|
-
// Per runner, not global: a turn in one tab must not block another.
|
|
2407
|
-
if (runner.running)
|
|
2408
|
-
return send(res, 409, { error: "a turn is already running" });
|
|
2409
2406
|
const staged = ids
|
|
2410
2407
|
.map((id) => (typeof id === "string" ? attachments.get(id) : undefined))
|
|
2411
2408
|
.filter((entry) => Boolean(entry));
|
|
@@ -2416,6 +2413,19 @@ export async function startWebServer(opts) {
|
|
|
2416
2413
|
// a composer is known to have moved on, so it is a good moment to ask
|
|
2417
2414
|
// what else has been abandoned.
|
|
2418
2415
|
sweepAttachments();
|
|
2416
|
+
// Per runner, not global: a turn in one tab must not block another.
|
|
2417
|
+
// A second prompt into a busy runner waits, and is shown waiting. It
|
|
2418
|
+
// is not handed to the running turn on its own: putting words in front
|
|
2419
|
+
// of a model mid-thought is a thing to choose, not a thing that
|
|
2420
|
+
// happens because you typed fast. /sendnow is that choice.
|
|
2421
|
+
//
|
|
2422
|
+
// This used to 409, and the page discarded the reply, so an impatient
|
|
2423
|
+
// Enter cleared the composer and lost what had been typed.
|
|
2424
|
+
if (runner.running) {
|
|
2425
|
+
runner.queue.push({ id: randomUUID(), prompt, staged, references });
|
|
2426
|
+
broadcastQueue(runner);
|
|
2427
|
+
return send(res, 202, { ok: true, queued: true });
|
|
2428
|
+
}
|
|
2419
2429
|
void runTurn(runner, prompt, staged, references);
|
|
2420
2430
|
return send(res, 202, { ok: true, attached: staged.length, referenced: references.length });
|
|
2421
2431
|
}
|
|
@@ -2451,6 +2461,84 @@ export async function startWebServer(opts) {
|
|
|
2451
2461
|
runnerFor(body.session)?.running?.abort();
|
|
2452
2462
|
}
|
|
2453
2463
|
return send(res, 200, { ok: true });
|
|
2464
|
+
case "POST /unqueue": {
|
|
2465
|
+
const body = await readJson(req);
|
|
2466
|
+
const runner = runnerFor(body.session);
|
|
2467
|
+
if (!runner)
|
|
2468
|
+
return send(res, 404, { error: "no such session" });
|
|
2469
|
+
// Either kind: one handed to the running turn but not yet read, or
|
|
2470
|
+
// one still waiting for the turn to end.
|
|
2471
|
+
if (typeof body.id === "string" && runner.agent.retract(body.id)) {
|
|
2472
|
+
broadcastQueue(runner);
|
|
2473
|
+
return send(res, 200, { ok: true });
|
|
2474
|
+
}
|
|
2475
|
+
const at = runner.queue.findIndex((entry) => entry.id === body.id);
|
|
2476
|
+
if (at >= 0) {
|
|
2477
|
+
// The bytes came out of the staging map when this was queued, so
|
|
2478
|
+
// they are held by the entry alone and go when it does.
|
|
2479
|
+
runner.queue.splice(at, 1);
|
|
2480
|
+
broadcastQueue(runner);
|
|
2481
|
+
}
|
|
2482
|
+
return send(res, 200, { ok: true });
|
|
2483
|
+
}
|
|
2484
|
+
/**
|
|
2485
|
+
* One waiting message, now, without waiting for its place in the queue.
|
|
2486
|
+
*
|
|
2487
|
+
* While a turn runs that means handing it to the turn — the same path a
|
|
2488
|
+
* prompt typed mid-turn takes — so it is read at the next gap rather
|
|
2489
|
+
* than after the whole thing. Nothing is interrupted: "now" here is as
|
|
2490
|
+
* soon as the conversation has a place to put it.
|
|
2491
|
+
*
|
|
2492
|
+
* A message carrying files cannot ride the inbox, so it moves to the
|
|
2493
|
+
* head of the queue instead and the reply says so.
|
|
2494
|
+
*/
|
|
2495
|
+
case "POST /sendnow": {
|
|
2496
|
+
const body = await readJson(req);
|
|
2497
|
+
const runner = runnerFor(body.session);
|
|
2498
|
+
if (!runner)
|
|
2499
|
+
return send(res, 404, { error: "no such session" });
|
|
2500
|
+
const at = runner.queue.findIndex((entry) => entry.id === body.id);
|
|
2501
|
+
if (at < 0)
|
|
2502
|
+
return send(res, 404, { error: "no such queued message" });
|
|
2503
|
+
const entry = runner.queue[at];
|
|
2504
|
+
if (!runner.running) {
|
|
2505
|
+
runner.queue.splice(at, 1);
|
|
2506
|
+
broadcastQueue(runner);
|
|
2507
|
+
void runTurn(runner, entry.prompt, entry.staged, entry.references);
|
|
2508
|
+
return send(res, 202, { ok: true, sent: true });
|
|
2509
|
+
}
|
|
2510
|
+
const plain = entry.prompt && !entry.staged.length && !entry.references.length;
|
|
2511
|
+
if (plain && runner.agent.interject(entry.prompt)) {
|
|
2512
|
+
runner.queue.splice(at, 1);
|
|
2513
|
+
broadcastQueue(runner);
|
|
2514
|
+
return send(res, 202, { ok: true, interjected: true });
|
|
2515
|
+
}
|
|
2516
|
+
// Cannot be handed over, so the most that can be done is to put it
|
|
2517
|
+
// first in line for when the turn ends.
|
|
2518
|
+
runner.queue.splice(at, 1);
|
|
2519
|
+
runner.queue.unshift(entry);
|
|
2520
|
+
broadcastQueue(runner);
|
|
2521
|
+
return send(res, 200, { ok: true, moved: true });
|
|
2522
|
+
}
|
|
2523
|
+
/**
|
|
2524
|
+
* Send what is waiting, now.
|
|
2525
|
+
*
|
|
2526
|
+
* The queue drains itself after a turn that ends on its own, but not
|
|
2527
|
+
* after an interrupt or an error — stopping means stop, and a message
|
|
2528
|
+
* typed before the failure should not fire into whatever caused it.
|
|
2529
|
+
* This is how the page restarts a queue left standing that way.
|
|
2530
|
+
*/
|
|
2531
|
+
case "POST /flush": {
|
|
2532
|
+
const body = await readJson(req);
|
|
2533
|
+
const runner = runnerFor(body.session);
|
|
2534
|
+
if (!runner)
|
|
2535
|
+
return send(res, 404, { error: "no such session" });
|
|
2536
|
+
if (runner.running)
|
|
2537
|
+
return send(res, 409, { error: "a turn is already running" });
|
|
2538
|
+
if (!drainQueue(runner))
|
|
2539
|
+
return send(res, 200, { ok: true, sent: false });
|
|
2540
|
+
return send(res, 202, { ok: true, sent: true });
|
|
2541
|
+
}
|
|
2454
2542
|
default:
|
|
2455
2543
|
return send(res, 404, { error: "not found" });
|
|
2456
2544
|
}
|
|
@@ -2669,6 +2757,9 @@ export async function startWebServer(opts) {
|
|
|
2669
2757
|
contextWindow: model.contextWindow,
|
|
2670
2758
|
contextUsed: model.contextWindow ? Math.min(1, used / model.contextWindow) : undefined,
|
|
2671
2759
|
busy: Boolean(runner.running),
|
|
2760
|
+
// So a reload, or a second tab, sees what is still waiting. The queue
|
|
2761
|
+
// lives on the runner rather than in the page for exactly this reason.
|
|
2762
|
+
queued: queueView(runner),
|
|
2672
2763
|
};
|
|
2673
2764
|
}
|
|
2674
2765
|
/**
|
|
@@ -2885,6 +2976,46 @@ export async function startWebServer(opts) {
|
|
|
2885
2976
|
});
|
|
2886
2977
|
return snapshot;
|
|
2887
2978
|
}
|
|
2979
|
+
/**
|
|
2980
|
+
* What the page needs to draw the waiting chips, without the bytes.
|
|
2981
|
+
*
|
|
2982
|
+
* Interjections first, and marked as such: they reach the model during the
|
|
2983
|
+
* running turn, while the queue waits for it to finish. Drawn as one list
|
|
2984
|
+
* because that is the order the conversation will read them in.
|
|
2985
|
+
*/
|
|
2986
|
+
const queueView = (runner) => [
|
|
2987
|
+
...runner.agent.pendingInterjections.map((entry) => ({
|
|
2988
|
+
id: entry.id,
|
|
2989
|
+
text: entry.text,
|
|
2990
|
+
attachments: [],
|
|
2991
|
+
references: [],
|
|
2992
|
+
soon: true,
|
|
2993
|
+
})),
|
|
2994
|
+
...runner.queue.map((entry) => ({
|
|
2995
|
+
id: entry.id,
|
|
2996
|
+
text: entry.prompt,
|
|
2997
|
+
attachments: entry.staged.map((file) => file.name),
|
|
2998
|
+
references: entry.references,
|
|
2999
|
+
soon: false,
|
|
3000
|
+
})),
|
|
3001
|
+
];
|
|
3002
|
+
const broadcastQueue = (runner) => {
|
|
3003
|
+
broadcast({ type: "queue", session: runner.session.id, queued: queueView(runner) });
|
|
3004
|
+
};
|
|
3005
|
+
/**
|
|
3006
|
+
* Start the next waiting prompt, if there is one.
|
|
3007
|
+
*
|
|
3008
|
+
* Returns whether a turn was started, so a caller can tell an empty queue
|
|
3009
|
+
* from one that is now running.
|
|
3010
|
+
*/
|
|
3011
|
+
function drainQueue(runner) {
|
|
3012
|
+
const next = runner.queue.shift();
|
|
3013
|
+
if (!next)
|
|
3014
|
+
return false;
|
|
3015
|
+
broadcastQueue(runner);
|
|
3016
|
+
void runTurn(runner, next.prompt, next.staged, next.references);
|
|
3017
|
+
return true;
|
|
3018
|
+
}
|
|
2888
3019
|
async function runTurn(runner, prompt, staged = [], references = []) {
|
|
2889
3020
|
const controller = new AbortController();
|
|
2890
3021
|
runner.running = controller;
|
|
@@ -2907,6 +3038,7 @@ export async function startWebServer(opts) {
|
|
|
2907
3038
|
const note = referenceNote(references);
|
|
2908
3039
|
const text = note ? `${note}\n\n${prompt}` : prompt;
|
|
2909
3040
|
const input = staged.length ? buildMessage(text, staged) : text;
|
|
3041
|
+
let failed = false;
|
|
2910
3042
|
try {
|
|
2911
3043
|
for await (const event of runner.agent.run(input, { signal: controller.signal })) {
|
|
2912
3044
|
// The agent's own question_request is not forwarded: the page draws
|
|
@@ -2915,10 +3047,25 @@ export async function startWebServer(opts) {
|
|
|
2915
3047
|
// the copy without that id could only ever 404.
|
|
2916
3048
|
if (event.type === "question_request")
|
|
2917
3049
|
continue;
|
|
3050
|
+
// The agent reports a provider failure as an event and breaks its own
|
|
3051
|
+
// loop rather than throwing, so the catch below never sees it — and a
|
|
3052
|
+
// queue that drained into a dead provider would replay the failure
|
|
3053
|
+
// once per waiting message.
|
|
3054
|
+
if (event.type === "error" && event.fatal)
|
|
3055
|
+
failed = true;
|
|
3056
|
+
// Read into the conversation: the chip goes, and the words appear in
|
|
3057
|
+
// the transcript where the model saw them rather than where they were
|
|
3058
|
+
// typed. `prompt` is what the page draws a user message from.
|
|
3059
|
+
if (event.type === "interjected") {
|
|
3060
|
+
broadcastQueue(runner);
|
|
3061
|
+
broadcast({ type: "prompt", session: id, text: event.text, interjected: true });
|
|
3062
|
+
continue;
|
|
3063
|
+
}
|
|
2918
3064
|
broadcast({ ...event, session: id });
|
|
2919
3065
|
}
|
|
2920
3066
|
}
|
|
2921
3067
|
catch (err) {
|
|
3068
|
+
failed = true;
|
|
2922
3069
|
broadcast({
|
|
2923
3070
|
type: "error",
|
|
2924
3071
|
session: id,
|
|
@@ -2942,12 +3089,24 @@ export async function startWebServer(opts) {
|
|
|
2942
3089
|
}
|
|
2943
3090
|
finally {
|
|
2944
3091
|
runner.running = undefined;
|
|
3092
|
+
// The window between the loop's last read of the inbox and the run
|
|
3093
|
+
// admitting it is over. Whatever landed in it is put at the head of the
|
|
3094
|
+
// queue rather than left unread inside a finished agent.
|
|
3095
|
+
const unread = runner.agent.takeUnread();
|
|
3096
|
+
for (let i = unread.length - 1; i >= 0; i--) {
|
|
3097
|
+
runner.queue.unshift({ id: unread[i].id, prompt: unread[i].text, staged: [], references: [] });
|
|
3098
|
+
}
|
|
2945
3099
|
broadcast({
|
|
2946
3100
|
type: "idle",
|
|
2947
3101
|
session: id,
|
|
2948
3102
|
state: state(runner),
|
|
2949
3103
|
changes: runner.ledger.list(),
|
|
2950
3104
|
});
|
|
3105
|
+
// Only after a turn that ended on its own. An interrupt means stop, and
|
|
3106
|
+
// an error would usually meet the next prompt with the same error, so
|
|
3107
|
+
// both leave the queue standing for the person to send or drop.
|
|
3108
|
+
if (!controller.signal.aborted && !failed)
|
|
3109
|
+
drainQueue(runner);
|
|
2951
3110
|
}
|
|
2952
3111
|
}
|
|
2953
3112
|
function stream(res) {
|