specrails-desktop 2.60.0 → 2.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/dist/assets/ActivityFeedPage-C_pqc281.js +1 -0
- package/client/dist/assets/{AgentBrowserCapture-D4a3wYM1.js → AgentBrowserCapture-vz6rbDtu.js} +1 -1
- package/client/dist/assets/AgentModeAnalyticsPane-BHCcUahS.js +2 -0
- package/client/dist/assets/AgentModeCodePane-DhmxgG3W.js +2 -0
- package/client/dist/assets/{AgentModeJobsPane-25tQlLpF.js → AgentModeJobsPane-99fBBLri.js} +1 -1
- package/client/dist/assets/{AgentsPage-CUDp_sfw.js → AgentsPage-CTwm_6T8.js} +3 -3
- package/client/dist/assets/{AnalyticsPage-DXWMGn8b.js → AnalyticsPage-YD8MLoPa.js} +1 -1
- package/client/dist/assets/CodePage-CLJn2F2e.js +3 -0
- package/client/dist/assets/{DesktopAnalyticsPage-BkFBq0E6.js → DesktopAnalyticsPage-MWGQVbwp.js} +1 -1
- package/client/dist/assets/{DocsDialog-BL7uSbIJ.js → DocsDialog-XUCYKoq0.js} +1 -1
- package/client/dist/assets/{DocsPage-CsDLbmZX.js → DocsPage-dOLC8bXA.js} +1 -1
- package/client/dist/assets/{ExportDropdown-B4Olg1Fk.js → ExportDropdown-BvKQgwM9.js} +1 -1
- package/client/dist/assets/InteractiveJobComposer-DxOvwmBF.js +19 -0
- package/client/dist/assets/{JobDetailModal-DS_Owxs6.js → JobDetailModal-CkiubrkZ.js} +1 -1
- package/client/dist/assets/JobDetailPage-CNe4Wn7l.js +1 -0
- package/client/dist/assets/JobsPage-DSts_L_d.js +1 -0
- package/client/dist/assets/LoopBuilderPage-C9D9eer5.js +1 -0
- package/client/dist/assets/{LoopPreviewModal-CH1r-KhU.js → LoopPreviewModal-Dv3dxHr_.js} +1 -1
- package/client/dist/assets/{LoopWindowSurface-CiZdI6aw.js → LoopWindowSurface-DN2lbSnL.js} +1 -1
- package/client/dist/assets/{LoopsPage-BwFujdGi.js → LoopsPage-DHgIreEW.js} +2 -2
- package/client/dist/assets/MinimizedChatsContext-Ds2-R6qJ.js +1 -0
- package/client/dist/assets/PluginsPage-D1EJ9oja.js +2 -0
- package/client/dist/assets/ProjectSettingsDialog-DKpa1wKk.js +1 -0
- package/client/dist/assets/{RepositoryDeliveries-NNorcGzT.js → RepositoryDeliveries-CW4eKG3x.js} +1 -1
- package/client/dist/assets/{RepositoryScopeSelector-D-jb4bI_.js → RepositoryScopeSelector-DrtPqvc_.js} +1 -1
- package/client/dist/assets/ReviewPacketPage-BtjPI0Cc.js +4 -0
- package/client/dist/assets/{TemplatePreviewModal-5CZdAZ0G.js → TemplatePreviewModal-DcFlhUxy.js} +1 -1
- package/client/dist/assets/TicketDetailModalContext-Dea22CI-.js +16 -0
- package/client/dist/assets/{Trans-wmTUDVyC.js → Trans-CAY2Rd2f.js} +1 -1
- package/client/dist/assets/agent-5bSixVzz.js +1 -0
- package/client/dist/assets/agent-BR4EHfhq.js +1 -0
- package/client/dist/assets/agent-C0IKkJP8.js +1 -0
- package/client/dist/assets/agent-C7li9ayC.js +1 -0
- package/client/dist/assets/agent-D0dRmEen.js +1 -0
- package/client/dist/assets/{agent-BjttZLIl.js → agent-DZVGHm7H.js} +1 -1
- package/client/dist/assets/agent-Dip4h2YE.js +1 -0
- package/client/dist/assets/agent-Pp-P-n1p.js +1 -0
- package/client/dist/assets/agentRuntime-B-myNBUP.js +1 -0
- package/client/dist/assets/agentRuntime-Bjt7Ij6_.js +1 -0
- package/client/dist/assets/agentRuntime-C_yLPKDy.js +1 -0
- package/client/dist/assets/agentRuntime-D2kYQqNO.js +1 -0
- package/client/dist/assets/agentRuntime-DvhEe1um.js +1 -0
- package/client/dist/assets/agentRuntime-DxC6-9rN.js +1 -0
- package/client/dist/assets/agentRuntime-TKQyg137.js +1 -0
- package/client/dist/assets/agentRuntime-WSv8G0pW.js +1 -0
- package/client/dist/assets/ban-E4EaZe3a.js +1 -0
- package/client/dist/assets/chevron-up-CwGUHd6D.js +1 -0
- package/client/dist/assets/dist-Bq10nS2m.js +1 -0
- package/client/dist/assets/{formatDistanceToNow-Dax4zqPF.js → formatDistanceToNow-DlQY1Lm_.js} +1 -1
- package/client/dist/assets/{getTimezoneOffsetInMilliseconds-dpIo42JZ.js → getTimezoneOffsetInMilliseconds-COUeq65G.js} +1 -1
- package/client/dist/assets/index-BacrO_ub.css +2 -0
- package/client/dist/assets/index-CONHvEJw.js +83 -0
- package/client/dist/assets/{jira-api-CLrIvXFm.js → jira-api-C94VNHNY.js} +1 -1
- package/client/dist/assets/layers-eDMjG41I.js +1 -0
- package/client/dist/assets/{loops-Cm55CXJ4.js → loops-B2PhQLKR.js} +1 -1
- package/client/dist/assets/{loops-3IutojWt.js → loops-BqbF46st.js} +1 -1
- package/client/dist/assets/{loops-BAosWtXE.js → loops-CINnHbfI.js} +1 -1
- package/client/dist/assets/{loops-kIJJkaxf.js → loops-D-3XhH04.js} +1 -1
- package/client/dist/assets/{loops-DHQtnfze.js → loops-DKkEEjZ_.js} +1 -1
- package/client/dist/assets/{loops-D7tL_Saw.js → loops-DzWR5-v6.js} +1 -1
- package/client/dist/assets/{loops--KWr0X-w.js → loops-WyWDkhok.js} +1 -1
- package/client/dist/assets/{loops-BISDssL5.js → loops-scD5fIBf.js} +1 -1
- package/client/dist/assets/{narration-DvLQBW9U.js → narration-BEKMVxGm.js} +1 -1
- package/client/dist/assets/{narration-CHI06czm.js → narration-CjLwGKap.js} +1 -1
- package/client/dist/assets/{narration-DjKB3pQZ.js → narration-DANWuGsW.js} +1 -1
- package/client/dist/assets/{narration-eSx3gR0W.js → narration-DGeEDWiT.js} +1 -1
- package/client/dist/assets/{narration-DtCYvRLM.js → narration-DJgpGwIP.js} +1 -1
- package/client/dist/assets/{narration-n3hOA7LY.js → narration-DbHnz0qV.js} +1 -1
- package/client/dist/assets/{narration-thJOeT2-.js → narration-bZSsTZG8.js} +1 -1
- package/client/dist/assets/{narration--hUhjM5v.js → narration-i6aKJKEc.js} +1 -1
- package/client/dist/assets/{project-repositories-BkyT8AjU.js → project-repositories-Cc3oNv8X.js} +1 -1
- package/client/dist/assets/save-B6wGWwBR.js +1 -0
- package/client/dist/assets/{settings-Dn-pXukr.js → settings-BKJZ2KEM.js} +1 -1
- package/client/dist/assets/{settings-BBj_kZaD.js → settings-BV3jocIx.js} +1 -1
- package/client/dist/assets/{settings-COlH_P33.js → settings-BjM2Dw8_.js} +1 -1
- package/client/dist/assets/settings-CKu0LTib.js +1 -0
- package/client/dist/assets/{settings-D77mC_Lb.js → settings-DA77M48m.js} +1 -1
- package/client/dist/assets/{settings-ByVE7jdZ.js → settings-LfFnl1UH.js} +1 -1
- package/client/dist/assets/{settings-DZKy7MqN.js → settings-P1IQvhnL.js} +1 -1
- package/client/dist/assets/{settings-B5Hy_Kol.js → settings-eIMJjyK_.js} +1 -1
- package/client/dist/assets/{spending-Oov7p52A.js → spending-DemeFfrH.js} +1 -1
- package/client/dist/assets/{useDesktop-DpiS8Uov.js → useDesktop-9Lwoel_8.js} +1 -1
- package/client/dist/assets/{useRuntimeRuns-DBm1mul0.js → useRuntimeRuns-DL0xYoZ2.js} +1 -1
- package/client/dist/assets/{useSharedWebSocket-BZxKaVNT.js → useSharedWebSocket-BEHPeU1Y.js} +2 -2
- package/client/dist/assets/zap-CHo3aCD1.js +1 -0
- package/client/dist/index.html +26 -22
- package/docs/guide/de/integrations/6-agent-chat.md +11 -0
- package/docs/guide/en/integrations/6-agent-chat.md +11 -0
- package/docs/guide/es/integrations/6-agent-chat.md +11 -0
- package/docs/guide/fr/integrations/6-agent-chat.md +11 -0
- package/docs/guide/it/integrations/6-agent-chat.md +11 -0
- package/docs/guide/ja/integrations/6-agent-chat.md +11 -0
- package/docs/guide/pt/integrations/6-agent-chat.md +11 -0
- package/docs/guide/zh/integrations/6-agent-chat.md +11 -0
- package/docs/internals/README.md +1 -0
- package/docs/internals/agent-sessions.md +157 -0
- package/docs/internals/api-reference.md +33 -4
- package/docs/internals/configuration.md +36 -0
- package/docs/internals/core-runtime-updates.md +10 -3
- package/docs/internals/operations-runbook.md +11 -0
- package/docs/internals/programmatic-agent-runtime.md +2 -0
- package/docs/internals/source-map.md +40 -1
- package/mcp-bridge/dist/specrails-mcp.js +82 -15
- package/package.json +1 -1
- package/server/dist/agent-mcp-config.js +41 -0
- package/server/dist/core-compat.js +2 -0
- package/server/dist/desktop-db.js +101 -4
- package/server/dist/desktop-router.js +31 -2
- package/server/dist/index.js +58 -0
- package/server/dist/mcp/tools/app-settings.js +11 -0
- package/server/dist/mcp/tools/mission.js +45 -2
- package/server/dist/mcp/tools/specs.js +2 -2
- package/server/dist/modules/accounting/runtime/result-event.js +9 -1
- package/server/dist/modules/agent-runtime/runtime/agent-runtime-bridge.js +12 -0
- package/server/dist/modules/agent-runtime/runtime/agent-runtime-controls.js +25 -0
- package/server/dist/modules/agent-runtime/runtime/agent-runtime-effective-config.js +2 -0
- package/server/dist/modules/agent-runtime/runtime/agent-runtime-settings-router.js +6 -2
- package/server/dist/modules/agent-runtime/runtime/agent-runtime-settings.js +18 -6
- package/server/dist/modules/agent-sessions/adapters/host-process.js +73 -0
- package/server/dist/modules/agent-sessions/adapters/http.js +31 -0
- package/server/dist/modules/agent-sessions/adapters/rpc-client.js +147 -0
- package/server/dist/modules/agent-sessions/application/session-event-pump.js +99 -0
- package/server/dist/modules/agent-sessions/domain/availability.js +25 -0
- package/server/dist/modules/agent-sessions/domain/errors.js +23 -0
- package/server/dist/modules/agent-sessions/domain/host-state.js +80 -0
- package/server/dist/modules/agent-sessions/domain/projection.js +147 -0
- package/server/dist/modules/agent-sessions/domain/protocol.js +59 -0
- package/server/dist/modules/agent-sessions/domain/subagent-policy.js +34 -0
- package/server/dist/modules/agent-sessions/index.js +31 -0
- package/server/dist/modules/agent-sessions/ports.js +2 -0
- package/server/dist/modules/agent-sessions/runtime/core-sessions-availability.js +43 -0
- package/server/dist/modules/agent-sessions/runtime/session-host-registry.js +226 -0
- package/server/dist/modules/agents/runtime/agent-store.js +3 -1
- package/server/dist/modules/loops/runtime/loop-core-factory.js +2 -2
- package/server/dist/modules/loops/runtime/loop-definition.js +1 -0
- package/server/dist/modules/loops/runtime/loop-factory.js +1 -1
- package/server/dist/modules/loops/runtime/loop-graph.js +8 -1
- package/server/dist/modules/loops/runtime/loop-implement-recipe.js +41 -11
- package/server/dist/modules/loops/runtime/loop-run-manager.js +4 -0
- package/server/dist/modules/loops/runtime/loop-templates.js +3 -3
- package/server/dist/modules/missions/runtime/agent-chat-manager.js +137 -40
- package/server/dist/modules/missions/runtime/agent-chat-router.js +68 -0
- package/server/dist/modules/missions/runtime/agent-operator-prompt.js +8 -9
- package/server/dist/modules/missions/runtime/agent-session-store.js +133 -0
- package/server/dist/modules/missions/runtime/core-session-runner.js +204 -0
- package/server/dist/modules/missions/runtime/mission-core-sessions.js +286 -0
- package/server/dist/modules/missions/runtime/mission-session-projector.js +168 -0
- package/server/dist/modules/project-settings/adapters/http.js +14 -2
- package/server/dist/modules/project-settings/adapters/sqlite.js +30 -2
- package/server/dist/modules/project-settings/domain.js +24 -0
- package/server/dist/modules/project-settings/index.js +2 -1
- package/server/dist/modules/specs/runtime/spec-models.js +1 -1
- package/server/dist/plugins/codex-spawn.js +15 -7
- package/server/dist/project-registry.js +32 -0
- package/server/dist/project-router-settings.js +7 -1
- package/server/dist/providers/claude-adapter.js +6 -1
- package/server/dist/schemas/agent-runtime.schema.json +9 -7
- package/client/dist/assets/ActivityFeedPage-3J7fR8yB.js +0 -1
- package/client/dist/assets/AgentModeAnalyticsPane-Dps30tk9.js +0 -2
- package/client/dist/assets/AgentModeCodePane-CvLimey8.js +0 -2
- package/client/dist/assets/CodePage-D981zRHy.js +0 -3
- package/client/dist/assets/InteractiveJobComposer-Z6fl4791.js +0 -19
- package/client/dist/assets/JobDetailPage-1TUQ9vlE.js +0 -1
- package/client/dist/assets/JobsPage-BSrl8P7u.js +0 -1
- package/client/dist/assets/LoopBuilderPage-Dh5uk2Qy.js +0 -1
- package/client/dist/assets/MinimizedChatsContext-BD2bsRBe.js +0 -1
- package/client/dist/assets/PluginsPage-B8f49Mvn.js +0 -2
- package/client/dist/assets/ProjectSettingsDialog-D7q-O6h3.js +0 -1
- package/client/dist/assets/ReviewPacketPage-DpZSzF-Q.js +0 -4
- package/client/dist/assets/TicketDetailModalContext-BHmtj1As.js +0 -16
- package/client/dist/assets/agent--8QAfoXL.js +0 -1
- package/client/dist/assets/agent-3HxXkIWX.js +0 -1
- package/client/dist/assets/agent-CpFCz32T.js +0 -1
- package/client/dist/assets/agent-DVnw6ybO.js +0 -1
- package/client/dist/assets/agent-Z5s_GTP0.js +0 -1
- package/client/dist/assets/agent-hv57-zyG.js +0 -1
- package/client/dist/assets/agent-xGrZ_aN1.js +0 -1
- package/client/dist/assets/agentRuntime-7Ydcvr6P.js +0 -1
- package/client/dist/assets/agentRuntime-BHVw9orA.js +0 -1
- package/client/dist/assets/agentRuntime-DCPDaQcV.js +0 -1
- package/client/dist/assets/agentRuntime-DKTw4Ajg.js +0 -1
- package/client/dist/assets/agentRuntime-DVhij_i_.js +0 -1
- package/client/dist/assets/agentRuntime-Doss-T8o.js +0 -1
- package/client/dist/assets/agentRuntime-eZSMUlkM.js +0 -1
- package/client/dist/assets/agentRuntime-qSSU7fQk.js +0 -1
- package/client/dist/assets/index-Bfunvi6w.css +0 -2
- package/client/dist/assets/index-g8S62tpS.js +0 -83
- package/client/dist/assets/save-OsUqVn99.js +0 -1
- package/client/dist/assets/settings-Dbyd6gmj.js +0 -1
- package/client/dist/assets/zap-CUOlvtT1.js +0 -1
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sessionPolicyFor = sessionPolicyFor;
|
|
4
|
+
exports.createCoreSessionRunner = createCoreSessionRunner;
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
const agent_sessions_1 = require("../../agent-sessions");
|
|
7
|
+
/** The complete session policy sent to Core (`session.update` replaces policy as a whole). */
|
|
8
|
+
function sessionPolicyFor(context) {
|
|
9
|
+
return { ...context.policy, mcp: { servers: context.mcpServers, inheritUserScope: context.policy.mcp?.inheritUserScope ?? false } };
|
|
10
|
+
}
|
|
11
|
+
function toNormalised(usage) {
|
|
12
|
+
if (!usage)
|
|
13
|
+
return undefined;
|
|
14
|
+
const result = {};
|
|
15
|
+
if (usage.inputTokens !== null)
|
|
16
|
+
result.tokens_in = usage.inputTokens;
|
|
17
|
+
if (usage.outputTokens !== null)
|
|
18
|
+
result.tokens_out = usage.outputTokens;
|
|
19
|
+
if (usage.cacheReadTokens !== null)
|
|
20
|
+
result.tokens_cache_read = usage.cacheReadTokens;
|
|
21
|
+
if (usage.cacheWriteTokens !== null)
|
|
22
|
+
result.tokens_cache_create = usage.cacheWriteTokens;
|
|
23
|
+
// Only billed cost is "native"; Core estimates are re-derived by Desktop's rate cards.
|
|
24
|
+
if (usage.costUsd !== null && !usage.costEstimated)
|
|
25
|
+
result.total_cost_usd = usage.costUsd;
|
|
26
|
+
if (usage.model)
|
|
27
|
+
result.model = usage.model;
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Runs one mission user turn inside the conversation's resident Core session,
|
|
32
|
+
* honouring the existing live-session runner contract (Liskov substitution for
|
|
33
|
+
* nativeLiveSessionRunner / runAiCliInvocation): the mission manager's queue,
|
|
34
|
+
* steering, receipts, checkpoints and settlement work unchanged. The turn ends
|
|
35
|
+
* when Core completes THIS turn; the session, its sub-agents and the provider
|
|
36
|
+
* process stay alive in Core. Background activity is projected elsewhere.
|
|
37
|
+
*/
|
|
38
|
+
function createCoreSessionRunner(context) {
|
|
39
|
+
return async (hooks) => {
|
|
40
|
+
const { client, sessionId } = context;
|
|
41
|
+
const opts = hooks.buildOpts;
|
|
42
|
+
const events = [];
|
|
43
|
+
const emit = (event) => { events.push(event); hooks.onEvent?.(event); };
|
|
44
|
+
const inputId = `in_${(0, node_crypto_1.randomUUID)()}`;
|
|
45
|
+
let turnId = null;
|
|
46
|
+
let streamed = '';
|
|
47
|
+
let providerRef = null;
|
|
48
|
+
let initialAccepted = false;
|
|
49
|
+
let settled = false;
|
|
50
|
+
let lastSeq = 0;
|
|
51
|
+
const unsubscribers = [];
|
|
52
|
+
let resolveDone;
|
|
53
|
+
const done = new Promise((resolve) => { resolveDone = resolve; });
|
|
54
|
+
const finish = (code, extra = {}) => {
|
|
55
|
+
if (settled)
|
|
56
|
+
return;
|
|
57
|
+
settled = true;
|
|
58
|
+
context.onHandle?.(null);
|
|
59
|
+
for (const unsubscribe of unsubscribers.splice(0))
|
|
60
|
+
unsubscribe();
|
|
61
|
+
if (extra.error)
|
|
62
|
+
emit({ kind: 'error', message: extra.error });
|
|
63
|
+
const usage = toNormalised(extra.usage);
|
|
64
|
+
resolveDone({ code, timedOut: false, spawnFailed: false, events, lastResultEvent: null, sessionId: providerRef ?? opts?.sessionId ?? null, stderrTail: '', child: null, ...(usage ? { usage } : {}), ...(turnId ? { coreTurnId: turnId } : {}), ...(extra.errorCode ? { errorCode: extra.errorCode } : {}) });
|
|
65
|
+
};
|
|
66
|
+
const apply = (envelope) => {
|
|
67
|
+
if (envelope.sessionId !== sessionId || envelope.seq <= lastSeq)
|
|
68
|
+
return;
|
|
69
|
+
lastSeq = envelope.seq;
|
|
70
|
+
const event = envelope.event;
|
|
71
|
+
switch (event.type) {
|
|
72
|
+
case 'session.provider-ref':
|
|
73
|
+
providerRef = event.providerSessionRef;
|
|
74
|
+
emit({ kind: 'session-started', sessionId: event.providerSessionRef });
|
|
75
|
+
return;
|
|
76
|
+
case 'input.state':
|
|
77
|
+
if (event.inputId !== inputId)
|
|
78
|
+
return;
|
|
79
|
+
if ((event.state === 'queued' || event.state === 'started') && !initialAccepted) {
|
|
80
|
+
initialAccepted = true;
|
|
81
|
+
hooks.onInitialInputAccepted?.();
|
|
82
|
+
}
|
|
83
|
+
if ((event.state === 'rejected' || event.state === 'interrupted') && !turnId)
|
|
84
|
+
finish(1, { error: `The agent did not start this turn (${event.reason ?? event.state}).` });
|
|
85
|
+
return;
|
|
86
|
+
case 'turn.started':
|
|
87
|
+
if (event.inputIds.includes(inputId))
|
|
88
|
+
turnId = event.turnId;
|
|
89
|
+
return;
|
|
90
|
+
case 'turn.output':
|
|
91
|
+
if (event.turnId !== turnId || event.channel !== 'text')
|
|
92
|
+
return;
|
|
93
|
+
streamed += event.delta;
|
|
94
|
+
emit({ kind: 'text-delta', text: event.delta });
|
|
95
|
+
return;
|
|
96
|
+
case 'turn.tool':
|
|
97
|
+
if (event.turnId !== turnId)
|
|
98
|
+
return;
|
|
99
|
+
if (event.phase === 'started')
|
|
100
|
+
emit({ kind: 'tool-use', name: event.name, toolUseId: event.toolUseId, inputPreview: event.input === undefined ? '' : JSON.stringify(event.input).slice(0, 1000) });
|
|
101
|
+
else
|
|
102
|
+
emit({ kind: 'tool-result', toolUseId: event.toolUseId, outputPreview: (event.output ?? '').slice(0, 4000), ...(event.isError ? { isError: true } : {}) });
|
|
103
|
+
return;
|
|
104
|
+
case 'turn.completed': {
|
|
105
|
+
if (event.turnId !== turnId)
|
|
106
|
+
return;
|
|
107
|
+
// Core's text is authoritative; emit whatever live deltas missed (e.g. after a lag).
|
|
108
|
+
if (event.text.startsWith(streamed) && event.text.length > streamed.length)
|
|
109
|
+
emit({ kind: 'text-delta', text: event.text.slice(streamed.length) });
|
|
110
|
+
if (event.status === 'completed')
|
|
111
|
+
finish(0, { usage: event.usage });
|
|
112
|
+
else if (event.status === 'stopped')
|
|
113
|
+
finish(null, { usage: event.usage });
|
|
114
|
+
else
|
|
115
|
+
finish(1, { usage: event.usage, error: event.error ?? `The agent turn ${event.status}.` });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
default:
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
/** Fill gaps from the journal so the turn's completion is never missed. */
|
|
123
|
+
let catchingUp = null;
|
|
124
|
+
const catchUp = async () => {
|
|
125
|
+
if (catchingUp)
|
|
126
|
+
return catchingUp;
|
|
127
|
+
catchingUp = (async () => {
|
|
128
|
+
for (;;) {
|
|
129
|
+
const page = await client.request('session.events', { sessionId, afterSeq: lastSeq, limit: 500 });
|
|
130
|
+
page.events.forEach(apply);
|
|
131
|
+
if (!page.hasMore || settled)
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
})().finally(() => { catchingUp = null; });
|
|
135
|
+
return catchingUp;
|
|
136
|
+
};
|
|
137
|
+
unsubscribers.push(client.onEvent((envelope) => {
|
|
138
|
+
if (envelope.sessionId !== sessionId || settled)
|
|
139
|
+
return;
|
|
140
|
+
if (envelope.seq > lastSeq + 1 && lastSeq > 0)
|
|
141
|
+
void catchUp().catch(() => undefined);
|
|
142
|
+
else
|
|
143
|
+
apply(envelope);
|
|
144
|
+
}), client.onLagged((id) => { if (id === sessionId)
|
|
145
|
+
void catchUp().catch(() => undefined); }));
|
|
146
|
+
// Registered last: a closed client calls back synchronously.
|
|
147
|
+
unsubscribers.push(client.onClose((reason) => finish(1, { error: `The session host stopped: ${reason}` })));
|
|
148
|
+
try {
|
|
149
|
+
// Open or resume the conversation's Core session, then align its configuration.
|
|
150
|
+
const config = { model: opts?.model, ...(opts?.reasoning_effort ? { effort: String(opts.reasoning_effort) } : {}), ...(opts?.systemPrompt || context.systemPromptAddendum ? { systemPrompt: [opts?.systemPrompt, context.systemPromptAddendum].filter(Boolean).join('\n\n') } : {}), policy: sessionPolicyFor(context) };
|
|
151
|
+
let snapshot;
|
|
152
|
+
try {
|
|
153
|
+
snapshot = (await client.request('session.open', { resume: { sessionId } })).snapshot;
|
|
154
|
+
await client.request('session.update', { sessionId, ...config });
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
if (!(0, agent_sessions_1.isSessionRequestError)(error, 'session_not_found'))
|
|
158
|
+
throw error;
|
|
159
|
+
snapshot = (await client.request('session.open', {
|
|
160
|
+
sessionId,
|
|
161
|
+
driver: context.driver,
|
|
162
|
+
cwd: hooks.cwd,
|
|
163
|
+
...config,
|
|
164
|
+
...(context.legacyProviderSessionRef ? { providerSessionRef: context.legacyProviderSessionRef } : {}),
|
|
165
|
+
...(context.metadata ? { metadata: context.metadata } : {}),
|
|
166
|
+
})).snapshot;
|
|
167
|
+
}
|
|
168
|
+
lastSeq = snapshot.lastSeq ?? 0;
|
|
169
|
+
context.onHandle?.({ interrupt: async () => { await client.request('session.interrupt', { sessionId }).catch(() => undefined); } });
|
|
170
|
+
hooks.onInputReady?.(createSteerSink(client, sessionId, () => !settled && turnId !== null));
|
|
171
|
+
await client.request('session.send', {
|
|
172
|
+
sessionId,
|
|
173
|
+
input: {
|
|
174
|
+
inputId,
|
|
175
|
+
text: opts?.prompt ?? '',
|
|
176
|
+
delivery: 'queue',
|
|
177
|
+
...(opts?.imagePaths?.length ? { attachments: opts.imagePaths.map((path) => ({ kind: 'image', path })) } : {}),
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
await catchUp();
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
finish(1, { error: error instanceof Error ? error.message : String(error), ...((0, agent_sessions_1.isSessionRequestError)(error) && error.code ? { errorCode: error.code } : {}) });
|
|
184
|
+
}
|
|
185
|
+
return done;
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/** Native steering into the running turn of the resident session. */
|
|
189
|
+
function createSteerSink(client, sessionId, turnOpen) {
|
|
190
|
+
return {
|
|
191
|
+
acceptedReceipt: 'received',
|
|
192
|
+
async send(input, onAccepted) {
|
|
193
|
+
if (!turnOpen())
|
|
194
|
+
return false;
|
|
195
|
+
await client.request('session.send', {
|
|
196
|
+
sessionId,
|
|
197
|
+
input: { inputId: input.id, text: input.text, delivery: 'steer', ...(input.imagePaths?.length ? { attachments: input.imagePaths.map((path) => ({ kind: 'image', path })) } : {}) },
|
|
198
|
+
});
|
|
199
|
+
// Core journals the input before it reaches the provider: it will never be replayed.
|
|
200
|
+
onAccepted?.();
|
|
201
|
+
return true;
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MissionCoreSessions = exports.GLOBAL_SCOPE = void 0;
|
|
4
|
+
const agent_mcp_config_1 = require("../../../agent-mcp-config");
|
|
5
|
+
const agent_sessions_1 = require("../../agent-sessions");
|
|
6
|
+
const agent_store_1 = require("../../agents/runtime/agent-store");
|
|
7
|
+
const agent_session_store_1 = require("./agent-session-store");
|
|
8
|
+
const core_session_runner_1 = require("./core-session-runner");
|
|
9
|
+
const mission_session_projector_1 = require("./mission-session-projector");
|
|
10
|
+
exports.GLOBAL_SCOPE = 'global';
|
|
11
|
+
/**
|
|
12
|
+
* Missions' side of Core agent sessions: decides whether a turn runs in Core,
|
|
13
|
+
* keeps each mission's session tracked and projected, and owns the lifetime of
|
|
14
|
+
* the Specrails MCP capability a resident session presents (it lives until the
|
|
15
|
+
* next turn rotates it or Core retires the provider process — continuation
|
|
16
|
+
* turns after sub-agents finish must still reach Specrails tools).
|
|
17
|
+
*/
|
|
18
|
+
class MissionCoreSessions {
|
|
19
|
+
deps;
|
|
20
|
+
capabilities = new Map();
|
|
21
|
+
handles = new Map();
|
|
22
|
+
/** Missions with a turn being prepared or running: their capability belongs to that turn. */
|
|
23
|
+
activeTurns = new Set();
|
|
24
|
+
tracked = new Set();
|
|
25
|
+
/** Last turn context per mission: the policy its session runs with. */
|
|
26
|
+
contexts = new Map();
|
|
27
|
+
constructor(deps) {
|
|
28
|
+
this.deps = deps;
|
|
29
|
+
}
|
|
30
|
+
scopeOf(conversation) {
|
|
31
|
+
return (conversation.pinned_project_id && this.deps.projectKey(conversation.pinned_project_id)) || exports.GLOBAL_SCOPE;
|
|
32
|
+
}
|
|
33
|
+
handle(conversationId) {
|
|
34
|
+
return this.handles.get(conversationId);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The Core turn context for this mission turn, or null when the turn must use
|
|
38
|
+
* the legacy runners (sessions disabled, scope degraded, provider not served).
|
|
39
|
+
*/
|
|
40
|
+
async prepareTurn(conversation, adapter, turn) {
|
|
41
|
+
const availability = await this.deps.availability();
|
|
42
|
+
if (!availability.enabled)
|
|
43
|
+
return null;
|
|
44
|
+
const scope = this.scopeOf(conversation);
|
|
45
|
+
if (!this.deps.registry.available(scope)) {
|
|
46
|
+
this.noticeFallback(conversation.id, scope);
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
let client;
|
|
50
|
+
try {
|
|
51
|
+
client = await this.deps.registry.acquire(scope);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
console.warn(`[agent-chat] Core sessions unavailable for ${scope}; using the legacy transport: ${error.message}`);
|
|
55
|
+
this.noticeFallback(conversation.id, scope);
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const drivers = client.initialize?.drivers ?? [];
|
|
59
|
+
if (!drivers.some((driver) => driver.id === adapter.id))
|
|
60
|
+
return null;
|
|
61
|
+
const conversationId = conversation.id;
|
|
62
|
+
if (!this.tracked.has(conversationId))
|
|
63
|
+
await this.track(scope, conversationId, this.deps.broadcast);
|
|
64
|
+
// Rotate the capability the resident bridge presents; the previous one is revoked.
|
|
65
|
+
const mcpServers = (0, agent_mcp_config_1.prepareAgentMcpSpec)({ conversationId, port: this.deps.port, capability: turn.capability, external: turn.external });
|
|
66
|
+
// App-installed plugin servers (same set a legacy spawn passes as argv); never shadow a configured name.
|
|
67
|
+
for (const plugin of turn.plugins ?? [])
|
|
68
|
+
if (!mcpServers.some((server) => server.name === plugin.name))
|
|
69
|
+
mcpServers.push(plugin);
|
|
70
|
+
const previous = this.capabilities.get(conversationId);
|
|
71
|
+
this.capabilities.set(conversationId, turn.capability);
|
|
72
|
+
this.activeTurns.add(conversationId);
|
|
73
|
+
if (previous && previous !== turn.capability)
|
|
74
|
+
this.deps.revokeCapability(previous);
|
|
75
|
+
// Parity with legacy missions: the adapter declares its permission level; user-scope MCP servers stay inherited.
|
|
76
|
+
const delegation = client.initialize?.capabilities?.delegation === 1;
|
|
77
|
+
const policy = {
|
|
78
|
+
subagents: this.subagentPolicy(conversation),
|
|
79
|
+
permissions: adapter.capabilities.sessionPermissions ?? 'workspace-write',
|
|
80
|
+
mcp: { inheritUserScope: true },
|
|
81
|
+
subagentRuntime: this.subagentRuntime(conversation, adapter.id, drivers, delegation),
|
|
82
|
+
};
|
|
83
|
+
this.contexts.set(conversationId, { policy, mcpServers, projectId: conversation.pinned_project_id ?? null, scope, provider: adapter.id, drivers, delegation });
|
|
84
|
+
return {
|
|
85
|
+
sessionId: conversationId,
|
|
86
|
+
client,
|
|
87
|
+
driver: adapter.id,
|
|
88
|
+
policy,
|
|
89
|
+
mcpServers,
|
|
90
|
+
...(policy.subagentRuntime?.mode === 'delegated' ? { systemPromptAddendum: delegationGuidance(policy.subagentRuntime) } : {}),
|
|
91
|
+
legacyProviderSessionRef: conversation.session_id ?? null,
|
|
92
|
+
metadata: { conversationId, surface: 'mission' },
|
|
93
|
+
onHandle: (handle) => {
|
|
94
|
+
if (handle) {
|
|
95
|
+
this.handles.set(conversationId, handle);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
this.handles.delete(conversationId);
|
|
99
|
+
this.activeTurns.delete(conversationId);
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The "Allow sub-agents" setting of a scope changed (`projectId: null` = missions
|
|
105
|
+
* without a project): send the new policy to each open session of that scope.
|
|
106
|
+
* Core applies it at the next idle point; with sub-agents running it reports
|
|
107
|
+
* the change as deferred, which the mission shows with "Stop agents and apply now".
|
|
108
|
+
*/
|
|
109
|
+
async refreshSubagentPolicy(scope) {
|
|
110
|
+
for (const [conversationId, context] of [...this.contexts]) {
|
|
111
|
+
if (context.projectId !== scope.projectId || !this.tracked.has(conversationId))
|
|
112
|
+
continue;
|
|
113
|
+
const conversation = (0, agent_store_1.getAgentConversation)(this.deps.db, conversationId);
|
|
114
|
+
if (!conversation)
|
|
115
|
+
continue;
|
|
116
|
+
const subagents = this.subagentPolicy(conversation);
|
|
117
|
+
const subagentRuntime = this.subagentRuntime(conversation, context.provider, context.drivers, context.delegation);
|
|
118
|
+
if (subagents === context.policy.subagents && JSON.stringify(subagentRuntime) === JSON.stringify(context.policy.subagentRuntime))
|
|
119
|
+
continue;
|
|
120
|
+
const next = { ...context, policy: { ...context.policy, subagents, subagentRuntime } };
|
|
121
|
+
this.contexts.set(conversationId, next);
|
|
122
|
+
try {
|
|
123
|
+
const client = await this.deps.registry.acquire(context.scope);
|
|
124
|
+
await client.request('session.update', { sessionId: conversationId, policy: (0, core_session_runner_1.sessionPolicyFor)(next) });
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
// The next turn sends the policy again; nothing is lost.
|
|
128
|
+
console.warn(`[agent-chat] could not update the sub-agent policy of ${conversationId}: ${error.message}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** The turn runs on the legacy transport because its scope is degraded: say why, once per turn. */
|
|
133
|
+
noticeFallback(conversationId, scope) {
|
|
134
|
+
if (this.deps.registry.status(scope) !== 'degraded')
|
|
135
|
+
return;
|
|
136
|
+
const code = this.deps.registry.degradedCode(scope) === 'journal_locked' ? 'journal_locked' : 'host_degraded';
|
|
137
|
+
const detail = this.deps.registry.hosts().find((host) => host.scope === scope)?.detail ?? null;
|
|
138
|
+
this.deps.broadcast({ type: 'agent_session_notice', conversationId, level: 'warning', code, scope, message: detail ?? code, timestamp: new Date().toISOString() });
|
|
139
|
+
}
|
|
140
|
+
/** The hybrid runtime for this mission; says so in the mission when a choice cannot be honoured. */
|
|
141
|
+
subagentRuntime(conversation, provider, drivers, delegation) {
|
|
142
|
+
const choice = this.deps.subagentRuntime?.(conversation) ?? null;
|
|
143
|
+
const resolution = (0, agent_sessions_1.resolveSubagentRuntime)({ choice, missionProvider: provider, drivers, delegation });
|
|
144
|
+
if (resolution.unavailable && choice && this.subagentPolicy(conversation) === 'enabled') {
|
|
145
|
+
this.deps.broadcast({ type: 'agent_session_notice', conversationId: conversation.id, level: 'warning', code: `subagents.${resolution.unavailable}`, provider: choice.provider, message: `${choice.provider} sub-agents are not available here; the mission agent runs its own.`, timestamp: new Date().toISOString() });
|
|
146
|
+
}
|
|
147
|
+
return resolution.runtime;
|
|
148
|
+
}
|
|
149
|
+
subagentPolicy(conversation) {
|
|
150
|
+
return this.deps.subagentPolicy?.(conversation) ?? 'enabled';
|
|
151
|
+
}
|
|
152
|
+
/** True when the capability of this turn must outlive the turn (resident session). */
|
|
153
|
+
retains(conversationId, capability) {
|
|
154
|
+
return this.capabilities.get(conversationId) === capability;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Core retired the provider process: nothing can present the capability anymore.
|
|
158
|
+
* While a turn is prepared or running the capability is that turn's: a process
|
|
159
|
+
* retired by its own config change (model, effort, policy) is replaced at once
|
|
160
|
+
* and the new process presents the same capability, so it must stay valid.
|
|
161
|
+
*/
|
|
162
|
+
releaseCapability(conversationId, options = {}) {
|
|
163
|
+
if (!options.force && this.activeTurns.has(conversationId))
|
|
164
|
+
return;
|
|
165
|
+
const capability = this.capabilities.get(conversationId);
|
|
166
|
+
if (!capability)
|
|
167
|
+
return;
|
|
168
|
+
this.capabilities.delete(conversationId);
|
|
169
|
+
this.deps.revokeCapability(capability);
|
|
170
|
+
(0, agent_mcp_config_1.removeAgentCapabilityFile)(conversationId);
|
|
171
|
+
}
|
|
172
|
+
/** True when this mission's sub-agents are launched by Core (another provider). */
|
|
173
|
+
delegates(conversationId) {
|
|
174
|
+
return this.contexts.get(conversationId)?.policy.subagentRuntime?.mode === 'delegated';
|
|
175
|
+
}
|
|
176
|
+
/** Launch a sub-agent on the configured provider (delegated runtime only). */
|
|
177
|
+
async delegate(conversation, params) {
|
|
178
|
+
this.assertDelegates(conversation.id);
|
|
179
|
+
const client = await this.deps.registry.acquire(this.scopeOf(conversation));
|
|
180
|
+
return client.request('session.delegate', { sessionId: conversation.id, ...params });
|
|
181
|
+
}
|
|
182
|
+
/** Results of delegated sub-agents that finished (or what still runs at the timeout). */
|
|
183
|
+
async waitSubagents(conversation, subagentIds, timeoutMs) {
|
|
184
|
+
this.assertDelegates(conversation.id);
|
|
185
|
+
const client = await this.deps.registry.acquire(this.scopeOf(conversation));
|
|
186
|
+
return client.request('session.waitSubagents', { sessionId: conversation.id, ...(subagentIds ? { subagentIds } : {}), timeoutMs });
|
|
187
|
+
}
|
|
188
|
+
assertDelegates(conversationId) {
|
|
189
|
+
if (!this.tracked.has(conversationId) || !this.delegates(conversationId)) {
|
|
190
|
+
throw new Error('This mission does not delegate sub-agents: its agent launches them with its own tool, or sub-agents are off.');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/** Stop sub-agents of a mission's Core session (all when `subagentIds` is omitted). */
|
|
194
|
+
async stopSubagents(conversation, subagentIds) {
|
|
195
|
+
if (!this.tracked.has(conversation.id))
|
|
196
|
+
return [];
|
|
197
|
+
const client = await this.deps.registry.acquire(this.scopeOf(conversation));
|
|
198
|
+
const result = await client.request('session.stopSubagents', { sessionId: conversation.id, ...(subagentIds ? { subagentIds } : {}) });
|
|
199
|
+
return result.stopped ?? [];
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Rebuild a mission's derived session rows (sub-agents, their output, resident
|
|
203
|
+
* state, cursor) by replaying Core's journal from the start. Turns already
|
|
204
|
+
* recorded as messages/invocations are recognized, not duplicated. The replay
|
|
205
|
+
* is silent; clients then receive the rebuilt state in one pass.
|
|
206
|
+
*/
|
|
207
|
+
async rebuildProjection(conversation) {
|
|
208
|
+
const scope = this.scopeOf(conversation);
|
|
209
|
+
const conversationId = conversation.id;
|
|
210
|
+
if (!(0, agent_session_store_1.getSessionCursor)(this.deps.db, conversationId))
|
|
211
|
+
return null;
|
|
212
|
+
this.deps.registry.untrack(scope, conversationId);
|
|
213
|
+
this.tracked.delete(conversationId);
|
|
214
|
+
(0, agent_session_store_1.resetSessionProjection)(this.deps.db, conversationId);
|
|
215
|
+
let muted = true;
|
|
216
|
+
try {
|
|
217
|
+
await this.track(scope, conversationId, (message) => { if (!muted)
|
|
218
|
+
this.deps.broadcast(message); });
|
|
219
|
+
}
|
|
220
|
+
finally {
|
|
221
|
+
muted = false;
|
|
222
|
+
}
|
|
223
|
+
const cursor = (0, agent_session_store_1.getSessionCursor)(this.deps.db, conversationId);
|
|
224
|
+
const subagents = (0, agent_session_store_1.listSubagents)(this.deps.db, conversationId);
|
|
225
|
+
const timestamp = new Date().toISOString();
|
|
226
|
+
if (cursor)
|
|
227
|
+
this.deps.broadcast({ type: 'agent_resident_state', conversationId, phase: cursor.residentPhase, liveSubagents: cursor.liveSubagents, processAlive: cursor.processAlive, timestamp });
|
|
228
|
+
for (const { conversationId: _owner, ...subagent } of subagents)
|
|
229
|
+
this.deps.broadcast({ type: 'agent_subagent', conversationId, subagent, timestamp });
|
|
230
|
+
return { lastSeq: cursor?.lastSeq ?? 0, subagents: subagents.length };
|
|
231
|
+
}
|
|
232
|
+
async track(scope, conversationId, broadcast) {
|
|
233
|
+
(0, agent_session_store_1.ensureSessionCursor)(this.deps.db, conversationId, conversationId, scope);
|
|
234
|
+
const projector = new mission_session_projector_1.MissionSessionProjector(conversationId, {
|
|
235
|
+
db: this.deps.db,
|
|
236
|
+
broadcast,
|
|
237
|
+
adapterFor: this.deps.adapterFor,
|
|
238
|
+
onProcessEnded: (id) => this.releaseCapability(id),
|
|
239
|
+
});
|
|
240
|
+
try {
|
|
241
|
+
await this.deps.registry.track(scope, conversationId, projector, () => { });
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
// A session that does not exist yet has nothing to replay; tracking starts empty.
|
|
245
|
+
if (error.code !== 'session_not_found')
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
this.tracked.add(conversationId);
|
|
249
|
+
}
|
|
250
|
+
/** Conversation deleted: close its Core session and drop all local state. */
|
|
251
|
+
async close(conversation) {
|
|
252
|
+
const scope = this.scopeOf(conversation);
|
|
253
|
+
this.handles.delete(conversation.id);
|
|
254
|
+
this.activeTurns.delete(conversation.id);
|
|
255
|
+
this.contexts.delete(conversation.id);
|
|
256
|
+
this.releaseCapability(conversation.id, { force: true });
|
|
257
|
+
if (!this.tracked.delete(conversation.id))
|
|
258
|
+
return;
|
|
259
|
+
this.deps.registry.untrack(scope, conversation.id);
|
|
260
|
+
try {
|
|
261
|
+
const client = await this.deps.registry.acquire(scope);
|
|
262
|
+
await client.request('session.close', { sessionId: conversation.id, reason: 'conversation_deleted' });
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
console.warn(`[agent-chat] could not close Core session ${conversation.id}: ${error.message}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
shutdown() {
|
|
269
|
+
for (const conversationId of [...this.capabilities.keys()])
|
|
270
|
+
this.releaseCapability(conversationId, { force: true });
|
|
271
|
+
this.handles.clear();
|
|
272
|
+
this.activeTurns.clear();
|
|
273
|
+
this.contexts.clear();
|
|
274
|
+
this.tracked.clear();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
exports.MissionCoreSessions = MissionCoreSessions;
|
|
278
|
+
/** What the mission agent needs to know when Core launches its sub-agents. */
|
|
279
|
+
function delegationGuidance(runtime) {
|
|
280
|
+
return [
|
|
281
|
+
'## Sub-agents',
|
|
282
|
+
`In this mission, sub-agents run on ${runtime.driver} (${runtime.model}) and are launched by Specrails, not by your own sub-agent tool (it is unavailable).`,
|
|
283
|
+
'To delegate a self-contained task, call `specrails_mission` with `action: "subagent_start"`, a short `description` and complete `instructions`; set `contextTurns` when it needs this conversation.',
|
|
284
|
+
'Start several to work in parallel. Collect results with `subagent_wait`; results you do not wait for arrive later as a message. Use `subagent_list` to see them and `subagent_stop` to stop them.',
|
|
285
|
+
].join('\n');
|
|
286
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MissionSessionProjector = void 0;
|
|
4
|
+
const desktop_db_1 = require("../../../desktop-db");
|
|
5
|
+
const result_event_1 = require("../../accounting/runtime/result-event");
|
|
6
|
+
const agent_store_1 = require("../../agents/runtime/agent-store");
|
|
7
|
+
const agent_session_store_1 = require("./agent-session-store");
|
|
8
|
+
/**
|
|
9
|
+
* Missions' ProjectionSink: persists what happens in a mission's Core session
|
|
10
|
+
* OUTSIDE user turns (which the mission manager settles itself): turns Core or
|
|
11
|
+
* policy started after sub-agents finished, the sub-agent tree and its output,
|
|
12
|
+
* and the resident phase. Every apply writes its rows and the cursor in one
|
|
13
|
+
* transaction; WebSocket messages are sent only after the commit.
|
|
14
|
+
*/
|
|
15
|
+
class MissionSessionProjector {
|
|
16
|
+
conversationId;
|
|
17
|
+
deps;
|
|
18
|
+
background = new Map();
|
|
19
|
+
constructor(conversationId, deps) {
|
|
20
|
+
this.conversationId = conversationId;
|
|
21
|
+
this.deps = deps;
|
|
22
|
+
}
|
|
23
|
+
cursor(_sessionId) {
|
|
24
|
+
return (0, agent_session_store_1.getSessionCursor)(this.deps.db, this.conversationId)?.lastSeq ?? 0;
|
|
25
|
+
}
|
|
26
|
+
apply(_sessionId, seq, ops) {
|
|
27
|
+
const conversationId = this.conversationId;
|
|
28
|
+
const outbox = [];
|
|
29
|
+
const timestamp = new Date().toISOString();
|
|
30
|
+
let processEnded = false;
|
|
31
|
+
this.deps.db.transaction(() => {
|
|
32
|
+
for (const op of ops) {
|
|
33
|
+
switch (op.kind) {
|
|
34
|
+
case 'resident.phase': {
|
|
35
|
+
const before = (0, agent_session_store_1.getSessionCursor)(this.deps.db, conversationId);
|
|
36
|
+
(0, agent_session_store_1.setResidentState)(this.deps.db, conversationId, op.phase, op.processAlive, op.liveSubagents);
|
|
37
|
+
if (before?.processAlive && !op.processAlive)
|
|
38
|
+
processEnded = true;
|
|
39
|
+
outbox.push({ type: 'agent_resident_state', conversationId, phase: op.phase, liveSubagents: op.liveSubagents, processAlive: op.processAlive, timestamp });
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
case 'turn.opened':
|
|
43
|
+
if (op.origin === 'user')
|
|
44
|
+
break;
|
|
45
|
+
this.background.set(op.turnId, { origin: op.origin, triggeredBy: op.triggeredBy, startedAt: op.at });
|
|
46
|
+
outbox.push({ type: 'agent_turn_started', conversationId, turnId: op.turnId, origin: op.origin, triggeredBy: op.triggeredBy, timestamp });
|
|
47
|
+
break;
|
|
48
|
+
case 'turn.delta':
|
|
49
|
+
if (op.channel === 'text' && this.background.has(op.turnId))
|
|
50
|
+
outbox.push({ type: 'agent_stream', conversationId, turnId: op.turnId, delta: op.delta, timestamp });
|
|
51
|
+
break;
|
|
52
|
+
case 'turn.tool':
|
|
53
|
+
if (!this.background.has(op.turnId))
|
|
54
|
+
break;
|
|
55
|
+
outbox.push(op.tool.phase === 'started'
|
|
56
|
+
? { type: 'agent_tool', conversationId, turnId: op.turnId, tool: op.tool.name, toolId: op.tool.toolUseId, timestamp }
|
|
57
|
+
: { type: 'agent_tool_result', conversationId, turnId: op.turnId, toolId: op.tool.toolUseId, output: (op.tool.output ?? '').slice(0, 4000), ...(op.tool.isError ? { isError: true } : {}), timestamp });
|
|
58
|
+
break;
|
|
59
|
+
case 'turn.closed': {
|
|
60
|
+
const turn = this.background.get(op.turnId);
|
|
61
|
+
if (!turn)
|
|
62
|
+
break;
|
|
63
|
+
this.background.delete(op.turnId);
|
|
64
|
+
const conversation = (0, agent_store_1.getAgentConversation)(this.deps.db, conversationId);
|
|
65
|
+
if (!conversation)
|
|
66
|
+
break;
|
|
67
|
+
// A journal replay (projection rebuild) must not record a turn twice.
|
|
68
|
+
if ((0, agent_session_store_1.isCoreTurnRecorded)(this.deps.db, conversationId, op.turnId)) {
|
|
69
|
+
outbox.push({ type: 'agent_turn_done', conversationId, turnId: op.turnId, origin: turn.origin, status: op.status, triggeredBy: turn.triggeredBy, timestamp });
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
const message = op.text.trim()
|
|
73
|
+
? (0, agent_store_1.addAgentMessage)(this.deps.db, { conversationId, role: 'assistant', content: op.text, turnOrigin: turn.origin, coreTurnId: op.turnId })
|
|
74
|
+
: null;
|
|
75
|
+
this.recordInvocation(conversation, { id: (0, agent_session_store_1.coreTurnInvocationId)(conversation.id, op.turnId), providerId: conversation.provider, model: null, usage: op.usage, status: op.status, origin: turn.origin, startedAt: turn.startedAt, finishedAt: op.at });
|
|
76
|
+
outbox.push({ type: 'agent_turn_done', conversationId, turnId: op.turnId, origin: turn.origin, status: op.status, triggeredBy: turn.triggeredBy, fullText: op.text, ...(message ? { messageId: message.id } : {}), timestamp });
|
|
77
|
+
if (conversation.pinned_project_id)
|
|
78
|
+
outbox.push({ type: 'spending.invalidated', projectId: conversation.pinned_project_id });
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
case 'subagent.upsert':
|
|
82
|
+
(0, agent_session_store_1.upsertSubagent)(this.deps.db, conversationId, op.subagent);
|
|
83
|
+
outbox.push({ type: 'agent_subagent', conversationId, subagent: op.subagent, timestamp });
|
|
84
|
+
break;
|
|
85
|
+
case 'subagent.output':
|
|
86
|
+
if (!this.subagentKnown(op.subagentId))
|
|
87
|
+
break;
|
|
88
|
+
(0, agent_session_store_1.appendSubagentEvent)(this.deps.db, conversationId, op.subagentId, op.seq, op.channel, op.delta, op.tool);
|
|
89
|
+
outbox.push({ type: 'agent_subagent_event', conversationId, subagentId: op.subagentId, seq: op.seq, channel: op.channel, ...(op.delta !== null ? { delta: op.delta } : {}), ...(op.tool ? { tool: op.tool } : {}), timestamp });
|
|
90
|
+
break;
|
|
91
|
+
case 'subagent.billed': {
|
|
92
|
+
// A delegated sub-agent's own spend: recorded once (deterministic id), on top of the parent's.
|
|
93
|
+
const conversation = (0, agent_store_1.getAgentConversation)(this.deps.db, conversationId);
|
|
94
|
+
if (!conversation)
|
|
95
|
+
break;
|
|
96
|
+
const invocationId = `core-subagent:${conversationId}:${op.subagentId}:${op.seq}`;
|
|
97
|
+
if (this.deps.db.prepare('SELECT 1 FROM agent_invocations WHERE id = ?').get(invocationId))
|
|
98
|
+
break;
|
|
99
|
+
const startedAt = this.subagentStartedAt(op.subagentId) ?? op.at;
|
|
100
|
+
this.recordInvocation(conversation, { id: invocationId, providerId: op.driver, model: op.model, usage: op.usage, status: 'completed', origin: 'subagent', startedAt, finishedAt: op.at });
|
|
101
|
+
if (conversation.pinned_project_id)
|
|
102
|
+
outbox.push({ type: 'spending.invalidated', projectId: conversation.pinned_project_id });
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
case 'session.updated':
|
|
106
|
+
outbox.push({ type: 'agent_session_updated', conversationId, outcome: op.outcome, changes: op.changes, timestamp });
|
|
107
|
+
break;
|
|
108
|
+
case 'notice':
|
|
109
|
+
outbox.push({ type: 'agent_session_notice', conversationId, level: op.level, code: op.code, message: op.message, timestamp });
|
|
110
|
+
break;
|
|
111
|
+
case 'provider.ref':
|
|
112
|
+
case 'input.state':
|
|
113
|
+
case 'session.closed':
|
|
114
|
+
// User inputs, provider refs and closure are settled by the mission manager.
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
(0, agent_session_store_1.advanceSessionCursor)(this.deps.db, conversationId, seq);
|
|
119
|
+
})();
|
|
120
|
+
for (const message of outbox)
|
|
121
|
+
this.deps.broadcast(message);
|
|
122
|
+
if (processEnded)
|
|
123
|
+
this.deps.onProcessEnded?.(conversationId);
|
|
124
|
+
}
|
|
125
|
+
subagentKnown(subagentId) {
|
|
126
|
+
return !!this.deps.db.prepare('SELECT 1 FROM agent_subagents WHERE conversation_id = ? AND subagent_id = ?').get(this.conversationId, subagentId);
|
|
127
|
+
}
|
|
128
|
+
subagentStartedAt(subagentId) {
|
|
129
|
+
const row = this.deps.db.prepare('SELECT started_at FROM agent_subagents WHERE conversation_id = ? AND subagent_id = ?').get(this.conversationId, subagentId);
|
|
130
|
+
return row?.started_at ?? null;
|
|
131
|
+
}
|
|
132
|
+
recordInvocation(conversation, invocation) {
|
|
133
|
+
let adapter;
|
|
134
|
+
try {
|
|
135
|
+
adapter = this.deps.adapterFor(invocation.providerId);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const usage = invocation.usage;
|
|
141
|
+
const normalised = {
|
|
142
|
+
...(usage.inputTokens !== null ? { tokens_in: usage.inputTokens } : {}),
|
|
143
|
+
...(usage.outputTokens !== null ? { tokens_out: usage.outputTokens } : {}),
|
|
144
|
+
...(usage.cacheReadTokens !== null ? { tokens_cache_read: usage.cacheReadTokens } : {}),
|
|
145
|
+
...(usage.cacheWriteTokens !== null ? { tokens_cache_create: usage.cacheWriteTokens } : {}),
|
|
146
|
+
...(usage.costUsd !== null && !usage.costEstimated ? { total_cost_usd: usage.costUsd } : {}),
|
|
147
|
+
...(usage.model ?? invocation.model ? { model: usage.model ?? invocation.model } : {}),
|
|
148
|
+
};
|
|
149
|
+
const fallbackModel = invocation.model ?? (invocation.providerId === conversation.provider ? conversation.model : null);
|
|
150
|
+
const { result, estimated } = (0, result_event_1.finaliseNormalisedResult)(adapter, normalised, {
|
|
151
|
+
...(fallbackModel ? { fallbackModel } : {}),
|
|
152
|
+
durationMs: Math.max(0, Date.parse(invocation.finishedAt) - Date.parse(invocation.startedAt)),
|
|
153
|
+
});
|
|
154
|
+
(0, desktop_db_1.recordAgentInvocation)(this.deps.db, {
|
|
155
|
+
id: invocation.id,
|
|
156
|
+
conversation_id: conversation.id,
|
|
157
|
+
project_id: conversation.pinned_project_id ?? null,
|
|
158
|
+
provider: adapter.id,
|
|
159
|
+
status: invocation.status === 'completed' ? 'success' : invocation.status === 'stopped' || invocation.status === 'interrupted' ? 'aborted' : 'failed',
|
|
160
|
+
started_at: invocation.startedAt,
|
|
161
|
+
finished_at: invocation.finishedAt,
|
|
162
|
+
total_cost_usd_estimated: estimated,
|
|
163
|
+
origin: invocation.origin,
|
|
164
|
+
...result,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
exports.MissionSessionProjector = MissionSessionProjector;
|