@axhub/genie 0.2.10 → 0.2.12

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.
Files changed (96) hide show
  1. package/dist/api-docs.html +2 -2
  2. package/dist/assets/App-Clb2COtW.js +274 -0
  3. package/dist/assets/ImagePlaygroundPage-DqhMSbM8.js +106 -0
  4. package/dist/assets/ImagePlaygroundPage-MEn3NN80.css +1 -0
  5. package/dist/assets/ReviewApp-CDcLYe-u.js +1 -0
  6. package/dist/assets/{_basePickBy-DVVb07UV.js → _basePickBy-jUZsM51q.js} +1 -1
  7. package/dist/assets/{_baseUniq-BtbziL5G.js → _baseUniq-BXglE6_v.js} +1 -1
  8. package/dist/assets/{arc-BsCC8yBD.js → arc-D-oFCFBv.js} +1 -1
  9. package/dist/assets/{architectureDiagram-2XIMDMQ5-woFp6eNI.js → architectureDiagram-2XIMDMQ5-DC8bAnQt.js} +1 -1
  10. package/dist/assets/{blockDiagram-WCTKOSBZ-ya8VAc2k.js → blockDiagram-WCTKOSBZ-C4semIRc.js} +1 -1
  11. package/dist/assets/{c4Diagram-IC4MRINW-CY1dZmIZ.js → c4Diagram-IC4MRINW-FHj1QO3y.js} +1 -1
  12. package/dist/assets/channel-BF4woPXX.js +1 -0
  13. package/dist/assets/{chunk-4BX2VUAB-CR1lAd74.js → chunk-4BX2VUAB-D-LjsQ_s.js} +1 -1
  14. package/dist/assets/{chunk-55IACEB6-CP98WcFC.js → chunk-55IACEB6-DI3j_d7A.js} +1 -1
  15. package/dist/assets/{chunk-FMBD7UC4-D9c7ijAB.js → chunk-FMBD7UC4-BEVnaLFN.js} +1 -1
  16. package/dist/assets/{chunk-JSJVCQXG-DQAGYOn-.js → chunk-JSJVCQXG-CSxpcErk.js} +1 -1
  17. package/dist/assets/{chunk-KX2RTZJC-BbTXiDq7.js → chunk-KX2RTZJC-BbuhDN4h.js} +1 -1
  18. package/dist/assets/{chunk-NQ4KR5QH-BI6AX0dr.js → chunk-NQ4KR5QH-C3x61XQa.js} +1 -1
  19. package/dist/assets/{chunk-QZHKN3VN-DB3V2Ifo.js → chunk-QZHKN3VN-DxWOFtPh.js} +1 -1
  20. package/dist/assets/{chunk-WL4C6EOR-DhzTthv6.js → chunk-WL4C6EOR-Bt2OauD2.js} +1 -1
  21. package/dist/assets/classDiagram-VBA2DB6C-D2kHlnQ7.js +1 -0
  22. package/dist/assets/classDiagram-v2-RAHNMMFH-D2kHlnQ7.js +1 -0
  23. package/dist/assets/clone-CqBvwCJW.js +1 -0
  24. package/dist/assets/{cose-bilkent-S5V4N54A-BQ09ZE2j.js → cose-bilkent-S5V4N54A-Dexadrue.js} +1 -1
  25. package/dist/assets/{dagre-KLK3FWXG-Dc2ueD_R.js → dagre-KLK3FWXG-F9U4X2xC.js} +1 -1
  26. package/dist/assets/{diagram-E7M64L7V-DP-LsQoL.js → diagram-E7M64L7V-B3V17aH3.js} +1 -1
  27. package/dist/assets/{diagram-IFDJBPK2-Cg6r42cB.js → diagram-IFDJBPK2-CdHAmLL1.js} +1 -1
  28. package/dist/assets/{diagram-P4PSJMXO-aHsfoUZE.js → diagram-P4PSJMXO-CrTNfk8K.js} +1 -1
  29. package/dist/assets/{erDiagram-INFDFZHY-qBXJ4aAz.js → erDiagram-INFDFZHY-vDh9SWK9.js} +1 -1
  30. package/dist/assets/{flowDiagram-PKNHOUZH-D_13emJM.js → flowDiagram-PKNHOUZH-DpltMg7L.js} +1 -1
  31. package/dist/assets/{ganttDiagram-A5KZAMGK-BvIcOLwz.js → ganttDiagram-A5KZAMGK-COTk2xur.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-K3NZZRJ6-ad0vvNcU.js → gitGraphDiagram-K3NZZRJ6-BNV7bvvj.js} +1 -1
  33. package/dist/assets/{graph-CeJCMjan.js → graph-Dkeg9oys.js} +1 -1
  34. package/dist/assets/{highlighted-body-TPN3WLV5-B_novwSz.js → highlighted-body-TPN3WLV5-DaiQEBwR.js} +1 -1
  35. package/dist/assets/index-DgGmiqsP.css +1 -0
  36. package/dist/assets/index-DvA901Vs.js +2 -0
  37. package/dist/assets/{infoDiagram-LFFYTUFH-lOxAqb3m.js → infoDiagram-LFFYTUFH-CZioW3Gt.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-PHBUUO56-DIr-51gj.js → ishikawaDiagram-PHBUUO56-BbqR3i1B.js} +1 -1
  39. package/dist/assets/{journeyDiagram-4ABVD52K-CYcIW0ZU.js → journeyDiagram-4ABVD52K-wfb-WHzl.js} +1 -1
  40. package/dist/assets/{kanban-definition-K7BYSVSG-C1ZK616a.js → kanban-definition-K7BYSVSG-B3c4y3VN.js} +1 -1
  41. package/dist/assets/{layout-CI2RM-v6.js → layout-Xr9Z2VGF.js} +1 -1
  42. package/dist/assets/{linear-DE7bISck.js → linear-JBmzAJtl.js} +1 -1
  43. package/dist/assets/{mermaid-O7DHMXV3-XxAJo8EK.js → mermaid-O7DHMXV3-fDuyNLKe.js} +238 -220
  44. package/dist/assets/{mindmap-definition-YRQLILUH-Dz6EFjmn.js → mindmap-definition-YRQLILUH-B5NTN_jD.js} +1 -1
  45. package/dist/assets/{pieDiagram-SKSYHLDU-DPpEzUed.js → pieDiagram-SKSYHLDU-CuO98GVu.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-337W2JSQ-xdoXNet7.js → quadrantDiagram-337W2JSQ-LL3f4vLf.js} +1 -1
  47. package/dist/assets/{requirementDiagram-Z7DCOOCP-DUq8H3CL.js → requirementDiagram-Z7DCOOCP-Di-2O6LH.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-WA2Y5GQK-CmqEUxRu.js → sankeyDiagram-WA2Y5GQK-9lHqrXqR.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-2WXFIKYE-DhtXRNiH.js → sequenceDiagram-2WXFIKYE-BQu-SoGr.js} +1 -1
  50. package/dist/assets/{stateDiagram-RAJIS63D-Dj0HOlbN.js → stateDiagram-RAJIS63D-BUxvd2BC.js} +1 -1
  51. package/dist/assets/stateDiagram-v2-FVOUBMTO-CDVexTiR.js +1 -0
  52. package/dist/assets/{timeline-definition-YZTLITO2-DUuJzZB5.js → timeline-definition-YZTLITO2-oP47UEU6.js} +1 -1
  53. package/dist/assets/{treemap-KZPCXAKY-DpYBQ0qr.js → treemap-KZPCXAKY-BRjDo2aE.js} +1 -1
  54. package/dist/assets/{vendor-codemirror-CMHSJ_9p.js → vendor-codemirror-BiCeS-y4.js} +1 -1
  55. package/dist/assets/{vendor-react-xmA_f8ig.js → vendor-react-DVlYPmi3.js} +1 -1
  56. package/dist/assets/{vennDiagram-LZ73GAT5-DpePUyOd.js → vennDiagram-LZ73GAT5-DrRqcDqo.js} +1 -1
  57. package/dist/assets/{xychartDiagram-JWTSCODW-Cfp1I4_U.js → xychartDiagram-JWTSCODW-DUXrymAi.js} +1 -1
  58. package/dist/index.html +4 -4
  59. package/package.json +25 -6
  60. package/scripts/refresh-acp-default-capabilities.mjs +160 -0
  61. package/server/acp-runtime/client.js +1137 -181
  62. package/server/acp-runtime/command-overrides.js +48 -0
  63. package/server/acp-runtime/index.js +576 -16
  64. package/server/acp-runtime/registry.js +6 -4
  65. package/server/acp-runtime/session-store.js +235 -92
  66. package/server/database/db.js +12 -3
  67. package/server/external-agent/ws.js +212 -11
  68. package/server/index.js +156 -53
  69. package/server/projects-watcher-config.js +4 -0
  70. package/server/projects.js +485 -125
  71. package/server/routes/cc-connect.js +5 -4
  72. package/server/routes/codex.js +24 -0
  73. package/server/routes/commands.js +166 -10
  74. package/server/routes/runs.js +641 -0
  75. package/server/routes/session-core.js +357 -109
  76. package/server/session-core/eventStore.js +0 -121
  77. package/server/session-core/providerAdapters.js +644 -163
  78. package/server/session-core/providerDiscovery.js +66 -38
  79. package/server/session-core/runRegistry.js +244 -0
  80. package/server/session-core/runtimeState.js +75 -3
  81. package/server/session-core/runtimeWriter.js +132 -10
  82. package/server/utils/codexImagePlayground.js +479 -0
  83. package/server/utils/localTerminal.js +56 -0
  84. package/server/utils/shellCommand.js +70 -0
  85. package/shared/acpCapabilities.js +393 -0
  86. package/shared/acpDefaultCapabilities.generated.json +141 -0
  87. package/shared/conversationEvents.js +425 -121
  88. package/dist/assets/App-CYCCsgwf.js +0 -264
  89. package/dist/assets/ReviewApp-0srHIXwb.js +0 -1
  90. package/dist/assets/channel-BMhScXFe.js +0 -1
  91. package/dist/assets/classDiagram-VBA2DB6C-CMIxlWcT.js +0 -1
  92. package/dist/assets/classDiagram-v2-RAHNMMFH-CMIxlWcT.js +0 -1
  93. package/dist/assets/clone-BPqOt4r3.js +0 -1
  94. package/dist/assets/index-C514cLyb.js +0 -2
  95. package/dist/assets/index-h1DBl_g3.css +0 -1
  96. package/dist/assets/stateDiagram-v2-FVOUBMTO-C9utf5gv.js +0 -1
