@amodalai/runtime 0.2.0 → 0.2.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.
Files changed (209) hide show
  1. package/dist/src/__fixtures__/README.md +4 -0
  2. package/dist/src/__fixtures__/e2e.test.d.ts +6 -0
  3. package/dist/src/__fixtures__/e2e.test.js +211 -0
  4. package/dist/src/__fixtures__/e2e.test.js.map +1 -0
  5. package/dist/src/__fixtures__/smoke-agent/automations/delivery-callback-test.json +9 -0
  6. package/dist/src/__fixtures__/smoke-agent/connections/mock-mcp/spec.json +1 -1
  7. package/dist/src/__fixtures__/smoke.test.js +715 -29
  8. package/dist/src/__fixtures__/smoke.test.js.map +1 -1
  9. package/dist/src/__fixtures__/test-env.d.ts +27 -0
  10. package/dist/src/__fixtures__/test-env.js +64 -0
  11. package/dist/src/__fixtures__/test-env.js.map +1 -0
  12. package/dist/src/__fixtures__/test-helpers.d.ts +30 -0
  13. package/dist/src/__fixtures__/test-helpers.js +120 -0
  14. package/dist/src/__fixtures__/test-helpers.js.map +1 -0
  15. package/dist/src/agent/agent-types.d.ts +22 -0
  16. package/dist/src/agent/agent-types.js.map +1 -1
  17. package/dist/src/agent/automation-bridge.d.ts +9 -0
  18. package/dist/src/agent/automation-bridge.js +26 -0
  19. package/dist/src/agent/automation-bridge.js.map +1 -1
  20. package/dist/src/agent/automation-bridge.test.js +63 -0
  21. package/dist/src/agent/automation-bridge.test.js.map +1 -1
  22. package/dist/src/agent/local-server.d.ts +0 -7
  23. package/dist/src/agent/local-server.js +230 -86
  24. package/dist/src/agent/local-server.js.map +1 -1
  25. package/dist/src/agent/local-server.test.js +14 -8
  26. package/dist/src/agent/local-server.test.js.map +1 -1
  27. package/dist/src/agent/loop-types.d.ts +81 -2
  28. package/dist/src/agent/loop-types.js +4 -0
  29. package/dist/src/agent/loop-types.js.map +1 -1
  30. package/dist/src/agent/loop.js +16 -3
  31. package/dist/src/agent/loop.js.map +1 -1
  32. package/dist/src/agent/loop.test.js +572 -8
  33. package/dist/src/agent/loop.test.js.map +1 -1
  34. package/dist/src/agent/proactive/delivery-router.d.ts +68 -0
  35. package/dist/src/agent/proactive/delivery-router.js +337 -0
  36. package/dist/src/agent/proactive/delivery-router.js.map +1 -0
  37. package/dist/src/agent/proactive/delivery-router.test.d.ts +6 -0
  38. package/dist/src/agent/proactive/delivery-router.test.js +455 -0
  39. package/dist/src/agent/proactive/delivery-router.test.js.map +1 -0
  40. package/dist/src/agent/proactive/proactive-runner.d.ts +23 -1
  41. package/dist/src/agent/proactive/proactive-runner.js +42 -10
  42. package/dist/src/agent/proactive/proactive-runner.js.map +1 -1
  43. package/dist/src/agent/proactive/proactive-runner.test.js +0 -2
  44. package/dist/src/agent/proactive/proactive-runner.test.js.map +1 -1
  45. package/dist/src/agent/routes/admin-chat-abort.test.d.ts +6 -0
  46. package/dist/src/agent/routes/admin-chat-abort.test.js +206 -0
  47. package/dist/src/agent/routes/admin-chat-abort.test.js.map +1 -0
  48. package/dist/src/agent/routes/admin-chat.js +0 -2
  49. package/dist/src/agent/routes/admin-chat.js.map +1 -1
  50. package/dist/src/agent/routes/task.test.js +0 -2
  51. package/dist/src/agent/routes/task.test.js.map +1 -1
  52. package/dist/src/agent/snapshot-server.js +0 -2
  53. package/dist/src/agent/snapshot-server.js.map +1 -1
  54. package/dist/src/agent/states/compacting.js +5 -3
  55. package/dist/src/agent/states/compacting.js.map +1 -1
  56. package/dist/src/agent/states/confirming.js +3 -0
  57. package/dist/src/agent/states/confirming.js.map +1 -1
  58. package/dist/src/agent/states/dispatching.js +45 -1
  59. package/dist/src/agent/states/dispatching.js.map +1 -1
  60. package/dist/src/agent/states/executing.js +225 -81
  61. package/dist/src/agent/states/executing.js.map +1 -1
  62. package/dist/src/agent/states/streaming.js +14 -0
  63. package/dist/src/agent/states/streaming.js.map +1 -1
  64. package/dist/src/agent/states/thinking.d.ts +1 -1
  65. package/dist/src/agent/states/thinking.js +246 -29
  66. package/dist/src/agent/states/thinking.js.map +1 -1
  67. package/dist/src/agent/token-estimate.d.ts +20 -6
  68. package/dist/src/agent/token-estimate.js +24 -3
  69. package/dist/src/agent/token-estimate.js.map +1 -1
  70. package/dist/src/agent/token-estimate.test.d.ts +6 -0
  71. package/dist/src/agent/token-estimate.test.js +44 -0
  72. package/dist/src/agent/token-estimate.test.js.map +1 -0
  73. package/dist/src/api/create-agent.js +0 -3
  74. package/dist/src/api/create-agent.js.map +1 -1
  75. package/dist/src/api/types.d.ts +0 -2
  76. package/dist/src/env-ref.d.ts +13 -0
  77. package/dist/src/env-ref.js +31 -0
  78. package/dist/src/env-ref.js.map +1 -0
  79. package/dist/src/env-ref.test.d.ts +6 -0
  80. package/dist/src/env-ref.test.js +34 -0
  81. package/dist/src/env-ref.test.js.map +1 -0
  82. package/dist/src/errors.d.ts +15 -0
  83. package/dist/src/errors.js +22 -0
  84. package/dist/src/errors.js.map +1 -1
  85. package/dist/src/errors.test.js +2 -2
  86. package/dist/src/errors.test.js.map +1 -1
  87. package/dist/src/events/event-bus.d.ts +54 -0
  88. package/dist/src/events/event-bus.js +84 -0
  89. package/dist/src/events/event-bus.js.map +1 -0
  90. package/dist/src/events/event-bus.test.d.ts +6 -0
  91. package/dist/src/events/event-bus.test.js +112 -0
  92. package/dist/src/events/event-bus.test.js.map +1 -0
  93. package/dist/src/events/events-route.d.ts +36 -0
  94. package/dist/src/events/events-route.js +80 -0
  95. package/dist/src/events/events-route.js.map +1 -0
  96. package/dist/src/events/events-route.test.d.ts +6 -0
  97. package/dist/src/events/events-route.test.js +134 -0
  98. package/dist/src/events/events-route.test.js.map +1 -0
  99. package/dist/src/events/store-event-wrapper.d.ts +19 -0
  100. package/dist/src/events/store-event-wrapper.js +57 -0
  101. package/dist/src/events/store-event-wrapper.js.map +1 -0
  102. package/dist/src/events/store-event-wrapper.test.d.ts +6 -0
  103. package/dist/src/events/store-event-wrapper.test.js +91 -0
  104. package/dist/src/events/store-event-wrapper.test.js.map +1 -0
  105. package/dist/src/middleware/auth.d.ts +0 -2
  106. package/dist/src/middleware/auth.js.map +1 -1
  107. package/dist/src/providers/search-provider.d.ts +64 -0
  108. package/dist/src/providers/search-provider.js +174 -0
  109. package/dist/src/providers/search-provider.js.map +1 -0
  110. package/dist/src/providers/types.d.ts +8 -0
  111. package/dist/src/routes/ai-stream.d.ts +15 -0
  112. package/dist/src/routes/ai-stream.js +9 -0
  113. package/dist/src/routes/ai-stream.js.map +1 -1
  114. package/dist/src/routes/chat-stream.d.ts +6 -0
  115. package/dist/src/routes/chat-stream.js +2 -0
  116. package/dist/src/routes/chat-stream.js.map +1 -1
  117. package/dist/src/routes/chat.d.ts +6 -0
  118. package/dist/src/routes/chat.js +2 -0
  119. package/dist/src/routes/chat.js.map +1 -1
  120. package/dist/src/routes/session-resolver.d.ts +5 -0
  121. package/dist/src/routes/session-resolver.js +1 -15
  122. package/dist/src/routes/session-resolver.js.map +1 -1
  123. package/dist/src/routes/session-resolver.test.js +7 -6
  124. package/dist/src/routes/session-resolver.test.js.map +1 -1
  125. package/dist/src/server.d.ts +6 -0
  126. package/dist/src/server.js +2 -0
  127. package/dist/src/server.js.map +1 -1
  128. package/dist/src/session/drizzle-session-store.d.ts +56 -0
  129. package/dist/src/session/drizzle-session-store.js +203 -0
  130. package/dist/src/session/drizzle-session-store.js.map +1 -0
  131. package/dist/src/session/manager.d.ts +6 -3
  132. package/dist/src/session/manager.js +46 -16
  133. package/dist/src/session/manager.js.map +1 -1
  134. package/dist/src/session/manager.test.js +12 -18
  135. package/dist/src/session/manager.test.js.map +1 -1
  136. package/dist/src/session/pglite-session-store.d.ts +23 -0
  137. package/dist/src/session/pglite-session-store.js +70 -0
  138. package/dist/src/session/pglite-session-store.js.map +1 -0
  139. package/dist/src/session/postgres-session-store.d.ts +44 -0
  140. package/dist/src/session/postgres-session-store.js +138 -0
  141. package/dist/src/session/postgres-session-store.js.map +1 -0
  142. package/dist/src/session/session-builder.d.ts +0 -2
  143. package/dist/src/session/session-builder.js +22 -2
  144. package/dist/src/session/session-builder.js.map +1 -1
  145. package/dist/src/session/session-builder.test.js +0 -2
  146. package/dist/src/session/session-builder.test.js.map +1 -1
  147. package/dist/src/session/session-store-selector.d.ts +49 -0
  148. package/dist/src/session/session-store-selector.js +60 -0
  149. package/dist/src/session/session-store-selector.js.map +1 -0
  150. package/dist/src/session/session-store-selector.test.d.ts +6 -0
  151. package/dist/src/session/session-store-selector.test.js +79 -0
  152. package/dist/src/session/session-store-selector.test.js.map +1 -0
  153. package/dist/src/session/store.d.ts +146 -32
  154. package/dist/src/session/store.js +126 -138
  155. package/dist/src/session/store.js.map +1 -1
  156. package/dist/src/session/store.test.js +385 -107
  157. package/dist/src/session/store.test.js.map +1 -1
  158. package/dist/src/session/tool-context-factory.d.ts +3 -2
  159. package/dist/src/session/tool-context-factory.js +1 -2
  160. package/dist/src/session/tool-context-factory.js.map +1 -1
  161. package/dist/src/session/tool-context-factory.test.js +1 -4
  162. package/dist/src/session/tool-context-factory.test.js.map +1 -1
  163. package/dist/src/session/types.d.ts +13 -6
  164. package/dist/src/stores/schema.d.ts +0 -34
  165. package/dist/src/stores/schema.js +6 -4
  166. package/dist/src/stores/schema.js.map +1 -1
  167. package/dist/src/tools/admin-file-tools.d.ts +29 -0
  168. package/dist/src/tools/admin-file-tools.js +525 -11
  169. package/dist/src/tools/admin-file-tools.js.map +1 -1
  170. package/dist/src/tools/admin-file-tools.test.js +373 -4
  171. package/dist/src/tools/admin-file-tools.test.js.map +1 -1
  172. package/dist/src/tools/custom-tool-adapter.test.js +0 -1
  173. package/dist/src/tools/custom-tool-adapter.test.js.map +1 -1
  174. package/dist/src/tools/dispatch-tool.d.ts +4 -4
  175. package/dist/src/tools/fetch-url-tool.d.ts +23 -0
  176. package/dist/src/tools/fetch-url-tool.js +333 -0
  177. package/dist/src/tools/fetch-url-tool.js.map +1 -0
  178. package/dist/src/tools/fetch-url-tool.test.d.ts +6 -0
  179. package/dist/src/tools/fetch-url-tool.test.js +228 -0
  180. package/dist/src/tools/fetch-url-tool.test.js.map +1 -0
  181. package/dist/src/tools/mcp-tool-adapter.test.js +0 -1
  182. package/dist/src/tools/mcp-tool-adapter.test.js.map +1 -1
  183. package/dist/src/tools/registry.test.js +0 -1
  184. package/dist/src/tools/registry.test.js.map +1 -1
  185. package/dist/src/tools/request-tool.test.js +0 -1
  186. package/dist/src/tools/request-tool.test.js.map +1 -1
  187. package/dist/src/tools/store-tools.test.js +0 -1
  188. package/dist/src/tools/store-tools.test.js.map +1 -1
  189. package/dist/src/tools/types.d.ts +20 -2
  190. package/dist/src/tools/web-search-tool.d.ts +31 -0
  191. package/dist/src/tools/web-search-tool.js +170 -0
  192. package/dist/src/tools/web-search-tool.js.map +1 -0
  193. package/dist/src/tools/web-search-tool.test.d.ts +6 -0
  194. package/dist/src/tools/web-search-tool.test.js +153 -0
  195. package/dist/src/tools/web-search-tool.test.js.map +1 -0
  196. package/dist/src/tools/web-tools-shared.d.ts +21 -0
  197. package/dist/src/tools/web-tools-shared.js +32 -0
  198. package/dist/src/tools/web-tools-shared.js.map +1 -0
  199. package/dist/src/types.d.ts +20 -0
  200. package/dist/src/types.js +13 -0
  201. package/dist/src/types.js.map +1 -1
  202. package/dist/tsconfig.tsbuildinfo +1 -1
  203. package/package.json +17 -3
  204. package/dist/src/agent/session-store.d.ts +0 -71
  205. package/dist/src/agent/session-store.js +0 -151
  206. package/dist/src/agent/session-store.js.map +0 -1
  207. package/dist/src/session/admin-file-tools.d.ts +0 -136
  208. package/dist/src/session/admin-file-tools.js +0 -240
  209. package/dist/src/session/admin-file-tools.js.map +0 -1
