@andreprado/agentkit 0.1.0-alpha.2 → 0.1.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -6
- package/docs/guides/add-tool.md +1 -1
- package/docs/guides/channels-implementation-map.md +243 -0
- package/docs/guides/create-agent.md +23 -4
- package/docs/guides/prepare-deploy.md +18 -6
- package/docs/guides/run-evals.md +28 -2
- package/docs/guides/security-rules.md +1 -1
- package/docs/guides/use-provider.md +16 -2
- package/docs/llms-full.txt +70 -38
- package/docs/llms.txt +15 -4
- package/package.json +1 -2
- package/src/cli/index.ts +1190 -36
- package/src/cloud/artifact.ts +48 -0
- package/src/cloud/client.ts +79 -0
- package/src/cloud/contracts.ts +47 -0
- package/src/cloud/index.ts +3 -0
- package/src/index.ts +1 -1
- package/src/providers/test.ts +51 -0
- package/src/runtime/chat.ts +59 -4
- package/src/runtime/deploy.ts +1 -1
- package/src/runtime/dev-server.ts +154 -16
- package/src/runtime/runtime-contract.ts +16 -2
- package/src/runtime/targets/cloudflare/build.ts +83 -4
- package/src/storage/sqlite.ts +2 -2
- package/src/templates/blank.ts +52 -9
- package/src/templates/dentista.ts +988 -0
- package/src/templates/index.ts +2 -0
- package/src/templates/support.ts +50 -9
- package/src/runtime/targets/cloudflare/deploy.ts +0 -5475
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
import type { AgentBuildResult, AgentManifestWithBundle } from "../runtime/targets/cloudflare/build";
|
|
5
|
+
import { AgentKitError } from "../runtime/errors";
|
|
6
|
+
import type { HostedDeployArtifact } from "./contracts";
|
|
7
|
+
|
|
8
|
+
export async function createHostedDeployArtifact(build: AgentBuildResult): Promise<HostedDeployArtifact> {
|
|
9
|
+
const [manifestSource, workerSource, wranglerSource] = await Promise.all([
|
|
10
|
+
readFile(build.manifestPath, "utf8"),
|
|
11
|
+
readFile(build.workerPath, "utf8"),
|
|
12
|
+
readFile(build.wranglerPath, "utf8"),
|
|
13
|
+
]);
|
|
14
|
+
const manifest = JSON.parse(manifestSource) as AgentManifestWithBundle;
|
|
15
|
+
const workerSha256 = sha256(workerSource);
|
|
16
|
+
|
|
17
|
+
if (workerSha256 !== build.bundleSha256 || workerSha256 !== manifest.bundle.sha256) {
|
|
18
|
+
throw new AgentKitError("artifact_integrity_error", "Worker SHA-256 does not match the build manifest.");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const artifact: HostedDeployArtifact = {
|
|
22
|
+
manifest,
|
|
23
|
+
worker: {
|
|
24
|
+
content_type: "application/javascript",
|
|
25
|
+
source: workerSource,
|
|
26
|
+
sha256: workerSha256,
|
|
27
|
+
},
|
|
28
|
+
wrangler: {
|
|
29
|
+
content_type: "application/json",
|
|
30
|
+
source: wranglerSource,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
if (build.tursoSchemaPath) {
|
|
35
|
+
const source = await readFile(build.tursoSchemaPath, "utf8");
|
|
36
|
+
artifact.turso_schema = {
|
|
37
|
+
content_type: "text/sql",
|
|
38
|
+
source,
|
|
39
|
+
sha256: sha256(source),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return artifact;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function sha256(source: string): string {
|
|
47
|
+
return createHash("sha256").update(source).digest("hex");
|
|
48
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { AgentBuildResult } from "../runtime/targets/cloudflare/build";
|
|
5
|
+
import type { LocalDeployState } from "../runtime/core/deploy-state";
|
|
6
|
+
import { AgentKitError } from "../runtime/errors";
|
|
7
|
+
import { createHostedDeployArtifact } from "./artifact";
|
|
8
|
+
import type { AgentKitCloudDeployOptions, CreateDeployRequest, CreateDeployResponse } from "./contracts";
|
|
9
|
+
|
|
10
|
+
export async function deployBuildToAgentKitCloud(
|
|
11
|
+
build: AgentBuildResult,
|
|
12
|
+
options: AgentKitCloudDeployOptions,
|
|
13
|
+
): Promise<CreateDeployResponse> {
|
|
14
|
+
const artifact = await createHostedDeployArtifact(build);
|
|
15
|
+
const headers = new Headers({
|
|
16
|
+
"Content-Type": "application/json",
|
|
17
|
+
});
|
|
18
|
+
const apiToken = options.apiToken ?? process.env.AGENTKIT_CLOUD_API_TOKEN;
|
|
19
|
+
|
|
20
|
+
if (apiToken) {
|
|
21
|
+
headers.set("Authorization", `Bearer ${apiToken}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const response = await fetch(new URL("/v1/deploys", normalizeApiUrl(options.apiUrl)), {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers,
|
|
27
|
+
body: JSON.stringify({
|
|
28
|
+
project_name: artifact.manifest.name,
|
|
29
|
+
anonymous: options.anonymous ?? !apiToken,
|
|
30
|
+
artifact,
|
|
31
|
+
} satisfies CreateDeployRequest),
|
|
32
|
+
});
|
|
33
|
+
const payload = await response.json().catch(() => null);
|
|
34
|
+
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
const code =
|
|
37
|
+
payload && typeof payload === "object" && "error" in payload
|
|
38
|
+
? String((payload as { error?: { code?: unknown } }).error?.code ?? "cloud_deploy_failed")
|
|
39
|
+
: "cloud_deploy_failed";
|
|
40
|
+
const message =
|
|
41
|
+
payload && typeof payload === "object" && "error" in payload
|
|
42
|
+
? String((payload as { error?: { message?: unknown } }).error?.message ?? "AgentKit Cloud deploy failed.")
|
|
43
|
+
: `AgentKit Cloud deploy failed with HTTP ${response.status}.`;
|
|
44
|
+
throw new AgentKitError(code, message);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return payload as CreateDeployResponse;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function writeLocalDeployState(root: string, deploy: CreateDeployResponse): Promise<string> {
|
|
51
|
+
const statePath = join(root, ".agentkit/deploy.json");
|
|
52
|
+
const state: LocalDeployState = {
|
|
53
|
+
project_id: deploy.project.id,
|
|
54
|
+
deploy_id: deploy.deploy.id,
|
|
55
|
+
url: deploy.deploy.url,
|
|
56
|
+
target: "cloudflare",
|
|
57
|
+
runtime: "edge",
|
|
58
|
+
resources: {
|
|
59
|
+
cloudflare: {
|
|
60
|
+
worker_url: deploy.deploy.url,
|
|
61
|
+
durable_object_class: "AgentKitConversationStore",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
owner_state: deploy.project.owner_state,
|
|
65
|
+
access: {
|
|
66
|
+
mode: deploy.deploy.access.mode,
|
|
67
|
+
private_token: deploy.deploy.access.private_token ?? undefined,
|
|
68
|
+
},
|
|
69
|
+
created_at: new Date().toISOString(),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
await mkdir(join(root, ".agentkit"), { recursive: true });
|
|
73
|
+
await writeFile(statePath, `${JSON.stringify(state, null, 2)}\n`);
|
|
74
|
+
return statePath;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalizeApiUrl(value: string): string {
|
|
78
|
+
return value.endsWith("/") ? value : `${value}/`;
|
|
79
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { AgentManifestWithBundle } from "../runtime/targets/cloudflare/build";
|
|
2
|
+
|
|
3
|
+
export type HostedDeployArtifact = {
|
|
4
|
+
manifest: AgentManifestWithBundle;
|
|
5
|
+
worker: {
|
|
6
|
+
content_type: "application/javascript";
|
|
7
|
+
source: string;
|
|
8
|
+
sha256: string;
|
|
9
|
+
};
|
|
10
|
+
wrangler: {
|
|
11
|
+
content_type: "application/json";
|
|
12
|
+
source: string;
|
|
13
|
+
};
|
|
14
|
+
turso_schema?: {
|
|
15
|
+
content_type: "text/sql";
|
|
16
|
+
source: string;
|
|
17
|
+
sha256: string;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type CreateDeployRequest = {
|
|
22
|
+
project_name: string;
|
|
23
|
+
anonymous: boolean;
|
|
24
|
+
artifact: HostedDeployArtifact;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type CreateDeployResponse = {
|
|
28
|
+
project: {
|
|
29
|
+
id: string;
|
|
30
|
+
owner_state: "anonymous" | "claimed";
|
|
31
|
+
};
|
|
32
|
+
deploy: {
|
|
33
|
+
id: string;
|
|
34
|
+
status: "ready" | "failed";
|
|
35
|
+
url: string;
|
|
36
|
+
access: {
|
|
37
|
+
mode: string;
|
|
38
|
+
private_token?: string;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type AgentKitCloudDeployOptions = {
|
|
44
|
+
apiUrl: string;
|
|
45
|
+
apiToken?: string;
|
|
46
|
+
anonymous?: boolean;
|
|
47
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -244,7 +244,7 @@ export { runToolFromCwd } from "./runtime/tool-runner";
|
|
|
244
244
|
export { startAgentDevServer } from "./runtime/dev-server";
|
|
245
245
|
export { inspectAgentCapsule } from "./runtime/inspect";
|
|
246
246
|
export { buildAgentCapsule } from "./runtime/build";
|
|
247
|
-
export { deployBuildToAgentKitCloud
|
|
247
|
+
export { deployBuildToAgentKitCloud } from "./runtime/deploy";
|
|
248
248
|
export { AgentKitError } from "./runtime/errors";
|
|
249
249
|
export type {
|
|
250
250
|
ChannelAdapter,
|
package/src/providers/test.ts
CHANGED
|
@@ -37,6 +37,17 @@ export const testProviderAdapter: ProviderAdapter = {
|
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
const rememberedName = answerRememberedName(input.messages);
|
|
41
|
+
|
|
42
|
+
if (rememberedName) {
|
|
43
|
+
return {
|
|
44
|
+
message: {
|
|
45
|
+
role: "assistant",
|
|
46
|
+
content: rememberedName,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
40
51
|
return {
|
|
41
52
|
message: {
|
|
42
53
|
role: "assistant",
|
|
@@ -58,6 +69,46 @@ function findLastUserMessage(messages: AgentMessage[]): AgentMessage | undefined
|
|
|
58
69
|
return undefined;
|
|
59
70
|
}
|
|
60
71
|
|
|
72
|
+
function answerRememberedName(messages: AgentMessage[]): string | null {
|
|
73
|
+
const lastUserMessage = findLastUserMessage(messages);
|
|
74
|
+
|
|
75
|
+
if (!lastUserMessage || !asksForName(lastUserMessage.content)) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (let index = messages.length - 2; index >= 0; index -= 1) {
|
|
80
|
+
const message = messages[index];
|
|
81
|
+
|
|
82
|
+
if (message.role !== "user") {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const name = extractName(message.content);
|
|
87
|
+
|
|
88
|
+
if (name) {
|
|
89
|
+
return /meu nome|qual\s+(é|e)\s*meu nome|como\s+(é|e)\s*meu nome/i.test(lastUserMessage.content)
|
|
90
|
+
? `Seu nome é ${name}.`
|
|
91
|
+
: `Your name is ${name}.`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function asksForName(content: string): boolean {
|
|
99
|
+
return /what(?:'s| is) my name\??/i.test(content) || /(como|qual)\s+(é|e)\s*(o\s+)?meu nome\??/i.test(content);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function extractName(content: string): string | null {
|
|
103
|
+
const match = content.match(/\bmy name is\s+(.+)$/i) ?? content.match(/\bmeu nome (?:é|e)\s+(.+)$/i);
|
|
104
|
+
|
|
105
|
+
if (!match) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return match[1].trim().replace(/[.!?]+$/g, "") || null;
|
|
110
|
+
}
|
|
111
|
+
|
|
61
112
|
function parseToolRequest(content: string): { name: string; input?: unknown } | null {
|
|
62
113
|
let parsed: unknown;
|
|
63
114
|
|
package/src/runtime/chat.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getProviderAdapter } from "../providers";
|
|
2
2
|
import type { AgentMessage, ProviderRunResult } from "../providers";
|
|
3
3
|
import type { ToolRuntimeContext } from "../index";
|
|
4
|
-
import type { PersistedChatRun } from "../storage/sqlite";
|
|
4
|
+
import type { ConversationRecord, ConversationSummary, PersistedChatRun } from "../storage/sqlite";
|
|
5
5
|
import type { LoadedAgentCapsule } from "./config";
|
|
6
6
|
import { loadAgentCapsule } from "./config";
|
|
7
7
|
import { openPreparedCapsuleStore } from "./database";
|
|
@@ -11,6 +11,7 @@ import { createToolRuntime } from "./tools";
|
|
|
11
11
|
|
|
12
12
|
export type RunAgentMessageOptions = {
|
|
13
13
|
message: string;
|
|
14
|
+
conversationId?: string;
|
|
14
15
|
signal?: AbortSignal;
|
|
15
16
|
runtime?: Partial<ToolRuntimeContext>;
|
|
16
17
|
};
|
|
@@ -36,12 +37,14 @@ export async function runAgentMessage(
|
|
|
36
37
|
|
|
37
38
|
const adapter = getProviderAdapter(capsule.config.provider);
|
|
38
39
|
const env = await loadCapsuleEnv(capsule.root);
|
|
39
|
-
const messages: AgentMessage[] = [{ role: "user", content }];
|
|
40
40
|
const { store } = await openPreparedCapsuleStore(capsule);
|
|
41
|
-
const
|
|
41
|
+
const conversationId = normalizeConversationId(options.conversationId);
|
|
42
|
+
const { conversation, previousMessages } = resolveConversation(store, {
|
|
42
43
|
agentName: capsule.config.name,
|
|
43
|
-
|
|
44
|
+
conversationId,
|
|
45
|
+
firstMessage: content,
|
|
44
46
|
});
|
|
47
|
+
const messages: AgentMessage[] = [...previousMessages, { role: "user", content }];
|
|
45
48
|
const userMessage = store.appendMessage({
|
|
46
49
|
conversationId: conversation.id,
|
|
47
50
|
role: "user",
|
|
@@ -116,6 +119,58 @@ export async function runAgentMessage(
|
|
|
116
119
|
}
|
|
117
120
|
}
|
|
118
121
|
|
|
122
|
+
function resolveConversation(
|
|
123
|
+
store: Awaited<ReturnType<typeof openPreparedCapsuleStore>>["store"],
|
|
124
|
+
input: {
|
|
125
|
+
agentName: string;
|
|
126
|
+
conversationId?: string;
|
|
127
|
+
firstMessage: string;
|
|
128
|
+
},
|
|
129
|
+
): { conversation: ConversationSummary; previousMessages: AgentMessage[] } {
|
|
130
|
+
if (input.conversationId) {
|
|
131
|
+
const existing = store.getConversation(input.conversationId);
|
|
132
|
+
|
|
133
|
+
if (existing) {
|
|
134
|
+
return {
|
|
135
|
+
conversation: existing,
|
|
136
|
+
previousMessages: toProviderMessages(existing),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const conversation = store.createConversation({
|
|
142
|
+
id: input.conversationId,
|
|
143
|
+
agentName: input.agentName,
|
|
144
|
+
title: titleFromMessage(input.firstMessage),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
conversation,
|
|
149
|
+
previousMessages: [],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function toProviderMessages(conversation: ConversationRecord): AgentMessage[] {
|
|
154
|
+
return conversation.messages.map((message) => ({
|
|
155
|
+
role: message.role,
|
|
156
|
+
content: message.content,
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function normalizeConversationId(value: string | undefined): string | undefined {
|
|
161
|
+
if (value === undefined) {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const conversationId = value.trim();
|
|
166
|
+
|
|
167
|
+
if (!conversationId) {
|
|
168
|
+
throw new AgentKitError("validation_error", "conversationId must be a non-empty string when provided.");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return conversationId;
|
|
172
|
+
}
|
|
173
|
+
|
|
119
174
|
function withToolVisibilityInstructions(instructions: string, tools: LoadedAgentCapsule["config"]["tools"] = []): string {
|
|
120
175
|
const internalTools = tools.filter((tool) => tool.visibility === "internal").map((tool) => tool.name).sort();
|
|
121
176
|
|
package/src/runtime/deploy.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "
|
|
1
|
+
export * from "../cloud";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
3
|
import { createServer as createHttpServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
|
3
4
|
import { createServer as createNetServer } from "node:net";
|
|
@@ -21,12 +22,18 @@ import { runToolFromCwd } from "./tool-runner";
|
|
|
21
22
|
export type AgentDevServerOptions = {
|
|
22
23
|
port?: number;
|
|
23
24
|
hostname?: string;
|
|
25
|
+
basePort?: number;
|
|
24
26
|
};
|
|
25
27
|
|
|
26
28
|
export type AgentDevServer = {
|
|
27
29
|
capsule: LoadedAgentCapsule;
|
|
28
30
|
hostname: string;
|
|
31
|
+
requestedPort: number | null;
|
|
29
32
|
port: number;
|
|
33
|
+
portConflict: {
|
|
34
|
+
port: number;
|
|
35
|
+
agent?: string;
|
|
36
|
+
} | null;
|
|
30
37
|
urls: {
|
|
31
38
|
chat: string;
|
|
32
39
|
api: string;
|
|
@@ -46,34 +53,83 @@ export async function startAgentDevServer(
|
|
|
46
53
|
): Promise<AgentDevServer> {
|
|
47
54
|
const capsule = await loadAgentCapsule(cwd);
|
|
48
55
|
const hostname = options.hostname ?? DEFAULT_HOSTNAME;
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
void handleNodeRequest(capsule, incoming, outgoing, channelDedupeKeys);
|
|
53
|
-
});
|
|
54
|
-
const actualPort = await listen(server, hostname, port);
|
|
56
|
+
const requestedPort = options.port ?? options.basePort ?? DEFAULT_PORT;
|
|
57
|
+
const autoPort = options.port === undefined;
|
|
58
|
+
const start = await listenForDevServer(capsule, hostname, requestedPort, autoPort);
|
|
55
59
|
|
|
56
|
-
if (actualPort === undefined) {
|
|
60
|
+
if (start.actualPort === undefined) {
|
|
57
61
|
throw new AgentKitError("runtime_error", "Could not determine the AgentKit dev server port.");
|
|
58
62
|
}
|
|
59
63
|
|
|
64
|
+
const actualPort = start.actualPort;
|
|
65
|
+
|
|
60
66
|
return {
|
|
61
67
|
capsule,
|
|
62
68
|
hostname,
|
|
69
|
+
requestedPort: options.port === undefined || options.port === 0 ? null : options.port,
|
|
63
70
|
port: actualPort,
|
|
71
|
+
portConflict: start.portConflict,
|
|
64
72
|
urls: {
|
|
65
73
|
chat: `http://${hostname}:${actualPort}`,
|
|
66
74
|
api: `http://${hostname}:${actualPort}/v1/chat`,
|
|
67
75
|
inspect: `http://${hostname}:${actualPort}/_agentkit`,
|
|
68
76
|
},
|
|
69
77
|
storage: capsule.storagePath ? relativePath(capsule.root, capsule.storagePath) : null,
|
|
70
|
-
server,
|
|
78
|
+
server: start.server,
|
|
71
79
|
async stop() {
|
|
72
|
-
await closeServer(server);
|
|
80
|
+
await closeServer(start.server);
|
|
73
81
|
},
|
|
74
82
|
};
|
|
75
83
|
}
|
|
76
84
|
|
|
85
|
+
async function listenForDevServer(
|
|
86
|
+
capsule: LoadedAgentCapsule,
|
|
87
|
+
hostname: string,
|
|
88
|
+
requestedPort: number,
|
|
89
|
+
autoPort: boolean,
|
|
90
|
+
): Promise<{ server: Server; actualPort: number; portConflict: AgentDevServer["portConflict"] }> {
|
|
91
|
+
if (requestedPort === 0) {
|
|
92
|
+
const server = createDevHttpServer(capsule);
|
|
93
|
+
const actualPort = await listen(server, hostname, await findAvailablePort());
|
|
94
|
+
return { server, actualPort, portConflict: null };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let firstConflict: AgentDevServer["portConflict"] = null;
|
|
98
|
+
const lastPort = autoPort ? requestedPort + 50 : requestedPort;
|
|
99
|
+
|
|
100
|
+
for (let port = requestedPort; port <= lastPort; port += 1) {
|
|
101
|
+
const server = createDevHttpServer(capsule);
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const actualPort = await listen(server, hostname, port);
|
|
105
|
+
return { server, actualPort, portConflict: port === requestedPort ? null : firstConflict };
|
|
106
|
+
} catch (error) {
|
|
107
|
+
await closeServerAfterListenError(server);
|
|
108
|
+
|
|
109
|
+
if (!autoPort || !isAddressInUseError(error)) {
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
firstConflict ??= {
|
|
114
|
+
port,
|
|
115
|
+
agent: await detectAgentKitOccupant(hostname, port),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
throw new AgentKitError(
|
|
121
|
+
"runtime_error",
|
|
122
|
+
`Could not allocate a local AgentKit dev server port after ${requestedPort}-${lastPort}.`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function createDevHttpServer(capsule: LoadedAgentCapsule): Server {
|
|
127
|
+
const channelDedupeKeys = new Set<string>();
|
|
128
|
+
return createHttpServer((incoming, outgoing) => {
|
|
129
|
+
void handleNodeRequest(capsule, incoming, outgoing, channelDedupeKeys);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
77
133
|
async function handleNodeRequest(
|
|
78
134
|
capsule: LoadedAgentCapsule,
|
|
79
135
|
incoming: IncomingMessage,
|
|
@@ -163,9 +219,10 @@ async function handleDevServerRequest(
|
|
|
163
219
|
|
|
164
220
|
if (request.method === "POST" && (route === "/v1/chat" || route === "/chat")) {
|
|
165
221
|
const body = await readJsonBody(request);
|
|
166
|
-
const message =
|
|
222
|
+
const { message, conversationId } = readChatRequest(body);
|
|
167
223
|
const result = await runAgentMessage(capsule, {
|
|
168
224
|
message,
|
|
225
|
+
conversationId,
|
|
169
226
|
signal: request.signal,
|
|
170
227
|
});
|
|
171
228
|
|
|
@@ -280,6 +337,7 @@ async function handlePortableChannel(
|
|
|
280
337
|
|
|
281
338
|
const result = await runAgentMessage(capsule, {
|
|
282
339
|
message: event.message.content,
|
|
340
|
+
conversationId: channelConversationId(channel.name, event.externalIdentity.key),
|
|
283
341
|
signal: request.signal,
|
|
284
342
|
});
|
|
285
343
|
deliveries.push({
|
|
@@ -377,20 +435,61 @@ async function readJsonBody(request: Request): Promise<unknown> {
|
|
|
377
435
|
}
|
|
378
436
|
}
|
|
379
437
|
|
|
380
|
-
function
|
|
381
|
-
if (!isRecord(body)
|
|
438
|
+
function readChatRequest(body: unknown): { message: string; conversationId?: string } {
|
|
439
|
+
if (!isRecord(body)) {
|
|
440
|
+
throw new AgentKitError(
|
|
441
|
+
"validation_error",
|
|
442
|
+
'Expected body shape: {"message":{"role":"user","content":"hello"},"conversationId":"optional-id"}.',
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const message = readUserMessage(body.message);
|
|
447
|
+
const conversationId = readOptionalConversationId(body.conversationId);
|
|
448
|
+
|
|
449
|
+
return { message, conversationId };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function readUserMessage(message: unknown): string {
|
|
453
|
+
if (typeof message === "string") {
|
|
454
|
+
const content = message.trim();
|
|
455
|
+
|
|
456
|
+
if (!content) {
|
|
457
|
+
throw new AgentKitError("validation_error", "message must be a non-empty string.");
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return content;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (!isRecord(message)) {
|
|
382
464
|
throw new AgentKitError("validation_error", 'Expected body shape: {"message":{"role":"user","content":"hello"}}.');
|
|
383
465
|
}
|
|
384
466
|
|
|
385
|
-
if (
|
|
467
|
+
if (message.role !== "user") {
|
|
386
468
|
throw new AgentKitError("validation_error", 'message.role must be "user".');
|
|
387
469
|
}
|
|
388
470
|
|
|
389
|
-
if (typeof
|
|
471
|
+
if (typeof message.content !== "string" || message.content.trim().length === 0) {
|
|
390
472
|
throw new AgentKitError("validation_error", "message.content must be a non-empty string.");
|
|
391
473
|
}
|
|
392
474
|
|
|
393
|
-
return
|
|
475
|
+
return message.content.trim();
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function readOptionalConversationId(value: unknown): string | undefined {
|
|
479
|
+
if (value === undefined || value === null) {
|
|
480
|
+
return undefined;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
484
|
+
throw new AgentKitError("validation_error", "conversationId must be a non-empty string when provided.");
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return value.trim();
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function channelConversationId(channelName: string, externalIdentityKey: string): string {
|
|
491
|
+
const hash = createHash("sha256").update(`${channelName}:${externalIdentityKey}`).digest("hex").slice(0, 32);
|
|
492
|
+
return `chn_${hash}`;
|
|
394
493
|
}
|
|
395
494
|
|
|
396
495
|
function readToolInput(body: unknown): unknown {
|
|
@@ -805,6 +904,7 @@ function renderChatApp(capsule: LoadedAgentCapsule, baseUrl: string, env: Record
|
|
|
805
904
|
const textarea = document.querySelector("#message");
|
|
806
905
|
const messages = document.querySelector("#messages");
|
|
807
906
|
const button = form.querySelector("button");
|
|
907
|
+
let conversationId = null;
|
|
808
908
|
|
|
809
909
|
function addMessage(role, content, className = "") {
|
|
810
910
|
const wrapper = document.createElement("article");
|
|
@@ -830,7 +930,7 @@ function renderChatApp(capsule: LoadedAgentCapsule, baseUrl: string, env: Record
|
|
|
830
930
|
const response = await fetch("/v1/chat", {
|
|
831
931
|
method: "POST",
|
|
832
932
|
headers: { "content-type": "application/json" },
|
|
833
|
-
body: JSON.stringify({ message: { role: "user", content } }),
|
|
933
|
+
body: JSON.stringify({ message: { role: "user", content }, conversationId }),
|
|
834
934
|
});
|
|
835
935
|
const data = await response.json();
|
|
836
936
|
|
|
@@ -838,6 +938,7 @@ function renderChatApp(capsule: LoadedAgentCapsule, baseUrl: string, env: Record
|
|
|
838
938
|
throw new Error(data.error?.message || "Chat request failed.");
|
|
839
939
|
}
|
|
840
940
|
|
|
941
|
+
conversationId = data.conversationId || conversationId;
|
|
841
942
|
addMessage(data.message.role, data.message.content);
|
|
842
943
|
} catch (error) {
|
|
843
944
|
addMessage("error", error instanceof Error ? error.message : String(error), "error");
|
|
@@ -881,6 +982,19 @@ function listen(server: Server, hostname: string, port: number): Promise<number>
|
|
|
881
982
|
});
|
|
882
983
|
}
|
|
883
984
|
|
|
985
|
+
async function closeServerAfterListenError(server: Server): Promise<void> {
|
|
986
|
+
if (!server.listening) {
|
|
987
|
+
try {
|
|
988
|
+
server.close();
|
|
989
|
+
} catch {
|
|
990
|
+
// The server never reached the listening state.
|
|
991
|
+
}
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
await closeServer(server);
|
|
996
|
+
}
|
|
997
|
+
|
|
884
998
|
function closeServer(server: Server): Promise<void> {
|
|
885
999
|
return new Promise((resolvePromise, reject) => {
|
|
886
1000
|
server.close((error) => {
|
|
@@ -894,6 +1008,30 @@ function closeServer(server: Server): Promise<void> {
|
|
|
894
1008
|
});
|
|
895
1009
|
}
|
|
896
1010
|
|
|
1011
|
+
function isAddressInUseError(error: unknown): boolean {
|
|
1012
|
+
return error instanceof Error && "code" in error && error.code === "EADDRINUSE";
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
async function detectAgentKitOccupant(hostname: string, port: number): Promise<string | undefined> {
|
|
1016
|
+
const controller = new AbortController();
|
|
1017
|
+
const timeout = setTimeout(() => controller.abort(), 300);
|
|
1018
|
+
|
|
1019
|
+
try {
|
|
1020
|
+
const response = await fetch(`http://${hostname}:${port}/_agentkit`, { signal: controller.signal });
|
|
1021
|
+
const payload = await response.json().catch(() => null);
|
|
1022
|
+
|
|
1023
|
+
if (payload && typeof payload === "object" && "agent" in payload && typeof payload.agent === "string") {
|
|
1024
|
+
return payload.agent;
|
|
1025
|
+
}
|
|
1026
|
+
} catch {
|
|
1027
|
+
return undefined;
|
|
1028
|
+
} finally {
|
|
1029
|
+
clearTimeout(timeout);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
return undefined;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
897
1035
|
function findAvailablePort(): Promise<number> {
|
|
898
1036
|
return new Promise((resolve, reject) => {
|
|
899
1037
|
const server = createNetServer();
|
|
@@ -42,17 +42,31 @@ export async function assertRuntimeContract(
|
|
|
42
42
|
const chat = await fetchJson(`${baseUrl}/chat`, {
|
|
43
43
|
method: "POST",
|
|
44
44
|
headers: { "content-type": "application/json" },
|
|
45
|
-
body: JSON.stringify({ message: { role: "user", content: "
|
|
45
|
+
body: JSON.stringify({ message: { role: "user", content: "my name is Ada" } }),
|
|
46
46
|
});
|
|
47
47
|
expect(chat.status).toBe(200);
|
|
48
48
|
expect(chat.body).toMatchObject({
|
|
49
49
|
message: {
|
|
50
50
|
role: "assistant",
|
|
51
|
-
content: "Echo:
|
|
51
|
+
content: "Echo: my name is Ada",
|
|
52
52
|
},
|
|
53
53
|
});
|
|
54
54
|
expect(typeof chat.body.conversationId).toBe("string");
|
|
55
55
|
|
|
56
|
+
const continuedChat = await fetchJson(`${baseUrl}/chat`, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: { "content-type": "application/json" },
|
|
59
|
+
body: JSON.stringify({ message: "what is my name?", conversationId: chat.body.conversationId }),
|
|
60
|
+
});
|
|
61
|
+
expect(continuedChat.status).toBe(200);
|
|
62
|
+
expect(continuedChat.body).toMatchObject({
|
|
63
|
+
conversationId: chat.body.conversationId,
|
|
64
|
+
message: {
|
|
65
|
+
role: "assistant",
|
|
66
|
+
content: "Your name is Ada.",
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
56
70
|
const tool = await fetchJson(`${baseUrl}/tools/${encodeURIComponent(options.tool.name)}`, {
|
|
57
71
|
method: "POST",
|
|
58
72
|
headers: { "content-type": "application/json" },
|