@@ -1,18 +1,12 @@
1
- import { promises as fs } from 'fs';
2
- import path from 'path';
3
- import os from 'os';
4
1
  import { SUPPORTED_PROVIDERS } from '../../shared/conversationEvents.js';
5
2
  import {
6
- detectProviderInstallationStatus,
7
- checkClaudeCredentials, checkCodexCredentials,
8
- checkGeminiCredentials,
9
- checkOpencodeCredentials
3
+ detectProviderInstallationStatus
10
4
  } from '../routes/cli-auth.js';
11
5
  import { listOpencodeModels } from '../opencode-cli.js';
12
6
  import { spawnCommand } from '../utils/spawnCommand.js';
13
7
  import {
14
8
  CLAUDE_MODELS,
15
- CODEX_MODELS, GEMINI_MODELS,
9
+ GEMINI_MODELS,
16
10
  OPENCODE_MODELS
17
11
  } from '../../shared/modelConstants.js';
18
12
 
@@ -21,6 +15,9 @@ const PROVIDER_CAPABILITIES = {
21
15
  gemini: { modes: ['default', 'acceptEdits', 'bypassPermissions'], thinking: false, resume: true },
22
16
  opencode: { modes: ['default', 'acceptEdits', 'bypassPermissions', 'plan'], thinking: false, resume: true }
23
17
  };
