@hexis-ai/engram-server 0.1.1 → 0.1.4
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/adapters/postgres.d.ts +3 -3
- package/dist/adapters/postgres.js +7 -9
- package/dist/logger.d.ts +26 -0
- package/dist/logger.js +33 -0
- package/dist/main.d.ts +5 -0
- package/dist/main.js +53 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +52 -4
- package/package.json +9 -6
|
@@ -8,6 +8,8 @@ import { type StorageAdapter } from "../storage";
|
|
|
8
8
|
*/
|
|
9
9
|
export interface SqlClient {
|
|
10
10
|
<T = Record<string, unknown>>(strings: TemplateStringsArray, ...values: unknown[]): Promise<T[]>;
|
|
11
|
+
/** Raw query for DDL / multi-statement strings. */
|
|
12
|
+
unsafe: (query: string) => Promise<unknown>;
|
|
11
13
|
}
|
|
12
14
|
export interface PostgresAdapterOptions {
|
|
13
15
|
/** Workspace scope baked into every row. Required for multi-tenant isolation. */
|
|
@@ -20,9 +22,7 @@ export declare class PostgresAdapter implements StorageAdapter {
|
|
|
20
22
|
constructor(opts: PostgresAdapterOptions);
|
|
21
23
|
/**
|
|
22
24
|
* Create the schema. Call once at boot. Safe to invoke repeatedly.
|
|
23
|
-
*
|
|
24
|
-
* `sql.unsafe()` in the real driver; for portability we issue them
|
|
25
|
-
* separately so any tagged-template impl works.
|
|
25
|
+
* Uses `sql.unsafe()` to ship the multi-statement DDL as a single batch.
|
|
26
26
|
*/
|
|
27
27
|
ensureSchema(): Promise<void>;
|
|
28
28
|
createSession(init: SessionInit & {
|
|
@@ -34,16 +34,10 @@ export class PostgresAdapter {
|
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
36
|
* Create the schema. Call once at boot. Safe to invoke repeatedly.
|
|
37
|
-
*
|
|
38
|
-
* `sql.unsafe()` in the real driver; for portability we issue them
|
|
39
|
-
* separately so any tagged-template impl works.
|
|
37
|
+
* Uses `sql.unsafe()` to ship the multi-statement DDL as a single batch.
|
|
40
38
|
*/
|
|
41
39
|
async ensureSchema() {
|
|
42
|
-
|
|
43
|
-
for (const stmt of stmts) {
|
|
44
|
-
// Tagged-template invocation with no interpolations.
|
|
45
|
-
await this.sql([stmt + ";"]);
|
|
46
|
-
}
|
|
40
|
+
await this.sql.unsafe(SCHEMA_SQL);
|
|
47
41
|
}
|
|
48
42
|
async createSession(init) {
|
|
49
43
|
await this.sql `
|
|
@@ -63,6 +57,10 @@ export class PostgresAdapter {
|
|
|
63
57
|
if (events.length === 0)
|
|
64
58
|
return;
|
|
65
59
|
for (const ev of events) {
|
|
60
|
+
// Pass the event object directly: postgres serializes JS objects as
|
|
61
|
+
// JSON for jsonb columns. Doing JSON.stringify ourselves then casting
|
|
62
|
+
// ::jsonb produced a doubly-encoded string value (jsonb containing
|
|
63
|
+
// a string instead of an object).
|
|
66
64
|
await this.sql `
|
|
67
65
|
INSERT INTO engram_events (workspace_id, session_id, seq, type, at, payload)
|
|
68
66
|
VALUES (
|
|
@@ -71,7 +69,7 @@ export class PostgresAdapter {
|
|
|
71
69
|
${ev.seq},
|
|
72
70
|
${ev.type},
|
|
73
71
|
${ev.at},
|
|
74
|
-
${
|
|
72
|
+
${ev}
|
|
75
73
|
)
|
|
76
74
|
ON CONFLICT (workspace_id, session_id, seq) DO NOTHING
|
|
77
75
|
`;
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured logging for engram-server.
|
|
3
|
+
*
|
|
4
|
+
* Emits one JSON object per line so Cloud Logging picks it up natively:
|
|
5
|
+
* {severity, message, ...fields}. Severity matches Google's "LogSeverity"
|
|
6
|
+
* enum names (DEBUG, INFO, WARNING, ERROR) so log explorer auto-classifies.
|
|
7
|
+
*
|
|
8
|
+
* Keep this dependency-free; structured logging is a small enough surface
|
|
9
|
+
* that pulling pino/winston isn't worth the dep weight.
|
|
10
|
+
*/
|
|
11
|
+
export interface LogFields {
|
|
12
|
+
request_id?: string;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
export declare const log: {
|
|
16
|
+
debug: (message: string, fields?: LogFields) => void;
|
|
17
|
+
info: (message: string, fields?: LogFields) => void;
|
|
18
|
+
warn: (message: string, fields?: LogFields) => void;
|
|
19
|
+
error: (message: string, fields?: LogFields) => void;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Generate a short request id when no upstream `x-request-id` is supplied.
|
|
23
|
+
* Not cryptographically strong — just enough to correlate one container's
|
|
24
|
+
* log lines for a single request.
|
|
25
|
+
*/
|
|
26
|
+
export declare function newRequestId(): string;
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured logging for engram-server.
|
|
3
|
+
*
|
|
4
|
+
* Emits one JSON object per line so Cloud Logging picks it up natively:
|
|
5
|
+
* {severity, message, ...fields}. Severity matches Google's "LogSeverity"
|
|
6
|
+
* enum names (DEBUG, INFO, WARNING, ERROR) so log explorer auto-classifies.
|
|
7
|
+
*
|
|
8
|
+
* Keep this dependency-free; structured logging is a small enough surface
|
|
9
|
+
* that pulling pino/winston isn't worth the dep weight.
|
|
10
|
+
*/
|
|
11
|
+
function emit(severity, message, fields) {
|
|
12
|
+
const line = JSON.stringify({ severity, message, ...fields });
|
|
13
|
+
if (severity === "ERROR")
|
|
14
|
+
console.error(line);
|
|
15
|
+
else if (severity === "WARNING")
|
|
16
|
+
console.warn(line);
|
|
17
|
+
else
|
|
18
|
+
console.log(line);
|
|
19
|
+
}
|
|
20
|
+
export const log = {
|
|
21
|
+
debug: (message, fields) => emit("DEBUG", message, fields),
|
|
22
|
+
info: (message, fields) => emit("INFO", message, fields),
|
|
23
|
+
warn: (message, fields) => emit("WARNING", message, fields),
|
|
24
|
+
error: (message, fields) => emit("ERROR", message, fields),
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Generate a short request id when no upstream `x-request-id` is supplied.
|
|
28
|
+
* Not cryptographically strong — just enough to correlate one container's
|
|
29
|
+
* log lines for a single request.
|
|
30
|
+
*/
|
|
31
|
+
export function newRequestId() {
|
|
32
|
+
return Math.random().toString(36).slice(2, 12);
|
|
33
|
+
}
|
package/dist/main.d.ts
ADDED
package/dist/main.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Production entrypoint.
|
|
3
|
+
*
|
|
4
|
+
* Reads configuration from environment:
|
|
5
|
+
*
|
|
6
|
+
* ENGRAM_API_KEY required single bearer key. Multi-tenant deploys
|
|
7
|
+
* should swap this for a real key store.
|
|
8
|
+
* PORT default 8080 HTTP listen port.
|
|
9
|
+
* DATABASE_URL optional if set, uses PostgresAdapter; otherwise
|
|
10
|
+
* falls back to InMemoryAdapter (NOT
|
|
11
|
+
* durable across restarts).
|
|
12
|
+
* DATABASE_SOCKET_PATH optional Cloud SQL Auth Proxy unix socket dir.
|
|
13
|
+
* When set, postgres connects via
|
|
14
|
+
* `host=<DATABASE_SOCKET_PATH>`.
|
|
15
|
+
* ENGRAM_WORKSPACE_ID default "default"
|
|
16
|
+
* workspace id baked into the postgres
|
|
17
|
+
* adapter for this single-tenant deploy.
|
|
18
|
+
*/
|
|
19
|
+
import { createServer } from "./server";
|
|
20
|
+
import { InMemoryAdapter } from "./adapters/memory";
|
|
21
|
+
import { PostgresAdapter } from "./adapters/postgres";
|
|
22
|
+
const PORT = Number(process.env.PORT ?? 8080);
|
|
23
|
+
const API_KEY = process.env.ENGRAM_API_KEY;
|
|
24
|
+
const WORKSPACE_ID = process.env.ENGRAM_WORKSPACE_ID ?? "default";
|
|
25
|
+
const DATABASE_URL = process.env.DATABASE_URL;
|
|
26
|
+
const DATABASE_SOCKET_PATH = process.env.DATABASE_SOCKET_PATH;
|
|
27
|
+
if (!API_KEY) {
|
|
28
|
+
console.error("[engram-server] ENGRAM_API_KEY is required");
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
const storage = await (async () => {
|
|
32
|
+
if (!DATABASE_URL) {
|
|
33
|
+
console.warn("[engram-server] DATABASE_URL not set — using InMemoryAdapter (data is volatile)");
|
|
34
|
+
return new InMemoryAdapter();
|
|
35
|
+
}
|
|
36
|
+
// postgres is a peer dep so we import it lazily; absence is a hard error here.
|
|
37
|
+
const { default: postgres } = await import("postgres");
|
|
38
|
+
const sql = DATABASE_SOCKET_PATH
|
|
39
|
+
? postgres(DATABASE_URL, { host: DATABASE_SOCKET_PATH })
|
|
40
|
+
: postgres(DATABASE_URL);
|
|
41
|
+
const adapter = new PostgresAdapter({
|
|
42
|
+
workspaceId: WORKSPACE_ID,
|
|
43
|
+
sql: sql,
|
|
44
|
+
});
|
|
45
|
+
await adapter.ensureSchema();
|
|
46
|
+
console.log(`[engram-server] postgres adapter ready (workspace=${WORKSPACE_ID})`);
|
|
47
|
+
return adapter;
|
|
48
|
+
})();
|
|
49
|
+
const app = createServer({
|
|
50
|
+
auth: (key) => (key === API_KEY ? { workspaceId: WORKSPACE_ID, storage } : null),
|
|
51
|
+
});
|
|
52
|
+
console.log(`[engram-server] listening on :${PORT}`);
|
|
53
|
+
export default { port: PORT, fetch: app.fetch };
|
package/dist/server.d.ts
CHANGED
package/dist/server.js
CHANGED
|
@@ -1,16 +1,64 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
2
|
import { buildIndex, search, } from "@hexis-ai/engram-core";
|
|
3
|
+
import { log, newRequestId } from "./logger";
|
|
3
4
|
export function createServer(opts) {
|
|
4
5
|
const newId = opts.newSessionId ?? (() => crypto.randomUUID());
|
|
5
6
|
const defaultLimit = opts.defaultListLimit ?? 100;
|
|
6
7
|
const maxLimit = opts.maxListLimit ?? 500;
|
|
7
8
|
const app = new Hono();
|
|
9
|
+
// Request id + access log middleware. Runs for every route.
|
|
10
|
+
app.use("*", async (c, next) => {
|
|
11
|
+
const rid = c.req.header("x-request-id") ?? newRequestId();
|
|
12
|
+
c.set("request_id", rid);
|
|
13
|
+
c.header("x-request-id", rid);
|
|
14
|
+
const start = Date.now();
|
|
15
|
+
let status = 500;
|
|
16
|
+
try {
|
|
17
|
+
await next();
|
|
18
|
+
status = c.res.status;
|
|
19
|
+
}
|
|
20
|
+
finally {
|
|
21
|
+
log.info("request", {
|
|
22
|
+
request_id: rid,
|
|
23
|
+
method: c.req.method,
|
|
24
|
+
path: c.req.path,
|
|
25
|
+
status,
|
|
26
|
+
duration_ms: Date.now() - start,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
app.onError((err, c) => {
|
|
31
|
+
log.error("unhandled", {
|
|
32
|
+
request_id: c.var.request_id,
|
|
33
|
+
path: c.req.path,
|
|
34
|
+
error: err instanceof Error ? err.message : String(err),
|
|
35
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
36
|
+
});
|
|
37
|
+
return c.json({ error: "internal_error" }, 500);
|
|
38
|
+
});
|
|
39
|
+
// Unauthenticated service info. Useful for browser sanity checks and
|
|
40
|
+
// health probes (Cloud Run, k8s, uptime monitors).
|
|
41
|
+
app.get("/", (c) => c.json({
|
|
42
|
+
service: "@hexis-ai/engram-server",
|
|
43
|
+
ok: true,
|
|
44
|
+
routes: {
|
|
45
|
+
sessions: "POST/GET /v1/sessions",
|
|
46
|
+
sessionById: "GET /v1/sessions/:id",
|
|
47
|
+
events: "POST /v1/sessions/:id/events",
|
|
48
|
+
search: "POST /v1/search",
|
|
49
|
+
},
|
|
50
|
+
}));
|
|
51
|
+
app.get("/healthz", (c) => c.json({ ok: true }));
|
|
8
52
|
app.use("/v1/*", async (c, next) => {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
53
|
+
// Prefer X-Api-Key so callers can use the Authorization header for
|
|
54
|
+
// platform-level auth (Cloud Run IAM with an ID token, for example)
|
|
55
|
+
// without collision. Falls back to Authorization: Bearer <key> for
|
|
56
|
+
// existing clients.
|
|
57
|
+
const apiKey = c.req.header("x-api-key") ??
|
|
58
|
+
c.req.header("authorization")?.match(/^Bearer\s+(.+)$/i)?.[1];
|
|
59
|
+
if (!apiKey)
|
|
12
60
|
return c.json({ error: "unauthorized" }, 401);
|
|
13
|
-
const ctx = await opts.auth(
|
|
61
|
+
const ctx = await opts.auth(apiKey);
|
|
14
62
|
if (!ctx)
|
|
15
63
|
return c.json({ error: "unauthorized" }, 401);
|
|
16
64
|
c.set("ctx", ctx);
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hexis-ai/engram-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Engram server: ingest agent session events, persist via a pluggable adapter, expose search.",
|
|
5
5
|
"keywords": ["engram", "agents", "search", "hono", "postgres", "server"],
|
|
6
|
-
"homepage": "https://github.com/hexis-
|
|
6
|
+
"homepage": "https://github.com/hexis-ltd/engram#readme",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/hexis-
|
|
9
|
+
"url": "git+https://github.com/hexis-ltd/engram.git",
|
|
10
10
|
"directory": "packages/server"
|
|
11
11
|
},
|
|
12
|
-
"bugs": "https://github.com/hexis-
|
|
12
|
+
"bugs": "https://github.com/hexis-ltd/engram/issues",
|
|
13
13
|
"author": "hexis ltd.",
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"type": "module",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"dev": "bun --hot src/dev.ts"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@hexis-ai/engram-core": "^0.1.
|
|
45
|
-
"@hexis-ai/engram-sdk": "^0.1.
|
|
44
|
+
"@hexis-ai/engram-core": "^0.1.4",
|
|
45
|
+
"@hexis-ai/engram-sdk": "^0.1.4",
|
|
46
46
|
"hono": "^4.6.0"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
@@ -51,6 +51,9 @@
|
|
|
51
51
|
"peerDependenciesMeta": {
|
|
52
52
|
"postgres": { "optional": true }
|
|
53
53
|
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"postgres": "^3.4.0"
|
|
56
|
+
},
|
|
54
57
|
"publishConfig": {
|
|
55
58
|
"access": "public"
|
|
56
59
|
}
|