@andreprado/agentkit 0.1.0-alpha.2 → 0.1.0-alpha.20
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 +67 -6
- package/docs/guides/add-channel.md +118 -7
- package/docs/guides/add-knowledge.md +144 -0
- package/docs/guides/add-managed-composio.md +163 -0
- package/docs/guides/add-tool.md +1 -1
- package/docs/guides/channel-security.md +97 -32
- package/docs/guides/connect-discord.md +178 -0
- package/docs/guides/connect-slack.md +126 -0
- package/docs/guides/connect-telegram.md +78 -1
- package/docs/guides/connect-whatsapp-zapster.md +112 -8
- package/docs/guides/create-agent.md +45 -4
- package/docs/guides/debug-channel.md +147 -0
- package/docs/guides/improve-from-production.md +151 -0
- package/docs/guides/prepare-deploy.md +47 -17
- package/docs/guides/replay-production-traces.md +72 -0
- package/docs/guides/run-evals.md +147 -20
- package/docs/guides/security-rules.md +7 -6
- package/docs/guides/send-feedback.md +135 -0
- package/docs/guides/use-provider.md +27 -3
- package/docs/llms-full.txt +303 -55
- package/docs/llms.txt +57 -7
- package/package.json +2 -5
- package/src/cli/args.ts +57 -0
- package/src/cli/cloud-client.ts +377 -0
- package/src/cli/commands/channels.ts +1315 -0
- package/src/cli/commands/feedback.ts +438 -0
- package/src/cli/commands/knowledge.ts +136 -0
- package/src/cli/commands/transcribe.ts +171 -0
- package/src/cli/constants.ts +4 -0
- package/src/cli/deploy-chat-ui.ts +535 -0
- package/src/cli/deploy-readiness.ts +481 -0
- package/src/cli/flags.ts +162 -0
- package/src/cli/help.ts +236 -0
- package/src/cli/index.ts +1167 -1005
- package/src/cli/process.ts +31 -0
- package/src/cloud/artifact.ts +139 -0
- package/src/cloud/client.ts +80 -0
- package/src/cloud/contracts.ts +63 -0
- package/src/cloud/index.ts +3 -0
- package/src/create-project.ts +21 -6
- package/src/index.ts +479 -7
- package/src/providers/pi.ts +70 -16
- package/src/providers/test.ts +88 -1
- package/src/providers/types.ts +7 -0
- package/src/runtime/channel-buffer.ts +30 -0
- package/src/runtime/channel-test-harness.ts +8 -1
- package/src/runtime/channels/discord.ts +896 -0
- package/src/runtime/channels/slack.ts +646 -0
- package/src/runtime/channels/telegram.ts +466 -23
- package/src/runtime/channels/whatsapp-meta.ts +9 -0
- package/src/runtime/channels/whatsapp-zapster.ts +677 -40
- package/src/runtime/channels.ts +86 -3
- package/src/runtime/chat.ts +130 -38
- package/src/runtime/config.ts +483 -18
- package/src/runtime/core/manifest.ts +103 -5
- package/src/runtime/core/targets.ts +5 -5
- package/src/runtime/database.ts +93 -2
- package/src/runtime/db-commands.ts +9 -0
- package/src/runtime/deploy-readiness.ts +46 -4
- package/src/runtime/deploy.ts +1 -1
- package/src/runtime/dev-server.ts +759 -41
- package/src/runtime/env.ts +8 -3
- package/src/runtime/evals.ts +589 -43
- package/src/runtime/improve.ts +868 -0
- package/src/runtime/inspect.ts +194 -4
- package/src/runtime/integrations/composio.ts +423 -0
- package/src/runtime/knowledge/chunk.ts +333 -0
- package/src/runtime/knowledge/config.ts +135 -0
- package/src/runtime/knowledge/embeddings.ts +133 -0
- package/src/runtime/knowledge/ingest.ts +521 -0
- package/src/runtime/knowledge/prompt-policy.ts +30 -0
- package/src/runtime/knowledge/retrieve.ts +303 -0
- package/src/runtime/knowledge/schema.ts +100 -0
- package/src/runtime/knowledge/tool.ts +64 -0
- package/src/runtime/knowledge/vector.ts +258 -0
- package/src/runtime/prompt-context.ts +141 -0
- package/src/runtime/runtime-contract.ts +86 -8
- package/src/runtime/skills.ts +95 -0
- package/src/runtime/spec.ts +152 -0
- package/src/runtime/sync.ts +144 -0
- package/src/runtime/targets/cloudflare/build.ts +1430 -185
- package/src/runtime/targets/container/server.ts +1 -1
- package/src/runtime/targets/vps/deploy.ts +26 -9
- package/src/runtime/tool-runner.ts +9 -1
- package/src/runtime/tools.ts +128 -2
- package/src/runtime/traces.ts +41 -0
- package/src/runtime/transcription.ts +483 -0
- package/src/storage/sqlite.ts +149 -3
- package/src/templates/blank.ts +76 -17
- package/src/templates/dentista.ts +1011 -0
- package/src/templates/index.ts +2 -0
- package/src/templates/skills/agentkit-build-agent/SKILL.md +52 -0
- package/src/templates/skills/agentkit-build-agent/templates/appointment-intake.instructions.md +21 -0
- package/src/templates/skills/agentkit-build-agent/templates/sales-qualifier.instructions.md +17 -0
- package/src/templates/skills/agentkit-build-agent/templates/support-agent.instructions.md +16 -0
- package/src/templates/skills/agentkit-capsule/SKILL.md +70 -0
- package/src/templates/skills/agentkit-capsule/references/docs-router.md +15 -0
- package/src/templates/skills/agentkit-channels/SKILL.md +104 -0
- package/src/templates/skills/agentkit-channels/references/channel-buffering.md +65 -0
- package/src/templates/skills/agentkit-channels/references/channel-debugging.md +66 -0
- package/src/templates/skills/agentkit-channels/references/discord.md +93 -0
- package/src/templates/skills/agentkit-channels/references/slack.md +56 -0
- package/src/templates/skills/agentkit-channels/references/telegram.md +72 -0
- package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +77 -0
- package/src/templates/skills/agentkit-database/SKILL.md +45 -0
- package/src/templates/skills/agentkit-database/templates/appointments.schema.sql +15 -0
- package/src/templates/skills/agentkit-database/templates/leads.schema.sql +17 -0
- package/src/templates/skills/agentkit-deploy/SKILL.md +50 -0
- package/src/templates/skills/agentkit-evals/SKILL.md +109 -0
- package/src/templates/skills/agentkit-evals/templates/multi-turn.eval.md +29 -0
- package/src/templates/skills/agentkit-evals/templates/no-leak.eval.md +18 -0
- package/src/templates/skills/agentkit-evals/templates/smoke.eval.md +18 -0
- package/src/templates/skills/agentkit-evals/templates/tool-call.eval.md +27 -0
- package/src/templates/skills/agentkit-improve/SKILL.md +86 -0
- package/src/templates/skills/agentkit-improve/references/replay-side-effects.md +18 -0
- package/src/templates/skills/agentkit-improve/references/trace-packets.md +22 -0
- package/src/templates/skills/agentkit-improve/templates/regression.eval.md +18 -0
- package/src/templates/skills/agentkit-integrations/SKILL.md +76 -0
- package/src/templates/skills/agentkit-knowledge/SKILL.md +43 -0
- package/src/templates/skills/agentkit-knowledge/templates/faq.md +14 -0
- package/src/templates/skills/agentkit-knowledge/templates/policies.md +14 -0
- package/src/templates/skills/agentkit-knowledge/templates/prices.csv +3 -0
- package/src/templates/skills/agentkit-prompts/SKILL.md +47 -0
- package/src/templates/skills/agentkit-prompts/templates/knowledge-grounded-faq.instructions.md +11 -0
- package/src/templates/skills/agentkit-provider/SKILL.md +60 -0
- package/src/templates/skills/agentkit-security/SKILL.md +56 -0
- package/src/templates/skills/agentkit-tools/SKILL.md +37 -0
- package/src/templates/skills/agentkit-tools/examples/database-write.tool.md +35 -0
- package/src/templates/skills/agentkit-tools/examples/eval-safe-external-action.tool.md +37 -0
- package/src/templates/skills/agentkit-tools/examples/lookup-order.tool.md +46 -0
- package/src/templates/skills/agentkit-troubleshooting/SKILL.md +76 -0
- package/src/templates/support.ts +77 -18
- package/docs/guides/channels-production-handoff.md +0 -99
- package/docs/portable-deploy-release-checklist.md +0 -41
- package/src/runtime/targets/cloudflare/deploy.ts +0 -5475
|
@@ -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";
|
|
@@ -6,27 +7,39 @@ import { Readable } from "node:stream";
|
|
|
6
7
|
|
|
7
8
|
import { openCapsuleStore, type ConversationRecord } from "../storage/sqlite";
|
|
8
9
|
import type { AgentChannel, ChannelProvider, ChannelType } from "../index";
|
|
9
|
-
import type { ChannelAdapter, RawWebhookEvent } from "./channels";
|
|
10
|
+
import type { ChannelAdapter, NormalizedChannelMessage, RawWebhookEvent } from "./channels";
|
|
11
|
+
import { discordChannelAdapter } from "./channels/discord";
|
|
12
|
+
import { slackChannelAdapter } from "./channels/slack";
|
|
10
13
|
import { telegramChannelAdapter } from "./channels/telegram";
|
|
11
14
|
import { websiteChannelAdapter } from "./channels/website";
|
|
12
15
|
import { metaWhatsappChannelAdapter } from "./channels/whatsapp-meta";
|
|
13
16
|
import { zapsterWhatsappChannelAdapter } from "./channels/whatsapp-zapster";
|
|
14
17
|
import { runAgentMessage } from "./chat";
|
|
18
|
+
import { isChannelBufferEnabled, renderBufferedChannelMessage, resolveChannelBuffer } from "./channel-buffer";
|
|
15
19
|
import { type LoadedAgentCapsule, loadAgentCapsule } from "./config";
|
|
16
20
|
import { AgentKitError, isAgentKitError } from "./errors";
|
|
17
21
|
import { loadCapsuleEnv } from "./env";
|
|
18
22
|
import { buildInspectState } from "./inspect";
|
|
23
|
+
import { syncConfiguredKnowledgeSources } from "./knowledge/ingest";
|
|
19
24
|
import { runToolFromCwd } from "./tool-runner";
|
|
25
|
+
import { resolveTranscriptionConfig, transcribeAudio } from "./transcription";
|
|
20
26
|
|
|
21
27
|
export type AgentDevServerOptions = {
|
|
22
28
|
port?: number;
|
|
23
29
|
hostname?: string;
|
|
30
|
+
basePort?: number;
|
|
31
|
+
access?: AgentDevServerAccessPolicy;
|
|
24
32
|
};
|
|
25
33
|
|
|
26
34
|
export type AgentDevServer = {
|
|
27
35
|
capsule: LoadedAgentCapsule;
|
|
28
36
|
hostname: string;
|
|
37
|
+
requestedPort: number | null;
|
|
29
38
|
port: number;
|
|
39
|
+
portConflict: {
|
|
40
|
+
port: number;
|
|
41
|
+
agent?: string;
|
|
42
|
+
} | null;
|
|
30
43
|
urls: {
|
|
31
44
|
chat: string;
|
|
32
45
|
api: string;
|
|
@@ -39,50 +52,133 @@ export type AgentDevServer = {
|
|
|
39
52
|
|
|
40
53
|
const DEFAULT_PORT = 4123;
|
|
41
54
|
const DEFAULT_HOSTNAME = "localhost";
|
|
55
|
+
const CHANNEL_DEDUPE_TTL_MS = 6 * 60 * 60 * 1000;
|
|
56
|
+
const CHANNEL_DEDUPE_MAX_KEYS = 10_000;
|
|
57
|
+
|
|
58
|
+
export type AgentDevServerAccessPolicy = "local" | "enforce";
|
|
59
|
+
|
|
60
|
+
type DevHttpServerOptions = {
|
|
61
|
+
access: AgentDevServerAccessPolicy;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
type DevChannelRuntimeState = {
|
|
65
|
+
dedupeKeys: BoundedDedupeCache;
|
|
66
|
+
buffers: Map<string, DevChannelBuffer>;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
type DevChannelBuffer = {
|
|
70
|
+
conversationId: string;
|
|
71
|
+
messages: Array<{
|
|
72
|
+
content: string;
|
|
73
|
+
receivedAt: string;
|
|
74
|
+
}>;
|
|
75
|
+
firstReceivedMs: number;
|
|
76
|
+
lastReceivedMs: number;
|
|
77
|
+
timer?: ReturnType<typeof setTimeout>;
|
|
78
|
+
};
|
|
42
79
|
|
|
43
80
|
export async function startAgentDevServer(
|
|
44
81
|
cwd = process.cwd(),
|
|
45
82
|
options: AgentDevServerOptions = {},
|
|
46
83
|
): Promise<AgentDevServer> {
|
|
47
84
|
const capsule = await loadAgentCapsule(cwd);
|
|
85
|
+
await syncConfiguredKnowledgeSources(capsule);
|
|
48
86
|
const hostname = options.hostname ?? DEFAULT_HOSTNAME;
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
|
|
87
|
+
const requestedPort = options.port ?? options.basePort ?? DEFAULT_PORT;
|
|
88
|
+
const autoPort = options.port === undefined;
|
|
89
|
+
const start = await listenForDevServer(capsule, hostname, requestedPort, autoPort, {
|
|
90
|
+
access: options.access ?? "local",
|
|
53
91
|
});
|
|
54
|
-
const actualPort = await listen(server, hostname, port);
|
|
55
92
|
|
|
56
|
-
if (actualPort === undefined) {
|
|
93
|
+
if (start.actualPort === undefined) {
|
|
57
94
|
throw new AgentKitError("runtime_error", "Could not determine the AgentKit dev server port.");
|
|
58
95
|
}
|
|
59
96
|
|
|
97
|
+
const actualPort = start.actualPort;
|
|
98
|
+
|
|
60
99
|
return {
|
|
61
100
|
capsule,
|
|
62
101
|
hostname,
|
|
102
|
+
requestedPort: options.port === undefined || options.port === 0 ? null : options.port,
|
|
63
103
|
port: actualPort,
|
|
104
|
+
portConflict: start.portConflict,
|
|
64
105
|
urls: {
|
|
65
106
|
chat: `http://${hostname}:${actualPort}`,
|
|
66
107
|
api: `http://${hostname}:${actualPort}/v1/chat`,
|
|
67
108
|
inspect: `http://${hostname}:${actualPort}/_agentkit`,
|
|
68
109
|
},
|
|
69
110
|
storage: capsule.storagePath ? relativePath(capsule.root, capsule.storagePath) : null,
|
|
70
|
-
server,
|
|
111
|
+
server: start.server,
|
|
71
112
|
async stop() {
|
|
72
|
-
await closeServer(server);
|
|
113
|
+
await closeServer(start.server);
|
|
73
114
|
},
|
|
74
115
|
};
|
|
75
116
|
}
|
|
76
117
|
|
|
77
|
-
async function
|
|
118
|
+
async function listenForDevServer(
|
|
78
119
|
capsule: LoadedAgentCapsule,
|
|
120
|
+
hostname: string,
|
|
121
|
+
requestedPort: number,
|
|
122
|
+
autoPort: boolean,
|
|
123
|
+
serverOptions: DevHttpServerOptions,
|
|
124
|
+
): Promise<{ server: Server; actualPort: number; portConflict: AgentDevServer["portConflict"] }> {
|
|
125
|
+
if (requestedPort === 0) {
|
|
126
|
+
const server = createDevHttpServer(capsule, serverOptions);
|
|
127
|
+
const actualPort = await listen(server, hostname, await findAvailablePort());
|
|
128
|
+
return { server, actualPort, portConflict: null };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let firstConflict: AgentDevServer["portConflict"] = null;
|
|
132
|
+
const lastPort = autoPort ? requestedPort + 50 : requestedPort;
|
|
133
|
+
|
|
134
|
+
for (let port = requestedPort; port <= lastPort; port += 1) {
|
|
135
|
+
const server = createDevHttpServer(capsule, serverOptions);
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const actualPort = await listen(server, hostname, port);
|
|
139
|
+
return { server, actualPort, portConflict: port === requestedPort ? null : firstConflict };
|
|
140
|
+
} catch (error) {
|
|
141
|
+
await closeServerAfterListenError(server);
|
|
142
|
+
|
|
143
|
+
if (!autoPort || !isAddressInUseError(error)) {
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
firstConflict ??= {
|
|
148
|
+
port,
|
|
149
|
+
agent: await detectAgentKitOccupant(hostname, port),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
throw new AgentKitError(
|
|
155
|
+
"runtime_error",
|
|
156
|
+
`Could not allocate a local AgentKit dev server port after ${requestedPort}-${lastPort}.`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function createDevHttpServer(capsule: LoadedAgentCapsule, options: DevHttpServerOptions): Server {
|
|
161
|
+
const channelRuntime: DevChannelRuntimeState = {
|
|
162
|
+
dedupeKeys: new BoundedDedupeCache(CHANNEL_DEDUPE_MAX_KEYS, CHANNEL_DEDUPE_TTL_MS),
|
|
163
|
+
buffers: new Map<string, DevChannelBuffer>(),
|
|
164
|
+
};
|
|
165
|
+
const capsuleRoot = capsule.root;
|
|
166
|
+
return createHttpServer((incoming, outgoing) => {
|
|
167
|
+
void handleNodeRequest(capsuleRoot, incoming, outgoing, channelRuntime, options);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function handleNodeRequest(
|
|
172
|
+
capsuleRoot: string,
|
|
79
173
|
incoming: IncomingMessage,
|
|
80
174
|
outgoing: ServerResponse,
|
|
81
|
-
|
|
175
|
+
channelRuntime: DevChannelRuntimeState,
|
|
176
|
+
options: DevHttpServerOptions,
|
|
82
177
|
): Promise<void> {
|
|
83
178
|
try {
|
|
179
|
+
const capsule = await loadAgentCapsule(capsuleRoot);
|
|
84
180
|
const request = nodeRequestToFetchRequest(incoming);
|
|
85
|
-
const response = await handleDevServerRequest(capsule, request,
|
|
181
|
+
const response = await handleDevServerRequest(capsule, request, channelRuntime, options);
|
|
86
182
|
await writeFetchResponse(outgoing, response);
|
|
87
183
|
} catch (error) {
|
|
88
184
|
await writeFetchResponse(outgoing, jsonError(error, statusForError(error)));
|
|
@@ -141,31 +237,48 @@ async function writeFetchResponse(outgoing: ServerResponse, response: Response):
|
|
|
141
237
|
async function handleDevServerRequest(
|
|
142
238
|
capsule: LoadedAgentCapsule,
|
|
143
239
|
request: Request,
|
|
144
|
-
|
|
240
|
+
channelRuntime: DevChannelRuntimeState,
|
|
241
|
+
options: DevHttpServerOptions,
|
|
145
242
|
): Promise<Response> {
|
|
146
243
|
try {
|
|
147
244
|
const url = new URL(request.url);
|
|
148
245
|
const route = normalizePath(url.pathname);
|
|
149
246
|
|
|
247
|
+
if (request.method === "GET" && route === "/health") {
|
|
248
|
+
return jsonResponse({ status: "ok", agent: capsule.config.name });
|
|
249
|
+
}
|
|
250
|
+
|
|
150
251
|
if (request.method === "GET" && route === "/") {
|
|
252
|
+
const auth = await authorizeDevRoute(capsule, request, options, "chat");
|
|
253
|
+
if (auth) {
|
|
254
|
+
return auth;
|
|
255
|
+
}
|
|
256
|
+
|
|
151
257
|
const env = await loadCapsuleEnv(capsule.root);
|
|
152
258
|
return htmlResponse(renderChatApp(capsule, url.origin, env));
|
|
153
259
|
}
|
|
154
260
|
|
|
155
261
|
if (request.method === "GET" && route === "/_agentkit") {
|
|
262
|
+
const auth = await authorizeDevRoute(capsule, request, options, "admin");
|
|
263
|
+
if (auth) {
|
|
264
|
+
return auth;
|
|
265
|
+
}
|
|
266
|
+
|
|
156
267
|
const env = await loadCapsuleEnv(capsule.root);
|
|
157
268
|
return jsonResponse(buildInspectState(capsule, env));
|
|
158
269
|
}
|
|
159
270
|
|
|
160
|
-
if (request.method === "GET" && route === "/health") {
|
|
161
|
-
return jsonResponse({ status: "ok", agent: capsule.config.name });
|
|
162
|
-
}
|
|
163
|
-
|
|
164
271
|
if (request.method === "POST" && (route === "/v1/chat" || route === "/chat")) {
|
|
165
272
|
const body = await readJsonBody(request);
|
|
166
|
-
const
|
|
273
|
+
const auth = await authorizeDevRoute(capsule, request, options, "chat", body);
|
|
274
|
+
if (auth) {
|
|
275
|
+
return auth;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const { message, conversationId } = readChatRequest(body);
|
|
167
279
|
const result = await runAgentMessage(capsule, {
|
|
168
280
|
message,
|
|
281
|
+
conversationId,
|
|
169
282
|
signal: request.signal,
|
|
170
283
|
});
|
|
171
284
|
|
|
@@ -181,6 +294,11 @@ async function handleDevServerRequest(
|
|
|
181
294
|
const toolMatch = route.match(/^\/tools\/([^/]+)$/);
|
|
182
295
|
|
|
183
296
|
if (request.method === "POST" && toolMatch) {
|
|
297
|
+
const auth = await authorizeDevRoute(capsule, request, options, "admin");
|
|
298
|
+
if (auth) {
|
|
299
|
+
return auth;
|
|
300
|
+
}
|
|
301
|
+
|
|
184
302
|
const body = await readJsonBody(request);
|
|
185
303
|
const result = await runToolFromCwd(capsule.root, {
|
|
186
304
|
name: decodeURIComponent(toolMatch[1]),
|
|
@@ -194,13 +312,18 @@ async function handleDevServerRequest(
|
|
|
194
312
|
const fileMatch = route.match(/^\/files\/(.+)$/);
|
|
195
313
|
|
|
196
314
|
if (fileMatch) {
|
|
315
|
+
const auth = await authorizeDevRoute(capsule, request, options, "admin");
|
|
316
|
+
if (auth) {
|
|
317
|
+
return auth;
|
|
318
|
+
}
|
|
319
|
+
|
|
197
320
|
return await handleLocalFile(capsule, request, decodeURIComponent(fileMatch[1]));
|
|
198
321
|
}
|
|
199
322
|
|
|
200
|
-
const channelMatch = route.match(/^\/channels\/([^/]+)\/(website|telegram|whatsapp)\/([^/]+)\/webhook$/);
|
|
323
|
+
const channelMatch = route.match(/^\/channels\/([^/]+)\/(website|telegram|whatsapp|discord|slack)\/([^/]+)\/webhook$/);
|
|
201
324
|
|
|
202
325
|
if (channelMatch) {
|
|
203
|
-
return await handlePortableChannel(capsule, request,
|
|
326
|
+
return await handlePortableChannel(capsule, request, channelRuntime, {
|
|
204
327
|
name: decodeURIComponent(channelMatch[1]),
|
|
205
328
|
type: channelMatch[2] as ChannelType,
|
|
206
329
|
provider: decodeURIComponent(channelMatch[3]) as ChannelProvider,
|
|
@@ -208,25 +331,149 @@ async function handleDevServerRequest(
|
|
|
208
331
|
}
|
|
209
332
|
|
|
210
333
|
if (request.method === "GET" && route === "/v1/conversations") {
|
|
334
|
+
const auth = await authorizeDevRoute(capsule, request, options, "admin");
|
|
335
|
+
if (auth) {
|
|
336
|
+
return auth;
|
|
337
|
+
}
|
|
338
|
+
|
|
211
339
|
return jsonResponse({ conversations: await listConversations(capsule) });
|
|
212
340
|
}
|
|
213
341
|
|
|
214
342
|
const conversationMatch = route.match(/^\/v1\/conversations\/([^/]+)$/);
|
|
215
343
|
|
|
216
344
|
if (request.method === "GET" && conversationMatch) {
|
|
345
|
+
const auth = await authorizeDevRoute(capsule, request, options, "admin");
|
|
346
|
+
if (auth) {
|
|
347
|
+
return auth;
|
|
348
|
+
}
|
|
349
|
+
|
|
217
350
|
return jsonResponse(await getConversation(capsule, decodeURIComponent(conversationMatch[1])));
|
|
218
351
|
}
|
|
219
352
|
|
|
353
|
+
const conversationTraceMatch = route.match(/^\/v1\/conversations\/([^/]+)\/trace$/);
|
|
354
|
+
|
|
355
|
+
if (request.method === "GET" && conversationTraceMatch) {
|
|
356
|
+
const auth = await authorizeDevRoute(capsule, request, options, "admin");
|
|
357
|
+
if (auth) {
|
|
358
|
+
return auth;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return jsonResponse(await getConversationTrace(capsule, decodeURIComponent(conversationTraceMatch[1])));
|
|
362
|
+
}
|
|
363
|
+
|
|
220
364
|
return jsonError(new AgentKitError("not_found", `Route ${request.method} ${route} was not found.`), 404);
|
|
221
365
|
} catch (error) {
|
|
222
366
|
return jsonError(error, statusForError(error));
|
|
223
367
|
}
|
|
224
368
|
}
|
|
225
369
|
|
|
370
|
+
async function authorizeDevRoute(
|
|
371
|
+
capsule: LoadedAgentCapsule,
|
|
372
|
+
request: Request,
|
|
373
|
+
options: DevHttpServerOptions,
|
|
374
|
+
kind: "chat" | "admin",
|
|
375
|
+
body?: unknown,
|
|
376
|
+
): Promise<Response | null> {
|
|
377
|
+
if (options.access === "local") {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const env = await loadCapsuleEnv(capsule.root);
|
|
382
|
+
|
|
383
|
+
if (kind === "admin") {
|
|
384
|
+
return authorizeBearerSecret(request, body, env.AGENTKIT_PRIVATE_TOKEN, {
|
|
385
|
+
missingCode: "deploy_not_ready",
|
|
386
|
+
missingMessage: "Privileged AgentKit endpoints require AGENTKIT_PRIVATE_TOKEN to be set.",
|
|
387
|
+
unauthorizedMessage: "Missing or invalid AgentKit admin token.",
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (capsule.config.access.mode === "public") {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (capsule.config.access.mode === "private") {
|
|
396
|
+
return authorizeBearerSecret(request, body, env.AGENTKIT_PRIVATE_TOKEN, {
|
|
397
|
+
missingCode: "deploy_not_ready",
|
|
398
|
+
missingMessage: "Private deploy requires AGENTKIT_PRIVATE_TOKEN to be set.",
|
|
399
|
+
unauthorizedMessage: "Missing or invalid deploy access token.",
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return authorizeBearerSecret(request, body, env.AGENTKIT_ACCESS_TOKEN, {
|
|
404
|
+
missingCode: "deploy_not_ready",
|
|
405
|
+
missingMessage: "Token deploy requires AGENTKIT_ACCESS_TOKEN to be set.",
|
|
406
|
+
unauthorizedMessage: "Missing or invalid deploy access token.",
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function authorizeBearerSecret(
|
|
411
|
+
request: Request,
|
|
412
|
+
body: unknown,
|
|
413
|
+
secret: string | undefined,
|
|
414
|
+
messages: { missingCode: string; missingMessage: string; unauthorizedMessage: string },
|
|
415
|
+
): Response | null {
|
|
416
|
+
if (!secret) {
|
|
417
|
+
return jsonResponse(
|
|
418
|
+
{
|
|
419
|
+
error: {
|
|
420
|
+
code: messages.missingCode,
|
|
421
|
+
message: messages.missingMessage,
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
{ status: 503 },
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (readRequestAccessToken(request, body) !== secret) {
|
|
429
|
+
return jsonResponse(
|
|
430
|
+
{
|
|
431
|
+
error: {
|
|
432
|
+
code: "unauthorized",
|
|
433
|
+
message: messages.unauthorizedMessage,
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
{ status: 401 },
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function readRequestAccessToken(request: Request, body: unknown): string {
|
|
444
|
+
const authorization = request.headers.get("authorization");
|
|
445
|
+
|
|
446
|
+
if (authorization?.startsWith("Bearer ")) {
|
|
447
|
+
return authorization.slice("Bearer ".length).trim();
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const header =
|
|
451
|
+
request.headers.get("x-agentkit-access-token") ??
|
|
452
|
+
request.headers.get("x-agentkit-deploy-access-token") ??
|
|
453
|
+
"";
|
|
454
|
+
const token = header.startsWith("Bearer ") ? header.slice("Bearer ".length) : header;
|
|
455
|
+
if (token.trim()) {
|
|
456
|
+
return token.trim();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (body && typeof body === "object") {
|
|
460
|
+
const record = body as { access_token?: unknown; accessToken?: unknown };
|
|
461
|
+
const bodyToken =
|
|
462
|
+
typeof record.access_token === "string"
|
|
463
|
+
? record.access_token
|
|
464
|
+
: typeof record.accessToken === "string"
|
|
465
|
+
? record.accessToken
|
|
466
|
+
: "";
|
|
467
|
+
return bodyToken.trim();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return "";
|
|
471
|
+
}
|
|
472
|
+
|
|
226
473
|
async function handlePortableChannel(
|
|
227
474
|
capsule: LoadedAgentCapsule,
|
|
228
475
|
request: Request,
|
|
229
|
-
|
|
476
|
+
channelRuntime: DevChannelRuntimeState,
|
|
230
477
|
route: { name: string; type: ChannelType; provider: ChannelProvider },
|
|
231
478
|
): Promise<Response> {
|
|
232
479
|
const channel = (capsule.config.channels ?? []).find(
|
|
@@ -258,28 +505,59 @@ async function handlePortableChannel(
|
|
|
258
505
|
throw new AgentKitError(verification.code, verification.message);
|
|
259
506
|
}
|
|
260
507
|
|
|
261
|
-
|
|
262
|
-
|
|
508
|
+
const webhookResponse = adapter.getWebhookResponse
|
|
509
|
+
? await adapter.getWebhookResponse({ ...rawEvent, channel, secrets })
|
|
510
|
+
: null;
|
|
511
|
+
|
|
512
|
+
if (webhookResponse) {
|
|
513
|
+
return channelAdapterResponse(webhookResponse);
|
|
263
514
|
}
|
|
264
515
|
|
|
265
516
|
const events = await adapter.normalizeInbound(rawEvent);
|
|
266
517
|
const deliveries = [];
|
|
267
518
|
|
|
268
519
|
for (const event of events) {
|
|
269
|
-
if (dedupeKeys.has(event.dedupeKey)) {
|
|
520
|
+
if (channelRuntime.dedupeKeys.has(event.dedupeKey)) {
|
|
270
521
|
deliveries.push({ state: "duplicate", dedupeKey: event.dedupeKey });
|
|
271
522
|
continue;
|
|
272
523
|
}
|
|
273
524
|
|
|
274
|
-
dedupeKeys.add(event.dedupeKey);
|
|
525
|
+
channelRuntime.dedupeKeys.add(event.dedupeKey);
|
|
526
|
+
|
|
527
|
+
const processedMessage = await processDevChannelMessage(capsule, adapter, channel, event.message, secrets);
|
|
275
528
|
|
|
276
|
-
if (event.kind !== "message" || !
|
|
529
|
+
if (event.kind !== "message" || !processedMessage) {
|
|
277
530
|
deliveries.push({ state: "skipped", dedupeKey: event.dedupeKey, unsupportedReason: event.unsupportedReason });
|
|
278
531
|
continue;
|
|
279
532
|
}
|
|
280
533
|
|
|
534
|
+
const conversationId = channelConversationId(channel.name, event.externalIdentity.key);
|
|
535
|
+
|
|
536
|
+
if (isChannelBufferEnabled(channel)) {
|
|
537
|
+
const buffer = resolveChannelBuffer(channel);
|
|
538
|
+
if (buffer.mode !== "debounce") {
|
|
539
|
+
throw new AgentKitError("runtime_error", "Unexpected channel buffer mode.");
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
bufferDevChannelMessage(capsule, channelRuntime, {
|
|
543
|
+
bufferKey: `${channel.name}:${event.externalIdentity.key}`,
|
|
544
|
+
conversationId,
|
|
545
|
+
content: processedMessage.content,
|
|
546
|
+
receivedAt: event.receivedAt,
|
|
547
|
+
buffer,
|
|
548
|
+
});
|
|
549
|
+
deliveries.push({
|
|
550
|
+
state: "buffered",
|
|
551
|
+
dedupeKey: event.dedupeKey,
|
|
552
|
+
conversationId,
|
|
553
|
+
flushAfterMs: buffer.quietWindowMs,
|
|
554
|
+
});
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
|
|
281
558
|
const result = await runAgentMessage(capsule, {
|
|
282
|
-
message:
|
|
559
|
+
message: processedMessage.content,
|
|
560
|
+
conversationId,
|
|
283
561
|
signal: request.signal,
|
|
284
562
|
});
|
|
285
563
|
deliveries.push({
|
|
@@ -290,10 +568,186 @@ async function handlePortableChannel(
|
|
|
290
568
|
});
|
|
291
569
|
}
|
|
292
570
|
|
|
571
|
+
if (channel.type === "discord" && channel.provider === "discord") {
|
|
572
|
+
return jsonResponse(discordLocalInteractionResponse(deliveries));
|
|
573
|
+
}
|
|
574
|
+
|
|
293
575
|
return jsonResponse({ deliveries });
|
|
294
576
|
}
|
|
295
577
|
|
|
578
|
+
async function processDevChannelMessage(
|
|
579
|
+
capsule: LoadedAgentCapsule,
|
|
580
|
+
adapter: ChannelAdapter,
|
|
581
|
+
channel: AgentChannel,
|
|
582
|
+
message: NormalizedChannelMessage | undefined,
|
|
583
|
+
secrets: Record<string, string>,
|
|
584
|
+
): Promise<NormalizedChannelMessage | null> {
|
|
585
|
+
if (!message) {
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (message.contentType === "text" && message.content) {
|
|
590
|
+
return message;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (message.contentType !== "audio" || !message.audio || channel.audio?.mode !== "transcribe") {
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (!adapter.downloadMedia) {
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const env = await loadCapsuleEnv(capsule.root);
|
|
602
|
+
const transcriptionConfig = resolveTranscriptionConfig(capsule.config.transcription);
|
|
603
|
+
const secretName = transcriptionConfig.secret;
|
|
604
|
+
const download = await adapter.downloadMedia({
|
|
605
|
+
type: channel.type,
|
|
606
|
+
provider: channel.provider,
|
|
607
|
+
channelId: channel.name,
|
|
608
|
+
message,
|
|
609
|
+
secrets: {
|
|
610
|
+
...secrets,
|
|
611
|
+
...(secretName ? { [secretName]: env[secretName] ?? "" } : {}),
|
|
612
|
+
...(env.AGENTKIT_TELEGRAM_API_BASE_URL ? { AGENTKIT_TELEGRAM_API_BASE_URL: env.AGENTKIT_TELEGRAM_API_BASE_URL } : {}),
|
|
613
|
+
},
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
if (!download.ok) {
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const transcription = await transcribeAudio({
|
|
621
|
+
config: transcriptionConfig,
|
|
622
|
+
secrets: {
|
|
623
|
+
...(secretName ? { [secretName]: env[secretName] ?? "" } : {}),
|
|
624
|
+
},
|
|
625
|
+
audio: download.audio,
|
|
626
|
+
filename: download.filename,
|
|
627
|
+
...(download.mimeType ? { mimeType: download.mimeType } : {}),
|
|
628
|
+
...(download.durationSeconds !== undefined ? { durationSeconds: download.durationSeconds } : {}),
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
if (!transcription.ok) {
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return {
|
|
636
|
+
role: "user",
|
|
637
|
+
contentType: "text",
|
|
638
|
+
content: `The user sent an audio message.\n\nTranscript:\n${transcription.text}`,
|
|
639
|
+
providerMessageId: message.providerMessageId,
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function bufferDevChannelMessage(
|
|
644
|
+
capsule: LoadedAgentCapsule,
|
|
645
|
+
runtime: DevChannelRuntimeState,
|
|
646
|
+
input: {
|
|
647
|
+
bufferKey: string;
|
|
648
|
+
conversationId: string;
|
|
649
|
+
content: string;
|
|
650
|
+
receivedAt: string;
|
|
651
|
+
buffer: {
|
|
652
|
+
quietWindowMs: number;
|
|
653
|
+
maxWaitMs: number;
|
|
654
|
+
maxMessages: number;
|
|
655
|
+
maxChars: number;
|
|
656
|
+
};
|
|
657
|
+
},
|
|
658
|
+
): void {
|
|
659
|
+
const now = Date.now();
|
|
660
|
+
const existing = runtime.buffers.get(input.bufferKey);
|
|
661
|
+
const pending: DevChannelBuffer = existing ?? {
|
|
662
|
+
conversationId: input.conversationId,
|
|
663
|
+
messages: [],
|
|
664
|
+
firstReceivedMs: now,
|
|
665
|
+
lastReceivedMs: now,
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
if (existing?.timer) {
|
|
669
|
+
clearTimeout(existing.timer);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
pending.messages.push({
|
|
673
|
+
content: input.content,
|
|
674
|
+
receivedAt: input.receivedAt,
|
|
675
|
+
});
|
|
676
|
+
pending.lastReceivedMs = now;
|
|
677
|
+
runtime.buffers.set(input.bufferKey, pending);
|
|
678
|
+
|
|
679
|
+
if (shouldFlushDevChannelBuffer(pending, input.buffer)) {
|
|
680
|
+
void flushDevChannelBuffer(capsule, runtime, input.bufferKey);
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const elapsedMs = now - pending.firstReceivedMs;
|
|
685
|
+
const delayMs = Math.max(0, Math.min(input.buffer.quietWindowMs, input.buffer.maxWaitMs - elapsedMs));
|
|
686
|
+
pending.timer = setTimeout(() => {
|
|
687
|
+
void flushDevChannelBuffer(capsule, runtime, input.bufferKey);
|
|
688
|
+
}, delayMs);
|
|
689
|
+
unrefTimer(pending.timer);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function shouldFlushDevChannelBuffer(
|
|
693
|
+
pending: DevChannelBuffer,
|
|
694
|
+
buffer: {
|
|
695
|
+
maxWaitMs: number;
|
|
696
|
+
maxMessages: number;
|
|
697
|
+
maxChars: number;
|
|
698
|
+
},
|
|
699
|
+
): boolean {
|
|
700
|
+
const totalChars = pending.messages.reduce((sum, message) => sum + message.content.length, 0);
|
|
701
|
+
return (
|
|
702
|
+
pending.messages.length >= buffer.maxMessages ||
|
|
703
|
+
totalChars >= buffer.maxChars ||
|
|
704
|
+
Date.now() - pending.firstReceivedMs >= buffer.maxWaitMs
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
async function flushDevChannelBuffer(
|
|
709
|
+
capsule: LoadedAgentCapsule,
|
|
710
|
+
runtime: DevChannelRuntimeState,
|
|
711
|
+
bufferKey: string,
|
|
712
|
+
): Promise<void> {
|
|
713
|
+
const pending = runtime.buffers.get(bufferKey);
|
|
714
|
+
|
|
715
|
+
if (!pending) {
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
runtime.buffers.delete(bufferKey);
|
|
720
|
+
|
|
721
|
+
if (pending.timer) {
|
|
722
|
+
clearTimeout(pending.timer);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const message = renderBufferedChannelMessage(pending.messages);
|
|
726
|
+
|
|
727
|
+
try {
|
|
728
|
+
await runAgentMessage(capsule, {
|
|
729
|
+
message,
|
|
730
|
+
conversationId: pending.conversationId,
|
|
731
|
+
runtime: {
|
|
732
|
+
invocation: "channel",
|
|
733
|
+
},
|
|
734
|
+
});
|
|
735
|
+
} catch {
|
|
736
|
+
// Local channel buffering is best-effort; hosted delivery logs own production failures.
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function unrefTimer(timer: ReturnType<typeof setTimeout>): void {
|
|
741
|
+
if (typeof timer === "object" && timer && "unref" in timer && typeof timer.unref === "function") {
|
|
742
|
+
timer.unref();
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
296
746
|
async function handleLocalFile(capsule: LoadedAgentCapsule, request: Request, rawKey: string): Promise<Response> {
|
|
747
|
+
if (capsule.config.storage.files?.driver !== "r2") {
|
|
748
|
+
throw new AgentKitError("files_not_configured", "File storage is not configured for this Agent Capsule.");
|
|
749
|
+
}
|
|
750
|
+
|
|
297
751
|
const key = normalizeFileKey(rawKey);
|
|
298
752
|
const root = resolve(capsule.root, ".agentkit/files");
|
|
299
753
|
const path = resolve(root, key);
|
|
@@ -303,14 +757,8 @@ async function handleLocalFile(capsule: LoadedAgentCapsule, request: Request, ra
|
|
|
303
757
|
}
|
|
304
758
|
|
|
305
759
|
if (request.method === "PUT") {
|
|
306
|
-
const
|
|
307
|
-
const
|
|
308
|
-
? capsule.config.storage.files.maxUploadBytes ?? 10 * 1024 * 1024
|
|
309
|
-
: 10 * 1024 * 1024;
|
|
310
|
-
|
|
311
|
-
if (bytes.byteLength > maxUploadBytes) {
|
|
312
|
-
throw new AgentKitError("file_too_large", `File exceeds max upload size of ${maxUploadBytes} bytes.`);
|
|
313
|
-
}
|
|
760
|
+
const maxUploadBytes = capsule.config.storage.files.maxUploadBytes ?? 10 * 1024 * 1024;
|
|
761
|
+
const bytes = await readRequestBytesWithLimit(request, maxUploadBytes);
|
|
314
762
|
|
|
315
763
|
await mkdir(dirname(path), { recursive: true });
|
|
316
764
|
await writeFile(path, bytes);
|
|
@@ -333,6 +781,102 @@ async function handleLocalFile(capsule: LoadedAgentCapsule, request: Request, ra
|
|
|
333
781
|
throw new AgentKitError("method_not_allowed", "Unsupported file method.");
|
|
334
782
|
}
|
|
335
783
|
|
|
784
|
+
export class BoundedDedupeCache {
|
|
785
|
+
private readonly entries = new Map<string, number>();
|
|
786
|
+
|
|
787
|
+
constructor(
|
|
788
|
+
private readonly maxKeys: number,
|
|
789
|
+
private readonly ttlMs: number,
|
|
790
|
+
) {}
|
|
791
|
+
|
|
792
|
+
has(key: string): boolean {
|
|
793
|
+
this.pruneExpired();
|
|
794
|
+
return this.entries.has(key);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
add(key: string): void {
|
|
798
|
+
this.pruneExpired();
|
|
799
|
+
this.entries.set(key, Date.now());
|
|
800
|
+
this.pruneOverflow();
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
get size(): number {
|
|
804
|
+
this.pruneExpired();
|
|
805
|
+
return this.entries.size;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
private pruneExpired(): void {
|
|
809
|
+
const cutoff = Date.now() - this.ttlMs;
|
|
810
|
+
|
|
811
|
+
for (const [key, timestamp] of this.entries) {
|
|
812
|
+
if (timestamp >= cutoff) {
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
this.entries.delete(key);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
private pruneOverflow(): void {
|
|
821
|
+
while (this.entries.size > this.maxKeys) {
|
|
822
|
+
const oldest = this.entries.keys().next().value;
|
|
823
|
+
|
|
824
|
+
if (typeof oldest !== "string") {
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
this.entries.delete(oldest);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
async function readRequestBytesWithLimit(request: Request, maxBytes: number): Promise<Uint8Array> {
|
|
834
|
+
const contentLength = request.headers.get("content-length");
|
|
835
|
+
|
|
836
|
+
if (contentLength) {
|
|
837
|
+
const declaredBytes = Number(contentLength);
|
|
838
|
+
|
|
839
|
+
if (Number.isFinite(declaredBytes) && declaredBytes > maxBytes) {
|
|
840
|
+
throw new AgentKitError("file_too_large", `File exceeds max upload size of ${maxBytes} bytes.`);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
if (!request.body) {
|
|
845
|
+
return new Uint8Array();
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const reader = request.body.getReader();
|
|
849
|
+
const chunks: Uint8Array[] = [];
|
|
850
|
+
let total = 0;
|
|
851
|
+
|
|
852
|
+
while (true) {
|
|
853
|
+
const { done, value } = await reader.read();
|
|
854
|
+
|
|
855
|
+
if (done) {
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
total += value.byteLength;
|
|
860
|
+
|
|
861
|
+
if (total > maxBytes) {
|
|
862
|
+
await reader.cancel().catch(() => undefined);
|
|
863
|
+
throw new AgentKitError("file_too_large", `File exceeds max upload size of ${maxBytes} bytes.`);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
chunks.push(value);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const bytes = new Uint8Array(total);
|
|
870
|
+
let offset = 0;
|
|
871
|
+
|
|
872
|
+
for (const chunk of chunks) {
|
|
873
|
+
bytes.set(chunk, offset);
|
|
874
|
+
offset += chunk.byteLength;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
return bytes;
|
|
878
|
+
}
|
|
879
|
+
|
|
336
880
|
async function listConversations(capsule: LoadedAgentCapsule) {
|
|
337
881
|
const store = await openCapsuleStore(capsule);
|
|
338
882
|
|
|
@@ -363,6 +907,32 @@ async function getConversation(capsule: LoadedAgentCapsule, conversationId: stri
|
|
|
363
907
|
}
|
|
364
908
|
}
|
|
365
909
|
|
|
910
|
+
async function getConversationTrace(capsule: LoadedAgentCapsule, conversationId: string) {
|
|
911
|
+
if (!conversationId) {
|
|
912
|
+
throw new AgentKitError("validation_error", "Missing conversation id.");
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const store = await openCapsuleStore(capsule);
|
|
916
|
+
|
|
917
|
+
try {
|
|
918
|
+
const conversation = store.getConversation(conversationId);
|
|
919
|
+
|
|
920
|
+
if (!conversation) {
|
|
921
|
+
throw new AgentKitError("conversation_not_found", `Conversation "${conversationId}" was not found.`);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
return {
|
|
925
|
+
...conversation,
|
|
926
|
+
runs: store.listRunsForConversation(conversation.id).map((run) => ({
|
|
927
|
+
...run,
|
|
928
|
+
toolCalls: store.listToolCallsForRun(run.id),
|
|
929
|
+
})),
|
|
930
|
+
};
|
|
931
|
+
} finally {
|
|
932
|
+
store.close();
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
366
936
|
async function readJsonBody(request: Request): Promise<unknown> {
|
|
367
937
|
const contentType = request.headers.get("content-type") ?? "";
|
|
368
938
|
|
|
@@ -377,20 +947,61 @@ async function readJsonBody(request: Request): Promise<unknown> {
|
|
|
377
947
|
}
|
|
378
948
|
}
|
|
379
949
|
|
|
380
|
-
function
|
|
381
|
-
if (!isRecord(body)
|
|
950
|
+
function readChatRequest(body: unknown): { message: string; conversationId?: string } {
|
|
951
|
+
if (!isRecord(body)) {
|
|
952
|
+
throw new AgentKitError(
|
|
953
|
+
"validation_error",
|
|
954
|
+
'Expected body shape: {"message":{"role":"user","content":"hello"},"conversationId":"optional-id"}.',
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
const message = readUserMessage(body.message);
|
|
959
|
+
const conversationId = readOptionalConversationId(body.conversationId);
|
|
960
|
+
|
|
961
|
+
return { message, conversationId };
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function readUserMessage(message: unknown): string {
|
|
965
|
+
if (typeof message === "string") {
|
|
966
|
+
const content = message.trim();
|
|
967
|
+
|
|
968
|
+
if (!content) {
|
|
969
|
+
throw new AgentKitError("validation_error", "message must be a non-empty string.");
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
return content;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
if (!isRecord(message)) {
|
|
382
976
|
throw new AgentKitError("validation_error", 'Expected body shape: {"message":{"role":"user","content":"hello"}}.');
|
|
383
977
|
}
|
|
384
978
|
|
|
385
|
-
if (
|
|
979
|
+
if (message.role !== "user") {
|
|
386
980
|
throw new AgentKitError("validation_error", 'message.role must be "user".');
|
|
387
981
|
}
|
|
388
982
|
|
|
389
|
-
if (typeof
|
|
983
|
+
if (typeof message.content !== "string" || message.content.trim().length === 0) {
|
|
390
984
|
throw new AgentKitError("validation_error", "message.content must be a non-empty string.");
|
|
391
985
|
}
|
|
392
986
|
|
|
393
|
-
return
|
|
987
|
+
return message.content.trim();
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function readOptionalConversationId(value: unknown): string | undefined {
|
|
991
|
+
if (value === undefined || value === null) {
|
|
992
|
+
return undefined;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
996
|
+
throw new AgentKitError("validation_error", "conversationId must be a non-empty string when provided.");
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
return value.trim();
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function channelConversationId(channelName: string, externalIdentityKey: string): string {
|
|
1003
|
+
const hash = createHash("sha256").update(`${channelName}:${externalIdentityKey}`).digest("hex").slice(0, 32);
|
|
1004
|
+
return `chn_${hash}`;
|
|
394
1005
|
}
|
|
395
1006
|
|
|
396
1007
|
function readToolInput(body: unknown): unknown {
|
|
@@ -433,6 +1044,7 @@ function statusForError(error: unknown): number {
|
|
|
433
1044
|
error.code === "conversation_not_found" ||
|
|
434
1045
|
error.code === "not_found" ||
|
|
435
1046
|
error.code === "tool_not_found" ||
|
|
1047
|
+
error.code === "files_not_configured" ||
|
|
436
1048
|
error.code === "file_not_found" ||
|
|
437
1049
|
error.code === "channel_not_found"
|
|
438
1050
|
) {
|
|
@@ -525,9 +1137,69 @@ function adapterForChannel(type: ChannelType, provider: ChannelProvider): Channe
|
|
|
525
1137
|
return metaWhatsappChannelAdapter;
|
|
526
1138
|
}
|
|
527
1139
|
|
|
1140
|
+
if (type === "discord" && provider === "discord") {
|
|
1141
|
+
return discordChannelAdapter;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
if (type === "slack" && provider === "slack") {
|
|
1145
|
+
return slackChannelAdapter;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
528
1148
|
throw new AgentKitError("channel_not_found", `No channel adapter for ${type}/${provider}.`);
|
|
529
1149
|
}
|
|
530
1150
|
|
|
1151
|
+
function channelAdapterResponse(response: {
|
|
1152
|
+
status?: number;
|
|
1153
|
+
headers?: Record<string, string>;
|
|
1154
|
+
body: string | Record<string, unknown>;
|
|
1155
|
+
}): Response {
|
|
1156
|
+
if (typeof response.body === "string") {
|
|
1157
|
+
return new Response(response.body, {
|
|
1158
|
+
status: response.status ?? 200,
|
|
1159
|
+
headers: response.headers,
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
return jsonResponse(response.body, {
|
|
1164
|
+
status: response.status ?? 200,
|
|
1165
|
+
headers: response.headers,
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
function discordLocalInteractionResponse(
|
|
1170
|
+
deliveries: Array<{
|
|
1171
|
+
state: string;
|
|
1172
|
+
message?: {
|
|
1173
|
+
content?: string;
|
|
1174
|
+
};
|
|
1175
|
+
}>,
|
|
1176
|
+
): Record<string, unknown> {
|
|
1177
|
+
const sent = deliveries.find((delivery) => delivery.state === "sent" && delivery.message?.content);
|
|
1178
|
+
|
|
1179
|
+
if (sent?.message?.content) {
|
|
1180
|
+
return {
|
|
1181
|
+
type: 4,
|
|
1182
|
+
data: {
|
|
1183
|
+
content: sent.message.content,
|
|
1184
|
+
allowed_mentions: {
|
|
1185
|
+
parse: [],
|
|
1186
|
+
},
|
|
1187
|
+
},
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
return {
|
|
1192
|
+
type: 4,
|
|
1193
|
+
data: {
|
|
1194
|
+
content: "This Discord interaction was received but could not be processed.",
|
|
1195
|
+
flags: 64,
|
|
1196
|
+
allowed_mentions: {
|
|
1197
|
+
parse: [],
|
|
1198
|
+
},
|
|
1199
|
+
},
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
|
|
531
1203
|
async function resolveChannelSecrets(root: string, channel: AgentChannel): Promise<Record<string, string>> {
|
|
532
1204
|
const env = await loadCapsuleEnv(root);
|
|
533
1205
|
return Object.fromEntries(channel.secrets.map((name) => [name, env[name] ?? ""]));
|
|
@@ -805,6 +1477,7 @@ function renderChatApp(capsule: LoadedAgentCapsule, baseUrl: string, env: Record
|
|
|
805
1477
|
const textarea = document.querySelector("#message");
|
|
806
1478
|
const messages = document.querySelector("#messages");
|
|
807
1479
|
const button = form.querySelector("button");
|
|
1480
|
+
let conversationId = null;
|
|
808
1481
|
|
|
809
1482
|
function addMessage(role, content, className = "") {
|
|
810
1483
|
const wrapper = document.createElement("article");
|
|
@@ -817,6 +1490,13 @@ function renderChatApp(capsule: LoadedAgentCapsule, baseUrl: string, env: Record
|
|
|
817
1490
|
messages.scrollTop = messages.scrollHeight;
|
|
818
1491
|
}
|
|
819
1492
|
|
|
1493
|
+
textarea.addEventListener("keydown", (event) => {
|
|
1494
|
+
if (event.key !== "Enter" || event.shiftKey || event.isComposing) return;
|
|
1495
|
+
event.preventDefault();
|
|
1496
|
+
if (button.disabled) return;
|
|
1497
|
+
form.requestSubmit();
|
|
1498
|
+
});
|
|
1499
|
+
|
|
820
1500
|
form.addEventListener("submit", async (event) => {
|
|
821
1501
|
event.preventDefault();
|
|
822
1502
|
const content = textarea.value.trim();
|
|
@@ -830,7 +1510,7 @@ function renderChatApp(capsule: LoadedAgentCapsule, baseUrl: string, env: Record
|
|
|
830
1510
|
const response = await fetch("/v1/chat", {
|
|
831
1511
|
method: "POST",
|
|
832
1512
|
headers: { "content-type": "application/json" },
|
|
833
|
-
body: JSON.stringify({ message: { role: "user", content } }),
|
|
1513
|
+
body: JSON.stringify({ message: { role: "user", content }, conversationId }),
|
|
834
1514
|
});
|
|
835
1515
|
const data = await response.json();
|
|
836
1516
|
|
|
@@ -838,6 +1518,7 @@ function renderChatApp(capsule: LoadedAgentCapsule, baseUrl: string, env: Record
|
|
|
838
1518
|
throw new Error(data.error?.message || "Chat request failed.");
|
|
839
1519
|
}
|
|
840
1520
|
|
|
1521
|
+
conversationId = data.conversationId || conversationId;
|
|
841
1522
|
addMessage(data.message.role, data.message.content);
|
|
842
1523
|
} catch (error) {
|
|
843
1524
|
addMessage("error", error instanceof Error ? error.message : String(error), "error");
|
|
@@ -881,6 +1562,19 @@ function listen(server: Server, hostname: string, port: number): Promise<number>
|
|
|
881
1562
|
});
|
|
882
1563
|
}
|
|
883
1564
|
|
|
1565
|
+
async function closeServerAfterListenError(server: Server): Promise<void> {
|
|
1566
|
+
if (!server.listening) {
|
|
1567
|
+
try {
|
|
1568
|
+
server.close();
|
|
1569
|
+
} catch {
|
|
1570
|
+
// The server never reached the listening state.
|
|
1571
|
+
}
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
await closeServer(server);
|
|
1576
|
+
}
|
|
1577
|
+
|
|
884
1578
|
function closeServer(server: Server): Promise<void> {
|
|
885
1579
|
return new Promise((resolvePromise, reject) => {
|
|
886
1580
|
server.close((error) => {
|
|
@@ -894,6 +1588,30 @@ function closeServer(server: Server): Promise<void> {
|
|
|
894
1588
|
});
|
|
895
1589
|
}
|
|
896
1590
|
|
|
1591
|
+
function isAddressInUseError(error: unknown): boolean {
|
|
1592
|
+
return error instanceof Error && "code" in error && error.code === "EADDRINUSE";
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
async function detectAgentKitOccupant(hostname: string, port: number): Promise<string | undefined> {
|
|
1596
|
+
const controller = new AbortController();
|
|
1597
|
+
const timeout = setTimeout(() => controller.abort(), 300);
|
|
1598
|
+
|
|
1599
|
+
try {
|
|
1600
|
+
const response = await fetch(`http://${hostname}:${port}/_agentkit`, { signal: controller.signal });
|
|
1601
|
+
const payload = await response.json().catch(() => null);
|
|
1602
|
+
|
|
1603
|
+
if (payload && typeof payload === "object" && "agent" in payload && typeof payload.agent === "string") {
|
|
1604
|
+
return payload.agent;
|
|
1605
|
+
}
|
|
1606
|
+
} catch {
|
|
1607
|
+
return undefined;
|
|
1608
|
+
} finally {
|
|
1609
|
+
clearTimeout(timeout);
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
return undefined;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
897
1615
|
function findAvailablePort(): Promise<number> {
|
|
898
1616
|
return new Promise((resolve, reject) => {
|
|
899
1617
|
const server = createNetServer();
|