18
+ const MODEL_DISCOVERY_CACHE_TTL_MS = 30 * 60 * 1000;
19
+ const modelDiscoveryCache = new Map();
20
+ const modelDiscoveryInFlight = new Map();
24
21
 
25
22
  function runCommand(command, args = [], options = {}) {
26
23
  return new Promise((resolve) => {
@@ -160,64 +157,92 @@ async function discoverClaudeModels() {
160
157
  }
161
158
 
162
159
  async function discoverCodexModels() {
163
- let currentModel = null;
164
- try {
165
- const configPath = path.join(os.homedir(), '.codex', 'config.toml');
166
- const content = await fs.readFile(configPath, 'utf8');
167
- const match = content.match(/^model\s*=\s*["']([^"']+)["']/m);
168
- if (match) currentModel = match[1];
169
- } catch {
170
- }
171
- return { models: mapModelOptions(CODEX_MODELS.OPTIONS, 'runtime-fallback'), source: 'runtime-fallback', error: null, currentModel: currentModel || null };
160
+ return { models: [], source: 'acp-only', error: null, currentModel: null };
172
161
  }
173
162
 
174
163
  async function discoverGeminiModels() {
175
164
  return { models: mapModelOptions(GEMINI_MODELS.OPTIONS, 'runtime-fallback'), source: 'runtime-fallback', error: null, currentModel: null };
176
165
  }
177
166
 
178
- const AUTH_CHECKERS = {
179
- claude: checkClaudeCredentials, codex: checkCodexCredentials,
180
- gemini: checkGeminiCredentials,
181
- opencode: checkOpencodeCredentials
182
- };
183
-
184
167
  const MODEL_DISCOVERY = {
185
168
  claude: discoverClaudeModels, codex: discoverCodexModels,
186
169
  gemini: discoverGeminiModels,
187
170
  opencode: discoverOpenCodeModels
188
171
  };
189
172
 
173
+ function createModelDiscoveryCacheKey(provider, projectPath) {
174
+ const normalizedProvider = String(provider || '').trim().toLowerCase();
175
+ const normalizedProjectPath = String(projectPath || '').trim();
176
+ return `${normalizedProvider}:${normalizedProjectPath || 'global'}`;
177
+ }
178
+
179
+ function cloneJsonValue(value) {
180
+ if (value === undefined) {
181
+ return undefined;
182
+ }
183
+
184
+ return JSON.parse(JSON.stringify(value));
185
+ }
186
+
187
+ async function discoverProviderModels(provider, projectPath, options = {}) {
188
+ const cacheKey = createModelDiscoveryCacheKey(provider, projectPath);
189
+ const now = Date.now();
190
+ const force = options.force === true || String(options.force || '').toLowerCase() === 'true';
191
+
192
+ if (!force) {
193
+ const cached = modelDiscoveryCache.get(cacheKey);
194
+ if (cached && cached.expiresAt > now) {
195
+ return cloneJsonValue(cached.result);
196
+ }
197
+
198
+ if (modelDiscoveryInFlight.has(cacheKey)) {
199
+ return modelDiscoveryInFlight.get(cacheKey);
200
+ }
201
+ }
202
+
203
+ const discoveryPromise = MODEL_DISCOVERY[provider](projectPath)
204
+ .then((result) => {
205
+ const normalizedResult = result && typeof result === 'object'
206
+ ? result
207
+ : { models: [], source: 'unavailable', error: 'Model discovery failed' };
208
+
209
+ modelDiscoveryCache.set(cacheKey, {
210
+ result: cloneJsonValue(normalizedResult),
211
+ expiresAt: Date.now() + MODEL_DISCOVERY_CACHE_TTL_MS
212
+ });
213
+
214
+ return normalizedResult;
215
+ })
216
+ .finally(() => {
217
+ modelDiscoveryInFlight.delete(cacheKey);
218
+ });
219
+
220
+ modelDiscoveryInFlight.set(cacheKey, discoveryPromise);
221
+ return discoveryPromise;
222
+ }
223
+
190
224
  export async function discoverProvider(provider, options = {}) {
191
225
  const normalizedProvider = String(provider || '').toLowerCase();
192
226
  if (!SUPPORTED_PROVIDERS.includes(normalizedProvider)) {
193
227
  throw new Error(`Unsupported provider: ${provider}`);
194
228
  }
195
229
 
196
- const [installation, auth] = await Promise.all([
197
- detectProviderInstallationStatus(normalizedProvider),
198
- AUTH_CHECKERS[normalizedProvider]().catch((error) => ({ authenticated: false, email: null, error: error.message }))
199
- ]);
230
+ const installation = await detectProviderInstallationStatus(normalizedProvider);
200
231
 
201
232
  const modelsResult = installation.installed
202
- ? await MODEL_DISCOVERY[normalizedProvider](options.projectPath)
233
+ ? await discoverProviderModels(normalizedProvider, options.projectPath, options)
203
234
  : { models: [], source: 'unavailable', error: installation.reason || 'Provider not installed' };
204
235
 
205
- let availability = 'unavailable';
206
- if (installation.installed && auth.authenticated) availability = 'available';
207
- else if (installation.installed && !auth.authenticated) availability = 'unauthenticated';
208
-
209
236
  return {
210
237
  provider: normalizedProvider,
211
- availability,
238
+ availability: installation.installed ? 'available' : 'unavailable',
212
239
  installed: !!installation.installed,
213
- authenticated: !!auth.authenticated,
214
- auth,
215
240
  installation,
216
241
  capabilities: PROVIDER_CAPABILITIES[normalizedProvider],
217
242
  models: modelsResult.models,
218
243
  modelSource: modelsResult.source,
219
244
  discoveryError: modelsResult.error || null,
220
- currentModel: modelsResult.currentModel || modelsResult.models?.[0]?.id || null
245
+ currentModel: modelsResult.currentModel || null
221
246
  };
222
247
  }
223
248
 
@@ -227,8 +252,6 @@ export async function discoverAllProviders(options = {}) {
227
252
  provider,
228
253
  availability: 'probe_failed',
229
254
  installed: false,
230
- authenticated: false,
231
- auth: { authenticated: false, email: null, error: error.message },
232
255
  installation: null,
233
256
  capabilities: PROVIDER_CAPABILITIES[provider],
234
257
  models: [],
@@ -238,3 +261,8 @@ export async function discoverAllProviders(options = {}) {
238
261
  }))
239
262
  ));
