@sisu-ai/server 1.0.0
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/README.md +89 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.js +121 -0
- package/dist/router.d.ts +3 -0
- package/dist/router.js +28 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# @sisu-ai/server
|
|
2
|
+
|
|
3
|
+
Standalone HTTP/HTTPS adapter for Sisu agents. Spin up an HTTP server or attach to an existing one while keeping the small core philosophy.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i @sisu-ai/server
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { Agent } from '@sisu-ai/core';
|
|
15
|
+
import { Server } from '@sisu-ai/server';
|
|
16
|
+
import { agentRunApi } from '@sisu-ai/mw-agent-run-api';
|
|
17
|
+
|
|
18
|
+
const app = new Agent().use(agentRunApi());
|
|
19
|
+
const server = new Server(app, {
|
|
20
|
+
port: 3000,
|
|
21
|
+
// Optional banner (enabled by default). Add endpoints to list them.
|
|
22
|
+
bannerEndpoints: [
|
|
23
|
+
'POST /api/runs/start',
|
|
24
|
+
'GET /api/runs/:id/status',
|
|
25
|
+
'GET /api/runs/:id/stream',
|
|
26
|
+
'POST /api/runs/:id/cancel',
|
|
27
|
+
],
|
|
28
|
+
createCtx: (req, res) => ({ req, res, messages: [], signal: new AbortController().signal })
|
|
29
|
+
});
|
|
30
|
+
server.listen();
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Options
|
|
34
|
+
|
|
35
|
+
- port/host/backlog/path/tls: standard Node listen options.
|
|
36
|
+
- basePath: base URL path for your agent routes. Default: `/api`.
|
|
37
|
+
- healthPath: health endpoint or `false` to disable. Default: `/health`.
|
|
38
|
+
- createCtx(req, res): build your per-request context; `Server` injects `agent` and a default `log` if missing.
|
|
39
|
+
- logBanner: print a startup banner. Default: `true`.
|
|
40
|
+
- bannerEndpoints: string lines printed under the banner (e.g., `GET /api/runs/:id/status`).
|
|
41
|
+
- logLevel: `'debug' | 'info' | 'warn' | 'error'`; sets the default console logger level.
|
|
42
|
+
- logger: provide a custom logger implementing Sisu `Logger`.
|
|
43
|
+
- redactLogKeys: additional keys to redact in logs (merged with built-ins).
|
|
44
|
+
|
|
45
|
+
## Request Logging
|
|
46
|
+
|
|
47
|
+
The server emits basic structured logs for every request and response using the default logger (or your `logger`).
|
|
48
|
+
|
|
49
|
+
- Request: `[server] request { method, url }`
|
|
50
|
+
- Response: `[server] response { method, url, status, duration_ms }`
|
|
51
|
+
|
|
52
|
+
Control verbosity via `logLevel` or `LOG_LEVEL`.
|
|
53
|
+
|
|
54
|
+
## Events API
|
|
55
|
+
|
|
56
|
+
Subscribe to server lifecycle and per-request events without using the `listen` callback.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
const server = new Server(app, { port: 3000 });
|
|
60
|
+
|
|
61
|
+
server
|
|
62
|
+
.on('listening', ({ url }) => {
|
|
63
|
+
console.log('ready at', url);
|
|
64
|
+
})
|
|
65
|
+
.on('request', ({ method, url }) => {
|
|
66
|
+
// e.g., metrics, audit
|
|
67
|
+
})
|
|
68
|
+
.on('response', ({ method, url, status, duration_ms }) => {
|
|
69
|
+
// e.g., record duration_ms to your metrics system
|
|
70
|
+
})
|
|
71
|
+
.on('error', (err) => {
|
|
72
|
+
console.error('server error', err);
|
|
73
|
+
})
|
|
74
|
+
.on('close', () => {
|
|
75
|
+
console.log('server closed');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
server.listen();
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Features
|
|
82
|
+
|
|
83
|
+
- Health endpoint (`/health` by default)
|
|
84
|
+
- Attach to existing `http`/`https` server or listen directly
|
|
85
|
+
- Supports TLS and UNIX sockets
|
|
86
|
+
- Injects the agent into each request context so middleware can spawn runs
|
|
87
|
+
- Startup banner: prints listen address, health path, base path, and optional endpoints
|
|
88
|
+
- Request logs: per-request/response lines, redaction support
|
|
89
|
+
- Events API: `listening`, `request`, `response`, `error`, `close`
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import http, { type IncomingMessage, type ServerResponse } from 'http';
|
|
2
|
+
import https from 'https';
|
|
3
|
+
import type { Agent, Logger } from '@sisu-ai/core';
|
|
4
|
+
import type { AddressInfo } from 'net';
|
|
5
|
+
export { matchRoute } from './router.js';
|
|
6
|
+
export interface ListenOptions<Ctx> {
|
|
7
|
+
tls?: https.ServerOptions;
|
|
8
|
+
port?: number;
|
|
9
|
+
host?: string;
|
|
10
|
+
backlog?: number;
|
|
11
|
+
path?: string;
|
|
12
|
+
basePath?: string;
|
|
13
|
+
createCtx?: (req: IncomingMessage, res: ServerResponse) => Promise<Ctx> | Ctx;
|
|
14
|
+
healthPath?: string | false;
|
|
15
|
+
logBanner?: boolean;
|
|
16
|
+
bannerEndpoints?: string[];
|
|
17
|
+
logger?: Logger;
|
|
18
|
+
logLevel?: 'debug' | 'info' | 'warn' | 'error';
|
|
19
|
+
redactLogKeys?: string[];
|
|
20
|
+
}
|
|
21
|
+
export declare class Server<Ctx = any> {
|
|
22
|
+
private agent;
|
|
23
|
+
private opts;
|
|
24
|
+
private server?;
|
|
25
|
+
private basePath;
|
|
26
|
+
private healthPath;
|
|
27
|
+
private createCtx;
|
|
28
|
+
private emitter;
|
|
29
|
+
constructor(agent: Agent<any>, opts?: ListenOptions<Ctx>);
|
|
30
|
+
private handle;
|
|
31
|
+
listener(): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
32
|
+
listen(cb?: () => void): http.Server<typeof http.IncomingMessage, typeof http.ServerResponse> | https.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
|
|
33
|
+
attach(server: http.Server | https.Server): void;
|
|
34
|
+
close(cb?: (err?: Error) => void): void;
|
|
35
|
+
address(): string | AddressInfo | null | undefined;
|
|
36
|
+
on(event: 'listening', handler: (e: {
|
|
37
|
+
url: string;
|
|
38
|
+
address: string | AddressInfo | null | undefined;
|
|
39
|
+
}) => void): this;
|
|
40
|
+
on(event: 'request', handler: (e: {
|
|
41
|
+
method: string;
|
|
42
|
+
url: string;
|
|
43
|
+
}) => void): this;
|
|
44
|
+
on(event: 'response', handler: (e: {
|
|
45
|
+
method: string;
|
|
46
|
+
url: string;
|
|
47
|
+
status: number;
|
|
48
|
+
duration_ms: number;
|
|
49
|
+
}) => void): this;
|
|
50
|
+
on(event: 'error', handler: (err: Error) => void): this;
|
|
51
|
+
on(event: 'close', handler: () => void): this;
|
|
52
|
+
once(event: 'listening', handler: (e: {
|
|
53
|
+
url: string;
|
|
54
|
+
address: string | AddressInfo | null | undefined;
|
|
55
|
+
}) => void): this;
|
|
56
|
+
once(event: 'request', handler: (e: {
|
|
57
|
+
method: string;
|
|
58
|
+
url: string;
|
|
59
|
+
}) => void): this;
|
|
60
|
+
once(event: 'response', handler: (e: {
|
|
61
|
+
method: string;
|
|
62
|
+
url: string;
|
|
63
|
+
status: number;
|
|
64
|
+
duration_ms: number;
|
|
65
|
+
}) => void): this;
|
|
66
|
+
once(event: 'error', handler: (err: Error) => void): this;
|
|
67
|
+
once(event: 'close', handler: () => void): this;
|
|
68
|
+
off(event: 'listening', handler: (e: {
|
|
69
|
+
url: string;
|
|
70
|
+
address: string | AddressInfo | null | undefined;
|
|
71
|
+
}) => void): this;
|
|
72
|
+
off(event: 'request', handler: (e: {
|
|
73
|
+
method: string;
|
|
74
|
+
url: string;
|
|
75
|
+
}) => void): this;
|
|
76
|
+
off(event: 'response', handler: (e: {
|
|
77
|
+
method: string;
|
|
78
|
+
url: string;
|
|
79
|
+
status: number;
|
|
80
|
+
duration_ms: number;
|
|
81
|
+
}) => void): this;
|
|
82
|
+
off(event: 'error', handler: (err: Error) => void): this;
|
|
83
|
+
off(event: 'close', handler: () => void): this;
|
|
84
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import https from 'https';
|
|
3
|
+
import { createConsoleLogger, createRedactingLogger } from '@sisu-ai/core';
|
|
4
|
+
import { EventEmitter } from 'events';
|
|
5
|
+
export { matchRoute } from './router.js';
|
|
6
|
+
export class Server {
|
|
7
|
+
constructor(agent, opts = {}) {
|
|
8
|
+
this.agent = agent;
|
|
9
|
+
this.opts = opts;
|
|
10
|
+
this.emitter = new EventEmitter();
|
|
11
|
+
this.basePath = opts.basePath ?? '/api';
|
|
12
|
+
this.healthPath = opts.healthPath ?? '/health';
|
|
13
|
+
this.createCtx = opts.createCtx ?? ((req, res) => ({ req, res }));
|
|
14
|
+
}
|
|
15
|
+
async handle(req, res) {
|
|
16
|
+
// Set up server-level request logging (independent of ctx)
|
|
17
|
+
const baseLogger = this.opts.logger ?? createConsoleLogger({ level: this.opts.logLevel, timestamps: true });
|
|
18
|
+
const srvLogger = createRedactingLogger(baseLogger, { keys: this.opts.redactLogKeys });
|
|
19
|
+
const started = Date.now();
|
|
20
|
+
const { method = 'GET', url = '' } = req;
|
|
21
|
+
srvLogger.info?.('[server] request', { method, url });
|
|
22
|
+
res.once?.('finish', () => {
|
|
23
|
+
const ms = Date.now() - started;
|
|
24
|
+
srvLogger.info?.('[server] response', { method, url, status: res.statusCode, duration_ms: ms });
|
|
25
|
+
this.emitter.emit('response', { method, url, status: res.statusCode, duration_ms: ms });
|
|
26
|
+
});
|
|
27
|
+
this.emitter.emit('request', { method, url });
|
|
28
|
+
if (this.healthPath && req.url === this.healthPath) {
|
|
29
|
+
res.statusCode = 200;
|
|
30
|
+
res.end('ok');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (!req.url || !req.url.startsWith(this.basePath)) {
|
|
34
|
+
res.statusCode = 404;
|
|
35
|
+
res.end();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const ctx = await this.createCtx(req, res);
|
|
39
|
+
ctx.agent = this.agent;
|
|
40
|
+
// Provide a default logger if not present, mirroring CLI behavior
|
|
41
|
+
if (!ctx.log) {
|
|
42
|
+
ctx.log = srvLogger;
|
|
43
|
+
}
|
|
44
|
+
// Mark this context as an HTTP transport envelope and capture minimal request meta
|
|
45
|
+
const headers = req.headers || {};
|
|
46
|
+
const httpMeta = {
|
|
47
|
+
method: req.method || 'GET',
|
|
48
|
+
url: req.url || '',
|
|
49
|
+
ip: (req.socket && (req.socket.remoteAddress || '')) || '',
|
|
50
|
+
headers: {
|
|
51
|
+
'user-agent': typeof headers['user-agent'] === 'string' ? headers['user-agent'] : undefined,
|
|
52
|
+
'accept': typeof headers['accept'] === 'string' ? headers['accept'] : undefined,
|
|
53
|
+
'content-type': typeof headers['content-type'] === 'string' ? headers['content-type'] : undefined,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
ctx.state = { ...(ctx.state ?? {}), _transport: { type: 'http' }, _http: httpMeta };
|
|
57
|
+
const handler = this.agent.handler();
|
|
58
|
+
await handler(ctx);
|
|
59
|
+
// Only synthesize a 404 when nothing has been written at all.
|
|
60
|
+
// If headers were sent (e.g., SSE), keep the connection as-is.
|
|
61
|
+
if (!res.writableEnded && !res.headersSent) {
|
|
62
|
+
res.statusCode = 404;
|
|
63
|
+
res.end();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
listener() {
|
|
67
|
+
return this.handle.bind(this);
|
|
68
|
+
}
|
|
69
|
+
listen(cb) {
|
|
70
|
+
const listener = this.listener();
|
|
71
|
+
this.server = this.opts.tls
|
|
72
|
+
? https.createServer(this.opts.tls, listener)
|
|
73
|
+
: http.createServer(listener);
|
|
74
|
+
if (this.opts.path) {
|
|
75
|
+
this.server.listen(this.opts.path, cb);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
this.server.listen(this.opts.port ?? 0, this.opts.host, this.opts.backlog, cb);
|
|
79
|
+
}
|
|
80
|
+
this.server.on('error', (err) => this.emitter.emit('error', err));
|
|
81
|
+
this.server.on('close', () => this.emitter.emit('close'));
|
|
82
|
+
const printBanner = this.opts.logBanner !== false;
|
|
83
|
+
if (printBanner) {
|
|
84
|
+
const addr = this.server.address();
|
|
85
|
+
let url = '';
|
|
86
|
+
if (typeof addr === 'object' && addr && 'port' in addr) {
|
|
87
|
+
const host = this.opts.host && this.opts.host !== '0.0.0.0' ? this.opts.host : 'localhost';
|
|
88
|
+
url = `http://${host}:${addr.port}`;
|
|
89
|
+
}
|
|
90
|
+
else if (typeof addr === 'string') {
|
|
91
|
+
url = addr;
|
|
92
|
+
}
|
|
93
|
+
if (url)
|
|
94
|
+
console.log(`[server] listening on ${url}`);
|
|
95
|
+
if (this.healthPath)
|
|
96
|
+
console.log(`[server] health: GET ${this.healthPath}`);
|
|
97
|
+
if (this.basePath)
|
|
98
|
+
console.log(`[server] basePath: ${this.basePath}`);
|
|
99
|
+
if (this.opts.bannerEndpoints?.length) {
|
|
100
|
+
console.log('[server] endpoints:');
|
|
101
|
+
for (const ep of this.opts.bannerEndpoints)
|
|
102
|
+
console.log(` ${ep}`);
|
|
103
|
+
}
|
|
104
|
+
this.emitter.emit('listening', { url, address: addr });
|
|
105
|
+
}
|
|
106
|
+
return this.server;
|
|
107
|
+
}
|
|
108
|
+
attach(server) {
|
|
109
|
+
server.on('request', this.listener());
|
|
110
|
+
this.server = server;
|
|
111
|
+
}
|
|
112
|
+
close(cb) {
|
|
113
|
+
this.server?.close(cb);
|
|
114
|
+
}
|
|
115
|
+
address() {
|
|
116
|
+
return this.server?.address();
|
|
117
|
+
}
|
|
118
|
+
on(event, handler) { this.emitter.on(event, handler); return this; }
|
|
119
|
+
once(event, handler) { this.emitter.once(event, handler); return this; }
|
|
120
|
+
off(event, handler) { this.emitter.off(event, handler); return this; }
|
|
121
|
+
}
|
package/dist/router.d.ts
ADDED
package/dist/router.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Lightweight path matcher for server + middleware
|
|
2
|
+
// Example: matchRoute('/api/runs/123/status', '/api', '/runs/:id/status')
|
|
3
|
+
// => { params: { id: '123' } }
|
|
4
|
+
export function matchRoute(url, basePath, template) {
|
|
5
|
+
if (!url.startsWith(basePath))
|
|
6
|
+
return null;
|
|
7
|
+
const q = url.indexOf('?');
|
|
8
|
+
const path = url.slice(basePath.length, q >= 0 ? q : undefined) || '/';
|
|
9
|
+
const tSegs = template.split('/').filter(Boolean);
|
|
10
|
+
const pSegs = path.split('/').filter(Boolean);
|
|
11
|
+
if (tSegs.length !== pSegs.length)
|
|
12
|
+
return null;
|
|
13
|
+
const params = {};
|
|
14
|
+
for (let i = 0; i < tSegs.length; i++) {
|
|
15
|
+
const t = tSegs[i];
|
|
16
|
+
const p = pSegs[i];
|
|
17
|
+
if (t.startsWith(':')) {
|
|
18
|
+
const name = t.slice(1);
|
|
19
|
+
if (!name)
|
|
20
|
+
return null;
|
|
21
|
+
params[name] = p;
|
|
22
|
+
}
|
|
23
|
+
else if (t !== p) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return { params };
|
|
28
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sisu-ai/server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc -b"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"@sisu-ai/core": "1.0.2"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/finger-gun/sisu",
|
|
23
|
+
"directory": "packages/server"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/finger-gun/sisu#readme",
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/finger-gun/sisu/issues"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"sisu",
|
|
31
|
+
"ai",
|
|
32
|
+
"server",
|
|
33
|
+
"adapter"
|
|
34
|
+
]
|
|
35
|
+
}
|