@moxxy/plugin-channel-http 0.26.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/LICENSE +21 -0
- package/dist/channel.d.ts +50 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/channel.js +164 -0
- package/dist/channel.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/router.d.ts +95 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +354 -0
- package/dist/router.js.map +1 -0
- package/package.json +67 -0
- package/src/attach.test.ts +85 -0
- package/src/channel.test.ts +165 -0
- package/src/channel.ts +211 -0
- package/src/index.ts +58 -0
- package/src/integration.test.ts +254 -0
- package/src/router.test.ts +665 -0
- package/src/router.ts +424 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Moxxy (moxxy.ai)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ClientSession as Session } from '@moxxy/sdk';
|
|
2
|
+
import type { Channel, ChannelHandle, ChannelStartOptsBase, PermissionResolver } from '@moxxy/sdk';
|
|
3
|
+
export interface HttpChannelOptions {
|
|
4
|
+
readonly port?: number;
|
|
5
|
+
readonly host?: string;
|
|
6
|
+
/** Bearer token required on every protected route. If unset, auth is disabled (dev-only). */
|
|
7
|
+
readonly authToken?: string;
|
|
8
|
+
/** Max turns running concurrently on the shared session; excess requests get
|
|
9
|
+
* a 429. Bounds provider-stream fan-out and history interleaving. */
|
|
10
|
+
readonly maxConcurrentTurns?: number;
|
|
11
|
+
/** Abort an SSE turn whose consumer has stalled (socket open but not reading)
|
|
12
|
+
* for this many ms, so a half-open client can't keep a provider stream alive
|
|
13
|
+
* forever. A live-but-slow consumer resets the timer on every flushed write.
|
|
14
|
+
* Defaults to 120_000; set <= 0 to disable (not recommended for a network
|
|
15
|
+
* bind). */
|
|
16
|
+
readonly streamStallMs?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Tool names that the model is allowed to call without further interaction.
|
|
19
|
+
* This is the entire permission story for HTTP — there's no human in the
|
|
20
|
+
* loop to click "allow", so the operator declares trust upfront. Anything
|
|
21
|
+
* not in this list is denied.
|
|
22
|
+
*/
|
|
23
|
+
readonly allowedTools?: ReadonlyArray<string>;
|
|
24
|
+
readonly logger?: {
|
|
25
|
+
info?(msg: string, meta?: Record<string, unknown>): void;
|
|
26
|
+
warn?(msg: string, meta?: Record<string, unknown>): void;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export interface HttpStartOpts extends ChannelStartOptsBase {
|
|
30
|
+
readonly session: Session;
|
|
31
|
+
}
|
|
32
|
+
export declare class HttpChannel implements Channel<HttpStartOpts> {
|
|
33
|
+
readonly name = "http";
|
|
34
|
+
readonly permissionResolver: PermissionResolver;
|
|
35
|
+
private readonly port;
|
|
36
|
+
private readonly host;
|
|
37
|
+
private readonly authToken;
|
|
38
|
+
private readonly maxConcurrentTurns;
|
|
39
|
+
private readonly streamStallMs;
|
|
40
|
+
private readonly logger;
|
|
41
|
+
private server;
|
|
42
|
+
private boundPortValue;
|
|
43
|
+
/** The actual TCP port the server bound to after {@link start}. Equals the
|
|
44
|
+
* configured port, or the OS-assigned one when `port: 0` (ephemeral) — so
|
|
45
|
+
* callers (and tests) can address the server without guessing a free port. */
|
|
46
|
+
get boundPort(): number;
|
|
47
|
+
constructor(opts?: HttpChannelOptions);
|
|
48
|
+
start(startOpts: HttpStartOpts): Promise<ChannelHandle>;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=channel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,IAAI,OAAO,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,KAAK,EACV,OAAO,EACP,aAAa,EACb,oBAAoB,EACpB,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAOpB,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,6FAA6F;IAC7F,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;0EACsE;IACtE,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC;;;;iBAIa;IACb,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC9C,QAAQ,CAAC,MAAM,CAAC,EAAE;QAChB,IAAI,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;QACzD,IAAI,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;KAC1D,CAAC;CACH;AAED,MAAM,WAAW,aAAc,SAAQ,oBAAoB;IACzD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED,qBAAa,WAAY,YAAW,OAAO,CAAC,aAAa,CAAC;IACxD,QAAQ,CAAC,IAAI,UAAU;IACvB,QAAQ,CAAC,kBAAkB,EAAE,kBAAkB,CAAC;IAChD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgB;IAC1C,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAS;IAC5C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;IACnD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA+B;IACtD,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,cAAc,CAAK;IAE3B;;mFAE+E;IAC/E,IAAI,SAAS,IAAI,MAAM,CAEtB;gBAEW,IAAI,GAAE,kBAAuB;IAoBnC,KAAK,CAAC,SAAS,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;CA6H9D"}
|
package/dist/channel.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { createAllowListResolver, denyByDefaultResolver } from '@moxxy/sdk';
|
|
3
|
+
import { routeRequest, TurnLimiter } from './router.js';
|
|
4
|
+
/** Hosts auth-disabled mode is only safe to bind on. A non-loopback bind with
|
|
5
|
+
* no token would expose an unauthenticated agent endpoint to the network. */
|
|
6
|
+
const LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', 'localhost', '::ffff:127.0.0.1']);
|
|
7
|
+
export class HttpChannel {
|
|
8
|
+
name = 'http';
|
|
9
|
+
permissionResolver;
|
|
10
|
+
port;
|
|
11
|
+
host;
|
|
12
|
+
authToken;
|
|
13
|
+
maxConcurrentTurns;
|
|
14
|
+
streamStallMs;
|
|
15
|
+
logger;
|
|
16
|
+
server = null;
|
|
17
|
+
boundPortValue = 0;
|
|
18
|
+
/** The actual TCP port the server bound to after {@link start}. Equals the
|
|
19
|
+
* configured port, or the OS-assigned one when `port: 0` (ephemeral) — so
|
|
20
|
+
* callers (and tests) can address the server without guessing a free port. */
|
|
21
|
+
get boundPort() {
|
|
22
|
+
return this.boundPortValue;
|
|
23
|
+
}
|
|
24
|
+
constructor(opts = {}) {
|
|
25
|
+
this.port = opts.port ?? 3737;
|
|
26
|
+
this.host = opts.host ?? '127.0.0.1';
|
|
27
|
+
this.authToken = opts.authToken ?? null;
|
|
28
|
+
this.maxConcurrentTurns =
|
|
29
|
+
typeof opts.maxConcurrentTurns === 'number' && opts.maxConcurrentTurns > 0
|
|
30
|
+
? Math.floor(opts.maxConcurrentTurns)
|
|
31
|
+
: 4;
|
|
32
|
+
// undefined => router uses its DEFAULT_STREAM_STALL_MS; a finite number
|
|
33
|
+
// (including <= 0 to disable) is honored as-is.
|
|
34
|
+
this.streamStallMs =
|
|
35
|
+
typeof opts.streamStallMs === 'number' && Number.isFinite(opts.streamStallMs)
|
|
36
|
+
? opts.streamStallMs
|
|
37
|
+
: undefined;
|
|
38
|
+
this.logger = opts.logger;
|
|
39
|
+
this.permissionResolver = opts.allowedTools && opts.allowedTools.length > 0
|
|
40
|
+
? createAllowListResolver([...opts.allowedTools])
|
|
41
|
+
: denyByDefaultResolver;
|
|
42
|
+
}
|
|
43
|
+
async start(startOpts) {
|
|
44
|
+
// Guard against a double-start: unconditionally reassigning `this.server`
|
|
45
|
+
// would orphan the first server (its port stays bound, sockets stay open)
|
|
46
|
+
// since `stop()` only ever closes the most-recently-assigned one.
|
|
47
|
+
if (this.server) {
|
|
48
|
+
throw new Error('HttpChannel is already started — call stop() before starting again.');
|
|
49
|
+
}
|
|
50
|
+
// Refuse to expose an unauthenticated agent endpoint to the network. With
|
|
51
|
+
// no token the only gate is the deny-by-default tool resolver; a
|
|
52
|
+
// non-loopback bind would still let anyone reach /v1/turn.
|
|
53
|
+
if (this.authToken === null && !LOOPBACK_HOSTS.has(this.host)) {
|
|
54
|
+
throw new Error(`HttpChannel refuses to bind non-loopback host "${this.host}" without an authToken. ` +
|
|
55
|
+
'Set authToken (or MOXXY_HTTP_TOKEN), or bind 127.0.0.1.');
|
|
56
|
+
}
|
|
57
|
+
const ctx = {
|
|
58
|
+
session: startOpts.session,
|
|
59
|
+
authToken: this.authToken,
|
|
60
|
+
logger: this.logger,
|
|
61
|
+
turnLimiter: new TurnLimiter(this.maxConcurrentTurns),
|
|
62
|
+
...(this.streamStallMs !== undefined ? { streamStallMs: this.streamStallMs } : {}),
|
|
63
|
+
};
|
|
64
|
+
const server = createServer(async (req, res) => {
|
|
65
|
+
const handler = routeRequest(req);
|
|
66
|
+
if (!handler) {
|
|
67
|
+
res.writeHead(404, { 'content-type': 'application/json' });
|
|
68
|
+
res.end(JSON.stringify({ error: 'not_found', path: req.url }));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
await handler(req, res, ctx);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
// Log the full error server-side; return a generic message so internal
|
|
76
|
+
// paths/provider details can't leak to a (possibly remote) caller.
|
|
77
|
+
this.logger?.warn?.('http handler threw', { err: String(err) });
|
|
78
|
+
if (!res.headersSent) {
|
|
79
|
+
res.writeHead(500, { 'content-type': 'application/json' });
|
|
80
|
+
res.end(JSON.stringify({ error: 'internal' }));
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
try {
|
|
84
|
+
res.end();
|
|
85
|
+
}
|
|
86
|
+
catch { /* ignore */ }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
// Tighten timeouts so a slow-loris client can't tie up sockets by dribbling
|
|
91
|
+
// headers/bodies. Defaults (requestTimeout 300s) are too generous for an
|
|
92
|
+
// agent endpoint. Cap total connections so a flood can't exhaust handles.
|
|
93
|
+
server.headersTimeout = 10_000;
|
|
94
|
+
server.requestTimeout = 30_000;
|
|
95
|
+
server.keepAliveTimeout = 5_000;
|
|
96
|
+
server.maxConnections = 256;
|
|
97
|
+
this.server = server;
|
|
98
|
+
// `running` rejects on a post-listen server error and resolves on a clean
|
|
99
|
+
// close. Declared up front so the persistent error handler installed below
|
|
100
|
+
// (after the listen-scoped one is detached) can settle it.
|
|
101
|
+
let rejectRunning;
|
|
102
|
+
let resolveRunning;
|
|
103
|
+
const running = new Promise((resolve, reject) => {
|
|
104
|
+
resolveRunning = resolve;
|
|
105
|
+
rejectRunning = reject;
|
|
106
|
+
});
|
|
107
|
+
// A fire-and-forget caller may never attach a .catch; without this default
|
|
108
|
+
// observer a post-listen server error escalates to a process-level
|
|
109
|
+
// unhandledRejection (fatal under --unhandled-rejections=strict). Real
|
|
110
|
+
// awaiters still see the rejection — a settled promise fans out to all.
|
|
111
|
+
running.catch(() => { });
|
|
112
|
+
const listening = new Promise((resolve, reject) => {
|
|
113
|
+
// Scoped to the listen handshake only — detached once we're listening so
|
|
114
|
+
// it can't no-op on the already-settled promise (and swallow runtime
|
|
115
|
+
// errors). The persistent handler below takes over afterwards.
|
|
116
|
+
const onListenError = (err) => reject(err);
|
|
117
|
+
server.once('error', onListenError);
|
|
118
|
+
server.listen(this.port, this.host, () => {
|
|
119
|
+
server.off('error', onListenError);
|
|
120
|
+
// A runtime server error after listen would otherwise be unhandled
|
|
121
|
+
// (Node re-throws an 'error' with no listener). Log it and fail the
|
|
122
|
+
// `running` promise so callers awaiting it observe the crash instead of
|
|
123
|
+
// mistaking it for a clean shutdown.
|
|
124
|
+
server.on('error', (err) => {
|
|
125
|
+
this.logger?.warn?.('http server error', { err: String(err) });
|
|
126
|
+
rejectRunning(err);
|
|
127
|
+
});
|
|
128
|
+
const addr = server.address();
|
|
129
|
+
this.boundPortValue = typeof addr === 'object' && addr ? addr.port : this.port;
|
|
130
|
+
this.logger?.info?.('http channel listening', {
|
|
131
|
+
host: this.host,
|
|
132
|
+
port: this.boundPortValue,
|
|
133
|
+
authEnabled: this.authToken !== null,
|
|
134
|
+
});
|
|
135
|
+
resolve();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
await listening;
|
|
139
|
+
server.once('close', () => resolveRunning());
|
|
140
|
+
return {
|
|
141
|
+
running,
|
|
142
|
+
stop: async () => {
|
|
143
|
+
const srv = this.server;
|
|
144
|
+
// Clear the handle first so a concurrent/double stop() is a no-op rather
|
|
145
|
+
// than racing two close() calls on the same server (the second close
|
|
146
|
+
// gets an 'ERR_SERVER_NOT_RUNNING' error in its callback).
|
|
147
|
+
if (this.server === srv) {
|
|
148
|
+
this.server = null;
|
|
149
|
+
this.boundPortValue = 0;
|
|
150
|
+
}
|
|
151
|
+
await new Promise((resolve) => {
|
|
152
|
+
if (!srv)
|
|
153
|
+
return resolve();
|
|
154
|
+
srv.close((err) => {
|
|
155
|
+
if (err)
|
|
156
|
+
this.logger?.warn?.('http server close error', { err: String(err) });
|
|
157
|
+
resolve();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=channel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel.js","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type Plugin } from '@moxxy/sdk';
|
|
2
|
+
export { HttpChannel, type HttpChannelOptions, type HttpStartOpts } from './channel.js';
|
|
3
|
+
export { routeRequest, handleHealth, handleTurn, handleTurnStream, handleTurnAudio, turnRequestSchema, type TurnRequest, type RouterContext, type RouteHandler, } from './router.js';
|
|
4
|
+
export declare const httpChannelDef: import("@moxxy/sdk").ChannelDef<import("./channel.js").HttpStartOpts>;
|
|
5
|
+
export declare const httpChannelPlugin: Plugin;
|
|
6
|
+
export default httpChannelPlugin;
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAA+B,KAAK,MAAM,EAAE,MAAM,YAAY,CAAC;AAGtE,OAAO,EAAE,WAAW,EAAE,KAAK,kBAAkB,EAAE,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AACxF,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,gBAAgB,EAChB,eAAe,EACf,iBAAiB,EACjB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,YAAY,GAClB,MAAM,aAAa,CAAC;AAErB,eAAO,MAAM,cAAc,uEAiCzB,CAAC;AAEH,eAAO,MAAM,iBAAiB,EAAE,MAI9B,CAAC;AAEH,eAAe,iBAAiB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { defineChannel, definePlugin } from '@moxxy/sdk';
|
|
2
|
+
import { HttpChannel } from './channel.js';
|
|
3
|
+
export { HttpChannel } from './channel.js';
|
|
4
|
+
export { routeRequest, handleHealth, handleTurn, handleTurnStream, handleTurnAudio, turnRequestSchema, } from './router.js';
|
|
5
|
+
export const httpChannelDef = defineChannel({
|
|
6
|
+
name: 'http',
|
|
7
|
+
description: 'Request-response HTTP channel. POST /v1/turn + SSE streaming. Bearer-token auth + allow-list perms.',
|
|
8
|
+
create: (deps) => {
|
|
9
|
+
const opts = deps.options ?? {};
|
|
10
|
+
return new HttpChannel({
|
|
11
|
+
port: typeof opts.port === 'number' ? opts.port : undefined,
|
|
12
|
+
host: typeof opts.host === 'string' ? opts.host : undefined,
|
|
13
|
+
authToken: typeof opts.authToken === 'string' ? opts.authToken : process.env.MOXXY_HTTP_TOKEN,
|
|
14
|
+
allowedTools: Array.isArray(opts.allowedTools) ? opts.allowedTools : undefined,
|
|
15
|
+
maxConcurrentTurns: typeof opts.maxConcurrentTurns === 'number' ? opts.maxConcurrentTurns : undefined,
|
|
16
|
+
streamStallMs: typeof opts.streamStallMs === 'number' ? opts.streamStallMs : undefined,
|
|
17
|
+
logger: deps.logger,
|
|
18
|
+
});
|
|
19
|
+
},
|
|
20
|
+
isAvailable: async (deps) => {
|
|
21
|
+
const tools = deps.options?.['allowedTools'];
|
|
22
|
+
const authToken = deps.options?.['authToken'] ?? process.env.MOXXY_HTTP_TOKEN;
|
|
23
|
+
if (!authToken) {
|
|
24
|
+
return {
|
|
25
|
+
ok: false,
|
|
26
|
+
reason: "No auth token. Set MOXXY_HTTP_TOKEN or pass channels.http.authToken in moxxy.config.ts.",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (!Array.isArray(tools) || tools.length === 0) {
|
|
30
|
+
return {
|
|
31
|
+
ok: false,
|
|
32
|
+
reason: 'No allowed-tools list. The HTTP channel needs an upfront tool allow-list since there is no human in the loop. Set channels.http.allowedTools in moxxy.config.ts.',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return { ok: true };
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
export const httpChannelPlugin = definePlugin({
|
|
39
|
+
name: '@moxxy/plugin-channel-http',
|
|
40
|
+
version: '0.0.0',
|
|
41
|
+
channels: [httpChannelDef],
|
|
42
|
+
});
|
|
43
|
+
export default httpChannelPlugin;
|
|
44
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,YAAY,EAAe,MAAM,YAAY,CAAC;AACtE,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C,OAAO,EAAE,WAAW,EAA+C,MAAM,cAAc,CAAC;AACxF,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,gBAAgB,EAChB,eAAe,EACf,iBAAiB,GAIlB,MAAM,aAAa,CAAC;AAErB,MAAM,CAAC,MAAM,cAAc,GAAG,aAAa,CAAC;IAC1C,IAAI,EAAE,MAAM;IACZ,WAAW,EAAE,qGAAqG;IAClH,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;QACf,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC;QAChC,OAAO,IAAI,WAAW,CAAC;YACrB,IAAI,EAAE,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS;YAC3D,IAAI,EAAE,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS;YAC3D,SAAS,EAAE,OAAO,IAAI,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB;YAC7F,YAAY,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,CAAE,IAAI,CAAC,YAAyB,CAAC,CAAC,CAAC,SAAS;YAC5F,kBAAkB,EAAE,OAAO,IAAI,CAAC,kBAAkB,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC,CAAC,SAAS;YACrG,aAAa,EAAE,OAAO,IAAI,CAAC,aAAa,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS;YACtF,MAAM,EAAE,IAAI,CAAC,MAAe;SAC7B,CAAC,CAAC;IACL,CAAC;IACD,WAAW,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;QAC1B,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC,cAAc,CAAC,CAAC;QAC7C,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC,WAAW,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;QAC9E,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,MAAM,EAAE,yFAAyF;aAClG,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAChD,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,MAAM,EACJ,kKAAkK;aACrK,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;IACtB,CAAC;CACF,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,iBAAiB,GAAW,YAAY,CAAC;IACpD,IAAI,EAAE,4BAA4B;IAClC,OAAO,EAAE,OAAO;IAChB,QAAQ,EAAE,CAAC,cAAc,CAAC;CAC3B,CAAC,CAAC;AAEH,eAAe,iBAAiB,CAAC"}
|
package/dist/router.d.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
3
|
+
import type { ClientSession as Session } from '@moxxy/sdk';
|
|
4
|
+
import type { MoxxyEvent } from '@moxxy/sdk';
|
|
5
|
+
export declare const turnRequestSchema: z.ZodObject<{
|
|
6
|
+
prompt: z.ZodString;
|
|
7
|
+
model: z.ZodOptional<z.ZodString>;
|
|
8
|
+
systemPrompt: z.ZodOptional<z.ZodString>;
|
|
9
|
+
}, "strip", z.ZodTypeAny, {
|
|
10
|
+
prompt: string;
|
|
11
|
+
model?: string | undefined;
|
|
12
|
+
systemPrompt?: string | undefined;
|
|
13
|
+
}, {
|
|
14
|
+
prompt: string;
|
|
15
|
+
model?: string | undefined;
|
|
16
|
+
systemPrompt?: string | undefined;
|
|
17
|
+
}>;
|
|
18
|
+
export type TurnRequest = z.infer<typeof turnRequestSchema>;
|
|
19
|
+
/**
|
|
20
|
+
* Counting semaphore gating how many turns run concurrently on the single
|
|
21
|
+
* shared session. The HTTP server otherwise accepts unlimited parallel
|
|
22
|
+
* requests, each calling `session.runTurn` on the SAME session — N concurrent
|
|
23
|
+
* clients would spawn N provider streams (cost/memory/connection blowup) and
|
|
24
|
+
* interleave one shared conversation history. `tryAcquire` is non-blocking so
|
|
25
|
+
* excess requests get a fast 429 instead of queueing unbounded.
|
|
26
|
+
*/
|
|
27
|
+
export declare class TurnLimiter {
|
|
28
|
+
private readonly max;
|
|
29
|
+
private inUse;
|
|
30
|
+
constructor(max: number);
|
|
31
|
+
tryAcquire(): boolean;
|
|
32
|
+
release(): void;
|
|
33
|
+
get active(): number;
|
|
34
|
+
}
|
|
35
|
+
export interface RouterContext {
|
|
36
|
+
readonly session: Session;
|
|
37
|
+
readonly authToken: string | null;
|
|
38
|
+
readonly logger?: {
|
|
39
|
+
warn(msg: string, meta?: Record<string, unknown>): void;
|
|
40
|
+
};
|
|
41
|
+
/** Bounds concurrent in-flight turns on the shared session. Optional so
|
|
42
|
+
* existing embedders/tests keep working unbounded; the channel always
|
|
43
|
+
* supplies one. */
|
|
44
|
+
readonly turnLimiter?: TurnLimiter;
|
|
45
|
+
/** Abort an SSE turn whose consumer has stalled (socket open but not reading)
|
|
46
|
+
* for this many ms. Bounds the backpressure park in {@link handleTurnStream}
|
|
47
|
+
* so a half-open client can't keep a provider stream (and billing) alive
|
|
48
|
+
* forever. Optional; defaults to {@link DEFAULT_STREAM_STALL_MS}. */
|
|
49
|
+
readonly streamStallMs?: number;
|
|
50
|
+
}
|
|
51
|
+
/** Default write-side idle timeout for an SSE turn. A live-but-slow consumer
|
|
52
|
+
* resets this on every flushed write; only a truly stalled (open-but-silent)
|
|
53
|
+
* reader trips it, at which point we abort the turn — the same outcome as a
|
|
54
|
+
* clean disconnect, just without waiting for a TCP close that may never come.
|
|
55
|
+
* Generous so normal inter-event gaps (model thinking, tool calls) don't trip
|
|
56
|
+
* it. */
|
|
57
|
+
export declare const DEFAULT_STREAM_STALL_MS = 120000;
|
|
58
|
+
export type RouteHandler = (req: IncomingMessage, res: ServerResponse, ctx: RouterContext) => Promise<void>;
|
|
59
|
+
/** Match HTTP request to a handler. Returns null if no route matches. */
|
|
60
|
+
export declare function routeRequest(req: IncomingMessage): RouteHandler | null;
|
|
61
|
+
export declare function handleHealth(_req: IncomingMessage, res: ServerResponse): Promise<void>;
|
|
62
|
+
/** Run options forwarded to `session.runTurn`, minus the abort `signal`
|
|
63
|
+
* (which `driveTurn` owns). */
|
|
64
|
+
export type TurnRunOptions = Omit<Parameters<Session['runTurn']>[1] & object, 'signal'>;
|
|
65
|
+
/** Hard cap on events buffered for a single non-streaming turn. The buffered
|
|
66
|
+
* path holds every event in memory and returns them in one JSON blob; a
|
|
67
|
+
* runaway/tool-looping turn could otherwise grow this without bound. Once
|
|
68
|
+
* exceeded we abort the turn rather than keep allocating. Generous enough that
|
|
69
|
+
* no normal turn hits it. */
|
|
70
|
+
export declare const MAX_BUFFERED_EVENTS = 50000;
|
|
71
|
+
/**
|
|
72
|
+
* Drive a single buffered (non-streaming) turn to completion: drain every
|
|
73
|
+
* event, abort the turn if the client hangs up (so the model stops billing
|
|
74
|
+
* with nobody listening), and pull out the final assistant message. Shared by
|
|
75
|
+
* `handleTurn` and `handleTurnAudio` so the abort wiring and event/assistant
|
|
76
|
+
* extraction live in exactly one place. Re-throws on turn failure so each
|
|
77
|
+
* caller can shape its own error reply.
|
|
78
|
+
*/
|
|
79
|
+
export declare function driveTurn(session: Session, prompt: string, runOptions: TurnRunOptions, res: ServerResponse): Promise<{
|
|
80
|
+
events: MoxxyEvent[];
|
|
81
|
+
assistant: string;
|
|
82
|
+
}>;
|
|
83
|
+
export declare function handleTurn(req: IncomingMessage, res: ServerResponse, ctx: RouterContext): Promise<void>;
|
|
84
|
+
/**
|
|
85
|
+
* Audio-in turn. Designed for iOS Shortcuts and curl: the client POSTs
|
|
86
|
+
* raw audio bytes with `Content-Type: audio/<format>`. Optional query
|
|
87
|
+
* params (`model`, `language`, `systemPrompt`) tune the run.
|
|
88
|
+
*
|
|
89
|
+
* The session must have an active Transcriber registered (e.g. via
|
|
90
|
+
* `@moxxy/plugin-stt-whisper`); without one the endpoint returns 503
|
|
91
|
+
* rather than transparently dropping the audio.
|
|
92
|
+
*/
|
|
93
|
+
export declare function handleTurnAudio(req: IncomingMessage, res: ServerResponse, ctx: RouterContext): Promise<void>;
|
|
94
|
+
export declare function handleTurnStream(req: IncomingMessage, res: ServerResponse, ctx: RouterContext): Promise<void>;
|
|
95
|
+
//# sourceMappingURL=router.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjE,OAAO,KAAK,EAAE,aAAa,IAAI,OAAO,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,eAAO,MAAM,iBAAiB;;;;;;;;;;;;EAI5B,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D;;;;;;;GAOG;AACH,qBAAa,WAAW;IAEV,OAAO,CAAC,QAAQ,CAAC,GAAG;IADhC,OAAO,CAAC,KAAK,CAAK;gBACW,GAAG,EAAE,MAAM;IACxC,UAAU,IAAI,OAAO;IAKrB,OAAO,IAAI,IAAI;IAGf,IAAI,MAAM,IAAI,MAAM,CAEnB;CACF;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;KAAE,CAAC;IAC9E;;wBAEoB;IACpB,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,CAAC;IACnC;;;0EAGsE;IACtE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC;AAED;;;;;UAKU;AACV,eAAO,MAAM,uBAAuB,SAAU,CAAC;AAE/C,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,EAAE,GAAG,EAAE,aAAa,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAgB5G,yEAAyE;AACzE,wBAAgB,YAAY,CAAC,GAAG,EAAE,eAAe,GAAG,YAAY,GAAG,IAAI,CAWtE;AAED,wBAAsB,YAAY,CAChC,IAAI,EAAE,eAAe,EACrB,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,IAAI,CAAC,CAGf;AAuBD;gCACgC;AAChC,MAAM,MAAM,cAAc,GAAG,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,EAAE,QAAQ,CAAC,CAAC;AAExF;;;;8BAI8B;AAC9B,eAAO,MAAM,mBAAmB,QAAS,CAAC;AAE1C;;;;;;;GAOG;AACH,wBAAsB,SAAS,CAC7B,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,cAAc,EAC1B,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC;IAAE,MAAM,EAAE,UAAU,EAAE,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CAsBtD;AAED,wBAAsB,UAAU,CAC9B,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,aAAa,GACjB,OAAO,CAAC,IAAI,CAAC,CAuCf;AAED;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,aAAa,GACjB,OAAO,CAAC,IAAI,CAAC,CAsFf;AAED,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,aAAa,GACjB,OAAO,CAAC,IAAI,CAAC,CAyGf"}
|