@@ -47,6 +47,69 @@ describe('bridgeAutomation', () => {
47
47
  expect(result.prompt).toBe(prompt);
48
48
  });
49
49
  });
50
+ describe('bridgeAutomation env:NAME resolution', () => {
51
+ it('resolves env:NAME refs in delivery webhook URLs', () => {
52
+ process.env['TEST_WEBHOOK_URL'] = 'https://hooks.example.com/abc123';
53
+ try {
54
+ const result = bridgeAutomation(makeAutomation({
55
+ delivery: {
56
+ targets: [{ type: 'webhook', url: 'env:TEST_WEBHOOK_URL' }],
57
+ },
58
+ }));
59
+ expect(result.delivery?.targets[0]).toEqual({
60
+ type: 'webhook',
61
+ url: 'https://hooks.example.com/abc123',
62
+ });
63
+ }
64
+ finally {
65
+ delete process.env['TEST_WEBHOOK_URL'];
66
+ }
67
+ });
68
+ it('resolves env:NAME refs in failureAlert webhook URLs', () => {
69
+ process.env['TEST_ALERT_URL'] = 'https://alerts.example.com/oncall';
70
+ try {
71
+ const result = bridgeAutomation(makeAutomation({
72
+ failureAlert: {
73
+ targets: [{ type: 'webhook', url: 'env:TEST_ALERT_URL' }],
74
+ },
75
+ }));
76
+ expect(result.failureAlert?.targets[0]).toEqual({
77
+ type: 'webhook',
78
+ url: 'https://alerts.example.com/oncall',
79
+ });
80
+ }
81
+ finally {
82
+ delete process.env['TEST_ALERT_URL'];
83
+ }
84
+ });
85
+ it('leaves callback targets unchanged', () => {
86
+ const result = bridgeAutomation(makeAutomation({
87
+ delivery: {
88
+ targets: [{ type: 'callback', name: 'my-handler' }],
89
+ },
90
+ }));
91
+ expect(result.delivery?.targets[0]).toEqual({ type: 'callback', name: 'my-handler' });
92
+ });
93
+ it('leaves literal http URLs unchanged', () => {
94
+ const result = bridgeAutomation(makeAutomation({
95
+ delivery: {
96
+ targets: [{ type: 'webhook', url: 'https://hooks.example.com/fixed' }],
97
+ },
98
+ }));
99
+ expect(result.delivery?.targets[0]).toEqual({
100
+ type: 'webhook',
101
+ url: 'https://hooks.example.com/fixed',
102
+ });
103
+ });
104
+ it('throws at bridge time if env var is missing (fail fast at boot)', () => {
105
+ delete process.env['NONEXISTENT_WEBHOOK'];
106
+ expect(() => bridgeAutomation(makeAutomation({
107
+ delivery: {
108
+ targets: [{ type: 'webhook', url: 'env:NONEXISTENT_WEBHOOK' }],
109
+ },
110
+ }))).toThrow(/NONEXISTENT_WEBHOOK/);
111
+ });
112
+ });
50
113
  describe('bridgeAutomations', () => {
51
114
  it('converts an array of automations', () => {
52
115
  const automations = [
@@ -1 +1 @@
1
- {"version":3,"file":"automation-bridge.test.js","sourceRoot":"","sources":["../../../src/agent/automation-bridge.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"automation-bridge.test.js","sourceRoot":"","sources":["../../../src/agent/automation-bridge.test.ts"],"names":[],"mappings":"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"}
@@ -5,11 +5,4 @@
5
5
  */
6
6
  import type { LocalServerConfig } from './agent-types.js';
7
7
  import type { ServerInstance } from '../server.js';
8
- /**
9
- * Creates an Express server for repo-based agent mode.
10
- *
11
- * Loads the `.amodal/` config from `config.repoPath`, creates a
12
- * `StandaloneSessionManager`, mounts all routes, and optionally watches
13
- * for config changes (hot reload).
14
- */
15
8
  export declare function createLocalServer(config: LocalServerConfig): Promise<ServerInstance>;
@@ -18,11 +18,15 @@ import { existsSync } from 'node:fs';
18
18
  import path from 'node:path';
19
19
  import { loadRepo } from '@amodalai/core';
20
20
  import { StandaloneSessionManager } from '../session/manager.js';
21
- import { PGLiteSessionStore } from '../session/store.js';
21
+ import { selectSessionStore } from '../session/session-store-selector.js';
22
+ import { resolveEnvRef } from '../env-ref.js';
22
23
  import { buildSessionComponents } from '../session/session-builder.js';
23
24
  import { LocalToolExecutor } from './tool-executor-local.js';
24
25
  import { buildMcpConfigs } from './mcp-config.js';
25
26
  import { ConfigWatcher } from './config-watcher.js';
27
+ import { RuntimeEventBus } from '../events/event-bus.js';
28
+ import { createEventsRouter } from '../events/events-route.js';
29
+ import { wrapStoreBackendWithEvents } from '../events/store-event-wrapper.js';
26
30
  import { ProactiveRunner } from './proactive/proactive-runner.js';
27
31
  import { createChatStreamRouter } from '../routes/chat-stream.js';
28
32
  import { createChatRouter } from '../routes/chat.js';
@@ -37,14 +41,20 @@ import { createStoresRouter } from './routes/stores.js';
37
41
  import { createFilesRouter } from './routes/files.js';
38
42
  import { createEvalRouter } from './routes/evals.js';
39
43
  import { errorHandler } from '../middleware/error-handler.js';
44
+ import { asyncHandler } from '../routes/route-helpers.js';
40
45
  import { createPGLiteStoreBackend } from '../stores/pglite-store-backend.js';
41
- import { SessionStore } from './session-store.js';
42
46
  import { EvalStore } from './eval-store.js';
43
47
  import { buildPages } from './page-builder.js';
44
48
  import { LOCAL_APP_ID } from '../constants.js';
45
49
  import { log, createLogger } from '../logger.js';
50
+ // Each check must use an endpoint that returns 200 on a valid key and
51
+ // a distinct auth-failure status (typically 401) on a bad key. Do NOT
52
+ // use endpoints with method guards that might return 405 before the
53
+ // auth check — `GET /v1/messages` on Anthropic does exactly that, and
54
+ // makes every key (good or bad) look invalid because Anthropic returns
55
+ // 405 for wrong-method regardless of whether the x-api-key is real.
46
56
  const PROVIDER_CHECKS = [
47
- { provider: 'anthropic', envVar: 'ANTHROPIC_API_KEY', url: 'https://api.anthropic.com/v1/messages', authHeader: (key) => ({ 'x-api-key': key, 'anthropic-version': '2023-06-01' }) },
57
+ { provider: 'anthropic', envVar: 'ANTHROPIC_API_KEY', url: 'https://api.anthropic.com/v1/models', authHeader: (key) => ({ 'x-api-key': key, 'anthropic-version': '2023-06-01' }) },
48
58
  { provider: 'openai', envVar: 'OPENAI_API_KEY', url: 'https://api.openai.com/v1/models', authHeader: (key) => ({ Authorization: `Bearer ${key}` }) },
49
59
  ];
50
60
  async function checkProviders() {
@@ -80,7 +90,33 @@ async function checkProviders() {
80
90
  * `StandaloneSessionManager`, mounts all routes, and optionally watches
81
91
  * for config changes (hot reload).
82
92
  */
93
+ /**
94
+ * Install a process-level unhandledRejection listener that logs instead
95
+ * of crashing. An escaped rejection is always a bug — we want loud logs,
96
+ * not silent outages. The previous behavior (default Node: print + crash)
97
+ * turned small bugs (one leaked promise) into whole-server downtime for
98
+ * every active session. Logging + continuing preserves service for all
99
+ * other sessions while still surfacing the issue to operators.
100
+ *
101
+ * Idempotent: only installs once per process (the local-server can be
102
+ * created and torn down repeatedly during tests).
103
+ */
104
+ let unhandledRejectionListenerInstalled = false;
105
+ function installUnhandledRejectionLogger() {
106
+ if (unhandledRejectionListenerInstalled)
107
+ return;
108
+ unhandledRejectionListenerInstalled = true;
109
+ process.on('unhandledRejection', (reason) => {
110
+ const err = reason instanceof Error ? reason : new Error(String(reason));
111
+ log.error('unhandled_rejection', {
112
+ name: err.name,
113
+ message: err.message,
114
+ stack: err.stack,
115
+ });
116
+ });
117
+ }
83
118
  export async function createLocalServer(config) {
119
+ installUnhandledRejectionLogger();
84
120
  let bundle = await loadRepo({ localPath: config.repoPath });
85
121
  // Check provider API keys in the background at startup
86
122
  let providerStatuses = PROVIDER_CHECKS.map((c) => ({
@@ -110,9 +146,7 @@ export async function createLocalServer(config) {
110
146
  const storeConfig = bundle.config.stores;
111
147
  const backend = storeConfig?.backend ?? 'pglite';
112
148
  if (backend === 'postgres' && storeConfig?.postgresUrl) {
113
- const connUrl = storeConfig.postgresUrl.startsWith('env:')
114
- ? process.env[storeConfig.postgresUrl.slice(4)] ?? ''
115
- : storeConfig.postgresUrl;
149
+ const connUrl = resolveEnvRef(storeConfig.postgresUrl) ?? '';
116
150
  if (!connUrl) {
117
151
  log.error('store_postgres_url_missing', { configured: storeConfig.postgresUrl });
118
152
  }
@@ -178,19 +212,41 @@ export async function createLocalServer(config) {
178
212
  }
179
213
  }
180
214
  // -------------------------------------------------------------------------
215
+ // Runtime event bus (powers /api/events SSE for live UI updates)
216
+ // -------------------------------------------------------------------------
217
+ const eventBus = new RuntimeEventBus({
218
+ onListenerError: (err, event) => {
219
+ log.warn('event_bus_listener_error', {
220
+ seq: event.seq,
221
+ type: event.type,
222
+ error: err instanceof Error ? err.message : String(err),
223
+ });
224
+ },
225
+ });
226
+ // Wrap the store backend so every write emits store_updated events.
227
+ // Covers every write path through one seam: tools, REST routes, admin
228
+ // file tools, task execution — they all go through this backend.
229
+ if (storeBackend) {
230
+ storeBackend = wrapStoreBackendWithEvents(storeBackend, eventBus);
231
+ }
232
+ // -------------------------------------------------------------------------
181
233
  // Session manager (new standalone stack)
182
234
  // -------------------------------------------------------------------------
183
235
  const sessionLogger = createLogger({ component: 'session-manager' });
184
- const sessionStore = new PGLiteSessionStore({ logger: sessionLogger });
185
- await sessionStore.initialize();
236
+ const sessionDataDir = `${config.repoPath}/.amodal/session-data`;
237
+ const sessionStore = await selectSessionStore({
238
+ backend: bundle.config.stores?.backend,
239
+ postgresUrl: resolveEnvRef(bundle.config.stores?.postgresUrl),
240
+ logger: sessionLogger,
241
+ dataDir: sessionDataDir,
242
+ });
186
243
  const sessionManager = new StandaloneSessionManager({
187
244
  logger: sessionLogger,
188
245
  store: sessionStore,
189
246
  ttlMs: config.sessionTtlMs,
247
+ eventBus,
190
248
  });
191
249
  sessionManager.start();
192
- // Legacy session store for UI history (file-based)
193
- const legacySessionStore = new SessionStore(config.repoPath);
194
250
  // -------------------------------------------------------------------------
195
251
  // MCP connections (shared across sessions)
196
252
  // -------------------------------------------------------------------------
@@ -235,8 +291,6 @@ export async function createLocalServer(config) {
235
291
  sessionType: 'automation',
236
292
  });
237
293
  const session = sessionManager.create({
238
- tenantId: 'local',
239
- userId: 'automation',
240
294
  provider: components.provider,
241
295
  toolRegistry: components.toolRegistry,
242
296
  permissionChecker: components.permissionChecker,
@@ -255,9 +309,15 @@ export async function createLocalServer(config) {
255
309
  createSessionComponents: createAutomationSessionComponents,
256
310
  logger: log,
257
311
  webhookSecret: config.webhookSecret,
258
- onSessionComplete: (session) => {
312
+ summarizeToolResult: config.summarizeToolResult,
313
+ onSessionComplete: (session, automationName) => {
314
+ // Tag the automation name onto metadata so the UI can filter
315
+ // sessions by automation via /sessions?automation=<name>.
316
+ session.metadata.automationName = automationName;
259
317
  void sessionManager.persist(session);
260
318
  },
319
+ eventBus,
320
+ onAutomationResult: config.onAutomationResult,
261
321
  });
262
322
  // -------------------------------------------------------------------------
263
323
  // Config watcher (hot reload)
@@ -327,7 +387,11 @@ export async function createLocalServer(config) {
327
387
  // Resolve resume session ID
328
388
  let resumeSessionId = config.resumeSessionId;
329
389
  if (resumeSessionId === 'latest') {
330
- resumeSessionId = legacySessionStore.latest() ?? undefined;
390
+ const { sessions: recent } = await sessionStore.list({
391
+ limit: 1,
392
+ filter: { appId: LOCAL_APP_ID },
393
+ });
394
+ resumeSessionId = recent[0]?.id;
331
395
  }
332
396
  if (resumeSessionId) {
333
397
  log.debug('resume_session', { sessionId: resumeSessionId });
@@ -336,43 +400,48 @@ export async function createLocalServer(config) {
336
400
  app.get('/config', (_req, res) => {
337
401
  res.json({ resumeSessionId: resumeSessionId ?? null });
338
402
  });
339
- // Sessions endpoints (legacy file-based store for UI history)
340
- app.get('/sessions', (req, res) => {
403
+ // Sessions endpoints served directly from the DrizzleSessionStore.
404
+ //
405
+ // Dev-UI consumers (sidebar Recent list, Sessions page, Automation detail
406
+ // page) don't paginate — they render what they get and slice the top N.
407
+ // A 500-session ceiling keeps the response bounded without forcing a
408
+ // cursor API on the client today. If dev sessions regularly exceed this,
409
+ // the store already supports cursor pagination via SessionListOptions.
410
+ const SESSION_LIST_LIMIT = 500;
411
+ app.get('/sessions', asyncHandler(async (req, res) => {
341
412
  const automationFilter = typeof req.query?.['automation'] === 'string' ? String(req.query['automation']) : undefined;
342
- const all = legacySessionStore.list();
343
- const visible = all.filter((s) => s.appId !== 'eval-runner' && s.appId !== 'admin');
344
- const filtered = automationFilter ? visible.filter((s) => s.automationName === automationFilter) : visible;
345
- res.json({ sessions: filtered });
346
- });
347
- app.get('/session/:id', (req, res) => {
348
- const persisted = legacySessionStore.load(req.params['id'] ?? '');
413
+ // Automation filter uses metadata.automationName; otherwise restrict
414
+ // to chat sessions by metadata.appId (excludes eval-runner / admin).
415
+ const filter = automationFilter
416
+ ? { automationName: automationFilter }
417
+ : { appId: LOCAL_APP_ID };
418
+ const { sessions: rows } = await sessionStore.list({ limit: SESSION_LIST_LIMIT, filter });
419
+ const sessions = rows.map((s) => {
420
+ const title = typeof s.metadata['title'] === 'string' ? s.metadata['title'] : undefined;
421
+ const appId = typeof s.metadata['appId'] === 'string' ? s.metadata['appId'] : LOCAL_APP_ID;
422
+ const automationName = typeof s.metadata['automationName'] === 'string' ? s.metadata['automationName'] : undefined;
423
+ return {
424
+ id: s.id,
425
+ appId,
426
+ title,
427
+ summary: title ?? extractFirstUserText(s.messages) ?? 'Untitled',
428
+ createdAt: s.createdAt.getTime(),
429
+ lastAccessedAt: s.updatedAt.getTime(),
430
+ automationName,
431
+ };
432
+ });
433
+ res.json({ sessions });
434
+ }));
435
+ app.get('/session/:id', asyncHandler(async (req, res) => {
436
+ const persisted = await sessionStore.load(req.params['id'] ?? '');
349
437
  if (!persisted) {
350
438
  res.status(404).json({ error: 'Session not found' });
351
439
  return;
352
440
  }
353
- const messages = persisted.conversationHistory.map((msg) => {
354
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Persisted message
355
- const m = msg;
356
- if (m['type'] === 'user') {
357
- return { role: 'user', text: String(m['text'] ?? '') };
358
- }
359
- if (m['type'] === 'assistant_text') {
360
- return { role: 'assistant', text: String(m['text'] ?? ''), toolCalls: m['toolCalls'] };
361
- }
362
- if (m['role'] === 'user') {
363
- return { role: 'user', text: typeof m['content'] === 'string' ? m['content'] : '' };
364
- }
365
- if (m['role'] === 'assistant') {
366
- const blocks = Array.isArray(m['content']) ? m['content'] : [];
367
- const isTextBlock = (b) => typeof b === 'object' && b !== null && 'type' in b && b['type'] === 'text' && 'text' in b;
368
- const text = blocks.filter(isTextBlock).map((b) => b.text).join('');
369
- return { role: 'assistant', text };
370
- }
371
- return { role: String(m['role'] ?? m['type'] ?? 'unknown'), text: String(m['text'] ?? '') };
372
- });
441
+ const messages = persisted.messages.map(flattenModelMessage).filter((m) => m !== null);
373
442
  res.json({ session_id: persisted.id, messages });
374
- });
375
- app.patch('/session/:id', express.json(), (req, res) => {
443
+ }));
444
+ app.patch('/session/:id', express.json(), asyncHandler(async (req, res) => {
376
445
  const sessionId = req.params['id'] ?? '';
377
446
  const body = req.body;
378
447
  if (!body || typeof body !== 'object' || !('title' in body) || typeof body['title'] !== 'string') {
@@ -381,72 +450,67 @@ export async function createLocalServer(config) {
381
450
  }
382
451
  // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- validated above
383
452
  const title = body['title'];
384
- const updated = legacySessionStore.updateTitle(sessionId, title);
385
- if (!updated) {
386
- res.status(404).json({ error: 'Session not found' });
387
- return;
453
+ // Live session: mutate metadata on the shared object and persist so
454
+ // the next /sessions read reflects the new title. A concurrent
455
+ // runMessage may be mid-turn, but JSON.stringify runs atomically in
456
+ // JS's single-threaded event loop — no torn writes. The next
457
+ // end-of-turn persist will overwrite with the completed messages
458
+ // array; metadata.title stays because it's on the live session.
459
+ //
460
+ // Not-live: load → mutate → save. No race possible.
461
+ const live = sessionManager.get(sessionId);
462
+ if (live) {
463
+ live.metadata.title = title;
464
+ await sessionManager.persist(live);
388
465
  }
466
+ else {
467
+ const persisted = await sessionStore.load(sessionId);
468
+ if (!persisted) {
469
+ res.status(404).json({ error: 'Session not found' });
470
+ return;
471
+ }
472
+ persisted.metadata.title = title;
473
+ persisted.updatedAt = new Date();
474
+ await sessionStore.save(persisted);
475
+ }
476
+ // Emit session_updated so the sidebar picks up the new title live.
477
+ eventBus.emit({ type: 'session_updated', sessionId, appId: LOCAL_APP_ID, title });
389
478
  res.json({ ok: true });
390
- });
391
- app.delete('/session/:id', (req, res) => {
479
+ }));
480
+ app.delete('/session/:id', asyncHandler(async (req, res) => {
392
481
  const sessionId = req.params['id'] ?? '';
393
- void sessionManager.destroy(sessionId);
394
- const deleted = legacySessionStore.delete(sessionId);
482
+ await sessionManager.destroy(sessionId);
483
+ const deleted = await sessionStore.delete(sessionId);
395
484
  if (!deleted) {
396
485
  res.status(404).json({ error: 'Session not found' });
397
486
  return;
398
487
  }
488
+ eventBus.emit({ type: 'session_deleted', sessionId });
399
489
  res.json({ ok: true });
400
- });
490
+ }));
401
491
  // File browser/editor
402
492
  app.use(createFilesRouter({ repoPath: config.repoPath }));
493
+ // Event bus SSE stream (live UI updates)
494
+ app.use(createEventsRouter({ bus: eventBus, logger: log }));
403
495
  // Evals
404
496
  const evalStore = new EvalStore(config.repoPath);
405
497
  app.use(createEvalRouter({ getBundle, evalStore, repoPath: config.repoPath, getPort: () => config.port }));
406
498
  // Feedback
407
499
  const feedbackStore = new FeedbackStore(config.repoPath);
408
500
  app.use(createFeedbackRouter({ feedbackStore }));
409
- // Chat routes (new stack)
501
+ // Chat routes (new stack) — persistence is handled inside runMessage /
502
+ // route-helpers, so no explicit hooks are needed here.
410
503
  app.use(createChatStreamRouter({
411
504
  sessionManager,
412
505
  bundleResolver: { staticBundle: bundle },
413
506
  shared,
414
- createStreamHooks: () => ({
415
- onSessionPersist: (sessionId) => {
416
- const session = sessionManager.get(sessionId);
417
- if (session) {
418
- void sessionManager.persist(session);
419
- // Also mirror to the legacy file-based store so the /sessions
420
- // endpoints (read by the UI history panel) see chat sessions.
421
- // Without this, chat sessions only land in PGLite and the UI
422
- // only sees automation/task sessions. PGLite remains the source
423
- // of truth — a mirror failure is logged and swallowed (the hook
424
- // is invoked from fireDrainHooks after the response has drained,
425
- // so throwing here would break the route handler).
426
- try {
427
- legacySessionStore.save({
428
- id: session.id,
429
- appId: session.appId,
430
- title: session.metadata.title,
431
- messages: session.messages,
432
- createdAt: session.createdAt,
433
- lastAccessedAt: session.lastAccessedAt,
434
- });
435
- }
436
- catch (err) {
437
- log.warn('legacy_session_mirror_failed', {
438
- sessionId: session.id,
439
- error: err instanceof Error ? err.message : String(err),
440
- });
441
- }
442
- }
443
- },
444
- }),
507
+ summarizeToolResult: config.summarizeToolResult,
445
508
  }));
446
509
  app.use(createChatRouter({
447
510
  sessionManager,
448
511
  bundleResolver: { staticBundle: bundle },
449
512
  shared,
513
+ summarizeToolResult: config.summarizeToolResult,
450
514
  }));
451
515
  // Task runner
452
516
  app.use(createTaskRouter({ sessionManager, createTaskSession: createAutomationSessionComponents }));
@@ -520,6 +584,8 @@ export async function createLocalServer(config) {
520
584
  // Shared resources and session components will pick up the new
521
585
  // bundle on next session creation via getBundle().
522
586
  log.info('config_reloaded', { name: newBundle.config.name });
587
+ eventBus.emit({ type: 'manifest_changed' });
588
+ eventBus.emit({ type: 'files_changed' });
523
589
  });
524
590
  watcher.start();
525
591
  }
@@ -560,4 +626,82 @@ export async function createLocalServer(config) {
560
626
  },
561
627
  };
562
628
  }
629
+ // ---------------------------------------------------------------------------
630
+ // /sessions + /session/:id response helpers
631
+ // ---------------------------------------------------------------------------
632
+ /** Max length of the first-user-message excerpt shown in session lists. */
633
+ const SUMMARY_EXCERPT_MAX = 80;
634
+ function isRecord(x) {
635
+ return typeof x === 'object' && x !== null;
636
+ }
637
+ function isTextPart(part) {
638
+ return isRecord(part) && part['type'] === 'text' && typeof part['text'] === 'string';
639
+ }
640
+ function isToolCallPart(part) {
641
+ return (isRecord(part) &&
642
+ part['type'] === 'tool-call' &&
643
+ typeof part['toolCallId'] === 'string' &&
644
+ typeof part['toolName'] === 'string');
645
+ }
646
+ function getMessageRole(raw) {
647
+ if (!isRecord(raw))
648
+ return null;
649
+ const role = raw['role'];
650
+ return typeof role === 'string' ? role : null;
651
+ }
652
+ function getMessageContent(raw) {
653
+ if (!isRecord(raw))
654
+ return undefined;
655
+ return raw['content'];
656
+ }
657
+ /** Truncate with an ellipsis when the source exceeds the excerpt budget. */
658
+ function excerpt(s) {
659
+ return s.length > SUMMARY_EXCERPT_MAX ? `${s.slice(0, SUMMARY_EXCERPT_MAX)}…` : s;
660
+ }
661
+ /** Extract the first user-message text from a persisted message array for list summaries. */
662
+ function extractFirstUserText(messages) {
663
+ for (const raw of messages) {
664
+ if (getMessageRole(raw) !== 'user')
665
+ continue;
666
+ const content = getMessageContent(raw);
667
+ if (typeof content === 'string')
668
+ return excerpt(content);
669
+ if (Array.isArray(content)) {
670
+ const firstText = content.find(isTextPart);
671
+ if (firstText)
672
+ return excerpt(firstText.text);
673
+ }
674
+ }
675
+ return undefined;
676
+ }
677
+ /**
678
+ * Flatten a persisted `ModelMessage` (ai SDK v6) into the shape the web UI's
679
+ * /session/:id consumer expects: {role, text, toolCalls?}. Returns null for
680
+ * tool-result messages and for assistant turns with no renderable content
681
+ * (the history panel shows conversation + tool-call chips, not raw tool
682
+ * plumbing).
683
+ */
684
+ function flattenModelMessage(raw) {
685
+ const role = getMessageRole(raw);
686
+ if (role !== 'user' && role !== 'assistant')
687
+ return null;
688
+ const content = getMessageContent(raw);
689
+ if (typeof content === 'string') {
690
+ return { role, text: content };
691
+ }
692
+ if (Array.isArray(content)) {
693
+ const text = content.filter(isTextPart).map((p) => p.text).join('');
694
+ const toolCalls = role === 'assistant'
695
+ ? content.filter(isToolCallPart).map((p) => ({
696
+ toolId: p.toolCallId,
697
+ toolName: p.toolName,
698
+ parameters: isRecord(p.input) ? p.input : {},
699
+ }))
700
+ : [];
701
+ if (text.length === 0 && toolCalls.length === 0)
702
+ return null;
703
+ return toolCalls.length > 0 ? { role, text, toolCalls } : { role, text };
704
+ }
705
+ return null;
706
+ }
563
707
  //# sourceMappingURL=local-server.js.map