@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.
@@ -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 { TStreamFailureListener } from './submit-stream.js';
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 function submitHandler(
105
- sessionFactory: TSessionFactory,
106
- claims: ITurnClaims,
107
- onStreamFailure?: TStreamFailureListener,
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(c, relayTurn(session, body.prompt, releaseOnce, onStreamFailure));
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;
@@ -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 session.submit(prompt);
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