@gnldev/agui 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/LICENSE +201 -0
- package/README.md +188 -0
- package/dist/convert.d.ts +36 -0
- package/dist/convert.js +133 -0
- package/dist/convert.js.map +1 -0
- package/dist/handler.d.ts +5 -0
- package/dist/handler.js +5 -0
- package/dist/handler.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/route.d.ts +53 -0
- package/dist/route.js +324 -0
- package/dist/route.js.map +1 -0
- package/dist/types.d.ts +88 -0
- package/dist/types.js +22 -0
- package/dist/types.js.map +1 -0
- package/package.json +63 -0
package/dist/route.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Context } from 'hono';
|
|
2
|
+
import { type FetchHandler } from './handler.js';
|
|
3
|
+
import type { CreateGnlConfig, GnlIdentity } from '@gnldev/durable';
|
|
4
|
+
export interface PipeAguiStreamOptions {
|
|
5
|
+
/** AG-UI threadId. If not given, runId is used (single-thread default). */
|
|
6
|
+
threadId?: string;
|
|
7
|
+
}
|
|
8
|
+
/** Stream fullStream as AG-UI SSE events (starts with RUN_STARTED, ends with RUN_FINISHED/RUN_ERROR). */
|
|
9
|
+
export declare function pipeAguiStream(c: Context, runId: string, result: any, opts?: PipeAguiStreamOptions): Response;
|
|
10
|
+
export interface CreateAguiRouteOptions {
|
|
11
|
+
/** AG-UI threadId resolver (from request + body). If not given, uses body.threadId, else runId. */
|
|
12
|
+
resolveThreadId?: (c: Context, body: any) => string | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* WHO this request acts for — resolved from something the SERVER trusts, never from the body.
|
|
15
|
+
*
|
|
16
|
+
* This route declares auth out of scope (see createAguiRoute's own note) and that boundary is
|
|
17
|
+
* right. What was NOT right is what the boundary implied: `body.context` went to the engine
|
|
18
|
+
* untouched, and the engine reads the reserved context keys as "the server established this". So
|
|
19
|
+
* a request could name its own subject. Measured on the sibling route (chat-adapter, identical
|
|
20
|
+
* shape): a POST carrying `{"context":{"__gnl_resourceId":"KURBAN"}}` produced a run owned by
|
|
21
|
+
* that name — and once ownership stamping landed, the forged name became the LOCK's value too,
|
|
22
|
+
* i.e. the caller handed itself the key.
|
|
23
|
+
*
|
|
24
|
+
* The route now always seals. With no resolver the seal carries no identity, which strips the
|
|
25
|
+
* reserved keys: no forged subject gets in, and none is asserted either.
|
|
26
|
+
*
|
|
27
|
+
* HONEST BOUND: a resolver reading an unauthenticated request asserts a subject nobody verified.
|
|
28
|
+
* Put auth in front of this route, or the subject is only as good as the caller's honesty.
|
|
29
|
+
*/
|
|
30
|
+
resolveResourceId?: (c: Context, body: any) => string | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* WHO and WHICH CONVERSATION, in one hook — the SAME signature @gnldev/chat-adapter's route takes, so a
|
|
33
|
+
* host that has written this function once can mount either adapter with it.
|
|
34
|
+
*
|
|
35
|
+
* One signature for both routes is the point. Until now the two adapters asked the same question
|
|
36
|
+
* with two hooks each, in two shapes, and this one had the sharper lesson: `resolveThreadId` was
|
|
37
|
+
* called, its answer went into the SSE envelope, and the run still read and wrote the thread the
|
|
38
|
+
* client named (see the Turkish note in the stream call below). A host could wire identity, watch
|
|
39
|
+
* the right value appear in the response, and have changed nothing about where memory went.
|
|
40
|
+
*
|
|
41
|
+
* Takes the web `Request` rather than the Hono `Context`, matching @gnldev/server's `OrgOptions.resolve`:
|
|
42
|
+
* a host mounting this from Express or Fastify has a Request and no Context.
|
|
43
|
+
*
|
|
44
|
+
* PRECEDENCE: `resolveResourceId` / `resolveThreadId` still win, field by field — an existing
|
|
45
|
+
* deployment's answer is not taken away by a newer convenience. Called once per request.
|
|
46
|
+
*
|
|
47
|
+
* HONEST BOUND: this route declares auth out of scope. A resolver reading an unauthenticated
|
|
48
|
+
* request asserts a subject nobody verified.
|
|
49
|
+
*/
|
|
50
|
+
identity?: GnlIdentity;
|
|
51
|
+
}
|
|
52
|
+
/** The AG-UI route as a fetch handler — mount with `app.mount(path, ...)` on a Hono host. */
|
|
53
|
+
export declare function createAguiRoute(config: CreateGnlConfig, opts?: CreateAguiRouteOptions): FetchHandler;
|
package/dist/route.js
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { toFetchHandler } from './handler.js';
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
import { limitBreachFromSteps, blockedFromSteps, BLOCKED_ERROR_CODES, blockedErrorCode, callerConflictCode, sealRequestContext, resolveWorkIdentity } from '@gnldev/durable';
|
|
4
|
+
import { createGnl } from '@gnldev/durable';
|
|
5
|
+
import { streamSSE } from 'hono/streaming';
|
|
6
|
+
// The two limit codes are READ from @gnldev/server rather than spelled again here. The event/data
|
|
7
|
+
// SHAPE is deliberately a copy (see the note above), but a code is not a shape: it is the string a
|
|
8
|
+
// caller matches on, and this route mirroring sse.ts by hand is exactly how the docs check ended up
|
|
9
|
+
// with a list it had to maintain. `interruptsFromSteps` already makes this a build-order dependency.
|
|
10
|
+
import { interruptsFromSteps, EDGE_ERROR_CODES } from '@gnldev/server';
|
|
11
|
+
import { toAguiEvents, initialAguiConvertState } from './convert.js';
|
|
12
|
+
import { EventType } from './types.js';
|
|
13
|
+
/** Stream fullStream as AG-UI SSE events (starts with RUN_STARTED, ends with RUN_FINISHED/RUN_ERROR). */
|
|
14
|
+
export function pipeAguiStream(c, runId, result, opts) {
|
|
15
|
+
const threadId = opts?.threadId ?? runId;
|
|
16
|
+
const ctx = { threadId, runId };
|
|
17
|
+
// The header patch, written out rather than imported from @gnldev/server: importing a runtime
|
|
18
|
+
// helper across packages resolves through that package's BUILT output, and a stale dist turns the
|
|
19
|
+
// call into `undefined` — measured, this exact swap emptied the stream and every test here failed
|
|
20
|
+
// on `JSON.parse('')`. Six lines of duplication beats a build-order dependency.
|
|
21
|
+
//
|
|
22
|
+
// Why the headers: Hono sets `Cache-Control: no-cache`, which says nothing about re-encoding, so a
|
|
23
|
+
// compression middleware in the host's chain buffers the stream into one chunk delivered at the
|
|
24
|
+
// end (measured on Express: 13 progressive chunks became 1). `no-transform` stops it;
|
|
25
|
+
// `X-Accel-Buffering: no` is the nginx half — measured behind a real one: load-bearing exactly
|
|
26
|
+
// when the client speaks HTTP/1.1 to a gzipping proxy (without it the stream collapses into one
|
|
27
|
+
// chunk at the end), and inert otherwise, HTTP/2 included.
|
|
28
|
+
// Kept IN SYNC with packages/server/src/sse.ts and packages/studio/src/sse.ts.
|
|
29
|
+
const res = streamSSE(c, async (stream) => {
|
|
30
|
+
// In AG-UI every SSE frame is a single JSON event; the type is inside the event JSON (the spec has
|
|
31
|
+
// no event/data split of its own) → the SSE `event:` field is NOT USED, only `data:` is written.
|
|
32
|
+
const write = async (event) => {
|
|
33
|
+
await stream.writeSSE({ data: JSON.stringify(event) });
|
|
34
|
+
};
|
|
35
|
+
let state = initialAguiConvertState;
|
|
36
|
+
const emit = async (gnlEvent) => {
|
|
37
|
+
const out = toAguiEvents(gnlEvent, ctx, state);
|
|
38
|
+
state = out.state;
|
|
39
|
+
for (const e of out.events)
|
|
40
|
+
await write(e);
|
|
41
|
+
};
|
|
42
|
+
const started = { type: EventType.RUN_STARTED, threadId, runId };
|
|
43
|
+
await write(started);
|
|
44
|
+
try {
|
|
45
|
+
for await (const part of result.fullStream) {
|
|
46
|
+
if (stream.aborted)
|
|
47
|
+
break;
|
|
48
|
+
switch (part.type) {
|
|
49
|
+
case 'text-delta': {
|
|
50
|
+
const text = part.text ?? part.delta ?? '';
|
|
51
|
+
if (text)
|
|
52
|
+
await emit({ event: 'text-delta', data: { text } });
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
case 'tool-call':
|
|
56
|
+
await emit({ event: 'tool-call', data: { toolCallId: part.toolCallId, toolName: part.toolName, input: part.input } });
|
|
57
|
+
break;
|
|
58
|
+
case 'tool-result':
|
|
59
|
+
if (part.output?.__gnl_suspend)
|
|
60
|
+
break; // suspend sentinel is carried by the interrupt event
|
|
61
|
+
if (part.output?.__gnl_limit_exceeded)
|
|
62
|
+
break; // INTERNAL API sentinel — does not leak, carried by the error event
|
|
63
|
+
if (part.output?.__gnl_blocked)
|
|
64
|
+
break; // K1: the block sentinel is also an INTERNAL API — carried by the error event
|
|
65
|
+
await emit({ event: 'tool-result', data: { toolCallId: part.toolCallId, toolName: part.toolName, output: part.output } });
|
|
66
|
+
break;
|
|
67
|
+
// P0.1: kept IN SYNC with sse.ts (see the sync note at the top of this file) — reasoning/
|
|
68
|
+
// tool-input/source/file/step/tool-error events + the raw marker; nothing silently dropped.
|
|
69
|
+
case 'reasoning-start':
|
|
70
|
+
await emit({ event: 'reasoning-start', data: { id: part.id } });
|
|
71
|
+
break;
|
|
72
|
+
case 'reasoning-delta': {
|
|
73
|
+
const text = part.text ?? part.delta ?? '';
|
|
74
|
+
if (text)
|
|
75
|
+
await emit({ event: 'reasoning-delta', data: { id: part.id, text } });
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
case 'reasoning-end':
|
|
79
|
+
await emit({ event: 'reasoning-end', data: { id: part.id } });
|
|
80
|
+
break;
|
|
81
|
+
case 'tool-input-start':
|
|
82
|
+
await emit({ event: 'tool-input-start', data: { toolCallId: part.toolCallId ?? part.id, toolName: part.toolName } });
|
|
83
|
+
break;
|
|
84
|
+
case 'tool-input-delta':
|
|
85
|
+
await emit({ event: 'tool-input-delta', data: { toolCallId: part.toolCallId ?? part.id, delta: part.delta } });
|
|
86
|
+
break;
|
|
87
|
+
case 'tool-input-end':
|
|
88
|
+
await emit({ event: 'tool-input-end', data: { toolCallId: part.toolCallId ?? part.id } });
|
|
89
|
+
break;
|
|
90
|
+
case 'source':
|
|
91
|
+
await emit({ event: 'source', data: { sourceType: part.sourceType, id: part.id, url: part.url, title: part.title } });
|
|
92
|
+
break;
|
|
93
|
+
case 'file':
|
|
94
|
+
await emit({ event: 'file', data: { mediaType: part.file?.mediaType, base64: part.file?.base64 } });
|
|
95
|
+
break;
|
|
96
|
+
case 'start-step':
|
|
97
|
+
await emit({ event: 'step-start', data: {} });
|
|
98
|
+
break;
|
|
99
|
+
case 'finish-step':
|
|
100
|
+
await emit({ event: 'step-finish', data: { finishReason: part.finishReason, usage: part.usage } });
|
|
101
|
+
break;
|
|
102
|
+
case 'tool-error':
|
|
103
|
+
await emit({ event: 'tool-error', data: { toolCallId: part.toolCallId, toolName: part.toolName, error: String(part.error?.message ?? part.error) } });
|
|
104
|
+
break;
|
|
105
|
+
case 'error':
|
|
106
|
+
await emit({ event: 'error', data: { error: String(part.error?.message ?? part.error) } });
|
|
107
|
+
break;
|
|
108
|
+
case 'start':
|
|
109
|
+
case 'finish':
|
|
110
|
+
case 'text-start':
|
|
111
|
+
case 'text-end':
|
|
112
|
+
case 'abort':
|
|
113
|
+
case 'raw':
|
|
114
|
+
break; // deliberately no event — same list and reasons as sse.ts
|
|
115
|
+
default:
|
|
116
|
+
await emit({ event: 'raw', data: { type: part.type } }); // unknown part → type-only marker, never silent
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const steps = await result.steps;
|
|
121
|
+
const breach = limitBreachFromSteps(steps);
|
|
122
|
+
if (breach) {
|
|
123
|
+
await emit({
|
|
124
|
+
event: 'error',
|
|
125
|
+
data: {
|
|
126
|
+
error: breach.message,
|
|
127
|
+
code: breach.kind === 'loop' ? EDGE_ERROR_CODES.toolLoopDetected : EDGE_ERROR_CODES.runLimitExceeded,
|
|
128
|
+
detail: breach.detail,
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
// K1: the block sentinel (side-effect/retry/busy) → same contract as a limit breach: terminal error, no done.
|
|
134
|
+
const blocked = blockedFromSteps(steps);
|
|
135
|
+
if (blocked) {
|
|
136
|
+
await emit({ event: 'error', data: { error: blocked.message, code: BLOCKED_ERROR_CODES[blocked.code] ?? 'run_busy', detail: blocked.detail } });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const interrupts = interruptsFromSteps(steps);
|
|
140
|
+
// FAZ-2 (chat-adapter parity): the approval ADDRESS travels with the interrupt — a client that
|
|
141
|
+
// resumes without this runId starts a fresh run and the suspended one leaks forever. Parity is
|
|
142
|
+
// measured by SCHEMA POSITION, not field name: chat-adapter stamps runId INSIDE each record
|
|
143
|
+
// (ui-stream.ts), so a shared client helper (approvalPayload) must find it there on BOTH
|
|
144
|
+
// adapters — the envelope copy stays for consumers already reading it.
|
|
145
|
+
if (interrupts.length)
|
|
146
|
+
await emit({ event: 'interrupt', data: { interrupts: interrupts.map((i) => ({ ...i, runId })), runId } });
|
|
147
|
+
const finishReason = await Promise.resolve(result.finishReason).catch(() => undefined);
|
|
148
|
+
const usage = await Promise.resolve(result.usage).catch(() => undefined);
|
|
149
|
+
await emit({ event: 'done', data: { runId, finishReason, usage } });
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
await emit({ event: 'error', data: { error: String(e?.message ?? e) } });
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
res.headers.set('Cache-Control', 'no-cache, no-transform');
|
|
156
|
+
res.headers.set('X-Accel-Buffering', 'no');
|
|
157
|
+
return res;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* The caller's own name for the work, reflected back in a refusal (§8, rules 1-3).
|
|
161
|
+
*
|
|
162
|
+
* FROM THE REQUEST, never from storage: a swept run keeps only a hash of its workKey (§10.3), so
|
|
163
|
+
* reading the name back would undo a deletion. The caller already knows what it sent.
|
|
164
|
+
*
|
|
165
|
+
* `detail` and not `error`: the sentence is the most casually logged field in any HTTP client, and a
|
|
166
|
+
* workKey is a business name. Same three lines as @gnldev/server's and @gnldev/chat-adapter's — the
|
|
167
|
+
* dependency direction forbids sharing one (agui → durable, never agui → server for a renderer).
|
|
168
|
+
*/
|
|
169
|
+
function withWorkKey(detail, workKey) {
|
|
170
|
+
if (workKey === undefined)
|
|
171
|
+
return detail;
|
|
172
|
+
return detail && typeof detail === 'object' ? { ...detail, workKey } : { workKey };
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Produces a single-endpoint Hono router from a createGnl config that CopilotKit's AG-UI HttpAgent can talk to:
|
|
176
|
+
* POST /agents/:name/run {runId, prompt|messages, threadId?, approvals?} → AG-UI SSE
|
|
177
|
+
* Deliberately kept small: NO auth/org/budget gates (if needed, use @gnldev/server's createRestApi
|
|
178
|
+
* and pass its stream result to pipeAguiStream — see README).
|
|
179
|
+
*/
|
|
180
|
+
function aguiRouteApp(config, opts = {}) {
|
|
181
|
+
const gnl = createGnl(config);
|
|
182
|
+
// Same warning, same wording and same posture as @gnldev/chat-adapter's route: in production, a route
|
|
183
|
+
// that can name nobody starts runs that are born ownerless, and an ownership gate with no owner to
|
|
184
|
+
// compare against passes. Warn once at construction — never throw, because a deployment that puts
|
|
185
|
+
// its own boundary in front of this route is not broken and must not be stopped at boot.
|
|
186
|
+
if (process.env.NODE_ENV === 'production' && !opts.identity && !opts.resolveResourceId) {
|
|
187
|
+
console.warn('[gnl agui-route] no `identity` and no `resolveResourceId` in production — runs will be born ownerless; ' +
|
|
188
|
+
'ownership gates stay fail-open (a run with no owner is refused to nobody). Pass `identity: (req) => ({ resourceId })` ' +
|
|
189
|
+
'reading your session/JWT — never the request body.');
|
|
190
|
+
}
|
|
191
|
+
const app = new Hono();
|
|
192
|
+
app.post('/agents/:name/run', async (c) => {
|
|
193
|
+
const name = c.req.param('name');
|
|
194
|
+
const body = (await c.req.json().catch(() => ({})));
|
|
195
|
+
// Resolved once — the same reason chat-adapter states: a resolver that reads the request may
|
|
196
|
+
// answer differently the second time, and these two fields must agree about who this is.
|
|
197
|
+
// Resolved FIRST, because the identity decision below cannot be made without knowing the subject.
|
|
198
|
+
const ident = await opts.identity?.(c.req.raw);
|
|
199
|
+
const subject = opts.resolveResourceId?.(c, body) ?? ident?.resourceId;
|
|
200
|
+
// WHICH ORGANIZATION — only `identity` can say, and never the body. Same rule and same reason as
|
|
201
|
+
// the sibling route: an org is an isolation boundary, so it comes from the hook that already
|
|
202
|
+
// reads a verified session, and `sealRequestContext` strips any the caller tried to assert.
|
|
203
|
+
const org = ident?.orgId;
|
|
204
|
+
// WHICH RUN (package #5, §7). Two names can arrive, and they follow different rules:
|
|
205
|
+
//
|
|
206
|
+
// `body.workKey` — DECLARED. Always a workKey, always fail-closed: without an address the
|
|
207
|
+
// engine cannot tell whose job this is (§6), and the field is new, so nobody loses anything.
|
|
208
|
+
//
|
|
209
|
+
// `Idempotency-Key` — IMPLICIT, usually stamped by a gateway. A workKey when there is a
|
|
210
|
+
// subject, otherwise the raw runId it has been since FAZ-1. The precedence and the reason are
|
|
211
|
+
// @gnldev/chat-adapter's, as they were when this header was first accepted here — and so is the
|
|
212
|
+
// fallback: this route ships with no auth, and refusing every identity-less deployment that
|
|
213
|
+
// put a proxy in front of it would be a regression dressed as a rule.
|
|
214
|
+
//
|
|
215
|
+
// Nothing else moves: with no identity at all this route still answers 400, as it always has.
|
|
216
|
+
const header = c.req.header('Idempotency-Key');
|
|
217
|
+
if (!body.runId && !body.workKey && header) {
|
|
218
|
+
if (subject)
|
|
219
|
+
body.workKey = header;
|
|
220
|
+
else
|
|
221
|
+
body.runId = header;
|
|
222
|
+
}
|
|
223
|
+
if (!body.runId && !body.workKey)
|
|
224
|
+
return c.json({ error: 'runId or workKey is required (one names an id, the other names the work)' }, 400);
|
|
225
|
+
let identity;
|
|
226
|
+
try {
|
|
227
|
+
identity = resolveWorkIdentity(`agent:${name}`, {
|
|
228
|
+
...(body.runId !== undefined ? { runId: body.runId } : {}),
|
|
229
|
+
...(body.workKey !== undefined ? { workKey: body.workKey } : {}),
|
|
230
|
+
// The agent's own declaration — which address a name is unique within is a property of the
|
|
231
|
+
// work, not of the request (see AgentConfig.workScope).
|
|
232
|
+
scopeKind: gnl.agent(name).workScope ?? 'resource',
|
|
233
|
+
...(subject ? { resourceId: subject } : {}),
|
|
234
|
+
// THE ORG — REST parity (§7). @gnldev/server passes it; without it an `'org'` workScope
|
|
235
|
+
// lands on the deployment sentinel (§10.2) and the same org's same named work gets a
|
|
236
|
+
// DIFFERENT id here than it does through REST. Note this is also what makes an org-scoped
|
|
237
|
+
// run with NO subject work: an org address is an address, so the fail-closed `'resource'`
|
|
238
|
+
// rule above does not apply to the nightly-reconciliation case.
|
|
239
|
+
...(org ? { orgId: org } : {}),
|
|
240
|
+
anonymous: 'refuse',
|
|
241
|
+
surface: `POST /agents/${name}/run`,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
catch (e) {
|
|
245
|
+
return c.json({ error: String(e?.message ?? e) }, 400);
|
|
246
|
+
}
|
|
247
|
+
const runId = identity.runId;
|
|
248
|
+
const declared = identity.work?.workKey;
|
|
249
|
+
// `identity` sits below the dedicated resolver and above the body: it is server-derived, the body
|
|
250
|
+
// is not.
|
|
251
|
+
const threadId = opts.resolveThreadId?.(c, body) ?? ident?.threadId ?? body.threadId ?? runId;
|
|
252
|
+
let result;
|
|
253
|
+
try {
|
|
254
|
+
result = await gnl.stream(name, {
|
|
255
|
+
// The NAME when one was declared, the raw id otherwise — the door re-resolves the same tuple
|
|
256
|
+
// and lands on the same id, and only the name gets written into the run's record.
|
|
257
|
+
...(declared !== undefined ? { workKey: declared } : { runId }),
|
|
258
|
+
prompt: body.prompt,
|
|
259
|
+
messages: body.messages,
|
|
260
|
+
// `threadId`, hesaplanan değer — `body.threadId` DEĞİL. Bu satır ölü koddu: `resolveThreadId`
|
|
261
|
+
// çağrılıyor, sonucu yalnız SSE zarfına gidiyordu, koşum yine istemcinin dediği thread'e
|
|
262
|
+
// yazıp okuyordu. Yani host'un kimliği auth'tan türetmek için verdiği TEK kanca hafızayı hiç
|
|
263
|
+
// etkilemiyordu; host "düzelttim" sanıyordu.
|
|
264
|
+
threadId,
|
|
265
|
+
approvals: body.approvals,
|
|
266
|
+
// HER ZAMAN mühürlü — kimlik bilinmese bile. Ayrılmış anahtarlar motorun "bunu sunucu
|
|
267
|
+
// doğruladı" kanalıdır; mühürsüz bir gövde o kanalın sahibi olur.
|
|
268
|
+
context: sealRequestContext(body.context ?? {}, {
|
|
269
|
+
...(subject ? { resourceId: subject } : {}),
|
|
270
|
+
// The org goes into the seal too, so the record and the dynamic `system`/`tools` see the
|
|
271
|
+
// same organization the id was derived under — the derivation and the seal must not
|
|
272
|
+
// disagree about which boundary this run is inside.
|
|
273
|
+
...(org ? { orgId: org } : {}),
|
|
274
|
+
}),
|
|
275
|
+
...(subject ? { resourceId: subject } : {}),
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
catch (e) {
|
|
279
|
+
// A refusal thrown BEFORE the stream exists is still one of ours, and it used to arrive as a bare
|
|
280
|
+
// 400 with the reason flattened into prose. `streamDurable` asserts thread ownership and takes the
|
|
281
|
+
// run lock before it returns anything, so `RunThreadMismatchError`, `RunBusyError`,
|
|
282
|
+
// `SideEffectRetryBlockedError` and `RetryLimitExceededError` all land here — the same errors
|
|
283
|
+
// @gnldev/server answers with a status and a `code`. A client talking to this route had to match
|
|
284
|
+
// on the sentence instead, which is the practice the typed errors exist to end.
|
|
285
|
+
//
|
|
286
|
+
// Errors raised MID-stream are a different contract and are untouched: once frames are flowing
|
|
287
|
+
// they surface as an SSE `error` event (see pipeAguiStream), because a response already committed to
|
|
288
|
+
// 200 cannot become a 409.
|
|
289
|
+
const name = e?.name;
|
|
290
|
+
if (name === 'RunThreadMismatchError') {
|
|
291
|
+
return c.json({ error: e.message, code: 'run_thread_mismatch', detail: withWorkKey(e.detail, declared) }, 409);
|
|
292
|
+
}
|
|
293
|
+
// FAZ-4 caller-conflict family (K9: durable's single map — this route was the consumer the
|
|
294
|
+
// first cut forgot; a critical-profile input/actor/swept refusal must not collapse to a bare 400).
|
|
295
|
+
const conflict = callerConflictCode(e);
|
|
296
|
+
if (conflict) {
|
|
297
|
+
return c.json({ error: e.message, code: conflict, detail: withWorkKey(e.detail, declared) }, 409);
|
|
298
|
+
}
|
|
299
|
+
const blocked = blockedErrorCode(e);
|
|
300
|
+
if (blocked) {
|
|
301
|
+
const body = { error: e?.message ?? String(e), code: blocked, detail: e?.detail };
|
|
302
|
+
const res = blocked === 'retry_limit_exceeded'
|
|
303
|
+
? c.json(body, 422)
|
|
304
|
+
: c.json({ ...body, resumable: true }, 409);
|
|
305
|
+
// Same Retry-After contract as @gnldev/server and chat-adapter: run_busy is "correct, only early".
|
|
306
|
+
if (blocked === 'run_busy')
|
|
307
|
+
res.headers.set('Retry-After', '5');
|
|
308
|
+
return res;
|
|
309
|
+
}
|
|
310
|
+
if (name === 'RunLimitExceededError' || name === 'ToolLoopDetectedError') {
|
|
311
|
+
const code = name === 'RunLimitExceededError' ? EDGE_ERROR_CODES.runLimitExceeded : EDGE_ERROR_CODES.toolLoopDetected;
|
|
312
|
+
return c.json({ error: e.message, code, detail: e.detail, resumable: true }, 422);
|
|
313
|
+
}
|
|
314
|
+
return c.json({ error: String(e?.message ?? e) }, 400);
|
|
315
|
+
}
|
|
316
|
+
return pipeAguiStream(c, runId, result, { threadId });
|
|
317
|
+
});
|
|
318
|
+
return app;
|
|
319
|
+
}
|
|
320
|
+
/** The AG-UI route as a fetch handler — mount with `app.mount(path, ...)` on a Hono host. */
|
|
321
|
+
export function createAguiRoute(config, opts = {}) {
|
|
322
|
+
return toFetchHandler(aguiRouteApp(config, opts));
|
|
323
|
+
}
|
|
324
|
+
//# sourceMappingURL=route.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route.js","sourceRoot":"","sources":["../src/route.ts"],"names":[],"mappings":"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","sourcesContent":["// pipeAguiStream: the AG-UI-output counterpart of @gnldev/server's pipeAgentStream. Streams fullStream (AI SDK\n// StreamTextResult) as SSE, but instead of writing GNL's own event/data schema, converts it via toAguiEvents\n// and writes AG-UI events. createAguiRoute: a small single-endpoint (POST /agents/:name/run) factory —\n// an endpoint that CopilotKit's AG-UI HttpAgent can POST to.\n//\n// NOTE (deliberate choice — don't break the architecture): the switch that converts fullStream parts to the\n// GNL event/data shape is copied from INSIDE pipeAgentStream in packages/server/src/sse.ts (there is no\n// exported hook). sse.ts is a sensitive file carrying the W3 resumable-id contract and locked in by\n// process-kill/exactly-once tests — rather than adding a \"sink\" parameter there, keeping a small,\n// independent copy here is safer (without touching the existing architecture). The event/data shape of the\n// two copies must be kept in sync with sse.ts; see test/route.test.ts (parallel tests with the same mock patterns).\nimport type { Context } from 'hono';\nimport { toFetchHandler, type FetchHandler } from './handler.js';\nimport { Hono } from 'hono';\nimport { limitBreachFromSteps, blockedFromSteps, BLOCKED_ERROR_CODES, blockedErrorCode, callerConflictCode, sealRequestContext, resolveWorkIdentity } from '@gnldev/durable';\nimport type { CreateGnlConfig, GnlIdentity, ResolvedWorkIdentity } from '@gnldev/durable';\nimport { createGnl } from '@gnldev/durable';\nimport { streamSSE } from 'hono/streaming';\n// The two limit codes are READ from @gnldev/server rather than spelled again here. The event/data\n// SHAPE is deliberately a copy (see the note above), but a code is not a shape: it is the string a\n// caller matches on, and this route mirroring sse.ts by hand is exactly how the docs check ended up\n// with a list it had to maintain. `interruptsFromSteps` already makes this a build-order dependency.\nimport { interruptsFromSteps, EDGE_ERROR_CODES } from '@gnldev/server';\nimport { toAguiEvents, initialAguiConvertState, type GnlSseEvent } from './convert.js';\nimport { EventType, type AguiEvent, type RunStartedEvent } from './types.js';\n\nexport interface PipeAguiStreamOptions {\n /** AG-UI threadId. If not given, runId is used (single-thread default). */\n threadId?: string;\n}\n\n/** Stream fullStream as AG-UI SSE events (starts with RUN_STARTED, ends with RUN_FINISHED/RUN_ERROR). */\nexport function pipeAguiStream(c: Context, runId: string, result: any, opts?: PipeAguiStreamOptions) {\n const threadId = opts?.threadId ?? runId;\n const ctx = { threadId, runId };\n // The header patch, written out rather than imported from @gnldev/server: importing a runtime\n // helper across packages resolves through that package's BUILT output, and a stale dist turns the\n // call into `undefined` — measured, this exact swap emptied the stream and every test here failed\n // on `JSON.parse('')`. Six lines of duplication beats a build-order dependency.\n //\n // Why the headers: Hono sets `Cache-Control: no-cache`, which says nothing about re-encoding, so a\n // compression middleware in the host's chain buffers the stream into one chunk delivered at the\n // end (measured on Express: 13 progressive chunks became 1). `no-transform` stops it;\n // `X-Accel-Buffering: no` is the nginx half — measured behind a real one: load-bearing exactly\n // when the client speaks HTTP/1.1 to a gzipping proxy (without it the stream collapses into one\n // chunk at the end), and inert otherwise, HTTP/2 included.\n // Kept IN SYNC with packages/server/src/sse.ts and packages/studio/src/sse.ts.\n const res = streamSSE(c, async (stream) => {\n // In AG-UI every SSE frame is a single JSON event; the type is inside the event JSON (the spec has\n // no event/data split of its own) → the SSE `event:` field is NOT USED, only `data:` is written.\n const write = async (event: AguiEvent) => {\n await stream.writeSSE({ data: JSON.stringify(event) });\n };\n let state = initialAguiConvertState;\n const emit = async (gnlEvent: GnlSseEvent) => {\n const out = toAguiEvents(gnlEvent, ctx, state);\n state = out.state;\n for (const e of out.events) await write(e);\n };\n const started: RunStartedEvent = { type: EventType.RUN_STARTED, threadId, runId };\n await write(started);\n try {\n for await (const part of result.fullStream) {\n if (stream.aborted) break;\n switch (part.type) {\n case 'text-delta': {\n const text = part.text ?? part.delta ?? '';\n if (text) await emit({ event: 'text-delta', data: { text } });\n break;\n }\n case 'tool-call':\n await emit({ event: 'tool-call', data: { toolCallId: part.toolCallId, toolName: part.toolName, input: part.input } });\n break;\n case 'tool-result':\n if (part.output?.__gnl_suspend) break; // suspend sentinel is carried by the interrupt event\n if (part.output?.__gnl_limit_exceeded) break; // INTERNAL API sentinel — does not leak, carried by the error event\n if (part.output?.__gnl_blocked) break; // K1: the block sentinel is also an INTERNAL API — carried by the error event\n await emit({ event: 'tool-result', data: { toolCallId: part.toolCallId, toolName: part.toolName, output: part.output } });\n break;\n // P0.1: kept IN SYNC with sse.ts (see the sync note at the top of this file) — reasoning/\n // tool-input/source/file/step/tool-error events + the raw marker; nothing silently dropped.\n case 'reasoning-start':\n await emit({ event: 'reasoning-start', data: { id: part.id } });\n break;\n case 'reasoning-delta': {\n const text = part.text ?? part.delta ?? '';\n if (text) await emit({ event: 'reasoning-delta', data: { id: part.id, text } });\n break;\n }\n case 'reasoning-end':\n await emit({ event: 'reasoning-end', data: { id: part.id } });\n break;\n case 'tool-input-start':\n await emit({ event: 'tool-input-start', data: { toolCallId: part.toolCallId ?? part.id, toolName: part.toolName } });\n break;\n case 'tool-input-delta':\n await emit({ event: 'tool-input-delta', data: { toolCallId: part.toolCallId ?? part.id, delta: part.delta } });\n break;\n case 'tool-input-end':\n await emit({ event: 'tool-input-end', data: { toolCallId: part.toolCallId ?? part.id } });\n break;\n case 'source':\n await emit({ event: 'source', data: { sourceType: part.sourceType, id: part.id, url: part.url, title: part.title } });\n break;\n case 'file':\n await emit({ event: 'file', data: { mediaType: part.file?.mediaType, base64: part.file?.base64 } });\n break;\n case 'start-step':\n await emit({ event: 'step-start', data: {} });\n break;\n case 'finish-step':\n await emit({ event: 'step-finish', data: { finishReason: part.finishReason, usage: part.usage } });\n break;\n case 'tool-error':\n await emit({ event: 'tool-error', data: { toolCallId: part.toolCallId, toolName: part.toolName, error: String((part as any).error?.message ?? (part as any).error) } });\n break;\n case 'error':\n await emit({ event: 'error', data: { error: String((part as any).error?.message ?? (part as any).error) } });\n break;\n case 'start': case 'finish': case 'text-start': case 'text-end': case 'abort': case 'raw':\n break; // deliberately no event — same list and reasons as sse.ts\n default:\n await emit({ event: 'raw', data: { type: part.type } }); // unknown part → type-only marker, never silent\n break;\n }\n }\n const steps = await result.steps;\n const breach = limitBreachFromSteps(steps);\n if (breach) {\n await emit({\n event: 'error',\n data: {\n error: breach.message,\n code: breach.kind === 'loop' ? EDGE_ERROR_CODES.toolLoopDetected : EDGE_ERROR_CODES.runLimitExceeded,\n detail: breach.detail,\n },\n });\n return;\n }\n // K1: the block sentinel (side-effect/retry/busy) → same contract as a limit breach: terminal error, no done.\n const blocked = blockedFromSteps(steps);\n if (blocked) {\n await emit({ event: 'error', data: { error: blocked.message, code: BLOCKED_ERROR_CODES[blocked.code] ?? 'run_busy', detail: blocked.detail } });\n return;\n }\n const interrupts = interruptsFromSteps(steps);\n // FAZ-2 (chat-adapter parity): the approval ADDRESS travels with the interrupt — a client that\n // resumes without this runId starts a fresh run and the suspended one leaks forever. Parity is\n // measured by SCHEMA POSITION, not field name: chat-adapter stamps runId INSIDE each record\n // (ui-stream.ts), so a shared client helper (approvalPayload) must find it there on BOTH\n // adapters — the envelope copy stays for consumers already reading it.\n if (interrupts.length) await emit({ event: 'interrupt', data: { interrupts: interrupts.map((i) => ({ ...i, runId })), runId } });\n const finishReason = await Promise.resolve(result.finishReason).catch(() => undefined);\n const usage = await Promise.resolve(result.usage).catch(() => undefined);\n await emit({ event: 'done', data: { runId, finishReason, usage } });\n } catch (e: any) {\n await emit({ event: 'error', data: { error: String(e?.message ?? e) } });\n }\n });\n res.headers.set('Cache-Control', 'no-cache, no-transform');\n res.headers.set('X-Accel-Buffering', 'no');\n return res;\n}\n\n/**\n * The caller's own name for the work, reflected back in a refusal (§8, rules 1-3).\n *\n * FROM THE REQUEST, never from storage: a swept run keeps only a hash of its workKey (§10.3), so\n * reading the name back would undo a deletion. The caller already knows what it sent.\n *\n * `detail` and not `error`: the sentence is the most casually logged field in any HTTP client, and a\n * workKey is a business name. Same three lines as @gnldev/server's and @gnldev/chat-adapter's — the\n * dependency direction forbids sharing one (agui → durable, never agui → server for a renderer).\n */\nfunction withWorkKey(detail: unknown, workKey?: string): unknown {\n if (workKey === undefined) return detail;\n return detail && typeof detail === 'object' ? { ...(detail as Record<string, unknown>), workKey } : { workKey };\n}\n\nexport interface CreateAguiRouteOptions {\n /** AG-UI threadId resolver (from request + body). If not given, uses body.threadId, else runId. */\n resolveThreadId?: (c: Context, body: any) => string | undefined;\n /**\n * WHO this request acts for — resolved from something the SERVER trusts, never from the body.\n *\n * This route declares auth out of scope (see createAguiRoute's own note) and that boundary is\n * right. What was NOT right is what the boundary implied: `body.context` went to the engine\n * untouched, and the engine reads the reserved context keys as \"the server established this\". So\n * a request could name its own subject. Measured on the sibling route (chat-adapter, identical\n * shape): a POST carrying `{\"context\":{\"__gnl_resourceId\":\"KURBAN\"}}` produced a run owned by\n * that name — and once ownership stamping landed, the forged name became the LOCK's value too,\n * i.e. the caller handed itself the key.\n *\n * The route now always seals. With no resolver the seal carries no identity, which strips the\n * reserved keys: no forged subject gets in, and none is asserted either.\n *\n * HONEST BOUND: a resolver reading an unauthenticated request asserts a subject nobody verified.\n * Put auth in front of this route, or the subject is only as good as the caller's honesty.\n */\n resolveResourceId?: (c: Context, body: any) => string | undefined;\n /**\n * WHO and WHICH CONVERSATION, in one hook — the SAME signature @gnldev/chat-adapter's route takes, so a\n * host that has written this function once can mount either adapter with it.\n *\n * One signature for both routes is the point. Until now the two adapters asked the same question\n * with two hooks each, in two shapes, and this one had the sharper lesson: `resolveThreadId` was\n * called, its answer went into the SSE envelope, and the run still read and wrote the thread the\n * client named (see the Turkish note in the stream call below). A host could wire identity, watch\n * the right value appear in the response, and have changed nothing about where memory went.\n *\n * Takes the web `Request` rather than the Hono `Context`, matching @gnldev/server's `OrgOptions.resolve`:\n * a host mounting this from Express or Fastify has a Request and no Context.\n *\n * PRECEDENCE: `resolveResourceId` / `resolveThreadId` still win, field by field — an existing\n * deployment's answer is not taken away by a newer convenience. Called once per request.\n *\n * HONEST BOUND: this route declares auth out of scope. A resolver reading an unauthenticated\n * request asserts a subject nobody verified.\n */\n identity?: GnlIdentity;\n}\n\n/**\n * Produces a single-endpoint Hono router from a createGnl config that CopilotKit's AG-UI HttpAgent can talk to:\n * POST /agents/:name/run {runId, prompt|messages, threadId?, approvals?} → AG-UI SSE\n * Deliberately kept small: NO auth/org/budget gates (if needed, use @gnldev/server's createRestApi\n * and pass its stream result to pipeAguiStream — see README).\n */\nfunction aguiRouteApp(config: CreateGnlConfig, opts: CreateAguiRouteOptions = {}): Hono {\n const gnl = createGnl(config);\n // Same warning, same wording and same posture as @gnldev/chat-adapter's route: in production, a route\n // that can name nobody starts runs that are born ownerless, and an ownership gate with no owner to\n // compare against passes. Warn once at construction — never throw, because a deployment that puts\n // its own boundary in front of this route is not broken and must not be stopped at boot.\n if (process.env.NODE_ENV === 'production' && !opts.identity && !opts.resolveResourceId) {\n console.warn(\n '[gnl agui-route] no `identity` and no `resolveResourceId` in production — runs will be born ownerless; ' +\n 'ownership gates stay fail-open (a run with no owner is refused to nobody). Pass `identity: (req) => ({ resourceId })` ' +\n 'reading your session/JWT — never the request body.',\n );\n }\n const app = new Hono();\n app.post('/agents/:name/run', async (c) => {\n const name = c.req.param('name');\n const body = (await c.req.json().catch(() => ({}))) as any;\n // Resolved once — the same reason chat-adapter states: a resolver that reads the request may\n // answer differently the second time, and these two fields must agree about who this is.\n // Resolved FIRST, because the identity decision below cannot be made without knowing the subject.\n const ident = await opts.identity?.(c.req.raw);\n const subject = opts.resolveResourceId?.(c, body) ?? ident?.resourceId;\n // WHICH ORGANIZATION — only `identity` can say, and never the body. Same rule and same reason as\n // the sibling route: an org is an isolation boundary, so it comes from the hook that already\n // reads a verified session, and `sealRequestContext` strips any the caller tried to assert.\n const org = ident?.orgId;\n // WHICH RUN (package #5, §7). Two names can arrive, and they follow different rules:\n //\n // `body.workKey` — DECLARED. Always a workKey, always fail-closed: without an address the\n // engine cannot tell whose job this is (§6), and the field is new, so nobody loses anything.\n //\n // `Idempotency-Key` — IMPLICIT, usually stamped by a gateway. A workKey when there is a\n // subject, otherwise the raw runId it has been since FAZ-1. The precedence and the reason are\n // @gnldev/chat-adapter's, as they were when this header was first accepted here — and so is the\n // fallback: this route ships with no auth, and refusing every identity-less deployment that\n // put a proxy in front of it would be a regression dressed as a rule.\n //\n // Nothing else moves: with no identity at all this route still answers 400, as it always has.\n const header = c.req.header('Idempotency-Key');\n if (!body.runId && !body.workKey && header) {\n if (subject) body.workKey = header;\n else body.runId = header;\n }\n if (!body.runId && !body.workKey) return c.json({ error: 'runId or workKey is required (one names an id, the other names the work)' }, 400);\n let identity: ResolvedWorkIdentity;\n try {\n identity = resolveWorkIdentity(`agent:${name}`, {\n ...(body.runId !== undefined ? { runId: body.runId } : {}),\n ...(body.workKey !== undefined ? { workKey: body.workKey } : {}),\n // The agent's own declaration — which address a name is unique within is a property of the\n // work, not of the request (see AgentConfig.workScope).\n scopeKind: gnl.agent(name).workScope ?? 'resource',\n ...(subject ? { resourceId: subject } : {}),\n // THE ORG — REST parity (§7). @gnldev/server passes it; without it an `'org'` workScope\n // lands on the deployment sentinel (§10.2) and the same org's same named work gets a\n // DIFFERENT id here than it does through REST. Note this is also what makes an org-scoped\n // run with NO subject work: an org address is an address, so the fail-closed `'resource'`\n // rule above does not apply to the nightly-reconciliation case.\n ...(org ? { orgId: org } : {}),\n anonymous: 'refuse',\n surface: `POST /agents/${name}/run`,\n });\n } catch (e: any) {\n return c.json({ error: String(e?.message ?? e) }, 400);\n }\n const runId = identity.runId!;\n const declared = identity.work?.workKey;\n // `identity` sits below the dedicated resolver and above the body: it is server-derived, the body\n // is not.\n const threadId = opts.resolveThreadId?.(c, body) ?? ident?.threadId ?? body.threadId ?? runId;\n let result: any;\n try {\n result = await gnl.stream(name, {\n // The NAME when one was declared, the raw id otherwise — the door re-resolves the same tuple\n // and lands on the same id, and only the name gets written into the run's record.\n ...(declared !== undefined ? { workKey: declared } : { runId }),\n prompt: body.prompt,\n messages: body.messages,\n // `threadId`, hesaplanan değer — `body.threadId` DEĞİL. Bu satır ölü koddu: `resolveThreadId`\n // çağrılıyor, sonucu yalnız SSE zarfına gidiyordu, koşum yine istemcinin dediği thread'e\n // yazıp okuyordu. Yani host'un kimliği auth'tan türetmek için verdiği TEK kanca hafızayı hiç\n // etkilemiyordu; host \"düzelttim\" sanıyordu.\n threadId,\n approvals: body.approvals,\n // HER ZAMAN mühürlü — kimlik bilinmese bile. Ayrılmış anahtarlar motorun \"bunu sunucu\n // doğruladı\" kanalıdır; mühürsüz bir gövde o kanalın sahibi olur.\n context: sealRequestContext(body.context ?? {}, {\n ...(subject ? { resourceId: subject } : {}),\n // The org goes into the seal too, so the record and the dynamic `system`/`tools` see the\n // same organization the id was derived under — the derivation and the seal must not\n // disagree about which boundary this run is inside.\n ...(org ? { orgId: org } : {}),\n }),\n ...(subject ? { resourceId: subject } : {}),\n });\n } catch (e: any) {\n // A refusal thrown BEFORE the stream exists is still one of ours, and it used to arrive as a bare\n // 400 with the reason flattened into prose. `streamDurable` asserts thread ownership and takes the\n // run lock before it returns anything, so `RunThreadMismatchError`, `RunBusyError`,\n // `SideEffectRetryBlockedError` and `RetryLimitExceededError` all land here — the same errors\n // @gnldev/server answers with a status and a `code`. A client talking to this route had to match\n // on the sentence instead, which is the practice the typed errors exist to end.\n //\n // Errors raised MID-stream are a different contract and are untouched: once frames are flowing\n // they surface as an SSE `error` event (see pipeAguiStream), because a response already committed to\n // 200 cannot become a 409.\n const name = (e as { name?: string })?.name;\n if (name === 'RunThreadMismatchError') {\n return c.json({ error: e.message, code: 'run_thread_mismatch', detail: withWorkKey(e.detail, declared) }, 409);\n }\n // FAZ-4 caller-conflict family (K9: durable's single map — this route was the consumer the\n // first cut forgot; a critical-profile input/actor/swept refusal must not collapse to a bare 400).\n const conflict = callerConflictCode(e);\n if (conflict) {\n return c.json({ error: e.message, code: conflict, detail: withWorkKey(e.detail, declared) }, 409);\n }\n const blocked = blockedErrorCode(e);\n if (blocked) {\n const body = { error: e?.message ?? String(e), code: blocked, detail: e?.detail };\n const res = blocked === 'retry_limit_exceeded'\n ? c.json(body, 422)\n : c.json({ ...body, resumable: true }, 409);\n // Same Retry-After contract as @gnldev/server and chat-adapter: run_busy is \"correct, only early\".\n if (blocked === 'run_busy') res.headers.set('Retry-After', '5');\n return res;\n }\n if (name === 'RunLimitExceededError' || name === 'ToolLoopDetectedError') {\n const code = name === 'RunLimitExceededError' ? EDGE_ERROR_CODES.runLimitExceeded : EDGE_ERROR_CODES.toolLoopDetected;\n return c.json({ error: e.message, code, detail: e.detail, resumable: true }, 422);\n }\n return c.json({ error: String(e?.message ?? e) }, 400);\n }\n return pipeAguiStream(c, runId, result, { threadId });\n });\n return app;\n}\n\n/** The AG-UI route as a fetch handler — mount with `app.mount(path, ...)` on a Hono host. */\nexport function createAguiRoute(config: CreateGnlConfig, opts: CreateAguiRouteOptions = {}): FetchHandler {\n return toFetchHandler(aguiRouteApp(config, opts));\n}\n"]}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/** The core type tag carried by all AG-UI events. */
|
|
2
|
+
export declare enum EventType {
|
|
3
|
+
RUN_STARTED = "RUN_STARTED",
|
|
4
|
+
RUN_FINISHED = "RUN_FINISHED",
|
|
5
|
+
RUN_ERROR = "RUN_ERROR",
|
|
6
|
+
TEXT_MESSAGE_START = "TEXT_MESSAGE_START",
|
|
7
|
+
TEXT_MESSAGE_CONTENT = "TEXT_MESSAGE_CONTENT",
|
|
8
|
+
TEXT_MESSAGE_END = "TEXT_MESSAGE_END",
|
|
9
|
+
TOOL_CALL_START = "TOOL_CALL_START",
|
|
10
|
+
TOOL_CALL_ARGS = "TOOL_CALL_ARGS",
|
|
11
|
+
TOOL_CALL_END = "TOOL_CALL_END",
|
|
12
|
+
TOOL_CALL_RESULT = "TOOL_CALL_RESULT",
|
|
13
|
+
/** The spec's general-purpose escape hatch — for signals with no counterpart in the core (see interrupt mapping). */
|
|
14
|
+
CUSTOM = "CUSTOM"
|
|
15
|
+
}
|
|
16
|
+
/** Fields common to every event. `timestamp`/`rawEvent` exist in the official spec but we only fill them in when needed. */
|
|
17
|
+
export interface BaseAguiEvent {
|
|
18
|
+
type: EventType;
|
|
19
|
+
timestamp?: number;
|
|
20
|
+
/** Escape hatch for extra/raw data that doesn't fit the core schema (e.g. GNL error.detail). */
|
|
21
|
+
rawEvent?: unknown;
|
|
22
|
+
}
|
|
23
|
+
export interface RunStartedEvent extends BaseAguiEvent {
|
|
24
|
+
type: EventType.RUN_STARTED;
|
|
25
|
+
threadId: string;
|
|
26
|
+
runId: string;
|
|
27
|
+
}
|
|
28
|
+
export interface RunFinishedEvent extends BaseAguiEvent {
|
|
29
|
+
type: EventType.RUN_FINISHED;
|
|
30
|
+
threadId: string;
|
|
31
|
+
runId: string;
|
|
32
|
+
/** UNCERTAIN: we're not sure whether/what the official spec's field carrying the run result is called —
|
|
33
|
+
* as a best effort we carry GNL's finishReason/usage here. */
|
|
34
|
+
result?: unknown;
|
|
35
|
+
}
|
|
36
|
+
export interface RunErrorEvent extends BaseAguiEvent {
|
|
37
|
+
type: EventType.RUN_ERROR;
|
|
38
|
+
message: string;
|
|
39
|
+
code?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface TextMessageStartEvent extends BaseAguiEvent {
|
|
42
|
+
type: EventType.TEXT_MESSAGE_START;
|
|
43
|
+
messageId: string;
|
|
44
|
+
/** UNCERTAIN: it's not clear from the spec whether the role is always 'assistant' or can vary — since
|
|
45
|
+
* the text streamed on the GNL side is always agent output, we give a fixed 'assistant'. */
|
|
46
|
+
role?: 'assistant';
|
|
47
|
+
}
|
|
48
|
+
export interface TextMessageContentEvent extends BaseAguiEvent {
|
|
49
|
+
type: EventType.TEXT_MESSAGE_CONTENT;
|
|
50
|
+
messageId: string;
|
|
51
|
+
delta: string;
|
|
52
|
+
}
|
|
53
|
+
export interface TextMessageEndEvent extends BaseAguiEvent {
|
|
54
|
+
type: EventType.TEXT_MESSAGE_END;
|
|
55
|
+
messageId: string;
|
|
56
|
+
}
|
|
57
|
+
export interface ToolCallStartEvent extends BaseAguiEvent {
|
|
58
|
+
type: EventType.TOOL_CALL_START;
|
|
59
|
+
toolCallId: string;
|
|
60
|
+
toolCallName: string;
|
|
61
|
+
/** UNCERTAIN: we're not sure whether the spec has an optional field linking to the assistant message that started the tool call. */
|
|
62
|
+
parentMessageId?: string;
|
|
63
|
+
}
|
|
64
|
+
export interface ToolCallArgsEvent extends BaseAguiEvent {
|
|
65
|
+
type: EventType.TOOL_CALL_ARGS;
|
|
66
|
+
toolCallId: string;
|
|
67
|
+
/** In the official spec this is an incremental JSON chunk (streaming args) — since GNL's tool-call
|
|
68
|
+
* gives arguments COMPLETE (not streaming), here the entire JSON is sent in a SINGLE delta (see README note). */
|
|
69
|
+
delta: string;
|
|
70
|
+
}
|
|
71
|
+
export interface ToolCallEndEvent extends BaseAguiEvent {
|
|
72
|
+
type: EventType.TOOL_CALL_END;
|
|
73
|
+
toolCallId: string;
|
|
74
|
+
}
|
|
75
|
+
export interface ToolCallResultEvent extends BaseAguiEvent {
|
|
76
|
+
type: EventType.TOOL_CALL_RESULT;
|
|
77
|
+
messageId: string;
|
|
78
|
+
toolCallId: string;
|
|
79
|
+
content: string;
|
|
80
|
+
role?: 'tool';
|
|
81
|
+
}
|
|
82
|
+
/** For signals with no counterpart in the core (GNL interrupt/HITL is carried here — see README). */
|
|
83
|
+
export interface CustomEvent extends BaseAguiEvent {
|
|
84
|
+
type: EventType.CUSTOM;
|
|
85
|
+
name: string;
|
|
86
|
+
value: unknown;
|
|
87
|
+
}
|
|
88
|
+
export type AguiEvent = RunStartedEvent | RunFinishedEvent | RunErrorEvent | TextMessageStartEvent | TextMessageContentEvent | TextMessageEndEvent | ToolCallStartEvent | ToolCallArgsEvent | ToolCallEndEvent | ToolCallResultEvent | CustomEvent;
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// AG-UI (https://github.com/ag-ui-protocol/ag-ui — CopilotKit's open agent↔UI event protocol) core
|
|
2
|
+
// event types. HONESTY NOTE: this is a hand-extracted TS equivalent of the SUBSET of the known spec we
|
|
3
|
+
// need, WITHOUT installing the official `@ag-ui/core` package — not verified against the official
|
|
4
|
+
// conformance test. Fields we're not sure about (rawEvent, RunFinishedEvent.result, TextMessageStart.role
|
|
5
|
+
// being fixed) are marked in comments; no made-up fields were ADDED.
|
|
6
|
+
/** The core type tag carried by all AG-UI events. */
|
|
7
|
+
export var EventType;
|
|
8
|
+
(function (EventType) {
|
|
9
|
+
EventType["RUN_STARTED"] = "RUN_STARTED";
|
|
10
|
+
EventType["RUN_FINISHED"] = "RUN_FINISHED";
|
|
11
|
+
EventType["RUN_ERROR"] = "RUN_ERROR";
|
|
12
|
+
EventType["TEXT_MESSAGE_START"] = "TEXT_MESSAGE_START";
|
|
13
|
+
EventType["TEXT_MESSAGE_CONTENT"] = "TEXT_MESSAGE_CONTENT";
|
|
14
|
+
EventType["TEXT_MESSAGE_END"] = "TEXT_MESSAGE_END";
|
|
15
|
+
EventType["TOOL_CALL_START"] = "TOOL_CALL_START";
|
|
16
|
+
EventType["TOOL_CALL_ARGS"] = "TOOL_CALL_ARGS";
|
|
17
|
+
EventType["TOOL_CALL_END"] = "TOOL_CALL_END";
|
|
18
|
+
EventType["TOOL_CALL_RESULT"] = "TOOL_CALL_RESULT";
|
|
19
|
+
/** The spec's general-purpose escape hatch — for signals with no counterpart in the core (see interrupt mapping). */
|
|
20
|
+
EventType["CUSTOM"] = "CUSTOM";
|
|
21
|
+
})(EventType || (EventType = {}));
|
|
22
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,mGAAmG;AACnG,uGAAuG;AACvG,kGAAkG;AAClG,0GAA0G;AAC1G,qEAAqE;AAErE,qDAAqD;AACrD,MAAM,CAAN,IAAY,SAaX;AAbD,WAAY,SAAS;IACnB,wCAA2B,CAAA;IAC3B,0CAA6B,CAAA;IAC7B,oCAAuB,CAAA;IACvB,sDAAyC,CAAA;IACzC,0DAA6C,CAAA;IAC7C,kDAAqC,CAAA;IACrC,gDAAmC,CAAA;IACnC,8CAAiC,CAAA;IACjC,4CAA+B,CAAA;IAC/B,kDAAqC,CAAA;IACrC,qHAAqH;IACrH,8BAAiB,CAAA;AACnB,CAAC,EAbW,SAAS,KAAT,SAAS,QAapB","sourcesContent":["// AG-UI (https://github.com/ag-ui-protocol/ag-ui — CopilotKit's open agent↔UI event protocol) core\n// event types. HONESTY NOTE: this is a hand-extracted TS equivalent of the SUBSET of the known spec we\n// need, WITHOUT installing the official `@ag-ui/core` package — not verified against the official\n// conformance test. Fields we're not sure about (rawEvent, RunFinishedEvent.result, TextMessageStart.role\n// being fixed) are marked in comments; no made-up fields were ADDED.\n\n/** The core type tag carried by all AG-UI events. */\nexport enum EventType {\n RUN_STARTED = 'RUN_STARTED',\n RUN_FINISHED = 'RUN_FINISHED',\n RUN_ERROR = 'RUN_ERROR',\n TEXT_MESSAGE_START = 'TEXT_MESSAGE_START',\n TEXT_MESSAGE_CONTENT = 'TEXT_MESSAGE_CONTENT',\n TEXT_MESSAGE_END = 'TEXT_MESSAGE_END',\n TOOL_CALL_START = 'TOOL_CALL_START',\n TOOL_CALL_ARGS = 'TOOL_CALL_ARGS',\n TOOL_CALL_END = 'TOOL_CALL_END',\n TOOL_CALL_RESULT = 'TOOL_CALL_RESULT',\n /** The spec's general-purpose escape hatch — for signals with no counterpart in the core (see interrupt mapping). */\n CUSTOM = 'CUSTOM',\n}\n\n/** Fields common to every event. `timestamp`/`rawEvent` exist in the official spec but we only fill them in when needed. */\nexport interface BaseAguiEvent {\n type: EventType;\n timestamp?: number;\n /** Escape hatch for extra/raw data that doesn't fit the core schema (e.g. GNL error.detail). */\n rawEvent?: unknown;\n}\n\nexport interface RunStartedEvent extends BaseAguiEvent {\n type: EventType.RUN_STARTED;\n threadId: string;\n runId: string;\n}\n\nexport interface RunFinishedEvent extends BaseAguiEvent {\n type: EventType.RUN_FINISHED;\n threadId: string;\n runId: string;\n /** UNCERTAIN: we're not sure whether/what the official spec's field carrying the run result is called —\n * as a best effort we carry GNL's finishReason/usage here. */\n result?: unknown;\n}\n\nexport interface RunErrorEvent extends BaseAguiEvent {\n type: EventType.RUN_ERROR;\n message: string;\n code?: string;\n}\n\nexport interface TextMessageStartEvent extends BaseAguiEvent {\n type: EventType.TEXT_MESSAGE_START;\n messageId: string;\n /** UNCERTAIN: it's not clear from the spec whether the role is always 'assistant' or can vary — since\n * the text streamed on the GNL side is always agent output, we give a fixed 'assistant'. */\n role?: 'assistant';\n}\n\nexport interface TextMessageContentEvent extends BaseAguiEvent {\n type: EventType.TEXT_MESSAGE_CONTENT;\n messageId: string;\n delta: string;\n}\n\nexport interface TextMessageEndEvent extends BaseAguiEvent {\n type: EventType.TEXT_MESSAGE_END;\n messageId: string;\n}\n\nexport interface ToolCallStartEvent extends BaseAguiEvent {\n type: EventType.TOOL_CALL_START;\n toolCallId: string;\n toolCallName: string;\n /** UNCERTAIN: we're not sure whether the spec has an optional field linking to the assistant message that started the tool call. */\n parentMessageId?: string;\n}\n\nexport interface ToolCallArgsEvent extends BaseAguiEvent {\n type: EventType.TOOL_CALL_ARGS;\n toolCallId: string;\n /** In the official spec this is an incremental JSON chunk (streaming args) — since GNL's tool-call\n * gives arguments COMPLETE (not streaming), here the entire JSON is sent in a SINGLE delta (see README note). */\n delta: string;\n}\n\nexport interface ToolCallEndEvent extends BaseAguiEvent {\n type: EventType.TOOL_CALL_END;\n toolCallId: string;\n}\n\nexport interface ToolCallResultEvent extends BaseAguiEvent {\n type: EventType.TOOL_CALL_RESULT;\n messageId: string;\n toolCallId: string;\n content: string;\n role?: 'tool';\n}\n\n/** For signals with no counterpart in the core (GNL interrupt/HITL is carried here — see README). */\nexport interface CustomEvent extends BaseAguiEvent {\n type: EventType.CUSTOM;\n name: string;\n value: unknown;\n}\n\nexport type AguiEvent =\n | RunStartedEvent\n | RunFinishedEvent\n | RunErrorEvent\n | TextMessageStartEvent\n | TextMessageContentEvent\n | TextMessageEndEvent\n | ToolCallStartEvent\n | ToolCallArgsEvent\n | ToolCallEndEvent\n | ToolCallResultEvent\n | CustomEvent;\n"]}
|