@ordewell/web 0.10.2 → 0.10.4
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 +174 -73
- package/dist/server/main.js.map +1 -1
- package/package.json +2 -2
package/dist/server/main.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
var hono = require('hono');
|
|
5
5
|
var child_process = require('child_process');
|
|
6
|
+
var crypto = require('crypto');
|
|
6
7
|
var core = require('@ordewell/core');
|
|
7
8
|
var fs = require('fs');
|
|
8
9
|
var path = require('path');
|
|
@@ -31,60 +32,59 @@ var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
|
|
|
31
32
|
var path__namespace = /*#__PURE__*/_interopNamespace(path);
|
|
32
33
|
|
|
33
34
|
var WebConfig = class extends core.BaseConfig {
|
|
34
|
-
_provider = null;
|
|
35
|
-
_providerCacheKey = null;
|
|
36
|
-
_providerCacheLists = void 0;
|
|
37
35
|
_enabledRunnersOverride;
|
|
38
36
|
_providerModelLists;
|
|
39
37
|
_modelOverride;
|
|
38
|
+
_pickedModel;
|
|
39
|
+
_agentCatalog;
|
|
40
40
|
constructor(opts) {
|
|
41
41
|
super();
|
|
42
42
|
this._enabledRunnersOverride = opts?.enabledRunners;
|
|
43
|
-
|
|
43
|
+
const lists = opts?.providerModelLists;
|
|
44
|
+
this._providerModelLists = typeof lists === "function" ? lists : () => lists;
|
|
44
45
|
this._modelOverride = opts?.modelOverride;
|
|
46
|
+
this._pickedModel = opts?.pickedModel ?? (() => void 0);
|
|
47
|
+
this._agentCatalog = opts?.agentCatalog ?? (() => []);
|
|
48
|
+
}
|
|
49
|
+
agentCatalog(provider) {
|
|
50
|
+
return this._agentCatalog(provider);
|
|
45
51
|
}
|
|
46
52
|
setProviderModelLists(lists) {
|
|
47
|
-
this._providerModelLists = lists;
|
|
48
|
-
|
|
53
|
+
this._providerModelLists = () => lists;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Read live, not fixed at construction: a session's WebConfig lives for the
|
|
57
|
+
* whole session while /model, /planner and /key mutate process.env under
|
|
58
|
+
* it. `routedProvider` keeps that cheap.
|
|
59
|
+
*/
|
|
60
|
+
get planner() {
|
|
61
|
+
return this.resolvePlanner({ ...this.configuredPlanner(), override: this._modelOverride }, this._providerModelLists());
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* What the daemon inherited at startup, or what a settings write has set
|
|
65
|
+
* since; the model counts as picked only while it is the one a write picked.
|
|
66
|
+
*/
|
|
67
|
+
configuredPlanner() {
|
|
68
|
+
const model = process.env.ORCHESTRATOR_MODEL || "";
|
|
69
|
+
const pick = this._pickedModel();
|
|
70
|
+
return { model, effort: process.env.ORDEWELL_PLANNER_EFFORT || "", pickedFor: pick?.model === model ? pick.provider : void 0 };
|
|
49
71
|
}
|
|
50
72
|
get orchestratorModel() {
|
|
51
|
-
return this.
|
|
73
|
+
return this.planner.model;
|
|
52
74
|
}
|
|
53
75
|
get aiProvider() {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return this._provider;
|
|
59
|
-
}
|
|
60
|
-
if (explicitEnv && core.isCliProvider(explicitEnv)) {
|
|
61
|
-
this._provider = explicitEnv;
|
|
62
|
-
this._providerCacheKey = cacheKey;
|
|
63
|
-
this._providerCacheLists = this._providerModelLists;
|
|
64
|
-
return this._provider;
|
|
65
|
-
}
|
|
66
|
-
if (model && this._providerModelLists) {
|
|
67
|
-
const resolved = core.resolveProvider(model, this._providerModelLists);
|
|
68
|
-
if (resolved) {
|
|
69
|
-
this._provider = resolved;
|
|
70
|
-
this._providerCacheKey = cacheKey;
|
|
71
|
-
this._providerCacheLists = this._providerModelLists;
|
|
72
|
-
return this._provider;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
this._provider = core.BaseConfig.detectProvider("openrouter");
|
|
76
|
-
this._providerCacheKey = cacheKey;
|
|
77
|
-
this._providerCacheLists = this._providerModelLists;
|
|
78
|
-
return this._provider;
|
|
76
|
+
return this.planner.provider;
|
|
77
|
+
}
|
|
78
|
+
get plannerThinkingEffort() {
|
|
79
|
+
return this.planner.effort || void 0;
|
|
79
80
|
}
|
|
80
81
|
get apiKey() {
|
|
81
82
|
const provider = this.aiProvider;
|
|
82
83
|
return core.isApiProvider(provider) ? this.getProviderApiKey(provider) : "";
|
|
83
84
|
}
|
|
84
85
|
get planningModel() {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return process.env.ORCHESTRATOR_MODEL || "";
|
|
86
|
+
const model = this.plannerChoiceFor(this.aiProvider, this.configuredPlanner()).model;
|
|
87
|
+
return this.aiProvider === "google" ? core.normalizeGeminiModel(process.env.GEMINI_MODEL || model) : model;
|
|
88
88
|
}
|
|
89
89
|
get enabledRunners() {
|
|
90
90
|
if (this._enabledRunnersOverride) return this._enabledRunnersOverride;
|
|
@@ -93,6 +93,8 @@ var WebConfig = class extends core.BaseConfig {
|
|
|
93
93
|
const parsed = envRunners.split(",").map((s) => s.trim()).filter(Boolean);
|
|
94
94
|
if (parsed.length > 0) return parsed;
|
|
95
95
|
}
|
|
96
|
+
const toggles = ["ORDEWELL_CLAUDE_CODE_ENABLED", "ORDEWELL_CODEX_ENABLED", "ORDEWELL_OPENCODE_ENABLED"];
|
|
97
|
+
if (toggles.every((name) => process.env[name] === void 0)) return core.defaultEnabledRunners();
|
|
96
98
|
const out = [];
|
|
97
99
|
if (process.env.ORDEWELL_CLAUDE_CODE_ENABLED !== "false" && process.env.ORDEWELL_CLAUDE_CODE_ENABLED !== "0") {
|
|
98
100
|
out.push("claude-code");
|
|
@@ -372,12 +374,26 @@ var PoolAwareRunner = class {
|
|
|
372
374
|
var OrchestratorPool = class {
|
|
373
375
|
sessions = /* @__PURE__ */ new Map();
|
|
374
376
|
clients = /* @__PURE__ */ new Map();
|
|
377
|
+
/**
|
|
378
|
+
* This process's broadcast numbering (see `StreamStamp`), per session id
|
|
379
|
+
* rather than per Session object: a session replaced under the same id
|
|
380
|
+
* keeps counting up, so a client never mistakes its first broadcast for one
|
|
381
|
+
* of the old session's it already showed.
|
|
382
|
+
*/
|
|
383
|
+
stream = crypto.randomUUID();
|
|
384
|
+
sequence = /* @__PURE__ */ new Map();
|
|
375
385
|
registry = new core.RunnerRegistry();
|
|
376
386
|
modelResolver;
|
|
377
|
-
runnerInstallation
|
|
387
|
+
runnerInstallation;
|
|
378
388
|
cachedProviderLists;
|
|
379
389
|
settingsService = new core.SettingsService();
|
|
380
390
|
plannerModelMemory = new core.PlannerModelMemory(this.settingsService);
|
|
391
|
+
/**
|
|
392
|
+
* The model a settings write picked in this daemon. The environment's model
|
|
393
|
+
* is otherwise inherited, and an agent planning only because detection
|
|
394
|
+
* found it does not plan with an inherited model it cannot be shown to run.
|
|
395
|
+
*/
|
|
396
|
+
pickedModel;
|
|
381
397
|
runner;
|
|
382
398
|
/** One probe per daemon, asked again whenever the runner list is refreshed (ADR-0033). */
|
|
383
399
|
codexSandbox;
|
|
@@ -385,6 +401,7 @@ var OrchestratorPool = class {
|
|
|
385
401
|
const pluginNotice = core.removedPluginNotice();
|
|
386
402
|
if (pluginNotice) console.warn(pluginNotice);
|
|
387
403
|
this.runner = deps.runner ?? new core.StructuredRunner();
|
|
404
|
+
this.runnerInstallation = deps.runnerInstallation ?? new core.RunnerInstallation(this.registry);
|
|
388
405
|
this.modelResolver = deps.modelResolver ?? new core.ModelResolver(this.registry, new WebConfig());
|
|
389
406
|
this.codexSandbox = deps.codexSandbox ?? new core.CodexSandboxAvailability(this.registry, { spawn: child_process.spawn, fetch: globalThis.fetch }, { cwd: process.cwd(), env: process.env });
|
|
390
407
|
}
|
|
@@ -396,6 +413,29 @@ var OrchestratorPool = class {
|
|
|
396
413
|
warmCodexSandbox() {
|
|
397
414
|
this.codexSandbox.ensure().catch((err) => console.error(`[web] codex sandbox probe failed: ${err instanceof Error ? err.message : err}`));
|
|
398
415
|
}
|
|
416
|
+
/**
|
|
417
|
+
* Find the coding agent that plans when the user has chosen no planner and
|
|
418
|
+
* set no vendor key (ADR-0009), for the daemon to start as it starts. Every
|
|
419
|
+
* planner turn waits on it, so a goal typed the instant the TUI opens
|
|
420
|
+
* does not plan with an openrouter key nobody set.
|
|
421
|
+
*/
|
|
422
|
+
detectInstalledPlanner() {
|
|
423
|
+
return core.detectInstalledPlanner(this.runnerInstallation);
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* A new session's first planner turn. It waits for that probe at the
|
|
427
|
+
* session's own planner gate, where a reply sent meanwhile queues behind
|
|
428
|
+
* it, and a Stop or the session closing or being replaced ends the wait.
|
|
429
|
+
*/
|
|
430
|
+
async firstTurn(sessionId, session, turn) {
|
|
431
|
+
try {
|
|
432
|
+
return await turn();
|
|
433
|
+
} catch (err) {
|
|
434
|
+
if (err instanceof core.PlannerNotReadyError && this.sessions.get(sessionId) === session) this.destroy(sessionId);
|
|
435
|
+
if (err instanceof core.PlannerTurnStoppedError && !session.planState) throw new core.PlannerTurnDiscardedError();
|
|
436
|
+
throw err;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
399
439
|
subscribe(sessionId, ws) {
|
|
400
440
|
if (!this.clients.has(sessionId)) this.clients.set(sessionId, /* @__PURE__ */ new Set());
|
|
401
441
|
this.clients.get(sessionId).add(ws);
|
|
@@ -405,7 +445,9 @@ var OrchestratorPool = class {
|
|
|
405
445
|
if (this.clients.get(sessionId)?.size === 0) this.clients.delete(sessionId);
|
|
406
446
|
}
|
|
407
447
|
broadcast(sessionId, msg) {
|
|
408
|
-
const
|
|
448
|
+
const seq = (this.sequence.get(sessionId) ?? 0) + 1;
|
|
449
|
+
this.sequence.set(sessionId, seq);
|
|
450
|
+
const payload = JSON.stringify({ ...msg, stream: this.stream, seq });
|
|
409
451
|
for (const ws of this.clients.get(sessionId) ?? []) {
|
|
410
452
|
if (ws.readyState === ws.OPEN) ws.send(payload);
|
|
411
453
|
}
|
|
@@ -444,6 +486,14 @@ var OrchestratorPool = class {
|
|
|
444
486
|
runSnapshot(sessionId) {
|
|
445
487
|
return this.sessions.get(sessionId)?.runSnapshot() ?? null;
|
|
446
488
|
}
|
|
489
|
+
/** The planner's approval requests still waiting, as they were announced. */
|
|
490
|
+
announcedApprovals(sessionId) {
|
|
491
|
+
return this.sessions.get(sessionId)?.announcedApprovals() ?? [];
|
|
492
|
+
}
|
|
493
|
+
/** The last broadcast of the session so far: a snapshot taken now accounts for it and everything before. */
|
|
494
|
+
streamPosition(sessionId) {
|
|
495
|
+
return { stream: this.stream, seq: this.sequence.get(sessionId) ?? 0 };
|
|
496
|
+
}
|
|
447
497
|
runtimeSettings() {
|
|
448
498
|
return core.sessionRuntimeSettings(this.settingsService.getAll());
|
|
449
499
|
}
|
|
@@ -456,11 +506,7 @@ var OrchestratorPool = class {
|
|
|
456
506
|
core.ensureStateDirIgnored(workspace);
|
|
457
507
|
core.assertWorkspaceIsProject(workspace);
|
|
458
508
|
}
|
|
459
|
-
const config =
|
|
460
|
-
enabledRunners: this.enabledRunnerOverride(),
|
|
461
|
-
modelOverride,
|
|
462
|
-
providerModelLists: this.cachedProviderLists
|
|
463
|
-
});
|
|
509
|
+
const config = this.plannerConfig(modelOverride);
|
|
464
510
|
const fsAdapter = new PoolFileSystem(workspace);
|
|
465
511
|
const broadcast = (msg) => this.broadcast(sessionId, msg);
|
|
466
512
|
const runner = new PoolAwareRunner(sessionId, broadcast, this.runner);
|
|
@@ -484,6 +530,22 @@ var OrchestratorPool = class {
|
|
|
484
530
|
sessionId
|
|
485
531
|
});
|
|
486
532
|
}
|
|
533
|
+
/**
|
|
534
|
+
* The one way the pool builds a config that answers "who plans": sessions,
|
|
535
|
+
* the settings response and the switch judgement all route through it, so
|
|
536
|
+
* what the settings route reports is what the next turn plans with. The
|
|
537
|
+
* lists are handed over as a reader, not a snapshot, so a credential change
|
|
538
|
+
* reaches a session that is already open.
|
|
539
|
+
*/
|
|
540
|
+
plannerConfig(modelOverride) {
|
|
541
|
+
return new WebConfig({
|
|
542
|
+
enabledRunners: this.enabledRunnerOverride(),
|
|
543
|
+
modelOverride,
|
|
544
|
+
providerModelLists: () => this.cachedProviderLists,
|
|
545
|
+
pickedModel: () => this.pickedModel,
|
|
546
|
+
agentCatalog: (provider) => this.plannerCatalogFor(provider).map((model) => model.id)
|
|
547
|
+
});
|
|
548
|
+
}
|
|
487
549
|
/**
|
|
488
550
|
* The user's runner choice, or undefined when they have never made one —
|
|
489
551
|
* which is what lets `WebConfig` fall back to the environment's defaults.
|
|
@@ -504,7 +566,7 @@ var OrchestratorPool = class {
|
|
|
504
566
|
this.settingsService.setEnabledRunners(next);
|
|
505
567
|
}
|
|
506
568
|
getSettings() {
|
|
507
|
-
const config =
|
|
569
|
+
const config = this.plannerConfig();
|
|
508
570
|
const userSettings = this.settingsService.getAll();
|
|
509
571
|
return {
|
|
510
572
|
orchestratorModel: config.orchestratorModel,
|
|
@@ -555,21 +617,14 @@ var OrchestratorPool = class {
|
|
|
555
617
|
* model id, so this is read again after a write rather than carried over.
|
|
556
618
|
*/
|
|
557
619
|
plannerProviderInEffect() {
|
|
558
|
-
return
|
|
559
|
-
enabledRunners: this.enabledRunnerOverride(),
|
|
560
|
-
providerModelLists: this.cachedProviderLists
|
|
561
|
-
}).aiProvider;
|
|
620
|
+
return this.plannerConfig().aiProvider;
|
|
562
621
|
}
|
|
563
622
|
updateSettings(changes) {
|
|
564
|
-
const current =
|
|
565
|
-
provider: this.plannerProviderInEffect(),
|
|
566
|
-
model: process.env.ORCHESTRATOR_MODEL ?? "",
|
|
567
|
-
effort: process.env.ORDEWELL_PLANNER_EFFORT ?? ""
|
|
568
|
-
};
|
|
623
|
+
const current = this.plannerConfig().planner;
|
|
569
624
|
const rawEnv = changes.env && typeof changes.env === "object" && !Array.isArray(changes.env) ? changes.env : void 0;
|
|
570
625
|
const admission = rawEnv ? core.admitSettingsEnv(rawEnv) : void 0;
|
|
571
626
|
const envChanges = admission ? admission.accepted : void 0;
|
|
572
|
-
const plannerChange = {
|
|
627
|
+
const plannerChange = changes.inherited === true ? {} : {
|
|
573
628
|
provider: envChanges?.AI_PROVIDER,
|
|
574
629
|
model: envChanges?.ORCHESTRATOR_MODEL ?? stringField(changes.orchestratorModel),
|
|
575
630
|
effort: envChanges?.ORDEWELL_PLANNER_EFFORT ?? stringField(changes.plannerThinkingEffort)
|
|
@@ -592,11 +647,13 @@ var OrchestratorPool = class {
|
|
|
592
647
|
if (touched) {
|
|
593
648
|
this.modelResolver.invalidate();
|
|
594
649
|
this.modelResolver.refreshRunnerModels();
|
|
650
|
+
this.dropUnkeyedProviderLists();
|
|
595
651
|
}
|
|
596
652
|
}
|
|
597
653
|
if (selection) {
|
|
598
|
-
process.env.ORCHESTRATOR_MODEL = selection.next.model;
|
|
654
|
+
if (plannerChange.provider !== void 0 || plannerChange.model !== void 0) process.env.ORCHESTRATOR_MODEL = selection.next.model;
|
|
599
655
|
process.env.ORDEWELL_PLANNER_EFFORT = selection.next.effort;
|
|
656
|
+
if (plannerChange.model !== void 0) this.pickedModel = { model: selection.next.model, provider: this.plannerProviderInEffect() };
|
|
600
657
|
}
|
|
601
658
|
if (changes.modelAllowlist && typeof changes.modelAllowlist === "object") {
|
|
602
659
|
const allowlist = changes.modelAllowlist;
|
|
@@ -619,6 +676,20 @@ var OrchestratorPool = class {
|
|
|
619
676
|
if (selection?.recall) settings.switchRecall = selection.recall;
|
|
620
677
|
return settings;
|
|
621
678
|
}
|
|
679
|
+
/**
|
|
680
|
+
* A vendor whose credential is gone keeps no list to route a model to it,
|
|
681
|
+
* or the next turn would be sent to a planner the settings response no
|
|
682
|
+
* longer names. Pruned rather than cleared: the vendors that still hold a
|
|
683
|
+
* key keep routing their models until the catalog is discovered again, so
|
|
684
|
+
* adding a second key does not move the planner off the first.
|
|
685
|
+
*/
|
|
686
|
+
dropUnkeyedProviderLists() {
|
|
687
|
+
if (!this.cachedProviderLists) return;
|
|
688
|
+
const keyed = new Set(core.configuredProviders(new WebConfig()));
|
|
689
|
+
this.cachedProviderLists = Object.fromEntries(
|
|
690
|
+
Object.entries(this.cachedProviderLists).filter(([provider]) => keyed.has(provider))
|
|
691
|
+
);
|
|
692
|
+
}
|
|
622
693
|
setOrchestratorModel(model) {
|
|
623
694
|
process.env.ORCHESTRATOR_MODEL = model;
|
|
624
695
|
return model;
|
|
@@ -651,14 +722,14 @@ var OrchestratorPool = class {
|
|
|
651
722
|
models: allModels,
|
|
652
723
|
modelsByRunner,
|
|
653
724
|
modesByRunner: core.runnerModesFrom(this.registry, runnerIds),
|
|
654
|
-
orchestratorModel:
|
|
725
|
+
orchestratorModel: this.plannerConfig().orchestratorModel,
|
|
655
726
|
providers,
|
|
656
727
|
orchestratorModels,
|
|
657
728
|
providerErrors
|
|
658
729
|
};
|
|
659
730
|
}
|
|
660
731
|
getRunnerState() {
|
|
661
|
-
const config =
|
|
732
|
+
const config = this.plannerConfig();
|
|
662
733
|
return { enabledRunners: config.enabledRunners, orchestratorModel: config.orchestratorModel };
|
|
663
734
|
}
|
|
664
735
|
/**
|
|
@@ -667,9 +738,11 @@ var OrchestratorPool = class {
|
|
|
667
738
|
* runner can't be spawned.
|
|
668
739
|
*/
|
|
669
740
|
async getInstalledRunners() {
|
|
741
|
+
await this.codexSandbox.refresh();
|
|
742
|
+
this.runnerInstallation.refresh?.();
|
|
743
|
+
await this.detectInstalledPlanner();
|
|
670
744
|
const config = new WebConfig({ enabledRunners: this.enabledRunnerOverride() });
|
|
671
745
|
const enabled = new Set(config.enabledRunners);
|
|
672
|
-
await this.codexSandbox.refresh();
|
|
673
746
|
const runners = this.registry.list();
|
|
674
747
|
const installedIds = new Set(
|
|
675
748
|
await this.runnerInstallation.filterInstalled(runners.map((p) => p.manifest.name))
|
|
@@ -681,11 +754,20 @@ var OrchestratorPool = class {
|
|
|
681
754
|
}));
|
|
682
755
|
}
|
|
683
756
|
async generatePlan(sessionId, goal, runners, workspace, modelOverride, options) {
|
|
757
|
+
const session = this.openPlanningSession(sessionId, workspace, modelOverride, options);
|
|
758
|
+
return core.migratePlanState(await this.firstTurn(sessionId, session, () => session.generatePlan(goal, runners)));
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Build the session a new planning conversation runs in and register it in
|
|
762
|
+
* place of any live one, for {@link firstTurn} to run its first turn on.
|
|
763
|
+
*/
|
|
764
|
+
openPlanningSession(sessionId, workspace, modelOverride, options) {
|
|
684
765
|
const session = this.createSessionFor(sessionId, workspace, modelOverride, options);
|
|
766
|
+
if (core.installedPlannerAnswered()) session.assertPlannerReady();
|
|
685
767
|
this.destroy(sessionId);
|
|
686
768
|
this.sessions.set(sessionId, session);
|
|
687
769
|
this.warmCodexSandbox();
|
|
688
|
-
return
|
|
770
|
+
return session;
|
|
689
771
|
}
|
|
690
772
|
/**
|
|
691
773
|
* Conversational planning (ADR-0002): start the planner dialogue for a
|
|
@@ -694,13 +776,16 @@ var OrchestratorPool = class {
|
|
|
694
776
|
* assistant message lives in conversationHistory).
|
|
695
777
|
*/
|
|
696
778
|
async startPlanning(sessionId, goal, runners, workspace, modelOverride, options) {
|
|
697
|
-
const session = this.
|
|
698
|
-
this.
|
|
699
|
-
this.sessions.set(sessionId, session);
|
|
700
|
-
this.warmCodexSandbox();
|
|
701
|
-
return session.startPlanning(goal, runners);
|
|
779
|
+
const session = this.openPlanningSession(sessionId, workspace, modelOverride, options);
|
|
780
|
+
return this.firstTurn(sessionId, session, () => session.startPlanning(goal, runners));
|
|
702
781
|
}
|
|
703
|
-
/**
|
|
782
|
+
/**
|
|
783
|
+
* Continue the planner dialogue with the user's reply: queued behind the
|
|
784
|
+
* first turn, even while it waits for the installed-planner probe, and
|
|
785
|
+
* refused only while a turn of another kind holds the conversation. The
|
|
786
|
+
* session's own planner gate waits for the probe and judges the planner, as
|
|
787
|
+
* it does for every turn.
|
|
788
|
+
*/
|
|
704
789
|
async continuePlanning(sessionId, message) {
|
|
705
790
|
return this.session(sessionId).continueConversation(message);
|
|
706
791
|
}
|
|
@@ -788,6 +873,7 @@ function classify(err) {
|
|
|
788
873
|
if (err instanceof core.AlreadyExecutingError) return { status: 409, code: "already_executing" };
|
|
789
874
|
if (err instanceof core.WorkspaceNotAProjectError) return { status: 400, code: "workspace_not_a_project" };
|
|
790
875
|
if (err instanceof core.WorkspaceNotFoundError) return { status: 400, code: "workspace_not_found" };
|
|
876
|
+
if (err instanceof core.PlannerNotReadyError) return { status: 409, code: "planner_not_ready" };
|
|
791
877
|
if (err instanceof core.PlannerTurnStoppedError) return { status: 409, code: "planner_turn_stopped" };
|
|
792
878
|
if (err instanceof core.PlannerTurnDiscardedError) return { status: 409, code: "planner_turn_discarded" };
|
|
793
879
|
if (err instanceof core.ConversationBusyError) return { status: 409, code: "conversation_busy" };
|
|
@@ -807,7 +893,8 @@ function failure(c, err, label, { fallback = 500, message = "Internal error" } =
|
|
|
807
893
|
const body = {
|
|
808
894
|
error: err instanceof Error && err.message || message,
|
|
809
895
|
...classified ? { code: classified.code } : {},
|
|
810
|
-
...err instanceof core.WorkspaceNotAProjectError ? { workspace: err.workspace } : {}
|
|
896
|
+
...err instanceof core.WorkspaceNotAProjectError ? { workspace: err.workspace } : {},
|
|
897
|
+
...err instanceof core.PlannerNotReadyError ? { plannerNotReady: err.reason } : {}
|
|
811
898
|
};
|
|
812
899
|
return c.json(body, status);
|
|
813
900
|
}
|
|
@@ -860,7 +947,7 @@ function sessionsRoute(pool2) {
|
|
|
860
947
|
});
|
|
861
948
|
return router;
|
|
862
949
|
}
|
|
863
|
-
var NO_CHECKPOINT = "The task is not waiting at a checkpoint
|
|
950
|
+
var NO_CHECKPOINT = "The task is not waiting at a checkpoint: it was answered already, withdrawn, or has no runner left to hear it.";
|
|
864
951
|
async function runUserCommand(session, command) {
|
|
865
952
|
const outcome = await session.execute(command, { kind: "user" });
|
|
866
953
|
if (!outcome.ok) throw new core.TaskControlError(outcome.message);
|
|
@@ -874,7 +961,7 @@ function plansRoute(pool2) {
|
|
|
874
961
|
if (!goal) return c.json({ error: "goal is required" }, 400);
|
|
875
962
|
const ws = workspace || c.req.query("workspace") || process.cwd();
|
|
876
963
|
const queryRunners = c.req.query("runners");
|
|
877
|
-
const runnerList = Array.isArray(runners) ? runners : runners ? [runners] : queryRunners ? queryRunners.split(",").map((s) => s.trim()).filter(Boolean) :
|
|
964
|
+
const runnerList = Array.isArray(runners) ? runners : runners ? [runners] : queryRunners ? queryRunners.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
878
965
|
const plan = await pool2.generatePlan(c.req.param("sessionId"), goal, runnerList, ws, model, { allowInit });
|
|
879
966
|
const { models, modelsByRunner } = await pool2.getProviderModels();
|
|
880
967
|
const notes = (pool2.session(c.req.param("sessionId")).planState?.conversationHistory ?? []).filter((e) => e.kind === "system").map((e) => e.content);
|
|
@@ -897,7 +984,9 @@ function plansRoute(pool2) {
|
|
|
897
984
|
router.post("/:sessionId/stop", async (c) => {
|
|
898
985
|
try {
|
|
899
986
|
if (pool2.hasSession(c.req.param("sessionId"))) {
|
|
900
|
-
|
|
987
|
+
const session = pool2.session(c.req.param("sessionId"));
|
|
988
|
+
const outcome = await session.execute({ command: "stop_run" }, { kind: "user" });
|
|
989
|
+
if (!outcome.ok && (session.isExecuting || session.startsPending)) throw new core.TaskControlError(outcome.message);
|
|
901
990
|
}
|
|
902
991
|
return c.json({ status: "stopped" });
|
|
903
992
|
} catch (err) {
|
|
@@ -1181,7 +1270,7 @@ function plansRoute(pool2) {
|
|
|
1181
1270
|
const { goal, runners, workspace, model, allowInit } = await c.req.json();
|
|
1182
1271
|
if (!goal) return c.json({ error: "goal is required" }, 400);
|
|
1183
1272
|
const ws = workspace || c.req.query("workspace") || process.cwd();
|
|
1184
|
-
const runnerList = Array.isArray(runners) ? runners : runners ? [runners] :
|
|
1273
|
+
const runnerList = Array.isArray(runners) ? runners : runners ? [runners] : void 0;
|
|
1185
1274
|
const plan = await pool2.startPlanning(c.req.param("sessionId"), goal, runnerList, ws, model, { allowInit });
|
|
1186
1275
|
return c.json({ plan: core.surfacePlan(plan) });
|
|
1187
1276
|
} catch (err) {
|
|
@@ -1251,14 +1340,15 @@ function plansRoute(pool2) {
|
|
|
1251
1340
|
function runnersRoute(pool2) {
|
|
1252
1341
|
const router = new hono.Hono();
|
|
1253
1342
|
router.get("/", async (c) => {
|
|
1254
|
-
const state = pool2.getRunnerState();
|
|
1255
1343
|
const runners = await pool2.getInstalledRunners();
|
|
1344
|
+
const state = pool2.getRunnerState();
|
|
1256
1345
|
return c.json({
|
|
1257
1346
|
runners,
|
|
1258
1347
|
orchestratorModel: state.orchestratorModel
|
|
1259
1348
|
});
|
|
1260
1349
|
});
|
|
1261
1350
|
router.put("/:id", async (c) => {
|
|
1351
|
+
await core.installedPlannerDetected();
|
|
1262
1352
|
const id = c.req.param("id");
|
|
1263
1353
|
const body = await c.req.json();
|
|
1264
1354
|
pool2.setRunnerEnabled(id, body.enabled);
|
|
@@ -1314,7 +1404,7 @@ function approvalsRoute(pool2) {
|
|
|
1314
1404
|
);
|
|
1315
1405
|
if (!outcome.ok) return refuse(c, 400, outcome.message, "refused");
|
|
1316
1406
|
} else if (!pool2.resolveApproval(sessionId, approvalId, answer)) {
|
|
1317
|
-
return refuse(c, 409, "Approval is no longer outstanding
|
|
1407
|
+
return refuse(c, 409, "Approval is no longer outstanding: it timed out or was already answered.", "approval_not_outstanding");
|
|
1318
1408
|
}
|
|
1319
1409
|
return c.json({ ok: true, granted: core.isGranted(decided), decision: decided.decision });
|
|
1320
1410
|
});
|
|
@@ -1322,11 +1412,13 @@ function approvalsRoute(pool2) {
|
|
|
1322
1412
|
}
|
|
1323
1413
|
function settingsRoute(pool2) {
|
|
1324
1414
|
const router = new hono.Hono();
|
|
1325
|
-
router.get("/", (c) => {
|
|
1415
|
+
router.get("/", async (c) => {
|
|
1416
|
+
await core.installedPlannerDetected();
|
|
1326
1417
|
const settings = pool2.getSettings();
|
|
1327
1418
|
return c.json(settings);
|
|
1328
1419
|
});
|
|
1329
1420
|
router.patch("/", async (c) => {
|
|
1421
|
+
await core.installedPlannerDetected();
|
|
1330
1422
|
const body = await c.req.json();
|
|
1331
1423
|
if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).length === 0) {
|
|
1332
1424
|
return refuse(c, 400, "At least one setting field is required");
|
|
@@ -1378,7 +1470,15 @@ function createWsHandler(pool2) {
|
|
|
1378
1470
|
});
|
|
1379
1471
|
ws.send(JSON.stringify({ type: "connected", sessionId }));
|
|
1380
1472
|
const view = pool2.runSnapshot(sessionId);
|
|
1381
|
-
if (view)
|
|
1473
|
+
if (view) {
|
|
1474
|
+
ws.send(JSON.stringify({
|
|
1475
|
+
type: "run_snapshot",
|
|
1476
|
+
sessionId,
|
|
1477
|
+
view,
|
|
1478
|
+
approvals: pool2.announcedApprovals(sessionId),
|
|
1479
|
+
...pool2.streamPosition(sessionId)
|
|
1480
|
+
}));
|
|
1481
|
+
}
|
|
1382
1482
|
pool2.subscribe(sessionId, ws);
|
|
1383
1483
|
});
|
|
1384
1484
|
return (request, socket, head) => {
|
|
@@ -1552,6 +1652,7 @@ if (portIdx !== -1 && args[portIdx + 1]) {
|
|
|
1552
1652
|
var watchParentIdx = args.indexOf("--watch-parent");
|
|
1553
1653
|
var watchParentPid = watchParentIdx !== -1 ? parseInt(args[watchParentIdx + 1], 10) : void 0;
|
|
1554
1654
|
var pool = new OrchestratorPool({ runner: new core.StructuredRunner() });
|
|
1655
|
+
void pool.detectInstalledPlanner();
|
|
1555
1656
|
var server = http.createServer();
|
|
1556
1657
|
var ownsToken = false;
|
|
1557
1658
|
server.listen(port, "127.0.0.1", () => {
|