@andreprado/agentkit 0.1.0-alpha.3 → 0.1.0-alpha.4
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 +9 -3
- package/docs/guides/add-tool.md +1 -1
- package/docs/guides/channels-implementation-map.md +243 -0
- package/docs/guides/create-agent.md +22 -1
- package/docs/guides/prepare-deploy.md +17 -6
- package/docs/guides/run-evals.md +28 -2
- package/docs/guides/security-rules.md +1 -1
- package/docs/guides/use-provider.md +16 -2
- package/docs/llms-full.txt +67 -36
- package/docs/llms.txt +13 -3
- package/package.json +1 -1
- package/src/cli/index.ts +318 -14
- package/src/providers/test.ts +51 -0
- package/src/runtime/chat.ts +59 -4
- package/src/runtime/dev-server.ts +154 -16
- package/src/runtime/runtime-contract.ts +16 -2
- package/src/runtime/targets/cloudflare/build.ts +79 -4
- package/src/storage/sqlite.ts +2 -2
- package/src/templates/blank.ts +41 -0
- package/src/templates/dentista.ts +988 -0
- package/src/templates/index.ts +2 -0
- package/src/templates/support.ts +39 -0
package/src/runtime/chat.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getProviderAdapter } from "../providers";
|
|
2
2
|
import type { AgentMessage, ProviderRunResult } from "../providers";
|
|
3
3
|
import type { ToolRuntimeContext } from "../index";
|
|
4
|
-
import type { PersistedChatRun } from "../storage/sqlite";
|
|
4
|
+
import type { ConversationRecord, ConversationSummary, PersistedChatRun } from "../storage/sqlite";
|
|
5
5
|
import type { LoadedAgentCapsule } from "./config";
|
|
6
6
|
import { loadAgentCapsule } from "./config";
|
|
7
7
|
import { openPreparedCapsuleStore } from "./database";
|
|
@@ -11,6 +11,7 @@ import { createToolRuntime } from "./tools";
|
|
|
11
11
|
|
|
12
12
|
export type RunAgentMessageOptions = {
|
|
13
13
|
message: string;
|
|
14
|
+
conversationId?: string;
|
|
14
15
|
signal?: AbortSignal;
|
|
15
16
|
runtime?: Partial<ToolRuntimeContext>;
|
|
16
17
|
};
|
|
@@ -36,12 +37,14 @@ export async function runAgentMessage(
|
|
|
36
37
|
|
|
37
38
|
const adapter = getProviderAdapter(capsule.config.provider);
|
|
38
39
|
const env = await loadCapsuleEnv(capsule.root);
|
|
39
|
-
const messages: AgentMessage[] = [{ role: "user", content }];
|
|
40
40
|
const { store } = await openPreparedCapsuleStore(capsule);
|
|
41
|
-
const
|
|
41
|
+
const conversationId = normalizeConversationId(options.conversationId);
|
|
42
|
+
const { conversation, previousMessages } = resolveConversation(store, {
|
|
42
43
|
agentName: capsule.config.name,
|
|
43
|
-
|
|
44
|
+
conversationId,
|
|
45
|
+
firstMessage: content,
|
|
44
46
|
});
|
|
47
|
+
const messages: AgentMessage[] = [...previousMessages, { role: "user", content }];
|
|
45
48
|
const userMessage = store.appendMessage({
|
|
46
49
|
conversationId: conversation.id,
|
|
47
50
|
role: "user",
|
|
@@ -116,6 +119,58 @@ export async function runAgentMessage(
|
|
|
116
119
|
}
|
|
117
120
|
}
|
|
118
121
|
|
|
122
|
+
function resolveConversation(
|
|
123
|
+
store: Awaited<ReturnType<typeof openPreparedCapsuleStore>>["store"],
|
|
124
|
+
input: {
|
|
125
|
+
agentName: string;
|
|
126
|
+
conversationId?: string;
|
|
127
|
+
firstMessage: string;
|
|
128
|
+
},
|
|
129
|
+
): { conversation: ConversationSummary; previousMessages: AgentMessage[] } {
|
|
130
|
+
if (input.conversationId) {
|
|
131
|
+
const existing = store.getConversation(input.conversationId);
|
|
132
|
+
|
|
133
|
+
if (existing) {
|
|
134
|
+
return {
|
|
135
|
+
conversation: existing,
|
|
136
|
+
previousMessages: toProviderMessages(existing),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const conversation = store.createConversation({
|
|
142
|
+
id: input.conversationId,
|
|
143
|
+
agentName: input.agentName,
|
|
144
|
+
title: titleFromMessage(input.firstMessage),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
conversation,
|
|
149
|
+
previousMessages: [],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function toProviderMessages(conversation: ConversationRecord): AgentMessage[] {
|
|
154
|
+
return conversation.messages.map((message) => ({
|
|
155
|
+
role: message.role,
|
|
156
|
+
content: message.content,
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function normalizeConversationId(value: string | undefined): string | undefined {
|
|
161
|
+
if (value === undefined) {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const conversationId = value.trim();
|
|
166
|
+
|
|
167
|
+
if (!conversationId) {
|
|
168
|
+
throw new AgentKitError("validation_error", "conversationId must be a non-empty string when provided.");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return conversationId;
|
|
172
|
+
}
|
|
173
|
+
|
|
119
174
|
function withToolVisibilityInstructions(instructions: string, tools: LoadedAgentCapsule["config"]["tools"] = []): string {
|
|
120
175
|
const internalTools = tools.filter((tool) => tool.visibility === "internal").map((tool) => tool.name).sort();
|
|
121
176
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
3
|
import { createServer as createHttpServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
3
4
|
import { createServer as createNetServer } from "node:net";
|
|
@@ -21,12 +22,18 @@ import { runToolFromCwd } from "./tool-runner";
|
|
|
21
22
|
export type AgentDevServerOptions = {
|
|
22
23
|
port?: number;
|
|
23
24
|
hostname?: string;
|
|
25
|
+
basePort?: number;
|
|
24
26
|
};
|
|
25
27
|
|
|
26
28
|
export type AgentDevServer = {
|
|
27
29
|
capsule: LoadedAgentCapsule;
|
|
28
30
|
hostname: string;
|
|
31
|
+
requestedPort: number | null;
|
|
29
32
|
port: number;
|
|
33
|
+
portConflict: {
|
|
34
|
+
port: number;
|
|
35
|
+
agent?: string;
|
|
36
|
+
} | null;
|
|
30
37
|
urls: {
|
|
31
38
|
chat: string;
|
|
32
39
|
api: string;
|
|
@@ -46,34 +53,83 @@ export async function startAgentDevServer(
|
|
|
46
53
|
): Promise<AgentDevServer> {
|
|
47
54
|
const capsule = await loadAgentCapsule(cwd);
|
|
48
55
|
const hostname = options.hostname ?? DEFAULT_HOSTNAME;
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
void handleNodeRequest(capsule, incoming, outgoing, channelDedupeKeys);
|
|
53
|
-
});
|
|
54
|
-
const actualPort = await listen(server, hostname, port);
|
|
56
|
+
const requestedPort = options.port ?? options.basePort ?? DEFAULT_PORT;
|
|
57
|
+
const autoPort = options.port === undefined;
|
|
58
|
+
const start = await listenForDevServer(capsule, hostname, requestedPort, autoPort);
|
|
55
59
|
|
|
56
|
-
if (actualPort === undefined) {
|
|
60
|
+
if (start.actualPort === undefined) {
|
|
57
61
|
throw new AgentKitError("runtime_error", "Could not determine the AgentKit dev server port.");
|
|
58
62
|
}
|
|
59
63
|
|
|
64
|
+
const actualPort = start.actualPort;
|
|
65
|
+
|
|
60
66
|
return {
|
|
61
67
|
capsule,
|
|
62
68
|
hostname,
|
|
69
|
+
requestedPort: options.port === undefined || options.port === 0 ? null : options.port,
|
|
63
70
|
port: actualPort,
|
|
71
|
+
portConflict: start.portConflict,
|
|
64
72
|
urls: {
|
|
65
73
|
chat: `http://${hostname}:${actualPort}`,
|
|
66
74
|
api: `http://${hostname}:${actualPort}/v1/chat`,
|
|
67
75
|
inspect: `http://${hostname}:${actualPort}/_agentkit`,
|
|
68
76
|
},
|
|
69
77
|
storage: capsule.storagePath ? relativePath(capsule.root, capsule.storagePath) : null,
|
|
70
|
-
server,
|
|
78
|
+
server: start.server,
|
|
71
79
|
async stop() {
|
|
72
|
-
await closeServer(server);
|
|
80
|
+
await closeServer(start.server);
|
|
73
81
|
},
|
|
74
82
|
};
|
|
75
83
|
}
|
|
76
84
|
|
|
85
|
+
async function listenForDevServer(
|
|
86
|
+
capsule: LoadedAgentCapsule,
|
|
87
|
+
hostname: string,
|
|
88
|
+
requestedPort: number,
|
|
89
|
+
autoPort: boolean,
|
|
90
|
+
): Promise<{ server: Server; actualPort: number; portConflict: AgentDevServer["portConflict"] }> {
|
|
91
|
+
if (requestedPort === 0) {
|
|
92
|
+
const server = createDevHttpServer(capsule);
|
|
93
|
+
const actualPort = await listen(server, hostname, await findAvailablePort());
|
|
94
|
+
return { server, actualPort, portConflict: null };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let firstConflict: AgentDevServer["portConflict"] = null;
|
|
98
|
+
const lastPort = autoPort ? requestedPort + 50 : requestedPort;
|
|
99
|
+
|
|
100
|
+
for (let port = requestedPort; port <= lastPort; port += 1) {
|
|
101
|
+
const server = createDevHttpServer(capsule);
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const actualPort = await listen(server, hostname, port);
|
|
105
|
+
return { server, actualPort, portConflict: port === requestedPort ? null : firstConflict };
|
|
106
|
+
} catch (error) {
|
|
107
|
+
await closeServerAfterListenError(server);
|
|
108
|
+
|
|
109
|
+
if (!autoPort || !isAddressInUseError(error)) {
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
firstConflict ??= {
|
|
114
|
+
port,
|
|
115
|
+
agent: await detectAgentKitOccupant(hostname, port),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
throw new AgentKitError(
|
|
121
|
+
"runtime_error",
|
|
122
|
+
`Could not allocate a local AgentKit dev server port after ${requestedPort}-${lastPort}.`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function createDevHttpServer(capsule: LoadedAgentCapsule): Server {
|
|
127
|
+
const channelDedupeKeys = new Set<string>();
|
|
128
|
+
return createHttpServer((incoming, outgoing) => {
|
|
129
|
+
void handleNodeRequest(capsule, incoming, outgoing, channelDedupeKeys);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
77
133
|
async function handleNodeRequest(
|
|
78
134
|
capsule: LoadedAgentCapsule,
|
|
79
135
|
incoming: IncomingMessage,
|
|
@@ -163,9 +219,10 @@ async function handleDevServerRequest(
|
|
|
163
219
|
|
|
164
220
|
if (request.method === "POST" && (route === "/v1/chat" || route === "/chat")) {
|
|
165
221
|
const body = await readJsonBody(request);
|
|
166
|
-
const message =
|
|
222
|
+
const { message, conversationId } = readChatRequest(body);
|
|
167
223
|
const result = await runAgentMessage(capsule, {
|
|
168
224
|
message,
|
|
225
|
+
conversationId,
|
|
169
226
|
signal: request.signal,
|
|
170
227
|
});
|
|
171
228
|
|
|
@@ -280,6 +337,7 @@ async function handlePortableChannel(
|
|
|
280
337
|
|
|
281
338
|
const result = await runAgentMessage(capsule, {
|
|
282
339
|
message: event.message.content,
|
|
340
|
+
conversationId: channelConversationId(channel.name, event.externalIdentity.key),
|
|
283
341
|
signal: request.signal,
|
|
284
342
|
});
|
|
285
343
|
deliveries.push({
|
|
@@ -377,20 +435,61 @@ async function readJsonBody(request: Request): Promise<unknown> {
|
|
|
377
435
|
}
|
|
378
436
|
}
|
|
379
437
|
|
|
380
|
-
function
|
|
381
|
-
if (!isRecord(body)
|
|
438
|
+
function readChatRequest(body: unknown): { message: string; conversationId?: string } {
|
|
439
|
+
if (!isRecord(body)) {
|
|
440
|
+
throw new AgentKitError(
|
|
441
|
+
"validation_error",
|
|
442
|
+
'Expected body shape: {"message":{"role":"user","content":"hello"},"conversationId":"optional-id"}.',
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const message = readUserMessage(body.message);
|
|
447
|
+
const conversationId = readOptionalConversationId(body.conversationId);
|
|
448
|
+
|
|
449
|
+
return { message, conversationId };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function readUserMessage(message: unknown): string {
|
|
453
|
+
if (typeof message === "string") {
|
|
454
|
+
const content = message.trim();
|
|
455
|
+
|
|
456
|
+
if (!content) {
|
|
457
|
+
throw new AgentKitError("validation_error", "message must be a non-empty string.");
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return content;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (!isRecord(message)) {
|
|
382
464
|
throw new AgentKitError("validation_error", 'Expected body shape: {"message":{"role":"user","content":"hello"}}.');
|
|
383
465
|
}
|
|
384
466
|
|
|
385
|
-
if (
|
|
467
|
+
if (message.role !== "user") {
|
|
386
468
|
throw new AgentKitError("validation_error", 'message.role must be "user".');
|
|
387
469
|
}
|
|
388
470
|
|
|
389
|
-
if (typeof
|
|
471
|
+
if (typeof message.content !== "string" || message.content.trim().length === 0) {
|
|
390
472
|
throw new AgentKitError("validation_error", "message.content must be a non-empty string.");
|
|
391
473
|
}
|
|
392
474
|
|
|
393
|
-
return
|
|
475
|
+
return message.content.trim();
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function readOptionalConversationId(value: unknown): string | undefined {
|
|
479
|
+
if (value === undefined || value === null) {
|
|
480
|
+
return undefined;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
484
|
+
throw new AgentKitError("validation_error", "conversationId must be a non-empty string when provided.");
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return value.trim();
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function channelConversationId(channelName: string, externalIdentityKey: string): string {
|
|
491
|
+
const hash = createHash("sha256").update(`${channelName}:${externalIdentityKey}`).digest("hex").slice(0, 32);
|
|
492
|
+
return `chn_${hash}`;
|
|
394
493
|
}
|
|
395
494
|
|
|
396
495
|
function readToolInput(body: unknown): unknown {
|
|
@@ -805,6 +904,7 @@ function renderChatApp(capsule: LoadedAgentCapsule, baseUrl: string, env: Record
|
|
|
805
904
|
const textarea = document.querySelector("#message");
|
|
806
905
|
const messages = document.querySelector("#messages");
|
|
807
906
|
const button = form.querySelector("button");
|
|
907
|
+
let conversationId = null;
|
|
808
908
|
|
|
809
909
|
function addMessage(role, content, className = "") {
|
|
810
910
|
const wrapper = document.createElement("article");
|
|
@@ -830,7 +930,7 @@ function renderChatApp(capsule: LoadedAgentCapsule, baseUrl: string, env: Record
|
|
|
830
930
|
const response = await fetch("/v1/chat", {
|
|
831
931
|
method: "POST",
|
|
832
932
|
headers: { "content-type": "application/json" },
|
|
833
|
-
body: JSON.stringify({ message: { role: "user", content } }),
|
|
933
|
+
body: JSON.stringify({ message: { role: "user", content }, conversationId }),
|
|
834
934
|
});
|
|
835
935
|
const data = await response.json();
|
|
836
936
|
|
|
@@ -838,6 +938,7 @@ function renderChatApp(capsule: LoadedAgentCapsule, baseUrl: string, env: Record
|
|
|
838
938
|
throw new Error(data.error?.message || "Chat request failed.");
|
|
839
939
|
}
|
|
840
940
|
|
|
941
|
+
conversationId = data.conversationId || conversationId;
|
|
841
942
|
addMessage(data.message.role, data.message.content);
|
|
842
943
|
} catch (error) {
|
|
843
944
|
addMessage("error", error instanceof Error ? error.message : String(error), "error");
|
|
@@ -881,6 +982,19 @@ function listen(server: Server, hostname: string, port: number): Promise<number>
|
|
|
881
982
|
});
|
|
882
983
|
}
|
|
883
984
|
|
|
985
|
+
async function closeServerAfterListenError(server: Server): Promise<void> {
|
|
986
|
+
if (!server.listening) {
|
|
987
|
+
try {
|
|
988
|
+
server.close();
|
|
989
|
+
} catch {
|
|
990
|
+
// The server never reached the listening state.
|
|
991
|
+
}
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
await closeServer(server);
|
|
996
|
+
}
|
|
997
|
+
|
|
884
998
|
function closeServer(server: Server): Promise<void> {
|
|
885
999
|
return new Promise((resolvePromise, reject) => {
|
|
886
1000
|
server.close((error) => {
|
|
@@ -894,6 +1008,30 @@ function closeServer(server: Server): Promise<void> {
|
|
|
894
1008
|
});
|
|
895
1009
|
}
|
|
896
1010
|
|
|
1011
|
+
function isAddressInUseError(error: unknown): boolean {
|
|
1012
|
+
return error instanceof Error && "code" in error && error.code === "EADDRINUSE";
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
async function detectAgentKitOccupant(hostname: string, port: number): Promise<string | undefined> {
|
|
1016
|
+
const controller = new AbortController();
|
|
1017
|
+
const timeout = setTimeout(() => controller.abort(), 300);
|
|
1018
|
+
|
|
1019
|
+
try {
|
|
1020
|
+
const response = await fetch(`http://${hostname}:${port}/_agentkit`, { signal: controller.signal });
|
|
1021
|
+
const payload = await response.json().catch(() => null);
|
|
1022
|
+
|
|
1023
|
+
if (payload && typeof payload === "object" && "agent" in payload && typeof payload.agent === "string") {
|
|
1024
|
+
return payload.agent;
|
|
1025
|
+
}
|
|
1026
|
+
} catch {
|
|
1027
|
+
return undefined;
|
|
1028
|
+
} finally {
|
|
1029
|
+
clearTimeout(timeout);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
return undefined;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
897
1035
|
function findAvailablePort(): Promise<number> {
|
|
898
1036
|
return new Promise((resolve, reject) => {
|
|
899
1037
|
const server = createNetServer();
|
|
@@ -42,17 +42,31 @@ export async function assertRuntimeContract(
|
|
|
42
42
|
const chat = await fetchJson(`${baseUrl}/chat`, {
|
|
43
43
|
method: "POST",
|
|
44
44
|
headers: { "content-type": "application/json" },
|
|
45
|
-
body: JSON.stringify({ message: { role: "user", content: "
|
|
45
|
+
body: JSON.stringify({ message: { role: "user", content: "my name is Ada" } }),
|
|
46
46
|
});
|
|
47
47
|
expect(chat.status).toBe(200);
|
|
48
48
|
expect(chat.body).toMatchObject({
|
|
49
49
|
message: {
|
|
50
50
|
role: "assistant",
|
|
51
|
-
content: "Echo:
|
|
51
|
+
content: "Echo: my name is Ada",
|
|
52
52
|
},
|
|
53
53
|
});
|
|
54
54
|
expect(typeof chat.body.conversationId).toBe("string");
|
|
55
55
|
|
|
56
|
+
const continuedChat = await fetchJson(`${baseUrl}/chat`, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: { "content-type": "application/json" },
|
|
59
|
+
body: JSON.stringify({ message: "what is my name?", conversationId: chat.body.conversationId }),
|
|
60
|
+
});
|
|
61
|
+
expect(continuedChat.status).toBe(200);
|
|
62
|
+
expect(continuedChat.body).toMatchObject({
|
|
63
|
+
conversationId: chat.body.conversationId,
|
|
64
|
+
message: {
|
|
65
|
+
role: "assistant",
|
|
66
|
+
content: "Your name is Ada.",
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
56
70
|
const tool = await fetchJson(`${baseUrl}/tools/${encodeURIComponent(options.tool.name)}`, {
|
|
57
71
|
method: "POST",
|
|
58
72
|
headers: { "content-type": "application/json" },
|
|
@@ -542,9 +542,33 @@ export class AgentKitConversationStore extends DurableObject {
|
|
|
542
542
|
return this.persistMessages(await request.json());
|
|
543
543
|
}
|
|
544
544
|
|
|
545
|
+
if (request.method === "GET" && url.pathname === "/messages") {
|
|
546
|
+
return this.getMessages(url.searchParams.get("conversationId"));
|
|
547
|
+
}
|
|
548
|
+
|
|
545
549
|
return Response.json({ error: { code: "not_found", message: "Unknown Durable Object route." } }, { status: 404 });
|
|
546
550
|
}
|
|
547
551
|
|
|
552
|
+
getMessages(conversationId) {
|
|
553
|
+
if (!conversationId) {
|
|
554
|
+
return Response.json({ messages: [] });
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const rows = this.sql.exec(
|
|
558
|
+
"SELECT role, content FROM messages WHERE conversation_id = ? ORDER BY rowid ASC",
|
|
559
|
+
String(conversationId),
|
|
560
|
+
).toArray();
|
|
561
|
+
|
|
562
|
+
return Response.json({
|
|
563
|
+
messages: rows
|
|
564
|
+
.filter((row) => row.role === "user" || row.role === "assistant")
|
|
565
|
+
.map((row) => ({
|
|
566
|
+
role: String(row.role),
|
|
567
|
+
content: String(row.content),
|
|
568
|
+
})),
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
|
|
548
572
|
persistMessages(input) {
|
|
549
573
|
const now = new Date().toISOString();
|
|
550
574
|
const conversationId = String(input.conversationId || crypto.randomUUID());
|
|
@@ -915,7 +939,15 @@ async function handleChat(request, env) {
|
|
|
915
939
|
);
|
|
916
940
|
}
|
|
917
941
|
|
|
918
|
-
|
|
942
|
+
let conversationId;
|
|
943
|
+
|
|
944
|
+
try {
|
|
945
|
+
conversationId = normalizeConversationId(body.conversationId) ?? crypto.randomUUID();
|
|
946
|
+
} catch (error) {
|
|
947
|
+
return hostedErrorResponse(error);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
const previousMessages = await loadConversationMessages(env, conversationId).catch(() => []);
|
|
919
951
|
let content;
|
|
920
952
|
const toolCalls = [];
|
|
921
953
|
|
|
@@ -943,7 +975,7 @@ async function handleChat(request, env) {
|
|
|
943
975
|
|
|
944
976
|
if (content === undefined) {
|
|
945
977
|
try {
|
|
946
|
-
content = await callHostedProvider(env, message, request.signal);
|
|
978
|
+
content = await callHostedProvider(env, [...previousMessages, { role: "user", content: message }], request.signal);
|
|
947
979
|
} catch (error) {
|
|
948
980
|
await persistFailure(env, conversationId, message, error);
|
|
949
981
|
return hostedErrorResponse(error);
|
|
@@ -982,7 +1014,7 @@ function isRecord(value) {
|
|
|
982
1014
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
983
1015
|
}
|
|
984
1016
|
|
|
985
|
-
async function callHostedProvider(env,
|
|
1017
|
+
async function callHostedProvider(env, messages, signal) {
|
|
986
1018
|
const adapter = getProviderAdapter(manifest.provider);
|
|
987
1019
|
const result = await adapter.run({
|
|
988
1020
|
agent: {
|
|
@@ -991,7 +1023,7 @@ async function callHostedProvider(env, message, signal) {
|
|
|
991
1023
|
provider: manifest.provider,
|
|
992
1024
|
},
|
|
993
1025
|
instructions: withToolVisibilityInstructions(instructions, hostedTools),
|
|
994
|
-
messages
|
|
1026
|
+
messages,
|
|
995
1027
|
tools: hostedTools,
|
|
996
1028
|
toolRuntime: createHostedProviderToolRuntime(env, signal),
|
|
997
1029
|
env,
|
|
@@ -1570,6 +1602,49 @@ async function persist(env, input) {
|
|
|
1570
1602
|
return response.json();
|
|
1571
1603
|
}
|
|
1572
1604
|
|
|
1605
|
+
async function loadConversationMessages(env, conversationId) {
|
|
1606
|
+
if (!env.AGENTKIT_CONVERSATIONS || !conversationId) {
|
|
1607
|
+
return [];
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
const id = env.AGENTKIT_CONVERSATIONS.idFromName(conversationId);
|
|
1611
|
+
const stub = env.AGENTKIT_CONVERSATIONS.get(id);
|
|
1612
|
+
const response = await stub.fetch(
|
|
1613
|
+
\`https://agentkit.internal/messages?conversationId=\${encodeURIComponent(conversationId)}\`,
|
|
1614
|
+
);
|
|
1615
|
+
|
|
1616
|
+
if (!response.ok) {
|
|
1617
|
+
return [];
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
const payload = await response.json();
|
|
1621
|
+
return Array.isArray(payload.messages)
|
|
1622
|
+
? payload.messages.filter((message) =>
|
|
1623
|
+
message &&
|
|
1624
|
+
typeof message === "object" &&
|
|
1625
|
+
(message.role === "user" || message.role === "assistant") &&
|
|
1626
|
+
typeof message.content === "string"
|
|
1627
|
+
)
|
|
1628
|
+
: [];
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
function normalizeConversationId(value) {
|
|
1632
|
+
if (value === undefined || value === null) {
|
|
1633
|
+
return null;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
if (typeof value !== "string") {
|
|
1637
|
+
throw agentKitError("validation_error", "conversationId must be a non-empty string when provided.");
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
const conversationId = value.trim();
|
|
1641
|
+
if (!conversationId) {
|
|
1642
|
+
throw agentKitError("validation_error", "conversationId must be a non-empty string when provided.");
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
return conversationId;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1573
1648
|
function agentKitError(code, message) {
|
|
1574
1649
|
const error = new Error(message);
|
|
1575
1650
|
error.code = code;
|
package/src/storage/sqlite.ts
CHANGED
|
@@ -218,10 +218,10 @@ export class SqliteAgentKitStore {
|
|
|
218
218
|
this.db.close();
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
createConversation(input: { agentName: string; title: string | null; now?: string }): ConversationSummary {
|
|
221
|
+
createConversation(input: { id?: string; agentName: string; title: string | null; now?: string }): ConversationSummary {
|
|
222
222
|
return this.sqlite(() => {
|
|
223
223
|
const now = input.now ?? new Date().toISOString();
|
|
224
|
-
const id = randomId();
|
|
224
|
+
const id = input.id ?? randomId();
|
|
225
225
|
|
|
226
226
|
this.db
|
|
227
227
|
.prepare(
|
package/src/templates/blank.ts
CHANGED
|
@@ -65,6 +65,7 @@ node_modules/
|
|
|
65
65
|
contents: `# Optional: only needed after switching agentkit.config.ts to a Pi-backed real provider.
|
|
66
66
|
OPENAI_API_KEY=
|
|
67
67
|
ANTHROPIC_API_KEY=
|
|
68
|
+
OPENROUTER_API_KEY=
|
|
68
69
|
`,
|
|
69
70
|
},
|
|
70
71
|
{
|
|
@@ -151,6 +152,7 @@ Start building immediately:
|
|
|
151
152
|
- Edit \`prompts/instructions.md\` for the agent behavior.
|
|
152
153
|
- Edit \`agentkit.config.ts\` for provider, tools, secrets, access, and storage.
|
|
153
154
|
- Add TypeScript tools under \`tools/\` when the requested agent needs actions or external data.
|
|
155
|
+
- Do not wait for a wizard or recipe. AgentKit provides the scaffold and contract; you decide the implementation from the owner's brief.
|
|
154
156
|
- Ask follow-up questions only when missing information blocks a safe local implementation.
|
|
155
157
|
- State assumptions in the final response.
|
|
156
158
|
|
|
@@ -170,6 +172,13 @@ Start building immediately:
|
|
|
170
172
|
- \`npm run agentkit -- env list\`: list local secret names without printing values.
|
|
171
173
|
- \`npm run agentkit -- docs full\`: print the full AgentKit contract path.
|
|
172
174
|
|
|
175
|
+
## Testing With A UI
|
|
176
|
+
|
|
177
|
+
- Local UI: run \`npm run dev\`, open the printed \`Chat:\` URL, and tell the owner the exact URL.
|
|
178
|
+
- Hosted UI: after \`npm run agentkit -- deploy\`, run \`npm run agentkit -- chat-ui --deploy\`, open the printed \`Chat:\` URL, and tell the owner it is connected to the hosted deploy.
|
|
179
|
+
- \`test/fake\` is deterministic. It is useful for scaffold checks, direct tool checks, and fake-provider evals, but it does not validate natural conversation quality.
|
|
180
|
+
- Before claiming real conversation behavior is tested, ask the owner which provider to use: OpenRouter, OpenAI, Anthropic, or another supported provider. Do not choose for them.
|
|
181
|
+
|
|
173
182
|
This blank capsule starts with the built-in \`test/fake\` provider, so local chat works without secrets or internet access. It is also deploy-ready by default: the user can edit the prompt, add tools, and run \`npm run agentkit -- deploy\`.
|
|
174
183
|
|
|
175
184
|
## Local Runtime Contract
|
|
@@ -213,11 +222,13 @@ Use \`ctx.db\` as the canonical helper. \`ctx.database\` and \`ctx.storage.sql\`
|
|
|
213
222
|
- Put production secret values into managed secrets with \`npm run agentkit -- secret set <NAME> --from-local-env\`, \`--from-env\`, or \`--stdin\`, not into committed files or shell history.
|
|
214
223
|
- The user should not choose a deploy target, create hosted databases, create buckets, copy production secrets into this capsule, or run operator/admin commands.
|
|
215
224
|
- Hosted deploy writes the local chat/UI deploy access token to \`.agentkit/chat-access-token.json\`. Create extra client-facing tokens with \`npm run agentkit -- access token create <name> --out <path>\`.
|
|
225
|
+
- Use \`npm run agentkit -- deploy --smoke "hello"\` or \`npm run agentkit -- deploy smoke --message "hello"\` for an official hosted chat smoke check.
|
|
216
226
|
|
|
217
227
|
## Rules
|
|
218
228
|
|
|
219
229
|
- Keep required local secret names in \`.env.schema\` and values in ignored \`.env\`. AgentKit local commands load \`.env\` directly.
|
|
220
230
|
- Real providers are resolved by AgentKit through the internal Pi SDK backend; keep project code on \`@andreprado/agentkit\`.
|
|
231
|
+
- The owner must choose the real provider before you switch from \`test/fake\`. Update \`agentkit.config.ts\`, \`.env.schema\`, and local/hosted secrets after that choice.
|
|
221
232
|
- Do not commit \`.env\` or \`.agentkit/\`.
|
|
222
233
|
- Edit the agent contract in \`agentkit.config.ts\`.
|
|
223
234
|
- Edit instructions in \`prompts/instructions.md\`.
|
|
@@ -243,6 +254,7 @@ Turn the request into a working local capsule:
|
|
|
243
254
|
- Update \`agentkit.config.ts\` when tools, secrets, provider, or access rules change.
|
|
244
255
|
- Add TypeScript tools under \`tools/\` for real actions or external data.
|
|
245
256
|
- Keep the first version runnable with \`test/fake\` unless the owner explicitly asks for a real provider.
|
|
257
|
+
- Do not use a wizard or recipe. Build the capsule directly from the scaffold, the AgentKit contract, and the owner's brief.
|
|
246
258
|
- Make practical assumptions and list them in your final response.
|
|
247
259
|
- Ask follow-up questions only when missing information blocks a safe local implementation.
|
|
248
260
|
|
|
@@ -254,6 +266,8 @@ npm run agentkit -- inspect
|
|
|
254
266
|
npm run chat -- --message "hello"
|
|
255
267
|
\`\`\`
|
|
256
268
|
|
|
269
|
+
\`test/fake\` proves the scaffold and deterministic tool paths. It does not prove natural conversation quality.
|
|
270
|
+
|
|
257
271
|
\`agentkit new\` installs dependencies by default. Run \`npm install\` only if the capsule was created with \`--no-install\`, install failed, or \`node_modules\` was deleted.
|
|
258
272
|
|
|
259
273
|
Set local development secrets without opening code:
|
|
@@ -264,6 +278,27 @@ npm run agentkit -- inspect
|
|
|
264
278
|
npm run chat -- --message "hello"
|
|
265
279
|
\`\`\`
|
|
266
280
|
|
|
281
|
+
## Testing With A UI
|
|
282
|
+
|
|
283
|
+
Local UI:
|
|
284
|
+
|
|
285
|
+
\`\`\`sh
|
|
286
|
+
npm run dev
|
|
287
|
+
\`\`\`
|
|
288
|
+
|
|
289
|
+
Open the printed \`Chat:\` URL and tell the owner the exact URL.
|
|
290
|
+
|
|
291
|
+
Hosted deploy UI:
|
|
292
|
+
|
|
293
|
+
\`\`\`sh
|
|
294
|
+
npm run agentkit -- deploy
|
|
295
|
+
npm run agentkit -- chat-ui --deploy
|
|
296
|
+
\`\`\`
|
|
297
|
+
|
|
298
|
+
Open the printed \`Chat:\` URL and tell the owner this local UI is connected to the hosted deploy.
|
|
299
|
+
|
|
300
|
+
Before claiming real conversation behavior has been tested, ask the owner which provider to use: OpenRouter, OpenAI, Anthropic, or another supported provider. Do not choose for them. After they choose, update \`agentkit.config.ts\`, \`.env.schema\`, local secrets, hosted secrets if deploying, then rerun chat/UI checks.
|
|
301
|
+
|
|
267
302
|
If you add a tool, also run a fake-provider tool smoke test:
|
|
268
303
|
|
|
269
304
|
\`\`\`sh
|
|
@@ -287,6 +322,7 @@ This capsule is hosted-deploy ready by default.
|
|
|
287
322
|
5. Put production secrets into managed secrets with \`npm run agentkit -- secret set <NAME> --from-local-env\`, \`--from-env\`, or \`--stdin\`.
|
|
288
323
|
6. Run \`npm run agentkit -- deploy\`.
|
|
289
324
|
7. Run \`npm run agentkit -- chat-ui --deploy\` to test the hosted agent through a local UI using the auto-created \`.agentkit/chat-access-token.json\`. Create extra client-facing deploy access tokens with \`npm run agentkit -- access token create <name> --out <path>\` when a separate website or app needs its own credential.
|
|
325
|
+
8. Use \`npm run agentkit -- deploy --smoke "hello"\` during deploy or \`npm run agentkit -- deploy smoke --message "hello"\` afterward for an official hosted smoke check.
|
|
290
326
|
|
|
291
327
|
AgentKit owns hosted infrastructure and production secrets. Do not put production secret values in this capsule. Do not run operator/admin commands from a user capsule.
|
|
292
328
|
`,
|
|
@@ -303,6 +339,7 @@ Use AgentKit conventions when editing this project.
|
|
|
303
339
|
- Local runtime state lives in \`.agentkit/\` and should not be committed.
|
|
304
340
|
- The default provider is \`test/fake\`, which needs no secrets.
|
|
305
341
|
- Real providers run through AgentKit's internal Pi SDK backend.
|
|
342
|
+
- Ask the owner which real provider to use before switching from \`test/fake\`; do not choose OpenRouter, OpenAI, or Anthropic automatically.
|
|
306
343
|
- Production secrets must be managed secrets, not committed files.
|
|
307
344
|
- Keep required local secret names in \`.env.schema\` and values in ignored \`.env\`. AgentKit local commands load \`.env\` directly.
|
|
308
345
|
- Treat the owner's natural-language request as the brief and start implementing inside this capsule.
|
|
@@ -328,6 +365,10 @@ When a real provider or tool needs a local development secret, keep the required
|
|
|
328
365
|
|
|
329
366
|
\`npm run dev\` runs the whole capsule locally. It should expose local chat, API, inspect, and storage endpoints.
|
|
330
367
|
|
|
368
|
+
For UI testing, run \`npm run dev\` and open the printed \`Chat:\` URL. After hosted deploy, run \`npm run agentkit -- chat-ui --deploy\` and open its printed \`Chat:\` URL.
|
|
369
|
+
|
|
370
|
+
\`test/fake\` does not validate real conversation quality. The owner must choose OpenRouter, OpenAI, Anthropic, or another supported provider before real model behavior is tested.
|
|
371
|
+
|
|
331
372
|
## Files
|
|
332
373
|
|
|
333
374
|
- \`agentkit.config.ts\`: agent contract.
|