@ordewell/web 0.9.1 → 0.10.0
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/server/main.js +210 -94
- package/dist/server/main.js.map +1 -1
- package/package.json +2 -2
package/dist/server/main.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
var hono = require('hono');
|
|
5
|
+
var child_process = require('child_process');
|
|
5
6
|
var core = require('@ordewell/core');
|
|
6
7
|
var fs = require('fs');
|
|
7
8
|
var path = require('path');
|
|
8
|
-
var child_process = require('child_process');
|
|
9
9
|
var ws = require('ws');
|
|
10
10
|
var http = require('http');
|
|
11
11
|
|
|
@@ -304,17 +304,8 @@ var PoolFileSystem = class extends core.BaseFileSystem {
|
|
|
304
304
|
};
|
|
305
305
|
}
|
|
306
306
|
async listDirImpl(absPath, depth) {
|
|
307
|
-
if (depth > 1) {
|
|
308
|
-
const result = await run("tree", ["-L", String(depth), "-I", "node_modules|.git|dist|build", absPath], { timeout: 1e4, env: this.searchEnv() });
|
|
309
|
-
if (result.code === 0 && result.stdout.trim()) {
|
|
310
|
-
const lines = result.stdout.trim().split("\n");
|
|
311
|
-
return { success: true, output: lines.slice(0, 300).join("\n"), truncated: lines.length > 300 };
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
307
|
try {
|
|
315
|
-
|
|
316
|
-
const lines = entries.slice(0, 200).map((e) => `${e.isDirectory() ? "D" : "F"} ${e.name}`);
|
|
317
|
-
return { success: true, output: lines.join("\n") || "(empty directory)", truncated: entries.length > 200 };
|
|
308
|
+
return await core.listTree(absPath, depth, async (abs) => (await fs.promises.readdir(abs, { withFileTypes: true })).map((e) => ({ name: e.name, directory: e.isDirectory() })));
|
|
318
309
|
} catch (err) {
|
|
319
310
|
return { success: false, output: `Error listing ${absPath}: ${err.message}`, truncated: false };
|
|
320
311
|
}
|
|
@@ -388,11 +379,22 @@ var OrchestratorPool = class {
|
|
|
388
379
|
settingsService = new core.SettingsService();
|
|
389
380
|
plannerModelMemory = new core.PlannerModelMemory(this.settingsService);
|
|
390
381
|
runner;
|
|
382
|
+
/** One probe per daemon, asked again whenever the runner list is refreshed (ADR-0033). */
|
|
383
|
+
codexSandbox;
|
|
391
384
|
constructor(deps = {}) {
|
|
392
385
|
const pluginNotice = core.removedPluginNotice();
|
|
393
386
|
if (pluginNotice) console.warn(pluginNotice);
|
|
394
387
|
this.runner = deps.runner ?? new core.StructuredRunner();
|
|
395
388
|
this.modelResolver = deps.modelResolver ?? new core.ModelResolver(this.registry, new WebConfig());
|
|
389
|
+
this.codexSandbox = deps.codexSandbox ?? new core.CodexSandboxAvailability(this.registry, { spawn: child_process.spawn, fetch: globalThis.fetch }, { cwd: process.cwd(), env: process.env });
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Not awaited: planning must start at once, and the daemon already warms the
|
|
393
|
+
* probe at startup through the catalog. This only covers a daemon that was
|
|
394
|
+
* asked to plan before that finished.
|
|
395
|
+
*/
|
|
396
|
+
warmCodexSandbox() {
|
|
397
|
+
this.codexSandbox.ensure().catch((err) => console.error(`[web] codex sandbox probe failed: ${err instanceof Error ? err.message : err}`));
|
|
396
398
|
}
|
|
397
399
|
subscribe(sessionId, ws) {
|
|
398
400
|
if (!this.clients.has(sessionId)) this.clients.set(sessionId, /* @__PURE__ */ new Set());
|
|
@@ -438,6 +440,10 @@ var OrchestratorPool = class {
|
|
|
438
440
|
getGoal(sessionId) {
|
|
439
441
|
return this.sessions.get(sessionId)?.currentGoal ?? "";
|
|
440
442
|
}
|
|
443
|
+
/** Null for a session this process is not holding: there is no run to describe. */
|
|
444
|
+
runSnapshot(sessionId) {
|
|
445
|
+
return this.sessions.get(sessionId)?.runSnapshot() ?? null;
|
|
446
|
+
}
|
|
441
447
|
runtimeSettings() {
|
|
442
448
|
return core.sessionRuntimeSettings(this.settingsService.getAll());
|
|
443
449
|
}
|
|
@@ -514,11 +520,23 @@ var OrchestratorPool = class {
|
|
|
514
520
|
modelAllowlist: userSettings.modelAllowlist,
|
|
515
521
|
// The model remembered per planner backend (this task), so a surface can
|
|
516
522
|
// render what's remembered without a second round-trip.
|
|
517
|
-
plannerModels: userSettings.plannerModels
|
|
523
|
+
plannerModels: userSettings.plannerModels,
|
|
524
|
+
run: this.settingsService.getRunSettings()
|
|
518
525
|
};
|
|
519
526
|
}
|
|
520
527
|
/**
|
|
521
|
-
*
|
|
528
|
+
* Save the user's run settings, and apply them at once to `sessionId`'s run
|
|
529
|
+
* when that session is open here (ADR-0030). Other sessions' runs keep
|
|
530
|
+
* theirs. Throws a {@link RunSettingsError}, saving nothing, for a value
|
|
531
|
+
* out of range.
|
|
532
|
+
*/
|
|
533
|
+
updateRunSettings(changes, sessionId) {
|
|
534
|
+
this.settingsService.updateRunSettings(changes);
|
|
535
|
+
if (sessionId) this.sessions.get(sessionId)?.applyRunSettings(changes);
|
|
536
|
+
return this.getSettings();
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* The catalog `selectPlanner` should judge `provider`'s
|
|
522
540
|
* remembered model against — a harness planner's own runner catalog, or the
|
|
523
541
|
* cross-provider picker list for a vendor (ADR-0009), same split as
|
|
524
542
|
* `plannerModelItems` in the TUI reducer. Read from whatever the resolver
|
|
@@ -532,28 +550,32 @@ var OrchestratorPool = class {
|
|
|
532
550
|
}
|
|
533
551
|
return this.modelResolver.getCachedPickerOptions().map((o) => ({ id: o.id }));
|
|
534
552
|
}
|
|
535
|
-
|
|
536
|
-
|
|
553
|
+
/**
|
|
554
|
+
* The provider planning right now. A vendor planner is resolved from its
|
|
555
|
+
* model id, so this is read again after a write rather than carried over.
|
|
556
|
+
*/
|
|
557
|
+
plannerProviderInEffect() {
|
|
558
|
+
return new WebConfig({
|
|
537
559
|
enabledRunners: this.enabledRunnerOverride(),
|
|
538
560
|
providerModelLists: this.cachedProviderLists
|
|
539
561
|
}).aiProvider;
|
|
562
|
+
}
|
|
563
|
+
updateSettings(changes) {
|
|
564
|
+
const current = {
|
|
565
|
+
provider: this.plannerProviderInEffect(),
|
|
566
|
+
model: process.env.ORCHESTRATOR_MODEL ?? "",
|
|
567
|
+
effort: process.env.ORDEWELL_PLANNER_EFFORT ?? ""
|
|
568
|
+
};
|
|
540
569
|
const rawEnv = changes.env && typeof changes.env === "object" && !Array.isArray(changes.env) ? changes.env : void 0;
|
|
541
|
-
const
|
|
542
|
-
const envChanges =
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
}
|
|
551
|
-
if (typeof changes.orchestratorModel === "string") {
|
|
552
|
-
process.env.ORCHESTRATOR_MODEL = changes.orchestratorModel;
|
|
553
|
-
}
|
|
554
|
-
if (typeof changes.plannerThinkingEffort === "string") {
|
|
555
|
-
process.env.ORDEWELL_PLANNER_EFFORT = changes.plannerThinkingEffort;
|
|
556
|
-
}
|
|
570
|
+
const admission = rawEnv ? core.admitSettingsEnv(rawEnv) : void 0;
|
|
571
|
+
const envChanges = admission ? admission.accepted : void 0;
|
|
572
|
+
const plannerChange = {
|
|
573
|
+
provider: envChanges?.AI_PROVIDER,
|
|
574
|
+
model: envChanges?.ORCHESTRATOR_MODEL ?? stringField(changes.orchestratorModel),
|
|
575
|
+
effort: envChanges?.ORDEWELL_PLANNER_EFFORT ?? stringField(changes.plannerThinkingEffort)
|
|
576
|
+
};
|
|
577
|
+
const touchesPlanner = Object.values(plannerChange).some((v) => v !== void 0);
|
|
578
|
+
const selection = touchesPlanner ? core.selectPlanner(current, plannerChange, this.plannerModelMemory, this.plannerCatalogFor(plannerChange.provider ?? current.provider)) : void 0;
|
|
557
579
|
if (envChanges) {
|
|
558
580
|
let touched = false;
|
|
559
581
|
for (const [key, value] of Object.entries(envChanges)) {
|
|
@@ -572,9 +594,9 @@ var OrchestratorPool = class {
|
|
|
572
594
|
this.modelResolver.refreshRunnerModels();
|
|
573
595
|
}
|
|
574
596
|
}
|
|
575
|
-
if (
|
|
576
|
-
process.env.ORCHESTRATOR_MODEL =
|
|
577
|
-
process.env.ORDEWELL_PLANNER_EFFORT =
|
|
597
|
+
if (selection) {
|
|
598
|
+
process.env.ORCHESTRATOR_MODEL = selection.next.model;
|
|
599
|
+
process.env.ORDEWELL_PLANNER_EFFORT = selection.next.effort;
|
|
578
600
|
}
|
|
579
601
|
if (changes.modelAllowlist && typeof changes.modelAllowlist === "object") {
|
|
580
602
|
const allowlist = changes.modelAllowlist;
|
|
@@ -586,22 +608,15 @@ var OrchestratorPool = class {
|
|
|
586
608
|
}
|
|
587
609
|
}
|
|
588
610
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
providerModelLists: this.cachedProviderLists
|
|
594
|
-
}).aiProvider;
|
|
595
|
-
this.plannerModelMemory.remember(
|
|
596
|
-
rememberProvider,
|
|
597
|
-
process.env.ORCHESTRATOR_MODEL || "",
|
|
598
|
-
process.env.ORDEWELL_PLANNER_EFFORT || ""
|
|
599
|
-
);
|
|
611
|
+
if (selection?.remember) {
|
|
612
|
+
const { provider, model, effort } = selection.remember;
|
|
613
|
+
const switched = provider !== current.provider;
|
|
614
|
+
this.plannerModelMemory.remember(switched ? provider : this.plannerProviderInEffect(), model, effort);
|
|
600
615
|
}
|
|
601
|
-
const rejectedEnvKeys =
|
|
616
|
+
const rejectedEnvKeys = admission?.rejected ?? [];
|
|
602
617
|
const settings = this.getSettings();
|
|
603
618
|
if (rejectedEnvKeys.length > 0) settings.rejectedEnvKeys = rejectedEnvKeys;
|
|
604
|
-
if (
|
|
619
|
+
if (selection?.recall) settings.switchRecall = selection.recall;
|
|
605
620
|
return settings;
|
|
606
621
|
}
|
|
607
622
|
setOrchestratorModel(model) {
|
|
@@ -613,7 +628,8 @@ var OrchestratorPool = class {
|
|
|
613
628
|
const runnerIds = this.registry.list().map((p) => p.manifest.name);
|
|
614
629
|
const [runnerModels, providerLists] = await Promise.all([
|
|
615
630
|
this.modelResolver.modelsForRunners(runnerIds),
|
|
616
|
-
this.modelResolver.refresh()
|
|
631
|
+
this.modelResolver.refresh(),
|
|
632
|
+
this.codexSandbox.ensure()
|
|
617
633
|
]);
|
|
618
634
|
const orchestratorModels = await this.modelResolver.pickerOptions();
|
|
619
635
|
const providerErrors = this.modelResolver.getDiscoveryErrors();
|
|
@@ -653,6 +669,7 @@ var OrchestratorPool = class {
|
|
|
653
669
|
async getInstalledRunners() {
|
|
654
670
|
const config = new WebConfig({ enabledRunners: this.enabledRunnerOverride() });
|
|
655
671
|
const enabled = new Set(config.enabledRunners);
|
|
672
|
+
await this.codexSandbox.refresh();
|
|
656
673
|
const runners = this.registry.list();
|
|
657
674
|
const installedIds = new Set(
|
|
658
675
|
await this.runnerInstallation.filterInstalled(runners.map((p) => p.manifest.name))
|
|
@@ -667,6 +684,7 @@ var OrchestratorPool = class {
|
|
|
667
684
|
const session = this.createSessionFor(sessionId, workspace, modelOverride, options);
|
|
668
685
|
this.destroy(sessionId);
|
|
669
686
|
this.sessions.set(sessionId, session);
|
|
687
|
+
this.warmCodexSandbox();
|
|
670
688
|
return core.migratePlanState(await session.generatePlan(goal, runners));
|
|
671
689
|
}
|
|
672
690
|
/**
|
|
@@ -679,6 +697,7 @@ var OrchestratorPool = class {
|
|
|
679
697
|
const session = this.createSessionFor(sessionId, workspace, modelOverride, options);
|
|
680
698
|
this.destroy(sessionId);
|
|
681
699
|
this.sessions.set(sessionId, session);
|
|
700
|
+
this.warmCodexSandbox();
|
|
682
701
|
return session.startPlanning(goal, runners);
|
|
683
702
|
}
|
|
684
703
|
/** Continue the planner dialogue with the user's reply; refused while a planner turn is in flight. */
|
|
@@ -760,6 +779,9 @@ function getSessionList(workspace) {
|
|
|
760
779
|
function removeSession(sessionId, workspace) {
|
|
761
780
|
return core.deleteSession(sessionId, workspace);
|
|
762
781
|
}
|
|
782
|
+
function stringField(value) {
|
|
783
|
+
return typeof value === "string" ? value : void 0;
|
|
784
|
+
}
|
|
763
785
|
function classify(err) {
|
|
764
786
|
if (err instanceof core.SessionNotFoundError) return { status: 404, code: "session_not_found" };
|
|
765
787
|
if (err instanceof core.NoPlanError) return { status: 400, code: "no_plan" };
|
|
@@ -827,7 +849,9 @@ function sessionsRoute(pool2) {
|
|
|
827
849
|
});
|
|
828
850
|
router.delete("/:id", (c) => {
|
|
829
851
|
const ws = c.req.query("workspace") || process.cwd();
|
|
830
|
-
const
|
|
852
|
+
const id = c.req.param("id");
|
|
853
|
+
pool2.destroy(id);
|
|
854
|
+
const ok = removeSession(id, ws);
|
|
831
855
|
return c.json({ ok });
|
|
832
856
|
});
|
|
833
857
|
router.post("/:id/close", (c) => {
|
|
@@ -837,6 +861,11 @@ function sessionsRoute(pool2) {
|
|
|
837
861
|
return router;
|
|
838
862
|
}
|
|
839
863
|
var NO_CHECKPOINT = "The task is not waiting at a checkpoint \u2014 it was answered already, withdrawn, or has no runner left to hear it.";
|
|
864
|
+
async function runUserCommand(session, command) {
|
|
865
|
+
const outcome = await session.execute(command, { kind: "user" });
|
|
866
|
+
if (!outcome.ok) throw new core.TaskControlError(outcome.message);
|
|
867
|
+
return outcome;
|
|
868
|
+
}
|
|
840
869
|
function plansRoute(pool2) {
|
|
841
870
|
const router = new hono.Hono();
|
|
842
871
|
router.post("/:sessionId/generate", async (c) => {
|
|
@@ -856,15 +885,24 @@ function plansRoute(pool2) {
|
|
|
856
885
|
});
|
|
857
886
|
router.post("/:sessionId/execute", async (c) => {
|
|
858
887
|
try {
|
|
859
|
-
|
|
888
|
+
const session = pool2.session(c.req.param("sessionId"));
|
|
889
|
+
if (!session.planState || session.planTasks.length === 0) throw new core.NoPlanError("execute");
|
|
890
|
+
if (session.isExecuting) throw new core.AlreadyExecutingError();
|
|
891
|
+
await runUserCommand(session, { command: "run_plan" });
|
|
860
892
|
return c.json({ status: "running" });
|
|
861
893
|
} catch (err) {
|
|
862
894
|
return failure(c, err, "execute");
|
|
863
895
|
}
|
|
864
896
|
});
|
|
865
|
-
router.post("/:sessionId/stop", (c) => {
|
|
866
|
-
|
|
867
|
-
|
|
897
|
+
router.post("/:sessionId/stop", async (c) => {
|
|
898
|
+
try {
|
|
899
|
+
if (pool2.hasSession(c.req.param("sessionId"))) {
|
|
900
|
+
await pool2.session(c.req.param("sessionId")).execute({ command: "stop_run" }, { kind: "user" });
|
|
901
|
+
}
|
|
902
|
+
return c.json({ status: "stopped" });
|
|
903
|
+
} catch (err) {
|
|
904
|
+
return failure(c, err, "stop");
|
|
905
|
+
}
|
|
868
906
|
});
|
|
869
907
|
router.post("/:sessionId/planning/stop", (c) => {
|
|
870
908
|
const cancelled = pool2.cancelPlanning(c.req.param("sessionId"));
|
|
@@ -886,34 +924,79 @@ function plansRoute(pool2) {
|
|
|
886
924
|
return failure(c, err, "mark incomplete", { fallback: 404, message: "Not found" });
|
|
887
925
|
}
|
|
888
926
|
});
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
927
|
+
router.post("/:sessionId/tasks/:taskId/resume/cancel", (c) => {
|
|
928
|
+
try {
|
|
929
|
+
const cancelled = pool2.session(c.req.param("sessionId")).cancelScheduledResume(c.req.param("taskId"));
|
|
930
|
+
return c.json({ cancelled });
|
|
931
|
+
} catch (err) {
|
|
932
|
+
return failure(c, err, "cancel resume", { fallback: 404, message: "Not found" });
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
router.post("/:sessionId/tasks/:taskId/resume/now", async (c) => {
|
|
936
|
+
try {
|
|
937
|
+
await pool2.session(c.req.param("sessionId")).resumeNow(c.req.param("taskId"));
|
|
938
|
+
return c.json({ ok: true });
|
|
939
|
+
} catch (err) {
|
|
940
|
+
return failure(c, err, "resume now", { fallback: 404, message: "Not found" });
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
for (const [segment, command] of [
|
|
944
|
+
["run", (id) => ({ command: "run_task", task: id })],
|
|
945
|
+
["retry", (id) => ({ command: "retry_task", task: id, from: "fresh" })],
|
|
946
|
+
["cancel", (id) => ({ command: "stop_task", task: id })]
|
|
894
947
|
]) {
|
|
895
948
|
router.post(`/:sessionId/tasks/:taskId/${segment}`, async (c) => {
|
|
896
949
|
try {
|
|
897
|
-
await
|
|
950
|
+
await runUserCommand(pool2.session(c.req.param("sessionId")), command(c.req.param("taskId")));
|
|
898
951
|
return c.json({ ok: true });
|
|
899
952
|
} catch (err) {
|
|
900
953
|
return failure(c, err, segment);
|
|
901
954
|
}
|
|
902
955
|
});
|
|
903
956
|
}
|
|
957
|
+
router.post("/:sessionId/tasks/:taskId/skip", async (c) => {
|
|
958
|
+
try {
|
|
959
|
+
const body = await c.req.json().catch(() => ({}));
|
|
960
|
+
const reason = typeof body === "object" && body !== null && "reason" in body && typeof body.reason === "string" ? body.reason : void 0;
|
|
961
|
+
await runUserCommand(pool2.session(c.req.param("sessionId")), { command: "skip_task", task: c.req.param("taskId"), ...reason ? { reason } : {} });
|
|
962
|
+
return c.json({ ok: true });
|
|
963
|
+
} catch (err) {
|
|
964
|
+
return failure(c, err, "skip");
|
|
965
|
+
}
|
|
966
|
+
});
|
|
967
|
+
router.post("/:sessionId/tasks/:taskId/force-start", async (c) => {
|
|
968
|
+
try {
|
|
969
|
+
await pool2.session(c.req.param("sessionId")).forceStartTask(c.req.param("taskId"));
|
|
970
|
+
return c.json({ ok: true });
|
|
971
|
+
} catch (err) {
|
|
972
|
+
return failure(c, err, "force-start");
|
|
973
|
+
}
|
|
974
|
+
});
|
|
904
975
|
const messageHandler = (send) => async (c) => {
|
|
905
976
|
try {
|
|
906
977
|
const body = await c.req.json().catch(() => ({}));
|
|
907
978
|
const text = typeof body === "object" && body !== null && "text" in body ? body.text : void 0;
|
|
908
979
|
if (typeof text !== "string" || !text.trim()) return c.json({ error: "text is required" }, 400);
|
|
909
|
-
const id = send(pool2.session(c.req.param("sessionId")), c.req.param("taskId"), text);
|
|
980
|
+
const id = await send(pool2.session(c.req.param("sessionId")), c.req.param("taskId"), text);
|
|
910
981
|
return c.json({ id });
|
|
911
982
|
} catch (err) {
|
|
912
983
|
return failure(c, err, "task edit");
|
|
913
984
|
}
|
|
914
985
|
};
|
|
915
|
-
router.post("/:sessionId/tasks/:taskId/messages", messageHandler((session, taskId, text) => session
|
|
986
|
+
router.post("/:sessionId/tasks/:taskId/messages", messageHandler(async (session, taskId, text) => (await runUserCommand(session, { command: "message_task", task: taskId, text })).id ?? ""));
|
|
916
987
|
router.post("/:sessionId/tasks/:taskId/messages/now", messageHandler((session, taskId, text) => session.forceSendTaskMessage(taskId, text)));
|
|
988
|
+
router.post("/:sessionId/tasks/:taskId/commands", async (c) => {
|
|
989
|
+
try {
|
|
990
|
+
const body = await c.req.json().catch(() => ({}));
|
|
991
|
+
const fields = typeof body === "object" && body !== null ? body : {};
|
|
992
|
+
if (typeof fields.name !== "string" || !fields.name.trim()) return c.json({ error: "name is required" }, 400);
|
|
993
|
+
const args2 = typeof fields.args === "string" ? fields.args : "";
|
|
994
|
+
const id = pool2.session(c.req.param("sessionId")).invokeTaskCommand(c.req.param("taskId"), fields.name.trim(), args2);
|
|
995
|
+
return c.json({ id });
|
|
996
|
+
} catch (err) {
|
|
997
|
+
return failure(c, err, "task edit");
|
|
998
|
+
}
|
|
999
|
+
});
|
|
917
1000
|
router.post("/:sessionId/tasks/:taskId/messages/:messageId/now", (c) => {
|
|
918
1001
|
try {
|
|
919
1002
|
const sent = pool2.session(c.req.param("sessionId")).forceSendQueuedTaskMessage(c.req.param("taskId"), c.req.param("messageId"));
|
|
@@ -927,7 +1010,7 @@ function plansRoute(pool2) {
|
|
|
927
1010
|
const body = await c.req.json().catch(() => ({}));
|
|
928
1011
|
const text = typeof body === "object" && body !== null && "text" in body ? body.text : void 0;
|
|
929
1012
|
if (typeof text !== "string" || !text.trim()) return c.json({ error: "text is required" }, 400);
|
|
930
|
-
await pool2.session(c.req.param("sessionId"))
|
|
1013
|
+
await runUserCommand(pool2.session(c.req.param("sessionId")), { command: "retry_task", task: c.req.param("taskId"), from: "session", message: text });
|
|
931
1014
|
return c.json({ ok: true });
|
|
932
1015
|
} catch (err) {
|
|
933
1016
|
return failure(c, err, "task edit");
|
|
@@ -1115,6 +1198,16 @@ function plansRoute(pool2) {
|
|
|
1115
1198
|
return failure(c, err, "conversation message");
|
|
1116
1199
|
}
|
|
1117
1200
|
});
|
|
1201
|
+
router.post("/:sessionId/converse/message/now", async (c) => {
|
|
1202
|
+
try {
|
|
1203
|
+
const { message } = await c.req.json();
|
|
1204
|
+
if (!message) return c.json({ error: "message is required" }, 400);
|
|
1205
|
+
const plan = await pool2.session(c.req.param("sessionId")).forceSendPrompt(message);
|
|
1206
|
+
return c.json({ plan: core.surfacePlan(plan) });
|
|
1207
|
+
} catch (err) {
|
|
1208
|
+
return failure(c, err, "conversation message");
|
|
1209
|
+
}
|
|
1210
|
+
});
|
|
1118
1211
|
router.post("/:sessionId/conversation/fork", (c) => {
|
|
1119
1212
|
try {
|
|
1120
1213
|
const fork = pool2.forkConversation(c.req.param("sessionId"));
|
|
@@ -1212,11 +1305,18 @@ function approvalsRoute(pool2) {
|
|
|
1212
1305
|
const approvalId = c.req.param("approvalId");
|
|
1213
1306
|
if (!pool2.hasSession(sessionId)) return refuse(c, 404, `Unknown session: ${sessionId}`, "session_not_found");
|
|
1214
1307
|
const answer = answerOf(await c.req.json().catch(() => null));
|
|
1215
|
-
|
|
1308
|
+
const decided = core.toApprovalDecision(answer);
|
|
1309
|
+
const request = pool2.outstandingApprovals(sessionId).find((pending) => pending.id === approvalId)?.request;
|
|
1310
|
+
if (decided.decision === "deny" && request?.taskId) {
|
|
1311
|
+
const outcome = await pool2.session(sessionId).execute(
|
|
1312
|
+
{ command: "answer_approval", task: request.taskId, approval: approvalId, answer: "deny", ...decided.note ? { note: decided.note } : {} },
|
|
1313
|
+
{ kind: "user" }
|
|
1314
|
+
);
|
|
1315
|
+
if (!outcome.ok) return refuse(c, 400, outcome.message, "refused");
|
|
1316
|
+
} else if (!pool2.resolveApproval(sessionId, approvalId, answer)) {
|
|
1216
1317
|
return refuse(c, 409, "Approval is no longer outstanding \u2014 it timed out or was already answered.", "approval_not_outstanding");
|
|
1217
1318
|
}
|
|
1218
|
-
|
|
1219
|
-
return c.json({ ok: true, granted: core.isGranted({ decision }), decision });
|
|
1319
|
+
return c.json({ ok: true, granted: core.isGranted(decided), decision: decided.decision });
|
|
1220
1320
|
});
|
|
1221
1321
|
return router;
|
|
1222
1322
|
}
|
|
@@ -1228,10 +1328,21 @@ function settingsRoute(pool2) {
|
|
|
1228
1328
|
});
|
|
1229
1329
|
router.patch("/", async (c) => {
|
|
1230
1330
|
const body = await c.req.json();
|
|
1231
|
-
if (!body || typeof body !== "object" || Object.keys(body).length === 0) {
|
|
1331
|
+
if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).length === 0) {
|
|
1232
1332
|
return refuse(c, 400, "At least one setting field is required");
|
|
1233
1333
|
}
|
|
1234
|
-
const
|
|
1334
|
+
const { run: run2, sessionId, ...rest } = body;
|
|
1335
|
+
if (run2 !== void 0) {
|
|
1336
|
+
if (!run2 || typeof run2 !== "object" || Array.isArray(run2)) return refuse(c, 400, "run must be an object of run settings");
|
|
1337
|
+
if (sessionId !== void 0 && typeof sessionId !== "string") return refuse(c, 400, "sessionId must be a string");
|
|
1338
|
+
try {
|
|
1339
|
+
pool2.updateRunSettings(run2, sessionId);
|
|
1340
|
+
} catch (err) {
|
|
1341
|
+
if (err instanceof core.RunSettingsError) return refuse(c, 400, err.message);
|
|
1342
|
+
throw err;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
const result = Object.keys(rest).length > 0 ? pool2.updateSettings(rest) : pool2.getSettings();
|
|
1235
1346
|
return c.json(result);
|
|
1236
1347
|
});
|
|
1237
1348
|
return router;
|
|
@@ -1266,6 +1377,8 @@ function createWsHandler(pool2) {
|
|
|
1266
1377
|
pool2.unsubscribe(sessionId, ws);
|
|
1267
1378
|
});
|
|
1268
1379
|
ws.send(JSON.stringify({ type: "connected", sessionId }));
|
|
1380
|
+
const view = pool2.runSnapshot(sessionId);
|
|
1381
|
+
if (view) ws.send(JSON.stringify({ type: "run_snapshot", sessionId, view }));
|
|
1269
1382
|
pool2.subscribe(sessionId, ws);
|
|
1270
1383
|
});
|
|
1271
1384
|
return (request, socket, head) => {
|
|
@@ -1331,9 +1444,9 @@ function withPort(hostname, port2) {
|
|
|
1331
1444
|
}
|
|
1332
1445
|
|
|
1333
1446
|
// server/app.ts
|
|
1334
|
-
function createApp(pool2,
|
|
1335
|
-
const
|
|
1336
|
-
|
|
1447
|
+
function createApp(pool2, admission) {
|
|
1448
|
+
const app = new hono.Hono();
|
|
1449
|
+
app.use("*", async (c, next) => {
|
|
1337
1450
|
const decision = decideAdmission({
|
|
1338
1451
|
origin: c.req.header("origin") ?? null,
|
|
1339
1452
|
// The request URL's authority is built from the Host header upstream, so
|
|
@@ -1341,21 +1454,21 @@ function createApp(pool2, admission2) {
|
|
|
1341
1454
|
// absent entirely, which fails the check anyway.
|
|
1342
1455
|
host: c.req.header("host") ?? new URL(c.req.url).host,
|
|
1343
1456
|
authorization: c.req.header("authorization") ?? null
|
|
1344
|
-
},
|
|
1457
|
+
}, admission);
|
|
1345
1458
|
if (!decision.admit) return c.json({ error: decision.reason }, decision.status);
|
|
1346
1459
|
await next();
|
|
1347
1460
|
});
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
return
|
|
1461
|
+
app.route("/api/sessions", sessionsRoute(pool2));
|
|
1462
|
+
app.route("/api/plans", plansRoute(pool2));
|
|
1463
|
+
app.route("/api/runners", runnersRoute(pool2));
|
|
1464
|
+
app.route("/api/models", modelsRoute(pool2));
|
|
1465
|
+
app.route("/api/settings", settingsRoute(pool2));
|
|
1466
|
+
app.route("/api/commands", commandsRoute());
|
|
1467
|
+
app.route("/api/workspaces", workspacesRoute());
|
|
1468
|
+
app.route("/api/approvals", approvalsRoute(pool2));
|
|
1469
|
+
return app;
|
|
1357
1470
|
}
|
|
1358
|
-
function attachWsHandler(server2, pool2,
|
|
1471
|
+
function attachWsHandler(server2, pool2, admission) {
|
|
1359
1472
|
const handler = createWsHandler(pool2);
|
|
1360
1473
|
server2.on("upgrade", (request, socket, head) => {
|
|
1361
1474
|
const decision = decideAdmission({
|
|
@@ -1363,7 +1476,7 @@ function attachWsHandler(server2, pool2, admission2) {
|
|
|
1363
1476
|
host: request.headers.host ?? null,
|
|
1364
1477
|
authorization: request.headers.authorization ?? null,
|
|
1365
1478
|
secWebSocketProtocol: request.headers["sec-websocket-protocol"] ?? null
|
|
1366
|
-
},
|
|
1479
|
+
}, admission);
|
|
1367
1480
|
if (!decision.admit) {
|
|
1368
1481
|
refuseUpgrade(socket, decision);
|
|
1369
1482
|
return;
|
|
@@ -1391,7 +1504,7 @@ ${body}`
|
|
|
1391
1504
|
}
|
|
1392
1505
|
|
|
1393
1506
|
// server/nodeAdapter.ts
|
|
1394
|
-
function createRequestListener(
|
|
1507
|
+
function createRequestListener(app) {
|
|
1395
1508
|
return async (req, res) => {
|
|
1396
1509
|
const bufs = [];
|
|
1397
1510
|
for await (const chunk of req) bufs.push(chunk);
|
|
@@ -1411,7 +1524,7 @@ function createRequestListener(app2) {
|
|
|
1411
1524
|
body: req.method !== "GET" && req.method !== "HEAD" ? body : void 0
|
|
1412
1525
|
});
|
|
1413
1526
|
try {
|
|
1414
|
-
const webRes = await
|
|
1527
|
+
const webRes = await app.fetch(webReq);
|
|
1415
1528
|
res.writeHead(webRes.status, Object.fromEntries(webRes.headers.entries()));
|
|
1416
1529
|
if (webRes.body) {
|
|
1417
1530
|
const reader = webRes.body.getReader();
|
|
@@ -1438,21 +1551,24 @@ if (portIdx !== -1 && args[portIdx + 1]) {
|
|
|
1438
1551
|
}
|
|
1439
1552
|
var watchParentIdx = args.indexOf("--watch-parent");
|
|
1440
1553
|
var watchParentPid = watchParentIdx !== -1 ? parseInt(args[watchParentIdx + 1], 10) : void 0;
|
|
1441
|
-
var { token, file: tokenFile } = core.mintDaemonToken(port);
|
|
1442
|
-
var admission = { port, token, tokenFile };
|
|
1443
1554
|
var pool = new OrchestratorPool({ runner: new core.StructuredRunner() });
|
|
1444
|
-
var
|
|
1445
|
-
|
|
1446
|
-
console.error(`[web] model discovery warmup failed: ${err?.message ?? err}`);
|
|
1447
|
-
});
|
|
1448
|
-
app.get("/", (c) => c.json({ message: "Ordewell API", docs: "/api/workspaces" }));
|
|
1449
|
-
var server = http.createServer(createRequestListener(app));
|
|
1450
|
-
attachWsHandler(server, pool, admission);
|
|
1555
|
+
var server = http.createServer();
|
|
1556
|
+
var ownsToken = false;
|
|
1451
1557
|
server.listen(port, "127.0.0.1", () => {
|
|
1558
|
+
const { token, file: tokenFile } = core.mintDaemonToken(port);
|
|
1559
|
+
ownsToken = true;
|
|
1560
|
+
const admission = { port, token, tokenFile };
|
|
1561
|
+
const app = createApp(pool, admission);
|
|
1562
|
+
app.get("/", (c) => c.json({ message: "Ordewell API", docs: "/api/workspaces" }));
|
|
1563
|
+
server.on("request", createRequestListener(app));
|
|
1564
|
+
attachWsHandler(server, pool, admission);
|
|
1565
|
+
pool.getProviderModels().catch((err) => {
|
|
1566
|
+
console.error(`[web] model discovery warmup failed: ${err?.message ?? err}`);
|
|
1567
|
+
});
|
|
1452
1568
|
console.error(`[web] Ordewell API server: http://localhost:${port}`);
|
|
1453
1569
|
});
|
|
1454
1570
|
function shutdown() {
|
|
1455
|
-
core.clearDaemonToken(port);
|
|
1571
|
+
if (ownsToken) core.clearDaemonToken(port);
|
|
1456
1572
|
pool.destroyAll();
|
|
1457
1573
|
server.close();
|
|
1458
1574
|
process.exit(0);
|