@robota-sdk/agent-transport-http 3.0.0-beta.63 → 3.0.0-beta.77
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 +661 -21
- package/README.md +27 -58
- package/dist/node/index.cjs +1 -148
- package/dist/node/index.d.ts +16 -27
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +2 -120
- package/dist/node/index.js.map +1 -0
- package/package.json +19 -15
- package/src/__tests__/http-transport.test.ts +55 -0
- package/src/__tests__/routes.test.ts +168 -0
- package/src/http-transport.ts +41 -0
- package/src/index.ts +4 -0
- package/src/routes.ts +152 -0
- package/dist/node/index.d.cts +0 -44
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for HTTP transport routes.
|
|
3
|
+
* Uses Hono's built-in test client — no real HTTP server needed.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
7
|
+
import { createAgentRoutes } from '../routes.js';
|
|
8
|
+
import type { IInteractiveSession } from '@robota-sdk/agent-interface-transport';
|
|
9
|
+
|
|
10
|
+
function createMockSession(overrides?: Record<string, unknown>) {
|
|
11
|
+
return {
|
|
12
|
+
submit: vi.fn(),
|
|
13
|
+
abort: vi.fn(),
|
|
14
|
+
cancelQueue: vi.fn(),
|
|
15
|
+
getMessages: vi.fn().mockReturnValue([
|
|
16
|
+
{ role: 'user', content: 'hello' },
|
|
17
|
+
{ role: 'assistant', content: 'world' },
|
|
18
|
+
]),
|
|
19
|
+
getContextState: vi.fn().mockReturnValue({
|
|
20
|
+
usedPercentage: 10,
|
|
21
|
+
usedTokens: 1000,
|
|
22
|
+
maxTokens: 200000,
|
|
23
|
+
}),
|
|
24
|
+
isExecuting: vi.fn().mockReturnValue(false),
|
|
25
|
+
getPendingPrompt: vi.fn().mockReturnValue(null),
|
|
26
|
+
executeCommand: vi.fn().mockResolvedValue({
|
|
27
|
+
message: 'Conversation cleared.',
|
|
28
|
+
success: true,
|
|
29
|
+
}),
|
|
30
|
+
listCommands: vi.fn().mockReturnValue([]),
|
|
31
|
+
on: vi.fn(),
|
|
32
|
+
off: vi.fn(),
|
|
33
|
+
...overrides,
|
|
34
|
+
} as unknown as IInteractiveSession;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('HTTP Transport Routes', () => {
|
|
38
|
+
function createApp(session?: IInteractiveSession) {
|
|
39
|
+
const mockSession = session ?? createMockSession();
|
|
40
|
+
const app = createAgentRoutes({
|
|
41
|
+
sessionFactory: () => mockSession,
|
|
42
|
+
});
|
|
43
|
+
return { app, mockSession };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── POST /abort ───────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
it('POST /abort calls session.abort()', async () => {
|
|
49
|
+
const { app, mockSession } = createApp();
|
|
50
|
+
const res = await app.request('/abort', { method: 'POST' });
|
|
51
|
+
expect(res.status).toBe(200);
|
|
52
|
+
expect(await res.json()).toEqual({ ok: true });
|
|
53
|
+
expect(mockSession.abort).toHaveBeenCalled();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// ── POST /cancel-queue ────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
it('POST /cancel-queue calls session.cancelQueue()', async () => {
|
|
59
|
+
const { app, mockSession } = createApp();
|
|
60
|
+
const res = await app.request('/cancel-queue', { method: 'POST' });
|
|
61
|
+
expect(res.status).toBe(200);
|
|
62
|
+
expect(await res.json()).toEqual({ ok: true });
|
|
63
|
+
expect(mockSession.cancelQueue).toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ── GET /messages ─────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
it('GET /messages returns message history', async () => {
|
|
69
|
+
const { app } = createApp();
|
|
70
|
+
const res = await app.request('/messages');
|
|
71
|
+
expect(res.status).toBe(200);
|
|
72
|
+
const body = await res.json();
|
|
73
|
+
expect(body).toHaveLength(2);
|
|
74
|
+
expect(body[0].role).toBe('user');
|
|
75
|
+
expect(body[1].role).toBe('assistant');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ── GET /context ──────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
it('GET /context returns context window state', async () => {
|
|
81
|
+
const { app } = createApp();
|
|
82
|
+
const res = await app.request('/context');
|
|
83
|
+
expect(res.status).toBe(200);
|
|
84
|
+
const body = await res.json();
|
|
85
|
+
expect(body.usedTokens).toBe(1000);
|
|
86
|
+
expect(body.maxTokens).toBe(200000);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// ── GET /executing ────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
it('GET /executing returns execution status', async () => {
|
|
92
|
+
const { app } = createApp();
|
|
93
|
+
const res = await app.request('/executing');
|
|
94
|
+
expect(res.status).toBe(200);
|
|
95
|
+
expect(await res.json()).toEqual({ executing: false });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ── GET /pending ──────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
it('GET /pending returns null when no queue', async () => {
|
|
101
|
+
const { app } = createApp();
|
|
102
|
+
const res = await app.request('/pending');
|
|
103
|
+
expect(res.status).toBe(200);
|
|
104
|
+
expect(await res.json()).toEqual({ pending: null });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('GET /pending returns queued prompt', async () => {
|
|
108
|
+
const mockSession = createMockSession({
|
|
109
|
+
getPendingPrompt: vi.fn().mockReturnValue('queued prompt'),
|
|
110
|
+
});
|
|
111
|
+
const { app } = createApp(mockSession);
|
|
112
|
+
const res = await app.request('/pending');
|
|
113
|
+
expect(res.status).toBe(200);
|
|
114
|
+
expect(await res.json()).toEqual({ pending: 'queued prompt' });
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// ── POST /command ─────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
it('POST /command executes system command via session.executeCommand()', async () => {
|
|
120
|
+
const { app, mockSession } = createApp();
|
|
121
|
+
const res = await app.request('/command', {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
headers: { 'Content-Type': 'application/json' },
|
|
124
|
+
body: JSON.stringify({ name: 'clear', args: '' }),
|
|
125
|
+
});
|
|
126
|
+
expect(res.status).toBe(200);
|
|
127
|
+
const body = await res.json();
|
|
128
|
+
expect(body.success).toBe(true);
|
|
129
|
+
expect(body.message).toBe('Conversation cleared.');
|
|
130
|
+
expect(mockSession.executeCommand).toHaveBeenCalledWith('clear', '');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('POST /command returns 400 without name', async () => {
|
|
134
|
+
const { app } = createApp();
|
|
135
|
+
const res = await app.request('/command', {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers: { 'Content-Type': 'application/json' },
|
|
138
|
+
body: JSON.stringify({}),
|
|
139
|
+
});
|
|
140
|
+
expect(res.status).toBe(400);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('POST /command returns 404 for unknown command', async () => {
|
|
144
|
+
const mockSession = createMockSession({
|
|
145
|
+
executeCommand: vi.fn().mockResolvedValue(null),
|
|
146
|
+
});
|
|
147
|
+
const { app } = createApp(mockSession);
|
|
148
|
+
|
|
149
|
+
const res = await app.request('/command', {
|
|
150
|
+
method: 'POST',
|
|
151
|
+
headers: { 'Content-Type': 'application/json' },
|
|
152
|
+
body: JSON.stringify({ name: 'nonexistent' }),
|
|
153
|
+
});
|
|
154
|
+
expect(res.status).toBe(404);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ── POST /submit validation ───────────────────────────────────
|
|
158
|
+
|
|
159
|
+
it('POST /submit returns 400 without prompt', async () => {
|
|
160
|
+
const { app } = createApp();
|
|
161
|
+
const res = await app.request('/submit', {
|
|
162
|
+
method: 'POST',
|
|
163
|
+
headers: { 'Content-Type': 'application/json' },
|
|
164
|
+
body: JSON.stringify({}),
|
|
165
|
+
});
|
|
166
|
+
expect(res.status).toBe(400);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ITransportAdapter implementation for HTTP transport.
|
|
3
|
+
*
|
|
4
|
+
* Wraps createAgentRoutes into the unified ITransportAdapter interface
|
|
5
|
+
* while exposing the underlying Hono app via getApp().
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createAgentRoutes } from './routes.js';
|
|
9
|
+
|
|
10
|
+
import type { IInteractiveSession, ITransportAdapter } from '@robota-sdk/agent-interface-transport';
|
|
11
|
+
import type { Hono } from 'hono';
|
|
12
|
+
|
|
13
|
+
export interface IHttpTransportOptions {
|
|
14
|
+
/** Optional: base path prefix for routes. */
|
|
15
|
+
basePath?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createHttpTransport(
|
|
19
|
+
options?: IHttpTransportOptions,
|
|
20
|
+
): ITransportAdapter<IInteractiveSession> & { getApp(): Hono } {
|
|
21
|
+
let session: IInteractiveSession | null = null;
|
|
22
|
+
let app: Hono | null = null;
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
name: 'http',
|
|
26
|
+
attach(s: IInteractiveSession) {
|
|
27
|
+
session = s;
|
|
28
|
+
},
|
|
29
|
+
async start() {
|
|
30
|
+
if (!session) throw new Error('No session attached. Call attach() first.');
|
|
31
|
+
app = createAgentRoutes({ sessionFactory: () => session! });
|
|
32
|
+
},
|
|
33
|
+
async stop() {
|
|
34
|
+
app = null;
|
|
35
|
+
},
|
|
36
|
+
getApp() {
|
|
37
|
+
if (!app) throw new Error('Transport not started. Call start() first.');
|
|
38
|
+
return app;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
package/src/index.ts
ADDED
package/src/routes.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP transport adapter — exposes IInteractiveSession over REST API.
|
|
3
|
+
*
|
|
4
|
+
* Built on Hono for Cloudflare Workers + Node.js + AWS Lambda compatibility.
|
|
5
|
+
* Each endpoint maps 1:1 to an IInteractiveSession API method.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Hono } from 'hono';
|
|
9
|
+
import { streamSSE } from 'hono/streaming';
|
|
10
|
+
|
|
11
|
+
import type { IInteractiveSession } from '@robota-sdk/agent-interface-transport';
|
|
12
|
+
import type { Context } from 'hono';
|
|
13
|
+
|
|
14
|
+
/** Callback that resolves an IInteractiveSession from the request context. */
|
|
15
|
+
export type TSessionFactory = (c: Context) => IInteractiveSession | Promise<IInteractiveSession>;
|
|
16
|
+
|
|
17
|
+
export interface IAgentRoutesOptions {
|
|
18
|
+
/** Resolve an IInteractiveSession per request (e.g., by auth token, session ID). */
|
|
19
|
+
sessionFactory: TSessionFactory;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create a Hono router with all agent HTTP endpoints.
|
|
24
|
+
*
|
|
25
|
+
* Usage:
|
|
26
|
+
* ```typescript
|
|
27
|
+
* const routes = createAgentRoutes({ sessionFactory });
|
|
28
|
+
* app.route('/agent', routes); // mount on existing app
|
|
29
|
+
* export default routes; // or use standalone (CF Workers)
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function createAgentRoutes(options: IAgentRoutesOptions): Hono {
|
|
33
|
+
const { sessionFactory } = options;
|
|
34
|
+
const app = new Hono();
|
|
35
|
+
|
|
36
|
+
// POST /submit — execute prompt, stream events via SSE
|
|
37
|
+
app.post('/submit', async (c) => {
|
|
38
|
+
const session = await sessionFactory(c);
|
|
39
|
+
const body = await c.req.json<{ prompt: string }>();
|
|
40
|
+
|
|
41
|
+
if (!body.prompt || typeof body.prompt !== 'string') {
|
|
42
|
+
return c.json({ error: 'prompt is required' }, 400);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return streamSSE(c, async (stream) => {
|
|
46
|
+
const cleanup: Array<() => void> = [];
|
|
47
|
+
|
|
48
|
+
const subscribe = <T>(event: string, handler: (data: T) => void): void => {
|
|
49
|
+
session.on(event as 'text_delta', handler as () => void);
|
|
50
|
+
cleanup.push(() => session.off(event as 'text_delta', handler as () => void));
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
let completed = false;
|
|
54
|
+
const done = new Promise<void>((resolve) => {
|
|
55
|
+
subscribe('text_delta', (delta: string) => {
|
|
56
|
+
stream.writeSSE({ event: 'text_delta', data: JSON.stringify({ delta }) });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
subscribe('tool_start', (state) => {
|
|
60
|
+
stream.writeSSE({ event: 'tool_start', data: JSON.stringify(state) });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
subscribe('tool_end', (state) => {
|
|
64
|
+
stream.writeSSE({ event: 'tool_end', data: JSON.stringify(state) });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
subscribe('thinking', (isThinking: boolean) => {
|
|
68
|
+
stream.writeSSE({ event: 'thinking', data: JSON.stringify({ isThinking }) });
|
|
69
|
+
if (!isThinking && completed) {
|
|
70
|
+
resolve();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
subscribe('complete', (result) => {
|
|
75
|
+
completed = true;
|
|
76
|
+
stream.writeSSE({ event: 'complete', data: JSON.stringify(result) });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
subscribe('interrupted', (result) => {
|
|
80
|
+
completed = true;
|
|
81
|
+
stream.writeSSE({ event: 'interrupted', data: JSON.stringify(result) });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
subscribe('error', (error: Error) => {
|
|
85
|
+
completed = true;
|
|
86
|
+
stream.writeSSE({ event: 'error', data: JSON.stringify({ message: error.message }) });
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await session.submit(body.prompt);
|
|
91
|
+
await done;
|
|
92
|
+
|
|
93
|
+
for (const fn of cleanup) fn();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// POST /command — execute system command
|
|
98
|
+
app.post('/command', async (c) => {
|
|
99
|
+
const session = await sessionFactory(c);
|
|
100
|
+
const body = await c.req.json<{ name: string; args?: string }>();
|
|
101
|
+
|
|
102
|
+
if (!body.name || typeof body.name !== 'string') {
|
|
103
|
+
return c.json({ error: 'name is required' }, 400);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const result = await session.executeCommand(body.name, body.args ?? '');
|
|
107
|
+
if (!result) {
|
|
108
|
+
return c.json({ error: `Unknown command: ${body.name}` }, 404);
|
|
109
|
+
}
|
|
110
|
+
return c.json(result);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// POST /abort — abort current execution
|
|
114
|
+
app.post('/abort', async (c) => {
|
|
115
|
+
const session = await sessionFactory(c);
|
|
116
|
+
session.abort();
|
|
117
|
+
return c.json({ ok: true });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// POST /cancel-queue — cancel queued prompt
|
|
121
|
+
app.post('/cancel-queue', async (c) => {
|
|
122
|
+
const session = await sessionFactory(c);
|
|
123
|
+
session.cancelQueue();
|
|
124
|
+
return c.json({ ok: true });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// GET /messages — get message history
|
|
128
|
+
app.get('/messages', async (c) => {
|
|
129
|
+
const session = await sessionFactory(c);
|
|
130
|
+
return c.json(session.getMessages());
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// GET /context — get context window state
|
|
134
|
+
app.get('/context', async (c) => {
|
|
135
|
+
const session = await sessionFactory(c);
|
|
136
|
+
return c.json(session.getContextState());
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// GET /executing — check if currently executing
|
|
140
|
+
app.get('/executing', async (c) => {
|
|
141
|
+
const session = await sessionFactory(c);
|
|
142
|
+
return c.json({ executing: session.isExecuting() });
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// GET /pending — get pending queued prompt
|
|
146
|
+
app.get('/pending', async (c) => {
|
|
147
|
+
const session = await sessionFactory(c);
|
|
148
|
+
return c.json({ pending: session.getPendingPrompt() });
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return app;
|
|
152
|
+
}
|
package/dist/node/index.d.cts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { Context, Hono } from 'hono';
|
|
2
|
-
import { InteractiveSession, ITransportAdapter } from '@robota-sdk/agent-sdk';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* HTTP transport adapter — exposes InteractiveSession over REST API.
|
|
6
|
-
*
|
|
7
|
-
* Built on Hono for Cloudflare Workers + Node.js + AWS Lambda compatibility.
|
|
8
|
-
* Each endpoint maps 1:1 to an InteractiveSession API method.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
/** Callback that resolves an InteractiveSession from the request context. */
|
|
12
|
-
type ISessionFactory = (c: Context) => InteractiveSession | Promise<InteractiveSession>;
|
|
13
|
-
interface IAgentRoutesOptions {
|
|
14
|
-
/** Resolve an InteractiveSession per request (e.g., by auth token, session ID). */
|
|
15
|
-
sessionFactory: ISessionFactory;
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Create a Hono router with all agent HTTP endpoints.
|
|
19
|
-
*
|
|
20
|
-
* Usage:
|
|
21
|
-
* ```typescript
|
|
22
|
-
* const routes = createAgentRoutes({ sessionFactory });
|
|
23
|
-
* app.route('/agent', routes); // mount on existing app
|
|
24
|
-
* export default routes; // or use standalone (CF Workers)
|
|
25
|
-
* ```
|
|
26
|
-
*/
|
|
27
|
-
declare function createAgentRoutes(options: IAgentRoutesOptions): Hono;
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* ITransportAdapter implementation for HTTP transport.
|
|
31
|
-
*
|
|
32
|
-
* Wraps createAgentRoutes into the unified ITransportAdapter interface
|
|
33
|
-
* while exposing the underlying Hono app via getApp().
|
|
34
|
-
*/
|
|
35
|
-
|
|
36
|
-
interface IHttpTransportOptions {
|
|
37
|
-
/** Optional: base path prefix for routes. */
|
|
38
|
-
basePath?: string;
|
|
39
|
-
}
|
|
40
|
-
declare function createHttpTransport(options?: IHttpTransportOptions): ITransportAdapter & {
|
|
41
|
-
getApp(): Hono;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
export { type IAgentRoutesOptions, type IHttpTransportOptions, type ISessionFactory, createAgentRoutes, createHttpTransport };
|