@rcrsr/rill-agent-chat 0.20.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 +53 -0
- package/dist/index.d.ts +222 -0
- package/dist/index.js +697 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# @rcrsr/rill-agent-chat
|
|
2
|
+
|
|
3
|
+
OpenAI-compatible chat completions harness for [`@rcrsr/rill-agent`](https://www.npmjs.com/package/@rcrsr/rill-agent). Wraps an `AgentRouter` in a Hono server that speaks the OpenAI Chat Completions wire format over Server-Sent Events (SSE). Stateless and provider-independent — no Azure or vendor SDK at runtime. Consumable by the openai SDK, LiteLLM, the Vercel AI SDK, and any HTTP client.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @rcrsr/rill-agent @rcrsr/rill-agent-chat
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { loadManifest, createRouter } from '@rcrsr/rill-agent';
|
|
15
|
+
import { createChatHarness } from '@rcrsr/rill-agent-chat';
|
|
16
|
+
|
|
17
|
+
const manifest = await loadManifest('./build');
|
|
18
|
+
const router = await createRouter(manifest);
|
|
19
|
+
|
|
20
|
+
const harness = createChatHarness(router);
|
|
21
|
+
await harness.listen(3000);
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
An agent is chat-eligible when its handler returns a stream of OpenAI-shaped chunks. See [the reference](docs/agent-chat.md) for the handler contract and `inspectChatHandler`.
|
|
25
|
+
|
|
26
|
+
## Routes
|
|
27
|
+
|
|
28
|
+
| Method | Path | Description |
|
|
29
|
+
|--------|------|-------------|
|
|
30
|
+
| `POST` | `/v1/chat/completions` | OpenAI-compatible; the `model` field selects the agent |
|
|
31
|
+
| `POST` | `/agents/:name/chat` | Per-agent chat |
|
|
32
|
+
| `POST` | `/chat` | Default agent chat |
|
|
33
|
+
| `GET` | `/agents` | List chat-eligible agents |
|
|
34
|
+
| `GET` | `/health` | Liveness (always on) |
|
|
35
|
+
| `GET` | `/metrics` | Request counters (always on) |
|
|
36
|
+
|
|
37
|
+
Request and response bodies follow the OpenAI Chat Completions format. Set `"stream": true` for an SSE stream of `chat.completion.chunk` events. Toggle the first four routes via `options.routes`.
|
|
38
|
+
|
|
39
|
+
## API
|
|
40
|
+
|
|
41
|
+
- `createChatHarness(router, options?)` — returns a `ChatHarness` (`{ app, listen(port), close() }`)
|
|
42
|
+
- `inspectChatHandler(handler)` — reports whether a handler is chat-eligible
|
|
43
|
+
- `validateMessages(messages)` — validates an incoming messages array
|
|
44
|
+
|
|
45
|
+
The default export is a `RillHarness` adapter consumed by the rill CLI bundle mode (`rill run`) when this package is declared as a bundle harness.
|
|
46
|
+
|
|
47
|
+
## Documentation
|
|
48
|
+
|
|
49
|
+
- [Reference](docs/agent-chat.md) — routes, options, the handler contract, streaming, and error shapes
|
|
50
|
+
|
|
51
|
+
## License
|
|
52
|
+
|
|
53
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// Generated by dts-bundle-generator v9.5.1
|
|
2
|
+
|
|
3
|
+
import { Hono } from 'hono';
|
|
4
|
+
|
|
5
|
+
export interface HandlerDescription {
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly description?: string | undefined;
|
|
8
|
+
readonly params: ReadonlyArray<{
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly type: string;
|
|
11
|
+
readonly required: boolean;
|
|
12
|
+
readonly description?: string | undefined;
|
|
13
|
+
readonly defaultValue?: unknown;
|
|
14
|
+
}>;
|
|
15
|
+
/**
|
|
16
|
+
* Handler return type annotation, formatted with the same grammar as
|
|
17
|
+
* parameter type strings (e.g. `stream(dict(content: string)):string`).
|
|
18
|
+
* Undefined when the handler closure has no `:T` annotation, or when the
|
|
19
|
+
* rill-build emitting the handler is too old to expose the field.
|
|
20
|
+
*/
|
|
21
|
+
readonly returnType?: string | undefined;
|
|
22
|
+
}
|
|
23
|
+
export interface InitContext {
|
|
24
|
+
readonly globalVars?: Record<string, string> | undefined;
|
|
25
|
+
readonly ahiResolver?: ((agentName: string, request: RunRequest) => Promise<RunResponse>) | undefined;
|
|
26
|
+
}
|
|
27
|
+
export interface RunRequest {
|
|
28
|
+
readonly params?: Record<string, unknown> | undefined;
|
|
29
|
+
readonly timeout?: number | undefined;
|
|
30
|
+
}
|
|
31
|
+
export interface RunContext {
|
|
32
|
+
readonly sessionVars?: Record<string, string> | undefined;
|
|
33
|
+
readonly onLog?: ((message: string) => void) | undefined;
|
|
34
|
+
readonly onChunk?: ((chunk: unknown) => Promise<void>) | undefined;
|
|
35
|
+
readonly signal?: AbortSignal | undefined;
|
|
36
|
+
}
|
|
37
|
+
export interface RunResponse {
|
|
38
|
+
readonly state: "completed" | "error";
|
|
39
|
+
readonly result: unknown;
|
|
40
|
+
readonly streamed?: boolean | undefined;
|
|
41
|
+
}
|
|
42
|
+
export interface AgentHandler {
|
|
43
|
+
describe(): HandlerDescription | null;
|
|
44
|
+
init(context?: InitContext): Promise<void>;
|
|
45
|
+
execute(request?: RunRequest, context?: RunContext): Promise<RunResponse>;
|
|
46
|
+
dispose(): Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
export interface AgentManifest {
|
|
49
|
+
readonly defaultAgent: string;
|
|
50
|
+
readonly agents: ReadonlyMap<string, AgentHandler>;
|
|
51
|
+
}
|
|
52
|
+
export interface AgentRouter {
|
|
53
|
+
readonly manifest: AgentManifest;
|
|
54
|
+
run(agentName: string, request: RunRequest, context?: RunContext): Promise<RunResponse>;
|
|
55
|
+
describe(agentName: string): HandlerDescription | null;
|
|
56
|
+
agents(): string[];
|
|
57
|
+
defaultAgent(): string;
|
|
58
|
+
dispose(): Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
export interface ChatMessage {
|
|
61
|
+
role: "system" | "user" | "assistant";
|
|
62
|
+
content: string;
|
|
63
|
+
}
|
|
64
|
+
export interface ChatRequest {
|
|
65
|
+
model?: string | undefined;
|
|
66
|
+
messages: ChatMessage[];
|
|
67
|
+
stream?: boolean | undefined;
|
|
68
|
+
}
|
|
69
|
+
export interface UsageMetadata {
|
|
70
|
+
prompt_tokens?: number | undefined;
|
|
71
|
+
completion_tokens?: number | undefined;
|
|
72
|
+
total_tokens?: number | undefined;
|
|
73
|
+
}
|
|
74
|
+
export interface ChatDelta {
|
|
75
|
+
role?: "assistant" | undefined;
|
|
76
|
+
content?: string | undefined;
|
|
77
|
+
}
|
|
78
|
+
export interface ChatChunk {
|
|
79
|
+
id?: string | undefined;
|
|
80
|
+
object?: "chat.completion.chunk" | undefined;
|
|
81
|
+
created?: number | undefined;
|
|
82
|
+
model?: string | undefined;
|
|
83
|
+
choices: [
|
|
84
|
+
{
|
|
85
|
+
index?: number | undefined;
|
|
86
|
+
delta: ChatDelta;
|
|
87
|
+
finish_reason?: "stop" | "length" | "error" | null | undefined;
|
|
88
|
+
}
|
|
89
|
+
];
|
|
90
|
+
usage?: UsageMetadata | undefined;
|
|
91
|
+
/** Present only on in-band error frames (finish_reason: 'error'). */
|
|
92
|
+
error?: {
|
|
93
|
+
message: string;
|
|
94
|
+
} | undefined;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* AHI resolver function — same shape as the resolver produced by createRouter
|
|
98
|
+
* in @rcrsr/rill-agent. Defined here to avoid importing a runtime value for a
|
|
99
|
+
* type-only position.
|
|
100
|
+
*/
|
|
101
|
+
export type AhiResolver = (agentName: string, request: RunRequest) => Promise<RunResponse>;
|
|
102
|
+
export interface ChatHarnessOptions {
|
|
103
|
+
routes?: {
|
|
104
|
+
openai?: boolean | undefined;
|
|
105
|
+
perAgent?: boolean | undefined;
|
|
106
|
+
defaultAgent?: boolean | undefined;
|
|
107
|
+
discovery?: boolean | undefined;
|
|
108
|
+
} | undefined;
|
|
109
|
+
cors?: boolean | undefined;
|
|
110
|
+
port?: number | undefined;
|
|
111
|
+
}
|
|
112
|
+
export interface ChatHarness {
|
|
113
|
+
listen(port?: number): Promise<void>;
|
|
114
|
+
close(): Promise<void>;
|
|
115
|
+
readonly app: Hono;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Create a chat harness wrapping an AgentRouter.
|
|
119
|
+
*
|
|
120
|
+
* Routes (all configurable via options.routes):
|
|
121
|
+
* POST /v1/chat/completions — OpenAI-compatible; model field selects agent
|
|
122
|
+
* POST /agents/:name/chat — per-agent chat
|
|
123
|
+
* POST /chat — default agent chat
|
|
124
|
+
* GET /agents — discovery; chat-eligible agents only
|
|
125
|
+
*
|
|
126
|
+
* Always-present routes:
|
|
127
|
+
* GET /health — returns 200 "OK"
|
|
128
|
+
* GET /metrics — returns JSON metrics counters
|
|
129
|
+
*
|
|
130
|
+
* Throws ChatSignatureError if defaultAgent route is enabled but the default
|
|
131
|
+
* agent is missing or its declared signature is not chat-eligible (see
|
|
132
|
+
* inspectChatHandler).
|
|
133
|
+
*/
|
|
134
|
+
export declare function createChatHarness(router: AgentRouter, options?: ChatHarnessOptions): ChatHarness;
|
|
135
|
+
/**
|
|
136
|
+
* Error thrown at factory time when the default agent route is enabled but
|
|
137
|
+
* the default agent is missing, or its declared signature is not
|
|
138
|
+
* chat-eligible. Not mapped to an HTTP status; indicates misconfiguration at
|
|
139
|
+
* startup.
|
|
140
|
+
*/
|
|
141
|
+
export declare class ChatSignatureError extends Error {
|
|
142
|
+
constructor(message: string);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Chat eligibility validates the handler's declared signature via describe().
|
|
146
|
+
*
|
|
147
|
+
* The contract: a chat handler is a stream closure with the canonical chat
|
|
148
|
+
* input shape and the canonical chat chunk shape. Concretely, the handler's
|
|
149
|
+
* `describe()` must report:
|
|
150
|
+
* - exactly one required param named `messages` with type
|
|
151
|
+
* `list(dict(role: string, content: string))`;
|
|
152
|
+
* - a `returnType` of the form `stream(<EXPECTED_CHUNK_TYPE>)` or
|
|
153
|
+
* `stream(<EXPECTED_CHUNK_TYPE>):<resolution>` where the resolution type
|
|
154
|
+
* is unconstrained — the harness only consumes chunks via onChunk and
|
|
155
|
+
* leaves the closure free to resolve to a string, number, dict, etc.
|
|
156
|
+
*
|
|
157
|
+
* Any deviation in either the param shape or the chunk shape rejects the
|
|
158
|
+
* handler at `createChatHarness()` time rather than failing on the first
|
|
159
|
+
* request. Both fields are emitted by rill-build ≥ 0.19.6 (paired with
|
|
160
|
+
* rill ≥ 0.19.3).
|
|
161
|
+
*/
|
|
162
|
+
export declare function inspectChatHandler(handler: AgentHandler): {
|
|
163
|
+
eligible: true;
|
|
164
|
+
} | {
|
|
165
|
+
eligible: false;
|
|
166
|
+
reason: string;
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* Validates that value is a non-empty array of well-formed ChatMessage objects.
|
|
170
|
+
* Returns a discriminated union: success carries the narrowed ChatMessage array,
|
|
171
|
+
* failure carries a human-readable error string.
|
|
172
|
+
*/
|
|
173
|
+
export declare function validateMessages(value: unknown): {
|
|
174
|
+
valid: true;
|
|
175
|
+
messages: ChatMessage[];
|
|
176
|
+
} | {
|
|
177
|
+
valid: false;
|
|
178
|
+
error: string;
|
|
179
|
+
};
|
|
180
|
+
export interface RillHarnessLogger {
|
|
181
|
+
info(...args: unknown[]): void;
|
|
182
|
+
warn(...args: unknown[]): void;
|
|
183
|
+
error(...args: unknown[]): void;
|
|
184
|
+
}
|
|
185
|
+
export interface RillCompiledPackage {
|
|
186
|
+
readonly mount: string;
|
|
187
|
+
readonly buildOutput: {
|
|
188
|
+
readonly outputPath: string;
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
export interface RillServeContext {
|
|
192
|
+
readonly config: Record<string, unknown>;
|
|
193
|
+
readonly logger: RillHarnessLogger;
|
|
194
|
+
readonly packages: readonly RillCompiledPackage[];
|
|
195
|
+
readonly requestedMount: string | undefined;
|
|
196
|
+
readonly args: readonly string[];
|
|
197
|
+
readonly onShutdown: (handler: () => void | Promise<void>) => void;
|
|
198
|
+
readonly onSourceChange: (handler: () => void | Promise<void>) => void;
|
|
199
|
+
}
|
|
200
|
+
export interface RillPostBuildContext {
|
|
201
|
+
readonly outputDir: string;
|
|
202
|
+
readonly packages: readonly RillCompiledPackage[];
|
|
203
|
+
readonly logger: RillHarnessLogger;
|
|
204
|
+
}
|
|
205
|
+
export interface RillHarness {
|
|
206
|
+
readonly name: string;
|
|
207
|
+
readonly postBuild?: (ctx: RillPostBuildContext) => Promise<void>;
|
|
208
|
+
readonly serve?: (ctx: RillServeContext) => Promise<number>;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Default export consumed by the rill CLI (`rill install --replace`,
|
|
212
|
+
* `rill run`) when this package is declared as a bundle harness. `serve`
|
|
213
|
+
* assembles a router from the bundle's compiled packages and hosts it over the
|
|
214
|
+
* OpenAI-compatible chat harness on `config.port` (default 3000).
|
|
215
|
+
*/
|
|
216
|
+
declare const harness: RillHarness;
|
|
217
|
+
|
|
218
|
+
export {
|
|
219
|
+
harness as default,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
// src/harness.ts
|
|
2
|
+
import { cors } from "hono/cors";
|
|
3
|
+
|
|
4
|
+
// ../../shared/hono-kit/src/index.ts
|
|
5
|
+
import { existsSync } from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { Hono } from "hono";
|
|
8
|
+
import { serve } from "@hono/node-server";
|
|
9
|
+
function assertJsonObject(parsed) {
|
|
10
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
11
|
+
throw new Error("Request body must be a JSON object");
|
|
12
|
+
}
|
|
13
|
+
return parsed;
|
|
14
|
+
}
|
|
15
|
+
function createHarnessLifecycle(options) {
|
|
16
|
+
const app = new Hono();
|
|
17
|
+
let server;
|
|
18
|
+
async function listen(port) {
|
|
19
|
+
if (server !== void 0) {
|
|
20
|
+
throw new Error("Server is already listening");
|
|
21
|
+
}
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
server = serve({ fetch: app.fetch, port }, () => {
|
|
24
|
+
options?.serverTweaks?.(server);
|
|
25
|
+
resolve();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
async function close() {
|
|
30
|
+
if (server !== void 0) {
|
|
31
|
+
server.close();
|
|
32
|
+
server = void 0;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return { app, listen, close };
|
|
36
|
+
}
|
|
37
|
+
function compiledPackageEntries(ctx) {
|
|
38
|
+
return ctx.packages.map((p) => ({
|
|
39
|
+
name: p.mount,
|
|
40
|
+
dir: p.buildOutput.outputPath
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
function readHarnessPort(config, fallback) {
|
|
44
|
+
const p = config["port"];
|
|
45
|
+
if (typeof p === "number" && Number.isInteger(p)) return p;
|
|
46
|
+
if (typeof p === "string" && /^\d+$/.test(p)) return Number(p);
|
|
47
|
+
return fallback;
|
|
48
|
+
}
|
|
49
|
+
function assertCompiledHandlers(ctx) {
|
|
50
|
+
for (const pkg of ctx.packages) {
|
|
51
|
+
const handlerPath = path.join(pkg.buildOutput.outputPath, "handler.js");
|
|
52
|
+
if (!existsSync(handlerPath)) {
|
|
53
|
+
throw new Error(`missing handler file: ${handlerPath}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function runRillServe(ctx, start) {
|
|
58
|
+
const handle = await start(compiledPackageEntries(ctx));
|
|
59
|
+
ctx.onShutdown(async () => {
|
|
60
|
+
await handle.close();
|
|
61
|
+
});
|
|
62
|
+
return new Promise(() => {
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/errors.ts
|
|
67
|
+
var ChatSignatureError = class extends Error {
|
|
68
|
+
constructor(message) {
|
|
69
|
+
super(message);
|
|
70
|
+
this.name = "ChatSignatureError";
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var ChatChunkError = class extends Error {
|
|
74
|
+
/** HTTP status code for this error. */
|
|
75
|
+
statusCode;
|
|
76
|
+
constructor(message) {
|
|
77
|
+
super(message);
|
|
78
|
+
this.name = "ChatChunkError";
|
|
79
|
+
this.statusCode = 500;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// src/eligibility.ts
|
|
84
|
+
var EXPECTED_MESSAGES_TYPE = "list(dict(role: string, content: string))";
|
|
85
|
+
var EXPECTED_CHUNK_TYPE = "dict(choices: list(dict(delta: dict(role: string, content: string), finish_reason: string)))";
|
|
86
|
+
function extractStreamChunkType(returnType) {
|
|
87
|
+
const prefix = "stream(";
|
|
88
|
+
if (!returnType.startsWith(prefix)) return null;
|
|
89
|
+
let depth = 1;
|
|
90
|
+
const start = prefix.length;
|
|
91
|
+
for (let i = start; i < returnType.length; i++) {
|
|
92
|
+
const c = returnType[i];
|
|
93
|
+
if (c === "(") {
|
|
94
|
+
depth++;
|
|
95
|
+
} else if (c === ")") {
|
|
96
|
+
depth--;
|
|
97
|
+
if (depth === 0) {
|
|
98
|
+
return returnType.slice(start, i);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
function inspectChatHandler(handler) {
|
|
105
|
+
const description = handler.describe();
|
|
106
|
+
if (description === null) {
|
|
107
|
+
return {
|
|
108
|
+
eligible: false,
|
|
109
|
+
reason: "handler.describe() returned null"
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const params = description.params;
|
|
113
|
+
if (params.length !== 1) {
|
|
114
|
+
return {
|
|
115
|
+
eligible: false,
|
|
116
|
+
reason: `expected exactly one param "messages", got ${params.length}`
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const p = params[0];
|
|
120
|
+
if (p === void 0) {
|
|
121
|
+
return {
|
|
122
|
+
eligible: false,
|
|
123
|
+
reason: "handler.describe() returned an empty params slot"
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (p.name !== "messages") {
|
|
127
|
+
return {
|
|
128
|
+
eligible: false,
|
|
129
|
+
reason: `expected param "messages", got "${p.name}"`
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (!p.required) {
|
|
133
|
+
return {
|
|
134
|
+
eligible: false,
|
|
135
|
+
reason: 'expected param "messages" to be required'
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
if (p.type !== EXPECTED_MESSAGES_TYPE) {
|
|
139
|
+
return {
|
|
140
|
+
eligible: false,
|
|
141
|
+
reason: `expected param "messages" of type "${EXPECTED_MESSAGES_TYPE}", got "${p.type}"`
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (description.returnType === void 0) {
|
|
145
|
+
return {
|
|
146
|
+
eligible: false,
|
|
147
|
+
reason: "handler.describe() did not report a returnType (rebuild with rill-cli >= 0.19.6)"
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
const chunkType = extractStreamChunkType(description.returnType);
|
|
151
|
+
if (chunkType === null) {
|
|
152
|
+
return {
|
|
153
|
+
eligible: false,
|
|
154
|
+
reason: `expected return type "stream(<chunk>)" or "stream(<chunk>):<ret>", got "${description.returnType}"`
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (chunkType !== EXPECTED_CHUNK_TYPE) {
|
|
158
|
+
return {
|
|
159
|
+
eligible: false,
|
|
160
|
+
reason: `expected stream chunk type "${EXPECTED_CHUNK_TYPE}", got "${chunkType}"`
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return { eligible: true };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/stream.ts
|
|
167
|
+
var encoder = new TextEncoder();
|
|
168
|
+
function toAsyncIterable(source) {
|
|
169
|
+
if (Symbol.asyncIterator in source) {
|
|
170
|
+
return source;
|
|
171
|
+
}
|
|
172
|
+
return source;
|
|
173
|
+
}
|
|
174
|
+
function fillChunk(chunk, defaults) {
|
|
175
|
+
return {
|
|
176
|
+
id: chunk.id ?? defaults.id,
|
|
177
|
+
object: chunk.object ?? "chat.completion.chunk",
|
|
178
|
+
created: chunk.created ?? defaults.created,
|
|
179
|
+
model: chunk.model ?? defaults.model,
|
|
180
|
+
choices: chunk.choices,
|
|
181
|
+
...chunk.usage !== void 0 ? { usage: chunk.usage } : {}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function encodeChunk(chunk) {
|
|
185
|
+
return encoder.encode(`data: ${JSON.stringify(chunk)}
|
|
186
|
+
|
|
187
|
+
`);
|
|
188
|
+
}
|
|
189
|
+
var DONE_FRAME = encoder.encode("data: [DONE]\n\n");
|
|
190
|
+
function buildErrorChunk(defaults) {
|
|
191
|
+
return {
|
|
192
|
+
id: defaults.id,
|
|
193
|
+
object: "chat.completion.chunk",
|
|
194
|
+
created: defaults.created,
|
|
195
|
+
model: defaults.model,
|
|
196
|
+
choices: [{ index: 0, delta: {}, finish_reason: "error" }],
|
|
197
|
+
error: { message: "Internal server error" }
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
async function createChatStreamResponse(source, options) {
|
|
201
|
+
const iterable = toAsyncIterable(source);
|
|
202
|
+
const iter = iterable[Symbol.asyncIterator]();
|
|
203
|
+
const streamId = `chatcmpl-${crypto.randomUUID()}`;
|
|
204
|
+
const created = Math.floor(Date.now() / 1e3);
|
|
205
|
+
const defaults = { id: streamId, created, model: options.model };
|
|
206
|
+
const firstResult = await iter.next();
|
|
207
|
+
if (firstResult.done === true) {
|
|
208
|
+
const body2 = new ReadableStream({
|
|
209
|
+
start(controller) {
|
|
210
|
+
controller.enqueue(DONE_FRAME);
|
|
211
|
+
controller.close();
|
|
212
|
+
},
|
|
213
|
+
cancel() {
|
|
214
|
+
options.abortController.abort();
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
return new Response(body2, { status: 200, headers: sseHeaders() });
|
|
218
|
+
}
|
|
219
|
+
const firstChunk = fillChunk(firstResult.value, defaults);
|
|
220
|
+
const body = new ReadableStream({
|
|
221
|
+
start(controller) {
|
|
222
|
+
(async () => {
|
|
223
|
+
controller.enqueue(encodeChunk(firstChunk));
|
|
224
|
+
let result = await iter.next();
|
|
225
|
+
while (result.done !== true) {
|
|
226
|
+
controller.enqueue(encodeChunk(fillChunk(result.value, defaults)));
|
|
227
|
+
result = await iter.next();
|
|
228
|
+
}
|
|
229
|
+
controller.enqueue(DONE_FRAME);
|
|
230
|
+
controller.close();
|
|
231
|
+
})().catch((err) => {
|
|
232
|
+
if (options.abortController.signal.aborted || controller.desiredSize === null) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
console.error(err);
|
|
236
|
+
options.onError?.(err);
|
|
237
|
+
try {
|
|
238
|
+
controller.enqueue(encodeChunk(buildErrorChunk(defaults)));
|
|
239
|
+
controller.enqueue(DONE_FRAME);
|
|
240
|
+
controller.close();
|
|
241
|
+
} catch {
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
},
|
|
245
|
+
cancel() {
|
|
246
|
+
options.abortController.abort();
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
return new Response(body, { status: 200, headers: sseHeaders() });
|
|
250
|
+
}
|
|
251
|
+
function checkChunkForError(chunk) {
|
|
252
|
+
const finishReason = chunk.choices[0]?.finish_reason;
|
|
253
|
+
if (finishReason === "error" || chunk.error !== void 0) {
|
|
254
|
+
throw new ChatChunkError(chunk.error?.message ?? "stream error");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
async function createChatCompletionResponse(source, options) {
|
|
258
|
+
const iterable = toAsyncIterable(source);
|
|
259
|
+
const iter = iterable[Symbol.asyncIterator]();
|
|
260
|
+
const id = `chatcmpl-${crypto.randomUUID()}`;
|
|
261
|
+
const created = Math.floor(Date.now() / 1e3);
|
|
262
|
+
const firstResult = await iter.next();
|
|
263
|
+
let content = "";
|
|
264
|
+
let lastFinishReason;
|
|
265
|
+
let usage;
|
|
266
|
+
if (firstResult.done !== true) {
|
|
267
|
+
checkChunkForError(firstResult.value);
|
|
268
|
+
({ content, lastFinishReason, usage } = accumulateChunk(
|
|
269
|
+
firstResult.value,
|
|
270
|
+
content,
|
|
271
|
+
usage
|
|
272
|
+
));
|
|
273
|
+
let result = await iter.next();
|
|
274
|
+
while (result.done !== true) {
|
|
275
|
+
checkChunkForError(result.value);
|
|
276
|
+
({ content, lastFinishReason, usage } = accumulateChunk(
|
|
277
|
+
result.value,
|
|
278
|
+
content,
|
|
279
|
+
usage
|
|
280
|
+
));
|
|
281
|
+
result = await iter.next();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const finishReason = lastFinishReason !== void 0 && lastFinishReason !== null ? lastFinishReason : "stop";
|
|
285
|
+
const body = {
|
|
286
|
+
id,
|
|
287
|
+
object: "chat.completion",
|
|
288
|
+
created,
|
|
289
|
+
model: options.model,
|
|
290
|
+
choices: [
|
|
291
|
+
{
|
|
292
|
+
index: 0,
|
|
293
|
+
message: { role: "assistant", content },
|
|
294
|
+
finish_reason: finishReason
|
|
295
|
+
}
|
|
296
|
+
],
|
|
297
|
+
...usage !== void 0 ? { usage } : {}
|
|
298
|
+
};
|
|
299
|
+
return new Response(JSON.stringify(body), {
|
|
300
|
+
status: 200,
|
|
301
|
+
headers: { "Content-Type": "application/json" }
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
function accumulateChunk(chunk, content, usage) {
|
|
305
|
+
const delta = chunk.choices[0]?.delta.content;
|
|
306
|
+
const nextContent = delta !== void 0 ? content + delta : content;
|
|
307
|
+
const nextUsage = chunk.usage !== void 0 ? chunk.usage : usage;
|
|
308
|
+
return {
|
|
309
|
+
content: nextContent,
|
|
310
|
+
lastFinishReason: chunk.choices[0]?.finish_reason,
|
|
311
|
+
usage: nextUsage
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
function sseHeaders() {
|
|
315
|
+
return {
|
|
316
|
+
"Content-Type": "text/event-stream",
|
|
317
|
+
"Cache-Control": "no-cache",
|
|
318
|
+
Connection: "keep-alive"
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// src/validate.ts
|
|
323
|
+
var VALID_ROLES = ["system", "user", "assistant"];
|
|
324
|
+
var MAX_MESSAGES = 1e4;
|
|
325
|
+
var MAX_CONTENT_LENGTH = 32e3;
|
|
326
|
+
function validateMessages(value) {
|
|
327
|
+
if (!Array.isArray(value)) {
|
|
328
|
+
return { valid: false, error: "messages must be an array" };
|
|
329
|
+
}
|
|
330
|
+
if (value.length === 0) {
|
|
331
|
+
return { valid: false, error: "messages must be a non-empty array" };
|
|
332
|
+
}
|
|
333
|
+
if (value.length > MAX_MESSAGES) {
|
|
334
|
+
return {
|
|
335
|
+
valid: false,
|
|
336
|
+
error: `messages must not exceed ${MAX_MESSAGES} items`
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
const messages = [];
|
|
340
|
+
for (let i = 0; i < value.length; i++) {
|
|
341
|
+
const item = value[i];
|
|
342
|
+
if (typeof item !== "object" || item === null || !("role" in item) || !VALID_ROLES.includes(
|
|
343
|
+
item["role"]
|
|
344
|
+
)) {
|
|
345
|
+
return {
|
|
346
|
+
valid: false,
|
|
347
|
+
error: `messages[${i}].role must be one of: system, user, assistant`
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
const record = item;
|
|
351
|
+
if (typeof record["content"] !== "string") {
|
|
352
|
+
return {
|
|
353
|
+
valid: false,
|
|
354
|
+
error: `messages[${i}].content must be a string`
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
if (record["content"].length > MAX_CONTENT_LENGTH) {
|
|
358
|
+
return {
|
|
359
|
+
valid: false,
|
|
360
|
+
error: `messages[${i}].content must not exceed ${MAX_CONTENT_LENGTH} characters`
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
messages.push({
|
|
364
|
+
role: record["role"],
|
|
365
|
+
content: record["content"]
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
return { valid: true, messages };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// src/harness.ts
|
|
372
|
+
function getHandlerFromRouter(router, name) {
|
|
373
|
+
return router.manifest.agents.get(name);
|
|
374
|
+
}
|
|
375
|
+
function invokeHandlerAsStream(handler, messages, abortController) {
|
|
376
|
+
return new ReadableStream({
|
|
377
|
+
start(controller) {
|
|
378
|
+
void (async () => {
|
|
379
|
+
try {
|
|
380
|
+
await handler.execute(
|
|
381
|
+
{ params: { messages } },
|
|
382
|
+
{
|
|
383
|
+
signal: abortController.signal,
|
|
384
|
+
onChunk: async (chunk) => {
|
|
385
|
+
if (abortController.signal.aborted) return;
|
|
386
|
+
controller.enqueue(chunk);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
);
|
|
390
|
+
controller.close();
|
|
391
|
+
} catch (err) {
|
|
392
|
+
controller.error(err);
|
|
393
|
+
}
|
|
394
|
+
})();
|
|
395
|
+
},
|
|
396
|
+
cancel() {
|
|
397
|
+
abortController.abort();
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
function createChatHarness(router, options) {
|
|
402
|
+
if (router === null || router === void 0) {
|
|
403
|
+
throw new TypeError("router is required");
|
|
404
|
+
}
|
|
405
|
+
const routeFlags = {
|
|
406
|
+
openai: options?.routes?.openai !== false,
|
|
407
|
+
perAgent: options?.routes?.perAgent !== false,
|
|
408
|
+
defaultAgent: options?.routes?.defaultAgent !== false,
|
|
409
|
+
discovery: options?.routes?.discovery !== false
|
|
410
|
+
};
|
|
411
|
+
const eligibleAgents = /* @__PURE__ */ new Set();
|
|
412
|
+
const ineligibleReasons = /* @__PURE__ */ new Map();
|
|
413
|
+
for (const name of router.agents()) {
|
|
414
|
+
const handler = getHandlerFromRouter(router, name);
|
|
415
|
+
if (handler !== void 0) {
|
|
416
|
+
const result = inspectChatHandler(handler);
|
|
417
|
+
if (result.eligible) {
|
|
418
|
+
eligibleAgents.add(name);
|
|
419
|
+
} else {
|
|
420
|
+
ineligibleReasons.set(name, result.reason);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const defaultAgentName = router.defaultAgent();
|
|
425
|
+
if (routeFlags.defaultAgent) {
|
|
426
|
+
if (defaultAgentName === "" || !router.agents().includes(defaultAgentName)) {
|
|
427
|
+
throw new ChatSignatureError(
|
|
428
|
+
`Default agent "${defaultAgentName}" is not a known agent`
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
if (!eligibleAgents.has(defaultAgentName)) {
|
|
432
|
+
const reason = ineligibleReasons.get(defaultAgentName) ?? "unknown reason";
|
|
433
|
+
throw new ChatSignatureError(
|
|
434
|
+
`Default agent "${defaultAgentName}" has incompatible chat signature: ${reason}`
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const lifecycle = createHarnessLifecycle();
|
|
439
|
+
const app = lifecycle.app;
|
|
440
|
+
let requests = 0;
|
|
441
|
+
let errors = 0;
|
|
442
|
+
let activeConnections = 0;
|
|
443
|
+
if (options?.cors === true) {
|
|
444
|
+
app.use("*", cors());
|
|
445
|
+
}
|
|
446
|
+
app.get("/health", (c) => c.text("OK", 200));
|
|
447
|
+
app.get(
|
|
448
|
+
"/metrics",
|
|
449
|
+
(c) => c.json({
|
|
450
|
+
requests,
|
|
451
|
+
errors,
|
|
452
|
+
active_connections: activeConnections
|
|
453
|
+
})
|
|
454
|
+
);
|
|
455
|
+
async function streamChatResponse(source, resolvedAgent, abortController) {
|
|
456
|
+
activeConnections++;
|
|
457
|
+
const { readable, writable } = new TransformStream();
|
|
458
|
+
(async () => {
|
|
459
|
+
const writer = writable.getWriter();
|
|
460
|
+
try {
|
|
461
|
+
if (Symbol.asyncIterator in source) {
|
|
462
|
+
for await (const chunk of source) {
|
|
463
|
+
if (abortController.signal.aborted) break;
|
|
464
|
+
await writer.write(chunk);
|
|
465
|
+
}
|
|
466
|
+
} else {
|
|
467
|
+
const reader = source.getReader();
|
|
468
|
+
try {
|
|
469
|
+
let result = await reader.read();
|
|
470
|
+
while (!result.done) {
|
|
471
|
+
if (abortController.signal.aborted) {
|
|
472
|
+
await reader.cancel();
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
await writer.write(result.value);
|
|
476
|
+
result = await reader.read();
|
|
477
|
+
}
|
|
478
|
+
} finally {
|
|
479
|
+
reader.releaseLock();
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (abortController.signal.aborted) {
|
|
483
|
+
await writer.abort();
|
|
484
|
+
} else {
|
|
485
|
+
await writer.close();
|
|
486
|
+
}
|
|
487
|
+
} catch (err) {
|
|
488
|
+
await writer.abort(err);
|
|
489
|
+
} finally {
|
|
490
|
+
activeConnections--;
|
|
491
|
+
requests++;
|
|
492
|
+
}
|
|
493
|
+
})().catch(() => {
|
|
494
|
+
});
|
|
495
|
+
try {
|
|
496
|
+
return await createChatStreamResponse(readable, {
|
|
497
|
+
model: resolvedAgent,
|
|
498
|
+
abortController,
|
|
499
|
+
onError: () => {
|
|
500
|
+
errors++;
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
} catch (err) {
|
|
504
|
+
console.error(err);
|
|
505
|
+
errors++;
|
|
506
|
+
return new Response(
|
|
507
|
+
JSON.stringify({ error: { message: "Internal server error" } }),
|
|
508
|
+
{
|
|
509
|
+
status: 500,
|
|
510
|
+
headers: { "Content-Type": "application/json" }
|
|
511
|
+
}
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
async function bufferedChatResponse(source, resolvedAgent) {
|
|
516
|
+
activeConnections++;
|
|
517
|
+
try {
|
|
518
|
+
return await createChatCompletionResponse(source, {
|
|
519
|
+
model: resolvedAgent
|
|
520
|
+
});
|
|
521
|
+
} catch (err) {
|
|
522
|
+
errors++;
|
|
523
|
+
if (err instanceof ChatChunkError) {
|
|
524
|
+
return new Response(
|
|
525
|
+
JSON.stringify({ error: { message: err.message } }),
|
|
526
|
+
{
|
|
527
|
+
status: err.statusCode,
|
|
528
|
+
headers: { "Content-Type": "application/json" }
|
|
529
|
+
}
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
console.error(err);
|
|
533
|
+
return new Response(
|
|
534
|
+
JSON.stringify({ error: { message: "Internal server error" } }),
|
|
535
|
+
{
|
|
536
|
+
status: 500,
|
|
537
|
+
headers: { "Content-Type": "application/json" }
|
|
538
|
+
}
|
|
539
|
+
);
|
|
540
|
+
} finally {
|
|
541
|
+
activeConnections--;
|
|
542
|
+
requests++;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
function dispatchChatResponse(source, resolvedAgent, abortController, wantsStream) {
|
|
546
|
+
return wantsStream ? streamChatResponse(source, resolvedAgent, abortController) : bufferedChatResponse(source, resolvedAgent);
|
|
547
|
+
}
|
|
548
|
+
async function parseChatBody(c) {
|
|
549
|
+
try {
|
|
550
|
+
const parsed = await c.req.json();
|
|
551
|
+
return assertJsonObject(parsed);
|
|
552
|
+
} catch (err) {
|
|
553
|
+
return c.json(
|
|
554
|
+
{
|
|
555
|
+
error: {
|
|
556
|
+
message: err instanceof Error ? err.message : "Invalid JSON"
|
|
557
|
+
}
|
|
558
|
+
},
|
|
559
|
+
400
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
async function handleChatRequest(c, body, agentName, defaultFallback) {
|
|
564
|
+
const validation = validateMessages(body["messages"]);
|
|
565
|
+
if (!validation.valid) {
|
|
566
|
+
errors++;
|
|
567
|
+
requests++;
|
|
568
|
+
return c.json({ error: { message: validation.error } }, 400);
|
|
569
|
+
}
|
|
570
|
+
let resolvedAgent = agentName;
|
|
571
|
+
if (!router.agents().includes(resolvedAgent) || !eligibleAgents.has(resolvedAgent)) {
|
|
572
|
+
if (defaultFallback && eligibleAgents.has(defaultAgentName)) {
|
|
573
|
+
resolvedAgent = defaultAgentName;
|
|
574
|
+
} else if (defaultFallback) {
|
|
575
|
+
errors++;
|
|
576
|
+
requests++;
|
|
577
|
+
return c.json(
|
|
578
|
+
{ error: { message: "No default agent configured" } },
|
|
579
|
+
500
|
|
580
|
+
);
|
|
581
|
+
} else {
|
|
582
|
+
errors++;
|
|
583
|
+
requests++;
|
|
584
|
+
return c.json(
|
|
585
|
+
{ error: { message: `Agent "${agentName}" not found` } },
|
|
586
|
+
404
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
const chatReq = {
|
|
591
|
+
messages: validation.messages,
|
|
592
|
+
...typeof body["model"] === "string" ? { model: body["model"] } : {},
|
|
593
|
+
...typeof body["stream"] === "boolean" ? { stream: body["stream"] } : {}
|
|
594
|
+
};
|
|
595
|
+
const abortController = new AbortController();
|
|
596
|
+
const handler = getHandlerFromRouter(router, resolvedAgent);
|
|
597
|
+
if (handler === void 0) {
|
|
598
|
+
errors++;
|
|
599
|
+
requests++;
|
|
600
|
+
return c.json(
|
|
601
|
+
{
|
|
602
|
+
error: { message: `Agent "${resolvedAgent}" handler not accessible` }
|
|
603
|
+
},
|
|
604
|
+
500
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
const source = invokeHandlerAsStream(
|
|
608
|
+
handler,
|
|
609
|
+
chatReq.messages,
|
|
610
|
+
abortController
|
|
611
|
+
);
|
|
612
|
+
const wantsStream = chatReq.stream === true;
|
|
613
|
+
return dispatchChatResponse(
|
|
614
|
+
source,
|
|
615
|
+
resolvedAgent,
|
|
616
|
+
abortController,
|
|
617
|
+
wantsStream
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
if (routeFlags.perAgent) {
|
|
621
|
+
app.post("/agents/:name/chat", async (c) => {
|
|
622
|
+
const body = await parseChatBody(c);
|
|
623
|
+
if (body instanceof Response) {
|
|
624
|
+
errors++;
|
|
625
|
+
requests++;
|
|
626
|
+
return body;
|
|
627
|
+
}
|
|
628
|
+
const name = c.req.param("name");
|
|
629
|
+
return handleChatRequest(c, body, name, false);
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
if (routeFlags.openai) {
|
|
633
|
+
app.post("/v1/chat/completions", async (c) => {
|
|
634
|
+
const body = await parseChatBody(c);
|
|
635
|
+
if (body instanceof Response) {
|
|
636
|
+
errors++;
|
|
637
|
+
requests++;
|
|
638
|
+
return body;
|
|
639
|
+
}
|
|
640
|
+
const agentName = typeof body["model"] === "string" && body["model"] !== "" ? body["model"] : defaultAgentName;
|
|
641
|
+
return handleChatRequest(c, body, agentName, true);
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
if (routeFlags.defaultAgent) {
|
|
645
|
+
app.post("/chat", async (c) => {
|
|
646
|
+
const body = await parseChatBody(c);
|
|
647
|
+
if (body instanceof Response) {
|
|
648
|
+
errors++;
|
|
649
|
+
requests++;
|
|
650
|
+
return body;
|
|
651
|
+
}
|
|
652
|
+
return handleChatRequest(c, body, defaultAgentName, true);
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
if (routeFlags.discovery) {
|
|
656
|
+
app.get("/agents", (c) => {
|
|
657
|
+
requests++;
|
|
658
|
+
const agents = router.agents().filter((n) => eligibleAgents.has(n)).map((n) => ({
|
|
659
|
+
name: n,
|
|
660
|
+
description: router.describe(n)?.description,
|
|
661
|
+
default: n === defaultAgentName
|
|
662
|
+
}));
|
|
663
|
+
return c.json({ agents });
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
return {
|
|
667
|
+
app,
|
|
668
|
+
listen: (port) => lifecycle.listen(port ?? options?.port ?? 3e3),
|
|
669
|
+
close: lifecycle.close
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// src/index.ts
|
|
674
|
+
import { createRouter, assembleManifest } from "@rcrsr/rill-agent";
|
|
675
|
+
var HARNESS_NAME = "@rcrsr/rill-agent-chat";
|
|
676
|
+
var harness = {
|
|
677
|
+
name: HARNESS_NAME,
|
|
678
|
+
postBuild: async (ctx) => {
|
|
679
|
+
assertCompiledHandlers(ctx);
|
|
680
|
+
},
|
|
681
|
+
serve: (ctx) => runRillServe(ctx, async (entries) => {
|
|
682
|
+
const router = await createRouter(await assembleManifest(entries));
|
|
683
|
+
const port = readHarnessPort(ctx.config, 3e3);
|
|
684
|
+
const server = createChatHarness(router, { port });
|
|
685
|
+
await server.listen(port);
|
|
686
|
+
ctx.logger.info(`[${HARNESS_NAME}] listening on :${port}`);
|
|
687
|
+
return server;
|
|
688
|
+
})
|
|
689
|
+
};
|
|
690
|
+
var index_default = harness;
|
|
691
|
+
export {
|
|
692
|
+
ChatSignatureError,
|
|
693
|
+
createChatHarness,
|
|
694
|
+
index_default as default,
|
|
695
|
+
inspectChatHandler,
|
|
696
|
+
validateMessages
|
|
697
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rcrsr/rill-agent-chat",
|
|
3
|
+
"version": "0.20.0",
|
|
4
|
+
"description": "rill agent chat harness — Hono-based chat endpoint wrapping AgentRouter",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Andre Bremer",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"rill": {
|
|
9
|
+
"role": "harness"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsup && dts-bundle-generator --config dts-bundle-generator.config.cjs && node -e \"const fs=require('fs');const walk=(d)=>fs.readdirSync(d,{withFileTypes:true}).flatMap(e=>e.isDirectory()?walk(d+'/'+e.name):[d+'/'+e.name]);const hits=walk('dist').filter(f=>fs.readFileSync(f,'utf8').includes('@rcrsr/rill-agent-hono-kit'));if(hits.length){console.error('hono-kit leak in dist:',hits);process.exit(1);}\"",
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"lint": "oxlint --config ../../../.oxlintrc.json src/ tests/",
|
|
22
|
+
"check": "pnpm run build && pnpm run test && pnpm run lint"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@rcrsr/rill-agent": "workspace:~",
|
|
26
|
+
"@hono/node-server": "^2.0.1",
|
|
27
|
+
"hono": "^4.12.16"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@rcrsr/rill-agent-hono-kit": "workspace:^",
|
|
31
|
+
"dts-bundle-generator": "^9.5.1",
|
|
32
|
+
"tsup": "^8.5.0"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist"
|
|
36
|
+
],
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/rcrsr/rill-agent.git",
|
|
43
|
+
"directory": "packages/agent/chat"
|
|
44
|
+
}
|
|
45
|
+
}
|