@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
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Amodal Labs, Inc.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
7
|
+
import { selectSessionStore } from './session-store-selector.js';
|
|
8
|
+
import { DrizzleSessionStore } from './drizzle-session-store.js';
|
|
9
|
+
import { createLogger } from '../logger.js';
|
|
10
|
+
const logger = createLogger({ component: 'test:selector' });
|
|
11
|
+
/**
|
|
12
|
+
* Both backends return `DrizzleSessionStore` today; we assert on the
|
|
13
|
+
* `backendName` tag to tell them apart in tests. If that instance type
|
|
14
|
+
* ever changes, this narrow cast is the one place to update.
|
|
15
|
+
*/
|
|
16
|
+
function asDrizzle(store) {
|
|
17
|
+
expect(store).toBeInstanceOf(DrizzleSessionStore);
|
|
18
|
+
return store;
|
|
19
|
+
}
|
|
20
|
+
describe('selectSessionStore', () => {
|
|
21
|
+
const created = [];
|
|
22
|
+
afterEach(async () => {
|
|
23
|
+
while (created.length > 0) {
|
|
24
|
+
const s = created.pop();
|
|
25
|
+
await s.close();
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
it('defaults to PGLite when no backend specified', async () => {
|
|
29
|
+
const store = await selectSessionStore({ logger });
|
|
30
|
+
created.push(store);
|
|
31
|
+
expect(asDrizzle(store).backendName).toBe('pglite');
|
|
32
|
+
});
|
|
33
|
+
it('uses PGLite when backend is explicitly pglite', async () => {
|
|
34
|
+
const store = await selectSessionStore({ backend: 'pglite', logger });
|
|
35
|
+
created.push(store);
|
|
36
|
+
expect(asDrizzle(store).backendName).toBe('pglite');
|
|
37
|
+
});
|
|
38
|
+
it('falls back to PGLite when backend=postgres but no URL is set', async () => {
|
|
39
|
+
const store = await selectSessionStore({ backend: 'postgres', logger });
|
|
40
|
+
created.push(store);
|
|
41
|
+
expect(asDrizzle(store).backendName).toBe('pglite');
|
|
42
|
+
});
|
|
43
|
+
it('falls back to PGLite when postgresUrl is an empty string', async () => {
|
|
44
|
+
// Caller is responsible for resolving env: refs; empty string is
|
|
45
|
+
// what they pass when `env:VAR` was unset.
|
|
46
|
+
const store = await selectSessionStore({
|
|
47
|
+
backend: 'postgres',
|
|
48
|
+
postgresUrl: '',
|
|
49
|
+
logger,
|
|
50
|
+
});
|
|
51
|
+
created.push(store);
|
|
52
|
+
expect(asDrizzle(store).backendName).toBe('pglite');
|
|
53
|
+
});
|
|
54
|
+
it('falls back to PGLite when Postgres connection fails to initialize', async () => {
|
|
55
|
+
// Unreachable host + very short connect timeout → init throws →
|
|
56
|
+
// selector logs error and falls back to PGLite. This is the
|
|
57
|
+
// "runtime must boot even on misconfigured Postgres" path.
|
|
58
|
+
const store = await selectSessionStore({
|
|
59
|
+
backend: 'postgres',
|
|
60
|
+
// Port 1 is reserved and connection will be refused fast
|
|
61
|
+
postgresUrl: 'postgres://nobody:nothing@127.0.0.1:1/missing?connect_timeout=1',
|
|
62
|
+
logger,
|
|
63
|
+
});
|
|
64
|
+
created.push(store);
|
|
65
|
+
expect(asDrizzle(store).backendName).toBe('pglite');
|
|
66
|
+
});
|
|
67
|
+
const pgUrl = process.env['TEST_POSTGRES_URL'] ?? '';
|
|
68
|
+
const itPg = pgUrl ? it : it.skip;
|
|
69
|
+
itPg('uses Postgres backend when backend=postgres and URL is set', async () => {
|
|
70
|
+
const store = await selectSessionStore({
|
|
71
|
+
backend: 'postgres',
|
|
72
|
+
postgresUrl: pgUrl,
|
|
73
|
+
logger,
|
|
74
|
+
});
|
|
75
|
+
created.push(store);
|
|
76
|
+
expect(asDrizzle(store).backendName).toBe('postgres');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
//# sourceMappingURL=session-store-selector.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-store-selector.test.js","sourceRoot":"","sources":["../../../src/session/session-store-selector.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,55 +3,169 @@
|
|
|
3
3
|
* Copyright 2026 Amodal Labs, Inc.
|
|
4
4
|
* SPDX-License-Identifier: MIT
|
|
5
5
|
*/
|
|
6
|
+
/**
|
|
7
|
+
* Session persistence layer — interface, types, and shared helpers.
|
|
8
|
+
*
|
|
9
|
+
* Concrete implementations:
|
|
10
|
+
* - `DrizzleSessionStore` (./drizzle-session-store.ts) — the single
|
|
11
|
+
* query layer shared by both backends.
|
|
12
|
+
* - `createPGLiteSessionStore` (./pglite-session-store.ts) — factory
|
|
13
|
+
* for local-dev / in-memory PGLite.
|
|
14
|
+
* - `createPostgresSessionStore` (./postgres-session-store.ts) —
|
|
15
|
+
* factory for hosted runtime / ISV production.
|
|
16
|
+
*
|
|
17
|
+
* The Drizzle schema lives in `../stores/schema.ts`, shared with the
|
|
18
|
+
* store document tables.
|
|
19
|
+
*/
|
|
20
|
+
import { and } from 'drizzle-orm';
|
|
21
|
+
import type { AnyPgColumn } from 'drizzle-orm/pg-core';
|
|
22
|
+
import type { agentSessions } from '../stores/schema.js';
|
|
6
23
|
import type { PersistedSession } from './types.js';
|
|
7
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Options for `SessionStore.list()`.
|
|
26
|
+
*
|
|
27
|
+
* Cursor-based pagination over `updated_at` (newest first). The cursor
|
|
28
|
+
* encodes the updatedAt/id pair of the last row returned — pass it back
|
|
29
|
+
* unchanged to fetch the next page.
|
|
30
|
+
*
|
|
31
|
+
* Filters match against metadata JSONB paths using equality. Only
|
|
32
|
+
* `snake_case` / simple identifier keys are accepted — see
|
|
33
|
+
* `validateFilterKey` for the exact rules. Passing an untrusted key
|
|
34
|
+
* throws `SessionStoreError`.
|
|
35
|
+
*
|
|
36
|
+
* `updatedAfter` / `updatedBefore` filter by the `updated_at` column
|
|
37
|
+
* (inclusive). Useful for "sessions touched this week" queries.
|
|
38
|
+
*/
|
|
39
|
+
export interface SessionListOptions {
|
|
40
|
+
readonly limit?: number;
|
|
41
|
+
readonly cursor?: string;
|
|
42
|
+
readonly filter?: Readonly<Record<string, unknown>>;
|
|
43
|
+
readonly updatedAfter?: Date;
|
|
44
|
+
readonly updatedBefore?: Date;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Result of `SessionStore.list()` — sessions plus an opaque cursor
|
|
48
|
+
* for the next page. `nextCursor` is `null` when there are no more rows.
|
|
49
|
+
*/
|
|
50
|
+
export interface SessionListResult {
|
|
51
|
+
readonly sessions: PersistedSession[];
|
|
52
|
+
readonly nextCursor: string | null;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Optional callbacks fired after mutations. Each hook is awaited, so
|
|
56
|
+
* a failing hook propagates to the caller. Keep hooks fast — they sit
|
|
57
|
+
* on the write path.
|
|
58
|
+
*
|
|
59
|
+
* Intended use: dual-write to a consumer's own table (hosted-runtime
|
|
60
|
+
* adoption path), emit observability events, invalidate caches.
|
|
61
|
+
*/
|
|
62
|
+
export interface SessionStoreHooks {
|
|
63
|
+
onAfterSave?: (session: PersistedSession) => Promise<void> | void;
|
|
64
|
+
onAfterDelete?: (sessionId: string) => Promise<void> | void;
|
|
65
|
+
onAfterCleanup?: (opts: {
|
|
66
|
+
deleted: number;
|
|
67
|
+
before: Date;
|
|
68
|
+
}) => Promise<void> | void;
|
|
69
|
+
}
|
|
8
70
|
/**
|
|
9
71
|
* Interface for session persistence backends.
|
|
10
72
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
73
|
+
* One concrete implementation (`DrizzleSessionStore`) with two factories:
|
|
74
|
+
* `createPGLiteSessionStore` for local dev, `createPostgresSessionStore`
|
|
75
|
+
* for hosted runtime / ISV production. Both share the `agentSessions`
|
|
76
|
+
* Drizzle schema and the query helpers in this module.
|
|
13
77
|
*/
|
|
14
78
|
export interface SessionStore {
|
|
15
79
|
/** Initialize the backing store (create tables, run migrations). */
|
|
16
80
|
initialize(): Promise<void>;
|
|
17
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* Save or update a session.
|
|
83
|
+
*
|
|
84
|
+
* **Semantics: last-write-wins.** The implementation does an
|
|
85
|
+
* unconditional `onConflictDoUpdate` — no optimistic-concurrency
|
|
86
|
+
* version check. If two concurrent `save()` calls target the same
|
|
87
|
+
* session ID, the later write silently overwrites the earlier one,
|
|
88
|
+
* including any messages the earlier caller added.
|
|
89
|
+
*
|
|
90
|
+
* **Callers must serialize per-session writes.** The built-in
|
|
91
|
+
* `StandaloneSessionManager` does this by routing all writes for a
|
|
92
|
+
* session through a single in-memory object. External callers that
|
|
93
|
+
* share a session across workers must either (a) use a single
|
|
94
|
+
* writer per session, or (b) wrap `save()` with their own advisory
|
|
95
|
+
* locking. The `version` column is persisted and surfaced on read
|
|
96
|
+
* but is currently informational only — reserved for future OCC.
|
|
97
|
+
*/
|
|
18
98
|
save(session: PersistedSession): Promise<void>;
|
|
19
|
-
/** Load a session by
|
|
99
|
+
/** Load a session by id. Returns null if not found. */
|
|
20
100
|
load(sessionId: string): Promise<PersistedSession | null>;
|
|
21
|
-
/**
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
101
|
+
/**
|
|
102
|
+
* List sessions newest first. Returns sessions + a pagination cursor.
|
|
103
|
+
* Callers that need any form of scoping should namespace their
|
|
104
|
+
* session IDs directly (e.g. `app-a:session-123`) and/or filter by
|
|
105
|
+
* `metadata` fields via `opts.filter`.
|
|
106
|
+
*/
|
|
107
|
+
list(opts?: SessionListOptions): Promise<SessionListResult>;
|
|
108
|
+
/** Delete a session by id. Returns true if a row was deleted. */
|
|
26
109
|
delete(sessionId: string): Promise<boolean>;
|
|
27
110
|
/** Delete sessions not updated since `before`. Returns count deleted. */
|
|
28
111
|
cleanup(before: Date): Promise<number>;
|
|
29
112
|
/** Close the backing store. */
|
|
30
113
|
close(): Promise<void>;
|
|
31
114
|
}
|
|
115
|
+
export declare function validateFilterKey(backend: string, key: string): void;
|
|
116
|
+
/** Encode a cursor as opaque base64 of "updatedAt.ms|id". */
|
|
117
|
+
export declare function encodeCursor(updatedAt: Date, id: string): string;
|
|
118
|
+
/** Decode a cursor; throws SessionStoreError on malformed input. */
|
|
119
|
+
export declare function decodeCursor(backend: string, cursor: string): {
|
|
120
|
+
updatedAt: Date;
|
|
121
|
+
id: string;
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Map a `PersistedSession` to its row shape for insert/update. Separate
|
|
125
|
+
* function so both backends do the exact same type conversion.
|
|
126
|
+
*/
|
|
127
|
+
export declare function sessionToRow(session: PersistedSession): {
|
|
128
|
+
id: string;
|
|
129
|
+
messages: unknown[];
|
|
130
|
+
tokenUsage: {
|
|
131
|
+
inputTokens: number;
|
|
132
|
+
outputTokens: number;
|
|
133
|
+
totalTokens: number;
|
|
134
|
+
};
|
|
135
|
+
metadata: Record<string, unknown>;
|
|
136
|
+
version: number;
|
|
137
|
+
createdAt: Date;
|
|
138
|
+
updatedAt: Date;
|
|
139
|
+
};
|
|
32
140
|
/**
|
|
33
|
-
*
|
|
141
|
+
* Inverse of `sessionToRow`.
|
|
34
142
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
143
|
+
* Validates at the JSONB boundary: rejects rows with an unknown
|
|
144
|
+
* `version` (future schema migration signal) or a non-array `messages`
|
|
145
|
+
* payload (would indicate a malformed write from an older runtime or
|
|
146
|
+
* manual DB edit). Throws `SessionStoreError` with context when a row
|
|
147
|
+
* fails validation rather than returning a quietly-broken session
|
|
148
|
+
* that crashes deep in the agent loop.
|
|
37
149
|
*/
|
|
38
|
-
export declare
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
load(sessionId: string): Promise<PersistedSession | null>;
|
|
50
|
-
list(tenantId: string, opts?: {
|
|
51
|
-
limit?: number;
|
|
52
|
-
}): Promise<PersistedSession[]>;
|
|
53
|
-
delete(sessionId: string): Promise<boolean>;
|
|
54
|
-
cleanup(before: Date): Promise<number>;
|
|
55
|
-
close(): Promise<void>;
|
|
56
|
-
private ensureDb;
|
|
150
|
+
export declare function rowToPersistedSession(backend: string, row: typeof agentSessions.$inferSelect): PersistedSession;
|
|
151
|
+
/**
|
|
152
|
+
* Structural shape the `buildListConditions` helper needs. Any
|
|
153
|
+
* `agent_sessions`-compatible Drizzle pgTable (default name or a
|
|
154
|
+
* consumer-supplied one) satisfies this — both backends use the same
|
|
155
|
+
* column names, only the table name differs.
|
|
156
|
+
*/
|
|
157
|
+
export interface AgentSessionsColumns {
|
|
158
|
+
updatedAt: AnyPgColumn;
|
|
159
|
+
id: AnyPgColumn;
|
|
160
|
+
metadata: AnyPgColumn;
|
|
57
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Build the where-clause fragments for `list()`. Generic over the
|
|
164
|
+
* table's column bindings so both PGLite (static `agentSessions`) and
|
|
165
|
+
* Postgres (dynamic `makeAgentSessionsTable(name)`) pass their own
|
|
166
|
+
* tables without a cast.
|
|
167
|
+
*
|
|
168
|
+
* Returns `undefined` when `opts` yields no conditions — drizzle's
|
|
169
|
+
* `.where(undefined)` is a no-op, equivalent to "no filter".
|
|
170
|
+
*/
|
|
171
|
+
export declare function buildListConditions(backend: string, opts: SessionListOptions | undefined, table: AgentSessionsColumns): ReturnType<typeof and>;
|
|
@@ -4,159 +4,109 @@
|
|
|
4
4
|
* SPDX-License-Identifier: MIT
|
|
5
5
|
*/
|
|
6
6
|
/**
|
|
7
|
-
* Session persistence layer.
|
|
7
|
+
* Session persistence layer — interface, types, and shared helpers.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* shared
|
|
9
|
+
* Concrete implementations:
|
|
10
|
+
* - `DrizzleSessionStore` (./drizzle-session-store.ts) — the single
|
|
11
|
+
* query layer shared by both backends.
|
|
12
|
+
* - `createPGLiteSessionStore` (./pglite-session-store.ts) — factory
|
|
13
|
+
* for local-dev / in-memory PGLite.
|
|
14
|
+
* - `createPostgresSessionStore` (./postgres-session-store.ts) —
|
|
15
|
+
* factory for hosted runtime / ISV production.
|
|
16
|
+
*
|
|
17
|
+
* The Drizzle schema lives in `../stores/schema.ts`, shared with the
|
|
18
|
+
* store document tables.
|
|
12
19
|
*/
|
|
13
|
-
import { eq, lt,
|
|
14
|
-
import {
|
|
15
|
-
import { agentSessions } from '../stores/schema.js';
|
|
16
|
-
import { ConfigError } from '../errors.js';
|
|
20
|
+
import { and, eq, lt, or, gte, lte, sql } from 'drizzle-orm';
|
|
21
|
+
import { SessionStoreError } from '../errors.js';
|
|
17
22
|
// ---------------------------------------------------------------------------
|
|
18
|
-
// PGLite
|
|
23
|
+
// Shared helpers — used by both PGLite and Postgres implementations
|
|
19
24
|
// ---------------------------------------------------------------------------
|
|
20
25
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
26
|
+
* Reject filter keys that could be used for SQL injection. We only
|
|
27
|
+
* allow simple identifiers: letters, digits, underscores. The keys
|
|
28
|
+
* become part of a JSONB path (`metadata->>'key'`) so they must be
|
|
29
|
+
* safe to interpolate.
|
|
25
30
|
*/
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
this.dataDir = opts.dataDir;
|
|
34
|
-
this.logger = opts.logger;
|
|
35
|
-
}
|
|
36
|
-
async initialize() {
|
|
37
|
-
if (this.db)
|
|
38
|
-
return;
|
|
39
|
-
if (this.dataDir) {
|
|
40
|
-
const { mkdirSync } = await import('node:fs');
|
|
41
|
-
mkdirSync(this.dataDir, { recursive: true });
|
|
42
|
-
}
|
|
43
|
-
const { PGlite } = await import('@electric-sql/pglite');
|
|
44
|
-
this.pglite = new PGlite(this.dataDir ?? undefined);
|
|
45
|
-
this.db = drizzle(this.pglite);
|
|
46
|
-
// Create table via raw SQL — Drizzle schema defines the shape,
|
|
47
|
-
// but we use raw DDL for initialization (no drizzle-kit in runtime).
|
|
48
|
-
await this.pglite.exec(`
|
|
49
|
-
CREATE TABLE IF NOT EXISTS agent_sessions (
|
|
50
|
-
id TEXT PRIMARY KEY,
|
|
51
|
-
tenant_id TEXT NOT NULL,
|
|
52
|
-
user_id TEXT NOT NULL,
|
|
53
|
-
messages JSONB NOT NULL,
|
|
54
|
-
token_usage JSONB NOT NULL,
|
|
55
|
-
metadata JSONB DEFAULT '{}',
|
|
56
|
-
version INTEGER NOT NULL DEFAULT 1,
|
|
57
|
-
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
58
|
-
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
CREATE INDEX IF NOT EXISTS idx_agent_sessions_tenant
|
|
62
|
-
ON agent_sessions (tenant_id, updated_at DESC);
|
|
63
|
-
`);
|
|
64
|
-
this.logger.info('session_store_initialized', {
|
|
65
|
-
dataDir: this.dataDir ?? 'in-memory',
|
|
31
|
+
const SAFE_KEY_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
32
|
+
export function validateFilterKey(backend, key) {
|
|
33
|
+
if (!SAFE_KEY_RE.test(key)) {
|
|
34
|
+
throw new SessionStoreError(`Invalid filter field name: ${JSON.stringify(key)}`, {
|
|
35
|
+
backend,
|
|
36
|
+
operation: 'list',
|
|
37
|
+
context: { key, expected: 'identifier: letters, digits, underscore only' },
|
|
66
38
|
});
|
|
67
39
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
target: agentSessions.id,
|
|
86
|
-
set: {
|
|
87
|
-
messages: values.messages,
|
|
88
|
-
tokenUsage: values.tokenUsage,
|
|
89
|
-
metadata: values.metadata,
|
|
90
|
-
updatedAt: values.updatedAt,
|
|
91
|
-
},
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
async load(sessionId) {
|
|
95
|
-
this.ensureDb();
|
|
96
|
-
const rows = await this.db
|
|
97
|
-
.select()
|
|
98
|
-
.from(agentSessions)
|
|
99
|
-
.where(eq(agentSessions.id, sessionId))
|
|
100
|
-
.limit(1);
|
|
101
|
-
if (rows.length === 0)
|
|
102
|
-
return null;
|
|
103
|
-
return rowToPersistedSession(rows[0]);
|
|
104
|
-
}
|
|
105
|
-
async list(tenantId, opts) {
|
|
106
|
-
this.ensureDb();
|
|
107
|
-
const rows = await this.db
|
|
108
|
-
.select()
|
|
109
|
-
.from(agentSessions)
|
|
110
|
-
.where(eq(agentSessions.tenantId, tenantId))
|
|
111
|
-
.orderBy(desc(agentSessions.updatedAt))
|
|
112
|
-
.limit(opts?.limit ?? 50);
|
|
113
|
-
return rows.map(rowToPersistedSession);
|
|
114
|
-
}
|
|
115
|
-
async delete(sessionId) {
|
|
116
|
-
this.ensureDb();
|
|
117
|
-
const result = await this.db
|
|
118
|
-
.delete(agentSessions)
|
|
119
|
-
.where(eq(agentSessions.id, sessionId))
|
|
120
|
-
.returning({ id: agentSessions.id });
|
|
121
|
-
return result.length > 0;
|
|
40
|
+
}
|
|
41
|
+
/** Encode a cursor as opaque base64 of "updatedAt.ms|id". */
|
|
42
|
+
export function encodeCursor(updatedAt, id) {
|
|
43
|
+
return Buffer.from(`${updatedAt.getTime()}|${id}`, 'utf8').toString('base64url');
|
|
44
|
+
}
|
|
45
|
+
/** Decode a cursor; throws SessionStoreError on malformed input. */
|
|
46
|
+
export function decodeCursor(backend, cursor) {
|
|
47
|
+
try {
|
|
48
|
+
const decoded = Buffer.from(cursor, 'base64url').toString('utf8');
|
|
49
|
+
const sep = decoded.indexOf('|');
|
|
50
|
+
if (sep < 0)
|
|
51
|
+
throw new Error('missing separator');
|
|
52
|
+
const ms = Number(decoded.slice(0, sep));
|
|
53
|
+
const id = decoded.slice(sep + 1);
|
|
54
|
+
if (!Number.isFinite(ms) || !id)
|
|
55
|
+
throw new Error('bad parts');
|
|
56
|
+
return { updatedAt: new Date(ms), id };
|
|
122
57
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (result.length > 0) {
|
|
130
|
-
this.logger.info('session_store_cleanup', { deleted: result.length });
|
|
131
|
-
}
|
|
132
|
-
return result.length;
|
|
58
|
+
catch (cause) {
|
|
59
|
+
throw new SessionStoreError('Invalid pagination cursor', {
|
|
60
|
+
backend,
|
|
61
|
+
operation: 'list',
|
|
62
|
+
cause,
|
|
63
|
+
});
|
|
133
64
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Map a `PersistedSession` to its row shape for insert/update. Separate
|
|
68
|
+
* function so both backends do the exact same type conversion.
|
|
69
|
+
*/
|
|
70
|
+
export function sessionToRow(session) {
|
|
71
|
+
return {
|
|
72
|
+
id: session.id,
|
|
73
|
+
messages: session.messages,
|
|
74
|
+
tokenUsage: session.tokenUsage,
|
|
75
|
+
metadata: session.metadata,
|
|
76
|
+
version: session.version,
|
|
77
|
+
createdAt: session.createdAt,
|
|
78
|
+
updatedAt: session.updatedAt,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Inverse of `sessionToRow`.
|
|
83
|
+
*
|
|
84
|
+
* Validates at the JSONB boundary: rejects rows with an unknown
|
|
85
|
+
* `version` (future schema migration signal) or a non-array `messages`
|
|
86
|
+
* payload (would indicate a malformed write from an older runtime or
|
|
87
|
+
* manual DB edit). Throws `SessionStoreError` with context when a row
|
|
88
|
+
* fails validation rather than returning a quietly-broken session
|
|
89
|
+
* that crashes deep in the agent loop.
|
|
90
|
+
*/
|
|
91
|
+
export function rowToPersistedSession(backend, row) {
|
|
92
|
+
if (row.version !== 1) {
|
|
93
|
+
throw new SessionStoreError(`Unsupported persisted session version: ${row.version}`, {
|
|
94
|
+
backend,
|
|
95
|
+
operation: 'load',
|
|
96
|
+
context: { sessionId: row.id, version: row.version, supported: 1 },
|
|
97
|
+
});
|
|
140
98
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
99
|
+
if (!Array.isArray(row.messages)) {
|
|
100
|
+
throw new SessionStoreError('Persisted session has non-array messages payload', {
|
|
101
|
+
backend,
|
|
102
|
+
operation: 'load',
|
|
103
|
+
context: { sessionId: row.id, messagesType: typeof row.messages },
|
|
104
|
+
});
|
|
148
105
|
}
|
|
149
|
-
}
|
|
150
|
-
// ---------------------------------------------------------------------------
|
|
151
|
-
// Row conversion
|
|
152
|
-
// ---------------------------------------------------------------------------
|
|
153
|
-
function rowToPersistedSession(row) {
|
|
154
106
|
return {
|
|
155
107
|
version: 1,
|
|
156
108
|
id: row.id,
|
|
157
|
-
|
|
158
|
-
userId: row.userId,
|
|
159
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- JSONB boundary: we wrote these values
|
|
109
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- JSONB boundary: array-shape checked above; element shape is not validated
|
|
160
110
|
messages: row.messages,
|
|
161
111
|
tokenUsage: row.tokenUsage,
|
|
162
112
|
metadata: (row.metadata ?? {}),
|
|
@@ -164,4 +114,42 @@ function rowToPersistedSession(row) {
|
|
|
164
114
|
updatedAt: row.updatedAt,
|
|
165
115
|
};
|
|
166
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Build the where-clause fragments for `list()`. Generic over the
|
|
119
|
+
* table's column bindings so both PGLite (static `agentSessions`) and
|
|
120
|
+
* Postgres (dynamic `makeAgentSessionsTable(name)`) pass their own
|
|
121
|
+
* tables without a cast.
|
|
122
|
+
*
|
|
123
|
+
* Returns `undefined` when `opts` yields no conditions — drizzle's
|
|
124
|
+
* `.where(undefined)` is a no-op, equivalent to "no filter".
|
|
125
|
+
*/
|
|
126
|
+
export function buildListConditions(backend, opts, table) {
|
|
127
|
+
const conditions = [];
|
|
128
|
+
if (opts?.cursor) {
|
|
129
|
+
const { updatedAt: cursorTs, id: cursorId } = decodeCursor(backend, opts.cursor);
|
|
130
|
+
// Compound (updated_at, id) comparison makes pagination stable even
|
|
131
|
+
// when multiple rows share the same `updated_at` (batch inserts in
|
|
132
|
+
// the same ms, clock-skewed sources). Matches the secondary sort
|
|
133
|
+
// key on `id` used by list() so every row is visited exactly once.
|
|
134
|
+
const cursorCondition = or(lt(table.updatedAt, cursorTs), and(eq(table.updatedAt, cursorTs), lt(table.id, cursorId)));
|
|
135
|
+
if (cursorCondition)
|
|
136
|
+
conditions.push(cursorCondition);
|
|
137
|
+
}
|
|
138
|
+
if (opts?.updatedAfter)
|
|
139
|
+
conditions.push(gte(table.updatedAt, opts.updatedAfter));
|
|
140
|
+
if (opts?.updatedBefore)
|
|
141
|
+
conditions.push(lte(table.updatedAt, opts.updatedBefore));
|
|
142
|
+
if (opts?.filter) {
|
|
143
|
+
for (const [key, val] of Object.entries(opts.filter)) {
|
|
144
|
+
validateFilterKey(backend, key);
|
|
145
|
+
// metadata->>'key' = value (string equality). JSONB `->>` returns
|
|
146
|
+
// text; we coerce the user-supplied value to its JSON string form
|
|
147
|
+
// via parameter binding, so non-string values like numbers/bools
|
|
148
|
+
// compare correctly: metadata->>'n' = '42'.
|
|
149
|
+
const textVal = typeof val === 'string' ? val : JSON.stringify(val);
|
|
150
|
+
conditions.push(sql `${table.metadata}->>${sql.raw(`'${key}'`)} = ${textVal}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return and(...conditions);
|
|
154
|
+
}
|
|
167
155
|
//# sourceMappingURL=store.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../../src/session/store.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../../src/session/store.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAC,GAAG,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAC,MAAM,aAAa,CAAC;AAM3D,OAAO,EAAC,iBAAiB,EAAC,MAAM,cAAc,CAAC;AA4G/C,8EAA8E;AAC9E,oEAAoE;AACpE,8EAA8E;AAE9E;;;;;GAKG;AACH,MAAM,WAAW,GAAG,0BAA0B,CAAC;AAE/C,MAAM,UAAU,iBAAiB,CAAC,OAAe,EAAE,GAAW;IAC5D,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QAC3B,MAAM,IAAI,iBAAiB,CACzB,8BAA8B,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,EACnD;YACE,OAAO;YACP,SAAS,EAAE,MAAM;YACjB,OAAO,EAAE,EAAC,GAAG,EAAE,QAAQ,EAAE,8CAA8C,EAAC;SACzE,CACF,CAAC;IACJ,CAAC;AACH,CAAC;AAED,6DAA6D;AAC7D,MAAM,UAAU,YAAY,CAAC,SAAe,EAAE,EAAU;IACtD,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AACnF,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,YAAY,CAC1B,OAAe,EACf,MAAc;IAEd,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAClE,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,GAAG,GAAG,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;QAClD,MAAM,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;QACzC,MAAM,EAAE,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;QAClC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,WAAW,CAAC,CAAC;QAC9D,OAAO,EAAC,SAAS,EAAE,IAAI,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,EAAC,CAAC;IACvC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,iBAAiB,CAAC,2BAA2B,EAAE;YACvD,OAAO;YACP,SAAS,EAAE,MAAM;YACjB,KAAK;SACN,CAAC,CAAC;IACL,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,OAAyB;IASpD,OAAO;QACL,EAAE,EAAE,OAAO,CAAC,EAAE;QACd,QAAQ,EAAE,OAAO,CAAC,QAAqB;QACvC,UAAU,EAAE,OAAO,CAAC,UAInB;QACD,QAAQ,EAAE,OAAO,CAAC,QAAmC;QACrD,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,SAAS,EAAE,OAAO,CAAC,SAAS;KAC7B,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,qBAAqB,CACnC,OAAe,EACf,GAAsC;IAEtC,IAAI,GAAG,CAAC,OAAO,KAAK,CAAC,EAAE,CAAC;QACtB,MAAM,IAAI,iBAAiB,CACzB,0CAA0C,GAAG,CAAC,OAAO,EAAE,EACvD;YACE,OAAO;YACP,SAAS,EAAE,MAAM;YACjB,OAAO,EAAE,EAAC,SAAS,EAAE,GAAG,CAAC,EAAE,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,SAAS,EAAE,CAAC,EAAC;SACjE,CACF,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;QACjC,MAAM,IAAI,iBAAiB,CAAC,kDAAkD,EAAE;YAC9E,OAAO;YACP,SAAS,EAAE,MAAM;YACjB,OAAO,EAAE,EAAC,SAAS,EAAE,GAAG,CAAC,EAAE,EAAE,YAAY,EAAE,OAAO,GAAG,CAAC,QAAQ,EAAC;SAChE,CAAC,CAAC;IACL,CAAC;IACD,OAAO;QACL,OAAO,EAAE,CAAC;QACV,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,oJAAoJ;QACpJ,QAAQ,EAAE,GAAG,CAAC,QAA0B;QAExC,UAAU,EAAE,GAAG,CAAC,UAAwB;QAExC,QAAQ,EAAE,CAAC,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAoB;QACjD,SAAS,EAAE,GAAG,CAAC,SAAS;QACxB,SAAS,EAAE,GAAG,CAAC,SAAS;KACzB,CAAC;AACJ,CAAC;AAcD;;;;;;;;GAQG;AACH,MAAM,UAAU,mBAAmB,CACjC,OAAe,EACf,IAAoC,EACpC,KAA2B;IAE3B,MAAM,UAAU,GAAiC,EAAE,CAAC;IAEpD,IAAI,IAAI,EAAE,MAAM,EAAE,CAAC;QACjB,MAAM,EAAC,SAAS,EAAE,QAAQ,EAAE,EAAE,EAAE,QAAQ,EAAC,GAAG,YAAY,CAAC,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;QAC/E,oEAAoE;QACpE,mEAAmE;QACnE,iEAAiE;QACjE,mEAAmE;QACnE,MAAM,eAAe,GAAG,EAAE,CACxB,EAAE,CAAC,KAAK,CAAC,SAAS,EAAE,QAAQ,CAAC,EAC7B,GAAG,CAAC,EAAE,CAAC,KAAK,CAAC,SAAS,EAAE,QAAQ,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC,CAC3D,CAAC;QACF,IAAI,eAAe;YAAE,UAAU,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;IACxD,CAAC;IAED,IAAI,IAAI,EAAE,YAAY;QAAE,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC;IACjF,IAAI,IAAI,EAAE,aAAa;QAAE,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC;IAEnF,IAAI,IAAI,EAAE,MAAM,EAAE,CAAC;QACjB,KAAK,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;YACrD,iBAAiB,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;YAChC,kEAAkE;YAClE,kEAAkE;YAClE,iEAAiE;YACjE,4CAA4C;YAC5C,MAAM,OAAO,GAAG,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;YACpE,UAAU,CAAC,IAAI,CACb,GAAG,CAAA,GAAG,KAAK,CAAC,QAAQ,MAAM,GAAG,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,OAAO,EAAE,CAC7D,CAAC;QACJ,CAAC;IACH,CAAC;IAED,OAAO,GAAG,CAAC,GAAG,UAAU,CAAC,CAAC;AAC5B,CAAC"}
|