@ordewell/web 0.10.1 → 0.10.3
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 +173 -73
- package/dist/server/main.js.map +1 -1
- package/package.json +4 -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
|
-
const explicit = explicitEnv;
|
|
61
|
-
if (explicit && core.isCliProvider(explicit)) {
|
|
62
|
-
this._provider = explicit;
|
|
63
|
-
this._providerCacheKey = cacheKey;
|
|
64
|
-
this._providerCacheLists = this._providerModelLists;
|
|
65
|
-
return this._provider;
|
|
66
|
-
}
|
|
67
|
-
if (model && this._providerModelLists) {
|
|
68
|
-
const resolved = core.resolveProvider(model, this._providerModelLists);
|
|
69
|
-
if (resolved) {
|
|
70
|
-
this._provider = resolved;
|
|
71
|
-
this._providerCacheKey = cacheKey;
|
|
72
|
-
this._providerCacheLists = this._providerModelLists;
|
|
73
|
-
return this._provider;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
this._provider = core.BaseConfig.detectProvider("openrouter");
|
|
77
|
-
this._providerCacheKey = cacheKey;
|
|
78
|
-
this._providerCacheLists = this._providerModelLists;
|
|
79
|
-
return this._provider;
|
|
76
|
+
return this.planner.provider;
|
|
77
|
+
}
|
|
78
|
+
get plannerThinkingEffort() {
|
|
79
|
+
return this.planner.effort || void 0;
|
|
80
80
|
}
|
|
81
81
|
get apiKey() {
|
|
82
|
-
|
|
82
|
+
const provider = this.aiProvider;
|
|
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
|
/**
|
|
@@ -681,11 +752,20 @@ var OrchestratorPool = class {
|
|
|
681
752
|
}));
|
|
682
753
|
}
|
|
683
754
|
async generatePlan(sessionId, goal, runners, workspace, modelOverride, options) {
|
|
755
|
+
const session = this.openPlanningSession(sessionId, workspace, modelOverride, options);
|
|
756
|
+
return core.migratePlanState(await this.firstTurn(sessionId, session, () => session.generatePlan(goal, runners)));
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Build the session a new planning conversation runs in and register it in
|
|
760
|
+
* place of any live one, for {@link firstTurn} to run its first turn on.
|
|
761
|
+
*/
|
|
762
|
+
openPlanningSession(sessionId, workspace, modelOverride, options) {
|
|
684
763
|
const session = this.createSessionFor(sessionId, workspace, modelOverride, options);
|
|
764
|
+
if (core.installedPlannerAnswered()) session.assertPlannerReady();
|
|
685
765
|
this.destroy(sessionId);
|
|
686
766
|
this.sessions.set(sessionId, session);
|
|
687
767
|
this.warmCodexSandbox();
|
|
688
|
-
return
|
|
768
|
+
return session;
|
|
689
769
|
}
|
|
690
770
|
/**
|
|
691
771
|
* Conversational planning (ADR-0002): start the planner dialogue for a
|
|
@@ -694,13 +774,16 @@ var OrchestratorPool = class {
|
|
|
694
774
|
* assistant message lives in conversationHistory).
|
|
695
775
|
*/
|
|
696
776
|
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);
|
|
777
|
+
const session = this.openPlanningSession(sessionId, workspace, modelOverride, options);
|
|
778
|
+
return this.firstTurn(sessionId, session, () => session.startPlanning(goal, runners));
|
|
702
779
|
}
|
|
703
|
-
/**
|
|
780
|
+
/**
|
|
781
|
+
* Continue the planner dialogue with the user's reply: queued behind the
|
|
782
|
+
* first turn, even while it waits for the installed-planner probe, and
|
|
783
|
+
* refused only while a turn of another kind holds the conversation. The
|
|
784
|
+
* session's own planner gate waits for the probe and judges the planner, as
|
|
785
|
+
* it does for every turn.
|
|
786
|
+
*/
|
|
704
787
|
async continuePlanning(sessionId, message) {
|
|
705
788
|
return this.session(sessionId).continueConversation(message);
|
|
706
789
|
}
|
|
@@ -788,6 +871,7 @@ function classify(err) {
|
|
|
788
871
|
if (err instanceof core.AlreadyExecutingError) return { status: 409, code: "already_executing" };
|
|
789
872
|
if (err instanceof core.WorkspaceNotAProjectError) return { status: 400, code: "workspace_not_a_project" };
|
|
790
873
|
if (err instanceof core.WorkspaceNotFoundError) return { status: 400, code: "workspace_not_found" };
|
|
874
|
+
if (err instanceof core.PlannerNotReadyError) return { status: 409, code: "planner_not_ready" };
|
|
791
875
|
if (err instanceof core.PlannerTurnStoppedError) return { status: 409, code: "planner_turn_stopped" };
|
|
792
876
|
if (err instanceof core.PlannerTurnDiscardedError) return { status: 409, code: "planner_turn_discarded" };
|
|
793
877
|
if (err instanceof core.ConversationBusyError) return { status: 409, code: "conversation_busy" };
|
|
@@ -807,7 +891,8 @@ function failure(c, err, label, { fallback = 500, message = "Internal error" } =
|
|
|
807
891
|
const body = {
|
|
808
892
|
error: err instanceof Error && err.message || message,
|
|
809
893
|
...classified ? { code: classified.code } : {},
|
|
810
|
-
...err instanceof core.WorkspaceNotAProjectError ? { workspace: err.workspace } : {}
|
|
894
|
+
...err instanceof core.WorkspaceNotAProjectError ? { workspace: err.workspace } : {},
|
|
895
|
+
...err instanceof core.PlannerNotReadyError ? { plannerNotReady: err.reason } : {}
|
|
811
896
|
};
|
|
812
897
|
return c.json(body, status);
|
|
813
898
|
}
|
|
@@ -860,7 +945,7 @@ function sessionsRoute(pool2) {
|
|
|
860
945
|
});
|
|
861
946
|
return router;
|
|
862
947
|
}
|
|
863
|
-
var NO_CHECKPOINT = "The task is not waiting at a checkpoint
|
|
948
|
+
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
949
|
async function runUserCommand(session, command) {
|
|
865
950
|
const outcome = await session.execute(command, { kind: "user" });
|
|
866
951
|
if (!outcome.ok) throw new core.TaskControlError(outcome.message);
|
|
@@ -874,7 +959,7 @@ function plansRoute(pool2) {
|
|
|
874
959
|
if (!goal) return c.json({ error: "goal is required" }, 400);
|
|
875
960
|
const ws = workspace || c.req.query("workspace") || process.cwd();
|
|
876
961
|
const queryRunners = c.req.query("runners");
|
|
877
|
-
const runnerList = Array.isArray(runners) ? runners : runners ? [runners] : queryRunners ? queryRunners.split(",").map((s) => s.trim()).filter(Boolean) :
|
|
962
|
+
const runnerList = Array.isArray(runners) ? runners : runners ? [runners] : queryRunners ? queryRunners.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
878
963
|
const plan = await pool2.generatePlan(c.req.param("sessionId"), goal, runnerList, ws, model, { allowInit });
|
|
879
964
|
const { models, modelsByRunner } = await pool2.getProviderModels();
|
|
880
965
|
const notes = (pool2.session(c.req.param("sessionId")).planState?.conversationHistory ?? []).filter((e) => e.kind === "system").map((e) => e.content);
|
|
@@ -897,7 +982,9 @@ function plansRoute(pool2) {
|
|
|
897
982
|
router.post("/:sessionId/stop", async (c) => {
|
|
898
983
|
try {
|
|
899
984
|
if (pool2.hasSession(c.req.param("sessionId"))) {
|
|
900
|
-
|
|
985
|
+
const session = pool2.session(c.req.param("sessionId"));
|
|
986
|
+
const outcome = await session.execute({ command: "stop_run" }, { kind: "user" });
|
|
987
|
+
if (!outcome.ok && (session.isExecuting || session.startsPending)) throw new core.TaskControlError(outcome.message);
|
|
901
988
|
}
|
|
902
989
|
return c.json({ status: "stopped" });
|
|
903
990
|
} catch (err) {
|
|
@@ -1181,7 +1268,7 @@ function plansRoute(pool2) {
|
|
|
1181
1268
|
const { goal, runners, workspace, model, allowInit } = await c.req.json();
|
|
1182
1269
|
if (!goal) return c.json({ error: "goal is required" }, 400);
|
|
1183
1270
|
const ws = workspace || c.req.query("workspace") || process.cwd();
|
|
1184
|
-
const runnerList = Array.isArray(runners) ? runners : runners ? [runners] :
|
|
1271
|
+
const runnerList = Array.isArray(runners) ? runners : runners ? [runners] : void 0;
|
|
1185
1272
|
const plan = await pool2.startPlanning(c.req.param("sessionId"), goal, runnerList, ws, model, { allowInit });
|
|
1186
1273
|
return c.json({ plan: core.surfacePlan(plan) });
|
|
1187
1274
|
} catch (err) {
|
|
@@ -1251,6 +1338,7 @@ function plansRoute(pool2) {
|
|
|
1251
1338
|
function runnersRoute(pool2) {
|
|
1252
1339
|
const router = new hono.Hono();
|
|
1253
1340
|
router.get("/", async (c) => {
|
|
1341
|
+
await core.installedPlannerDetected();
|
|
1254
1342
|
const state = pool2.getRunnerState();
|
|
1255
1343
|
const runners = await pool2.getInstalledRunners();
|
|
1256
1344
|
return c.json({
|
|
@@ -1259,6 +1347,7 @@ function runnersRoute(pool2) {
|
|
|
1259
1347
|
});
|
|
1260
1348
|
});
|
|
1261
1349
|
router.put("/:id", async (c) => {
|
|
1350
|
+
await core.installedPlannerDetected();
|
|
1262
1351
|
const id = c.req.param("id");
|
|
1263
1352
|
const body = await c.req.json();
|
|
1264
1353
|
pool2.setRunnerEnabled(id, body.enabled);
|
|
@@ -1314,7 +1403,7 @@ function approvalsRoute(pool2) {
|
|
|
1314
1403
|
);
|
|
1315
1404
|
if (!outcome.ok) return refuse(c, 400, outcome.message, "refused");
|
|
1316
1405
|
} else if (!pool2.resolveApproval(sessionId, approvalId, answer)) {
|
|
1317
|
-
return refuse(c, 409, "Approval is no longer outstanding
|
|
1406
|
+
return refuse(c, 409, "Approval is no longer outstanding: it timed out or was already answered.", "approval_not_outstanding");
|
|
1318
1407
|
}
|
|
1319
1408
|
return c.json({ ok: true, granted: core.isGranted(decided), decision: decided.decision });
|
|
1320
1409
|
});
|
|
@@ -1322,11 +1411,13 @@ function approvalsRoute(pool2) {
|
|
|
1322
1411
|
}
|
|
1323
1412
|
function settingsRoute(pool2) {
|
|
1324
1413
|
const router = new hono.Hono();
|
|
1325
|
-
router.get("/", (c) => {
|
|
1414
|
+
router.get("/", async (c) => {
|
|
1415
|
+
await core.installedPlannerDetected();
|
|
1326
1416
|
const settings = pool2.getSettings();
|
|
1327
1417
|
return c.json(settings);
|
|
1328
1418
|
});
|
|
1329
1419
|
router.patch("/", async (c) => {
|
|
1420
|
+
await core.installedPlannerDetected();
|
|
1330
1421
|
const body = await c.req.json();
|
|
1331
1422
|
if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).length === 0) {
|
|
1332
1423
|
return refuse(c, 400, "At least one setting field is required");
|
|
@@ -1378,7 +1469,15 @@ function createWsHandler(pool2) {
|
|
|
1378
1469
|
});
|
|
1379
1470
|
ws.send(JSON.stringify({ type: "connected", sessionId }));
|
|
1380
1471
|
const view = pool2.runSnapshot(sessionId);
|
|
1381
|
-
if (view)
|
|
1472
|
+
if (view) {
|
|
1473
|
+
ws.send(JSON.stringify({
|
|
1474
|
+
type: "run_snapshot",
|
|
1475
|
+
sessionId,
|
|
1476
|
+
view,
|
|
1477
|
+
approvals: pool2.announcedApprovals(sessionId),
|
|
1478
|
+
...pool2.streamPosition(sessionId)
|
|
1479
|
+
}));
|
|
1480
|
+
}
|
|
1382
1481
|
pool2.subscribe(sessionId, ws);
|
|
1383
1482
|
});
|
|
1384
1483
|
return (request, socket, head) => {
|
|
@@ -1552,6 +1651,7 @@ if (portIdx !== -1 && args[portIdx + 1]) {
|
|
|
1552
1651
|
var watchParentIdx = args.indexOf("--watch-parent");
|
|
1553
1652
|
var watchParentPid = watchParentIdx !== -1 ? parseInt(args[watchParentIdx + 1], 10) : void 0;
|
|
1554
1653
|
var pool = new OrchestratorPool({ runner: new core.StructuredRunner() });
|
|
1654
|
+
void pool.detectInstalledPlanner();
|
|
1555
1655
|
var server = http.createServer();
|
|
1556
1656
|
var ownsToken = false;
|
|
1557
1657
|
server.listen(port, "127.0.0.1", () => {
|