@agentapplicationprotocol/server 0.4.2 → 0.6.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/dist/src/agent.test.d.ts +1 -0
- package/dist/src/agent.test.js +61 -0
- package/dist/src/examples/basic/index.d.ts +1 -0
- package/dist/src/examples/basic/index.js +102 -0
- package/dist/src/examples/compact-history/index.d.ts +1 -0
- package/dist/src/examples/compact-history/index.js +99 -0
- package/dist/src/examples/compact-history/session.d.ts +18 -0
- package/dist/src/examples/compact-history/session.js +48 -0
- package/dist/src/model.test.d.ts +1 -0
- package/dist/src/model.test.js +154 -0
- package/dist/src/server.d.ts +4 -3
- package/dist/src/server.js +11 -15
- package/dist/src/server.test.d.ts +1 -0
- package/dist/src/server.test.js +208 -0
- package/dist/src/session.test.d.ts +1 -0
- package/dist/src/session.test.js +303 -0
- package/package.json +3 -2
|
@@ -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,102 @@
|
|
|
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 { 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) {
|
|
78
|
+
const session = sessions.get(sessionId);
|
|
79
|
+
if (!session)
|
|
80
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
81
|
+
return session.toSessionResponse();
|
|
82
|
+
},
|
|
83
|
+
async getSessionHistory(sessionId, type) {
|
|
84
|
+
const session = sessions.get(sessionId);
|
|
85
|
+
if (!session)
|
|
86
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
87
|
+
// history is never compacted, so compacted and full are the same
|
|
88
|
+
return session.history;
|
|
89
|
+
},
|
|
90
|
+
async listSessions() {
|
|
91
|
+
return { sessions: [...sessions.values()].map((s) => s.toSessionResponse()) };
|
|
92
|
+
},
|
|
93
|
+
async deleteSession(sessionId) {
|
|
94
|
+
sessions.delete(sessionId);
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
const port = Number(process.env.PORT ?? 3010);
|
|
98
|
+
const app = new hono_1.Hono();
|
|
99
|
+
app.use("*", (0, cors_1.cors)({ origin: "*" }));
|
|
100
|
+
app.route("/", (0, server_js_1.aap)(handler));
|
|
101
|
+
(0, node_server_1.serve)({ fetch: app.fetch, port });
|
|
102
|
+
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 { 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) {
|
|
76
|
+
const session = session_js_1.sessions.get(sessionId);
|
|
77
|
+
if (!session)
|
|
78
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
79
|
+
return session.toSessionResponse();
|
|
80
|
+
},
|
|
81
|
+
async getSessionHistory(sessionId, type) {
|
|
82
|
+
const session = session_js_1.sessions.get(sessionId);
|
|
83
|
+
if (!session)
|
|
84
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
85
|
+
return type === "compacted" ? session.history : session.fullHistory;
|
|
86
|
+
},
|
|
87
|
+
async listSessions() {
|
|
88
|
+
return { sessions: [...session_js_1.sessions.values()].map((s) => s.toSessionResponse()) };
|
|
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
|
+
});
|
package/dist/src/server.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
-
import { AgentResponse, CreateSessionRequest, CreateSessionResponse, MetaResponse, SessionListResponse, SessionResponse, SessionTurnRequest, SSEEvent } from "@agentapplicationprotocol/core";
|
|
2
|
+
import { AgentResponse, CreateSessionRequest, CreateSessionResponse, HistoryMessage, MetaResponse, SessionListResponse, SessionResponse, SessionTurnRequest, SSEEvent } from "@agentapplicationprotocol/core";
|
|
3
3
|
export interface Handler {
|
|
4
|
-
getMeta(): MetaResponse
|
|
4
|
+
getMeta(): Omit<MetaResponse, "version">;
|
|
5
5
|
listSessions(params: {
|
|
6
6
|
after?: string;
|
|
7
7
|
}): Promise<SessionListResponse>;
|
|
8
|
-
getSession(sessionId: string
|
|
8
|
+
getSession(sessionId: string): Promise<SessionResponse>;
|
|
9
|
+
getSessionHistory(sessionId: string, type: "compacted" | "full"): Promise<HistoryMessage[]>;
|
|
9
10
|
/** The last message in `req.messages` is guaranteed to be a user message. */
|
|
10
11
|
createSession(req: CreateSessionRequest): Promise<CreateSessionResponse> | AsyncIterable<SSEEvent>;
|
|
11
12
|
sendTurn(sessionId: string, req: SessionTurnRequest): Promise<AgentResponse> | AsyncIterable<SSEEvent>;
|
package/dist/src/server.js
CHANGED
|
@@ -41,7 +41,7 @@ function redactSecretOptions(session, agents) {
|
|
|
41
41
|
*/
|
|
42
42
|
function aap(handler) {
|
|
43
43
|
const router = new hono_1.Hono();
|
|
44
|
-
router.get("/meta", (c) => c.json(handler.getMeta()));
|
|
44
|
+
router.get("/meta", (c) => c.json({ version: 2, ...handler.getMeta() }));
|
|
45
45
|
router.put("/session", async (c) => {
|
|
46
46
|
const req = await c.req.json();
|
|
47
47
|
if (req.messages.at(-1)?.role !== "user")
|
|
@@ -61,20 +61,16 @@ function aap(handler) {
|
|
|
61
61
|
return c.json(result);
|
|
62
62
|
});
|
|
63
63
|
router.get("/session/:id", async (c) => {
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
session.history = undefined;
|
|
75
|
-
}
|
|
76
|
-
const meta = handler.getMeta();
|
|
77
|
-
return c.json(redactSecretOptions(session, meta.agents));
|
|
64
|
+
const session = await handler.getSession(c.req.param("id"));
|
|
65
|
+
const { agents } = handler.getMeta();
|
|
66
|
+
return c.json(redactSecretOptions(session, agents));
|
|
67
|
+
});
|
|
68
|
+
router.get("/session/:id/history", async (c) => {
|
|
69
|
+
const typeParam = c.req.query("type");
|
|
70
|
+
if (typeParam !== "compacted" && typeParam !== "full")
|
|
71
|
+
return c.json({ error: 'type must be "compacted" or "full"' }, 400);
|
|
72
|
+
const messages = await handler.getSessionHistory(c.req.param("id"), typeParam);
|
|
73
|
+
return c.json({ history: { [typeParam]: messages } });
|
|
78
74
|
});
|
|
79
75
|
router.get("/sessions", async (c) => {
|
|
80
76
|
const after = c.req.query("after");
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const hono_1 = require("hono");
|
|
5
|
+
const bearer_auth_1 = require("hono/bearer-auth");
|
|
6
|
+
const cors_1 = require("hono/cors");
|
|
7
|
+
const server_1 = require("./server");
|
|
8
|
+
const meta = { version: 2, agents: [] };
|
|
9
|
+
const session = { sessionId: "s1", agent: { name: "a" } };
|
|
10
|
+
const agentResponse = { stopReason: "end_turn", messages: [] };
|
|
11
|
+
const createSessionResponse = { ...agentResponse, sessionId: "s1" };
|
|
12
|
+
const sessionList = { sessions: [session] };
|
|
13
|
+
async function* sseEvents() {
|
|
14
|
+
yield { event: "turn_start" };
|
|
15
|
+
yield { event: "text", text: "hi" };
|
|
16
|
+
yield { event: "turn_stop", stopReason: "end_turn" };
|
|
17
|
+
}
|
|
18
|
+
function makeHandler(overrides = {}) {
|
|
19
|
+
return {
|
|
20
|
+
getMeta: vitest_1.vi.fn().mockReturnValue({ agents: [] }),
|
|
21
|
+
listSessions: vitest_1.vi.fn().mockResolvedValue(sessionList),
|
|
22
|
+
getSession: vitest_1.vi.fn().mockResolvedValue(session),
|
|
23
|
+
getSessionHistory: vitest_1.vi.fn().mockResolvedValue([]),
|
|
24
|
+
createSession: vitest_1.vi.fn().mockResolvedValue(createSessionResponse),
|
|
25
|
+
sendTurn: vitest_1.vi.fn().mockResolvedValue(agentResponse),
|
|
26
|
+
deleteSession: vitest_1.vi.fn().mockResolvedValue(undefined),
|
|
27
|
+
...overrides,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function makeApp(handler, setup) {
|
|
31
|
+
const app = new hono_1.Hono();
|
|
32
|
+
setup?.(app);
|
|
33
|
+
app.route("/", (0, server_1.aap)(handler));
|
|
34
|
+
return app;
|
|
35
|
+
}
|
|
36
|
+
function req(method, path, body, headers) {
|
|
37
|
+
return new Request(`http://localhost${path}`, {
|
|
38
|
+
method,
|
|
39
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
40
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
(0, vitest_1.describe)("aap middleware", () => {
|
|
44
|
+
(0, vitest_1.it)("GET /meta returns meta", async () => {
|
|
45
|
+
const app = makeApp(makeHandler());
|
|
46
|
+
const res = await app.fetch(req("GET", "/meta"));
|
|
47
|
+
(0, vitest_1.expect)(res.status).toBe(200);
|
|
48
|
+
(0, vitest_1.expect)(await res.json()).toEqual(meta);
|
|
49
|
+
});
|
|
50
|
+
(0, vitest_1.it)("GET /session/:id returns session", async () => {
|
|
51
|
+
const app = makeApp(makeHandler());
|
|
52
|
+
const res = await app.fetch(req("GET", "/session/s1"));
|
|
53
|
+
(0, vitest_1.expect)(res.status).toBe(200);
|
|
54
|
+
(0, vitest_1.expect)(await res.json()).toEqual(session);
|
|
55
|
+
});
|
|
56
|
+
(0, vitest_1.it)("GET /session/:id redacts secret options", async () => {
|
|
57
|
+
const secretSession = {
|
|
58
|
+
sessionId: "s1",
|
|
59
|
+
agent: { name: "a", options: { key: "mysecret", model: "gpt-4" } },
|
|
60
|
+
};
|
|
61
|
+
const app = makeApp(makeHandler({
|
|
62
|
+
getSession: vitest_1.vi.fn().mockResolvedValue(secretSession),
|
|
63
|
+
getMeta: vitest_1.vi.fn().mockReturnValue({
|
|
64
|
+
agents: [
|
|
65
|
+
{
|
|
66
|
+
name: "a",
|
|
67
|
+
version: "1.0.0",
|
|
68
|
+
options: [
|
|
69
|
+
{ type: "secret", name: "key", default: "" },
|
|
70
|
+
{ type: "text", name: "model", default: "" },
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
}),
|
|
75
|
+
}));
|
|
76
|
+
const res = await app.fetch(req("GET", "/session/s1"));
|
|
77
|
+
(0, vitest_1.expect)(res.status).toBe(200);
|
|
78
|
+
(0, vitest_1.expect)(await res.json()).toEqual({
|
|
79
|
+
...secretSession,
|
|
80
|
+
agent: { ...secretSession.agent, options: { key: "***", model: "gpt-4" } },
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
(0, vitest_1.it)("GET /sessions returns session list", async () => {
|
|
84
|
+
const handler = makeHandler();
|
|
85
|
+
const app = makeApp(handler);
|
|
86
|
+
const res = await app.fetch(req("GET", "/sessions?after=cursor1"));
|
|
87
|
+
(0, vitest_1.expect)(res.status).toBe(200);
|
|
88
|
+
(0, vitest_1.expect)(await res.json()).toEqual(sessionList);
|
|
89
|
+
(0, vitest_1.expect)(handler.listSessions).toHaveBeenCalledWith({ after: "cursor1" });
|
|
90
|
+
});
|
|
91
|
+
(0, vitest_1.it)("PUT /session returns 400 if last message is not a user message", async () => {
|
|
92
|
+
const app = makeApp(makeHandler());
|
|
93
|
+
const res = await app.fetch(req("PUT", "/session", {
|
|
94
|
+
agent: { name: "a" },
|
|
95
|
+
messages: [{ role: "assistant", content: "hi" }],
|
|
96
|
+
}));
|
|
97
|
+
(0, vitest_1.expect)(res.status).toBe(400);
|
|
98
|
+
});
|
|
99
|
+
(0, vitest_1.it)("PUT /session returns 201 with AgentResponse", async () => {
|
|
100
|
+
const app = makeApp(makeHandler());
|
|
101
|
+
const res = await app.fetch(req("PUT", "/session", { agent: { name: "a" }, messages: [{ role: "user", content: "hi" }] }));
|
|
102
|
+
(0, vitest_1.expect)(res.status).toBe(201);
|
|
103
|
+
(0, vitest_1.expect)(await res.json()).toEqual(createSessionResponse);
|
|
104
|
+
});
|
|
105
|
+
(0, vitest_1.it)("PUT /session with stream returns SSE", async () => {
|
|
106
|
+
const app = makeApp(makeHandler({ createSession: vitest_1.vi.fn().mockResolvedValue(sseEvents()) }));
|
|
107
|
+
const res = await app.fetch(req("PUT", "/session", {
|
|
108
|
+
agent: { name: "a" },
|
|
109
|
+
messages: [{ role: "user", content: "hi" }],
|
|
110
|
+
stream: "message",
|
|
111
|
+
}));
|
|
112
|
+
(0, vitest_1.expect)(res.status).toBe(200);
|
|
113
|
+
(0, vitest_1.expect)(res.headers.get("content-type")).toContain("text/event-stream");
|
|
114
|
+
});
|
|
115
|
+
(0, vitest_1.it)("POST /session/:id returns AgentResponse", async () => {
|
|
116
|
+
const app = makeApp(makeHandler());
|
|
117
|
+
const res = await app.fetch(req("POST", "/session/s1", { messages: [{ role: "user", content: "hi" }] }));
|
|
118
|
+
(0, vitest_1.expect)(res.status).toBe(200);
|
|
119
|
+
(0, vitest_1.expect)(await res.json()).toEqual(agentResponse);
|
|
120
|
+
});
|
|
121
|
+
(0, vitest_1.it)("POST /session/:id with stream returns SSE", async () => {
|
|
122
|
+
const app = makeApp(makeHandler({ sendTurn: vitest_1.vi.fn().mockResolvedValue(sseEvents()) }));
|
|
123
|
+
const res = await app.fetch(req("POST", "/session/s1", { messages: [{ role: "user", content: "hi" }], stream: "delta" }));
|
|
124
|
+
(0, vitest_1.expect)(res.status).toBe(200);
|
|
125
|
+
(0, vitest_1.expect)(res.headers.get("content-type")).toContain("text/event-stream");
|
|
126
|
+
});
|
|
127
|
+
(0, vitest_1.it)("DELETE /session/:id returns 204", async () => {
|
|
128
|
+
const app = makeApp(makeHandler());
|
|
129
|
+
const res = await app.fetch(req("DELETE", "/session/s1"));
|
|
130
|
+
(0, vitest_1.expect)(res.status).toBe(204);
|
|
131
|
+
});
|
|
132
|
+
(0, vitest_1.it)("auth middleware: returns 401 when rejected", async () => {
|
|
133
|
+
const app = makeApp(makeHandler(), (a) => a.use((0, bearer_auth_1.bearerAuth)({ token: "secret" })));
|
|
134
|
+
const res = await app.fetch(req("GET", "/meta"));
|
|
135
|
+
(0, vitest_1.expect)(res.status).toBe(401);
|
|
136
|
+
});
|
|
137
|
+
(0, vitest_1.it)("auth middleware: passes with valid token", async () => {
|
|
138
|
+
const app = makeApp(makeHandler(), (a) => a.use((0, bearer_auth_1.bearerAuth)({ token: "secret" })));
|
|
139
|
+
const res = await app.fetch(req("GET", "/meta", undefined, { Authorization: "Bearer secret" }));
|
|
140
|
+
(0, vitest_1.expect)(res.status).toBe(200);
|
|
141
|
+
});
|
|
142
|
+
(0, vitest_1.it)("base path: mounts under sub-router", async () => {
|
|
143
|
+
const app = new hono_1.Hono();
|
|
144
|
+
app.route("/api/v1", (0, server_1.aap)(makeHandler()));
|
|
145
|
+
const res = await app.fetch(req("GET", "/api/v1/meta"));
|
|
146
|
+
(0, vitest_1.expect)(res.status).toBe(200);
|
|
147
|
+
});
|
|
148
|
+
(0, vitest_1.it)("cors: sets Access-Control-Allow-Origin header", async () => {
|
|
149
|
+
const app = makeApp(makeHandler(), (a) => a.use((0, cors_1.cors)({ origin: "https://example.com" })));
|
|
150
|
+
const res = await app.fetch(req("GET", "/meta", undefined, { Origin: "https://example.com" }));
|
|
151
|
+
(0, vitest_1.expect)(res.headers.get("access-control-allow-origin")).toBe("https://example.com");
|
|
152
|
+
});
|
|
153
|
+
(0, vitest_1.it)("GET /session/:id does not redact when agent not found in meta", async () => {
|
|
154
|
+
const sessionWithOptions = {
|
|
155
|
+
sessionId: "s1",
|
|
156
|
+
agent: { name: "unknown", options: { key: "secret" } },
|
|
157
|
+
};
|
|
158
|
+
const app = makeApp(makeHandler({
|
|
159
|
+
getSession: vitest_1.vi.fn().mockResolvedValue(sessionWithOptions),
|
|
160
|
+
getMeta: vitest_1.vi.fn().mockReturnValue({ agents: [] }),
|
|
161
|
+
}));
|
|
162
|
+
const res = await app.fetch(req("GET", "/session/s1"));
|
|
163
|
+
(0, vitest_1.expect)(await res.json()).toEqual(sessionWithOptions);
|
|
164
|
+
});
|
|
165
|
+
(0, vitest_1.it)("GET /session/:id does not redact when agent has no secret options", async () => {
|
|
166
|
+
const sessionWithOptions = {
|
|
167
|
+
sessionId: "s1",
|
|
168
|
+
agent: { name: "a", options: { model: "gpt-4" } },
|
|
169
|
+
};
|
|
170
|
+
const app = makeApp(makeHandler({
|
|
171
|
+
getSession: vitest_1.vi.fn().mockResolvedValue(sessionWithOptions),
|
|
172
|
+
getMeta: vitest_1.vi.fn().mockReturnValue({
|
|
173
|
+
agents: [
|
|
174
|
+
{
|
|
175
|
+
name: "a",
|
|
176
|
+
version: "1.0.0",
|
|
177
|
+
options: [{ type: "text", name: "model", default: "" }],
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
}),
|
|
181
|
+
}));
|
|
182
|
+
const res = await app.fetch(req("GET", "/session/s1"));
|
|
183
|
+
(0, vitest_1.expect)(await res.json()).toEqual(sessionWithOptions);
|
|
184
|
+
});
|
|
185
|
+
(0, vitest_1.it)("GET /session/:id/history?type=compacted calls getSessionHistory", async () => {
|
|
186
|
+
const messages = [{ role: "user", content: "hi" }];
|
|
187
|
+
const handler = makeHandler({ getSessionHistory: vitest_1.vi.fn().mockResolvedValue(messages) });
|
|
188
|
+
const app = makeApp(handler);
|
|
189
|
+
const res = await app.fetch(req("GET", "/session/s1/history?type=compacted"));
|
|
190
|
+
(0, vitest_1.expect)(res.status).toBe(200);
|
|
191
|
+
(0, vitest_1.expect)(handler.getSessionHistory).toHaveBeenCalledWith("s1", "compacted");
|
|
192
|
+
(0, vitest_1.expect)(await res.json()).toEqual({ history: { compacted: messages } });
|
|
193
|
+
});
|
|
194
|
+
(0, vitest_1.it)("GET /session/:id/history?type=full calls getSessionHistory", async () => {
|
|
195
|
+
const messages = [{ role: "user", content: "hi" }];
|
|
196
|
+
const handler = makeHandler({ getSessionHistory: vitest_1.vi.fn().mockResolvedValue(messages) });
|
|
197
|
+
const app = makeApp(handler);
|
|
198
|
+
const res = await app.fetch(req("GET", "/session/s1/history?type=full"));
|
|
199
|
+
(0, vitest_1.expect)(res.status).toBe(200);
|
|
200
|
+
(0, vitest_1.expect)(handler.getSessionHistory).toHaveBeenCalledWith("s1", "full");
|
|
201
|
+
(0, vitest_1.expect)(await res.json()).toEqual({ history: { full: messages } });
|
|
202
|
+
});
|
|
203
|
+
(0, vitest_1.it)("GET /session/:id/history without valid ?type returns 400", async () => {
|
|
204
|
+
const app = makeApp(makeHandler());
|
|
205
|
+
const res = await app.fetch(req("GET", "/session/s1/history"));
|
|
206
|
+
(0, vitest_1.expect)(res.status).toBe(400);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,303 @@
|
|
|
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 session_1 = require("./session");
|
|
8
|
+
const agent_1 = require("./agent");
|
|
9
|
+
const zod_1 = __importDefault(require("zod"));
|
|
10
|
+
function makeModel(overrides = {}) {
|
|
11
|
+
return {
|
|
12
|
+
stream: vitest_1.vi.fn(async function* () {
|
|
13
|
+
yield { event: "text_delta", delta: "hi" };
|
|
14
|
+
yield { event: "turn_stop", stopReason: "end_turn" };
|
|
15
|
+
}),
|
|
16
|
+
call: vitest_1.vi.fn().mockResolvedValue({
|
|
17
|
+
stopReason: "end_turn",
|
|
18
|
+
messages: [{ role: "assistant", content: "hi" }],
|
|
19
|
+
}),
|
|
20
|
+
...overrides,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function makeAgent() {
|
|
24
|
+
const agent = new agent_1.Agent("test-agent");
|
|
25
|
+
agent.tool("echo", { inputSchema: zod_1.default.object({ msg: zod_1.default.string() }) }, async ({ msg }) => msg);
|
|
26
|
+
return agent;
|
|
27
|
+
}
|
|
28
|
+
function makeSession(agentConfig = { name: "test-agent" }, model = makeModel(), agent = makeAgent()) {
|
|
29
|
+
return new session_1.Session("sess-1", agent, model, agentConfig);
|
|
30
|
+
}
|
|
31
|
+
const userMsg = { role: "user", content: "hello" };
|
|
32
|
+
(0, vitest_1.describe)("Session", () => {
|
|
33
|
+
(0, vitest_1.it)("initializes with empty history and clientTools", () => {
|
|
34
|
+
const s = makeSession();
|
|
35
|
+
(0, vitest_1.expect)(s.sessionId).toBe("sess-1");
|
|
36
|
+
(0, vitest_1.expect)(s.history).toEqual([]);
|
|
37
|
+
(0, vitest_1.expect)(s.clientTools).toEqual([]);
|
|
38
|
+
});
|
|
39
|
+
(0, vitest_1.it)("toSessionResponse returns sessionId and agentConfig", () => {
|
|
40
|
+
const s = makeSession({ name: "test-agent", options: { model: "gpt-4" } });
|
|
41
|
+
(0, vitest_1.expect)(s.toSessionResponse()).toEqual({
|
|
42
|
+
sessionId: "sess-1",
|
|
43
|
+
agent: { name: "test-agent", options: { model: "gpt-4" } },
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
(0, vitest_1.it)("toSessionResponse includes tools when clientTools is set", () => {
|
|
47
|
+
const s = makeSession();
|
|
48
|
+
s.clientTools = [{ name: "t", description: "", inputSchema: {} }];
|
|
49
|
+
(0, vitest_1.expect)(s.toSessionResponse().tools).toHaveLength(1);
|
|
50
|
+
});
|
|
51
|
+
(0, vitest_1.describe)("runTurn (none mode)", () => {
|
|
52
|
+
(0, vitest_1.it)("calls model and accumulates history", async () => {
|
|
53
|
+
const s = makeSession();
|
|
54
|
+
const res = (await s.runTurn({ messages: [userMsg] }));
|
|
55
|
+
(0, vitest_1.expect)(res).toEqual({
|
|
56
|
+
stopReason: "end_turn",
|
|
57
|
+
messages: [{ role: "assistant", content: "hi" }],
|
|
58
|
+
});
|
|
59
|
+
(0, vitest_1.expect)(s.history).toHaveLength(2); // user + assistant
|
|
60
|
+
});
|
|
61
|
+
(0, vitest_1.it)("executes trusted tools inline and loops", async () => {
|
|
62
|
+
const agent = makeAgent();
|
|
63
|
+
const agentConfig = {
|
|
64
|
+
name: "test-agent",
|
|
65
|
+
tools: [{ name: "echo", trust: true }],
|
|
66
|
+
};
|
|
67
|
+
// First call returns tool_use, second returns end_turn
|
|
68
|
+
const model = makeModel({
|
|
69
|
+
call: vitest_1.vi
|
|
70
|
+
.fn()
|
|
71
|
+
.mockResolvedValueOnce({
|
|
72
|
+
stopReason: "tool_use",
|
|
73
|
+
messages: [
|
|
74
|
+
{
|
|
75
|
+
role: "assistant",
|
|
76
|
+
content: [
|
|
77
|
+
{ type: "tool_use", toolCallId: "c1", name: "echo", input: { msg: "hi" } },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
})
|
|
82
|
+
.mockResolvedValueOnce({
|
|
83
|
+
stopReason: "end_turn",
|
|
84
|
+
messages: [{ role: "assistant", content: "done" }],
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
const s = new session_1.Session("s", agent, model, agentConfig);
|
|
88
|
+
const res = await s.runTurn({ messages: [userMsg] });
|
|
89
|
+
(0, vitest_1.expect)(res.stopReason).toBe("end_turn");
|
|
90
|
+
(0, vitest_1.expect)(model.call).toHaveBeenCalledTimes(2);
|
|
91
|
+
});
|
|
92
|
+
(0, vitest_1.it)("stops and returns tool_use when tool is untrusted", async () => {
|
|
93
|
+
const agent = makeAgent();
|
|
94
|
+
const agentConfig = {
|
|
95
|
+
name: "test-agent",
|
|
96
|
+
tools: [{ name: "echo", trust: false }],
|
|
97
|
+
};
|
|
98
|
+
const model = makeModel({
|
|
99
|
+
call: vitest_1.vi.fn().mockResolvedValue({
|
|
100
|
+
stopReason: "tool_use",
|
|
101
|
+
messages: [
|
|
102
|
+
{
|
|
103
|
+
role: "assistant",
|
|
104
|
+
content: [{ type: "tool_use", toolCallId: "c1", name: "echo", input: { msg: "hi" } }],
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
}),
|
|
108
|
+
});
|
|
109
|
+
const s = new session_1.Session("s", agent, model, agentConfig);
|
|
110
|
+
const res = (await s.runTurn({ messages: [userMsg] }));
|
|
111
|
+
(0, vitest_1.expect)(res.stopReason).toBe("tool_use");
|
|
112
|
+
(0, vitest_1.expect)(model.call).toHaveBeenCalledTimes(1);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
(0, vitest_1.describe)("runTurn (delta mode)", () => {
|
|
116
|
+
(0, vitest_1.it)("yields turn_start and turn_stop events", async () => {
|
|
117
|
+
const s = makeSession();
|
|
118
|
+
const result = s.runTurn({ messages: [userMsg], stream: "delta" });
|
|
119
|
+
const events = [];
|
|
120
|
+
for await (const e of result)
|
|
121
|
+
events.push(e);
|
|
122
|
+
(0, vitest_1.expect)(events[0].event).toBe("turn_start");
|
|
123
|
+
(0, vitest_1.expect)(events.at(-1).event).toBe("turn_stop");
|
|
124
|
+
});
|
|
125
|
+
(0, vitest_1.it)("executes trusted tools inline and loops in delta mode", async () => {
|
|
126
|
+
const agent = makeAgent();
|
|
127
|
+
const agentConfig = {
|
|
128
|
+
name: "test-agent",
|
|
129
|
+
tools: [{ name: "echo", trust: true }],
|
|
130
|
+
};
|
|
131
|
+
const model = makeModel({
|
|
132
|
+
stream: vitest_1.vi
|
|
133
|
+
.fn()
|
|
134
|
+
.mockImplementationOnce(async function* () {
|
|
135
|
+
yield {
|
|
136
|
+
event: "tool_call",
|
|
137
|
+
toolCallId: "c1",
|
|
138
|
+
name: "echo",
|
|
139
|
+
input: { msg: "hi" },
|
|
140
|
+
};
|
|
141
|
+
yield { event: "turn_stop", stopReason: "tool_use" };
|
|
142
|
+
})
|
|
143
|
+
.mockImplementationOnce(async function* () {
|
|
144
|
+
yield { event: "text_delta", delta: "done" };
|
|
145
|
+
yield { event: "turn_stop", stopReason: "end_turn" };
|
|
146
|
+
}),
|
|
147
|
+
});
|
|
148
|
+
const s = new session_1.Session("s", agent, model, agentConfig);
|
|
149
|
+
const events = [];
|
|
150
|
+
for await (const e of s.runTurn({
|
|
151
|
+
messages: [userMsg],
|
|
152
|
+
stream: "delta",
|
|
153
|
+
})) {
|
|
154
|
+
events.push(e);
|
|
155
|
+
}
|
|
156
|
+
(0, vitest_1.expect)(events.some((e) => e.event === "tool_result")).toBe(true);
|
|
157
|
+
(0, vitest_1.expect)(model.stream).toHaveBeenCalledTimes(2);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
(0, vitest_1.describe)("runTurn (message mode)", () => {
|
|
161
|
+
(0, vitest_1.it)("yields turn_start, text, and turn_stop events", async () => {
|
|
162
|
+
const s = makeSession();
|
|
163
|
+
const result = s.runTurn({ messages: [userMsg], stream: "message" });
|
|
164
|
+
const events = [];
|
|
165
|
+
for await (const e of result)
|
|
166
|
+
events.push(e);
|
|
167
|
+
(0, vitest_1.expect)(events[0].event).toBe("turn_start");
|
|
168
|
+
(0, vitest_1.expect)(events.some((e) => e.event === "text")).toBe(true);
|
|
169
|
+
(0, vitest_1.expect)(events.at(-1).event).toBe("turn_stop");
|
|
170
|
+
});
|
|
171
|
+
(0, vitest_1.it)("yields thinking event for thinking blocks", async () => {
|
|
172
|
+
const model = makeModel({
|
|
173
|
+
call: vitest_1.vi.fn().mockResolvedValue({
|
|
174
|
+
stopReason: "end_turn",
|
|
175
|
+
messages: [{ role: "assistant", content: [{ type: "thinking", thinking: "hmm" }] }],
|
|
176
|
+
}),
|
|
177
|
+
});
|
|
178
|
+
const s = makeSession({ name: "test-agent" }, model);
|
|
179
|
+
const events = [];
|
|
180
|
+
for await (const e of s.runTurn({
|
|
181
|
+
messages: [userMsg],
|
|
182
|
+
stream: "message",
|
|
183
|
+
})) {
|
|
184
|
+
events.push(e);
|
|
185
|
+
}
|
|
186
|
+
(0, vitest_1.expect)(events.some((e) => e.event === "thinking")).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
(0, vitest_1.it)("executes trusted tools inline and loops in message mode", async () => {
|
|
189
|
+
const agent = makeAgent();
|
|
190
|
+
const agentConfig = {
|
|
191
|
+
name: "test-agent",
|
|
192
|
+
tools: [{ name: "echo", trust: true }],
|
|
193
|
+
};
|
|
194
|
+
const model = makeModel({
|
|
195
|
+
call: vitest_1.vi
|
|
196
|
+
.fn()
|
|
197
|
+
.mockResolvedValueOnce({
|
|
198
|
+
stopReason: "tool_use",
|
|
199
|
+
messages: [
|
|
200
|
+
{
|
|
201
|
+
role: "assistant",
|
|
202
|
+
content: [
|
|
203
|
+
{ type: "tool_use", toolCallId: "c1", name: "echo", input: { msg: "hi" } },
|
|
204
|
+
],
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
})
|
|
208
|
+
.mockResolvedValueOnce({
|
|
209
|
+
stopReason: "end_turn",
|
|
210
|
+
messages: [{ role: "assistant", content: "done" }],
|
|
211
|
+
}),
|
|
212
|
+
});
|
|
213
|
+
const s = new session_1.Session("s", agent, model, agentConfig);
|
|
214
|
+
const events = [];
|
|
215
|
+
for await (const e of s.runTurn({
|
|
216
|
+
messages: [userMsg],
|
|
217
|
+
stream: "message",
|
|
218
|
+
})) {
|
|
219
|
+
events.push(e);
|
|
220
|
+
}
|
|
221
|
+
(0, vitest_1.expect)(events.some((e) => e.event === "tool_result")).toBe(true);
|
|
222
|
+
(0, vitest_1.expect)(model.call).toHaveBeenCalledTimes(2);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
(0, vitest_1.describe)("runTurn with tool_permission", () => {
|
|
226
|
+
(0, vitest_1.it)("resolves granted tool_permission into tool result", async () => {
|
|
227
|
+
const agent = makeAgent();
|
|
228
|
+
const agentConfig = { name: "test-agent" };
|
|
229
|
+
// Seed history with an assistant message containing a tool_use
|
|
230
|
+
const model = makeModel();
|
|
231
|
+
const s = new session_1.Session("s", agent, model, agentConfig);
|
|
232
|
+
s.history = [
|
|
233
|
+
{ role: "user", content: "hi" },
|
|
234
|
+
{
|
|
235
|
+
role: "assistant",
|
|
236
|
+
content: [{ type: "tool_use", toolCallId: "c1", name: "echo", input: { msg: "hello" } }],
|
|
237
|
+
},
|
|
238
|
+
];
|
|
239
|
+
await s.runTurn({
|
|
240
|
+
messages: [{ role: "tool_permission", toolCallId: "c1", granted: true }],
|
|
241
|
+
});
|
|
242
|
+
// model.call should have received a tool result message
|
|
243
|
+
const callArg = model.call.mock.calls[0][0];
|
|
244
|
+
const toolResult = callArg.find((m) => m.role === "tool");
|
|
245
|
+
(0, vitest_1.expect)(toolResult).toBeDefined();
|
|
246
|
+
(0, vitest_1.expect)(toolResult.content).toBe('"hello"');
|
|
247
|
+
});
|
|
248
|
+
(0, vitest_1.it)("resolves denied tool_permission with denial message", async () => {
|
|
249
|
+
const agent = makeAgent();
|
|
250
|
+
const model = makeModel();
|
|
251
|
+
const s = new session_1.Session("s", agent, model, { name: "test-agent" });
|
|
252
|
+
s.history = [
|
|
253
|
+
{
|
|
254
|
+
role: "assistant",
|
|
255
|
+
content: [{ type: "tool_use", toolCallId: "c2", name: "echo", input: { msg: "x" } }],
|
|
256
|
+
},
|
|
257
|
+
];
|
|
258
|
+
await s.runTurn({
|
|
259
|
+
messages: [{ role: "tool_permission", toolCallId: "c2", granted: false, reason: "no" }],
|
|
260
|
+
});
|
|
261
|
+
const callArg = model.call.mock.calls[0][0];
|
|
262
|
+
const toolResult = callArg.find((m) => m.role === "tool");
|
|
263
|
+
(0, vitest_1.expect)(toolResult.content).toBe("Tool use denied: no");
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
(0, vitest_1.describe)("applySessionOverrides via runTurn", () => {
|
|
267
|
+
(0, vitest_1.it)("overrides clientTools when req.tools is provided", async () => {
|
|
268
|
+
const s = makeSession();
|
|
269
|
+
const newTool = { name: "x", description: "", inputSchema: {} };
|
|
270
|
+
await s.runTurn({ messages: [userMsg], tools: [newTool] });
|
|
271
|
+
(0, vitest_1.expect)(s.clientTools).toEqual([newTool]);
|
|
272
|
+
});
|
|
273
|
+
(0, vitest_1.it)("overrides agentConfig.tools when req.agent.tools is provided", async () => {
|
|
274
|
+
const s = makeSession();
|
|
275
|
+
await s.runTurn({ messages: [userMsg], agent: { tools: [{ name: "echo", trust: true }] } });
|
|
276
|
+
(0, vitest_1.expect)(s.agentConfig.tools).toEqual([{ name: "echo", trust: true }]);
|
|
277
|
+
});
|
|
278
|
+
(0, vitest_1.it)("overrides agentConfig.options when req.agent.options is provided", async () => {
|
|
279
|
+
const s = makeSession({ name: "test-agent", options: { model: "gpt-4" } });
|
|
280
|
+
await s.runTurn({ messages: [userMsg], agent: { options: { model: "gpt-5" } } });
|
|
281
|
+
(0, vitest_1.expect)(s.agentConfig.options).toEqual({ model: "gpt-5" });
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
(0, vitest_1.describe)("runNewSession", () => {
|
|
285
|
+
(0, vitest_1.it)("returns CreateSessionResponse with sessionId (none mode)", async () => {
|
|
286
|
+
const s = makeSession();
|
|
287
|
+
const res = await s.runNewSession({ agent: { name: "test-agent" }, messages: [userMsg] });
|
|
288
|
+
(0, vitest_1.expect)(res.sessionId).toBe("sess-1");
|
|
289
|
+
});
|
|
290
|
+
(0, vitest_1.it)("yields session_start event first (delta mode)", async () => {
|
|
291
|
+
const s = makeSession();
|
|
292
|
+
const result = s.runNewSession({
|
|
293
|
+
agent: { name: "test-agent" },
|
|
294
|
+
messages: [userMsg],
|
|
295
|
+
stream: "delta",
|
|
296
|
+
});
|
|
297
|
+
const events = [];
|
|
298
|
+
for await (const e of result)
|
|
299
|
+
events.push(e);
|
|
300
|
+
(0, vitest_1.expect)(events[0]).toEqual({ event: "session_start", sessionId: "sess-1" });
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentapplicationprotocol/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "AAP server",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"aap",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
24
24
|
"dist/src",
|
|
25
|
+
"!dist/src/examples",
|
|
25
26
|
"!dist/src/**/*.test.*"
|
|
26
27
|
],
|
|
27
28
|
"main": "dist/src/index.js",
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
"ai": "^6.0.141",
|
|
31
32
|
"hono": "^4.12.8",
|
|
32
33
|
"zod": "^4.3.6",
|
|
33
|
-
"@agentapplicationprotocol/core": "0.
|
|
34
|
+
"@agentapplicationprotocol/core": "0.6.0"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
36
37
|
"@ai-sdk/openai": "^3.0.48",
|