240
263
  }
264
+
265
+ export function resetProviderDiscoveryCachesForTests() {
266
+ modelDiscoveryCache.clear();
267
+ modelDiscoveryInFlight.clear();
268
+ }
@@ -0,0 +1,244 @@
1
+ import crypto from 'node:crypto';
2
+
3
+ export const RUN_ACTIVE_STATUSES = new Set([
4
+ 'queued',
5
+ 'running',
6
+ 'awaiting_approval',
7
+ 'awaiting_input',
8
+ ]);
9
+
10
+ export const RUN_TERMINAL_STATUSES = new Set([
11
+ 'succeeded',
12
+ 'failed',
13
+ 'canceled',
14
+ ]);
15
+
16
+ const RUN_STATUSES = new Set([
17
+ ...RUN_ACTIVE_STATUSES,
18
+ ...RUN_TERMINAL_STATUSES,
19
+ ]);
20
+
21
+ const DEFAULT_TERMINAL_TTL_MS = 30 * 60 * 1000;
22
+ const SUPPORTED_PROVIDERS = new Set(['claude', 'codex', 'gemini', 'opencode']);
23
+
24
+ function normalizeProvider(provider) {
25
+ const normalized = String(provider || '').trim().toLowerCase();
26
+ return SUPPORTED_PROVIDERS.has(normalized) ? normalized : 'claude';
27
+ }
28
+
29
+ function normalizeNullableString(value) {
30
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
31
+ }
32
+
33
+ function createRunId() {
34
+ if (typeof crypto.randomUUID === 'function') {
35
+ return `run_${crypto.randomUUID()}`;
36
+ }
37
+ return `run_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
38
+ }
39
+
40
+ function clone(value) {
41
+ return value && typeof value === 'object'
42
+ ? structuredClone(value)
43
+ : value;
44
+ }
45
+
46
+ export function createRunRegistry({
47
+ now = () => new Date().toISOString(),
48
+ ttlMs = DEFAULT_TERMINAL_TTL_MS,
49
+ } = {}) {
50
+ const runs = new Map();
51
+ const eventsByRunId = new Map();
52
+ const terminalExpiresAtByRunId = new Map();
53
+ const subscribersByRunId = new Map();
54
+
55
+ const currentTimestamp = () => now();
56
+ const currentTimeMs = () => Date.parse(currentTimestamp());
57
+
58
+ const pruneExpiredRun = (runId) => {
59
+ const expiresAt = terminalExpiresAtByRunId.get(runId);
60
+ if (!Number.isFinite(expiresAt)) {
61
+ return false;
62
+ }
63
+
64
+ if (currentTimeMs() <= expiresAt) {
65
+ return false;
66
+ }
67
+
68
+ runs.delete(runId);
69
+ eventsByRunId.delete(runId);
70
+ terminalExpiresAtByRunId.delete(runId);
71
+ subscribersByRunId.delete(runId);
72
+ return true;
73
+ };
74
+
75
+ const notifyRunEventSubscribers = (event) => {
76
+ const subscribers = subscribersByRunId.get(event.runId);
77
+ if (!subscribers || subscribers.size === 0) {
78
+ return;
79
+ }
80
+
81
+ subscribers.forEach((subscriber) => {
82
+ try {
83
+ subscriber(clone(event));
84
+ } catch (error) {
85
+ console.warn('[WARN] Run event subscriber failed:', error?.message || error);
86
+ }
87
+ });
88
+ };
89
+
90
+ const getMutableRun = (runId) => {
91
+ const normalizedRunId = normalizeNullableString(runId);
92
+ if (!normalizedRunId || pruneExpiredRun(normalizedRunId)) {
93
+ return null;
94
+ }
95
+ return runs.get(normalizedRunId) || null;
96
+ };
97
+
98
+ const rememberTerminalExpiry = (run) => {
99
+ if (RUN_TERMINAL_STATUSES.has(run.status)) {
100
+ terminalExpiresAtByRunId.set(run.runId, currentTimeMs() + ttlMs);
101
+ }
102
+ };
103
+
104
+ return {
105
+ createRun({ provider, sessionId = null, clientRequestId = null } = {}) {
106
+ const timestamp = currentTimestamp();
107
+ const run = {
108
+ runId: createRunId(),
109
+ provider: normalizeProvider(provider),
110
+ sessionId: normalizeNullableString(sessionId),
111
+ clientRequestId: normalizeNullableString(clientRequestId),
112
+ status: 'queued',
113
+ createdAt: timestamp,
114
+ updatedAt: timestamp,
115
+ lastEventSeq: 0,
116
+ };
117
+ runs.set(run.runId, run);
118
+ eventsByRunId.set(run.runId, []);
119
+ return clone(run);
120
+ },
121
+
122
+ getRun(runId) {
123
+ const run = getMutableRun(runId);
124
+ return run ? clone(run) : null;
125
+ },
126
+
127
+ updateRun(runId, patch = {}) {
128
+ const run = getMutableRun(runId);
129
+ if (!run) {
130
+ return null;
131
+ }
132
+
133
+ if (patch.sessionId !== undefined) {
134
+ run.sessionId = normalizeNullableString(patch.sessionId);
135
+ }
136
+
137
+ if (patch.clientRequestId !== undefined) {
138
+ run.clientRequestId = normalizeNullableString(patch.clientRequestId);
139
+ }
140
+
141
+ if (patch.status !== undefined) {
142
+ const nextStatus = String(patch.status || '').trim();
143
+ if (RUN_STATUSES.has(nextStatus) && !RUN_TERMINAL_STATUSES.has(run.status)) {
144
+ run.status = nextStatus;
145
+ rememberTerminalExpiry(run);
146
+ }
147
+ }
148
+
149
+ run.updatedAt = currentTimestamp();
150
+ return clone(run);
151
+ },
152
+
153
+ appendRunEvent(runId, eventInput = {}) {
154
+ const run = getMutableRun(runId);
155
+ if (!run) {
156
+ return null;
157
+ }
158
+
159
+ const nextSeq = run.lastEventSeq + 1;
160
+ const event = {
161
+ runId: run.runId,
162
+ seq: nextSeq,
163
+ type: eventInput.type || 'conversation-event',
164
+ provider: String(eventInput.provider || run.provider || '').trim() || run.provider,
165
+ sessionId: normalizeNullableString(eventInput.sessionId) || run.sessionId,
166
+ clientRequestId: normalizeNullableString(eventInput.clientRequestId) || run.clientRequestId,
167
+ event: clone(eventInput.event ?? null),
168
+ createdAt: currentTimestamp(),
169
+ };
170
+
171
+ run.lastEventSeq = nextSeq;
172
+ run.updatedAt = event.createdAt;
173
+ if (event.sessionId && !run.sessionId) {
174
+ run.sessionId = event.sessionId;
175
+ }
176
+ if (event.clientRequestId && !run.clientRequestId) {
177
+ run.clientRequestId = event.clientRequestId;
178
+ }
179
+
180
+ eventsByRunId.get(run.runId).push(event);
181
+ notifyRunEventSubscribers(event);
182
+ return clone(event);
183
+ },
184
+
185
+ listRunEvents(runId, { after = 0 } = {}) {
186
+ const run = getMutableRun(runId);
187
+ if (!run) {
188
+ return [];
189
+ }
190
+
191
+ const minSeq = Number.isFinite(Number(after)) ? Number(after) : 0;
192
+ return (eventsByRunId.get(run.runId) || [])
193
+ .filter((event) => event.seq > minSeq)
194
+ .map(clone);
195
+ },
196
+
197
+ listSessionRuns({ provider, sessionId, status = null } = {}) {
198
+ const normalizedProvider = normalizeProvider(provider);
199
+ const normalizedSessionId = normalizeNullableString(sessionId);
200
+ if (!normalizedSessionId) {
201
+ return [];
202
+ }
203
+
204
+ return Array.from(runs.values())
205
+ .filter((run) => {
206
+ if (pruneExpiredRun(run.runId)) {
207
+ return false;
208
+ }
209
+ if (run.provider !== normalizedProvider || run.sessionId !== normalizedSessionId) {
210
+ return false;
211
+ }
212
+ if (status === 'active') {
213
+ return RUN_ACTIVE_STATUSES.has(run.status);
214
+ }
215
+ return true;
216
+ })
217
+ .sort((left, right) => Date.parse(left.createdAt) - Date.parse(right.createdAt))
218
+ .map(clone);
219
+ },
220
+
221
+ subscribeRunEvents(runId, callback) {
222
+ const run = getMutableRun(runId);
223
+ if (!run || typeof callback !== 'function') {
224
+ return () => {};
225
+ }
226
+
227
+ if (!subscribersByRunId.has(run.runId)) {
228
+ subscribersByRunId.set(run.runId, new Set());
229
+ }
230
+
231
+ const subscribers = subscribersByRunId.get(run.runId);
232
+ subscribers.add(callback);
233
+
234
+ return () => {
235
+ subscribers.delete(callback);
236
+ if (subscribers.size === 0) {
237
+ subscribersByRunId.delete(run.runId);
238
+ }
239
+ };
240
+ },
241
+ };
242
+ }
243
+
244
+ export const runRegistry = createRunRegistry();
@@ -15,6 +15,7 @@ export const AGENT_RUNTIME_PHASES = {
15
15
  QUEUED: 'queued',
16
16
  STREAMING: 'streaming',
17
17
  AWAITING_APPROVAL: 'awaiting_approval',
18
+ AWAITING_INPUT: 'awaiting_input',
18
19
  COMPLETED: 'completed',
19
20
  ABORTED: 'aborted',
20
21
  ERRORED: 'errored'
@@ -23,7 +24,8 @@ export const AGENT_RUNTIME_PHASES = {
23
24
  const ACTIVE_PHASES = new Set([
24
25
  AGENT_RUNTIME_PHASES.QUEUED,
25
26
  AGENT_RUNTIME_PHASES.STREAMING,
26
- AGENT_RUNTIME_PHASES.AWAITING_APPROVAL
27
+ AGENT_RUNTIME_PHASES.AWAITING_APPROVAL,
28
+ AGENT_RUNTIME_PHASES.AWAITING_INPUT
27
29
  ]);
28
30
  const TERMINAL_PHASES = new Set([
29
31
  AGENT_RUNTIME_PHASES.COMPLETED,
@@ -188,11 +190,21 @@ function inferPhaseFromEvents(events = []) {
188
190
  continue;
189
191
  }
190
192
 
193
+ if (event.kind === CONVERSATION_EVENT_KINDS.ELICITATION_REQUEST) {
194
+ inferredPhase = AGENT_RUNTIME_PHASES.AWAITING_INPUT;
195
+ continue;
196
+ }
197
+
191
198
  if (event.kind === CONVERSATION_EVENT_KINDS.APPROVAL_RESOLVED) {
192
199
  inferredPhase = AGENT_RUNTIME_PHASES.STREAMING;
193
200
  continue;
194
201
  }
195
202
 
203
+ if (event.kind === CONVERSATION_EVENT_KINDS.ELICITATION_RESOLVED) {
204
+ inferredPhase = AGENT_RUNTIME_PHASES.STREAMING;
205
+ continue;
206
+ }
207
+
196
208
  if (
197
209
  event.kind === CONVERSATION_EVENT_KINDS.ASSISTANT_TEXT_START ||
198
210
  event.kind === CONVERSATION_EVENT_KINDS.ASSISTANT_TEXT_DELTA ||
@@ -252,6 +264,46 @@ function inferPendingApproval(events = []) {
252
264
  return pendingRequests.size > 0;
253
265
  }
254
266
 
267
+ function inferPendingElicitation(events = []) {
268
+ const pendingRequests = new Set();
269
+
270
+ for (const event of events) {
271
+ if (!event || typeof event !== 'object') {
272
+ continue;
273
+ }
274
+
275
+ const requestId = normalizeNonEmptyString(event.payload?.requestId) || normalizeNonEmptyString(event.extensions?.requestId);
276
+
277
+ if (event.kind === CONVERSATION_EVENT_KINDS.ELICITATION_REQUEST) {
278
+ if (requestId) {
279
+ pendingRequests.add(requestId);
280
+ }
281
+ continue;
282
+ }
283
+
284
+ if (event.kind === CONVERSATION_EVENT_KINDS.ELICITATION_RESOLVED) {
285
+ if (requestId) {
286
+ pendingRequests.delete(requestId);
287
+ }
288
+ continue;
289
+ }
290
+
291
+ if (event.kind === CONVERSATION_EVENT_KINDS.SESSION_STATE_CHANGED) {
292
+ const sessionState = normalizePhase(event.payload?.state);
293
+ if (
294
+ sessionState === AGENT_RUNTIME_PHASES.IDLE ||
295
+ sessionState === AGENT_RUNTIME_PHASES.COMPLETED ||
296
+ sessionState === AGENT_RUNTIME_PHASES.ABORTED ||
297
+ sessionState === AGENT_RUNTIME_PHASES.ERRORED
298
+ ) {
299
+ pendingRequests.clear();
300
+ }
301
+ }
302
+ }
303
+
304
+ return pendingRequests.size > 0;
305
+ }
306
+
255
307
  function getLatestEventTimestamp(events = []) {
256
308
  const latestEvent = [...events].reverse().find((event) => normalizeNonEmptyString(event?.timestamp));
257
309
  return latestEvent?.timestamp || null;
@@ -294,7 +346,17 @@ export async function buildSessionRuntimeSnapshot(provider, sessionId, options =
294
346
  }
295
347
 
296
348
  const recentEvents = Array.isArray(options.recentEvents) ? options.recentEvents : [];
297
- const { events: persistedEvents, context, error } = await loadProviderEvents(normalizedProvider, normalizedSessionId);
349
+ let persistedEvents = [];
350
+ let context = null;
351
+ let error = null;
352
+
353
+ if (!options.skipPersistedEvents) {
354
+ const loaded = await loadProviderEvents(normalizedProvider, normalizedSessionId);
355
+ persistedEvents = loaded.events;
356
+ context = loaded.context;
357
+ error = loaded.error;
358
+ }
359
+
298
360
  const mergedEvents = mergeConversationEventHistories(
299
361
  persistedEvents,
300
362
  recentEvents.filter((event) => event?.provider === normalizedProvider && event?.sessionId === normalizedSessionId)
@@ -308,11 +370,14 @@ export async function buildSessionRuntimeSnapshot(provider, sessionId, options =
308
370
 
309
371
  let phase = inferPhaseFromEvents(mergedEvents);
310
372
  const hasPendingApproval = active ? inferPendingApproval(mergedEvents) : false;
373
+ const hasPendingElicitation = active ? inferPendingElicitation(mergedEvents) : false;
311
374
 
312
375
  if (TERMINAL_PHASES.has(phase)) {
313
376
  // Keep terminal phase as-is.
314
377
  } else if (hasPendingApproval) {
315
378
  phase = AGENT_RUNTIME_PHASES.AWAITING_APPROVAL;
379
+ } else if (hasPendingElicitation) {
380
+ phase = AGENT_RUNTIME_PHASES.AWAITING_INPUT;
316
381
  } else if (active) {
317
382
  phase = ACTIVE_PHASES.has(phase) ? phase : AGENT_RUNTIME_PHASES.STREAMING;
318
383
  } else {
@@ -346,6 +411,10 @@ export function subscribeToSessionRuntimeStateChanges(listener) {
346
411
  }
347
412
 
348
413
  export async function publishSessionRuntimeStateChanges(events = []) {
414
+ if (sessionRuntimeListeners.size === 0) {
415
+ return;
416
+ }
417
+
349
418
  const groupedRecentEvents = new Map();
350
419
 
351
420
  events.forEach((event) => {
@@ -363,7 +432,10 @@ export async function publishSessionRuntimeStateChanges(events = []) {
363
432
 
364
433
  await Promise.all(Array.from(groupedRecentEvents.entries()).map(async ([subscriptionKey, recentEvents]) => {
365
434
  const [provider, sessionId] = subscriptionKey.split(':');
366
- const snapshot = await buildSessionRuntimeSnapshot(provider, sessionId, { recentEvents });
435
+ const snapshot = await buildSessionRuntimeSnapshot(provider, sessionId, {
436
+ recentEvents,
437
+ skipPersistedEvents: true
438
+ });
367
439
  if (!snapshot) {
368
440
  return;
369
441
  }