@gnldev/server 0.1.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/dist/node.js ADDED
@@ -0,0 +1,94 @@
1
+ // Binding the REST API to a Node server — Express, Fastify, Koa, Nest, or bare `node:http`.
2
+ //
3
+ // The twin of packages/studio/src/node.ts, and deliberately a copy rather than an import. A backend
4
+ // that wants agents and no dashboard had to install @gnldev/studio just to reach this function,
5
+ // which drags @gnldev/studio-ui — a React app — into a service that will never render a page. That
6
+ // is backwards, and one subpath is a smaller price than that dependency.
7
+ //
8
+ // The alternative, importing it from the sibling package, was tried and is worse than it looks: a
9
+ // cross-package runtime import resolves through the sibling's BUILT output, so a stale dist turns
10
+ // the call into `undefined` with no error until something downstream fails on empty input. Measured
11
+ // in this repo the same afternoon, in @gnldev/agui, where it emptied an SSE stream and every test in
12
+ // the package failed on `JSON.parse('')`. This file follows the convention sse.ts already set here.
13
+ //
14
+ // KEEP IN SYNC with packages/studio/src/node.ts. If they drift, the two packages answer the same
15
+ // misconfiguration differently, which is worse than either answer.
16
+ //
17
+ // Kept on a subpath so the root export stays runtime-neutral: importing the package must not drag
18
+ // Node's types into a Workers or Deno build.
19
+ import { getRequestListener } from '@hono/node-server';
20
+ import { EDGE_ERROR_CODES } from './edge-errors.js';
21
+ const CARRIES_BODY = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
22
+ /**
23
+ * True when the request announced a body and something already drained it.
24
+ *
25
+ * A body parser mounted ahead of this handler — `express.json()`, `koa-bodyparser`, Fastify's
26
+ * built-in JSON parser — reads the stream to the end and hands the result to the framework, not to
27
+ * us. What arrives here is a POST with no readable body, and the endpoint answers the only thing it
28
+ * can: "runId is required". Measured on a real Express app: the identical request returns 200 with a
29
+ * model answer without `express.json()`, and that 400 with it. The message accuses the caller of a
30
+ * mistake the caller did not make, which is worse than failing.
31
+ *
32
+ * Detection is deliberately narrow — a body was ANNOUNCED (content-length or chunked) and the
33
+ * readable side is ALREADY finished. A GET nobody read does not match; neither does a bodyless POST.
34
+ */
35
+ function bodyAlreadyConsumed(req) {
36
+ if (!CARRIES_BODY.has(req.method ?? ''))
37
+ return false;
38
+ const announced = Number(req.headers['content-length'] ?? 0) > 0 || req.headers['transfer-encoding'] !== undefined;
39
+ return announced && req.readableEnded;
40
+ }
41
+ /**
42
+ * Turns a fetch handler into a Node request listener.
43
+ *
44
+ * @experimental Closes the SSE/flush class of bug inside the Node bridge — a hand-written bridge
45
+ * that omits `res.flushHeaders()` withholds the head until the first chunk, so a quiet event stream
46
+ * hangs the client forever and nothing throws. It also answers plainly when a body parser upstream
47
+ * has already drained the request, instead of letting the endpoint blame the caller.
48
+ *
49
+ * What it does NOT do: audit the rest of your middleware chain, or touch path prefixes — a host that
50
+ * mounts under a sub-path has already stripped it from `req.url` (Express) or has not (Koa, Fastify,
51
+ * node:http), and only the host knows which.
52
+ *
53
+ * ```ts
54
+ * const api = createRestApi(config);
55
+ *
56
+ * express().use('/api', toNodeHandler(api)); // Express — before express.json()
57
+ * await fastify.register(middie); fastify.use('/api', toNodeHandler(api)); // Fastify
58
+ * koa.use(c2k((rq, rs, _next) => toNodeHandler(api)(rq, rs))); // Koa — three params, see below
59
+ * createServer(toNodeHandler(api)); // node:http
60
+ * ```
61
+ *
62
+ * On Koa, the middleware must declare THREE parameters even though it never calls the third:
63
+ * `koa-connect` switches on `fn.length` and, below three, assumes the middleware does not terminate
64
+ * the response — it calls `next()` immediately and Koa writes its own 404 over what was already
65
+ * sent. Measured: `ERR_HTTP_HEADERS_SENT` and a 404 on every route, from a two-parameter version of
66
+ * the same working code.
67
+ *
68
+ * Bind at the MIDDLEWARE layer, never as a route, and put it ahead of the body parser. Measured:
69
+ * `fastify.all('/api/*', …)` runs AFTER Fastify's built-in JSON parser has drained the stream —
70
+ * every GET passes, a POST with a body answers 400. Through `@fastify/middie` the same handler runs
71
+ * before parsing and works, with the host's own routes keeping their parsed bodies.
72
+ */
73
+ export function toNodeHandler(handler) {
74
+ const listener = getRequestListener(handler.fetch);
75
+ return (req, res) => {
76
+ // Deliberately NOT re-serialising `req.body` back into a stream. It would make the common case
77
+ // work and quietly change the bytes — key order, unicode escaping, and nothing at all for
78
+ // multipart or a raw payload — so a misordered chain would keep running until the day it
79
+ // matters. Ordering is also the answer the ecosystem settled on: better-auth's Node handler
80
+ // documents the same constraint, mount before the parser, for the same reason.
81
+ if (bodyAlreadyConsumed(req)) {
82
+ res.writeHead(500, { 'content-type': 'application/json' });
83
+ res.end(JSON.stringify({
84
+ error: 'request body was already consumed by a body parser mounted ahead of this handler '
85
+ + '(express.json, koa-bodyparser, Fastify\'s built-in JSON parser). Mount the GNL handler '
86
+ + 'BEFORE the parser — the parser still serves your own routes after it.',
87
+ code: EDGE_ERROR_CODES.bodyConsumedUpstream,
88
+ }));
89
+ return;
90
+ }
91
+ return listener(req, res);
92
+ };
93
+ }
94
+ //# sourceMappingURL=node.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node.js","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAAA,4FAA4F;AAC5F,EAAE;AACF,oGAAoG;AACpG,gGAAgG;AAChG,mGAAmG;AACnG,yEAAyE;AACzE,EAAE;AACF,kGAAkG;AAClG,kGAAkG;AAClG,oGAAoG;AACpG,qGAAqG;AACrG,oGAAoG;AACpG,EAAE;AACF,iGAAiG;AACjG,mEAAmE;AACnE,EAAE;AACF,kGAAkG;AAClG,6CAA6C;AAC7C,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAGvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEpD,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC;AAEjE;;;;;;;;;;;;GAYG;AACH,SAAS,mBAAmB,CAAC,GAAoB;IAC/C,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC;QAAE,OAAO,KAAK,CAAC;IACtD,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,mBAAmB,CAAC,KAAK,SAAS,CAAC;IACnH,OAAO,SAAS,IAAI,GAAG,CAAC,aAAa,CAAC;AACxC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,UAAU,aAAa,CAAC,OAAqB;IACjD,MAAM,QAAQ,GAAG,kBAAkB,CAAC,OAAO,CAAC,KAAY,CAAC,CAAC;IAC1D,OAAO,CAAC,GAAoB,EAAE,GAAmB,EAAE,EAAE;QACnD,+FAA+F;QAC/F,0FAA0F;QAC1F,yFAAyF;QACzF,4FAA4F;QAC5F,+EAA+E;QAC/E,IAAI,mBAAmB,CAAC,GAAG,CAAC,EAAE,CAAC;YAC7B,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC,CAAC;YAC3D,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC;gBACrB,KAAK,EAAE,mFAAmF;sBACtF,yFAAyF;sBACzF,uEAAuE;gBAC3E,IAAI,EAAE,gBAAgB,CAAC,oBAAoB;aAC5C,CAAC,CAAC,CAAC;YACJ,OAAO;QACT,CAAC;QACD,OAAO,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IAC5B,CAAC,CAAC;AACJ,CAAC","sourcesContent":["// Binding the REST API to a Node server — Express, Fastify, Koa, Nest, or bare `node:http`.\n//\n// The twin of packages/studio/src/node.ts, and deliberately a copy rather than an import. A backend\n// that wants agents and no dashboard had to install @gnldev/studio just to reach this function,\n// which drags @gnldev/studio-ui — a React app — into a service that will never render a page. That\n// is backwards, and one subpath is a smaller price than that dependency.\n//\n// The alternative, importing it from the sibling package, was tried and is worse than it looks: a\n// cross-package runtime import resolves through the sibling's BUILT output, so a stale dist turns\n// the call into `undefined` with no error until something downstream fails on empty input. Measured\n// in this repo the same afternoon, in @gnldev/agui, where it emptied an SSE stream and every test in\n// the package failed on `JSON.parse('')`. This file follows the convention sse.ts already set here.\n//\n// KEEP IN SYNC with packages/studio/src/node.ts. If they drift, the two packages answer the same\n// misconfiguration differently, which is worse than either answer.\n//\n// Kept on a subpath so the root export stays runtime-neutral: importing the package must not drag\n// Node's types into a Workers or Deno build.\nimport { getRequestListener } from '@hono/node-server';\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport type { FetchHandler } from './handler.js';\nimport { EDGE_ERROR_CODES } from './edge-errors.js';\n\nconst CARRIES_BODY = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);\n\n/**\n * True when the request announced a body and something already drained it.\n *\n * A body parser mounted ahead of this handler — `express.json()`, `koa-bodyparser`, Fastify's\n * built-in JSON parser — reads the stream to the end and hands the result to the framework, not to\n * us. What arrives here is a POST with no readable body, and the endpoint answers the only thing it\n * can: \"runId is required\". Measured on a real Express app: the identical request returns 200 with a\n * model answer without `express.json()`, and that 400 with it. The message accuses the caller of a\n * mistake the caller did not make, which is worse than failing.\n *\n * Detection is deliberately narrow — a body was ANNOUNCED (content-length or chunked) and the\n * readable side is ALREADY finished. A GET nobody read does not match; neither does a bodyless POST.\n */\nfunction bodyAlreadyConsumed(req: IncomingMessage): boolean {\n if (!CARRIES_BODY.has(req.method ?? '')) return false;\n const announced = Number(req.headers['content-length'] ?? 0) > 0 || req.headers['transfer-encoding'] !== undefined;\n return announced && req.readableEnded;\n}\n\n/**\n * Turns a fetch handler into a Node request listener.\n *\n * @experimental Closes the SSE/flush class of bug inside the Node bridge — a hand-written bridge\n * that omits `res.flushHeaders()` withholds the head until the first chunk, so a quiet event stream\n * hangs the client forever and nothing throws. It also answers plainly when a body parser upstream\n * has already drained the request, instead of letting the endpoint blame the caller.\n *\n * What it does NOT do: audit the rest of your middleware chain, or touch path prefixes — a host that\n * mounts under a sub-path has already stripped it from `req.url` (Express) or has not (Koa, Fastify,\n * node:http), and only the host knows which.\n *\n * ```ts\n * const api = createRestApi(config);\n *\n * express().use('/api', toNodeHandler(api)); // Express — before express.json()\n * await fastify.register(middie); fastify.use('/api', toNodeHandler(api)); // Fastify\n * koa.use(c2k((rq, rs, _next) => toNodeHandler(api)(rq, rs))); // Koa — three params, see below\n * createServer(toNodeHandler(api)); // node:http\n * ```\n *\n * On Koa, the middleware must declare THREE parameters even though it never calls the third:\n * `koa-connect` switches on `fn.length` and, below three, assumes the middleware does not terminate\n * the response — it calls `next()` immediately and Koa writes its own 404 over what was already\n * sent. Measured: `ERR_HTTP_HEADERS_SENT` and a 404 on every route, from a two-parameter version of\n * the same working code.\n *\n * Bind at the MIDDLEWARE layer, never as a route, and put it ahead of the body parser. Measured:\n * `fastify.all('/api/*', …)` runs AFTER Fastify's built-in JSON parser has drained the stream —\n * every GET passes, a POST with a body answers 400. Through `@fastify/middie` the same handler runs\n * before parsing and works, with the host's own routes keeping their parsed bodies.\n */\nexport function toNodeHandler(handler: FetchHandler) {\n const listener = getRequestListener(handler.fetch as any);\n return (req: IncomingMessage, res: ServerResponse) => {\n // Deliberately NOT re-serialising `req.body` back into a stream. It would make the common case\n // work and quietly change the bytes — key order, unicode escaping, and nothing at all for\n // multipart or a raw payload — so a misordered chain would keep running until the day it\n // matters. Ordering is also the answer the ecosystem settled on: better-auth's Node handler\n // documents the same constraint, mount before the parser, for the same reason.\n if (bodyAlreadyConsumed(req)) {\n res.writeHead(500, { 'content-type': 'application/json' });\n res.end(JSON.stringify({\n error: 'request body was already consumed by a body parser mounted ahead of this handler '\n + '(express.json, koa-bodyparser, Fastify\\'s built-in JSON parser). Mount the GNL handler '\n + 'BEFORE the parser — the parser still serves your own routes after it.',\n code: EDGE_ERROR_CODES.bodyConsumedUpstream,\n }));\n return;\n }\n return listener(req, res);\n };\n}\n"]}
@@ -0,0 +1 @@
1
+ export declare function buildOpenApi(agentNames: string[], workflowNames?: string[], title?: string): any;
@@ -0,0 +1,215 @@
1
+ // Generate an OpenAPI 3.1 schema from the createGnl agent + workflow registry (auto-docs).
2
+ import { EDGE_ERROR_CODES } from './edge-errors.js';
3
+ export function buildOpenApi(agentNames, workflowNames = [], title = 'gnl agents') {
4
+ // ONE identity per call, and the spec says which two spellings it accepts. `required: ['runId']`
5
+ // is gone rather than widened to a `oneOf`: a workKey-only request is valid, a runId-only request
6
+ // is valid, and both together are refused — which `oneOf` can express but no generator renders
7
+ // usefully. The prose carries it, the 400 enforces it.
8
+ const runBody = {
9
+ required: true,
10
+ content: {
11
+ 'application/json': {
12
+ schema: {
13
+ type: 'object',
14
+ description: 'Exactly one of `workKey` or `runId` is required.',
15
+ properties: {
16
+ workKey: {
17
+ type: 'string',
18
+ description: 'Your name for this unit of work (the invoice being issued, tonight\'s reconciliation) — not for a conversation. The engine derives the run id from it and returns that id in X-Gnl-Run-Id. Sending the same workKey again retries the SAME job; a conversation is `threadId`, a separate field. Keep sensitive data out of it: a workKey is reflected in error details and shown in Studio.',
19
+ },
20
+ runId: { type: 'string', description: 'A raw run id you already hold (a resume, a fork, an id you stored). Mutually exclusive with workKey.' },
21
+ prompt: { type: 'string' },
22
+ messages: { type: 'array', items: { type: 'object' } },
23
+ threadId: { type: 'string' },
24
+ approvals: { type: 'object', additionalProperties: { type: 'boolean' } },
25
+ },
26
+ },
27
+ },
28
+ },
29
+ };
30
+ const runResponse = {
31
+ '200': {
32
+ description: 'Durable run result',
33
+ content: {
34
+ 'application/json': {
35
+ schema: {
36
+ type: 'object',
37
+ properties: {
38
+ ok: { type: 'boolean' },
39
+ runId: { type: 'string' },
40
+ text: { type: 'string' },
41
+ interrupts: { type: 'array', items: { type: 'object' } },
42
+ },
43
+ },
44
+ },
45
+ },
46
+ },
47
+ // Decision #1: run-limit exceeded — the request is valid but couldn't be processed with the given
48
+ // `limits`; deterministic (retry DOESN'T HELP), `limits` can be raised and resumed with the SAME runId (resumable:true).
49
+ '422': {
50
+ description: 'Run limit exceeded (RunLimitExceededError/ToolLoopDetectedError) — can continue with the same runId after raising limits',
51
+ content: {
52
+ 'application/json': {
53
+ schema: {
54
+ type: 'object',
55
+ properties: {
56
+ error: { type: 'string' },
57
+ code: { type: 'string', enum: [EDGE_ERROR_CODES.runLimitExceeded, EDGE_ERROR_CODES.toolLoopDetected] },
58
+ detail: { type: 'object' },
59
+ resumable: { type: 'boolean' },
60
+ },
61
+ },
62
+ },
63
+ },
64
+ },
65
+ };
66
+ const paths = {};
67
+ // Documented as unauthenticated on purpose — see the route comments in index.ts. An orchestrator
68
+ // reading this spec needs to know it can probe these without arranging a credential first.
69
+ paths['/health'] = {
70
+ get: {
71
+ summary: 'Liveness — is the process alive? No storage access; unauthenticated',
72
+ description: 'Point LIVENESS probes here. Deliberately independent of storage: a failing database must not cause a healthy process to be killed and restarted.',
73
+ responses: { '200': { description: '{ status: "ok", uptimeSec }' } },
74
+ },
75
+ };
76
+ paths['/ready'] = {
77
+ get: {
78
+ summary: 'Readiness — can it serve traffic? Touches storage; unauthenticated',
79
+ description: 'Point READINESS/traffic probes here. Returns 503 when the journal is unreachable or does not answer within the probe budget, so the instance leaves the load balancer while staying alive to recover. The underlying error is never returned (it can carry connection details) — it goes to the logs.',
80
+ responses: {
81
+ '200': { description: '{ status: "ready" }' },
82
+ '503': { description: '{ status: "unavailable", storage: "unreachable" }' },
83
+ },
84
+ },
85
+ };
86
+ paths['/agents'] = {
87
+ get: { summary: 'List of registered agent metadata', responses: { '200': { description: 'AgentMeta list' } } },
88
+ };
89
+ for (const n of agentNames) {
90
+ paths[`/agents/${n}/run`] = {
91
+ post: { summary: `Run the '${n}' agent durably`, requestBody: runBody, responses: runResponse },
92
+ };
93
+ paths[`/agents/${n}/stream`] = {
94
+ post: {
95
+ summary: `Run the '${n}' agent durably + streaming (SSE)`,
96
+ requestBody: runBody,
97
+ responses: {
98
+ '200': {
99
+ description: 'SSE stream: text-delta/tool-call/tool-result/interrupt/done events',
100
+ content: { 'text/event-stream': { schema: { type: 'string' } } },
101
+ },
102
+ },
103
+ },
104
+ };
105
+ paths[`/agents/${n}/resume`] = {
106
+ post: {
107
+ summary: `Resume the '${n}' agent with runId + approvals`,
108
+ requestBody: {
109
+ required: true,
110
+ content: {
111
+ 'application/json': {
112
+ schema: {
113
+ type: 'object',
114
+ required: ['runId'],
115
+ properties: {
116
+ runId: { type: 'string' },
117
+ approvals: { type: 'object', additionalProperties: { type: 'boolean' } },
118
+ },
119
+ },
120
+ },
121
+ },
122
+ },
123
+ responses: runResponse,
124
+ },
125
+ };
126
+ }
127
+ if (workflowNames.length) {
128
+ paths['/workflows'] = {
129
+ get: { summary: 'List of registered workflows (name + steps)', responses: { '200': { description: 'WorkflowMeta list' } } },
130
+ };
131
+ const wfRunBody = {
132
+ required: true,
133
+ content: {
134
+ 'application/json': {
135
+ schema: {
136
+ type: 'object',
137
+ properties: {
138
+ runId: { type: 'string', description: 'A raw run id (optional; if given, the same id resumes). Mutually exclusive with workKey.' },
139
+ workKey: { type: 'string', description: 'Your name for this unit of work; the engine derives the run id from it and returns it in X-Gnl-Run-Id.' },
140
+ workScope: {
141
+ type: 'string',
142
+ enum: ['resource', 'org'],
143
+ description: "Which address the workKey is unique within. 'resource' (default) scopes the job to the resourceId that named it; 'org' scopes it to the installation — one job no matter who triggers it (a nightly reconciliation, a scheduled sweep). Choosing 'org' by mistake is the quiet mistake: two callers share one run.",
144
+ },
145
+ input: { description: 'Workflow input (workflow-specific)' },
146
+ },
147
+ },
148
+ },
149
+ },
150
+ };
151
+ const wfRunResponse = {
152
+ '200': {
153
+ description: 'WorkflowRunResult',
154
+ content: {
155
+ 'application/json': {
156
+ schema: {
157
+ type: 'object',
158
+ properties: {
159
+ ok: { type: 'boolean' },
160
+ runId: { type: 'string' },
161
+ output: { description: 'Workflow output (if completed)' },
162
+ suspended: { type: 'boolean' },
163
+ stepId: { type: 'string' },
164
+ steps: { type: 'array', items: { type: 'object' } },
165
+ },
166
+ },
167
+ },
168
+ },
169
+ },
170
+ };
171
+ for (const w of workflowNames) {
172
+ paths[`/workflows/${w}/run`] = {
173
+ post: { summary: `Run the '${w}' workflow durably (suspend/resume safe)`, requestBody: wfRunBody, responses: wfRunResponse },
174
+ };
175
+ }
176
+ }
177
+ paths['/runs/{id}'] = {
178
+ get: {
179
+ summary: "A run's journal timeline",
180
+ parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
181
+ responses: { '200': { description: 'Journal entry list' } },
182
+ },
183
+ };
184
+ // P0.3 documents the opt-in pagination/filter query params — with none given
185
+ // the response is still the legacy RunSummary array (see createRestApi's GET /runs JSDoc).
186
+ paths['/runs'] = {
187
+ get: {
188
+ summary: 'Run summaries — legacy array with no params, or a {items,nextCursor} page when ?limit/?cursor/?status/?agent is given',
189
+ parameters: [
190
+ { name: 'limit', in: 'query', schema: { type: 'integer', minimum: 1, maximum: 1000 } },
191
+ // Opaque on purpose — see `Page<T>` in @gnldev/durable. It happens to be a row offset today
192
+ // and is intended to become a `created_at`+`runId` key; a caller that computes one instead
193
+ // of echoing `nextCursor` breaks on that change, and has no contract to stand on.
194
+ {
195
+ name: 'cursor',
196
+ in: 'query',
197
+ description: 'Opaque continuation token — pass back the `nextCursor` from the previous page verbatim. Do not parse or compute it.',
198
+ schema: { type: 'string' },
199
+ },
200
+ { name: 'status', in: 'query', schema: { type: 'string', enum: ['completed', 'suspended'] } },
201
+ { name: 'agent', in: 'query', schema: { type: 'string' } },
202
+ ],
203
+ responses: { '200': { description: 'RunSummary list, or {items: RunSummary[], nextCursor?: string}' } },
204
+ },
205
+ };
206
+ paths['/runs/{id}/cancel'] = {
207
+ post: {
208
+ summary: 'Cancel in-flight generation for a run on THIS server instance (best-effort, single-instance only — see JSDoc)',
209
+ parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
210
+ responses: { '200': { description: '{ok:true, cancelled:number}' }, '404': { description: 'run not found / not visible in this scope' } },
211
+ },
212
+ };
213
+ return { openapi: '3.1.0', info: { title, version: '0.0.0' }, paths };
214
+ }
215
+ //# sourceMappingURL=openapi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openapi.js","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"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","sourcesContent":["// Generate an OpenAPI 3.1 schema from the createGnl agent + workflow registry (auto-docs).\nimport { EDGE_ERROR_CODES } from './edge-errors.js';\n\nexport function buildOpenApi(agentNames: string[], workflowNames: string[] = [], title = 'gnl agents'): any {\n // ONE identity per call, and the spec says which two spellings it accepts. `required: ['runId']`\n // is gone rather than widened to a `oneOf`: a workKey-only request is valid, a runId-only request\n // is valid, and both together are refused — which `oneOf` can express but no generator renders\n // usefully. The prose carries it, the 400 enforces it.\n const runBody = {\n required: true,\n content: {\n 'application/json': {\n schema: {\n type: 'object',\n description: 'Exactly one of `workKey` or `runId` is required.',\n properties: {\n workKey: {\n type: 'string',\n description:\n 'Your name for this unit of work (the invoice being issued, tonight\\'s reconciliation) — not for a conversation. The engine derives the run id from it and returns that id in X-Gnl-Run-Id. Sending the same workKey again retries the SAME job; a conversation is `threadId`, a separate field. Keep sensitive data out of it: a workKey is reflected in error details and shown in Studio.',\n },\n runId: { type: 'string', description: 'A raw run id you already hold (a resume, a fork, an id you stored). Mutually exclusive with workKey.' },\n prompt: { type: 'string' },\n messages: { type: 'array', items: { type: 'object' } },\n threadId: { type: 'string' },\n approvals: { type: 'object', additionalProperties: { type: 'boolean' } },\n },\n },\n },\n },\n };\n const runResponse = {\n '200': {\n description: 'Durable run result',\n content: {\n 'application/json': {\n schema: {\n type: 'object',\n properties: {\n ok: { type: 'boolean' },\n runId: { type: 'string' },\n text: { type: 'string' },\n interrupts: { type: 'array', items: { type: 'object' } },\n },\n },\n },\n },\n },\n // Decision #1: run-limit exceeded — the request is valid but couldn't be processed with the given\n // `limits`; deterministic (retry DOESN'T HELP), `limits` can be raised and resumed with the SAME runId (resumable:true).\n '422': {\n description: 'Run limit exceeded (RunLimitExceededError/ToolLoopDetectedError) — can continue with the same runId after raising limits',\n content: {\n 'application/json': {\n schema: {\n type: 'object',\n properties: {\n error: { type: 'string' },\n code: { type: 'string', enum: [EDGE_ERROR_CODES.runLimitExceeded, EDGE_ERROR_CODES.toolLoopDetected] },\n detail: { type: 'object' },\n resumable: { type: 'boolean' },\n },\n },\n },\n },\n },\n };\n\n const paths: Record<string, any> = {};\n // Documented as unauthenticated on purpose — see the route comments in index.ts. An orchestrator\n // reading this spec needs to know it can probe these without arranging a credential first.\n paths['/health'] = {\n get: {\n summary: 'Liveness — is the process alive? No storage access; unauthenticated',\n description: 'Point LIVENESS probes here. Deliberately independent of storage: a failing database must not cause a healthy process to be killed and restarted.',\n responses: { '200': { description: '{ status: \"ok\", uptimeSec }' } },\n },\n };\n paths['/ready'] = {\n get: {\n summary: 'Readiness — can it serve traffic? Touches storage; unauthenticated',\n description: 'Point READINESS/traffic probes here. Returns 503 when the journal is unreachable or does not answer within the probe budget, so the instance leaves the load balancer while staying alive to recover. The underlying error is never returned (it can carry connection details) — it goes to the logs.',\n responses: {\n '200': { description: '{ status: \"ready\" }' },\n '503': { description: '{ status: \"unavailable\", storage: \"unreachable\" }' },\n },\n },\n };\n paths['/agents'] = {\n get: { summary: 'List of registered agent metadata', responses: { '200': { description: 'AgentMeta list' } } },\n };\n for (const n of agentNames) {\n paths[`/agents/${n}/run`] = {\n post: { summary: `Run the '${n}' agent durably`, requestBody: runBody, responses: runResponse },\n };\n paths[`/agents/${n}/stream`] = {\n post: {\n summary: `Run the '${n}' agent durably + streaming (SSE)`,\n requestBody: runBody,\n responses: {\n '200': {\n description: 'SSE stream: text-delta/tool-call/tool-result/interrupt/done events',\n content: { 'text/event-stream': { schema: { type: 'string' } } },\n },\n },\n },\n };\n paths[`/agents/${n}/resume`] = {\n post: {\n summary: `Resume the '${n}' agent with runId + approvals`,\n requestBody: {\n required: true,\n content: {\n 'application/json': {\n schema: {\n type: 'object',\n required: ['runId'],\n properties: {\n runId: { type: 'string' },\n approvals: { type: 'object', additionalProperties: { type: 'boolean' } },\n },\n },\n },\n },\n },\n responses: runResponse,\n },\n };\n }\n if (workflowNames.length) {\n paths['/workflows'] = {\n get: { summary: 'List of registered workflows (name + steps)', responses: { '200': { description: 'WorkflowMeta list' } } },\n };\n const wfRunBody = {\n required: true,\n content: {\n 'application/json': {\n schema: {\n type: 'object',\n properties: {\n runId: { type: 'string', description: 'A raw run id (optional; if given, the same id resumes). Mutually exclusive with workKey.' },\n workKey: { type: 'string', description: 'Your name for this unit of work; the engine derives the run id from it and returns it in X-Gnl-Run-Id.' },\n workScope: {\n type: 'string',\n enum: ['resource', 'org'],\n description:\n \"Which address the workKey is unique within. 'resource' (default) scopes the job to the resourceId that named it; 'org' scopes it to the installation — one job no matter who triggers it (a nightly reconciliation, a scheduled sweep). Choosing 'org' by mistake is the quiet mistake: two callers share one run.\",\n },\n input: { description: 'Workflow input (workflow-specific)' },\n },\n },\n },\n },\n };\n const wfRunResponse = {\n '200': {\n description: 'WorkflowRunResult',\n content: {\n 'application/json': {\n schema: {\n type: 'object',\n properties: {\n ok: { type: 'boolean' },\n runId: { type: 'string' },\n output: { description: 'Workflow output (if completed)' },\n suspended: { type: 'boolean' },\n stepId: { type: 'string' },\n steps: { type: 'array', items: { type: 'object' } },\n },\n },\n },\n },\n },\n };\n for (const w of workflowNames) {\n paths[`/workflows/${w}/run`] = {\n post: { summary: `Run the '${w}' workflow durably (suspend/resume safe)`, requestBody: wfRunBody, responses: wfRunResponse },\n };\n }\n }\n\n paths['/runs/{id}'] = {\n get: {\n summary: \"A run's journal timeline\",\n parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],\n responses: { '200': { description: 'Journal entry list' } },\n },\n };\n // P0.3 documents the opt-in pagination/filter query params — with none given\n // the response is still the legacy RunSummary array (see createRestApi's GET /runs JSDoc).\n paths['/runs'] = {\n get: {\n summary: 'Run summaries — legacy array with no params, or a {items,nextCursor} page when ?limit/?cursor/?status/?agent is given',\n parameters: [\n { name: 'limit', in: 'query', schema: { type: 'integer', minimum: 1, maximum: 1000 } },\n // Opaque on purpose — see `Page<T>` in @gnldev/durable. It happens to be a row offset today\n // and is intended to become a `created_at`+`runId` key; a caller that computes one instead\n // of echoing `nextCursor` breaks on that change, and has no contract to stand on.\n {\n name: 'cursor',\n in: 'query',\n description: 'Opaque continuation token — pass back the `nextCursor` from the previous page verbatim. Do not parse or compute it.',\n schema: { type: 'string' },\n },\n { name: 'status', in: 'query', schema: { type: 'string', enum: ['completed', 'suspended'] } },\n { name: 'agent', in: 'query', schema: { type: 'string' } },\n ],\n responses: { '200': { description: 'RunSummary list, or {items: RunSummary[], nextCursor?: string}' } },\n },\n };\n paths['/runs/{id}/cancel'] = {\n post: {\n summary: 'Cancel in-flight generation for a run on THIS server instance (best-effort, single-instance only — see JSDoc)',\n parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],\n responses: { '200': { description: '{ok:true, cancelled:number}' }, '404': { description: 'run not found / not visible in this scope' } },\n },\n };\n\n return { openapi: '3.1.0', info: { title, version: '0.0.0' }, paths };\n}\n"]}
package/dist/sse.d.ts ADDED
@@ -0,0 +1,57 @@
1
+ import { streamSSE } from 'hono/streaming';
2
+ import type { Context } from 'hono';
3
+ import type { Interrupt } from '@gnldev/durable';
4
+ /**
5
+ * `streamSSE` plus the two headers a live stream needs to survive the trip to the browser.
6
+ *
7
+ * Hono sets `Cache-Control: no-cache`, which says "don't serve this from cache" and says nothing
8
+ * about re-encoding. So a compression middleware in the host's chain happily takes the stream and
9
+ * buffers it: measured on a real Express app, `compression()` turned 13 progressive chunks with the
10
+ * first at 750ms into ONE chunk delivered at the end. Status 200, no error, no live screen — the
11
+ * failure is invisible from both sides. `no-transform` is the standard way to say don't, and
12
+ * `compression` honours it (measured: first byte 1520ms -> 302ms with the flag on).
13
+ *
14
+ * `X-Accel-Buffering: no` is the nginx-specific half, and measurement narrowed where it matters to
15
+ * one square of a 2x2 — behind a real nginx, same stream dripping five deltas 300ms apart:
16
+ *
17
+ * HTTP/1.1 + gzip, no header → ONE chunk at 1511ms (collapsed)
18
+ * HTTP/1.1 + gzip, header → 306, 606, 911, 1211, 1511
19
+ * HTTP/2 + gzip, no header → 316, 616, 916, 1217, 1518 (fine without it)
20
+ * HTTP/2 + gzip, header → 312, 612, 913, 1214, 1514
21
+ *
22
+ * So it is load-bearing exactly when the CLIENT speaks HTTP/1.1 to a proxy that gzips, and inert
23
+ * everywhere else — including HTTP/2, which is what a browser usually gets over TLS. That leaves
24
+ * plenty of real traffic in the square that breaks: internal clients on plain HTTP, curl's default,
25
+ * anything not a modern browser. Worth a header; not worth believing it covers more than it does.
26
+ *
27
+ * Set AFTER `streamSSE` on purpose: it writes `Cache-Control` itself, so anything set on the context
28
+ * beforehand is overwritten. Patching the returned Response is what actually survives.
29
+ *
30
+ * Kept IN SYNC with packages/studio/src/sse.ts.
31
+ */
32
+ export declare function sseResponse(c: Context, cb: Parameters<typeof streamSSE>[1], onError?: Parameters<typeof streamSSE>[2]): Response;
33
+ /**
34
+ * Extracts suspended tool calls (Interrupt) from a completed step list — the SAME logic as
35
+ * runDurable, and now literally the same function: the sentinel goes straight into durable's
36
+ * `surfacedInterrupts`.
37
+ *
38
+ * It used to push the RAW sentinel, and that was wrong in exactly one shape — the one that matters
39
+ * most on this channel. A sub-agent that hits a human gate suspends its PARENT's record too, and
40
+ * that record is keyed by the parent's proxy call id because the suspend record, the replay and
41
+ * `consumeExistingRecord` all work through it. The engine surfaces the CHILD's interrupts and
42
+ * durable-tool deliberately IGNORES an answer addressed to the proxy — so a client following the
43
+ * standard contract (`approvals[interrupt.toolCallId] = true`) against this stream was answering an
44
+ * id the engine drops on the floor. Chat/SSE is the actual end-user channel: the question appeared,
45
+ * the human approved it, and nothing happened.
46
+ */
47
+ export declare function interruptsFromSteps(steps: any[]): Interrupt[];
48
+ /** pipeAgentStream options (opt-in — if not given, behavior is identical to before except for the id field). */
49
+ export interface PipeAgentStreamOptions {
50
+ /**
51
+ * The last event id the client saw (resolved from the Last-Event-ID header or body.lastEventId).
52
+ * If given, events with id <= lastEventId are NOT WRITTEN (production still runs from the start — replay is cheap).
53
+ */
54
+ lastEventId?: number;
55
+ }
56
+ /** Stream fullStream as SSE, then send interrupt + done events. Returns a Hono Response. */
57
+ export declare function pipeAgentStream(c: Context, runId: string, result: any, opts?: PipeAgentStreamOptions): Response;