@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.
- package/dist/src/__fixtures__/README.md +4 -0
- package/dist/src/__fixtures__/e2e.test.d.ts +6 -0
- package/dist/src/__fixtures__/e2e.test.js +211 -0
- package/dist/src/__fixtures__/e2e.test.js.map +1 -0
- package/dist/src/__fixtures__/smoke-agent/automations/delivery-callback-test.json +9 -0
- package/dist/src/__fixtures__/smoke-agent/connections/mock-mcp/spec.json +1 -1
- package/dist/src/__fixtures__/smoke.test.js +715 -29
- package/dist/src/__fixtures__/smoke.test.js.map +1 -1
- package/dist/src/__fixtures__/test-env.d.ts +27 -0
- package/dist/src/__fixtures__/test-env.js +64 -0
- package/dist/src/__fixtures__/test-env.js.map +1 -0
- package/dist/src/__fixtures__/test-helpers.d.ts +30 -0
- package/dist/src/__fixtures__/test-helpers.js +120 -0
- package/dist/src/__fixtures__/test-helpers.js.map +1 -0
- package/dist/src/agent/agent-types.d.ts +22 -0
- package/dist/src/agent/agent-types.js.map +1 -1
- package/dist/src/agent/automation-bridge.d.ts +9 -0
- package/dist/src/agent/automation-bridge.js +26 -0
- package/dist/src/agent/automation-bridge.js.map +1 -1
- package/dist/src/agent/automation-bridge.test.js +63 -0
- package/dist/src/agent/automation-bridge.test.js.map +1 -1
- package/dist/src/agent/local-server.d.ts +0 -7
- package/dist/src/agent/local-server.js +230 -86
- package/dist/src/agent/local-server.js.map +1 -1
- package/dist/src/agent/local-server.test.js +14 -8
- package/dist/src/agent/local-server.test.js.map +1 -1
- package/dist/src/agent/loop-types.d.ts +81 -2
- package/dist/src/agent/loop-types.js +4 -0
- package/dist/src/agent/loop-types.js.map +1 -1
- package/dist/src/agent/loop.js +16 -3
- package/dist/src/agent/loop.js.map +1 -1
- package/dist/src/agent/loop.test.js +572 -8
- package/dist/src/agent/loop.test.js.map +1 -1
- package/dist/src/agent/proactive/delivery-router.d.ts +68 -0
- package/dist/src/agent/proactive/delivery-router.js +337 -0
- package/dist/src/agent/proactive/delivery-router.js.map +1 -0
- package/dist/src/agent/proactive/delivery-router.test.d.ts +6 -0
- package/dist/src/agent/proactive/delivery-router.test.js +455 -0
- package/dist/src/agent/proactive/delivery-router.test.js.map +1 -0
- package/dist/src/agent/proactive/proactive-runner.d.ts +23 -1
- package/dist/src/agent/proactive/proactive-runner.js +42 -10
- package/dist/src/agent/proactive/proactive-runner.js.map +1 -1
- package/dist/src/agent/proactive/proactive-runner.test.js +0 -2
- package/dist/src/agent/proactive/proactive-runner.test.js.map +1 -1
- package/dist/src/agent/routes/admin-chat-abort.test.d.ts +6 -0
- package/dist/src/agent/routes/admin-chat-abort.test.js +206 -0
- package/dist/src/agent/routes/admin-chat-abort.test.js.map +1 -0
- package/dist/src/agent/routes/admin-chat.js +0 -2
- package/dist/src/agent/routes/admin-chat.js.map +1 -1
- package/dist/src/agent/routes/task.test.js +0 -2
- package/dist/src/agent/routes/task.test.js.map +1 -1
- package/dist/src/agent/snapshot-server.js +0 -2
- package/dist/src/agent/snapshot-server.js.map +1 -1
- package/dist/src/agent/states/compacting.js +5 -3
- package/dist/src/agent/states/compacting.js.map +1 -1
- package/dist/src/agent/states/confirming.js +3 -0
- package/dist/src/agent/states/confirming.js.map +1 -1
- package/dist/src/agent/states/dispatching.js +45 -1
- package/dist/src/agent/states/dispatching.js.map +1 -1
- package/dist/src/agent/states/executing.js +225 -81
- package/dist/src/agent/states/executing.js.map +1 -1
- package/dist/src/agent/states/streaming.js +14 -0
- package/dist/src/agent/states/streaming.js.map +1 -1
- package/dist/src/agent/states/thinking.d.ts +1 -1
- package/dist/src/agent/states/thinking.js +246 -29
- package/dist/src/agent/states/thinking.js.map +1 -1
- package/dist/src/agent/token-estimate.d.ts +20 -6
- package/dist/src/agent/token-estimate.js +24 -3
- package/dist/src/agent/token-estimate.js.map +1 -1
- package/dist/src/agent/token-estimate.test.d.ts +6 -0
- package/dist/src/agent/token-estimate.test.js +44 -0
- package/dist/src/agent/token-estimate.test.js.map +1 -0
- package/dist/src/api/create-agent.js +0 -3
- package/dist/src/api/create-agent.js.map +1 -1
- package/dist/src/api/types.d.ts +0 -2
- package/dist/src/env-ref.d.ts +13 -0
- package/dist/src/env-ref.js +31 -0
- package/dist/src/env-ref.js.map +1 -0
- package/dist/src/env-ref.test.d.ts +6 -0
- package/dist/src/env-ref.test.js +34 -0
- package/dist/src/env-ref.test.js.map +1 -0
- package/dist/src/errors.d.ts +15 -0
- package/dist/src/errors.js +22 -0
- package/dist/src/errors.js.map +1 -1
- package/dist/src/errors.test.js +2 -2
- package/dist/src/errors.test.js.map +1 -1
- package/dist/src/events/event-bus.d.ts +54 -0
- package/dist/src/events/event-bus.js +84 -0
- package/dist/src/events/event-bus.js.map +1 -0
- package/dist/src/events/event-bus.test.d.ts +6 -0
- package/dist/src/events/event-bus.test.js +112 -0
- package/dist/src/events/event-bus.test.js.map +1 -0
- package/dist/src/events/events-route.d.ts +36 -0
- package/dist/src/events/events-route.js +80 -0
- package/dist/src/events/events-route.js.map +1 -0
- package/dist/src/events/events-route.test.d.ts +6 -0
- package/dist/src/events/events-route.test.js +134 -0
- package/dist/src/events/events-route.test.js.map +1 -0
- package/dist/src/events/store-event-wrapper.d.ts +19 -0
- package/dist/src/events/store-event-wrapper.js +57 -0
- package/dist/src/events/store-event-wrapper.js.map +1 -0
- package/dist/src/events/store-event-wrapper.test.d.ts +6 -0
- package/dist/src/events/store-event-wrapper.test.js +91 -0
- package/dist/src/events/store-event-wrapper.test.js.map +1 -0
- package/dist/src/middleware/auth.d.ts +0 -2
- package/dist/src/middleware/auth.js.map +1 -1
- package/dist/src/providers/search-provider.d.ts +64 -0
- package/dist/src/providers/search-provider.js +174 -0
- package/dist/src/providers/search-provider.js.map +1 -0
- package/dist/src/providers/types.d.ts +8 -0
- package/dist/src/routes/ai-stream.d.ts +15 -0
- package/dist/src/routes/ai-stream.js +9 -0
- package/dist/src/routes/ai-stream.js.map +1 -1
- package/dist/src/routes/chat-stream.d.ts +6 -0
- package/dist/src/routes/chat-stream.js +2 -0
- package/dist/src/routes/chat-stream.js.map +1 -1
- package/dist/src/routes/chat.d.ts +6 -0
- package/dist/src/routes/chat.js +2 -0
- package/dist/src/routes/chat.js.map +1 -1
- package/dist/src/routes/session-resolver.d.ts +5 -0
- package/dist/src/routes/session-resolver.js +1 -15
- package/dist/src/routes/session-resolver.js.map +1 -1
- package/dist/src/routes/session-resolver.test.js +7 -6
- package/dist/src/routes/session-resolver.test.js.map +1 -1
- package/dist/src/server.d.ts +6 -0
- package/dist/src/server.js +2 -0
- package/dist/src/server.js.map +1 -1
- package/dist/src/session/drizzle-session-store.d.ts +56 -0
- package/dist/src/session/drizzle-session-store.js +203 -0
- package/dist/src/session/drizzle-session-store.js.map +1 -0
- package/dist/src/session/manager.d.ts +6 -3
- package/dist/src/session/manager.js +46 -16
- package/dist/src/session/manager.js.map +1 -1
- package/dist/src/session/manager.test.js +12 -18
- package/dist/src/session/manager.test.js.map +1 -1
- package/dist/src/session/pglite-session-store.d.ts +23 -0
- package/dist/src/session/pglite-session-store.js +70 -0
- package/dist/src/session/pglite-session-store.js.map +1 -0
- package/dist/src/session/postgres-session-store.d.ts +44 -0
- package/dist/src/session/postgres-session-store.js +138 -0
- package/dist/src/session/postgres-session-store.js.map +1 -0
- package/dist/src/session/session-builder.d.ts +0 -2
- package/dist/src/session/session-builder.js +22 -2
- package/dist/src/session/session-builder.js.map +1 -1
- package/dist/src/session/session-builder.test.js +0 -2
- package/dist/src/session/session-builder.test.js.map +1 -1
- package/dist/src/session/session-store-selector.d.ts +49 -0
- package/dist/src/session/session-store-selector.js +60 -0
- package/dist/src/session/session-store-selector.js.map +1 -0
- package/dist/src/session/session-store-selector.test.d.ts +6 -0
- package/dist/src/session/session-store-selector.test.js +79 -0
- package/dist/src/session/session-store-selector.test.js.map +1 -0
- package/dist/src/session/store.d.ts +146 -32
- package/dist/src/session/store.js +126 -138
- package/dist/src/session/store.js.map +1 -1
- package/dist/src/session/store.test.js +385 -107
- package/dist/src/session/store.test.js.map +1 -1
- package/dist/src/session/tool-context-factory.d.ts +3 -2
- package/dist/src/session/tool-context-factory.js +1 -2
- package/dist/src/session/tool-context-factory.js.map +1 -1
- package/dist/src/session/tool-context-factory.test.js +1 -4
- package/dist/src/session/tool-context-factory.test.js.map +1 -1
- package/dist/src/session/types.d.ts +13 -6
- package/dist/src/stores/schema.d.ts +0 -34
- package/dist/src/stores/schema.js +6 -4
- package/dist/src/stores/schema.js.map +1 -1
- package/dist/src/tools/admin-file-tools.d.ts +29 -0
- package/dist/src/tools/admin-file-tools.js +525 -11
- package/dist/src/tools/admin-file-tools.js.map +1 -1
- package/dist/src/tools/admin-file-tools.test.js +373 -4
- package/dist/src/tools/admin-file-tools.test.js.map +1 -1
- package/dist/src/tools/custom-tool-adapter.test.js +0 -1
- package/dist/src/tools/custom-tool-adapter.test.js.map +1 -1
- package/dist/src/tools/dispatch-tool.d.ts +4 -4
- package/dist/src/tools/fetch-url-tool.d.ts +23 -0
- package/dist/src/tools/fetch-url-tool.js +333 -0
- package/dist/src/tools/fetch-url-tool.js.map +1 -0
- package/dist/src/tools/fetch-url-tool.test.d.ts +6 -0
- package/dist/src/tools/fetch-url-tool.test.js +228 -0
- package/dist/src/tools/fetch-url-tool.test.js.map +1 -0
- package/dist/src/tools/mcp-tool-adapter.test.js +0 -1
- package/dist/src/tools/mcp-tool-adapter.test.js.map +1 -1
- package/dist/src/tools/registry.test.js +0 -1
- package/dist/src/tools/registry.test.js.map +1 -1
- package/dist/src/tools/request-tool.test.js +0 -1
- package/dist/src/tools/request-tool.test.js.map +1 -1
- package/dist/src/tools/store-tools.test.js +0 -1
- package/dist/src/tools/store-tools.test.js.map +1 -1
- package/dist/src/tools/types.d.ts +20 -2
- package/dist/src/tools/web-search-tool.d.ts +31 -0
- package/dist/src/tools/web-search-tool.js +170 -0
- package/dist/src/tools/web-search-tool.js.map +1 -0
- package/dist/src/tools/web-search-tool.test.d.ts +6 -0
- package/dist/src/tools/web-search-tool.test.js +153 -0
- package/dist/src/tools/web-search-tool.test.js.map +1 -0
- package/dist/src/tools/web-tools-shared.d.ts +21 -0
- package/dist/src/tools/web-tools-shared.js +32 -0
- package/dist/src/tools/web-tools-shared.js.map +1 -0
- package/dist/src/types.d.ts +20 -0
- package/dist/src/types.js +13 -0
- package/dist/src/types.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +17 -3
- package/dist/src/agent/session-store.d.ts +0 -71
- package/dist/src/agent/session-store.js +0 -151
- package/dist/src/agent/session-store.js.map +0 -1
- package/dist/src/session/admin-file-tools.d.ts +0 -136
- package/dist/src/session/admin-file-tools.js +0 -240
- 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 {
|
|
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/
|
|
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
|
|
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
|
|
185
|
-
await
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
340
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
const
|
|
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.
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
394
|
-
const deleted =
|
|
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
|
-
|
|
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
|