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