@alisio/server 0.1.0-alpha.8 → 0.1.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/bridges/interaction-bridge.js +13 -4
- package/dist/host/goal.d.ts +36 -0
- package/dist/host/goal.js +172 -0
- package/dist/host/plan.d.ts +10 -0
- package/dist/host/plan.js +45 -0
- package/dist/host/presets.d.ts +1 -1
- package/dist/host/presets.js +13 -4
- package/dist/host/run-scheduler.d.ts +25 -2
- package/dist/host/run-scheduler.js +18 -3
- package/dist/host/sessions.d.ts +10 -1
- package/dist/host/sessions.js +26 -5
- package/dist/host/workspace-host.d.ts +19 -4
- package/dist/host/workspace-host.js +63 -7
- package/dist/http/errors.js +7 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +178 -10
- package/dist/routes/changelog.d.ts +9 -0
- package/dist/routes/changelog.js +32 -0
- package/dist/routes/commands.d.ts +8 -0
- package/dist/routes/commands.js +137 -12
- package/dist/routes/goal.d.ts +21 -0
- package/dist/routes/goal.js +121 -0
- package/dist/routes/management.d.ts +12 -2
- package/dist/routes/management.js +49 -6
- package/dist/routes/plugin-views.d.ts +22 -0
- package/dist/routes/plugin-views.js +96 -0
- package/dist/routes/sessions.js +2 -0
- package/dist/routes/tasks.d.ts +5 -0
- package/dist/routes/tasks.js +70 -0
- package/dist/web/assets/AgentPicker-CgYzrrsh.js +1 -0
- package/dist/web/assets/AgentsModal-DQQp4Vj3.js +2 -0
- package/dist/web/assets/ArtifactPanel-I6u3qByq.js +2 -0
- package/dist/web/assets/BtwPanel-GypuLgxx.js +1 -0
- package/dist/web/assets/ChangelogDialog-BicA9ah6.css +1 -0
- package/dist/web/assets/ChangelogDialog-Cv0qoFca.js +1 -0
- package/dist/web/assets/CopyButton-B3PSPdbl.js +1 -0
- package/dist/web/assets/GoalBar-CHV3_kfx.css +1 -0
- package/dist/web/assets/GoalBar-DBE7KAva.js +1 -0
- package/dist/web/assets/MemoryTab-BDfJaFot.js +2 -0
- package/dist/web/assets/MemoryTab-BMdPbUkv.css +1 -0
- package/dist/web/assets/PermissionsMenu-BgOVGPRL.js +1 -0
- package/dist/web/assets/PlanReviewPanel-BNJgw4ah.js +1 -0
- package/dist/web/assets/PlanReviewPanel-DIB4EV2K.css +1 -0
- package/dist/web/assets/SettingsModal-D9zkXUeA.js +1 -0
- package/dist/web/assets/SpreadsheetPanel-C3BQhxhv.js +1 -0
- package/dist/web/assets/TasksTab-D8nzSXEr.js +1 -0
- package/dist/web/assets/TasksTab-xvZOft9V.css +1 -0
- package/dist/web/assets/TrajectoryTab-CH7ttdZT.js +1 -0
- package/dist/web/assets/TrajectoryTab-PJHG3yJv.css +1 -0
- package/dist/web/assets/{abnfDiagram-VCTEODGH-BSbQI796.js → abnfDiagram-VCTEODGH-B8huo-Kk.js} +1 -1
- package/dist/web/assets/{agents.module-B4txg5O-.js → agents.module-YUJKpT3J.js} +2 -2
- package/dist/web/assets/app-D6BT6vS-.js +3 -0
- package/dist/web/assets/architecture-7GRP2DOG-DnGQjmPz.js +1 -0
- package/dist/web/assets/{architectureDiagram-5GKGNRK7-B_aO4VWn.js → architectureDiagram-5GKGNRK7-BqGd8I5j.js} +1 -1
- package/dist/web/assets/{blockDiagram-I7D4REHJ-D0iOY295.js → blockDiagram-I7D4REHJ-DtQSxJUd.js} +1 -1
- package/dist/web/assets/{c4Diagram-7LVT6UL2-CQpmIVVf.js → c4Diagram-7LVT6UL2-BuNBp5yh.js} +1 -1
- package/dist/web/assets/channel-CEI3etVU.js +1 -0
- package/dist/web/assets/{chunk-4HAMMTFA-Y_4-541N.js → chunk-4HAMMTFA-390Mn_il.js} +1 -1
- package/dist/web/assets/{chunk-75Z2AOVW-4xYGp387.js → chunk-75Z2AOVW-Chht6Xhu.js} +1 -1
- package/dist/web/assets/chunk-DU6HZSFF-CMLEkaIG.js +125 -0
- package/dist/web/assets/{chunk-F27PBJKO-Cl_dLboe.js → chunk-F27PBJKO-SKMSua5B.js} +1 -1
- package/dist/web/assets/{chunk-GMAD6QVW-CjWWJjqH.js → chunk-GMAD6QVW-Df-qsmXX.js} +1 -1
- package/dist/web/assets/{chunk-GVQU2GXP-DsUbfXw0.js → chunk-GVQU2GXP-7PgapOOA.js} +1 -1
- package/dist/web/assets/{chunk-IMKFNOWR-CaH0S04F.js → chunk-IMKFNOWR-DRbZAlkh.js} +1 -1
- package/dist/web/assets/{chunk-L3NEJ4N5-BXAFNAb0.js → chunk-L3NEJ4N5-3E0ssqUP.js} +1 -1
- package/dist/web/assets/{chunk-OSK3NFVY-7Pr5dXXm.js → chunk-OSK3NFVY-B1GP22y4.js} +1 -1
- package/dist/web/assets/{chunk-P2QGCYS3-BbUBGz5z.js → chunk-P2QGCYS3-T_PO2L1c.js} +1 -1
- package/dist/web/assets/{chunk-POPQ4Y6H-CzsriQoR.js → chunk-POPQ4Y6H-DiREf_on.js} +1 -1
- package/dist/web/assets/{chunk-PWAF6VOD-tT3Jq6dq.js → chunk-PWAF6VOD-yOR4r5Wg.js} +1 -1
- package/dist/web/assets/{chunk-SHT3W25Y-DZj7IKKK.js → chunk-SHT3W25Y-LqpptdEy.js} +1 -1
- package/dist/web/assets/{chunk-SVP7TREG-DvDxDsmp.js → chunk-SVP7TREG-BkiLiJog.js} +1 -1
- package/dist/web/assets/{chunk-TICWLB2K-Bc1tYRg0.js → chunk-TICWLB2K-F9gzgGhl.js} +1 -1
- package/dist/web/assets/classDiagram-ZZMXUADV-B7WS1Q75.js +1 -0
- package/dist/web/assets/classDiagram-v2-VYDZK3BY-B7WS1Q75.js +1 -0
- package/dist/web/assets/{cynefin-OW5HDTMX-DnntdLgu.js → cynefin-OW5HDTMX-B_NT3eSJ.js} +1 -1
- package/dist/web/assets/{cynefinDiagram-5FMLGOSQ-C-L_BMup.js → cynefinDiagram-5FMLGOSQ-DpNbosPc.js} +1 -1
- package/dist/web/assets/{dagre-GXQ25YYZ-Bnq701MK.js → dagre-GXQ25YYZ-BgaTchbz.js} +1 -1
- package/dist/web/assets/{diagram-S7CK7UJ4-CNM_tSLn.js → diagram-S7CK7UJ4-BC0d5yAz.js} +1 -1
- package/dist/web/assets/{diagram-UQ7AKVKN-426C_diL.js → diagram-UQ7AKVKN-Bjm2wIY2.js} +1 -1
- package/dist/web/assets/{diagram-VSXAHHWV-t87gH2-E.js → diagram-VSXAHHWV-BcCWYw5W.js} +1 -1
- package/dist/web/assets/{diagram-VX7I27RA-DIRLLYsO.js → diagram-VX7I27RA-CzGzOQ_X.js} +1 -1
- package/dist/web/assets/{diagram-Z3DM3KII-RkCiIzTw.js → diagram-Z3DM3KII-CJZ8iKLl.js} +1 -1
- package/dist/web/assets/{ebnfDiagram-PWID7BFC-BxwCYlSo.js → ebnfDiagram-PWID7BFC-DKTo52mJ.js} +1 -1
- package/dist/web/assets/{engine-BvPI6I9U.js → engine-DE1KkajI.js} +4 -4
- package/dist/web/assets/{erDiagram-RLTQ6QDP-CjSDwe_l.js → erDiagram-RLTQ6QDP-Cssct0LD.js} +1 -1
- package/dist/web/assets/es-Bz5daKQI.js +3 -0
- package/dist/web/assets/eventmodeling-NTZA5JFV-Cro5xJnm.js +1 -0
- package/dist/web/assets/flowDiagram-HODETNUW-BiyE5zC5.js +1 -0
- package/dist/web/assets/{ganttDiagram-EL5Y4UJY-CVCoGOMO.js → ganttDiagram-EL5Y4UJY-DO1wNnfK.js} +1 -1
- package/dist/web/assets/{gitGraph-4MIJSDKK-G0WgwH1G.js → gitGraph-4MIJSDKK-CjTm6VaR.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-WWUBYQGX-91LuiqmR.js → gitGraphDiagram-WWUBYQGX-DoNpr-oE.js} +1 -1
- package/dist/web/assets/goal-BaR-xr88.js +1 -0
- package/dist/web/assets/{highlight-CTIGnBaH.js → highlight-C4F9bqUR.js} +1 -1
- package/dist/web/assets/i18n-UlAtkmCh.js +3 -0
- package/dist/web/assets/{icons-DIySg7KB.js → icons-BED7nKDs.js} +1 -1
- package/dist/web/assets/index-BTLOG0A7.css +1 -0
- package/dist/web/assets/index-CueRFPg9.js +74 -0
- package/dist/web/assets/{info-A6RAGUB7-Cd1A2Eag.js → info-A6RAGUB7-D0RLRe9r.js} +1 -1
- package/dist/web/assets/{infoDiagram-27XIBGKW-BS-OvxCz.js → infoDiagram-27XIBGKW-Xvh_yq1U.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-5VMMS53U-Bv6pOCVs.js → ishikawaDiagram-5VMMS53U-CKMcZ3YE.js} +1 -1
- package/dist/web/assets/{journeyDiagram-3NMN7TZE-B1wOa8hE.js → journeyDiagram-3NMN7TZE-CH4U_78p.js} +1 -1
- package/dist/web/assets/jsxRuntime-BPMqVzHG.js +1 -0
- package/dist/web/assets/{kanban-definition-UXKFOSKX-BEGlHqwg.js → kanban-definition-UXKFOSKX-YDba8pTP.js} +1 -1
- package/dist/web/assets/{line-DacvQsry.js → line-5g6gTA1Q.js} +1 -1
- package/dist/web/assets/{mermaid-parser.core-nJHzUWWd.js → mermaid-parser.core-DgCR7nMS.js} +3 -3
- package/dist/web/assets/{mindmap-definition-YA3MSWOX-DYdF-ojf.js → mindmap-definition-YA3MSWOX-BFhIglWE.js} +1 -1
- package/dist/web/assets/{packet-AYTQ26CC-DBiUCtRk.js → packet-AYTQ26CC-Bx6BMrJt.js} +1 -1
- package/dist/web/assets/{pegDiagram-XKGWAZYB-MdKS7UKA.js → pegDiagram-XKGWAZYB-DfFISLjH.js} +1 -1
- package/dist/web/assets/{pie-WAS4IAKB-CZpqMsqb.js → pie-WAS4IAKB-LV0HnA-W.js} +1 -1
- package/dist/web/assets/{pieDiagram-E7YTZNPT-CovIn7J1.js → pieDiagram-E7YTZNPT-CepfvSOf.js} +1 -1
- package/dist/web/assets/preload-helper-B3nfOi5I.js +1 -0
- package/dist/web/assets/{quadrantDiagram-AXDQQJYC-DW5EcMtk.js → quadrantDiagram-AXDQQJYC-xPm2UUev.js} +1 -1
- package/dist/web/assets/{radar-RG4KPBEZ-CN_aPDyn.js → radar-RG4KPBEZ-V5htrwTl.js} +1 -1
- package/dist/web/assets/{railroad-74A4TZTK-C6cv8Byh.js → railroad-74A4TZTK-b1KQjCKl.js} +1 -1
- package/dist/web/assets/railroad-abnf-HS5TGJTU-DGo9EEg0.js +1 -0
- package/dist/web/assets/railroad-ebnf-LZEXJU2U-pz5FmZJB.js +1 -0
- package/dist/web/assets/railroad-peg-WCYAUIDC-CaGbhEvZ.js +1 -0
- package/dist/web/assets/{railroadDiagram-O6MQD6OU-ph8nMiZB.js → railroadDiagram-O6MQD6OU-C7eNLTRf.js} +1 -1
- package/dist/web/assets/{requirementDiagram-BXWQKSXE-BGVse3WU.js → requirementDiagram-BXWQKSXE-CDkBY9hC.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-P5KCCOFB-BlqSlLfF.js → sankeyDiagram-P5KCCOFB-D3kcagKm.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-WJ2MYXX4-DVZcPeDh.js → sequenceDiagram-WJ2MYXX4-4bVDlu88.js} +1 -1
- package/dist/web/assets/settings-DTaiQjCk.css +1 -0
- package/dist/web/assets/settings.module-DoTtRfeo.js +1 -0
- package/dist/web/assets/shared-BLXABM7S.js +1 -0
- package/dist/web/assets/{stateDiagram-D77RDMKH-C71hZhx8.js → stateDiagram-D77RDMKH-ByCmUnvJ.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-MP3YSRHH-CRiIHTdb.js +1 -0
- package/dist/web/assets/{swimlanes-42K2YHIH-BBbsmiO3.js → swimlanes-42K2YHIH-C836H1FT.js} +1 -1
- package/dist/web/assets/swimlanesDiagram-VR7AAH4N-CK7iFVSI.js +8 -0
- package/dist/web/assets/tasks-bt4HCPLT.js +1 -0
- package/dist/web/assets/{timeline-definition-24CTP7MA-C0hBvrpl.js → timeline-definition-24CTP7MA-C4pJeiqC.js} +1 -1
- package/dist/web/assets/{treeView-Q6P3EWNA-D3YSHcSl.js → treeView-Q6P3EWNA-LZesboxH.js} +1 -1
- package/dist/web/assets/{treemap-WGGIJYW6-CGGxdU_V.js → treemap-WGGIJYW6-Cl6Q6_3w.js} +1 -1
- package/dist/web/assets/{vennDiagram-4TSXK5OY-Bddm4uv_.js → vennDiagram-4TSXK5OY-Beflgatq.js} +1 -1
- package/dist/web/assets/view-C6h2Ru4M.js +2 -0
- package/dist/web/assets/view-CMC_gonE.js +1 -0
- package/dist/web/assets/view-CN5B3WOk.js +1 -0
- package/dist/web/assets/{view-C_yQ0o_1.js → view-Cp9Y-lvL.js} +1 -1
- package/dist/web/assets/view-CqbEmEgS.js +5 -0
- package/dist/web/assets/view-DT0FGd26.js +1 -0
- package/dist/web/assets/view-DfL_xTXC.js +6 -0
- package/dist/web/assets/{view-Dzj8qJWx.js → view-DlDHrSuL.js} +2 -2
- package/dist/web/assets/view-rB_-ym6G.js +1 -0
- package/dist/web/assets/visible-DLChI0F-.js +1 -0
- package/dist/web/assets/{wardley-WFR3VGLG-DA15PMFL.js → wardley-WFR3VGLG-BPBKdRq1.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-VM6X3IG4-DdznHZAA.js → wardleyDiagram-VM6X3IG4-BoH1N_OA.js} +1 -1
- package/dist/web/assets/{xychartDiagram-S5SC5T6Z-CoDcKI66.js → xychartDiagram-S5SC5T6Z-B9h0fFPk.js} +1 -1
- package/dist/web/index.html +9 -5
- package/package.json +4 -4
- package/dist/web/assets/AgentPicker-BjlBiN4-.js +0 -1
- package/dist/web/assets/AgentsModal-Byee_0pe.js +0 -2
- package/dist/web/assets/ArtifactPanel-BcSTmXfm.js +0 -2
- package/dist/web/assets/BtwPanel-B2-T36dM.js +0 -1
- package/dist/web/assets/SettingsModal-CJ9ylIBw.js +0 -1
- package/dist/web/assets/SpreadsheetPanel-CE0J9nlK.js +0 -1
- package/dist/web/assets/app-Bg0oN1mF.js +0 -2
- package/dist/web/assets/architecture-7GRP2DOG-Cmrq8Mal.js +0 -1
- package/dist/web/assets/channel-8oC5JRJN.js +0 -1
- package/dist/web/assets/chunk-DU6HZSFF-DXftN_bq.js +0 -125
- package/dist/web/assets/classDiagram-ZZMXUADV-BAAKVLjZ.js +0 -1
- package/dist/web/assets/classDiagram-v2-VYDZK3BY-BAAKVLjZ.js +0 -1
- package/dist/web/assets/eventmodeling-NTZA5JFV-D6SC9raY.js +0 -1
- package/dist/web/assets/flowDiagram-HODETNUW-CmC03NQq.js +0 -1
- package/dist/web/assets/index-CRb-14T4.css +0 -1
- package/dist/web/assets/index-z9ytfY6a.js +0 -74
- package/dist/web/assets/jsxRuntime-Dibtq2co.js +0 -5
- package/dist/web/assets/railroad-abnf-HS5TGJTU-qNOnds0e.js +0 -1
- package/dist/web/assets/railroad-ebnf-LZEXJU2U-CiAajbqk.js +0 -1
- package/dist/web/assets/railroad-peg-WCYAUIDC-DWNjc-vb.js +0 -1
- package/dist/web/assets/shared-AIBQ1IP1.js +0 -1
- package/dist/web/assets/shared-DuS6eVxB.css +0 -1
- package/dist/web/assets/stateDiagram-v2-MP3YSRHH-B_lVvrO8.js +0 -1
- package/dist/web/assets/swimlanesDiagram-VR7AAH4N-_7TOzzT1.js +0 -8
- package/dist/web/assets/view-B9FRCDjf.js +0 -1
- package/dist/web/assets/view-BMxu6rtp.js +0 -1
- package/dist/web/assets/view-BoqU6MqN.js +0 -1
- package/dist/web/assets/view-D7N7RCo7.js +0 -2
- package/dist/web/assets/view-DNVoQwdm.js +0 -6
- package/dist/web/assets/view-DXUVAhlp.js +0 -5
- package/dist/web/assets/view-DeXp47AF.js +0 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
const RECENT = 500;
|
|
3
|
+
/** Longest free-text answer accepted ("Add context"). */
|
|
4
|
+
const TEXT_MAX = 20_000;
|
|
3
5
|
/**
|
|
4
6
|
* Plugin UI requests (`ui.select`, `ui.askQuestions`, `ui.open`) for web clients (RF-18). A
|
|
5
7
|
* request that names a session goes to its root session's streams; otherwise (every `select`)
|
|
@@ -29,6 +31,10 @@ export class InteractionBridge {
|
|
|
29
31
|
askQuestions: (request) => {
|
|
30
32
|
const empty = () => Object.fromEntries(request.questions.map((q) => [q.id, undefined]));
|
|
31
33
|
const ids = new Set(request.questions.map((q) => q.id));
|
|
34
|
+
// `"<questionId>:text"` carries the free text of an option with `textInput` ("Add context").
|
|
35
|
+
const textKeys = new Set(request.questions
|
|
36
|
+
.filter((q) => q.options.some((o) => o.textInput))
|
|
37
|
+
.map((q) => `${q.id}:text`));
|
|
32
38
|
return this.ask({
|
|
33
39
|
interactionId: randomUUID(),
|
|
34
40
|
...(request.session ? { sessionId: request.session } : {}),
|
|
@@ -37,15 +43,18 @@ export class InteractionBridge {
|
|
|
37
43
|
kind: "questions",
|
|
38
44
|
questions: request.questions,
|
|
39
45
|
...(request.label ? { label: request.label } : {}),
|
|
46
|
+
...(request.plan ? { plan: request.plan } : {}),
|
|
40
47
|
},
|
|
41
48
|
}, request.signal, empty(), (answer) => answer === null ||
|
|
42
49
|
(!!answer &&
|
|
43
50
|
typeof answer === "object" &&
|
|
44
51
|
!Array.isArray(answer) &&
|
|
45
|
-
Object.entries(answer).every(([id, value]) =>
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
(
|
|
52
|
+
Object.entries(answer).every(([id, value]) => textKeys.has(id)
|
|
53
|
+
? value === null || (typeof value === "string" && value.length <= TEXT_MAX)
|
|
54
|
+
: ids.has(id) &&
|
|
55
|
+
(value === null ||
|
|
56
|
+
typeof value === "string" ||
|
|
57
|
+
(Array.isArray(value) && value.every((v) => typeof v === "string"))))), (answer) => ({
|
|
49
58
|
...empty(),
|
|
50
59
|
...Object.fromEntries(Object.entries((answer ?? {})).map(([id, value]) => [
|
|
51
60
|
id,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type GoalHostView, type SQLiteStore } from "@alisio/core";
|
|
2
|
+
import type { GoalInfo, ServerFrame } from "@alisio/sdk";
|
|
3
|
+
import type { RunJob, RunOutcome, RunScheduler } from "./run-scheduler.ts";
|
|
4
|
+
import type { SessionService } from "./sessions.ts";
|
|
5
|
+
import { type WorkspaceHost } from "./workspace-host.ts";
|
|
6
|
+
export interface GoalDriverOptions {
|
|
7
|
+
catalog: SQLiteStore;
|
|
8
|
+
sessions: () => SessionService;
|
|
9
|
+
scheduler: () => RunScheduler;
|
|
10
|
+
workspaces: () => WorkspaceHost;
|
|
11
|
+
awaiting: (root: string) => "approval" | "question" | undefined;
|
|
12
|
+
toSession: (sessionId: string, frame: ServerFrame) => void;
|
|
13
|
+
closing: () => boolean;
|
|
14
|
+
log: (event: string, data: Record<string, unknown>) => void;
|
|
15
|
+
}
|
|
16
|
+
export declare class GoalDriver {
|
|
17
|
+
private readonly options;
|
|
18
|
+
private readonly store;
|
|
19
|
+
/** The last frame sent per session, so an unchanged goal is not sent again. */
|
|
20
|
+
private sent;
|
|
21
|
+
constructor(options: GoalDriverOptions);
|
|
22
|
+
private opened;
|
|
23
|
+
/** What the host knows about a session, for the core's blocker list. */
|
|
24
|
+
view(sessionId: string): GoalHostView;
|
|
25
|
+
/** The goal of a root session as UIs see it (with what it waits for), or undefined. */
|
|
26
|
+
info(sessionId: string): GoalInfo | undefined;
|
|
27
|
+
/** Sends `goal_changed` when what a UI shows changed (dedupe by content). */
|
|
28
|
+
refresh(sessionId: string): void;
|
|
29
|
+
/** Folds a finished run into the session's goal. Child sessions and tool-only jobs have none. */
|
|
30
|
+
settle(job: RunJob, outcome: RunOutcome): void;
|
|
31
|
+
/**
|
|
32
|
+
* Continues the goal if it may continue now. Safe to call from anywhere, any number of times:
|
|
33
|
+
* the core claims at most one continuation, and a busy session is simply left alone.
|
|
34
|
+
*/
|
|
35
|
+
pump(sessionId: string): "started" | "waiting" | "idle";
|
|
36
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The server side of `/goal`: the glue between the core goal service (state machine, caps,
|
|
3
|
+
* breakers, the claim of ONE continuation) and what only the server knows (the run scheduler, the
|
|
4
|
+
* pending approvals and questions, the open workspace applications, the SSE hub).
|
|
5
|
+
*
|
|
6
|
+
* Everything funnels through `pump(sessionId)`: it asks the core whether the goal continues now
|
|
7
|
+
* and, when it does, starts the continuation as a normal run (`goal-<goalId>-<n>` is the run's
|
|
8
|
+
* request id, so a double trigger starts one run). It is called after each user action, after each
|
|
9
|
+
* run, when the last background task ends, and when anything that blocks a goal may have changed.
|
|
10
|
+
* A goal never starts a run by any other path.
|
|
11
|
+
*/
|
|
12
|
+
import { randomUUID } from "node:crypto";
|
|
13
|
+
import { GoalStore, goalOutcome, goalWaiting, toGoalInfo, } from "@alisio/core";
|
|
14
|
+
import { workspaceId } from "./workspace-host.js";
|
|
15
|
+
export class GoalDriver {
|
|
16
|
+
options;
|
|
17
|
+
store;
|
|
18
|
+
/** The last frame sent per session, so an unchanged goal is not sent again. */
|
|
19
|
+
sent = new Map();
|
|
20
|
+
constructor(options) {
|
|
21
|
+
this.options = options;
|
|
22
|
+
this.store = new GoalStore(options.catalog.db);
|
|
23
|
+
}
|
|
24
|
+
opened(sessionId) {
|
|
25
|
+
try {
|
|
26
|
+
return this.options
|
|
27
|
+
.workspaces()
|
|
28
|
+
.get(workspaceId(this.options.catalog.get(sessionId).workspace));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** What the host knows about a session, for the core's blocker list. */
|
|
35
|
+
view(sessionId) {
|
|
36
|
+
const { catalog } = this.options;
|
|
37
|
+
const session = catalog.get(sessionId);
|
|
38
|
+
const opened = this.opened(sessionId);
|
|
39
|
+
const awaiting = this.options.awaiting(sessionId);
|
|
40
|
+
return {
|
|
41
|
+
running: this.options.scheduler().busy(sessionId) ||
|
|
42
|
+
!!opened?.app.runner.isRunning(sessionId) ||
|
|
43
|
+
!!catalog.lockedBy(sessionId),
|
|
44
|
+
queuedInput: false,
|
|
45
|
+
...(awaiting ? { awaiting } : {}),
|
|
46
|
+
planMode: this.options.sessions().activeAgentId(session) === "plan",
|
|
47
|
+
liveTasks: opened?.app.tasks.liveCount(sessionId) ?? 0,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** The goal of a root session as UIs see it (with what it waits for), or undefined. */
|
|
51
|
+
info(sessionId) {
|
|
52
|
+
const goal = this.store.get(sessionId);
|
|
53
|
+
if (!goal)
|
|
54
|
+
return undefined;
|
|
55
|
+
const enabled = this.opened(sessionId)?.app.goals.enabled ?? true;
|
|
56
|
+
const waiting = goalWaiting({
|
|
57
|
+
...this.view(sessionId),
|
|
58
|
+
status: goal.status,
|
|
59
|
+
enabled,
|
|
60
|
+
inflight: !!goal.inflight,
|
|
61
|
+
});
|
|
62
|
+
return { ...toGoalInfo(goal), ...(waiting ? { waiting } : {}) };
|
|
63
|
+
}
|
|
64
|
+
/** Sends `goal_changed` when what a UI shows changed (dedupe by content). */
|
|
65
|
+
refresh(sessionId) {
|
|
66
|
+
let info;
|
|
67
|
+
try {
|
|
68
|
+
info = this.info(sessionId);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const key = JSON.stringify(info ?? null);
|
|
74
|
+
if (this.sent.get(sessionId) === key)
|
|
75
|
+
return;
|
|
76
|
+
this.sent.set(sessionId, key);
|
|
77
|
+
this.options.toSession(sessionId, { t: "goal_changed", sessionId, goal: info ?? null });
|
|
78
|
+
}
|
|
79
|
+
/** Folds a finished run into the session's goal. Child sessions and tool-only jobs have none. */
|
|
80
|
+
settle(job, outcome) {
|
|
81
|
+
if (job.tool)
|
|
82
|
+
return;
|
|
83
|
+
const opened = this.opened(job.sessionId);
|
|
84
|
+
if (!opened || !opened.app.goals.get(job.sessionId))
|
|
85
|
+
return;
|
|
86
|
+
opened.app.goals.settle(job.sessionId, goalOutcome({
|
|
87
|
+
runId: job.runId,
|
|
88
|
+
startedAt: outcome.startedAt,
|
|
89
|
+
endedAt: outcome.endedAt,
|
|
90
|
+
cancelled: outcome.cancelled,
|
|
91
|
+
...(outcome.result ? { result: outcome.result } : {}),
|
|
92
|
+
...(outcome.error !== undefined ? { error: outcome.error } : {}),
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Continues the goal if it may continue now. Safe to call from anywhere, any number of times:
|
|
97
|
+
* the core claims at most one continuation, and a busy session is simply left alone.
|
|
98
|
+
*/
|
|
99
|
+
pump(sessionId) {
|
|
100
|
+
const { catalog } = this.options;
|
|
101
|
+
try {
|
|
102
|
+
const session = catalog.get(sessionId);
|
|
103
|
+
const opened = this.opened(sessionId);
|
|
104
|
+
if (this.options.closing() || session.parentId || session.archivedAt || !opened) {
|
|
105
|
+
this.refresh(sessionId);
|
|
106
|
+
return "idle";
|
|
107
|
+
}
|
|
108
|
+
const goals = opened.app.goals;
|
|
109
|
+
if (!goals.get(sessionId))
|
|
110
|
+
return "idle";
|
|
111
|
+
const decision = goals.next(sessionId, this.view(sessionId));
|
|
112
|
+
this.refresh(sessionId);
|
|
113
|
+
if (decision.kind === "wait")
|
|
114
|
+
return "waiting";
|
|
115
|
+
if (decision.kind !== "continue")
|
|
116
|
+
return "idle";
|
|
117
|
+
const { requestId, prompt, run } = decision;
|
|
118
|
+
const sessions = this.options.sessions();
|
|
119
|
+
const correlationId = randomUUID();
|
|
120
|
+
const { run: record, created } = sessions.beginRun({
|
|
121
|
+
id: randomUUID(),
|
|
122
|
+
session: sessionId,
|
|
123
|
+
status: "queued",
|
|
124
|
+
requestId,
|
|
125
|
+
correlationId,
|
|
126
|
+
model: session.model,
|
|
127
|
+
});
|
|
128
|
+
if (!created) {
|
|
129
|
+
// The same continuation was already started (a double trigger): nothing more to do, and
|
|
130
|
+
// the claim stays with that run.
|
|
131
|
+
if (["completed", "failed", "cancelled", "interrupted", "turns_exceeded"].includes(record.status))
|
|
132
|
+
goals.abandon(sessionId, requestId, prompt.kind);
|
|
133
|
+
return "started";
|
|
134
|
+
}
|
|
135
|
+
const job = {
|
|
136
|
+
runId: record.id,
|
|
137
|
+
sessionId,
|
|
138
|
+
workspaceId: opened.id,
|
|
139
|
+
app: opened.app,
|
|
140
|
+
text: prompt.text,
|
|
141
|
+
display: prompt.display,
|
|
142
|
+
correlationId,
|
|
143
|
+
// The goal's own caps (the runner resets its own per run): what is left of the budget and
|
|
144
|
+
// of the time cap, applied on top of the session's usual run options.
|
|
145
|
+
options: () => {
|
|
146
|
+
const base = sessions.runOptions(sessionId);
|
|
147
|
+
const timeout = opened.app.config.limits.timeoutMs;
|
|
148
|
+
return {
|
|
149
|
+
...base,
|
|
150
|
+
...(run.maxTokens !== undefined ? { maxTokens: run.maxTokens } : {}),
|
|
151
|
+
timeoutMs: Math.max(1000, Math.min(run.remainingMs, timeout ?? run.remainingMs)),
|
|
152
|
+
};
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
try {
|
|
156
|
+
this.options.scheduler().submit(job);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
goals.abandon(sessionId, requestId, prompt.kind);
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
return "started";
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
this.options.log("goal_pump_failed", {
|
|
166
|
+
sessionId,
|
|
167
|
+
error: error instanceof Error ? error.message : String(error),
|
|
168
|
+
});
|
|
169
|
+
return "idle";
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type SQLiteStore } from "@alisio/core";
|
|
2
|
+
import type { RunJob, RunScheduler } from "./run-scheduler.ts";
|
|
3
|
+
import type { SessionService } from "./sessions.ts";
|
|
4
|
+
export declare function followUpPlanRun(input: {
|
|
5
|
+
job: RunJob;
|
|
6
|
+
cancelled: boolean;
|
|
7
|
+
catalog: SQLiteStore;
|
|
8
|
+
sessions: SessionService;
|
|
9
|
+
scheduler: RunScheduler;
|
|
10
|
+
}): "started" | "dropped" | "none";
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What happens after a plan run (spec: Plan Review). `exit_plan` only RECORDS the user's decision
|
|
3
|
+
* while its run is still going; changing agents in the middle of a run would break the persisted
|
|
4
|
+
* session. Once the run has ended this follow-up looks at the session's plan state:
|
|
5
|
+
*
|
|
6
|
+
* - cancelled run: an approved plan is dropped and a still-pending one is withdrawn;
|
|
7
|
+
* - approved plan: claimed exactly once (compare-and-set), the session switches to `build` in the
|
|
8
|
+
* same transaction and ONE implementation run starts through the normal scheduler, carrying the
|
|
9
|
+
* approved plan as a persisted user message. The run's request id is the plan id, so even a
|
|
10
|
+
* second claim path could not create a second run.
|
|
11
|
+
*/
|
|
12
|
+
import { randomUUID } from "node:crypto";
|
|
13
|
+
import { IMPLEMENTATION_AGENT_ID, settlePlanRun } from "@alisio/core";
|
|
14
|
+
export function followUpPlanRun(input) {
|
|
15
|
+
const { job, catalog, sessions, scheduler } = input;
|
|
16
|
+
const sessionId = job.sessionId;
|
|
17
|
+
const next = settlePlanRun(catalog, sessionId, {
|
|
18
|
+
aborted: input.cancelled,
|
|
19
|
+
alsoSet: { agent: IMPLEMENTATION_AGENT_ID },
|
|
20
|
+
});
|
|
21
|
+
if (next.kind !== "implement")
|
|
22
|
+
return next.kind;
|
|
23
|
+
const correlationId = randomUUID();
|
|
24
|
+
const { run, created } = sessions.beginRun({
|
|
25
|
+
id: randomUUID(),
|
|
26
|
+
session: sessionId,
|
|
27
|
+
status: "queued",
|
|
28
|
+
requestId: `plan-${next.plan.planId}`,
|
|
29
|
+
correlationId,
|
|
30
|
+
model: sessions.get(sessionId).model,
|
|
31
|
+
});
|
|
32
|
+
if (!created)
|
|
33
|
+
return "none";
|
|
34
|
+
scheduler.submit({
|
|
35
|
+
runId: run.id,
|
|
36
|
+
sessionId,
|
|
37
|
+
workspaceId: job.workspaceId,
|
|
38
|
+
app: job.app,
|
|
39
|
+
text: next.text,
|
|
40
|
+
display: next.display,
|
|
41
|
+
correlationId,
|
|
42
|
+
options: () => sessions.runOptions(sessionId),
|
|
43
|
+
});
|
|
44
|
+
return "started";
|
|
45
|
+
}
|
package/dist/host/presets.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Policy } from "@alisio/core";
|
|
2
2
|
import type { PermissionPresetId, PermissionPresetInfo } from "@alisio/sdk";
|
|
3
3
|
export declare const PRESET_IDS: readonly PermissionPresetId[];
|
|
4
4
|
export declare const DEFAULT_PRESET: PermissionPresetId;
|
package/dist/host/presets.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PERMISSION_MODE_TABLE, presetToMode, READ_ONLY_POLICY, } from "@alisio/core";
|
|
1
2
|
export const PRESET_IDS = [
|
|
2
3
|
"read-only",
|
|
3
4
|
"ask",
|
|
@@ -5,12 +6,17 @@ export const PRESET_IDS = [
|
|
|
5
6
|
"full-access",
|
|
6
7
|
];
|
|
7
8
|
export const DEFAULT_PRESET = "workspace-write";
|
|
9
|
+
/** The policy of a permission mode (the table lives in core, shared with the TUI). */
|
|
10
|
+
const ofMode = (mode) => ({
|
|
11
|
+
policy: { ...PERMISSION_MODE_TABLE[mode].policy },
|
|
12
|
+
approvals: PERMISSION_MODE_TABLE[mode].approvals,
|
|
13
|
+
});
|
|
8
14
|
/** What each preset asks for before the server ceiling is applied (RF-08). */
|
|
9
15
|
const WANTED = {
|
|
10
|
-
"read-only": { policy: {
|
|
11
|
-
ask:
|
|
12
|
-
"workspace-write":
|
|
13
|
-
"full-access":
|
|
16
|
+
"read-only": { policy: { ...READ_ONLY_POLICY }, approvals: false },
|
|
17
|
+
ask: ofMode("ask"),
|
|
18
|
+
"workspace-write": ofMode("auto"),
|
|
19
|
+
"full-access": ofMode("full"),
|
|
14
20
|
};
|
|
15
21
|
const FLAG = {
|
|
16
22
|
write: "--allow-write",
|
|
@@ -32,9 +38,11 @@ export function presetInfo(id, ceiling) {
|
|
|
32
38
|
// processes (full-access); elsewhere python_run asks.
|
|
33
39
|
...(wanted.policy.process && ceiling.policy.analysis ? { analysis: true } : {}),
|
|
34
40
|
};
|
|
41
|
+
const mode = presetToMode(id);
|
|
35
42
|
if (ceiling.readOnly && id !== "read-only")
|
|
36
43
|
return {
|
|
37
44
|
id,
|
|
45
|
+
...(mode ? { mode } : {}),
|
|
38
46
|
available: false,
|
|
39
47
|
reason: "The server was started with --read-only",
|
|
40
48
|
policy: { write: false, process: false, external: false },
|
|
@@ -43,6 +51,7 @@ export function presetInfo(id, ceiling) {
|
|
|
43
51
|
const degraded = Object.keys(FLAG).filter((effect) => wanted.policy[effect] && !ceiling.policy[effect]);
|
|
44
52
|
return {
|
|
45
53
|
id,
|
|
54
|
+
...(mode ? { mode } : {}),
|
|
46
55
|
available: true,
|
|
47
56
|
...(degraded.length
|
|
48
57
|
? {
|
|
@@ -24,11 +24,34 @@ export interface RunJob {
|
|
|
24
24
|
input: Record<string, unknown>;
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
|
+
/** What a finished job reports (the goal controller folds it into the session's goal). */
|
|
28
|
+
export interface RunOutcome {
|
|
29
|
+
/** The job's abort signal fired (a cancel, or the server shutting down). */
|
|
30
|
+
cancelled: boolean;
|
|
31
|
+
/** What `runner.run` resolved with, when it did. */
|
|
32
|
+
result?: {
|
|
33
|
+
text: string;
|
|
34
|
+
status: string;
|
|
35
|
+
usage: {
|
|
36
|
+
input: number;
|
|
37
|
+
output: number;
|
|
38
|
+
};
|
|
39
|
+
toolCalls?: number;
|
|
40
|
+
};
|
|
41
|
+
/** Why it failed, when it did (never set for a cancelled job). */
|
|
42
|
+
error?: unknown;
|
|
43
|
+
/** When the job started running (ms), and when it ended. */
|
|
44
|
+
startedAt: number;
|
|
45
|
+
endedAt: number;
|
|
46
|
+
}
|
|
27
47
|
export interface RunSchedulerOptions {
|
|
28
48
|
/** Global semaphore across sessions and workspaces (RF-02, default 4). */
|
|
29
49
|
maxConcurrent?: number;
|
|
30
|
-
/**
|
|
31
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Called on every job state change (queued, running, finished). `finished` also says whether
|
|
52
|
+
* the job was cancelled (its abort signal fired), so a follow-up never outlives a cancel.
|
|
53
|
+
*/
|
|
54
|
+
onChange?: (job: RunJob, status: "queued" | "running" | "finished", outcome?: RunOutcome) => void;
|
|
32
55
|
}
|
|
33
56
|
/**
|
|
34
57
|
* FIFO scheduler with a global concurrency limit. A session has at most one job (queued or
|
|
@@ -31,6 +31,7 @@ export class RunScheduler {
|
|
|
31
31
|
}
|
|
32
32
|
start(job) {
|
|
33
33
|
job.status = "running";
|
|
34
|
+
job.startedAt = Date.now();
|
|
34
35
|
this.running++;
|
|
35
36
|
this.options.onChange?.(job, "running");
|
|
36
37
|
const { runner, store } = job.app;
|
|
@@ -56,7 +57,11 @@ export class RunScheduler {
|
|
|
56
57
|
catch (error) {
|
|
57
58
|
run = Promise.reject(error);
|
|
58
59
|
}
|
|
59
|
-
job.done = run.then(() =>
|
|
60
|
+
job.done = run.then((result) => {
|
|
61
|
+
if (result && typeof result === "object")
|
|
62
|
+
job.result = result;
|
|
63
|
+
}, (error) => {
|
|
64
|
+
job.error = error;
|
|
60
65
|
// A run that never reached the runner's journal (e.g. the session was claimed) must not
|
|
61
66
|
// stay `queued`; terminal rows are never overwritten.
|
|
62
67
|
store.endRun?.(job.runId, {
|
|
@@ -68,7 +73,13 @@ export class RunScheduler {
|
|
|
68
73
|
this.running--;
|
|
69
74
|
if (this.jobs.get(job.sessionId) === job)
|
|
70
75
|
this.jobs.delete(job.sessionId);
|
|
71
|
-
this.options.onChange?.(job, "finished"
|
|
76
|
+
this.options.onChange?.(job, "finished", {
|
|
77
|
+
cancelled: job.controller.signal.aborted,
|
|
78
|
+
...(job.result ? { result: job.result } : {}),
|
|
79
|
+
...(job.error !== undefined && !job.controller.signal.aborted ? { error: job.error } : {}),
|
|
80
|
+
startedAt: job.startedAt ?? Date.now(),
|
|
81
|
+
endedAt: Date.now(),
|
|
82
|
+
});
|
|
72
83
|
this.pump();
|
|
73
84
|
});
|
|
74
85
|
}
|
|
@@ -122,7 +133,11 @@ export class RunScheduler {
|
|
|
122
133
|
this.queue = this.queue.filter((queued) => queued !== job);
|
|
123
134
|
this.jobs.delete(sessionId);
|
|
124
135
|
job.app.store.endRun?.(job.runId, { status: "cancelled", error: "Cancelled" });
|
|
125
|
-
this.options.onChange?.(job, "finished"
|
|
136
|
+
this.options.onChange?.(job, "finished", {
|
|
137
|
+
cancelled: true,
|
|
138
|
+
startedAt: Date.now(),
|
|
139
|
+
endedAt: Date.now(),
|
|
140
|
+
});
|
|
126
141
|
}
|
|
127
142
|
return true;
|
|
128
143
|
}
|
package/dist/host/sessions.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface SessionServiceOptions {
|
|
|
12
12
|
awaitingInput?: (rootSessionId: string) => boolean;
|
|
13
13
|
/** Sends a frame to every connected stream (session status for sidebars). */
|
|
14
14
|
broadcast?: (frame: ServerFrame) => void;
|
|
15
|
+
/** A session's stored options changed (agent, preset, effort): a goal may now continue. */
|
|
16
|
+
onOptions?: (sessionId: string) => void;
|
|
15
17
|
}
|
|
16
18
|
/**
|
|
17
19
|
* Session views (derived UI status §6.2, wire/summary/detail shapes) and per-session run
|
|
@@ -23,6 +25,7 @@ export declare class SessionService {
|
|
|
23
25
|
/** Per-session policy objects: "allow for session" widens only its own session. */
|
|
24
26
|
private policies;
|
|
25
27
|
readonly ceiling: Ceiling;
|
|
28
|
+
private readonly goals;
|
|
26
29
|
constructor(options: SessionServiceOptions);
|
|
27
30
|
/** A session by id, or 404. */
|
|
28
31
|
get(id: string): Session;
|
|
@@ -59,10 +62,16 @@ export declare class SessionService {
|
|
|
59
62
|
approvals: boolean;
|
|
60
63
|
reasoningEffort?: string;
|
|
61
64
|
instructions?: string;
|
|
65
|
+
optInTools?: string[];
|
|
62
66
|
};
|
|
67
|
+
/** The agent id the session's next run uses (stored id, else the app's active agent). */
|
|
68
|
+
activeAgentId(session: Session): string | undefined;
|
|
69
|
+
private resolveAgent;
|
|
63
70
|
private agentOptions;
|
|
64
71
|
/** Merges keys into the session's stored options (undefined removes a key). */
|
|
65
72
|
setOptions(sessionId: string, options: Record<string, unknown>): void;
|
|
73
|
+
/** A session option (agent, preset, effort) changed: a goal that waited on it may continue. */
|
|
74
|
+
optionsChanged(sessionId: string): void;
|
|
66
75
|
runByRequest(sessionId: string, requestId: string): import("@alisio/core").RunRecord | undefined;
|
|
67
76
|
beginRun(input: Parameters<SQLiteStore["beginRun"]>[0]): {
|
|
68
77
|
run: import("@alisio/core").RunRecord;
|
|
@@ -77,5 +86,5 @@ export declare class SessionService {
|
|
|
77
86
|
/** Broadcasts the current status of a session to every stream (sidebars). */
|
|
78
87
|
notify(sessionId: string): void;
|
|
79
88
|
/** Late binding for components created after this service. */
|
|
80
|
-
bind(hooks: Pick<SessionServiceOptions, "awaitingInput" | "broadcast">): void;
|
|
89
|
+
bind(hooks: Pick<SessionServiceOptions, "awaitingInput" | "broadcast" | "onOptions">): void;
|
|
81
90
|
}
|
package/dist/host/sessions.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { agentCatalogFromState, agentRunOptions, resolveActiveAgent, } from "@alisio/core";
|
|
1
|
+
import { agentCatalogFromState, agentRunOptions, GoalStore, goalOptInTools, PLAN_AGENT_ID, resolveActiveAgent, } from "@alisio/core";
|
|
2
2
|
import { HttpError } from "../http/errors.js";
|
|
3
3
|
import { presetInfos, sessionPreset } from "./presets.js";
|
|
4
4
|
import { workspaceId, } from "./workspace-host.js";
|
|
@@ -12,8 +12,10 @@ export class SessionService {
|
|
|
12
12
|
/** Per-session policy objects: "allow for session" widens only its own session. */
|
|
13
13
|
policies = new Map();
|
|
14
14
|
ceiling;
|
|
15
|
+
goals;
|
|
15
16
|
constructor(options) {
|
|
16
17
|
this.options = options;
|
|
18
|
+
this.goals = new GoalStore(options.catalog.db);
|
|
17
19
|
const base = options.base;
|
|
18
20
|
const readOnly = !!base.readOnly;
|
|
19
21
|
this.ceiling = {
|
|
@@ -139,6 +141,9 @@ export class SessionService {
|
|
|
139
141
|
const { policy, approvals } = this.policy(session);
|
|
140
142
|
const effort = session.options?.effort;
|
|
141
143
|
const agent = this.agentOptions(session);
|
|
144
|
+
// The goal tools exist for the runs of a session with an ACTIVE goal (never in plan mode).
|
|
145
|
+
const goalTools = goalOptInTools(this.goals.get(session.id), this.activeAgentId(session) === PLAN_AGENT_ID);
|
|
146
|
+
const optInTools = [...(agent.optInTools ?? []), ...goalTools];
|
|
142
147
|
return {
|
|
143
148
|
policy: agent.policy ? { ...agent.policy } : policy,
|
|
144
149
|
approvals: agent.approvals ?? approvals,
|
|
@@ -149,12 +154,18 @@ export class SessionService {
|
|
|
149
154
|
? { reasoningEffort: agent.reasoningEffort }
|
|
150
155
|
: {}),
|
|
151
156
|
...(agent.instructions ? { instructions: agent.instructions } : {}),
|
|
157
|
+
...(optInTools.length ? { optInTools } : {}),
|
|
152
158
|
};
|
|
153
159
|
}
|
|
154
|
-
|
|
160
|
+
/** The agent id the session's next run uses (stored id, else the app's active agent). */
|
|
161
|
+
activeAgentId(session) {
|
|
155
162
|
const app = this.openApp(session)?.app;
|
|
163
|
+
const stored = session.options?.agent;
|
|
156
164
|
if (!app)
|
|
157
|
-
return
|
|
165
|
+
return typeof stored === "string" ? stored : undefined;
|
|
166
|
+
return this.resolveAgent(session, app)?.id;
|
|
167
|
+
}
|
|
168
|
+
resolveAgent(session, app) {
|
|
158
169
|
let state;
|
|
159
170
|
try {
|
|
160
171
|
state = app.plugins.pluginState("subagents", "mainAgents");
|
|
@@ -163,12 +174,22 @@ export class SessionService {
|
|
|
163
174
|
/* agent contributions are best-effort */
|
|
164
175
|
}
|
|
165
176
|
const stored = session.options?.agent;
|
|
166
|
-
|
|
167
|
-
|
|
177
|
+
return resolveActiveAgent(agentCatalogFromState(state), typeof stored === "string" ? stored : app.config.agents.active);
|
|
178
|
+
}
|
|
179
|
+
agentOptions(session) {
|
|
180
|
+
const app = this.openApp(session)?.app;
|
|
181
|
+
if (!app)
|
|
182
|
+
return {};
|
|
183
|
+
return agentRunOptions(this.resolveAgent(session, app));
|
|
168
184
|
}
|
|
169
185
|
/** Merges keys into the session's stored options (undefined removes a key). */
|
|
170
186
|
setOptions(sessionId, options) {
|
|
171
187
|
this.options.catalog.updateSessionMeta(sessionId, { options });
|
|
188
|
+
this.optionsChanged(sessionId);
|
|
189
|
+
}
|
|
190
|
+
/** A session option (agent, preset, effort) changed: a goal that waited on it may continue. */
|
|
191
|
+
optionsChanged(sessionId) {
|
|
192
|
+
this.options.onOptions?.(sessionId);
|
|
172
193
|
}
|
|
173
194
|
runByRequest(sessionId, requestId) {
|
|
174
195
|
return this.options.catalog.runByRequest(sessionId, requestId);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type AppOptions, createApplication, type SQLiteStore } from "@alisio/core";
|
|
2
|
-
import type { WorkspaceInfo } from "@alisio/sdk";
|
|
2
|
+
import type { ReloadReport, WorkspaceInfo } from "@alisio/sdk";
|
|
3
3
|
export type Application = Awaited<ReturnType<typeof createApplication>>;
|
|
4
4
|
/** `AppOptions` minus what the server owns per workspace (cwd, events, approvals). */
|
|
5
|
-
export type ServerAppOptions = Omit<AppOptions, "cwd" | "onEvent" | "approve" | "approveExternalDirectory">;
|
|
5
|
+
export type ServerAppOptions = Omit<AppOptions, "cwd" | "onEvent" | "approve" | "approveExternalDirectory" | "tasks" | "goals">;
|
|
6
6
|
/** An open workspace: one `Application` bound to one canonical path. */
|
|
7
7
|
export interface OpenWorkspace {
|
|
8
8
|
id: string;
|
|
@@ -22,11 +22,11 @@ export interface WorkspaceHostOptions {
|
|
|
22
22
|
/** Listed even before it has sessions (e.g. the directory `alisio serve` ran in). */
|
|
23
23
|
defaultWorkspace?: string;
|
|
24
24
|
/** Per-workspace wiring the server owns: events and approval handlers. */
|
|
25
|
-
wire?: (id: string, path: string) => Pick<AppOptions, "onEvent" | "approve" | "approveExternalDirectory">;
|
|
25
|
+
wire?: (id: string, path: string) => Pick<AppOptions, "onEvent" | "approve" | "approveExternalDirectory" | "tasks" | "goals">;
|
|
26
26
|
/** Called once an app is created (e.g. to bind interactive UI) and before it is closed. */
|
|
27
27
|
onOpen?: (workspace: OpenWorkspace) => void;
|
|
28
28
|
onClose?: (workspace: OpenWorkspace) => void;
|
|
29
|
-
/** A workspace with runs, subscribers
|
|
29
|
+
/** A workspace with runs, subscribers, pending approvals or live background tasks is never evicted. */
|
|
30
30
|
busy?: (id: string) => boolean;
|
|
31
31
|
/** Injectable for tests. */
|
|
32
32
|
create?: typeof createApplication;
|
|
@@ -58,6 +58,12 @@ export declare class WorkspaceHost {
|
|
|
58
58
|
get(id: string): OpenWorkspace | undefined;
|
|
59
59
|
/** Opens (or reuses) the app of a canonical workspace path, evicting an idle one if needed. */
|
|
60
60
|
openPath(path: string): Promise<OpenWorkspace>;
|
|
61
|
+
/**
|
|
62
|
+
* Builds the `Application` of a workspace: launch flags are explicit, one-run trust like in the
|
|
63
|
+
* terminal; otherwise the trust store decides and a workspace that would need a prompt opens
|
|
64
|
+
* untrusted.
|
|
65
|
+
*/
|
|
66
|
+
private build;
|
|
61
67
|
private create;
|
|
62
68
|
/** Closes the least recently used idle app, or fails with 503 `workspace_limit`. */
|
|
63
69
|
private evictOne;
|
|
@@ -72,6 +78,15 @@ export declare class WorkspaceHost {
|
|
|
72
78
|
* shared store) and the next request uses the new app.
|
|
73
79
|
*/
|
|
74
80
|
recycle(id: string): Promise<OpenWorkspace | undefined>;
|
|
81
|
+
/**
|
|
82
|
+
* `/reload` for one open workspace: validate, build the new app NEXT TO the current one and
|
|
83
|
+
* only then swap, so a broken configuration leaves the workspace and its sessions untouched
|
|
84
|
+
* (unlike `recycle`, which closes first). The caller refuses while runs are active (`busy`).
|
|
85
|
+
* SSE subscribers are unaffected: they read the shared store.
|
|
86
|
+
*/
|
|
87
|
+
reload(id: string, guard?: {
|
|
88
|
+
busy?: () => string | undefined;
|
|
89
|
+
}): Promise<ReloadReport>;
|
|
75
90
|
/** Closes every app in parallel (each close is capped by the core's teardown timeouts). */
|
|
76
91
|
closeAll(): Promise<void>;
|
|
77
92
|
/**
|