@robota-sdk/agent-transport-http 3.0.0-beta.87 → 3.0.0-beta.90
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/CHANGELOG.md +27 -0
- package/README.md +29 -13
- package/dist/node/index.cjs +1 -1
- package/dist/node/index.d.cts +17 -3
- package/dist/node/index.d.cts.map +1 -1
- package/dist/node/index.d.ts +17 -3
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +1 -1
- package/dist/node/index.js.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/http-transport.test.ts +6 -2
- package/src/__tests__/prompt-routes.test.ts +299 -0
- package/src/__tests__/routes.test.ts +32 -2
- package/src/http-session.ts +2 -0
- package/src/http-transport.ts +4 -1
- package/src/index.ts +1 -1
- package/src/open-prompts.ts +56 -0
- package/src/prompt-routes.ts +101 -0
- package/src/routes.ts +24 -3
- package/src/submit-route.ts +30 -8
- package/src/submit-stream.ts +58 -2
package/src/submit-route.ts
CHANGED
|
@@ -11,7 +11,8 @@ import { streamSSE } from 'hono/streaming';
|
|
|
11
11
|
import { relayTurn } from './submit-stream.js';
|
|
12
12
|
|
|
13
13
|
import type { IHttpTransportSession } from './http-session.js';
|
|
14
|
-
import type {
|
|
14
|
+
import type { IHttpOpenPrompts } from './open-prompts.js';
|
|
15
|
+
import type { IRelayPrompts, TStreamFailureListener, TTurnAttribution } from './submit-stream.js';
|
|
15
16
|
import type { ITurnClaims } from './turn-claims.js';
|
|
16
17
|
import type { Context } from 'hono';
|
|
17
18
|
|
|
@@ -101,18 +102,26 @@ function admitTurn(
|
|
|
101
102
|
return { claim };
|
|
102
103
|
}
|
|
103
104
|
|
|
104
|
-
export
|
|
105
|
-
sessionFactory: TSessionFactory
|
|
106
|
-
claims: ITurnClaims
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
export interface ISubmitRouteDeps {
|
|
106
|
+
readonly sessionFactory: TSessionFactory;
|
|
107
|
+
readonly claims: ITurnClaims;
|
|
108
|
+
readonly openPrompts: IHttpOpenPrompts;
|
|
109
|
+
readonly onStreamFailure?: TStreamFailureListener;
|
|
110
|
+
readonly attribution?: TTurnAttribution;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function submitHandler(deps: ISubmitRouteDeps) {
|
|
114
|
+
const { sessionFactory, claims, openPrompts, onStreamFailure, attribution } = deps;
|
|
109
115
|
return async (c: Context) => {
|
|
110
116
|
const session = await sessionFactory(c);
|
|
111
|
-
const body = await c.req.json<{ prompt: string }>();
|
|
117
|
+
const body = await c.req.json<{ prompt: string; receivePrompts?: unknown }>();
|
|
112
118
|
|
|
113
119
|
if (!body.prompt || typeof body.prompt !== 'string') {
|
|
114
120
|
return c.json({ error: 'prompt is required' }, 400);
|
|
115
121
|
}
|
|
122
|
+
if (body.receivePrompts !== undefined && typeof body.receivePrompts !== 'boolean') {
|
|
123
|
+
return c.json({ error: 'receivePrompts must be a boolean' }, 400);
|
|
124
|
+
}
|
|
116
125
|
|
|
117
126
|
// A session still building itself cannot name itself yet, and the claim needs its name. The
|
|
118
127
|
// claim itself is still taken synchronously after this, so two requests that both waited here
|
|
@@ -141,9 +150,19 @@ export function submitHandler(
|
|
|
141
150
|
const releaseOnce = (): void => {
|
|
142
151
|
if (!released) {
|
|
143
152
|
released = true;
|
|
153
|
+
// The stream that held this session's prompts is gone; what is still open is the session's
|
|
154
|
+
// to settle, and no longer this route's to list or answer.
|
|
155
|
+
openPrompts.clear(claim);
|
|
144
156
|
claims.release(claim);
|
|
145
157
|
}
|
|
146
158
|
};
|
|
159
|
+
const prompts: IRelayPrompts | undefined =
|
|
160
|
+
body.receivePrompts === true
|
|
161
|
+
? {
|
|
162
|
+
record: (frame) => openPrompts.record(claim, frame),
|
|
163
|
+
forget: (id) => openPrompts.forget(claim, id),
|
|
164
|
+
}
|
|
165
|
+
: undefined;
|
|
147
166
|
|
|
148
167
|
// The claim is taken OUTSIDE the callback, so its release cannot live only in the callback's
|
|
149
168
|
// `finally` — a claim whose release is not in the same protected region is a lock, not a claim,
|
|
@@ -165,7 +184,10 @@ export function submitHandler(
|
|
|
165
184
|
// Hono returns the Response synchronously and only the sync path is real — but review is
|
|
166
185
|
// right that a version returning a rejecting promise would sail past a bare `return`, and
|
|
167
186
|
// the cost of closing that class is one keyword.
|
|
168
|
-
return await streamSSE(
|
|
187
|
+
return await streamSSE(
|
|
188
|
+
c,
|
|
189
|
+
relayTurn(session, body.prompt, releaseOnce, onStreamFailure, attribution, prompts),
|
|
190
|
+
);
|
|
169
191
|
} catch (error) {
|
|
170
192
|
releaseOnce();
|
|
171
193
|
throw error;
|
package/src/submit-stream.ts
CHANGED
|
@@ -8,7 +8,14 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { IHttpTransportSession } from './http-session.js';
|
|
11
|
+
import type { THttpPromptFrame } from './open-prompts.js';
|
|
11
12
|
import type { SSEStreamingApi } from 'hono/streaming';
|
|
13
|
+
import type {
|
|
14
|
+
IAskRequestEvent,
|
|
15
|
+
IPermissionRequestEvent,
|
|
16
|
+
IPromptResolvedEvent,
|
|
17
|
+
ISubmitOptions,
|
|
18
|
+
} from '@robota-sdk/agent-interface-session';
|
|
12
19
|
|
|
13
20
|
/**
|
|
14
21
|
* Where the DETAIL of a post-headers stream failure goes — injected, never imported.
|
|
@@ -25,6 +32,22 @@ import type { SSEStreamingApi } from 'hono/streaming';
|
|
|
25
32
|
*/
|
|
26
33
|
export type TStreamFailureListener = (error: Error) => void;
|
|
27
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Who a submitted turn is attributed to, assigned by the HOST that composed the routes — never read
|
|
37
|
+
* from the request, because a client could otherwise claim to be the local operator or another
|
|
38
|
+
* surface in the usage record. Absent means the session's own default.
|
|
39
|
+
*/
|
|
40
|
+
export type TTurnAttribution = Pick<ISubmitOptions, 'driverId' | 'surface'>;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Where a prompt-receiving stream reports the prompts it forwarded, so a later request can list or
|
|
44
|
+
* answer them. Present only when the client asked to receive prompts.
|
|
45
|
+
*/
|
|
46
|
+
export interface IRelayPrompts {
|
|
47
|
+
record(frame: THttpPromptFrame): void;
|
|
48
|
+
forget(id: string): void;
|
|
49
|
+
}
|
|
50
|
+
|
|
28
51
|
/**
|
|
29
52
|
* Wire every relay subscription and the abort path; answer with the promise that settles when the
|
|
30
53
|
* turn is over (or refused one).
|
|
@@ -40,6 +63,7 @@ function wireRelay(
|
|
|
40
63
|
stream: SSEStreamingApi,
|
|
41
64
|
cleanup: Array<() => void>,
|
|
42
65
|
onFailure: TStreamFailureListener | undefined,
|
|
66
|
+
prompts: IRelayPrompts | undefined,
|
|
43
67
|
): Promise<void> {
|
|
44
68
|
const subscribe = <T>(event: string, handler: (data: T) => void): void => {
|
|
45
69
|
session.on(event as 'text_delta', handler as () => void);
|
|
@@ -63,6 +87,7 @@ function wireRelay(
|
|
|
63
87
|
subscribe('tool_start', (state) => void write('tool_start', state));
|
|
64
88
|
subscribe('tool_end', (state) => void write('tool_end', state));
|
|
65
89
|
subscribe('thinking', (isThinking: boolean) => void write('thinking', { isThinking }));
|
|
90
|
+
if (prompts !== undefined) subscribePrompts(subscribe, write, prompts);
|
|
66
91
|
|
|
67
92
|
// Flush the terminal event before resolving, so the resolve → cleanup → stream-close
|
|
68
93
|
// continuation cannot race ahead of the write.
|
|
@@ -87,6 +112,33 @@ function wireRelay(
|
|
|
87
112
|
return done;
|
|
88
113
|
}
|
|
89
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Forward the turn's permission and ask prompts, and their settlement, to a client that said it can
|
|
117
|
+
* answer them.
|
|
118
|
+
*
|
|
119
|
+
* Only such a client subscribes: the session parks a prompt only while someone listens for it, so a
|
|
120
|
+
* client that cannot answer must not listen, or the turn would wait on a prompt nobody settles
|
|
121
|
+
* instead of failing it closed at once.
|
|
122
|
+
*/
|
|
123
|
+
function subscribePrompts(
|
|
124
|
+
subscribe: <T>(event: string, handler: (data: T) => void) => void,
|
|
125
|
+
write: (event: string, data: unknown) => Promise<void>,
|
|
126
|
+
prompts: IRelayPrompts,
|
|
127
|
+
): void {
|
|
128
|
+
subscribe('permission_request', (event: IPermissionRequestEvent) => {
|
|
129
|
+
prompts.record({ type: 'permission_request', event });
|
|
130
|
+
void write('permission_request', event);
|
|
131
|
+
});
|
|
132
|
+
subscribe('ask_request', (event: IAskRequestEvent) => {
|
|
133
|
+
prompts.record({ type: 'ask_request', event });
|
|
134
|
+
void write('ask_request', event);
|
|
135
|
+
});
|
|
136
|
+
subscribe('prompt_resolved', (event: IPromptResolvedEvent) => {
|
|
137
|
+
prompts.forget(event.id);
|
|
138
|
+
void write('prompt_resolved', event);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
90
142
|
/**
|
|
91
143
|
* The abort path, alone, because every clause of it is load-bearing and review shaped each one.
|
|
92
144
|
*
|
|
@@ -154,6 +206,8 @@ export function relayTurn(
|
|
|
154
206
|
prompt: string,
|
|
155
207
|
release: () => void,
|
|
156
208
|
onFailure?: TStreamFailureListener,
|
|
209
|
+
attribution?: TTurnAttribution,
|
|
210
|
+
prompts?: IRelayPrompts,
|
|
157
211
|
): (stream: SSEStreamingApi) => Promise<void> {
|
|
158
212
|
return async (stream) => {
|
|
159
213
|
// NOTHING may escape this callback, and the reason is measured rather than stylistic. Hono's
|
|
@@ -168,8 +222,10 @@ export function relayTurn(
|
|
|
168
222
|
// later request gets 409 forever.
|
|
169
223
|
const cleanup: Array<() => void> = [];
|
|
170
224
|
try {
|
|
171
|
-
const done = wireRelay(session, stream, cleanup, onFailure);
|
|
172
|
-
await
|
|
225
|
+
const done = wireRelay(session, stream, cleanup, onFailure, prompts);
|
|
226
|
+
await (attribution === undefined
|
|
227
|
+
? session.submit(prompt)
|
|
228
|
+
: session.submit(prompt, undefined, undefined, { ...attribution }));
|
|
173
229
|
await done;
|
|
174
230
|
} finally {
|
|
175
231
|
// RUNTIME-14: teardown ALWAYS runs — on completion, error, OR client disconnect — so the
|