@agentapplicationprotocol/sdk 0.1.0 → 0.2.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/README.md +5 -2
- package/dist/client.d.ts +9 -8
- package/dist/client.js +41 -28
- package/dist/client.test.d.ts +1 -0
- package/dist/client.test.js +183 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +4 -2
- package/dist/server.d.ts +13 -10
- package/dist/server.js +14 -20
- package/dist/server.test.d.ts +1 -0
- package/dist/server.test.js +124 -0
- package/dist/types.d.ts +97 -26
- package/dist/utils.d.ts +14 -0
- package/dist/utils.js +84 -0
- package/dist/utils.test.d.ts +1 -0
- package/dist/utils.test.js +129 -0
- package/package.json +19 -20
package/README.md
CHANGED
|
@@ -11,6 +11,9 @@ TypeScript SDK for the [Agent Application Protocol (AAP)](https://github.com/age
|
|
|
11
11
|
npm install @agentapplicationprotocol/sdk
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
##
|
|
14
|
+
## Examples
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
- [Web playground](https://agentapplicationprotocol.github.io/playground/)
|
|
17
|
+
- [Example agents](https://github.com/agentapplicationprotocol/agents)
|
|
18
|
+
|
|
19
|
+
## [CHANGELOG](./CHANGELOG.md)
|
package/dist/client.d.ts
CHANGED
|
@@ -15,8 +15,17 @@ export declare class Client {
|
|
|
15
15
|
constructor({ baseUrl, apiKey }: ClientOptions);
|
|
16
16
|
private url;
|
|
17
17
|
private request;
|
|
18
|
+
private streamRequest;
|
|
18
19
|
/** GET /meta */
|
|
19
20
|
getMeta(): Promise<MetaResponse>;
|
|
21
|
+
/** GET /sessions */
|
|
22
|
+
listSessions(params?: {
|
|
23
|
+
after?: string;
|
|
24
|
+
}): Promise<SessionListResponse>;
|
|
25
|
+
/** Fetches all session IDs across all pages. */
|
|
26
|
+
listAllSessions(): Promise<string[]>;
|
|
27
|
+
/** GET /session/:id */
|
|
28
|
+
getSession(sessionId: string): Promise<SessionResponse>;
|
|
20
29
|
/** PUT /session — non-streaming */
|
|
21
30
|
createSession(req: CreateSessionRequest & {
|
|
22
31
|
stream?: "none";
|
|
@@ -33,14 +42,6 @@ export declare class Client {
|
|
|
33
42
|
sendTurn(sessionId: string, req: SessionTurnRequest & {
|
|
34
43
|
stream: "delta" | "message";
|
|
35
44
|
}): Promise<AsyncIterable<SSEEvent>>;
|
|
36
|
-
/** GET /session/:id */
|
|
37
|
-
getSession(sessionId: string): Promise<SessionResponse>;
|
|
38
|
-
/** GET /sessions */
|
|
39
|
-
listSessions(params?: {
|
|
40
|
-
limit?: number;
|
|
41
|
-
after?: string;
|
|
42
|
-
}): Promise<SessionListResponse>;
|
|
43
45
|
/** DELETE /session/:id */
|
|
44
46
|
deleteSession(sessionId: string): Promise<void>;
|
|
45
|
-
private streamRequest;
|
|
46
47
|
}
|
package/dist/client.js
CHANGED
|
@@ -15,7 +15,10 @@ exports.ClientError = ClientError;
|
|
|
15
15
|
class Client {
|
|
16
16
|
constructor({ baseUrl, apiKey }) {
|
|
17
17
|
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
18
|
-
this.headers = {
|
|
18
|
+
this.headers = {
|
|
19
|
+
"Content-Type": "application/json",
|
|
20
|
+
Authorization: `Bearer ${apiKey}`,
|
|
21
|
+
};
|
|
19
22
|
}
|
|
20
23
|
url(path) {
|
|
21
24
|
return `${this.baseUrl}${path}`;
|
|
@@ -34,11 +37,48 @@ class Client {
|
|
|
34
37
|
return undefined;
|
|
35
38
|
return res.json();
|
|
36
39
|
}
|
|
40
|
+
async streamRequest(method, path, body) {
|
|
41
|
+
const res = await fetch(this.url(path), {
|
|
42
|
+
method,
|
|
43
|
+
headers: { ...this.headers, Accept: "text/event-stream" },
|
|
44
|
+
body: JSON.stringify(body),
|
|
45
|
+
});
|
|
46
|
+
if (!res.ok || !res.body) {
|
|
47
|
+
const text = await res.text().catch(() => res.statusText);
|
|
48
|
+
throw new ClientError(method, path, res.status, text);
|
|
49
|
+
}
|
|
50
|
+
return parseSSE(res.body);
|
|
51
|
+
}
|
|
37
52
|
/** GET /meta */
|
|
38
53
|
getMeta() {
|
|
39
54
|
return this.request("GET", "/meta");
|
|
40
55
|
}
|
|
56
|
+
/** GET /sessions */
|
|
57
|
+
listSessions(params) {
|
|
58
|
+
let path = "/sessions";
|
|
59
|
+
if (params?.after) {
|
|
60
|
+
path += "?" + new URLSearchParams({ after: params.after }).toString();
|
|
61
|
+
}
|
|
62
|
+
return this.request("GET", path);
|
|
63
|
+
}
|
|
64
|
+
/** Fetches all session IDs across all pages. */
|
|
65
|
+
async listAllSessions() {
|
|
66
|
+
const sessions = [];
|
|
67
|
+
let after;
|
|
68
|
+
do {
|
|
69
|
+
const res = await this.listSessions({ after });
|
|
70
|
+
sessions.push(...res.sessions);
|
|
71
|
+
after = res.next;
|
|
72
|
+
} while (after);
|
|
73
|
+
return sessions;
|
|
74
|
+
}
|
|
75
|
+
/** GET /session/:id */
|
|
76
|
+
getSession(sessionId) {
|
|
77
|
+
return this.request("GET", `/session/${sessionId}`);
|
|
78
|
+
}
|
|
41
79
|
createSession(req) {
|
|
80
|
+
if (req.messages.at(-1)?.role !== "user")
|
|
81
|
+
throw new Error("Last message must be a user message");
|
|
42
82
|
if (req.stream === "delta" || req.stream === "message") {
|
|
43
83
|
return this.streamRequest("PUT", "/session", req);
|
|
44
84
|
}
|
|
@@ -50,37 +90,10 @@ class Client {
|
|
|
50
90
|
}
|
|
51
91
|
return this.request("POST", `/session/${sessionId}`, req);
|
|
52
92
|
}
|
|
53
|
-
/** GET /session/:id */
|
|
54
|
-
getSession(sessionId) {
|
|
55
|
-
return this.request("GET", `/session/${sessionId}`);
|
|
56
|
-
}
|
|
57
|
-
/** GET /sessions */
|
|
58
|
-
listSessions(params) {
|
|
59
|
-
let path = "/sessions";
|
|
60
|
-
if (params) {
|
|
61
|
-
const entries = Object.entries(params).filter((e) => e[1] !== undefined);
|
|
62
|
-
if (entries.length > 0) {
|
|
63
|
-
path += "?" + new URLSearchParams(entries.map(([k, v]) => [k, String(v)])).toString();
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return this.request("GET", path);
|
|
67
|
-
}
|
|
68
93
|
/** DELETE /session/:id */
|
|
69
94
|
deleteSession(sessionId) {
|
|
70
95
|
return this.request("DELETE", `/session/${sessionId}`);
|
|
71
96
|
}
|
|
72
|
-
async streamRequest(method, path, body) {
|
|
73
|
-
const res = await fetch(this.url(path), {
|
|
74
|
-
method,
|
|
75
|
-
headers: { ...this.headers, Accept: "text/event-stream" },
|
|
76
|
-
body: JSON.stringify(body),
|
|
77
|
-
});
|
|
78
|
-
if (!res.ok || !res.body) {
|
|
79
|
-
const text = await res.text().catch(() => res.statusText);
|
|
80
|
-
throw new ClientError(method, path, res.status, text);
|
|
81
|
-
}
|
|
82
|
-
return parseSSE(res.body);
|
|
83
|
-
}
|
|
84
97
|
}
|
|
85
98
|
exports.Client = Client;
|
|
86
99
|
async function* parseSSE(body) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const client_1 = require("./client");
|
|
5
|
+
const BASE_URL = "https://example.com";
|
|
6
|
+
const API_KEY = "test-key";
|
|
7
|
+
function mockFetch(body, status = 200) {
|
|
8
|
+
return vitest_1.vi.fn().mockResolvedValue({
|
|
9
|
+
ok: status >= 200 && status < 300,
|
|
10
|
+
status,
|
|
11
|
+
statusText: "OK",
|
|
12
|
+
json: () => Promise.resolve(body),
|
|
13
|
+
text: () => Promise.resolve(String(body)),
|
|
14
|
+
body: null,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function mockSSEFetch(events, status = 200) {
|
|
18
|
+
const chunks = events.map(({ event, ...data }) => new TextEncoder().encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`));
|
|
19
|
+
let i = 0;
|
|
20
|
+
const body = {
|
|
21
|
+
getReader: () => ({
|
|
22
|
+
read: async () => i < chunks.length ? { done: false, value: chunks[i++] } : { done: true, value: undefined },
|
|
23
|
+
releaseLock: vitest_1.vi.fn(),
|
|
24
|
+
}),
|
|
25
|
+
};
|
|
26
|
+
return vitest_1.vi
|
|
27
|
+
.fn()
|
|
28
|
+
.mockResolvedValue({
|
|
29
|
+
ok: status >= 200 && status < 300,
|
|
30
|
+
status,
|
|
31
|
+
body,
|
|
32
|
+
text: () => Promise.resolve(""),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
let client;
|
|
36
|
+
(0, vitest_1.beforeEach)(() => {
|
|
37
|
+
client = new client_1.Client({ baseUrl: BASE_URL, apiKey: API_KEY });
|
|
38
|
+
});
|
|
39
|
+
(0, vitest_1.describe)("Client", () => {
|
|
40
|
+
(0, vitest_1.it)("strips trailing slash from baseUrl", () => {
|
|
41
|
+
const c = new client_1.Client({ baseUrl: BASE_URL + "/", apiKey: API_KEY });
|
|
42
|
+
const fetch = mockFetch({});
|
|
43
|
+
vitest_1.vi.stubGlobal("fetch", fetch);
|
|
44
|
+
c.getMeta();
|
|
45
|
+
(0, vitest_1.expect)(fetch.mock.calls[0][0]).toBe(`${BASE_URL}/meta`);
|
|
46
|
+
});
|
|
47
|
+
(0, vitest_1.it)("sends Authorization header", async () => {
|
|
48
|
+
const fetch = mockFetch({ version: 1, agents: [] });
|
|
49
|
+
vitest_1.vi.stubGlobal("fetch", fetch);
|
|
50
|
+
await client.getMeta();
|
|
51
|
+
(0, vitest_1.expect)(fetch.mock.calls[0][1].headers["Authorization"]).toBe(`Bearer ${API_KEY}`);
|
|
52
|
+
});
|
|
53
|
+
(0, vitest_1.it)("getMeta: GET /meta", async () => {
|
|
54
|
+
const meta = { version: 1, agents: [] };
|
|
55
|
+
vitest_1.vi.stubGlobal("fetch", mockFetch(meta));
|
|
56
|
+
(0, vitest_1.expect)(await client.getMeta()).toEqual(meta);
|
|
57
|
+
});
|
|
58
|
+
(0, vitest_1.it)("getSession: GET /session/:id", async () => {
|
|
59
|
+
const session = { sessionId: "s1", agent: { name: "a" } };
|
|
60
|
+
vitest_1.vi.stubGlobal("fetch", mockFetch(session));
|
|
61
|
+
(0, vitest_1.expect)(await client.getSession("s1")).toEqual(session);
|
|
62
|
+
});
|
|
63
|
+
(0, vitest_1.it)("listSessions: GET /sessions without cursor", async () => {
|
|
64
|
+
const res = { sessions: ["s1"] };
|
|
65
|
+
const fetch = mockFetch(res);
|
|
66
|
+
vitest_1.vi.stubGlobal("fetch", fetch);
|
|
67
|
+
await client.listSessions();
|
|
68
|
+
(0, vitest_1.expect)(fetch.mock.calls[0][0]).toBe(`${BASE_URL}/sessions`);
|
|
69
|
+
});
|
|
70
|
+
(0, vitest_1.it)("listSessions: appends after param", async () => {
|
|
71
|
+
const fetch = mockFetch({ sessions: [] });
|
|
72
|
+
vitest_1.vi.stubGlobal("fetch", fetch);
|
|
73
|
+
await client.listSessions({ after: "cursor1" });
|
|
74
|
+
(0, vitest_1.expect)(fetch.mock.calls[0][0]).toBe(`${BASE_URL}/sessions?after=cursor1`);
|
|
75
|
+
});
|
|
76
|
+
(0, vitest_1.it)("listAllSessions: paginates until no next", async () => {
|
|
77
|
+
const fetch = vitest_1.vi
|
|
78
|
+
.fn()
|
|
79
|
+
.mockResolvedValueOnce({
|
|
80
|
+
ok: true,
|
|
81
|
+
status: 200,
|
|
82
|
+
json: () => Promise.resolve({ sessions: ["s1"], next: "c1" }),
|
|
83
|
+
})
|
|
84
|
+
.mockResolvedValueOnce({
|
|
85
|
+
ok: true,
|
|
86
|
+
status: 200,
|
|
87
|
+
json: () => Promise.resolve({ sessions: ["s2"] }),
|
|
88
|
+
});
|
|
89
|
+
vitest_1.vi.stubGlobal("fetch", fetch);
|
|
90
|
+
(0, vitest_1.expect)(await client.listAllSessions()).toEqual(["s1", "s2"]);
|
|
91
|
+
(0, vitest_1.expect)(fetch).toHaveBeenCalledTimes(2);
|
|
92
|
+
});
|
|
93
|
+
(0, vitest_1.it)("deleteSession: DELETE /session/:id returns void on 204", async () => {
|
|
94
|
+
vitest_1.vi.stubGlobal("fetch", vitest_1.vi.fn().mockResolvedValue({ ok: true, status: 204 }));
|
|
95
|
+
await (0, vitest_1.expect)(client.deleteSession("s1")).resolves.toBeUndefined();
|
|
96
|
+
});
|
|
97
|
+
(0, vitest_1.it)("createSession: throws if last message is not a user message", () => {
|
|
98
|
+
vitest_1.vi.stubGlobal("fetch", mockFetch({}));
|
|
99
|
+
(0, vitest_1.expect)(() => client.createSession({
|
|
100
|
+
agent: { name: "a" },
|
|
101
|
+
messages: [{ role: "assistant", content: "hi" }],
|
|
102
|
+
})).toThrow("Last message must be a user message");
|
|
103
|
+
});
|
|
104
|
+
(0, vitest_1.it)("createSession: non-streaming returns AgentResponse", async () => {
|
|
105
|
+
const res = { sessionId: "s1", stopReason: "end_turn", messages: [] };
|
|
106
|
+
vitest_1.vi.stubGlobal("fetch", mockFetch(res, 201));
|
|
107
|
+
const result = await client.createSession({
|
|
108
|
+
agent: { name: "a" },
|
|
109
|
+
messages: [{ role: "user", content: "hi" }],
|
|
110
|
+
});
|
|
111
|
+
(0, vitest_1.expect)(result).toEqual(res);
|
|
112
|
+
});
|
|
113
|
+
(0, vitest_1.it)("createSession: streaming returns SSE events", async () => {
|
|
114
|
+
const events = [
|
|
115
|
+
{ event: "session_start", sessionId: "s1" },
|
|
116
|
+
{ event: "turn_start" },
|
|
117
|
+
{ event: "turn_stop", stopReason: "end_turn" },
|
|
118
|
+
];
|
|
119
|
+
vitest_1.vi.stubGlobal("fetch", mockSSEFetch(events));
|
|
120
|
+
const stream = await client.createSession({
|
|
121
|
+
agent: { name: "a" },
|
|
122
|
+
messages: [{ role: "user", content: "hi" }],
|
|
123
|
+
stream: "message",
|
|
124
|
+
});
|
|
125
|
+
const received = [];
|
|
126
|
+
for await (const e of stream)
|
|
127
|
+
received.push(e);
|
|
128
|
+
(0, vitest_1.expect)(received).toEqual(events);
|
|
129
|
+
});
|
|
130
|
+
(0, vitest_1.it)("sendTurn: non-streaming returns AgentResponse", async () => {
|
|
131
|
+
const res = { stopReason: "end_turn", messages: [] };
|
|
132
|
+
vitest_1.vi.stubGlobal("fetch", mockFetch(res));
|
|
133
|
+
const result = await client.sendTurn("s1", { messages: [{ role: "user", content: "hi" }] });
|
|
134
|
+
(0, vitest_1.expect)(result).toEqual(res);
|
|
135
|
+
});
|
|
136
|
+
(0, vitest_1.it)("sendTurn: streaming returns SSE events", async () => {
|
|
137
|
+
const events = [
|
|
138
|
+
{ event: "turn_start" },
|
|
139
|
+
{ event: "text", text: "hello" },
|
|
140
|
+
{ event: "turn_stop", stopReason: "end_turn" },
|
|
141
|
+
];
|
|
142
|
+
vitest_1.vi.stubGlobal("fetch", mockSSEFetch(events));
|
|
143
|
+
const stream = await client.sendTurn("s1", {
|
|
144
|
+
messages: [{ role: "user", content: "hi" }],
|
|
145
|
+
stream: "message",
|
|
146
|
+
});
|
|
147
|
+
const received = [];
|
|
148
|
+
for await (const e of stream)
|
|
149
|
+
received.push(e);
|
|
150
|
+
(0, vitest_1.expect)(received).toEqual(events);
|
|
151
|
+
});
|
|
152
|
+
(0, vitest_1.it)("streamRequest: throws ClientError on non-ok response", async () => {
|
|
153
|
+
vitest_1.vi.stubGlobal("fetch", vitest_1.vi
|
|
154
|
+
.fn()
|
|
155
|
+
.mockResolvedValue({
|
|
156
|
+
ok: false,
|
|
157
|
+
status: 403,
|
|
158
|
+
body: null,
|
|
159
|
+
text: () => Promise.resolve("Forbidden"),
|
|
160
|
+
}));
|
|
161
|
+
await (0, vitest_1.expect)(client.createSession({
|
|
162
|
+
agent: { name: "a" },
|
|
163
|
+
messages: [{ role: "user", content: "hi" }],
|
|
164
|
+
stream: "delta",
|
|
165
|
+
})).rejects.toThrow(client_1.ClientError);
|
|
166
|
+
});
|
|
167
|
+
(0, vitest_1.it)("throws ClientError on non-ok response", async () => {
|
|
168
|
+
vitest_1.vi.stubGlobal("fetch", vitest_1.vi
|
|
169
|
+
.fn()
|
|
170
|
+
.mockResolvedValue({ ok: false, status: 401, text: () => Promise.resolve("Unauthorized") }));
|
|
171
|
+
await (0, vitest_1.expect)(client.getMeta()).rejects.toThrow(client_1.ClientError);
|
|
172
|
+
});
|
|
173
|
+
(0, vitest_1.it)("ClientError has correct properties", async () => {
|
|
174
|
+
vitest_1.vi.stubGlobal("fetch", vitest_1.vi
|
|
175
|
+
.fn()
|
|
176
|
+
.mockResolvedValue({ ok: false, status: 404, text: () => Promise.resolve("Not Found") }));
|
|
177
|
+
const err = await client.getSession("x").catch((e) => e);
|
|
178
|
+
(0, vitest_1.expect)(err).toBeInstanceOf(client_1.ClientError);
|
|
179
|
+
(0, vitest_1.expect)(err.status).toBe(404);
|
|
180
|
+
(0, vitest_1.expect)(err.method).toBe("GET");
|
|
181
|
+
(0, vitest_1.expect)(err.path).toBe("/session/x");
|
|
182
|
+
});
|
|
183
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { Client, ClientError } from "./client";
|
|
2
2
|
export type { ClientOptions } from "./client";
|
|
3
|
-
export { Server
|
|
3
|
+
export { Server } from "./server";
|
|
4
4
|
export type { ServerHandler, ServerOptions } from "./server";
|
|
5
|
-
export
|
|
5
|
+
export { sseEventsToMessages, resolvePendingToolUse } from "./utils";
|
|
6
|
+
export type * from "./types";
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.resolvePendingToolUse = exports.sseEventsToMessages = exports.Server = exports.ClientError = exports.Client = void 0;
|
|
4
4
|
var client_1 = require("./client");
|
|
5
5
|
Object.defineProperty(exports, "Client", { enumerable: true, get: function () { return client_1.Client; } });
|
|
6
6
|
Object.defineProperty(exports, "ClientError", { enumerable: true, get: function () { return client_1.ClientError; } });
|
|
7
7
|
var server_1 = require("./server");
|
|
8
8
|
Object.defineProperty(exports, "Server", { enumerable: true, get: function () { return server_1.Server; } });
|
|
9
|
-
|
|
9
|
+
var utils_1 = require("./utils");
|
|
10
|
+
Object.defineProperty(exports, "sseEventsToMessages", { enumerable: true, get: function () { return utils_1.sseEventsToMessages; } });
|
|
11
|
+
Object.defineProperty(exports, "resolvePendingToolUse", { enumerable: true, get: function () { return utils_1.resolvePendingToolUse; } });
|
package/dist/server.d.ts
CHANGED
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
-
import type {
|
|
2
|
+
import type { Context } from "hono";
|
|
3
3
|
import { AgentResponse, CreateSessionRequest, MetaResponse, SessionListResponse, SessionResponse, SessionTurnRequest, SSEEvent } from "./types";
|
|
4
4
|
export interface ServerHandler {
|
|
5
5
|
getMeta(): Promise<MetaResponse>;
|
|
6
|
-
createSession(req: CreateSessionRequest): Promise<AgentResponse | AsyncIterable<SSEEvent>> | AsyncIterable<SSEEvent>;
|
|
7
|
-
sendTurn(sessionId: string, req: SessionTurnRequest): Promise<AgentResponse | AsyncIterable<SSEEvent>> | AsyncIterable<SSEEvent>;
|
|
8
|
-
getSession(sessionId: string): Promise<SessionResponse>;
|
|
9
6
|
listSessions(params: {
|
|
10
7
|
after?: string;
|
|
11
8
|
}): Promise<SessionListResponse>;
|
|
9
|
+
getSession(sessionId: string): Promise<SessionResponse>;
|
|
10
|
+
/** The last message in `req.messages` is guaranteed to be a user message. */
|
|
11
|
+
createSession(req: CreateSessionRequest): Promise<AgentResponse | AsyncIterable<SSEEvent>>;
|
|
12
|
+
sendTurn(sessionId: string, req: SessionTurnRequest): Promise<AgentResponse | AsyncIterable<SSEEvent>>;
|
|
12
13
|
deleteSession(sessionId: string): Promise<void>;
|
|
13
14
|
}
|
|
14
|
-
export type { SSEStreamingApi };
|
|
15
|
-
export declare function writeSSEEvents(stream: SSEStreamingApi, events: AsyncIterable<SSEEvent>): Promise<void>;
|
|
16
15
|
export interface ServerOptions {
|
|
17
|
-
/**
|
|
18
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Called on every request to authenticate. Return false to reject.
|
|
18
|
+
* Use `c.req.path` to allow unauthenticated access to specific routes.
|
|
19
|
+
* @example
|
|
20
|
+
* // Allow unauthenticated access to GET /meta
|
|
21
|
+
* authenticate: (apiKey, c) => c.req.path === "/meta" || apiKey === "secret"
|
|
22
|
+
*/
|
|
23
|
+
authenticate?: (apiKey: string, c: Context) => boolean | Promise<boolean>;
|
|
19
24
|
/** CORS origin(s) to allow. Disabled by default. */
|
|
20
25
|
cors?: string | string[];
|
|
21
26
|
/** Base path to mount all routes under, e.g. "/api/v1". */
|
|
@@ -24,6 +29,4 @@ export interface ServerOptions {
|
|
|
24
29
|
export declare class Server {
|
|
25
30
|
readonly app: Hono;
|
|
26
31
|
constructor(handler: ServerHandler, options?: ServerOptions);
|
|
27
|
-
/** Returns the Hono fetch handler, ready to pass to any runtime (Node, Bun, Deno, etc.) */
|
|
28
|
-
fetch: (req: Request) => Response | Promise<Response>;
|
|
29
32
|
}
|
package/dist/server.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Server = void 0;
|
|
4
|
-
exports.writeSSEEvents = writeSSEEvents;
|
|
5
4
|
const hono_1 = require("hono");
|
|
6
5
|
const cors_1 = require("hono/cors");
|
|
7
6
|
const streaming_1 = require("hono/streaming");
|
|
7
|
+
// --- SSE helper ---
|
|
8
8
|
async function writeSSEEvents(stream, events) {
|
|
9
9
|
for await (const { event, ...data } of events) {
|
|
10
10
|
await stream.writeSSE({ event, data: JSON.stringify(data) });
|
|
@@ -12,37 +12,34 @@ async function writeSSEEvents(stream, events) {
|
|
|
12
12
|
}
|
|
13
13
|
class Server {
|
|
14
14
|
constructor(handler, options = {}) {
|
|
15
|
-
/** Returns the Hono fetch handler, ready to pass to any runtime (Node, Bun, Deno, etc.) */
|
|
16
|
-
this.fetch = (req) => this.app.fetch(req);
|
|
17
15
|
this.app = new hono_1.Hono();
|
|
18
16
|
const { authenticate } = options;
|
|
19
17
|
const router = options.base ? this.app.basePath(options.base) : this.app;
|
|
20
18
|
if (options.cors !== undefined) {
|
|
21
19
|
router.use("*", (0, cors_1.cors)({ origin: options.cors }));
|
|
22
20
|
}
|
|
23
|
-
const auth = async (apiKey) => {
|
|
24
|
-
if (!authenticate)
|
|
25
|
-
return true;
|
|
26
|
-
return authenticate(apiKey);
|
|
27
|
-
};
|
|
28
21
|
const getApiKey = (authHeader) => authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : "";
|
|
29
|
-
//
|
|
30
|
-
router.get("/meta", async (c) => {
|
|
31
|
-
const meta = await handler.getMeta();
|
|
32
|
-
return c.json(meta);
|
|
33
|
-
});
|
|
34
|
-
// Auth middleware for all other routes
|
|
22
|
+
// Auth middleware for all routes
|
|
35
23
|
router.use("*", async (c, next) => {
|
|
24
|
+
if (!authenticate)
|
|
25
|
+
return next();
|
|
36
26
|
const apiKey = getApiKey(c.req.header("Authorization"));
|
|
37
|
-
if (!(await
|
|
27
|
+
if (!(await authenticate(apiKey, c)))
|
|
38
28
|
return c.json({ error: "Unauthorized" }, 401);
|
|
39
29
|
return next();
|
|
40
30
|
});
|
|
31
|
+
// GET /meta
|
|
32
|
+
router.get("/meta", async (c) => {
|
|
33
|
+
const meta = await handler.getMeta();
|
|
34
|
+
return c.json(meta);
|
|
35
|
+
});
|
|
41
36
|
// PUT /session
|
|
42
37
|
router.put("/session", async (c) => {
|
|
43
38
|
const req = await c.req.json();
|
|
39
|
+
if (req.messages.at(-1)?.role !== "user")
|
|
40
|
+
return c.json({ error: "Last message must be a user message" }, 400);
|
|
44
41
|
const result = await handler.createSession(req);
|
|
45
|
-
if (
|
|
42
|
+
if (req.stream === "delta" || req.stream === "message") {
|
|
46
43
|
return (0, streaming_1.streamSSE)(c, (stream) => writeSSEEvents(stream, result));
|
|
47
44
|
}
|
|
48
45
|
return c.json(result, 201);
|
|
@@ -51,7 +48,7 @@ class Server {
|
|
|
51
48
|
router.post("/session/:id", async (c) => {
|
|
52
49
|
const req = await c.req.json();
|
|
53
50
|
const result = await handler.sendTurn(c.req.param("id"), req);
|
|
54
|
-
if (
|
|
51
|
+
if (req.stream === "delta" || req.stream === "message") {
|
|
55
52
|
return (0, streaming_1.streamSSE)(c, (stream) => writeSSEEvents(stream, result));
|
|
56
53
|
}
|
|
57
54
|
return c.json(result);
|
|
@@ -75,6 +72,3 @@ class Server {
|
|
|
75
72
|
}
|
|
76
73
|
}
|
|
77
74
|
exports.Server = Server;
|
|
78
|
-
function isAsyncIterable(value) {
|
|
79
|
-
return value != null && typeof value[Symbol.asyncIterator] === "function";
|
|
80
|
-
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const server_1 = require("./server");
|
|
5
|
+
const meta = { version: 1, agents: [] };
|
|
6
|
+
const session = { sessionId: "s1", agent: { name: "a" } };
|
|
7
|
+
const agentResponse = { stopReason: "end_turn", messages: [] };
|
|
8
|
+
const sessionList = { sessions: ["s1"] };
|
|
9
|
+
async function* sseEvents() {
|
|
10
|
+
yield { event: "turn_start" };
|
|
11
|
+
yield { event: "text", text: "hi" };
|
|
12
|
+
yield { event: "turn_stop", stopReason: "end_turn" };
|
|
13
|
+
}
|
|
14
|
+
function makeHandler(overrides = {}) {
|
|
15
|
+
return {
|
|
16
|
+
getMeta: vitest_1.vi.fn().mockResolvedValue(meta),
|
|
17
|
+
listSessions: vitest_1.vi.fn().mockResolvedValue(sessionList),
|
|
18
|
+
getSession: vitest_1.vi.fn().mockResolvedValue(session),
|
|
19
|
+
createSession: vitest_1.vi.fn().mockResolvedValue(agentResponse),
|
|
20
|
+
sendTurn: vitest_1.vi.fn().mockResolvedValue(agentResponse),
|
|
21
|
+
deleteSession: vitest_1.vi.fn().mockResolvedValue(undefined),
|
|
22
|
+
...overrides,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function req(method, path, body, headers) {
|
|
26
|
+
return new Request(`http://localhost${path}`, {
|
|
27
|
+
method,
|
|
28
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
29
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
(0, vitest_1.describe)("Server", () => {
|
|
33
|
+
(0, vitest_1.it)("GET /meta returns meta", async () => {
|
|
34
|
+
const server = new server_1.Server(makeHandler());
|
|
35
|
+
const res = await server.app.fetch(req("GET", "/meta"));
|
|
36
|
+
(0, vitest_1.expect)(res.status).toBe(200);
|
|
37
|
+
(0, vitest_1.expect)(await res.json()).toEqual(meta);
|
|
38
|
+
});
|
|
39
|
+
(0, vitest_1.it)("GET /session/:id returns session", async () => {
|
|
40
|
+
const server = new server_1.Server(makeHandler());
|
|
41
|
+
const res = await server.app.fetch(req("GET", "/session/s1"));
|
|
42
|
+
(0, vitest_1.expect)(res.status).toBe(200);
|
|
43
|
+
(0, vitest_1.expect)(await res.json()).toEqual(session);
|
|
44
|
+
});
|
|
45
|
+
(0, vitest_1.it)("GET /sessions returns session list", async () => {
|
|
46
|
+
const handler = makeHandler();
|
|
47
|
+
const server = new server_1.Server(handler);
|
|
48
|
+
const res = await server.app.fetch(req("GET", "/sessions?after=cursor1"));
|
|
49
|
+
(0, vitest_1.expect)(res.status).toBe(200);
|
|
50
|
+
(0, vitest_1.expect)(await res.json()).toEqual(sessionList);
|
|
51
|
+
(0, vitest_1.expect)(handler.listSessions).toHaveBeenCalledWith({ after: "cursor1" });
|
|
52
|
+
});
|
|
53
|
+
(0, vitest_1.it)("PUT /session returns 400 if last message is not a user message", async () => {
|
|
54
|
+
const server = new server_1.Server(makeHandler());
|
|
55
|
+
const res = await server.app.fetch(req("PUT", "/session", {
|
|
56
|
+
agent: { name: "a" },
|
|
57
|
+
messages: [{ role: "assistant", content: "hi" }],
|
|
58
|
+
}));
|
|
59
|
+
(0, vitest_1.expect)(res.status).toBe(400);
|
|
60
|
+
});
|
|
61
|
+
(0, vitest_1.it)("PUT /session returns 201 with AgentResponse", async () => {
|
|
62
|
+
const server = new server_1.Server(makeHandler());
|
|
63
|
+
const res = await server.app.fetch(req("PUT", "/session", { agent: { name: "a" }, messages: [{ role: "user", content: "hi" }] }));
|
|
64
|
+
(0, vitest_1.expect)(res.status).toBe(201);
|
|
65
|
+
(0, vitest_1.expect)(await res.json()).toEqual(agentResponse);
|
|
66
|
+
});
|
|
67
|
+
(0, vitest_1.it)("PUT /session with stream returns SSE", async () => {
|
|
68
|
+
const server = new server_1.Server(makeHandler({ createSession: vitest_1.vi.fn().mockResolvedValue(sseEvents()) }));
|
|
69
|
+
const res = await server.app.fetch(req("PUT", "/session", {
|
|
70
|
+
agent: { name: "a" },
|
|
71
|
+
messages: [{ role: "user", content: "hi" }],
|
|
72
|
+
stream: "message",
|
|
73
|
+
}));
|
|
74
|
+
(0, vitest_1.expect)(res.status).toBe(200);
|
|
75
|
+
(0, vitest_1.expect)(res.headers.get("content-type")).toContain("text/event-stream");
|
|
76
|
+
});
|
|
77
|
+
(0, vitest_1.it)("POST /session/:id returns AgentResponse", async () => {
|
|
78
|
+
const server = new server_1.Server(makeHandler());
|
|
79
|
+
const res = await server.app.fetch(req("POST", "/session/s1", { messages: [{ role: "user", content: "hi" }] }));
|
|
80
|
+
(0, vitest_1.expect)(res.status).toBe(200);
|
|
81
|
+
(0, vitest_1.expect)(await res.json()).toEqual(agentResponse);
|
|
82
|
+
});
|
|
83
|
+
(0, vitest_1.it)("POST /session/:id with stream returns SSE", async () => {
|
|
84
|
+
const server = new server_1.Server(makeHandler({ sendTurn: vitest_1.vi.fn().mockResolvedValue(sseEvents()) }));
|
|
85
|
+
const res = await server.app.fetch(req("POST", "/session/s1", { messages: [{ role: "user", content: "hi" }], stream: "delta" }));
|
|
86
|
+
(0, vitest_1.expect)(res.status).toBe(200);
|
|
87
|
+
(0, vitest_1.expect)(res.headers.get("content-type")).toContain("text/event-stream");
|
|
88
|
+
});
|
|
89
|
+
(0, vitest_1.it)("DELETE /session/:id returns 204", async () => {
|
|
90
|
+
const server = new server_1.Server(makeHandler());
|
|
91
|
+
const res = await server.app.fetch(req("DELETE", "/session/s1"));
|
|
92
|
+
(0, vitest_1.expect)(res.status).toBe(204);
|
|
93
|
+
});
|
|
94
|
+
(0, vitest_1.it)("authenticate: returns 401 when rejected", async () => {
|
|
95
|
+
const server = new server_1.Server(makeHandler(), { authenticate: () => false });
|
|
96
|
+
const res = await server.app.fetch(req("GET", "/meta"));
|
|
97
|
+
(0, vitest_1.expect)(res.status).toBe(401);
|
|
98
|
+
});
|
|
99
|
+
(0, vitest_1.it)("authenticate: passes apiKey and context", async () => {
|
|
100
|
+
const authenticate = vitest_1.vi.fn().mockReturnValue(true);
|
|
101
|
+
const server = new server_1.Server(makeHandler(), { authenticate });
|
|
102
|
+
await server.app.fetch(req("GET", "/meta", undefined, { Authorization: "Bearer mykey" }));
|
|
103
|
+
(0, vitest_1.expect)(authenticate).toHaveBeenCalledWith("mykey", vitest_1.expect.objectContaining({ req: vitest_1.expect.anything() }));
|
|
104
|
+
});
|
|
105
|
+
(0, vitest_1.it)("authenticate: allows per-route logic", async () => {
|
|
106
|
+
const server = new server_1.Server(makeHandler(), {
|
|
107
|
+
authenticate: (_, c) => c.req.path === "/meta",
|
|
108
|
+
});
|
|
109
|
+
const metaRes = await server.app.fetch(req("GET", "/meta"));
|
|
110
|
+
(0, vitest_1.expect)(metaRes.status).toBe(200);
|
|
111
|
+
const sessRes = await server.app.fetch(req("GET", "/session/s1"));
|
|
112
|
+
(0, vitest_1.expect)(sessRes.status).toBe(401);
|
|
113
|
+
});
|
|
114
|
+
(0, vitest_1.it)("base path prefixes all routes", async () => {
|
|
115
|
+
const server = new server_1.Server(makeHandler(), { base: "/api/v1" });
|
|
116
|
+
const res = await server.app.fetch(req("GET", "/api/v1/meta"));
|
|
117
|
+
(0, vitest_1.expect)(res.status).toBe(200);
|
|
118
|
+
});
|
|
119
|
+
(0, vitest_1.it)("cors: sets Access-Control-Allow-Origin header", async () => {
|
|
120
|
+
const server = new server_1.Server(makeHandler(), { cors: "https://example.com" });
|
|
121
|
+
const res = await server.app.fetch(req("GET", "/meta", undefined, { Origin: "https://example.com" }));
|
|
122
|
+
(0, vitest_1.expect)(res.headers.get("access-control-allow-origin")).toBe("https://example.com");
|
|
123
|
+
});
|
|
124
|
+
});
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { JSONSchema7 } from "json-schema";
|
|
2
2
|
export type JSONSchema = JSONSchema7;
|
|
3
|
+
/** A single block of content within a message. */
|
|
3
4
|
export type ContentBlock = {
|
|
4
5
|
type: "text";
|
|
5
6
|
text: string;
|
|
@@ -13,122 +14,195 @@ export type ContentBlock = {
|
|
|
13
14
|
input: Record<string, unknown>;
|
|
14
15
|
} | {
|
|
15
16
|
type: "image";
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
/** Supports `https://` URLs and `data:` URIs (base64). */
|
|
18
|
+
url: string;
|
|
18
19
|
};
|
|
19
|
-
|
|
20
|
+
/** A system-role message providing instructions to the agent. */
|
|
21
|
+
export interface SystemMessage {
|
|
20
22
|
role: "system";
|
|
21
23
|
content: string;
|
|
22
|
-
}
|
|
24
|
+
}
|
|
25
|
+
/** A user-role message. */
|
|
26
|
+
export interface UserMessage {
|
|
23
27
|
role: "user";
|
|
24
28
|
content: string | ContentBlock[];
|
|
25
|
-
}
|
|
29
|
+
}
|
|
30
|
+
/** An assistant-role message. */
|
|
31
|
+
export interface AssistantMessage {
|
|
26
32
|
role: "assistant";
|
|
27
33
|
content: string | ContentBlock[];
|
|
28
|
-
}
|
|
34
|
+
}
|
|
35
|
+
/** A tool result message returned by the application after a `tool_use` block. */
|
|
36
|
+
export interface ToolMessage {
|
|
29
37
|
role: "tool";
|
|
30
38
|
toolCallId: string;
|
|
31
39
|
content: string | ContentBlock[];
|
|
32
|
-
}
|
|
33
|
-
|
|
40
|
+
}
|
|
41
|
+
/** A message that can appear in conversation history. */
|
|
42
|
+
export type HistoryMessage = SystemMessage | UserMessage | AssistantMessage | ToolMessage;
|
|
43
|
+
/** Grants or denies permission for the server to invoke a tool on the client's behalf. */
|
|
44
|
+
export interface ToolPermissionMessage {
|
|
34
45
|
role: "tool_permission";
|
|
35
46
|
toolCallId: string;
|
|
47
|
+
/** Whether the client grants permission for the tool call. */
|
|
36
48
|
granted: boolean;
|
|
49
|
+
/** Optional explanation, especially useful when `granted` is `false`. */
|
|
37
50
|
reason?: string;
|
|
38
|
-
}
|
|
51
|
+
}
|
|
52
|
+
/** Declares a tool (application-side in requests; server-side in `/meta`). */
|
|
39
53
|
export interface ToolSpec {
|
|
40
54
|
name: string;
|
|
41
55
|
title?: string;
|
|
42
56
|
description: string;
|
|
43
57
|
inputSchema: JSONSchema;
|
|
44
58
|
}
|
|
59
|
+
/** References a server-side tool to enable for a session. */
|
|
45
60
|
export interface ServerToolRef {
|
|
61
|
+
/** Server tool name as declared in `/meta`. */
|
|
46
62
|
name: string;
|
|
47
|
-
|
|
63
|
+
/** If `true`, the server may invoke this tool without requesting client permission. Defaults to `false`. */
|
|
64
|
+
trust?: boolean;
|
|
48
65
|
}
|
|
66
|
+
/** A configurable option the client may set per request. */
|
|
49
67
|
export type AgentOption = {
|
|
68
|
+
type: "text";
|
|
50
69
|
name: string;
|
|
51
70
|
title?: string;
|
|
52
71
|
description?: string;
|
|
53
|
-
type: "text";
|
|
54
72
|
default: string;
|
|
55
73
|
} | {
|
|
74
|
+
type: "secret";
|
|
56
75
|
name: string;
|
|
57
76
|
title?: string;
|
|
58
77
|
description?: string;
|
|
59
|
-
type: "secret";
|
|
60
78
|
default: string;
|
|
61
79
|
} | {
|
|
80
|
+
type: "select";
|
|
62
81
|
name: string;
|
|
63
82
|
title?: string;
|
|
64
83
|
description?: string;
|
|
65
|
-
type: "select";
|
|
66
84
|
options: string[];
|
|
67
85
|
default: string;
|
|
68
86
|
};
|
|
87
|
+
/** Describes an agent available on the server, as returned by `GET /meta`. */
|
|
69
88
|
export interface AgentInfo {
|
|
89
|
+
/** Unique identifier for the agent on this server. */
|
|
70
90
|
name: string;
|
|
91
|
+
/** Human-readable display name. */
|
|
71
92
|
title?: string;
|
|
93
|
+
/** Semantic version of the agent. */
|
|
72
94
|
version: string;
|
|
73
95
|
description?: string;
|
|
96
|
+
/** Server-side tools the agent exposes to the client for configuration. */
|
|
74
97
|
tools?: ToolSpec[];
|
|
98
|
+
/** Configurable options the client may set per request. */
|
|
75
99
|
options?: AgentOption[];
|
|
100
|
+
/** Declares what the agent supports. Missing fields should be treated as unsupported. */
|
|
76
101
|
capabilities?: {
|
|
102
|
+
/** Declares what history the agent can return in `GET /session/:id`. */
|
|
77
103
|
history?: {
|
|
104
|
+
/** Server can return compacted history. */
|
|
78
105
|
compacted?: Record<string, never>;
|
|
106
|
+
/** Server can return full uncompacted history. */
|
|
79
107
|
full?: Record<string, never>;
|
|
80
108
|
};
|
|
109
|
+
/** Declares which stream modes the agent supports. */
|
|
81
110
|
stream?: {
|
|
111
|
+
/** Agent supports `"delta"` streaming. */
|
|
82
112
|
delta?: Record<string, never>;
|
|
113
|
+
/** Agent supports `"message"` streaming. */
|
|
83
114
|
message?: Record<string, never>;
|
|
115
|
+
/** Agent supports non-streaming (`"none"`) responses. */
|
|
84
116
|
none?: Record<string, never>;
|
|
85
117
|
};
|
|
118
|
+
/** Declares what application-provided inputs the agent supports. */
|
|
86
119
|
application?: {
|
|
120
|
+
/** Agent accepts application-side tools in requests. */
|
|
87
121
|
tools?: Record<string, never>;
|
|
88
122
|
};
|
|
123
|
+
/** Declares what image input the agent supports. */
|
|
124
|
+
image?: {
|
|
125
|
+
/** Agent accepts `https://` image URLs. */
|
|
126
|
+
http?: Record<string, never>;
|
|
127
|
+
/** Agent accepts `data:` URI (base64) images. */
|
|
128
|
+
data?: Record<string, never>;
|
|
129
|
+
};
|
|
89
130
|
};
|
|
90
131
|
}
|
|
132
|
+
/** Response body for `GET /meta`. */
|
|
91
133
|
export interface MetaResponse {
|
|
134
|
+
/** AAP protocol version. */
|
|
92
135
|
version: number;
|
|
93
136
|
agents: AgentInfo[];
|
|
94
137
|
}
|
|
95
138
|
export type StreamMode = "delta" | "message" | "none";
|
|
96
139
|
export type StopReason = "end_turn" | "tool_use" | "max_tokens" | "refusal" | "error";
|
|
140
|
+
/** Agent configuration supplied with a request. */
|
|
97
141
|
export interface AgentConfig {
|
|
142
|
+
/** Agent name to invoke. */
|
|
98
143
|
name: string;
|
|
144
|
+
/** Server-side tools to enable. If omitted, all exposed agent tools are disabled. */
|
|
99
145
|
tools?: ServerToolRef[];
|
|
146
|
+
/** Key-value pairs matching the agent's declared options. */
|
|
100
147
|
options?: Record<string, string>;
|
|
101
148
|
}
|
|
149
|
+
/** Request body for `PUT /session`. */
|
|
102
150
|
export interface CreateSessionRequest {
|
|
151
|
+
/** Agent configuration. `name` is required at session creation. */
|
|
103
152
|
agent: AgentConfig;
|
|
153
|
+
/** Response mode. Defaults to `"none"`. */
|
|
104
154
|
stream?: StreamMode;
|
|
105
|
-
|
|
155
|
+
/** Seed history. The last message must be a `user` message. */
|
|
156
|
+
messages: HistoryMessage[];
|
|
157
|
+
/** Application-side tools with full schema. */
|
|
106
158
|
tools?: ToolSpec[];
|
|
107
159
|
}
|
|
160
|
+
/** Request body for `POST /session/:id`. */
|
|
108
161
|
export interface SessionTurnRequest {
|
|
162
|
+
/** Session-level agent overrides. Agent name cannot be changed. */
|
|
163
|
+
agent?: Omit<AgentConfig, "name">;
|
|
164
|
+
/** Response mode. Defaults to `"none"`. */
|
|
109
165
|
stream?: StreamMode;
|
|
110
|
-
|
|
166
|
+
/** A single user message, or a mixed list of tool results and tool permissions. */
|
|
167
|
+
messages: (UserMessage | ToolMessage | ToolPermissionMessage)[];
|
|
168
|
+
/** Application-side tools. Overrides tools declared at session creation. */
|
|
111
169
|
tools?: ToolSpec[];
|
|
112
|
-
agent?: Omit<AgentConfig, "name">;
|
|
113
170
|
}
|
|
171
|
+
/** JSON response body for non-streaming (`stream: "none"`) requests. */
|
|
114
172
|
export interface AgentResponse {
|
|
173
|
+
/** Present in `PUT /session` response only. */
|
|
115
174
|
sessionId?: string;
|
|
116
175
|
stopReason: StopReason;
|
|
117
|
-
messages:
|
|
176
|
+
messages: HistoryMessage[];
|
|
118
177
|
}
|
|
178
|
+
/** Response body for `GET /session/:id`. */
|
|
119
179
|
export interface SessionResponse {
|
|
120
180
|
sessionId: string;
|
|
181
|
+
/** Secret option values in `agent.options` are redacted (e.g. `"***"`). */
|
|
121
182
|
agent: AgentConfig;
|
|
122
|
-
tools
|
|
183
|
+
/** Application-side tools declared for this session. */
|
|
184
|
+
tools?: ToolSpec[];
|
|
123
185
|
history?: {
|
|
124
|
-
|
|
125
|
-
|
|
186
|
+
/** Omitted if the server chooses not to expose. */
|
|
187
|
+
compacted?: HistoryMessage[];
|
|
188
|
+
/** Omitted if the server chooses not to expose. */
|
|
189
|
+
full?: HistoryMessage[];
|
|
126
190
|
};
|
|
127
191
|
}
|
|
192
|
+
/** Response body for `GET /sessions`. */
|
|
128
193
|
export interface SessionListResponse {
|
|
194
|
+
/** Array of session IDs. */
|
|
129
195
|
sessions: string[];
|
|
130
|
-
|
|
196
|
+
/** Pagination cursor; absent when there are no more results. Pass as `after` to get the next page. */
|
|
197
|
+
next?: string;
|
|
198
|
+
}
|
|
199
|
+
/** A tool call emitted by the agent during a streaming turn. */
|
|
200
|
+
export interface ToolCallEvent {
|
|
201
|
+
toolCallId: string;
|
|
202
|
+
name: string;
|
|
203
|
+
input: Record<string, unknown>;
|
|
131
204
|
}
|
|
205
|
+
/** SSE event data for `stream: "delta"` and `stream: "message"` responses. */
|
|
132
206
|
export type SSEEvent = {
|
|
133
207
|
event: "session_start";
|
|
134
208
|
sessionId: string;
|
|
@@ -146,12 +220,9 @@ export type SSEEvent = {
|
|
|
146
220
|
} | {
|
|
147
221
|
event: "thinking";
|
|
148
222
|
thinking: string;
|
|
149
|
-
} | {
|
|
223
|
+
} | ({
|
|
150
224
|
event: "tool_call";
|
|
151
|
-
|
|
152
|
-
name: string;
|
|
153
|
-
input: Record<string, unknown>;
|
|
154
|
-
} | {
|
|
225
|
+
} & ToolCallEvent) | {
|
|
155
226
|
event: "tool_result";
|
|
156
227
|
toolCallId: string;
|
|
157
228
|
content: string | ContentBlock[];
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { HistoryMessage, SSEEvent, ToolCallEvent, ToolSpec } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Converts a list of SSE events into `HistoryMessage[]`.
|
|
4
|
+
* Handles delta accumulation, tool call/result pairing, and assistant message finalization.
|
|
5
|
+
*/
|
|
6
|
+
export declare function sseEventsToMessages(events: SSEEvent[]): HistoryMessage[];
|
|
7
|
+
/**
|
|
8
|
+
* Inspects the last assistant message in `messages` and classifies its unresolved `tool_use` blocks
|
|
9
|
+
* into client-side tools (matched against `clientTools`) and server-side tools (requiring permission).
|
|
10
|
+
*/
|
|
11
|
+
export declare function resolvePendingToolUse(messages: HistoryMessage[], clientTools?: ToolSpec[]): {
|
|
12
|
+
client: ToolCallEvent[];
|
|
13
|
+
server: ToolCallEvent[];
|
|
14
|
+
};
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sseEventsToMessages = sseEventsToMessages;
|
|
4
|
+
exports.resolvePendingToolUse = resolvePendingToolUse;
|
|
5
|
+
/**
|
|
6
|
+
* Converts a list of SSE events into `HistoryMessage[]`.
|
|
7
|
+
* Handles delta accumulation, tool call/result pairing, and assistant message finalization.
|
|
8
|
+
*/
|
|
9
|
+
function sseEventsToMessages(events) {
|
|
10
|
+
const history = [];
|
|
11
|
+
const blocks = [];
|
|
12
|
+
let textAcc = "";
|
|
13
|
+
let thinkingAcc = "";
|
|
14
|
+
for (const event of events) {
|
|
15
|
+
// flush delta accumulators when a non-delta event arrives
|
|
16
|
+
if (event.event !== "text_delta" && event.event !== "thinking_delta") {
|
|
17
|
+
if (textAcc) {
|
|
18
|
+
blocks.push({ type: "text", text: textAcc });
|
|
19
|
+
textAcc = "";
|
|
20
|
+
}
|
|
21
|
+
if (thinkingAcc) {
|
|
22
|
+
blocks.push({ type: "thinking", thinking: thinkingAcc });
|
|
23
|
+
thinkingAcc = "";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
switch (event.event) {
|
|
27
|
+
case "text_delta":
|
|
28
|
+
textAcc += event.delta;
|
|
29
|
+
break;
|
|
30
|
+
case "thinking_delta":
|
|
31
|
+
thinkingAcc += event.delta;
|
|
32
|
+
break;
|
|
33
|
+
case "text":
|
|
34
|
+
blocks.push({ type: "text", text: event.text });
|
|
35
|
+
break;
|
|
36
|
+
case "thinking":
|
|
37
|
+
blocks.push({ type: "thinking", thinking: event.thinking });
|
|
38
|
+
break;
|
|
39
|
+
case "tool_call":
|
|
40
|
+
// accumulate tool_use blocks into the current assistant message
|
|
41
|
+
blocks.push({
|
|
42
|
+
type: "tool_use",
|
|
43
|
+
toolCallId: event.toolCallId,
|
|
44
|
+
name: event.name,
|
|
45
|
+
input: event.input,
|
|
46
|
+
});
|
|
47
|
+
break;
|
|
48
|
+
case "tool_result":
|
|
49
|
+
// flush accumulated assistant blocks before appending the tool result
|
|
50
|
+
if (blocks.length > 0) {
|
|
51
|
+
history.push({ role: "assistant", content: [...blocks] });
|
|
52
|
+
blocks.length = 0;
|
|
53
|
+
}
|
|
54
|
+
history.push({ role: "tool", toolCallId: event.toolCallId, content: event.content });
|
|
55
|
+
break;
|
|
56
|
+
case "turn_stop":
|
|
57
|
+
// finalize the assistant message
|
|
58
|
+
if (blocks.length > 0)
|
|
59
|
+
history.push({ role: "assistant", content: blocks });
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return history;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Inspects the last assistant message in `messages` and classifies its unresolved `tool_use` blocks
|
|
67
|
+
* into client-side tools (matched against `clientTools`) and server-side tools (requiring permission).
|
|
68
|
+
*/
|
|
69
|
+
function resolvePendingToolUse(messages, clientTools) {
|
|
70
|
+
const last = [...messages].reverse().find((m) => m.role === "assistant");
|
|
71
|
+
if (!last || !Array.isArray(last.content))
|
|
72
|
+
return { client: [], server: [] };
|
|
73
|
+
const resolved = new Set(messages.filter((m) => m.role === "tool").map((m) => m.toolCallId));
|
|
74
|
+
const clientNames = new Set(clientTools?.map((t) => t.name) ?? []);
|
|
75
|
+
const client = [];
|
|
76
|
+
const server = [];
|
|
77
|
+
for (const block of last.content) {
|
|
78
|
+
if (block.type !== "tool_use" || resolved.has(block.toolCallId))
|
|
79
|
+
continue;
|
|
80
|
+
const { toolCallId, name, input } = block;
|
|
81
|
+
(clientNames.has(name) ? client : server).push({ toolCallId, name, input });
|
|
82
|
+
}
|
|
83
|
+
return { client, server };
|
|
84
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const utils_1 = require("./utils");
|
|
5
|
+
(0, vitest_1.describe)("sseEventsToMessages", () => {
|
|
6
|
+
(0, vitest_1.it)("converts text event to assistant message", () => {
|
|
7
|
+
const events = [
|
|
8
|
+
{ event: "turn_start" },
|
|
9
|
+
{ event: "text", text: "hello" },
|
|
10
|
+
{ event: "turn_stop", stopReason: "end_turn" },
|
|
11
|
+
];
|
|
12
|
+
(0, vitest_1.expect)((0, utils_1.sseEventsToMessages)(events)).toEqual([
|
|
13
|
+
{ role: "assistant", content: [{ type: "text", text: "hello" }] },
|
|
14
|
+
]);
|
|
15
|
+
});
|
|
16
|
+
(0, vitest_1.it)("accumulates text_delta into text block", () => {
|
|
17
|
+
const events = [
|
|
18
|
+
{ event: "text_delta", delta: "hel" },
|
|
19
|
+
{ event: "text_delta", delta: "lo" },
|
|
20
|
+
{ event: "turn_stop", stopReason: "end_turn" },
|
|
21
|
+
];
|
|
22
|
+
(0, vitest_1.expect)((0, utils_1.sseEventsToMessages)(events)).toEqual([
|
|
23
|
+
{ role: "assistant", content: [{ type: "text", text: "hello" }] },
|
|
24
|
+
]);
|
|
25
|
+
});
|
|
26
|
+
(0, vitest_1.it)("accumulates thinking_delta into thinking block", () => {
|
|
27
|
+
const events = [
|
|
28
|
+
{ event: "thinking_delta", delta: "thin" },
|
|
29
|
+
{ event: "thinking_delta", delta: "king" },
|
|
30
|
+
{ event: "turn_stop", stopReason: "end_turn" },
|
|
31
|
+
];
|
|
32
|
+
(0, vitest_1.expect)((0, utils_1.sseEventsToMessages)(events)).toEqual([
|
|
33
|
+
{ role: "assistant", content: [{ type: "thinking", thinking: "thinking" }] },
|
|
34
|
+
]);
|
|
35
|
+
});
|
|
36
|
+
(0, vitest_1.it)("converts thinking event to thinking block", () => {
|
|
37
|
+
const events = [
|
|
38
|
+
{ event: "thinking", thinking: "deep thought" },
|
|
39
|
+
{ event: "turn_stop", stopReason: "end_turn" },
|
|
40
|
+
];
|
|
41
|
+
(0, vitest_1.expect)((0, utils_1.sseEventsToMessages)(events)).toEqual([
|
|
42
|
+
{ role: "assistant", content: [{ type: "thinking", thinking: "deep thought" }] },
|
|
43
|
+
]);
|
|
44
|
+
});
|
|
45
|
+
(0, vitest_1.it)("flushes assistant message before tool_result", () => {
|
|
46
|
+
const events = [
|
|
47
|
+
{ event: "tool_call", toolCallId: "c1", name: "search", input: { q: "x" } },
|
|
48
|
+
{ event: "tool_result", toolCallId: "c1", content: "result" },
|
|
49
|
+
{ event: "turn_stop", stopReason: "end_turn" },
|
|
50
|
+
];
|
|
51
|
+
const messages = (0, utils_1.sseEventsToMessages)(events);
|
|
52
|
+
(0, vitest_1.expect)(messages).toContainEqual({
|
|
53
|
+
role: "assistant",
|
|
54
|
+
content: [{ type: "tool_use", toolCallId: "c1", name: "search", input: { q: "x" } }],
|
|
55
|
+
});
|
|
56
|
+
(0, vitest_1.expect)(messages).toContainEqual({ role: "tool", toolCallId: "c1", content: "result" });
|
|
57
|
+
});
|
|
58
|
+
(0, vitest_1.it)("flushes delta accumulators when non-delta event arrives", () => {
|
|
59
|
+
const events = [
|
|
60
|
+
{ event: "text_delta", delta: "hi" },
|
|
61
|
+
{ event: "tool_call", toolCallId: "c1", name: "fn", input: {} },
|
|
62
|
+
{ event: "turn_stop", stopReason: "end_turn" },
|
|
63
|
+
];
|
|
64
|
+
const messages = (0, utils_1.sseEventsToMessages)(events);
|
|
65
|
+
const assistant = messages.find((m) => m.role === "assistant");
|
|
66
|
+
(0, vitest_1.expect)(Array.isArray(assistant?.content) && assistant.content[0]).toEqual({
|
|
67
|
+
type: "text",
|
|
68
|
+
text: "hi",
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
(0, vitest_1.it)("returns empty array for events with no content", () => {
|
|
72
|
+
const events = [
|
|
73
|
+
{ event: "turn_start" },
|
|
74
|
+
{ event: "turn_stop", stopReason: "end_turn" },
|
|
75
|
+
];
|
|
76
|
+
(0, vitest_1.expect)((0, utils_1.sseEventsToMessages)(events)).toEqual([]);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
(0, vitest_1.describe)("resolvePendingToolUse", () => {
|
|
80
|
+
const clientTool = {
|
|
81
|
+
name: "client_tool",
|
|
82
|
+
description: "d",
|
|
83
|
+
inputSchema: { type: "object" },
|
|
84
|
+
};
|
|
85
|
+
(0, vitest_1.it)("classifies client and server tools", () => {
|
|
86
|
+
const messages = [
|
|
87
|
+
{
|
|
88
|
+
role: "assistant",
|
|
89
|
+
content: [
|
|
90
|
+
{ type: "tool_use", toolCallId: "c1", name: "client_tool", input: {} },
|
|
91
|
+
{ type: "tool_use", toolCallId: "s1", name: "server_tool", input: {} },
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
const { client, server } = (0, utils_1.resolvePendingToolUse)(messages, [clientTool]);
|
|
96
|
+
(0, vitest_1.expect)(client).toEqual([{ toolCallId: "c1", name: "client_tool", input: {} }]);
|
|
97
|
+
(0, vitest_1.expect)(server).toEqual([{ toolCallId: "s1", name: "server_tool", input: {} }]);
|
|
98
|
+
});
|
|
99
|
+
(0, vitest_1.it)("skips tool_use blocks already resolved by tool_result", () => {
|
|
100
|
+
const messages = [
|
|
101
|
+
{
|
|
102
|
+
role: "assistant",
|
|
103
|
+
content: [{ type: "tool_use", toolCallId: "c1", name: "client_tool", input: {} }],
|
|
104
|
+
},
|
|
105
|
+
{ role: "tool", toolCallId: "c1", content: "done" },
|
|
106
|
+
];
|
|
107
|
+
(0, vitest_1.expect)((0, utils_1.resolvePendingToolUse)(messages, [clientTool])).toEqual({ client: [], server: [] });
|
|
108
|
+
});
|
|
109
|
+
(0, vitest_1.it)("returns empty if last assistant message has string content", () => {
|
|
110
|
+
const messages = [{ role: "assistant", content: "plain" }];
|
|
111
|
+
(0, vitest_1.expect)((0, utils_1.resolvePendingToolUse)(messages)).toEqual({ client: [], server: [] });
|
|
112
|
+
});
|
|
113
|
+
(0, vitest_1.it)("returns empty if no assistant message in history", () => {
|
|
114
|
+
const messages = [{ role: "user", content: "hi" }];
|
|
115
|
+
(0, vitest_1.expect)((0, utils_1.resolvePendingToolUse)(messages)).toEqual({ client: [], server: [] });
|
|
116
|
+
});
|
|
117
|
+
(0, vitest_1.it)("finds last assistant message even if not the last message", () => {
|
|
118
|
+
const messages = [
|
|
119
|
+
{
|
|
120
|
+
role: "assistant",
|
|
121
|
+
content: [{ type: "tool_use", toolCallId: "c1", name: "client_tool", input: {} }],
|
|
122
|
+
},
|
|
123
|
+
{ role: "tool", toolCallId: "c1", content: "done" },
|
|
124
|
+
{ role: "user", content: "thanks" },
|
|
125
|
+
];
|
|
126
|
+
// last message is user, but last assistant has resolved tool — should return empty
|
|
127
|
+
(0, vitest_1.expect)((0, utils_1.resolvePendingToolUse)(messages, [clientTool])).toEqual({ client: [], server: [] });
|
|
128
|
+
});
|
|
129
|
+
});
|
package/package.json
CHANGED
|
@@ -1,36 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentapplicationprotocol/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "TypeScript SDK for the Agent Application Protocol (AAP)",
|
|
5
|
-
"
|
|
6
|
-
"types": "dist/index.d.ts",
|
|
5
|
+
"license": "Apache-2.0",
|
|
7
6
|
"files": [
|
|
8
7
|
"dist"
|
|
9
8
|
],
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"types": "dist/index.d.ts",
|
|
10
11
|
"scripts": {
|
|
11
12
|
"build": "tsc",
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"example:server-tool-permission:server": "tsx src/examples/04-server-tool-permission/server.ts",
|
|
20
|
-
"example:server-tool-permission:client": "tsx src/examples/04-server-tool-permission/client.ts",
|
|
21
|
-
"example:server-tool-inline:server": "tsx src/examples/05-server-tool-inline/server.ts",
|
|
22
|
-
"example:server-tool-inline:client": "tsx src/examples/05-server-tool-inline/client.ts"
|
|
13
|
+
"test": "vitest run --exclude 'dist/**'",
|
|
14
|
+
"prepare": "husky",
|
|
15
|
+
"dev": "tsc --watch"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"eventsource-parser": "^3.0.6",
|
|
19
|
+
"hono": "^4.12.8"
|
|
23
20
|
},
|
|
24
|
-
"license": "Apache-2.0",
|
|
25
21
|
"devDependencies": {
|
|
26
|
-
"@hono/node-server": "^1.19.11",
|
|
27
22
|
"@types/json-schema": "^7.0.15",
|
|
28
23
|
"@types/node": "^25.5.0",
|
|
24
|
+
"@vitest/coverage-v8": "^4.1.2",
|
|
25
|
+
"husky": "^9.1.7",
|
|
26
|
+
"lint-staged": "^16.4.0",
|
|
27
|
+
"oxfmt": "^0.42.0",
|
|
29
28
|
"tsx": "^4.21.0",
|
|
30
|
-
"typescript": "^5.0.0"
|
|
29
|
+
"typescript": "^5.0.0",
|
|
30
|
+
"vitest": "^4.1.2"
|
|
31
31
|
},
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"hono": "^4.12.8"
|
|
32
|
+
"lint-staged": {
|
|
33
|
+
"*.{ts,json,md}": "oxfmt"
|
|
35
34
|
}
|
|
36
35
|
}
|