@agentuity/runtime 0.0.43 → 0.0.45
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/AGENTS.md +11 -9
- package/README.md +4 -4
- package/dist/_context.d.ts +12 -4
- package/dist/_context.d.ts.map +1 -1
- package/dist/_server.d.ts +7 -4
- package/dist/_server.d.ts.map +1 -1
- package/dist/_services.d.ts +13 -2
- package/dist/_services.d.ts.map +1 -1
- package/dist/_util.d.ts +1 -1
- package/dist/_util.d.ts.map +1 -1
- package/dist/_waituntil.d.ts +1 -3
- package/dist/_waituntil.d.ts.map +1 -1
- package/dist/agent.d.ts +41 -14
- package/dist/agent.d.ts.map +1 -1
- package/dist/app.d.ts +90 -8
- package/dist/app.d.ts.map +1 -1
- package/dist/eval.d.ts +79 -0
- package/dist/eval.d.ts.map +1 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/io/email.d.ts +77 -0
- package/dist/io/email.d.ts.map +1 -0
- package/dist/logger/console.d.ts +7 -1
- package/dist/logger/console.d.ts.map +1 -1
- package/dist/logger/user.d.ts.map +1 -1
- package/dist/otel/config.d.ts +3 -1
- package/dist/otel/config.d.ts.map +1 -1
- package/dist/otel/console.d.ts +2 -1
- package/dist/otel/console.d.ts.map +1 -1
- package/dist/otel/exporters/index.d.ts +4 -0
- package/dist/otel/exporters/index.d.ts.map +1 -0
- package/dist/otel/exporters/jsonl-log-exporter.d.ts +36 -0
- package/dist/otel/exporters/jsonl-log-exporter.d.ts.map +1 -0
- package/dist/otel/exporters/jsonl-metric-exporter.d.ts +40 -0
- package/dist/otel/exporters/jsonl-metric-exporter.d.ts.map +1 -0
- package/dist/otel/exporters/jsonl-trace-exporter.d.ts +36 -0
- package/dist/otel/exporters/jsonl-trace-exporter.d.ts.map +1 -0
- package/dist/otel/http.d.ts.map +1 -1
- package/dist/otel/logger.d.ts +8 -6
- package/dist/otel/logger.d.ts.map +1 -1
- package/dist/otel/otel.d.ts +8 -2
- package/dist/otel/otel.d.ts.map +1 -1
- package/dist/router.d.ts +4 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/services/evalrun/composite.d.ts +21 -0
- package/dist/services/evalrun/composite.d.ts.map +1 -0
- package/dist/services/evalrun/http.d.ts +24 -0
- package/dist/services/evalrun/http.d.ts.map +1 -0
- package/dist/services/evalrun/index.d.ts +5 -0
- package/dist/services/evalrun/index.d.ts.map +1 -0
- package/dist/services/evalrun/json.d.ts +21 -0
- package/dist/services/evalrun/json.d.ts.map +1 -0
- package/dist/services/evalrun/local.d.ts +19 -0
- package/dist/services/evalrun/local.d.ts.map +1 -0
- package/dist/services/local/_db.d.ts +4 -0
- package/dist/services/local/_db.d.ts.map +1 -0
- package/dist/services/local/_router.d.ts +3 -0
- package/dist/services/local/_router.d.ts.map +1 -0
- package/dist/services/local/_util.d.ts +18 -0
- package/dist/services/local/_util.d.ts.map +1 -0
- package/dist/services/local/index.d.ts +8 -0
- package/dist/services/local/index.d.ts.map +1 -0
- package/dist/services/local/keyvalue.d.ts +10 -0
- package/dist/services/local/keyvalue.d.ts.map +1 -0
- package/dist/services/local/objectstore.d.ts +11 -0
- package/dist/services/local/objectstore.d.ts.map +1 -0
- package/dist/services/local/stream.d.ts +10 -0
- package/dist/services/local/stream.d.ts.map +1 -0
- package/dist/services/local/vector.d.ts +13 -0
- package/dist/services/local/vector.d.ts.map +1 -0
- package/dist/services/session/composite.d.ts +21 -0
- package/dist/services/session/composite.d.ts.map +1 -0
- package/dist/services/session/http.d.ts +23 -0
- package/dist/services/session/http.d.ts.map +1 -0
- package/dist/services/session/index.d.ts +5 -0
- package/dist/services/session/index.d.ts.map +1 -0
- package/dist/services/session/json.d.ts +22 -0
- package/dist/services/session/json.d.ts.map +1 -0
- package/dist/services/session/local.d.ts +19 -0
- package/dist/services/session/local.d.ts.map +1 -0
- package/dist/session.d.ts +70 -0
- package/dist/session.d.ts.map +1 -0
- package/package.json +10 -6
- package/src/_config.ts +1 -1
- package/src/_context.ts +19 -16
- package/src/_server.ts +284 -42
- package/src/_services.ts +147 -34
- package/src/_util.ts +2 -3
- package/src/_waituntil.ts +5 -153
- package/src/agent.ts +667 -65
- package/src/app.ts +159 -13
- package/src/eval.ts +95 -0
- package/src/index.ts +6 -1
- package/src/io/email.ts +173 -0
- package/src/logger/console.ts +196 -17
- package/src/logger/user.ts +7 -3
- package/src/otel/config.ts +7 -44
- package/src/otel/console.ts +8 -4
- package/src/otel/exporters/README.md +217 -0
- package/src/otel/exporters/index.ts +3 -0
- package/src/otel/exporters/jsonl-log-exporter.ts +113 -0
- package/src/otel/exporters/jsonl-metric-exporter.ts +120 -0
- package/src/otel/exporters/jsonl-trace-exporter.ts +121 -0
- package/src/otel/http.ts +3 -1
- package/src/otel/logger.ts +87 -37
- package/src/otel/otel.ts +43 -22
- package/src/router.ts +44 -4
- package/src/services/evalrun/composite.ts +34 -0
- package/src/services/evalrun/http.ts +112 -0
- package/src/services/evalrun/index.ts +4 -0
- package/src/services/evalrun/json.ts +46 -0
- package/src/services/evalrun/local.ts +28 -0
- package/src/services/local/README.md +1576 -0
- package/src/services/local/_db.ts +182 -0
- package/src/services/local/_router.ts +86 -0
- package/src/services/local/_util.ts +49 -0
- package/src/services/local/index.ts +7 -0
- package/src/services/local/keyvalue.ts +118 -0
- package/src/services/local/objectstore.ts +152 -0
- package/src/services/local/stream.ts +296 -0
- package/src/services/local/vector.ts +264 -0
- package/src/services/session/composite.ts +33 -0
- package/src/services/session/http.ts +64 -0
- package/src/services/session/index.ts +4 -0
- package/src/services/session/json.ts +42 -0
- package/src/services/session/local.ts +28 -0
- package/src/session.ts +284 -0
- package/dist/_unauthenticated.d.ts +0 -26
- package/dist/_unauthenticated.d.ts.map +0 -1
- package/src/_unauthenticated.ts +0 -126
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { mkdirSync, existsSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
let dbInstance: Database | null = null;
|
|
7
|
+
|
|
8
|
+
export function getLocalDB(): Database {
|
|
9
|
+
if (dbInstance) {
|
|
10
|
+
return dbInstance;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const configDir = join(homedir(), '.config', 'agentuity');
|
|
14
|
+
|
|
15
|
+
if (!existsSync(configDir)) {
|
|
16
|
+
mkdirSync(configDir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const dbPath = join(configDir, 'local.db');
|
|
20
|
+
dbInstance = new Database(dbPath);
|
|
21
|
+
|
|
22
|
+
initializeTables(dbInstance);
|
|
23
|
+
cleanupOrphanedProjects(dbInstance);
|
|
24
|
+
|
|
25
|
+
return dbInstance;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function initializeTables(db: Database): void {
|
|
29
|
+
// KeyValue Storage table
|
|
30
|
+
db.run(`
|
|
31
|
+
CREATE TABLE IF NOT EXISTS kv_storage (
|
|
32
|
+
project_path TEXT NOT NULL,
|
|
33
|
+
name TEXT NOT NULL,
|
|
34
|
+
key TEXT NOT NULL,
|
|
35
|
+
value BLOB NOT NULL,
|
|
36
|
+
content_type TEXT NOT NULL DEFAULT 'application/octet-stream',
|
|
37
|
+
expires_at INTEGER,
|
|
38
|
+
created_at INTEGER NOT NULL,
|
|
39
|
+
updated_at INTEGER NOT NULL,
|
|
40
|
+
PRIMARY KEY (project_path, name, key)
|
|
41
|
+
)
|
|
42
|
+
`);
|
|
43
|
+
|
|
44
|
+
db.run(`
|
|
45
|
+
CREATE INDEX IF NOT EXISTS idx_kv_expires
|
|
46
|
+
ON kv_storage(expires_at)
|
|
47
|
+
WHERE expires_at IS NOT NULL
|
|
48
|
+
`);
|
|
49
|
+
|
|
50
|
+
// Object Storage table
|
|
51
|
+
db.run(`
|
|
52
|
+
CREATE TABLE IF NOT EXISTS object_storage (
|
|
53
|
+
project_path TEXT NOT NULL,
|
|
54
|
+
bucket TEXT NOT NULL,
|
|
55
|
+
key TEXT NOT NULL,
|
|
56
|
+
data BLOB NOT NULL,
|
|
57
|
+
content_type TEXT NOT NULL DEFAULT 'application/octet-stream',
|
|
58
|
+
content_encoding TEXT,
|
|
59
|
+
cache_control TEXT,
|
|
60
|
+
content_disposition TEXT,
|
|
61
|
+
content_language TEXT,
|
|
62
|
+
metadata TEXT,
|
|
63
|
+
created_at INTEGER NOT NULL,
|
|
64
|
+
updated_at INTEGER NOT NULL,
|
|
65
|
+
PRIMARY KEY (project_path, bucket, key)
|
|
66
|
+
)
|
|
67
|
+
`);
|
|
68
|
+
|
|
69
|
+
// Stream Storage table
|
|
70
|
+
db.run(`
|
|
71
|
+
CREATE TABLE IF NOT EXISTS stream_storage (
|
|
72
|
+
project_path TEXT NOT NULL,
|
|
73
|
+
id TEXT PRIMARY KEY,
|
|
74
|
+
name TEXT NOT NULL,
|
|
75
|
+
metadata TEXT,
|
|
76
|
+
content_type TEXT NOT NULL DEFAULT 'application/octet-stream',
|
|
77
|
+
data BLOB,
|
|
78
|
+
size_bytes INTEGER NOT NULL DEFAULT 0,
|
|
79
|
+
created_at INTEGER NOT NULL
|
|
80
|
+
)
|
|
81
|
+
`);
|
|
82
|
+
|
|
83
|
+
db.run(`
|
|
84
|
+
CREATE INDEX IF NOT EXISTS idx_stream_name
|
|
85
|
+
ON stream_storage(project_path, name)
|
|
86
|
+
`);
|
|
87
|
+
|
|
88
|
+
db.run(`
|
|
89
|
+
CREATE INDEX IF NOT EXISTS idx_stream_metadata
|
|
90
|
+
ON stream_storage(metadata)
|
|
91
|
+
`);
|
|
92
|
+
|
|
93
|
+
// Vector Storage table
|
|
94
|
+
db.run(`
|
|
95
|
+
CREATE TABLE IF NOT EXISTS vector_storage (
|
|
96
|
+
project_path TEXT NOT NULL,
|
|
97
|
+
name TEXT NOT NULL,
|
|
98
|
+
id TEXT PRIMARY KEY,
|
|
99
|
+
key TEXT NOT NULL,
|
|
100
|
+
embedding TEXT NOT NULL,
|
|
101
|
+
document TEXT,
|
|
102
|
+
metadata TEXT,
|
|
103
|
+
created_at INTEGER NOT NULL,
|
|
104
|
+
updated_at INTEGER NOT NULL,
|
|
105
|
+
UNIQUE (project_path, name, key)
|
|
106
|
+
)
|
|
107
|
+
`);
|
|
108
|
+
|
|
109
|
+
db.run(`
|
|
110
|
+
CREATE INDEX IF NOT EXISTS idx_vector_lookup
|
|
111
|
+
ON vector_storage(project_path, name, key)
|
|
112
|
+
`);
|
|
113
|
+
|
|
114
|
+
db.run(`
|
|
115
|
+
CREATE INDEX IF NOT EXISTS idx_vector_name
|
|
116
|
+
ON vector_storage(project_path, name)
|
|
117
|
+
`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function cleanupOrphanedProjects(db: Database): void {
|
|
121
|
+
// Get the current project path to exclude from cleanup
|
|
122
|
+
const currentProjectPath = process.cwd();
|
|
123
|
+
|
|
124
|
+
// Query all tables for unique project paths
|
|
125
|
+
const kvPaths = db.query('SELECT DISTINCT project_path FROM kv_storage').all() as Array<{
|
|
126
|
+
project_path: string;
|
|
127
|
+
}>;
|
|
128
|
+
const objectPaths = db.query('SELECT DISTINCT project_path FROM object_storage').all() as Array<{
|
|
129
|
+
project_path: string;
|
|
130
|
+
}>;
|
|
131
|
+
const streamPaths = db.query('SELECT DISTINCT project_path FROM stream_storage').all() as Array<{
|
|
132
|
+
project_path: string;
|
|
133
|
+
}>;
|
|
134
|
+
const vectorPaths = db.query('SELECT DISTINCT project_path FROM vector_storage').all() as Array<{
|
|
135
|
+
project_path: string;
|
|
136
|
+
}>;
|
|
137
|
+
|
|
138
|
+
// Combine and deduplicate all project paths
|
|
139
|
+
const allPaths = new Set<string>();
|
|
140
|
+
[...kvPaths, ...objectPaths, ...streamPaths, ...vectorPaths].forEach((row) => {
|
|
141
|
+
allPaths.add(row.project_path);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Check which paths no longer exist and are not the current project
|
|
145
|
+
const pathsToDelete: string[] = [];
|
|
146
|
+
for (const path of allPaths) {
|
|
147
|
+
if (path !== currentProjectPath && !existsSync(path)) {
|
|
148
|
+
pathsToDelete.push(path);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Delete data for removed projects
|
|
153
|
+
if (pathsToDelete.length > 0) {
|
|
154
|
+
const placeholders = pathsToDelete.map(() => '?').join(', ');
|
|
155
|
+
|
|
156
|
+
// Delete from all tables
|
|
157
|
+
const deleteKv = db.prepare(`DELETE FROM kv_storage WHERE project_path IN (${placeholders})`);
|
|
158
|
+
const deleteObject = db.prepare(
|
|
159
|
+
`DELETE FROM object_storage WHERE project_path IN (${placeholders})`
|
|
160
|
+
);
|
|
161
|
+
const deleteStream = db.prepare(
|
|
162
|
+
`DELETE FROM stream_storage WHERE project_path IN (${placeholders})`
|
|
163
|
+
);
|
|
164
|
+
const deleteVector = db.prepare(
|
|
165
|
+
`DELETE FROM vector_storage WHERE project_path IN (${placeholders})`
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
deleteKv.run(...pathsToDelete);
|
|
169
|
+
deleteObject.run(...pathsToDelete);
|
|
170
|
+
deleteStream.run(...pathsToDelete);
|
|
171
|
+
deleteVector.run(...pathsToDelete);
|
|
172
|
+
|
|
173
|
+
console.log(`[LocalDB] Cleaned up data for ${pathsToDelete.length} orphaned project(s)`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function closeLocalDB(): void {
|
|
178
|
+
if (dbInstance) {
|
|
179
|
+
dbInstance.close();
|
|
180
|
+
dbInstance = null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
import { createRouter } from '../../router';
|
|
3
|
+
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5
|
+
export function createLocalStorageRouter(db: Database, projectPath: string): any {
|
|
6
|
+
const router = createRouter();
|
|
7
|
+
|
|
8
|
+
// so we can detect if we're running in local mode easily
|
|
9
|
+
router.get('/_agentuity/local/health', (c) => c.text('OK'));
|
|
10
|
+
|
|
11
|
+
// Serve objects: GET /_agentuity/local/object/:bucket/:key
|
|
12
|
+
router.get('/_agentuity/local/object/:bucket/:key', async (c) => {
|
|
13
|
+
const bucket = c.req.param('bucket');
|
|
14
|
+
const key = c.req.param('key');
|
|
15
|
+
|
|
16
|
+
const query = db.query(`
|
|
17
|
+
SELECT data, content_type, content_encoding, cache_control,
|
|
18
|
+
content_disposition, content_language
|
|
19
|
+
FROM object_storage
|
|
20
|
+
WHERE project_path = ? AND bucket = ? AND key = ?
|
|
21
|
+
`);
|
|
22
|
+
|
|
23
|
+
const row = query.get(projectPath, bucket, key) as {
|
|
24
|
+
data: Buffer;
|
|
25
|
+
content_type: string;
|
|
26
|
+
content_encoding: string | null;
|
|
27
|
+
cache_control: string | null;
|
|
28
|
+
content_disposition: string | null;
|
|
29
|
+
content_language: string | null;
|
|
30
|
+
} | null;
|
|
31
|
+
|
|
32
|
+
if (!row) {
|
|
33
|
+
return c.notFound();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Set headers
|
|
37
|
+
const headers: Record<string, string> = {
|
|
38
|
+
'Content-Type': row.content_type,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
if (row.content_encoding) {
|
|
42
|
+
headers['Content-Encoding'] = row.content_encoding;
|
|
43
|
+
}
|
|
44
|
+
if (row.cache_control) {
|
|
45
|
+
headers['Cache-Control'] = row.cache_control;
|
|
46
|
+
}
|
|
47
|
+
if (row.content_disposition) {
|
|
48
|
+
headers['Content-Disposition'] = row.content_disposition;
|
|
49
|
+
}
|
|
50
|
+
if (row.content_language) {
|
|
51
|
+
headers['Content-Language'] = row.content_language;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return c.body(new Uint8Array(row.data), 200, headers);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Serve streams: GET /_agentuity/local/stream/:id
|
|
58
|
+
router.get('/_agentuity/local/stream/:id', async (c) => {
|
|
59
|
+
const id = c.req.param('id');
|
|
60
|
+
|
|
61
|
+
const query = db.query(`
|
|
62
|
+
SELECT data, content_type
|
|
63
|
+
FROM stream_storage
|
|
64
|
+
WHERE project_path = ? AND id = ?
|
|
65
|
+
`);
|
|
66
|
+
|
|
67
|
+
const row = query.get(projectPath, id) as {
|
|
68
|
+
data: Buffer | null;
|
|
69
|
+
content_type: string;
|
|
70
|
+
} | null;
|
|
71
|
+
|
|
72
|
+
if (!row) {
|
|
73
|
+
return c.notFound();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!row.data) {
|
|
77
|
+
return c.json({ error: 'Stream not finalized' }, 400);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return c.body(new Uint8Array(row.data), 200, {
|
|
81
|
+
'Content-Type': row.content_type,
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return router;
|
|
86
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Normalize a project path to an absolute path for consistent DB keys
|
|
5
|
+
*/
|
|
6
|
+
export function normalizeProjectPath(cwd: string = process.cwd()): string {
|
|
7
|
+
return resolve(cwd);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Simple character-based embedding for local vector search
|
|
12
|
+
* Not production-quality, but good enough for local dev/testing
|
|
13
|
+
*/
|
|
14
|
+
export function simpleEmbedding(text: string, dimensions = 128): number[] {
|
|
15
|
+
const vec = new Array(dimensions).fill(0);
|
|
16
|
+
const normalized = text.toLowerCase();
|
|
17
|
+
|
|
18
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
19
|
+
const charCode = normalized.charCodeAt(i);
|
|
20
|
+
vec[i % dimensions] += Math.sin(charCode * (i + 1));
|
|
21
|
+
vec[(i * 2) % dimensions] += Math.cos(charCode);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Normalize vector
|
|
25
|
+
const magnitude = Math.sqrt(vec.reduce((sum, v) => sum + v * v, 0));
|
|
26
|
+
return magnitude > 0 ? vec.map((v) => v / magnitude) : vec;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Calculate cosine similarity between two vectors
|
|
31
|
+
*/
|
|
32
|
+
export function cosineSimilarity(a: number[], b: number[]): number {
|
|
33
|
+
if (a.length !== b.length) {
|
|
34
|
+
throw new Error('Vectors must have the same dimension');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const dot = a.reduce((sum, ai, i) => sum + ai * (b[i] ?? 0), 0);
|
|
38
|
+
const normA = Math.sqrt(a.reduce((sum, ai) => sum + ai * ai, 0));
|
|
39
|
+
const normB = Math.sqrt(b.reduce((sum, bi) => sum + bi * bi, 0));
|
|
40
|
+
|
|
41
|
+
return normA > 0 && normB > 0 ? dot / (normA * normB) : 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get current timestamp in milliseconds
|
|
46
|
+
*/
|
|
47
|
+
export function now(): number {
|
|
48
|
+
return Date.now();
|
|
49
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { getLocalDB, closeLocalDB } from './_db';
|
|
2
|
+
export { normalizeProjectPath, simpleEmbedding, cosineSimilarity } from './_util';
|
|
3
|
+
export { createLocalStorageRouter } from './_router';
|
|
4
|
+
export { LocalKeyValueStorage } from './keyvalue';
|
|
5
|
+
export { LocalObjectStorage } from './objectstore';
|
|
6
|
+
export { LocalStreamStorage } from './stream';
|
|
7
|
+
export { LocalVectorStorage } from './vector';
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
import type {
|
|
3
|
+
KeyValueStorage,
|
|
4
|
+
DataResult,
|
|
5
|
+
DataResultNotFound,
|
|
6
|
+
KeyValueStorageSetParams,
|
|
7
|
+
} from '@agentuity/core';
|
|
8
|
+
import { now } from './_util';
|
|
9
|
+
|
|
10
|
+
export class LocalKeyValueStorage implements KeyValueStorage {
|
|
11
|
+
#db: Database;
|
|
12
|
+
#projectPath: string;
|
|
13
|
+
|
|
14
|
+
constructor(db: Database, projectPath: string) {
|
|
15
|
+
this.#db = db;
|
|
16
|
+
this.#projectPath = projectPath;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async get<T>(name: string, key: string): Promise<DataResult<T>> {
|
|
20
|
+
const query = this.#db.query(`
|
|
21
|
+
SELECT value, content_type, expires_at
|
|
22
|
+
FROM kv_storage
|
|
23
|
+
WHERE project_path = ? AND name = ? AND key = ?
|
|
24
|
+
`);
|
|
25
|
+
|
|
26
|
+
const row = query.get(this.#projectPath, name, key) as {
|
|
27
|
+
value: Buffer;
|
|
28
|
+
content_type: string;
|
|
29
|
+
expires_at: number | null;
|
|
30
|
+
} | null;
|
|
31
|
+
|
|
32
|
+
if (!row) {
|
|
33
|
+
return { exists: false } as DataResultNotFound;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check expiration
|
|
37
|
+
if (row.expires_at && row.expires_at < now()) {
|
|
38
|
+
// Delete expired row
|
|
39
|
+
await this.delete(name, key);
|
|
40
|
+
return { exists: false } as DataResultNotFound;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Deserialize based on content type
|
|
44
|
+
let data: T;
|
|
45
|
+
if (row.content_type === 'application/json') {
|
|
46
|
+
data = JSON.parse(row.value.toString('utf-8'));
|
|
47
|
+
} else if (row.content_type.startsWith('text/')) {
|
|
48
|
+
data = row.value.toString('utf-8') as T;
|
|
49
|
+
} else {
|
|
50
|
+
data = new Uint8Array(row.value) as T;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
data,
|
|
55
|
+
contentType: row.content_type,
|
|
56
|
+
exists: true,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async set<T = unknown>(
|
|
61
|
+
name: string,
|
|
62
|
+
key: string,
|
|
63
|
+
value: T,
|
|
64
|
+
params?: KeyValueStorageSetParams
|
|
65
|
+
): Promise<void> {
|
|
66
|
+
// Validate TTL
|
|
67
|
+
if (params?.ttl && params.ttl < 60) {
|
|
68
|
+
throw new Error(`ttl must be at least 60 seconds, got ${params.ttl}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Serialize value
|
|
72
|
+
let buffer: Buffer;
|
|
73
|
+
let contentType = params?.contentType || 'application/octet-stream';
|
|
74
|
+
|
|
75
|
+
if (typeof value === 'string') {
|
|
76
|
+
buffer = Buffer.from(value, 'utf-8');
|
|
77
|
+
if (!params?.contentType) {
|
|
78
|
+
contentType = 'text/plain';
|
|
79
|
+
}
|
|
80
|
+
} else if (value instanceof Uint8Array) {
|
|
81
|
+
buffer = Buffer.from(value);
|
|
82
|
+
} else if (value instanceof ArrayBuffer) {
|
|
83
|
+
buffer = Buffer.from(new Uint8Array(value));
|
|
84
|
+
} else if (typeof value === 'object') {
|
|
85
|
+
buffer = Buffer.from(JSON.stringify(value), 'utf-8');
|
|
86
|
+
contentType = 'application/json';
|
|
87
|
+
} else {
|
|
88
|
+
buffer = Buffer.from(String(value), 'utf-8');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Calculate expiration
|
|
92
|
+
const expiresAt = params?.ttl ? now() + params.ttl * 1000 : null;
|
|
93
|
+
const timestamp = now();
|
|
94
|
+
|
|
95
|
+
// UPSERT
|
|
96
|
+
const stmt = this.#db.prepare(`
|
|
97
|
+
INSERT INTO kv_storage (project_path, name, key, value, content_type, expires_at, created_at, updated_at)
|
|
98
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
99
|
+
ON CONFLICT(project_path, name, key)
|
|
100
|
+
DO UPDATE SET
|
|
101
|
+
value = excluded.value,
|
|
102
|
+
content_type = excluded.content_type,
|
|
103
|
+
expires_at = excluded.expires_at,
|
|
104
|
+
updated_at = excluded.updated_at
|
|
105
|
+
`);
|
|
106
|
+
|
|
107
|
+
stmt.run(this.#projectPath, name, key, buffer, contentType, expiresAt, timestamp, timestamp);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async delete(name: string, key: string): Promise<void> {
|
|
111
|
+
const stmt = this.#db.prepare(`
|
|
112
|
+
DELETE FROM kv_storage
|
|
113
|
+
WHERE project_path = ? AND name = ? AND key = ?
|
|
114
|
+
`);
|
|
115
|
+
|
|
116
|
+
stmt.run(this.#projectPath, name, key);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
import type {
|
|
3
|
+
ObjectStorage,
|
|
4
|
+
ObjectResult,
|
|
5
|
+
ObjectResultNotFound,
|
|
6
|
+
ObjectStorePutParams,
|
|
7
|
+
CreatePublicURLParams,
|
|
8
|
+
} from '@agentuity/core';
|
|
9
|
+
import { now } from './_util';
|
|
10
|
+
|
|
11
|
+
export class LocalObjectStorage implements ObjectStorage {
|
|
12
|
+
#db: Database;
|
|
13
|
+
#projectPath: string;
|
|
14
|
+
#serverUrl: string;
|
|
15
|
+
|
|
16
|
+
constructor(db: Database, projectPath: string, serverUrl: string) {
|
|
17
|
+
this.#db = db;
|
|
18
|
+
this.#projectPath = projectPath;
|
|
19
|
+
this.#serverUrl = serverUrl;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async get(bucket: string, key: string): Promise<ObjectResult> {
|
|
23
|
+
if (!bucket?.trim() || !key?.trim()) {
|
|
24
|
+
throw new Error('bucket and key are required');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const query = this.#db.query(`
|
|
28
|
+
SELECT data, content_type
|
|
29
|
+
FROM object_storage
|
|
30
|
+
WHERE project_path = ? AND bucket = ? AND key = ?
|
|
31
|
+
`);
|
|
32
|
+
|
|
33
|
+
const row = query.get(this.#projectPath, bucket, key) as {
|
|
34
|
+
data: Buffer;
|
|
35
|
+
content_type: string;
|
|
36
|
+
} | null;
|
|
37
|
+
|
|
38
|
+
if (!row) {
|
|
39
|
+
return { exists: false } as ObjectResultNotFound;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
exists: true,
|
|
44
|
+
data: new Uint8Array(row.data),
|
|
45
|
+
contentType: row.content_type,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async put(
|
|
50
|
+
bucket: string,
|
|
51
|
+
key: string,
|
|
52
|
+
data: Uint8Array | ArrayBuffer | ReadableStream,
|
|
53
|
+
params?: ObjectStorePutParams
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
if (!bucket?.trim() || !key?.trim()) {
|
|
56
|
+
throw new Error('bucket and key are required');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Convert data to Buffer
|
|
60
|
+
let buffer: Buffer;
|
|
61
|
+
if (data instanceof ReadableStream) {
|
|
62
|
+
// Read entire stream into buffer
|
|
63
|
+
const reader = data.getReader();
|
|
64
|
+
const chunks: Uint8Array[] = [];
|
|
65
|
+
while (true) {
|
|
66
|
+
const { done, value } = await reader.read();
|
|
67
|
+
if (done) break;
|
|
68
|
+
chunks.push(value);
|
|
69
|
+
}
|
|
70
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
71
|
+
buffer = Buffer.concat(
|
|
72
|
+
chunks.map((c) => Buffer.from(c)),
|
|
73
|
+
totalLength
|
|
74
|
+
);
|
|
75
|
+
} else if (data instanceof ArrayBuffer) {
|
|
76
|
+
buffer = Buffer.from(data);
|
|
77
|
+
} else {
|
|
78
|
+
buffer = Buffer.from(data);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const timestamp = now();
|
|
82
|
+
const metadata = params?.metadata ? JSON.stringify(params.metadata) : null;
|
|
83
|
+
|
|
84
|
+
const stmt = this.#db.prepare(`
|
|
85
|
+
INSERT INTO object_storage (
|
|
86
|
+
project_path, bucket, key, data, content_type,
|
|
87
|
+
content_encoding, cache_control, content_disposition,
|
|
88
|
+
content_language, metadata, created_at, updated_at
|
|
89
|
+
)
|
|
90
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
91
|
+
ON CONFLICT(project_path, bucket, key)
|
|
92
|
+
DO UPDATE SET
|
|
93
|
+
data = excluded.data,
|
|
94
|
+
content_type = excluded.content_type,
|
|
95
|
+
content_encoding = excluded.content_encoding,
|
|
96
|
+
cache_control = excluded.cache_control,
|
|
97
|
+
content_disposition = excluded.content_disposition,
|
|
98
|
+
content_language = excluded.content_language,
|
|
99
|
+
metadata = excluded.metadata,
|
|
100
|
+
updated_at = excluded.updated_at
|
|
101
|
+
`);
|
|
102
|
+
|
|
103
|
+
stmt.run(
|
|
104
|
+
this.#projectPath,
|
|
105
|
+
bucket,
|
|
106
|
+
key,
|
|
107
|
+
buffer,
|
|
108
|
+
params?.contentType || 'application/octet-stream',
|
|
109
|
+
params?.contentEncoding || null,
|
|
110
|
+
params?.cacheControl || null,
|
|
111
|
+
params?.contentDisposition || null,
|
|
112
|
+
params?.contentLanguage || null,
|
|
113
|
+
metadata,
|
|
114
|
+
timestamp,
|
|
115
|
+
timestamp
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async delete(bucket: string, key: string): Promise<boolean> {
|
|
120
|
+
if (!bucket?.trim() || !key?.trim()) {
|
|
121
|
+
throw new Error('bucket and key are required');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const stmt = this.#db.prepare(`
|
|
125
|
+
DELETE FROM object_storage
|
|
126
|
+
WHERE project_path = ? AND bucket = ? AND key = ?
|
|
127
|
+
`);
|
|
128
|
+
|
|
129
|
+
const result = stmt.run(this.#projectPath, bucket, key);
|
|
130
|
+
return result.changes > 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async createPublicURL(
|
|
134
|
+
bucket: string,
|
|
135
|
+
key: string,
|
|
136
|
+
_params?: CreatePublicURLParams
|
|
137
|
+
): Promise<string> {
|
|
138
|
+
if (!bucket?.trim() || !key?.trim()) {
|
|
139
|
+
throw new Error('bucket and key are required');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Verify object exists
|
|
143
|
+
const result = await this.get(bucket, key);
|
|
144
|
+
if (!result.exists) {
|
|
145
|
+
throw new Error('Object not found');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Return local HTTP URL
|
|
149
|
+
// Note: params.expiresDuration is ignored for local implementation
|
|
150
|
+
return `${this.#serverUrl}/_agentuity/local/object/${encodeURIComponent(bucket)}/${encodeURIComponent(key)}`;
|
|
151
|
+
}
|
|
152
|
+
}
|