@agentapplicationprotocol/server 0.4.1 → 0.5.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 ADDED
@@ -0,0 +1,34 @@
1
+ # @agentapplicationprotocol/server
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@agentapplicationprotocol/server)](https://www.npmjs.com/package/@agentapplicationprotocol/server)
4
+
5
+ AAP server for the [Agent Application Protocol (AAP)](https://github.com/agentapplicationprotocol/agent-application-protocol).
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @agentapplicationprotocol/server
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import { Hono } from "hono";
17
+ import { aap, Agent, Session } from "@agentapplicationprotocol/server";
18
+
19
+ const agent = new Agent("my-agent", { version: "1.0.0" });
20
+
21
+ const app = new Hono();
22
+ app.route(
23
+ "/",
24
+ aap({
25
+ getMeta: () => ({ version: 1, agents: [agent.info] }),
26
+ // ... implement Handler interface
27
+ }),
28
+ );
29
+ ```
30
+
31
+ ## Examples
32
+
33
+ - [Basic agent](./src/examples/basic)
34
+ - [Compact history agent](./src/examples/compact-history)
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const vitest_1 = require("vitest");
7
+ const agent_1 = require("./agent");
8
+ const zod_1 = __importDefault(require("zod"));
9
+ (0, vitest_1.describe)("Agent", () => {
10
+ (0, vitest_1.it)("initializes with name and defaults", () => {
11
+ const agent = new agent_1.Agent("my-agent");
12
+ (0, vitest_1.expect)(agent.info.name).toBe("my-agent");
13
+ (0, vitest_1.expect)(agent.info.version).toBe("1.0.0");
14
+ (0, vitest_1.expect)(agent.info.tools).toEqual([]);
15
+ (0, vitest_1.expect)(agent.tools.size).toBe(0);
16
+ });
17
+ (0, vitest_1.it)("accepts title, description, version options", () => {
18
+ const agent = new agent_1.Agent("a", { title: "T", description: "D", version: "2.0.0" });
19
+ (0, vitest_1.expect)(agent.info.title).toBe("T");
20
+ (0, vitest_1.expect)(agent.info.description).toBe("D");
21
+ (0, vitest_1.expect)(agent.info.version).toBe("2.0.0");
22
+ });
23
+ (0, vitest_1.it)("option() adds an option and returns this", () => {
24
+ const agent = new agent_1.Agent("a");
25
+ const result = agent.option({ type: "text", name: "model", default: "gpt-4" });
26
+ (0, vitest_1.expect)(result).toBe(agent);
27
+ (0, vitest_1.expect)(agent.info.options).toHaveLength(1);
28
+ (0, vitest_1.expect)(agent.info.options[0].name).toBe("model");
29
+ });
30
+ (0, vitest_1.it)("image() sets image capability and returns this", () => {
31
+ const agent = new agent_1.Agent("a");
32
+ const result = agent.image({ http: {} });
33
+ (0, vitest_1.expect)(result).toBe(agent);
34
+ (0, vitest_1.expect)(agent.info.capabilities?.image).toEqual({ http: {} });
35
+ });
36
+ (0, vitest_1.it)("history() sets history capability and returns this", () => {
37
+ const agent = new agent_1.Agent("a");
38
+ const result = agent.history({});
39
+ (0, vitest_1.expect)(result).toBe(agent);
40
+ (0, vitest_1.expect)(agent.info.capabilities?.history).toEqual({});
41
+ });
42
+ (0, vitest_1.it)("tool() registers tool spec and executor", async () => {
43
+ const agent = new agent_1.Agent("a");
44
+ agent.tool("add", {
45
+ description: "adds two numbers",
46
+ inputSchema: zod_1.default.object({ a: zod_1.default.number(), b: zod_1.default.number() }),
47
+ outputSchema: zod_1.default.number(),
48
+ }, async ({ a, b }) => a + b);
49
+ (0, vitest_1.expect)(agent.info.tools).toHaveLength(1);
50
+ (0, vitest_1.expect)(agent.info.tools[0].name).toBe("add");
51
+ (0, vitest_1.expect)(agent.tools.has("add")).toBe(true);
52
+ const result = await agent.tools.get("add")(JSON.stringify({ a: 2, b: 3 }));
53
+ (0, vitest_1.expect)(JSON.parse(result)).toBe(5);
54
+ });
55
+ (0, vitest_1.it)("tool() validates output with outputSchema", async () => {
56
+ const agent = new agent_1.Agent("a");
57
+ agent.tool("greet", { inputSchema: zod_1.default.object({ name: zod_1.default.string() }), outputSchema: zod_1.default.string() }, async ({ name }) => `hello ${name}`);
58
+ const result = await agent.tools.get("greet")(JSON.stringify({ name: "world" }));
59
+ (0, vitest_1.expect)(JSON.parse(result)).toBe("hello world");
60
+ });
61
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const node_server_1 = require("@hono/node-server");
4
+ const node_crypto_1 = require("node:crypto");
5
+ const zod_1 = require("zod");
6
+ const hono_1 = require("hono");
7
+ const cors_1 = require("hono/cors");
8
+ const server_js_1 = require("../../server.js");
9
+ const agent_js_1 = require("../../agent.js");
10
+ const model_js_1 = require("../../model.js");
11
+ const openai_1 = require("@ai-sdk/openai");
12
+ const session_js_1 = require("../../session.js");
13
+ /** In-memory session store. */
14
+ const sessions = new Map();
15
+ /** Agent definition with options, capabilities, and tools. */
16
+ const agent = new agent_js_1.Agent("basic-agent", {
17
+ version: "0.1.0",
18
+ description: "An AAP-compatible agent powered by Vercel AI SDK.",
19
+ })
20
+ .image({ http: {}, data: {} })
21
+ .history({ compacted: {}, full: {} })
22
+ .option({
23
+ name: "baseURL",
24
+ title: "LLM Base URL",
25
+ description: "OpenAI-compatible base URL",
26
+ type: "text",
27
+ default: "",
28
+ })
29
+ .option({
30
+ name: "apiKey",
31
+ title: "LLM API Key",
32
+ description: "OpenAI API key",
33
+ type: "secret",
34
+ default: "",
35
+ })
36
+ .option({
37
+ name: "model",
38
+ title: "Model",
39
+ description: "Model ID to use",
40
+ type: "text",
41
+ default: "gpt-4o",
42
+ })
43
+ .tool("web_fetch", {
44
+ description: "Fetch the text content of a URL",
45
+ inputSchema: zod_1.z.object({ url: zod_1.z.string().describe("URL to fetch") }),
46
+ }, async ({ url }) => {
47
+ const res = await fetch(url);
48
+ const text = await res.text();
49
+ return text
50
+ .replace(/<[^>]+>/g, " ")
51
+ .replace(/\s+/g, " ")
52
+ .trim()
53
+ .slice(0, 8000);
54
+ });
55
+ const handler = {
56
+ getMeta() {
57
+ return { version: 1, agents: [agent.info] };
58
+ },
59
+ createSession(req) {
60
+ const sessionId = `sess_${(0, node_crypto_1.randomUUID)()}`;
61
+ // Build the model from client-supplied options
62
+ const openai = (0, openai_1.createOpenAI)({
63
+ baseURL: req.agent.options?.baseURL || undefined,
64
+ apiKey: req.agent.options?.apiKey || undefined,
65
+ });
66
+ const model = new model_js_1.AiModelProvider(openai.chat(req.agent.options?.model ?? "gpt-4o"));
67
+ const session = new session_js_1.Session(sessionId, agent, model, req.agent, req.tools);
68
+ sessions.set(sessionId, session);
69
+ return session.runNewSession(req);
70
+ },
71
+ sendTurn(sessionId, req) {
72
+ const session = sessions.get(sessionId);
73
+ if (!session)
74
+ throw new Error(`Session not found: ${sessionId}`);
75
+ return session.runTurn(req);
76
+ },
77
+ async getSession(sessionId, history) {
78
+ const session = sessions.get(sessionId);
79
+ if (!session)
80
+ throw new Error(`Session not found: ${sessionId}`);
81
+ // history is never compacted, so compacted and full are the same
82
+ const historyData = history ? { [history]: session.history } : undefined;
83
+ return { ...session.toSessionResponse(), history: historyData };
84
+ },
85
+ async listSessions() {
86
+ return { sessions: [...sessions.keys()] };
87
+ },
88
+ async deleteSession(sessionId) {
89
+ sessions.delete(sessionId);
90
+ },
91
+ };
92
+ const port = Number(process.env.PORT ?? 3010);
93
+ const app = new hono_1.Hono();
94
+ app.use("*", (0, cors_1.cors)({ origin: "*" }));
95
+ app.route("/", (0, server_js_1.aap)(handler));
96
+ (0, node_server_1.serve)({ fetch: app.fetch, port });
97
+ console.log(`basic-agent running on http://localhost:${port}`);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const node_server_1 = require("@hono/node-server");
4
+ const node_crypto_1 = require("node:crypto");
5
+ const zod_1 = require("zod");
6
+ const hono_1 = require("hono");
7
+ const cors_1 = require("hono/cors");
8
+ const server_js_1 = require("../../server.js");
9
+ const agent_js_1 = require("../../agent.js");
10
+ const model_js_1 = require("../../model.js");
11
+ const openai_1 = require("@ai-sdk/openai");
12
+ const session_js_1 = require("./session.js");
13
+ /** Agent definition with options, capabilities, and tools. */
14
+ const agent = new agent_js_1.Agent("compact-history-agent", {
15
+ version: "0.1.0",
16
+ description: "An AAP-compatible agent with sliding-window history compaction, powered by Vercel AI SDK.",
17
+ })
18
+ .image({ http: {}, data: {} })
19
+ .history({ compacted: {}, full: {} })
20
+ .option({
21
+ name: "baseURL",
22
+ title: "LLM Base URL",
23
+ description: "OpenAI-compatible base URL",
24
+ type: "text",
25
+ default: "",
26
+ })
27
+ .option({
28
+ name: "apiKey",
29
+ title: "LLM API Key",
30
+ description: "OpenAI API key",
31
+ type: "secret",
32
+ default: "",
33
+ })
34
+ .option({
35
+ name: "model",
36
+ title: "Model",
37
+ description: "Model ID to use",
38
+ type: "text",
39
+ default: "gpt-4o",
40
+ })
41
+ .tool("web_fetch", {
42
+ description: "Fetch the text content of a URL",
43
+ inputSchema: zod_1.z.object({ url: zod_1.z.string().describe("URL to fetch") }),
44
+ }, async ({ url }) => {
45
+ const res = await fetch(url);
46
+ const text = await res.text();
47
+ return text
48
+ .replace(/<[^>]+>/g, " ")
49
+ .replace(/\s+/g, " ")
50
+ .trim()
51
+ .slice(0, 8000);
52
+ });
53
+ const handler = {
54
+ getMeta() {
55
+ return { version: 1, agents: [agent.info] };
56
+ },
57
+ createSession(req) {
58
+ const sessionId = `sess_${(0, node_crypto_1.randomUUID)()}`;
59
+ // Build the model from client-supplied options
60
+ const openai = (0, openai_1.createOpenAI)({
61
+ baseURL: req.agent.options?.baseURL || undefined,
62
+ apiKey: req.agent.options?.apiKey || undefined,
63
+ });
64
+ const model = new model_js_1.AiModelProvider(openai.chat(req.agent.options?.model ?? "gpt-4o"));
65
+ const session = new session_js_1.TruncatedHistorySession(sessionId, agent, model, req.agent, req.tools);
66
+ session_js_1.sessions.set(sessionId, session);
67
+ return session.runNewSession(req);
68
+ },
69
+ sendTurn(sessionId, req) {
70
+ const session = session_js_1.sessions.get(sessionId);
71
+ if (!session)
72
+ throw new Error(`Session not found: ${sessionId}`);
73
+ return session.runTurn(req);
74
+ },
75
+ async getSession(sessionId, history) {
76
+ const session = session_js_1.sessions.get(sessionId);
77
+ if (!session)
78
+ throw new Error(`Session not found: ${sessionId}`);
79
+ // history exposes both the compacted window and the full uncompacted history
80
+ const historyData = history === "compacted"
81
+ ? { compacted: session.history }
82
+ : history === "full"
83
+ ? { full: session.fullHistory }
84
+ : undefined;
85
+ return { ...session.toSessionResponse(), history: historyData };
86
+ },
87
+ async listSessions() {
88
+ return { sessions: [...session_js_1.sessions.keys()] };
89
+ },
90
+ async deleteSession(sessionId) {
91
+ session_js_1.sessions.delete(sessionId);
92
+ },
93
+ };
94
+ const port = Number(process.env.PORT ?? 3010);
95
+ const app = new hono_1.Hono();
96
+ app.use("*", (0, cors_1.cors)({ origin: "*" }));
97
+ app.route("/", (0, server_js_1.aap)(handler));
98
+ (0, node_server_1.serve)({ fetch: app.fetch, port });
99
+ console.log(`compact-history-agent running on http://localhost:${port}`);
@@ -0,0 +1,18 @@
1
+ import type { AgentResponse, DeltaSSEEvent, HistoryMessage } from "@agentapplicationprotocol/core";
2
+ import { Session } from "../../session.js";
3
+ /** In-memory session store. */
4
+ export declare const sessions: Map<string, TruncatedHistorySession>;
5
+ /**
6
+ * Session subclass with sliding-window history compaction.
7
+ * `this.history` holds the compacted window sent to the model.
8
+ * `fullHistory` retains the complete uncompacted history.
9
+ */
10
+ export declare class TruncatedHistorySession extends Session {
11
+ fullHistory: HistoryMessage[];
12
+ /** Appends new history entries to `fullHistory` and trims `this.history` to the compacted window. */
13
+ private syncAndCompact;
14
+ /** Streams the model response, then compacts history. */
15
+ protected stream(messages: HistoryMessage[]): AsyncIterable<DeltaSSEEvent>;
16
+ /** Calls the model, then compacts history. */
17
+ protected call(messages: HistoryMessage[]): Promise<AgentResponse>;
18
+ }
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TruncatedHistorySession = exports.sessions = void 0;
4
+ const session_js_1 = require("../../session.js");
5
+ /** In-memory session store. */
6
+ exports.sessions = new Map();
7
+ const COMPACTED_HISTORY_SIZE = 10;
8
+ /** Trims history to the last `COMPACTED_HISTORY_SIZE` messages, ensuring the window never starts on a `tool` message. */
9
+ function compact(history) {
10
+ if (history.length <= COMPACTED_HISTORY_SIZE)
11
+ return history;
12
+ let start = history.length - COMPACTED_HISTORY_SIZE;
13
+ // skip leading tool messages to avoid orphaned tool results
14
+ while (start < history.length && history[start].role === "tool")
15
+ start++;
16
+ return history.slice(start);
17
+ }
18
+ /**
19
+ * Session subclass with sliding-window history compaction.
20
+ * `this.history` holds the compacted window sent to the model.
21
+ * `fullHistory` retains the complete uncompacted history.
22
+ */
23
+ class TruncatedHistorySession extends session_js_1.Session {
24
+ constructor() {
25
+ super(...arguments);
26
+ this.fullHistory = [];
27
+ }
28
+ /** Appends new history entries to `fullHistory` and trims `this.history` to the compacted window. */
29
+ syncAndCompact(before) {
30
+ this.fullHistory.push(...this.history.slice(before));
31
+ this.history = compact(this.history);
32
+ }
33
+ /** Streams the model response, then compacts history. */
34
+ async *stream(messages) {
35
+ const before = this.history.length;
36
+ for await (const e of super.stream(messages))
37
+ yield e;
38
+ this.syncAndCompact(before);
39
+ }
40
+ /** Calls the model, then compacts history. */
41
+ async call(messages) {
42
+ const before = this.history.length;
43
+ const res = await super.call(messages);
44
+ this.syncAndCompact(before);
45
+ return res;
46
+ }
47
+ }
48
+ exports.TruncatedHistorySession = TruncatedHistorySession;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,154 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const model_1 = require("./model");
5
+ // --- ModelProvider base class ---
6
+ class ConcreteModel extends model_1.ModelProvider {
7
+ async *stream(_history) {
8
+ yield { event: "text_delta", delta: "hello" };
9
+ yield { event: "turn_stop", stopReason: "end_turn" };
10
+ }
11
+ }
12
+ (0, vitest_1.describe)("ModelProvider", () => {
13
+ (0, vitest_1.it)("call() falls back to stream() and returns AgentResponse", async () => {
14
+ const model = new ConcreteModel();
15
+ const res = await model.call([{ role: "user", content: "hi" }], []);
16
+ (0, vitest_1.expect)(res.stopReason).toBe("end_turn");
17
+ (0, vitest_1.expect)(res.messages).toHaveLength(1);
18
+ (0, vitest_1.expect)(res.messages[0]).toMatchObject({ role: "assistant" });
19
+ });
20
+ });
21
+ function makeLM() {
22
+ return {
23
+ specificationVersion: "v2",
24
+ provider: "test",
25
+ modelId: "test-model",
26
+ defaultObjectGenerationMode: undefined,
27
+ doStream: vitest_1.vi.fn(),
28
+ doGenerate: vitest_1.vi.fn(),
29
+ };
30
+ }
31
+ function mockStream(lm, chunks) {
32
+ lm.doStream.mockResolvedValue({
33
+ stream: new ReadableStream({
34
+ start(controller) {
35
+ for (const chunk of chunks)
36
+ controller.enqueue(chunk);
37
+ controller.close();
38
+ },
39
+ }),
40
+ rawValue: {},
41
+ warnings: [],
42
+ });
43
+ }
44
+ function mockGenerate(lm, finishReason, content) {
45
+ lm.doGenerate.mockResolvedValue({
46
+ finishReason,
47
+ usage: { inputTokens: 1, outputTokens: 1 },
48
+ content,
49
+ rawValue: {},
50
+ warnings: [],
51
+ response: { id: "r1", timestamp: new Date(), modelId: "test-model", headers: {} },
52
+ });
53
+ }
54
+ (0, vitest_1.describe)("AiModelProvider", () => {
55
+ (0, vitest_1.it)("stores the language model", () => {
56
+ const lm = makeLM();
57
+ (0, vitest_1.expect)(new model_1.AiModelProvider(lm).model).toBe(lm);
58
+ });
59
+ (0, vitest_1.it)("stream() yields text_delta and turn_stop", async () => {
60
+ const lm = makeLM();
61
+ mockStream(lm, [
62
+ { type: "text-delta", id: "t1", delta: "hello", providerMetadata: undefined },
63
+ {
64
+ type: "finish",
65
+ finishReason: "stop",
66
+ usage: { inputTokens: 1, outputTokens: 1 },
67
+ providerMetadata: undefined,
68
+ },
69
+ ]);
70
+ const events = [];
71
+ for await (const e of new model_1.AiModelProvider(lm).stream([{ role: "user", content: "hi" }], [])) {
72
+ events.push(e);
73
+ }
74
+ (0, vitest_1.expect)(events.some((e) => e.event === "text_delta")).toBe(true);
75
+ (0, vitest_1.expect)(events.some((e) => e.event === "turn_stop")).toBe(true);
76
+ });
77
+ (0, vitest_1.it)("stream() yields thinking_delta and tool_call", async () => {
78
+ const lm = makeLM();
79
+ mockStream(lm, [
80
+ { type: "reasoning-delta", id: "r1", delta: "hmm", providerMetadata: undefined },
81
+ {
82
+ type: "tool-call",
83
+ toolCallId: "c1",
84
+ toolName: "fn",
85
+ input: { x: 1 },
86
+ providerMetadata: undefined,
87
+ },
88
+ {
89
+ type: "finish",
90
+ finishReason: "tool-calls",
91
+ usage: { inputTokens: 1, outputTokens: 1 },
92
+ providerMetadata: undefined,
93
+ },
94
+ ]);
95
+ const events = [];
96
+ for await (const e of new model_1.AiModelProvider(lm).stream([{ role: "user", content: "hi" }], [])) {
97
+ events.push(e);
98
+ }
99
+ (0, vitest_1.expect)(events.some((e) => e.event === "thinking_delta")).toBe(true);
100
+ (0, vitest_1.expect)(events.some((e) => e.event === "tool_call")).toBe(true);
101
+ (0, vitest_1.expect)(events.find((e) => e.event === "turn_stop")).toMatchObject({ stopReason: "tool_use" });
102
+ });
103
+ (0, vitest_1.it)("call() returns AgentResponse with text content", async () => {
104
+ const lm = makeLM();
105
+ mockGenerate(lm, "stop", [{ type: "text", text: "done" }]);
106
+ const res = await new model_1.AiModelProvider(lm).call([{ role: "user", content: "hi" }], []);
107
+ (0, vitest_1.expect)(res.stopReason).toBe("end_turn");
108
+ (0, vitest_1.expect)(res.messages[0].role).toBe("assistant");
109
+ });
110
+ (0, vitest_1.it)("call() maps reasoning and tool-call blocks in response", async () => {
111
+ const lm = makeLM();
112
+ mockGenerate(lm, "stop", [
113
+ { type: "reasoning", text: "thinking..." },
114
+ { type: "tool-call", toolCallId: "c1", toolName: "fn", input: { x: 1 } },
115
+ ]);
116
+ const res = await new model_1.AiModelProvider(lm).call([{ role: "user", content: "hi" }], []);
117
+ const blocks = res.messages.find((m) => m.role === "assistant").content;
118
+ (0, vitest_1.expect)(blocks.some((b) => b.type === "thinking")).toBe(true);
119
+ (0, vitest_1.expect)(blocks.some((b) => b.type === "tool_use")).toBe(true);
120
+ });
121
+ (0, vitest_1.it)("call() passes system, image (data URI), assistant with thinking/tool_use, and tool messages", async () => {
122
+ const lm = makeLM();
123
+ mockGenerate(lm, "tool-calls", [
124
+ { type: "tool-call", toolCallId: "c1", toolName: "fn", input: { x: 1 } },
125
+ ]);
126
+ const history = [
127
+ { role: "system", content: "be helpful" },
128
+ { role: "user", content: [{ type: "image", url: "data:image/png;base64,abc" }] },
129
+ {
130
+ role: "assistant",
131
+ content: [
132
+ { type: "thinking", thinking: "hmm" },
133
+ { type: "tool_use", toolCallId: "c1", name: "fn", input: { x: 1 } },
134
+ ],
135
+ },
136
+ { role: "tool", toolCallId: "c1", content: "result" },
137
+ ];
138
+ const res = await new model_1.AiModelProvider(lm).call(history, []);
139
+ (0, vitest_1.expect)(res.stopReason).toBe("tool_use");
140
+ });
141
+ (0, vitest_1.it)("call() maps finishReason variants correctly", async () => {
142
+ const cases = [
143
+ ["length", "max_tokens"],
144
+ ["content-filter", "refusal"],
145
+ ["error", "error"],
146
+ ];
147
+ for (const [finishReason, expected] of cases) {
148
+ const lm = makeLM();
149
+ mockGenerate(lm, finishReason, []);
150
+ const res = await new model_1.AiModelProvider(lm).call([{ role: "user", content: "hi" }], []);
151
+ (0, vitest_1.expect)(res.stopReason).toBe(expected);
152
+ }
153
+ });
154
+ });
@@ -0,0 +1 @@
1
+ export {};