@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
package/README.md
CHANGED
|
@@ -1,78 +1,47 @@
|
|
|
1
1
|
# @robota-sdk/agent-transport-http
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
HTTP transport (built on [Hono](https://hono.dev)) for the Robota SDK. It exposes a running
|
|
4
|
+
`IInteractiveSession` over HTTP routes so a browser, another service, or any HTTP client can
|
|
5
|
+
drive an agent session.
|
|
4
6
|
|
|
5
7
|
## Installation
|
|
6
8
|
|
|
7
9
|
```bash
|
|
10
|
+
npm install @robota-sdk/agent-transport-http
|
|
11
|
+
# or
|
|
8
12
|
pnpm add @robota-sdk/agent-transport-http
|
|
9
13
|
```
|
|
10
14
|
|
|
11
15
|
## Usage
|
|
12
16
|
|
|
17
|
+
`createAgentRoutes` builds the agent route handlers from a `sessionFactory` that resolves an
|
|
18
|
+
`IInteractiveSession` per request (e.g. by auth token or session id). `createHttpTransport`
|
|
19
|
+
wraps them as a mountable transport with an optional `basePath`.
|
|
20
|
+
|
|
21
|
+
<!-- doc-example-skip: requires the host app's hono dependency -->
|
|
22
|
+
|
|
13
23
|
```typescript
|
|
14
24
|
import { createAgentRoutes } from '@robota-sdk/agent-transport-http';
|
|
15
|
-
import type { InteractiveSession } from '@robota-sdk/agent-sdk';
|
|
16
25
|
import { Hono } from 'hono';
|
|
17
26
|
|
|
18
27
|
const app = new Hono();
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
// Or export directly for Cloudflare Workers
|
|
28
|
-
export default routes;
|
|
28
|
+
app.route(
|
|
29
|
+
'/agent',
|
|
30
|
+
createAgentRoutes({
|
|
31
|
+
// Resolve (or create) the session for this request.
|
|
32
|
+
sessionFactory: (c) => resolveSessionForRequest(c),
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
29
35
|
```
|
|
30
36
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
## Endpoints
|
|
34
|
-
|
|
35
|
-
| Method | Path | Request Body | Response | Description |
|
|
36
|
-
| ------ | ------------- | -------------------------------- | -------------------------------- | ---------------------------- |
|
|
37
|
-
| POST | /submit | `{ prompt: string }` | SSE stream | Submit prompt, stream events |
|
|
38
|
-
| POST | /command | `{ name: string, args: string }` | `ICommandResult` JSON | Execute system command |
|
|
39
|
-
| POST | /abort | — | `{ ok: true }` | Abort current execution |
|
|
40
|
-
| POST | /cancel-queue | — | `{ ok: true }` | Cancel queued prompt |
|
|
41
|
-
| GET | /messages | — | `TUniversalMessage[]` JSON | Get message history |
|
|
42
|
-
| GET | /context | — | `IContextWindowState` JSON | Get context window state |
|
|
43
|
-
| GET | /executing | — | `{ executing: boolean }` JSON | Check if currently executing |
|
|
44
|
-
| GET | /pending | — | `{ pending: string\|null }` JSON | Get queued prompt |
|
|
45
|
-
|
|
46
|
-
### SSE Event Types (POST /submit)
|
|
47
|
-
|
|
48
|
-
| Event | Data | Description |
|
|
49
|
-
| ------------- | ------------------------- | ----------------------- |
|
|
50
|
-
| `text_delta` | `{ delta: string }` | Streaming text chunk |
|
|
51
|
-
| `tool_start` | `IToolState` | Tool execution began |
|
|
52
|
-
| `tool_end` | `IToolState` | Tool execution finished |
|
|
53
|
-
| `thinking` | `{ isThinking: boolean }` | Execution state changed |
|
|
54
|
-
| `complete` | `IExecutionResult` | Prompt completed |
|
|
55
|
-
| `interrupted` | `IExecutionResult` | Execution was aborted |
|
|
56
|
-
| `error` | `{ message: string }` | Execution error |
|
|
57
|
-
|
|
58
|
-
## ITransportAdapter
|
|
59
|
-
|
|
60
|
-
The HTTP transport implements the `ITransportAdapter` interface from `@robota-sdk/agent-sdk`:
|
|
61
|
-
|
|
62
|
-
```typescript
|
|
63
|
-
import { createHttpTransport } from '@robota-sdk/agent-transport-http';
|
|
64
|
-
import type { ITransportAdapter } from '@robota-sdk/agent-sdk';
|
|
65
|
-
|
|
66
|
-
const transport: ITransportAdapter = createHttpTransport();
|
|
67
|
-
|
|
68
|
-
transport.attach(interactiveSession);
|
|
69
|
-
await transport.start();
|
|
70
|
-
|
|
71
|
-
// Access the Hono app for serving
|
|
72
|
-
const app = transport.getApp();
|
|
73
|
-
```
|
|
37
|
+
## Exports
|
|
74
38
|
|
|
75
|
-
|
|
39
|
+
| Symbol | Kind | Description |
|
|
40
|
+
| ----------------------- | --------- | --------------------------------------------------------- |
|
|
41
|
+
| `createHttpTransport` | function | `(options?: IHttpTransportOptions)` — mountable transport |
|
|
42
|
+
| `createAgentRoutes` | function | `(options: IAgentRoutesOptions)` — Hono agent routes |
|
|
43
|
+
| `IHttpTransportOptions` | interface | `{ basePath?: string }` |
|
|
44
|
+
| `IAgentRoutesOptions` | interface | `{ sessionFactory: TSessionFactory }` |
|
|
45
|
+
| `TSessionFactory` | type | Resolves an `IInteractiveSession` per request |
|
|
76
46
|
|
|
77
|
-
|
|
78
|
-
- `hono` — HTTP framework
|
|
47
|
+
See [docs/SPEC.md](./docs/SPEC.md) for the full contract and route surface.
|
package/dist/node/index.cjs
CHANGED
|
@@ -1,148 +1 @@
|
|
|
1
|
-
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
var __export = (target, all) => {
|
|
7
|
-
for (var name in all)
|
|
8
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
-
};
|
|
10
|
-
var __copyProps = (to, from, except, desc) => {
|
|
11
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
-
for (let key of __getOwnPropNames(from))
|
|
13
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
-
|
|
20
|
-
// src/index.ts
|
|
21
|
-
var index_exports = {};
|
|
22
|
-
__export(index_exports, {
|
|
23
|
-
createAgentRoutes: () => createAgentRoutes,
|
|
24
|
-
createHttpTransport: () => createHttpTransport
|
|
25
|
-
});
|
|
26
|
-
module.exports = __toCommonJS(index_exports);
|
|
27
|
-
|
|
28
|
-
// src/routes.ts
|
|
29
|
-
var import_hono = require("hono");
|
|
30
|
-
var import_streaming = require("hono/streaming");
|
|
31
|
-
function createAgentRoutes(options) {
|
|
32
|
-
const { sessionFactory } = options;
|
|
33
|
-
const app = new import_hono.Hono();
|
|
34
|
-
app.post("/submit", async (c) => {
|
|
35
|
-
const session = await sessionFactory(c);
|
|
36
|
-
const body = await c.req.json();
|
|
37
|
-
if (!body.prompt || typeof body.prompt !== "string") {
|
|
38
|
-
return c.json({ error: "prompt is required" }, 400);
|
|
39
|
-
}
|
|
40
|
-
return (0, import_streaming.streamSSE)(c, async (stream) => {
|
|
41
|
-
const cleanup = [];
|
|
42
|
-
const subscribe = (event, handler) => {
|
|
43
|
-
session.on(event, handler);
|
|
44
|
-
cleanup.push(() => session.off(event, handler));
|
|
45
|
-
};
|
|
46
|
-
let completed = false;
|
|
47
|
-
const done = new Promise((resolve) => {
|
|
48
|
-
subscribe("text_delta", (delta) => {
|
|
49
|
-
stream.writeSSE({ event: "text_delta", data: JSON.stringify({ delta }) });
|
|
50
|
-
});
|
|
51
|
-
subscribe("tool_start", (state) => {
|
|
52
|
-
stream.writeSSE({ event: "tool_start", data: JSON.stringify(state) });
|
|
53
|
-
});
|
|
54
|
-
subscribe("tool_end", (state) => {
|
|
55
|
-
stream.writeSSE({ event: "tool_end", data: JSON.stringify(state) });
|
|
56
|
-
});
|
|
57
|
-
subscribe("thinking", (isThinking) => {
|
|
58
|
-
stream.writeSSE({ event: "thinking", data: JSON.stringify({ isThinking }) });
|
|
59
|
-
if (!isThinking && completed) {
|
|
60
|
-
resolve();
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
subscribe("complete", (result) => {
|
|
64
|
-
completed = true;
|
|
65
|
-
stream.writeSSE({ event: "complete", data: JSON.stringify(result) });
|
|
66
|
-
});
|
|
67
|
-
subscribe("interrupted", (result) => {
|
|
68
|
-
completed = true;
|
|
69
|
-
stream.writeSSE({ event: "interrupted", data: JSON.stringify(result) });
|
|
70
|
-
});
|
|
71
|
-
subscribe("error", (error) => {
|
|
72
|
-
completed = true;
|
|
73
|
-
stream.writeSSE({ event: "error", data: JSON.stringify({ message: error.message }) });
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
await session.submit(body.prompt);
|
|
77
|
-
await done;
|
|
78
|
-
for (const fn of cleanup) fn();
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
app.post("/command", async (c) => {
|
|
82
|
-
const session = await sessionFactory(c);
|
|
83
|
-
const body = await c.req.json();
|
|
84
|
-
if (!body.name || typeof body.name !== "string") {
|
|
85
|
-
return c.json({ error: "name is required" }, 400);
|
|
86
|
-
}
|
|
87
|
-
const result = await session.executeCommand(body.name, body.args ?? "");
|
|
88
|
-
if (!result) {
|
|
89
|
-
return c.json({ error: `Unknown command: ${body.name}` }, 404);
|
|
90
|
-
}
|
|
91
|
-
return c.json(result);
|
|
92
|
-
});
|
|
93
|
-
app.post("/abort", async (c) => {
|
|
94
|
-
const session = await sessionFactory(c);
|
|
95
|
-
session.abort();
|
|
96
|
-
return c.json({ ok: true });
|
|
97
|
-
});
|
|
98
|
-
app.post("/cancel-queue", async (c) => {
|
|
99
|
-
const session = await sessionFactory(c);
|
|
100
|
-
session.cancelQueue();
|
|
101
|
-
return c.json({ ok: true });
|
|
102
|
-
});
|
|
103
|
-
app.get("/messages", async (c) => {
|
|
104
|
-
const session = await sessionFactory(c);
|
|
105
|
-
return c.json(session.getMessages());
|
|
106
|
-
});
|
|
107
|
-
app.get("/context", async (c) => {
|
|
108
|
-
const session = await sessionFactory(c);
|
|
109
|
-
return c.json(session.getContextState());
|
|
110
|
-
});
|
|
111
|
-
app.get("/executing", async (c) => {
|
|
112
|
-
const session = await sessionFactory(c);
|
|
113
|
-
return c.json({ executing: session.isExecuting() });
|
|
114
|
-
});
|
|
115
|
-
app.get("/pending", async (c) => {
|
|
116
|
-
const session = await sessionFactory(c);
|
|
117
|
-
return c.json({ pending: session.getPendingPrompt() });
|
|
118
|
-
});
|
|
119
|
-
return app;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// src/http-transport.ts
|
|
123
|
-
function createHttpTransport(options) {
|
|
124
|
-
let session = null;
|
|
125
|
-
let app = null;
|
|
126
|
-
return {
|
|
127
|
-
name: "http",
|
|
128
|
-
attach(s) {
|
|
129
|
-
session = s;
|
|
130
|
-
},
|
|
131
|
-
async start() {
|
|
132
|
-
if (!session) throw new Error("No session attached. Call attach() first.");
|
|
133
|
-
app = createAgentRoutes({ sessionFactory: () => session });
|
|
134
|
-
},
|
|
135
|
-
async stop() {
|
|
136
|
-
app = null;
|
|
137
|
-
},
|
|
138
|
-
getApp() {
|
|
139
|
-
if (!app) throw new Error("Transport not started. Call start() first.");
|
|
140
|
-
return app;
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
145
|
-
0 && (module.exports = {
|
|
146
|
-
createAgentRoutes,
|
|
147
|
-
createHttpTransport
|
|
148
|
-
});
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("hono"),t=require("hono/streaming");function n(n){let{sessionFactory:r}=n,i=new e.Hono;return i.post(`/submit`,async e=>{let n=await r(e),i=await e.req.json();return!i.prompt||typeof i.prompt!=`string`?e.json({error:`prompt is required`},400):(0,t.streamSSE)(e,async e=>{let t=[],r=(e,r)=>{n.on(e,r),t.push(()=>n.off(e,r))},a=!1,o=new Promise(t=>{r(`text_delta`,t=>{e.writeSSE({event:`text_delta`,data:JSON.stringify({delta:t})})}),r(`tool_start`,t=>{e.writeSSE({event:`tool_start`,data:JSON.stringify(t)})}),r(`tool_end`,t=>{e.writeSSE({event:`tool_end`,data:JSON.stringify(t)})}),r(`thinking`,n=>{e.writeSSE({event:`thinking`,data:JSON.stringify({isThinking:n})}),!n&&a&&t()}),r(`complete`,t=>{a=!0,e.writeSSE({event:`complete`,data:JSON.stringify(t)})}),r(`interrupted`,t=>{a=!0,e.writeSSE({event:`interrupted`,data:JSON.stringify(t)})}),r(`error`,t=>{a=!0,e.writeSSE({event:`error`,data:JSON.stringify({message:t.message})})})});await n.submit(i.prompt),await o;for(let e of t)e()})}),i.post(`/command`,async e=>{let t=await r(e),n=await e.req.json();if(!n.name||typeof n.name!=`string`)return e.json({error:`name is required`},400);let i=await t.executeCommand(n.name,n.args??``);return i?e.json(i):e.json({error:`Unknown command: ${n.name}`},404)}),i.post(`/abort`,async e=>((await r(e)).abort(),e.json({ok:!0}))),i.post(`/cancel-queue`,async e=>((await r(e)).cancelQueue(),e.json({ok:!0}))),i.get(`/messages`,async e=>{let t=await r(e);return e.json(t.getMessages())}),i.get(`/context`,async e=>{let t=await r(e);return e.json(t.getContextState())}),i.get(`/executing`,async e=>{let t=await r(e);return e.json({executing:t.isExecuting()})}),i.get(`/pending`,async e=>{let t=await r(e);return e.json({pending:t.getPendingPrompt()})}),i}function r(e){let t=null,r=null;return{name:`http`,attach(e){t=e},async start(){if(!t)throw Error(`No session attached. Call attach() first.`);r=n({sessionFactory:()=>t})},async stop(){r=null},getApp(){if(!r)throw Error(`Transport not started. Call start() first.`);return r}}}exports.createAgentRoutes=n,exports.createHttpTransport=r;
|
package/dist/node/index.d.ts
CHANGED
|
@@ -1,18 +1,12 @@
|
|
|
1
|
-
import { Context, Hono } from
|
|
2
|
-
import {
|
|
1
|
+
import { Context, Hono } from "hono";
|
|
2
|
+
import { IInteractiveSession, ITransportAdapter } from "@robota-sdk/agent-interface-transport";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
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>;
|
|
4
|
+
//#region src/routes.d.ts
|
|
5
|
+
/** Callback that resolves an IInteractiveSession from the request context. */
|
|
6
|
+
type TSessionFactory = (c: Context) => IInteractiveSession | Promise<IInteractiveSession>;
|
|
13
7
|
interface IAgentRoutesOptions {
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
/** Resolve an IInteractiveSession per request (e.g., by auth token, session ID). */
|
|
9
|
+
sessionFactory: TSessionFactory;
|
|
16
10
|
}
|
|
17
11
|
/**
|
|
18
12
|
* Create a Hono router with all agent HTTP endpoints.
|
|
@@ -25,20 +19,15 @@ interface IAgentRoutesOptions {
|
|
|
25
19
|
* ```
|
|
26
20
|
*/
|
|
27
21
|
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
|
-
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/http-transport.d.ts
|
|
36
24
|
interface IHttpTransportOptions {
|
|
37
|
-
|
|
38
|
-
|
|
25
|
+
/** Optional: base path prefix for routes. */
|
|
26
|
+
basePath?: string;
|
|
39
27
|
}
|
|
40
|
-
declare function createHttpTransport(options?: IHttpTransportOptions): ITransportAdapter & {
|
|
41
|
-
|
|
28
|
+
declare function createHttpTransport(options?: IHttpTransportOptions): ITransportAdapter<IInteractiveSession> & {
|
|
29
|
+
getApp(): Hono;
|
|
42
30
|
};
|
|
43
|
-
|
|
44
|
-
export { type IAgentRoutesOptions, type IHttpTransportOptions, type
|
|
31
|
+
//#endregion
|
|
32
|
+
export { type IAgentRoutesOptions, type IHttpTransportOptions, type TSessionFactory, createAgentRoutes, createHttpTransport };
|
|
33
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/routes.ts","../../src/http-transport.ts"],"mappings":";;;;;KAcY,eAAA,IAAmB,CAAA,EAAG,OAAA,KAAY,mBAAA,GAAsB,OAAA,CAAQ,mBAAA;AAAA,UAE3D,mBAAA;EAF0D;EAIzE,cAAA,EAAgB,eAAe;AAAA;;;;;AAJ8D;AAE/F;;;;AAEiC;iBAajB,iBAAA,CAAkB,OAAA,EAAS,mBAAA,GAAsB,IAAI;;;UCnBpD,qBAAA;EDE2D;ECA1E,QAAQ;AAAA;AAAA,iBAGM,mBAAA,CACd,OAAA,GAAU,qBAAA,GACT,iBAAA,CAAkB,mBAAA;EAAyB,MAAA,IAAU,IAAA;AAAA"}
|
package/dist/node/index.js
CHANGED
|
@@ -1,120 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { streamSSE } from "hono/streaming";
|
|
4
|
-
function createAgentRoutes(options) {
|
|
5
|
-
const { sessionFactory } = options;
|
|
6
|
-
const app = new Hono();
|
|
7
|
-
app.post("/submit", async (c) => {
|
|
8
|
-
const session = await sessionFactory(c);
|
|
9
|
-
const body = await c.req.json();
|
|
10
|
-
if (!body.prompt || typeof body.prompt !== "string") {
|
|
11
|
-
return c.json({ error: "prompt is required" }, 400);
|
|
12
|
-
}
|
|
13
|
-
return streamSSE(c, async (stream) => {
|
|
14
|
-
const cleanup = [];
|
|
15
|
-
const subscribe = (event, handler) => {
|
|
16
|
-
session.on(event, handler);
|
|
17
|
-
cleanup.push(() => session.off(event, handler));
|
|
18
|
-
};
|
|
19
|
-
let completed = false;
|
|
20
|
-
const done = new Promise((resolve) => {
|
|
21
|
-
subscribe("text_delta", (delta) => {
|
|
22
|
-
stream.writeSSE({ event: "text_delta", data: JSON.stringify({ delta }) });
|
|
23
|
-
});
|
|
24
|
-
subscribe("tool_start", (state) => {
|
|
25
|
-
stream.writeSSE({ event: "tool_start", data: JSON.stringify(state) });
|
|
26
|
-
});
|
|
27
|
-
subscribe("tool_end", (state) => {
|
|
28
|
-
stream.writeSSE({ event: "tool_end", data: JSON.stringify(state) });
|
|
29
|
-
});
|
|
30
|
-
subscribe("thinking", (isThinking) => {
|
|
31
|
-
stream.writeSSE({ event: "thinking", data: JSON.stringify({ isThinking }) });
|
|
32
|
-
if (!isThinking && completed) {
|
|
33
|
-
resolve();
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
subscribe("complete", (result) => {
|
|
37
|
-
completed = true;
|
|
38
|
-
stream.writeSSE({ event: "complete", data: JSON.stringify(result) });
|
|
39
|
-
});
|
|
40
|
-
subscribe("interrupted", (result) => {
|
|
41
|
-
completed = true;
|
|
42
|
-
stream.writeSSE({ event: "interrupted", data: JSON.stringify(result) });
|
|
43
|
-
});
|
|
44
|
-
subscribe("error", (error) => {
|
|
45
|
-
completed = true;
|
|
46
|
-
stream.writeSSE({ event: "error", data: JSON.stringify({ message: error.message }) });
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
await session.submit(body.prompt);
|
|
50
|
-
await done;
|
|
51
|
-
for (const fn of cleanup) fn();
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
app.post("/command", async (c) => {
|
|
55
|
-
const session = await sessionFactory(c);
|
|
56
|
-
const body = await c.req.json();
|
|
57
|
-
if (!body.name || typeof body.name !== "string") {
|
|
58
|
-
return c.json({ error: "name is required" }, 400);
|
|
59
|
-
}
|
|
60
|
-
const result = await session.executeCommand(body.name, body.args ?? "");
|
|
61
|
-
if (!result) {
|
|
62
|
-
return c.json({ error: `Unknown command: ${body.name}` }, 404);
|
|
63
|
-
}
|
|
64
|
-
return c.json(result);
|
|
65
|
-
});
|
|
66
|
-
app.post("/abort", async (c) => {
|
|
67
|
-
const session = await sessionFactory(c);
|
|
68
|
-
session.abort();
|
|
69
|
-
return c.json({ ok: true });
|
|
70
|
-
});
|
|
71
|
-
app.post("/cancel-queue", async (c) => {
|
|
72
|
-
const session = await sessionFactory(c);
|
|
73
|
-
session.cancelQueue();
|
|
74
|
-
return c.json({ ok: true });
|
|
75
|
-
});
|
|
76
|
-
app.get("/messages", async (c) => {
|
|
77
|
-
const session = await sessionFactory(c);
|
|
78
|
-
return c.json(session.getMessages());
|
|
79
|
-
});
|
|
80
|
-
app.get("/context", async (c) => {
|
|
81
|
-
const session = await sessionFactory(c);
|
|
82
|
-
return c.json(session.getContextState());
|
|
83
|
-
});
|
|
84
|
-
app.get("/executing", async (c) => {
|
|
85
|
-
const session = await sessionFactory(c);
|
|
86
|
-
return c.json({ executing: session.isExecuting() });
|
|
87
|
-
});
|
|
88
|
-
app.get("/pending", async (c) => {
|
|
89
|
-
const session = await sessionFactory(c);
|
|
90
|
-
return c.json({ pending: session.getPendingPrompt() });
|
|
91
|
-
});
|
|
92
|
-
return app;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// src/http-transport.ts
|
|
96
|
-
function createHttpTransport(options) {
|
|
97
|
-
let session = null;
|
|
98
|
-
let app = null;
|
|
99
|
-
return {
|
|
100
|
-
name: "http",
|
|
101
|
-
attach(s) {
|
|
102
|
-
session = s;
|
|
103
|
-
},
|
|
104
|
-
async start() {
|
|
105
|
-
if (!session) throw new Error("No session attached. Call attach() first.");
|
|
106
|
-
app = createAgentRoutes({ sessionFactory: () => session });
|
|
107
|
-
},
|
|
108
|
-
async stop() {
|
|
109
|
-
app = null;
|
|
110
|
-
},
|
|
111
|
-
getApp() {
|
|
112
|
-
if (!app) throw new Error("Transport not started. Call start() first.");
|
|
113
|
-
return app;
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
export {
|
|
118
|
-
createAgentRoutes,
|
|
119
|
-
createHttpTransport
|
|
120
|
-
};
|
|
1
|
+
import{Hono as e}from"hono";import{streamSSE as t}from"hono/streaming";function n(n){let{sessionFactory:r}=n,i=new e;return i.post(`/submit`,async e=>{let n=await r(e),i=await e.req.json();return!i.prompt||typeof i.prompt!=`string`?e.json({error:`prompt is required`},400):t(e,async e=>{let t=[],r=(e,r)=>{n.on(e,r),t.push(()=>n.off(e,r))},a=!1,o=new Promise(t=>{r(`text_delta`,t=>{e.writeSSE({event:`text_delta`,data:JSON.stringify({delta:t})})}),r(`tool_start`,t=>{e.writeSSE({event:`tool_start`,data:JSON.stringify(t)})}),r(`tool_end`,t=>{e.writeSSE({event:`tool_end`,data:JSON.stringify(t)})}),r(`thinking`,n=>{e.writeSSE({event:`thinking`,data:JSON.stringify({isThinking:n})}),!n&&a&&t()}),r(`complete`,t=>{a=!0,e.writeSSE({event:`complete`,data:JSON.stringify(t)})}),r(`interrupted`,t=>{a=!0,e.writeSSE({event:`interrupted`,data:JSON.stringify(t)})}),r(`error`,t=>{a=!0,e.writeSSE({event:`error`,data:JSON.stringify({message:t.message})})})});await n.submit(i.prompt),await o;for(let e of t)e()})}),i.post(`/command`,async e=>{let t=await r(e),n=await e.req.json();if(!n.name||typeof n.name!=`string`)return e.json({error:`name is required`},400);let i=await t.executeCommand(n.name,n.args??``);return i?e.json(i):e.json({error:`Unknown command: ${n.name}`},404)}),i.post(`/abort`,async e=>((await r(e)).abort(),e.json({ok:!0}))),i.post(`/cancel-queue`,async e=>((await r(e)).cancelQueue(),e.json({ok:!0}))),i.get(`/messages`,async e=>{let t=await r(e);return e.json(t.getMessages())}),i.get(`/context`,async e=>{let t=await r(e);return e.json(t.getContextState())}),i.get(`/executing`,async e=>{let t=await r(e);return e.json({executing:t.isExecuting()})}),i.get(`/pending`,async e=>{let t=await r(e);return e.json({pending:t.getPendingPrompt()})}),i}function r(e){let t=null,r=null;return{name:`http`,attach(e){t=e},async start(){if(!t)throw Error(`No session attached. Call attach() first.`);r=n({sessionFactory:()=>t})},async stop(){r=null},getApp(){if(!r)throw Error(`Transport not started. Call start() first.`);return r}}}export{n as createAgentRoutes,r as createHttpTransport};
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/routes.ts","../../src/http-transport.ts"],"sourcesContent":["/**\n * HTTP transport adapter — exposes IInteractiveSession over REST API.\n *\n * Built on Hono for Cloudflare Workers + Node.js + AWS Lambda compatibility.\n * Each endpoint maps 1:1 to an IInteractiveSession API method.\n */\n\nimport { Hono } from 'hono';\nimport { streamSSE } from 'hono/streaming';\n\nimport type { IInteractiveSession } from '@robota-sdk/agent-interface-transport';\nimport type { Context } from 'hono';\n\n/** Callback that resolves an IInteractiveSession from the request context. */\nexport type TSessionFactory = (c: Context) => IInteractiveSession | Promise<IInteractiveSession>;\n\nexport interface IAgentRoutesOptions {\n /** Resolve an IInteractiveSession per request (e.g., by auth token, session ID). */\n sessionFactory: TSessionFactory;\n}\n\n/**\n * Create a Hono router with all agent HTTP endpoints.\n *\n * Usage:\n * ```typescript\n * const routes = createAgentRoutes({ sessionFactory });\n * app.route('/agent', routes); // mount on existing app\n * export default routes; // or use standalone (CF Workers)\n * ```\n */\nexport function createAgentRoutes(options: IAgentRoutesOptions): Hono {\n const { sessionFactory } = options;\n const app = new Hono();\n\n // POST /submit — execute prompt, stream events via SSE\n app.post('/submit', async (c) => {\n const session = await sessionFactory(c);\n const body = await c.req.json<{ prompt: string }>();\n\n if (!body.prompt || typeof body.prompt !== 'string') {\n return c.json({ error: 'prompt is required' }, 400);\n }\n\n return streamSSE(c, async (stream) => {\n const cleanup: Array<() => void> = [];\n\n const subscribe = <T>(event: string, handler: (data: T) => void): void => {\n session.on(event as 'text_delta', handler as () => void);\n cleanup.push(() => session.off(event as 'text_delta', handler as () => void));\n };\n\n let completed = false;\n const done = new Promise<void>((resolve) => {\n subscribe('text_delta', (delta: string) => {\n stream.writeSSE({ event: 'text_delta', data: JSON.stringify({ delta }) });\n });\n\n subscribe('tool_start', (state) => {\n stream.writeSSE({ event: 'tool_start', data: JSON.stringify(state) });\n });\n\n subscribe('tool_end', (state) => {\n stream.writeSSE({ event: 'tool_end', data: JSON.stringify(state) });\n });\n\n subscribe('thinking', (isThinking: boolean) => {\n stream.writeSSE({ event: 'thinking', data: JSON.stringify({ isThinking }) });\n if (!isThinking && completed) {\n resolve();\n }\n });\n\n subscribe('complete', (result) => {\n completed = true;\n stream.writeSSE({ event: 'complete', data: JSON.stringify(result) });\n });\n\n subscribe('interrupted', (result) => {\n completed = true;\n stream.writeSSE({ event: 'interrupted', data: JSON.stringify(result) });\n });\n\n subscribe('error', (error: Error) => {\n completed = true;\n stream.writeSSE({ event: 'error', data: JSON.stringify({ message: error.message }) });\n });\n });\n\n await session.submit(body.prompt);\n await done;\n\n for (const fn of cleanup) fn();\n });\n });\n\n // POST /command — execute system command\n app.post('/command', async (c) => {\n const session = await sessionFactory(c);\n const body = await c.req.json<{ name: string; args?: string }>();\n\n if (!body.name || typeof body.name !== 'string') {\n return c.json({ error: 'name is required' }, 400);\n }\n\n const result = await session.executeCommand(body.name, body.args ?? '');\n if (!result) {\n return c.json({ error: `Unknown command: ${body.name}` }, 404);\n }\n return c.json(result);\n });\n\n // POST /abort — abort current execution\n app.post('/abort', async (c) => {\n const session = await sessionFactory(c);\n session.abort();\n return c.json({ ok: true });\n });\n\n // POST /cancel-queue — cancel queued prompt\n app.post('/cancel-queue', async (c) => {\n const session = await sessionFactory(c);\n session.cancelQueue();\n return c.json({ ok: true });\n });\n\n // GET /messages — get message history\n app.get('/messages', async (c) => {\n const session = await sessionFactory(c);\n return c.json(session.getMessages());\n });\n\n // GET /context — get context window state\n app.get('/context', async (c) => {\n const session = await sessionFactory(c);\n return c.json(session.getContextState());\n });\n\n // GET /executing — check if currently executing\n app.get('/executing', async (c) => {\n const session = await sessionFactory(c);\n return c.json({ executing: session.isExecuting() });\n });\n\n // GET /pending — get pending queued prompt\n app.get('/pending', async (c) => {\n const session = await sessionFactory(c);\n return c.json({ pending: session.getPendingPrompt() });\n });\n\n return app;\n}\n","/**\n * ITransportAdapter implementation for HTTP transport.\n *\n * Wraps createAgentRoutes into the unified ITransportAdapter interface\n * while exposing the underlying Hono app via getApp().\n */\n\nimport { createAgentRoutes } from './routes.js';\n\nimport type { IInteractiveSession, ITransportAdapter } from '@robota-sdk/agent-interface-transport';\nimport type { Hono } from 'hono';\n\nexport interface IHttpTransportOptions {\n /** Optional: base path prefix for routes. */\n basePath?: string;\n}\n\nexport function createHttpTransport(\n options?: IHttpTransportOptions,\n): ITransportAdapter<IInteractiveSession> & { getApp(): Hono } {\n let session: IInteractiveSession | null = null;\n let app: Hono | null = null;\n\n return {\n name: 'http',\n attach(s: IInteractiveSession) {\n session = s;\n },\n async start() {\n if (!session) throw new Error('No session attached. Call attach() first.');\n app = createAgentRoutes({ sessionFactory: () => session! });\n },\n async stop() {\n app = null;\n },\n getApp() {\n if (!app) throw new Error('Transport not started. Call start() first.');\n return app;\n },\n };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@robota-sdk/agent-transport-http",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
4
|
-
"description": "HTTP transport
|
|
3
|
+
"version": "3.0.0-beta.77",
|
|
4
|
+
"description": "HTTP (Hono) transport for the Robota SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/node/index.js",
|
|
7
7
|
"types": "dist/node/index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
10
|
"types": "./dist/node/index.d.ts",
|
|
11
|
+
"source": "./src/index.ts",
|
|
11
12
|
"node": {
|
|
12
13
|
"import": "./dist/node/index.js",
|
|
13
14
|
"require": "./dist/node/index.cjs"
|
|
@@ -26,31 +27,34 @@
|
|
|
26
27
|
"bugs": {
|
|
27
28
|
"url": "https://github.com/woojubb/robota/issues"
|
|
28
29
|
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=22.0.0"
|
|
32
|
+
},
|
|
29
33
|
"files": [
|
|
30
|
-
"dist"
|
|
34
|
+
"dist",
|
|
35
|
+
"src"
|
|
31
36
|
],
|
|
32
37
|
"dependencies": {
|
|
33
|
-
"hono": "^4.
|
|
34
|
-
"@robota-sdk/agent-
|
|
38
|
+
"hono": "^4.12.25",
|
|
39
|
+
"@robota-sdk/agent-interface-transport": "3.0.0-beta.77"
|
|
35
40
|
},
|
|
36
41
|
"devDependencies": {
|
|
37
|
-
"rimraf": "^5.0.
|
|
38
|
-
"
|
|
39
|
-
"typescript": "^5.
|
|
40
|
-
"vitest": "^
|
|
42
|
+
"rimraf": "^5.0.10",
|
|
43
|
+
"tsdown": "^0.22.2",
|
|
44
|
+
"typescript": "^5.9.3",
|
|
45
|
+
"vitest": "^3.2.6"
|
|
41
46
|
},
|
|
42
|
-
"license": "
|
|
47
|
+
"license": "AGPL-3.0-only OR LicenseRef-Commercial",
|
|
43
48
|
"publishConfig": {
|
|
44
49
|
"access": "public"
|
|
45
50
|
},
|
|
46
51
|
"scripts": {
|
|
47
|
-
"build": "
|
|
48
|
-
"build:js": "
|
|
49
|
-
"build:types": "
|
|
52
|
+
"build": "tsdown",
|
|
53
|
+
"build:js": "tsdown --no-dts",
|
|
54
|
+
"build:types": "tsdown --dts",
|
|
55
|
+
"typecheck": "tsc --noEmit",
|
|
50
56
|
"test": "vitest run --passWithNoTests",
|
|
51
57
|
"test:coverage": "vitest run --coverage --passWithNoTests",
|
|
52
|
-
"typecheck": "tsc --noEmit",
|
|
53
|
-
"lint": "eslint src/ --ext .ts",
|
|
54
58
|
"clean": "rimraf dist"
|
|
55
59
|
}
|
|
56
60
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { createHttpTransport } from '../http-transport.js';
|
|
3
|
+
import type { IInteractiveSession } from '@robota-sdk/agent-interface-transport';
|
|
4
|
+
|
|
5
|
+
function createMockSession(): IInteractiveSession {
|
|
6
|
+
return {
|
|
7
|
+
submit: vi.fn(),
|
|
8
|
+
abort: vi.fn(),
|
|
9
|
+
cancelQueue: vi.fn(),
|
|
10
|
+
getMessages: vi.fn().mockReturnValue([]),
|
|
11
|
+
getContextState: vi
|
|
12
|
+
.fn()
|
|
13
|
+
.mockReturnValue({ usedPercentage: 0, usedTokens: 0, maxTokens: 200000 }),
|
|
14
|
+
isExecuting: vi.fn().mockReturnValue(false),
|
|
15
|
+
getPendingPrompt: vi.fn().mockReturnValue(null),
|
|
16
|
+
executeCommand: vi.fn().mockResolvedValue({ message: 'ok', success: true }),
|
|
17
|
+
listCommands: vi.fn().mockReturnValue([]),
|
|
18
|
+
on: vi.fn(),
|
|
19
|
+
off: vi.fn(),
|
|
20
|
+
} as unknown as IInteractiveSession;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('createHttpTransport', () => {
|
|
24
|
+
it('returns an adapter with name "http"', () => {
|
|
25
|
+
const transport = createHttpTransport();
|
|
26
|
+
expect(transport.name).toBe('http');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('throws if start() is called without attach()', async () => {
|
|
30
|
+
const transport = createHttpTransport();
|
|
31
|
+
await expect(transport.start()).rejects.toThrow('No session attached');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('throws if getApp() is called before start()', () => {
|
|
35
|
+
const transport = createHttpTransport();
|
|
36
|
+
expect(() => transport.getApp()).toThrow('Transport not started');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('creates a Hono app after attach + start', async () => {
|
|
40
|
+
const transport = createHttpTransport();
|
|
41
|
+
transport.attach(createMockSession() as never);
|
|
42
|
+
await transport.start();
|
|
43
|
+
const app = transport.getApp();
|
|
44
|
+
expect(app).toBeDefined();
|
|
45
|
+
expect(typeof app.fetch).toBe('function');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('nullifies app after stop()', async () => {
|
|
49
|
+
const transport = createHttpTransport();
|
|
50
|
+
transport.attach(createMockSession() as never);
|
|
51
|
+
await transport.start();
|
|
52
|
+
await transport.stop();
|
|
53
|
+
expect(() => transport.getApp()).toThrow('Transport not started');
|
|
54
|
+
});
|
|
55
|
+
});
|