@andreprado/agentkit 0.1.0-alpha.3 → 0.1.0-alpha.5

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.
@@ -0,0 +1,31 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ export function runInheritedCommand(command: string, args: string[], cwd: string): Promise<number> {
4
+ return new Promise((resolvePromise, reject) => {
5
+ const child = spawn(command, args, {
6
+ cwd,
7
+ stdio: "inherit",
8
+ });
9
+
10
+ child.once("error", reject);
11
+ child.once("close", (code) => resolvePromise(code ?? 1));
12
+ });
13
+ }
14
+
15
+ export async function waitForShutdown(stop: () => Promise<void>): Promise<void> {
16
+ await new Promise<void>((resolve) => {
17
+ let stopped = false;
18
+
19
+ const shutdown = () => {
20
+ if (stopped) {
21
+ return;
22
+ }
23
+
24
+ stopped = true;
25
+ void stop().finally(resolve);
26
+ };
27
+
28
+ process.once("SIGINT", shutdown);
29
+ process.once("SIGTERM", shutdown);
30
+ });
31
+ }
@@ -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
 
@@ -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 conversation = store.createConversation({
41
+ const conversationId = normalizeConversationId(options.conversationId);
42
+ const { conversation, previousMessages } = resolveConversation(store, {
42
43
  agentName: capsule.config.name,
43
- title: titleFromMessage(content),
44
+ conversationId,
45
+ firstMessage: content,
44
46
  });
47
+ const messages: AgentMessage[] = [...previousMessages, { role: "user", content }];
45
48
  const userMessage = store.appendMessage({
46
49
  conversationId: conversation.id,
47
50
  role: "user",
@@ -116,6 +119,58 @@ export async function runAgentMessage(
116
119
  }
117
120
  }
118
121
 
122
+ function resolveConversation(
123
+ store: Awaited<ReturnType<typeof openPreparedCapsuleStore>>["store"],
124
+ input: {
125
+ agentName: string;
126
+ conversationId?: string;
127
+ firstMessage: string;
128
+ },
129
+ ): { conversation: ConversationSummary; previousMessages: AgentMessage[] } {
130
+ if (input.conversationId) {
131
+ const existing = store.getConversation(input.conversationId);
132
+
133
+ if (existing) {
134
+ return {
135
+ conversation: existing,
136
+ previousMessages: toProviderMessages(existing),
137
+ };
138
+ }
139
+ }
140
+
141
+ const conversation = store.createConversation({
142
+ id: input.conversationId,
143
+ agentName: input.agentName,
144
+ title: titleFromMessage(input.firstMessage),
145
+ });
146
+
147
+ return {
148
+ conversation,
149
+ previousMessages: [],
150
+ };
151
+ }
152
+
153
+ function toProviderMessages(conversation: ConversationRecord): AgentMessage[] {
154
+ return conversation.messages.map((message) => ({
155
+ role: message.role,
156
+ content: message.content,
157
+ }));
158
+ }
159
+
160
+ function normalizeConversationId(value: string | undefined): string | undefined {
161
+ if (value === undefined) {
162
+ return undefined;
163
+ }
164
+
165
+ const conversationId = value.trim();
166
+
167
+ if (!conversationId) {
168
+ throw new AgentKitError("validation_error", "conversationId must be a non-empty string when provided.");
169
+ }
170
+
171
+ return conversationId;
172
+ }
173
+
119
174
  function withToolVisibilityInstructions(instructions: string, tools: LoadedAgentCapsule["config"]["tools"] = []): string {
120
175
  const internalTools = tools.filter((tool) => tool.visibility === "internal").map((tool) => tool.name).sort();
121
176
 
@@ -1,3 +1,4 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
3
  import { createServer as createHttpServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
3
4
  import { createServer as createNetServer } from "node:net";
@@ -21,12 +22,18 @@ import { runToolFromCwd } from "./tool-runner";
21
22
  export type AgentDevServerOptions = {
22
23
  port?: number;
23
24
  hostname?: string;
25
+ basePort?: number;
24
26
  };
25
27
 
26
28
  export type AgentDevServer = {
27
29
  capsule: LoadedAgentCapsule;
28
30
  hostname: string;
31
+ requestedPort: number | null;
29
32
  port: number;
33
+ portConflict: {
34
+ port: number;
35
+ agent?: string;
36
+ } | null;
30
37
  urls: {
31
38
  chat: string;
32
39
  api: string;
@@ -46,34 +53,83 @@ export async function startAgentDevServer(
46
53
  ): Promise<AgentDevServer> {
47
54
  const capsule = await loadAgentCapsule(cwd);
48
55
  const hostname = options.hostname ?? DEFAULT_HOSTNAME;
49
- const port = options.port === 0 ? await findAvailablePort() : options.port ?? DEFAULT_PORT;
50
- const channelDedupeKeys = new Set<string>();
51
- const server = createHttpServer((incoming, outgoing) => {
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 = readUserMessage(body);
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 readUserMessage(body: unknown): string {
381
- if (!isRecord(body) || !isRecord(body.message)) {
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 (body.message.role !== "user") {
467
+ if (message.role !== "user") {
386
468
  throw new AgentKitError("validation_error", 'message.role must be "user".');
387
469
  }
388
470
 
389
- if (typeof body.message.content !== "string" || body.message.content.trim().length === 0) {
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 body.message.content;
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: "contract hello" } }),
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: contract hello",
51
+ content: "Echo: my name is Ada",
52
52
  },
53
53
  });
54
54
  expect(typeof chat.body.conversationId).toBe("string");
55
55
 
56
+ const continuedChat = await fetchJson(`${baseUrl}/chat`, {
57
+ method: "POST",
58
+ headers: { "content-type": "application/json" },
59
+ body: JSON.stringify({ message: "what is my name?", conversationId: chat.body.conversationId }),
60
+ });
61
+ expect(continuedChat.status).toBe(200);
62
+ expect(continuedChat.body).toMatchObject({
63
+ conversationId: chat.body.conversationId,
64
+ message: {
65
+ role: "assistant",
66
+ content: "Your name is Ada.",
67
+ },
68
+ });
69
+
56
70
  const tool = await fetchJson(`${baseUrl}/tools/${encodeURIComponent(options.tool.name)}`, {
57
71
  method: "POST",
58
72
  headers: { "content-type": "application/json" },
@@ -542,9 +542,33 @@ export class AgentKitConversationStore extends DurableObject {
542
542
  return this.persistMessages(await request.json());
543
543
  }
544
544
 
545
+ if (request.method === "GET" && url.pathname === "/messages") {
546
+ return this.getMessages(url.searchParams.get("conversationId"));
547
+ }
548
+
545
549
  return Response.json({ error: { code: "not_found", message: "Unknown Durable Object route." } }, { status: 404 });
546
550
  }
547
551
 
552
+ getMessages(conversationId) {
553
+ if (!conversationId) {
554
+ return Response.json({ messages: [] });
555
+ }
556
+
557
+ const rows = this.sql.exec(
558
+ "SELECT role, content FROM messages WHERE conversation_id = ? ORDER BY rowid ASC",
559
+ String(conversationId),
560
+ ).toArray();
561
+
562
+ return Response.json({
563
+ messages: rows
564
+ .filter((row) => row.role === "user" || row.role === "assistant")
565
+ .map((row) => ({
566
+ role: String(row.role),
567
+ content: String(row.content),
568
+ })),
569
+ });
570
+ }
571
+
548
572
  persistMessages(input) {
549
573
  const now = new Date().toISOString();
550
574
  const conversationId = String(input.conversationId || crypto.randomUUID());
@@ -915,7 +939,15 @@ async function handleChat(request, env) {
915
939
  );
916
940
  }
917
941
 
918
- const conversationId = String(body.conversationId || crypto.randomUUID());
942
+ let conversationId;
943
+
944
+ try {
945
+ conversationId = normalizeConversationId(body.conversationId) ?? crypto.randomUUID();
946
+ } catch (error) {
947
+ return hostedErrorResponse(error);
948
+ }
949
+
950
+ const previousMessages = await loadConversationMessages(env, conversationId).catch(() => []);
919
951
  let content;
920
952
  const toolCalls = [];
921
953
 
@@ -943,7 +975,7 @@ async function handleChat(request, env) {
943
975
 
944
976
  if (content === undefined) {
945
977
  try {
946
- content = await callHostedProvider(env, message, request.signal);
978
+ content = await callHostedProvider(env, [...previousMessages, { role: "user", content: message }], request.signal);
947
979
  } catch (error) {
948
980
  await persistFailure(env, conversationId, message, error);
949
981
  return hostedErrorResponse(error);
@@ -982,7 +1014,7 @@ function isRecord(value) {
982
1014
  return typeof value === "object" && value !== null && !Array.isArray(value);
983
1015
  }
984
1016
 
985
- async function callHostedProvider(env, message, signal) {
1017
+ async function callHostedProvider(env, messages, signal) {
986
1018
  const adapter = getProviderAdapter(manifest.provider);
987
1019
  const result = await adapter.run({
988
1020
  agent: {
@@ -991,7 +1023,7 @@ async function callHostedProvider(env, message, signal) {
991
1023
  provider: manifest.provider,
992
1024
  },
993
1025
  instructions: withToolVisibilityInstructions(instructions, hostedTools),
994
- messages: [{ role: "user", content: message }],
1026
+ messages,
995
1027
  tools: hostedTools,
996
1028
  toolRuntime: createHostedProviderToolRuntime(env, signal),
997
1029
  env,
@@ -1570,6 +1602,49 @@ async function persist(env, input) {
1570
1602
  return response.json();
1571
1603
  }
1572
1604
 
1605
+ async function loadConversationMessages(env, conversationId) {
1606
+ if (!env.AGENTKIT_CONVERSATIONS || !conversationId) {
1607
+ return [];
1608
+ }
1609
+
1610
+ const id = env.AGENTKIT_CONVERSATIONS.idFromName(conversationId);
1611
+ const stub = env.AGENTKIT_CONVERSATIONS.get(id);
1612
+ const response = await stub.fetch(
1613
+ \`https://agentkit.internal/messages?conversationId=\${encodeURIComponent(conversationId)}\`,
1614
+ );
1615
+
1616
+ if (!response.ok) {
1617
+ return [];
1618
+ }
1619
+
1620
+ const payload = await response.json();
1621
+ return Array.isArray(payload.messages)
1622
+ ? payload.messages.filter((message) =>
1623
+ message &&
1624
+ typeof message === "object" &&
1625
+ (message.role === "user" || message.role === "assistant") &&
1626
+ typeof message.content === "string"
1627
+ )
1628
+ : [];
1629
+ }
1630
+
1631
+ function normalizeConversationId(value) {
1632
+ if (value === undefined || value === null) {
1633
+ return null;
1634
+ }
1635
+
1636
+ if (typeof value !== "string") {
1637
+ throw agentKitError("validation_error", "conversationId must be a non-empty string when provided.");
1638
+ }
1639
+
1640
+ const conversationId = value.trim();
1641
+ if (!conversationId) {
1642
+ throw agentKitError("validation_error", "conversationId must be a non-empty string when provided.");
1643
+ }
1644
+
1645
+ return conversationId;
1646
+ }
1647
+
1573
1648
  function agentKitError(code, message) {
1574
1649
  const error = new Error(message);
1575
1650
  error.code = code;
@@ -218,10 +218,10 @@ export class SqliteAgentKitStore {
218
218
  this.db.close();
219
219
  }
220
220
 
221
- createConversation(input: { agentName: string; title: string | null; now?: string }): ConversationSummary {
221
+ createConversation(input: { id?: string; agentName: string; title: string | null; now?: string }): ConversationSummary {
222
222
  return this.sqlite(() => {
223
223
  const now = input.now ?? new Date().toISOString();
224
- const id = randomId();
224
+ const id = input.id ?? randomId();
225
225
 
226
226
  this.db
227
227
  .prepare(