@andreprado/agentkit 0.1.0-alpha.14 → 0.1.0-alpha.15

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.
Files changed (42) hide show
  1. package/README.md +2 -0
  2. package/docs/guides/run-evals.md +73 -25
  3. package/docs/llms-full.txt +24 -9
  4. package/docs/llms.txt +2 -0
  5. package/package.json +1 -1
  6. package/src/cli/cloud-client.ts +30 -10
  7. package/src/cli/deploy-readiness.ts +32 -11
  8. package/src/cli/index.ts +20 -6
  9. package/src/cloud/client.ts +4 -3
  10. package/src/cloud/contracts.ts +1 -1
  11. package/src/create-project.ts +1 -1
  12. package/src/index.ts +24 -1
  13. package/src/providers/pi.ts +14 -1
  14. package/src/providers/test.ts +36 -0
  15. package/src/runtime/chat.ts +59 -42
  16. package/src/runtime/config.ts +11 -0
  17. package/src/runtime/core/manifest.ts +3 -3
  18. package/src/runtime/deploy-readiness.ts +3 -3
  19. package/src/runtime/dev-server.ts +171 -13
  20. package/src/runtime/env.ts +8 -3
  21. package/src/runtime/evals.ts +404 -69
  22. package/src/runtime/inspect.ts +12 -0
  23. package/src/runtime/prompt-context.ts +141 -0
  24. package/src/runtime/runtime-contract.ts +17 -7
  25. package/src/runtime/targets/cloudflare/build.ts +23 -3
  26. package/src/runtime/targets/container/server.ts +1 -1
  27. package/src/runtime/targets/vps/deploy.ts +25 -8
  28. package/src/runtime/tool-runner.ts +7 -0
  29. package/src/runtime/tools.ts +8 -2
  30. package/src/templates/blank.ts +8 -3
  31. package/src/templates/dentista.ts +18 -10
  32. package/src/templates/skills/agentkit-build-agent/SKILL.md +6 -5
  33. package/src/templates/skills/agentkit-build-agent/templates/appointment-intake.instructions.md +2 -1
  34. package/src/templates/skills/agentkit-capsule/SKILL.md +1 -1
  35. package/src/templates/skills/agentkit-evals/SKILL.md +53 -13
  36. package/src/templates/skills/agentkit-evals/templates/multi-turn.eval.md +13 -6
  37. package/src/templates/skills/agentkit-evals/templates/no-leak.eval.md +8 -4
  38. package/src/templates/skills/agentkit-evals/templates/smoke.eval.md +8 -4
  39. package/src/templates/skills/agentkit-evals/templates/tool-call.eval.md +16 -7
  40. package/src/templates/skills/agentkit-prompts/SKILL.md +3 -1
  41. package/src/templates/skills/agentkit-tools/SKILL.md +2 -1
  42. package/src/templates/support.ts +8 -3
@@ -0,0 +1,141 @@
1
+ export type RuntimePromptContextOptions = {
2
+ timeZone?: string | null;
3
+ env?: Record<string, string | undefined>;
4
+ now?: Date;
5
+ clock?: RuntimeClockContext;
6
+ };
7
+
8
+ export type RuntimePromptContext = {
9
+ isoTimestamp: string;
10
+ timeZone: string;
11
+ localDate: string;
12
+ localWeekday: string;
13
+ localDateTime: string;
14
+ };
15
+
16
+ export type RuntimeClockContext = RuntimePromptContext & {
17
+ now: Date;
18
+ };
19
+
20
+ const FALLBACK_TIME_ZONE = "UTC";
21
+ const LOCALE = "en-US";
22
+
23
+ export function appendRuntimePromptContext(
24
+ instructions: string,
25
+ options: RuntimePromptContextOptions = {},
26
+ ): string {
27
+ const context = options.clock ?? buildRuntimePromptContext(options);
28
+
29
+ return `${instructions.trimEnd()}
30
+
31
+ Runtime context:
32
+ - Current timestamp: ${context.isoTimestamp}
33
+ - Time zone: ${context.timeZone}
34
+ - Local date: ${context.localDate}
35
+ - Local weekday: ${context.localWeekday}
36
+ - Local date and time: ${context.localDateTime}
37
+
38
+ Use this runtime context to interpret relative dates and times such as today, tomorrow, next Friday, and morning/afternoon. If a scheduling request depends on a different user or business time zone, ask for confirmation before booking.`;
39
+ }
40
+
41
+ export function buildRuntimePromptContext(options: RuntimePromptContextOptions = {}): RuntimePromptContext {
42
+ const clock = buildRuntimeClockContext(options);
43
+
44
+ return {
45
+ isoTimestamp: clock.isoTimestamp,
46
+ timeZone: clock.timeZone,
47
+ localDate: clock.localDate,
48
+ localWeekday: clock.localWeekday,
49
+ localDateTime: clock.localDateTime,
50
+ };
51
+ }
52
+
53
+ export function buildRuntimeClockContext(options: RuntimePromptContextOptions = {}): RuntimeClockContext {
54
+ if (options.clock) {
55
+ return options.clock;
56
+ }
57
+
58
+ const now = new Date((options.now ?? new Date()).getTime());
59
+
60
+ if (Number.isNaN(now.getTime())) {
61
+ throw new Error("runtime prompt context requires a valid Date.");
62
+ }
63
+
64
+ const timeZone = resolveRuntimeTimeZone(options);
65
+
66
+ return {
67
+ now,
68
+ isoTimestamp: now.toISOString(),
69
+ timeZone,
70
+ localDate: formatLocalDate(now, timeZone),
71
+ localWeekday: formatDateTime(now, timeZone, {
72
+ weekday: "long",
73
+ }),
74
+ localDateTime: formatDateTime(now, timeZone, {
75
+ weekday: "long",
76
+ year: "numeric",
77
+ month: "long",
78
+ day: "numeric",
79
+ hour: "numeric",
80
+ minute: "2-digit",
81
+ timeZoneName: "short",
82
+ }),
83
+ };
84
+ }
85
+
86
+ export function resolveRuntimeTimeZone(options: RuntimePromptContextOptions = {}): string {
87
+ if (options.timeZone?.trim()) {
88
+ return normalizeTimeZone(options.timeZone, "timeZone");
89
+ }
90
+
91
+ const explicitEnvTimeZone = options.env?.AGENTKIT_TIME_ZONE?.trim();
92
+
93
+ if (explicitEnvTimeZone) {
94
+ return normalizeTimeZone(explicitEnvTimeZone, "AGENTKIT_TIME_ZONE");
95
+ }
96
+
97
+ const processTimeZone = options.env?.TZ?.trim();
98
+
99
+ if (processTimeZone && isValidTimeZone(processTimeZone)) {
100
+ return processTimeZone;
101
+ }
102
+
103
+ return normalizeTimeZone(Intl.DateTimeFormat().resolvedOptions().timeZone || FALLBACK_TIME_ZONE, "runtime time zone");
104
+ }
105
+
106
+ export function isValidTimeZone(value: string): boolean {
107
+ try {
108
+ new Intl.DateTimeFormat(LOCALE, { timeZone: value }).format(new Date(0));
109
+ return true;
110
+ } catch {
111
+ return false;
112
+ }
113
+ }
114
+
115
+ function normalizeTimeZone(value: string, source: string): string {
116
+ const timeZone = value.trim();
117
+
118
+ if (!isValidTimeZone(timeZone)) {
119
+ throw new Error(`${source} must be a valid IANA time zone such as "UTC" or "America/New_York".`);
120
+ }
121
+
122
+ return timeZone;
123
+ }
124
+
125
+ function formatLocalDate(date: Date, timeZone: string): string {
126
+ const parts = new Intl.DateTimeFormat(LOCALE, {
127
+ timeZone,
128
+ year: "numeric",
129
+ month: "2-digit",
130
+ day: "2-digit",
131
+ }).formatToParts(date);
132
+ const byType = Object.fromEntries(parts.map((part) => [part.type, part.value]));
133
+ return `${byType.year}-${byType.month}-${byType.day}`;
134
+ }
135
+
136
+ function formatDateTime(date: Date, timeZone: string, options: Intl.DateTimeFormatOptions): string {
137
+ return new Intl.DateTimeFormat(LOCALE, {
138
+ ...options,
139
+ timeZone,
140
+ }).format(date);
141
+ }
@@ -7,6 +7,10 @@ export type RuntimeContractTarget = {
7
7
 
8
8
  export type RuntimeContractOptions = {
9
9
  expectedAgentName: string;
10
+ auth?: {
11
+ chat?: HeadersInit;
12
+ admin?: HeadersInit;
13
+ };
10
14
  tool: {
11
15
  name: string;
12
16
  input: unknown;
@@ -20,20 +24,20 @@ export async function assertRuntimeContract(
20
24
  ): Promise<void> {
21
25
  const baseUrl = target.baseUrl.replace(/\/$/, "");
22
26
 
23
- const root = await fetch(`${baseUrl}/`);
27
+ const root = await fetch(`${baseUrl}/`, { headers: options.auth?.chat });
24
28
  expect(root.status).toBe(200);
25
29
 
26
30
  const health = await fetchJson(`${baseUrl}/health`);
27
31
  expect(health.status).toBe(200);
28
32
  expect(health.body).toMatchObject({ status: "ok" });
29
33
 
30
- const inspect = await fetchJson(`${baseUrl}/_agentkit`);
34
+ const inspect = await fetchJson(`${baseUrl}/_agentkit`, { headers: options.auth?.admin });
31
35
  expect(inspect.status).toBe(200);
32
36
  expect(inspect.body).toMatchObject({ agent: options.expectedAgentName });
33
37
 
34
38
  const unauthorized = await fetchJson(`${baseUrl}/chat`, {
35
39
  method: "POST",
36
- headers: { "content-type": "application/json" },
40
+ headers: withHeaders({ "content-type": "application/json" }, options.auth?.chat),
37
41
  body: JSON.stringify({ message: { role: "assistant", content: "not allowed" } }),
38
42
  });
39
43
  expect(unauthorized.status).toBe(400);
@@ -41,7 +45,7 @@ export async function assertRuntimeContract(
41
45
 
42
46
  const chat = await fetchJson(`${baseUrl}/chat`, {
43
47
  method: "POST",
44
- headers: { "content-type": "application/json" },
48
+ headers: withHeaders({ "content-type": "application/json" }, options.auth?.chat),
45
49
  body: JSON.stringify({ message: { role: "user", content: "my name is Ada" } }),
46
50
  });
47
51
  expect(chat.status).toBe(200);
@@ -55,7 +59,7 @@ export async function assertRuntimeContract(
55
59
 
56
60
  const continuedChat = await fetchJson(`${baseUrl}/chat`, {
57
61
  method: "POST",
58
- headers: { "content-type": "application/json" },
62
+ headers: withHeaders({ "content-type": "application/json" }, options.auth?.chat),
59
63
  body: JSON.stringify({ message: "what is my name?", conversationId: chat.body.conversationId }),
60
64
  });
61
65
  expect(continuedChat.status).toBe(200);
@@ -69,7 +73,7 @@ export async function assertRuntimeContract(
69
73
 
70
74
  const tool = await fetchJson(`${baseUrl}/tools/${encodeURIComponent(options.tool.name)}`, {
71
75
  method: "POST",
72
- headers: { "content-type": "application/json" },
76
+ headers: withHeaders({ "content-type": "application/json" }, options.auth?.admin),
73
77
  body: JSON.stringify({ input: options.tool.input }),
74
78
  });
75
79
  expect(tool.status).toBe(200);
@@ -78,7 +82,7 @@ export async function assertRuntimeContract(
78
82
  output: options.tool.expectedOutput,
79
83
  });
80
84
 
81
- const conversations = await fetchJson(`${baseUrl}/v1/conversations`);
85
+ const conversations = await fetchJson(`${baseUrl}/v1/conversations`, { headers: options.auth?.admin });
82
86
  expect(conversations.status).toBe(200);
83
87
  expect(conversations.body.conversations.length).toBeGreaterThanOrEqual(1);
84
88
  expect(conversations.body.conversations.some((conversation: { id: string }) => conversation.id === chat.body.conversationId)).toBe(true);
@@ -91,3 +95,9 @@ async function fetchJson(url: string, init?: RequestInit): Promise<{ status: num
91
95
  body: await response.json(),
92
96
  };
93
97
  }
98
+
99
+ function withHeaders(required: HeadersInit, extra: HeadersInit | undefined): Headers {
100
+ const headers = new Headers(extra);
101
+ new Headers(required).forEach((value, key) => headers.set(key, value));
102
+ return headers;
103
+ }
@@ -32,6 +32,7 @@ export type AgentManifest = {
32
32
  runtime: "edge";
33
33
  target: "cloudflare";
34
34
  provider: SharedAgentManifest["provider"];
35
+ timeZone: SharedAgentManifest["timeZone"];
35
36
  prompt: SharedAgentManifest["prompt"];
36
37
  secrets: SharedAgentManifest["secrets"];
37
38
  tools: SharedAgentManifest["tools"];
@@ -192,6 +193,7 @@ function buildManifest(
192
193
  runtime: "edge",
193
194
  target: "cloudflare",
194
195
  provider: shared.provider,
196
+ timeZone: shared.timeZone,
195
197
  prompt: shared.prompt,
196
198
  secrets: shared.secrets,
197
199
  tools: shared.tools,
@@ -538,10 +540,12 @@ function renderCloudflareWorker(input: { manifest: AgentManifest; instructions:
538
540
  const instructionsJson = JSON.stringify(input.instructions);
539
541
  const configImportPath = JSON.stringify(input.configPath);
540
542
  const providerImportPath = JSON.stringify(resolve(moduleDir, "../../../providers/index.ts"));
543
+ const promptContextImportPath = JSON.stringify(resolve(moduleDir, "../../prompt-context.ts"));
541
544
 
542
545
  return `import { DurableObject } from "cloudflare:workers";
543
546
  import agentConfig from ${configImportPath};
544
547
  import { getProviderAdapter } from ${providerImportPath};
548
+ import { appendRuntimePromptContext, buildRuntimeClockContext } from ${promptContextImportPath};
545
549
 
546
550
  const manifest = ${manifestJson};
547
551
  const instructions = ${instructionsJson};
@@ -1123,16 +1127,25 @@ function isRecord(value) {
1123
1127
 
1124
1128
  async function callHostedProvider(env, messages, signal) {
1125
1129
  const adapter = getProviderAdapter(manifest.provider);
1130
+ const clock = buildRuntimeClockContext({
1131
+ timeZone: manifest.timeZone,
1132
+ env,
1133
+ });
1126
1134
  const result = await adapter.run({
1127
1135
  agent: {
1128
1136
  name: manifest.name,
1129
1137
  runtime: manifest.runtime,
1130
1138
  provider: manifest.provider,
1131
1139
  },
1132
- instructions: withToolVisibilityInstructions(appendHostedKnowledgePromptPolicy(instructions), hostedTools),
1140
+ instructions: appendRuntimePromptContext(
1141
+ withToolVisibilityInstructions(appendHostedKnowledgePromptPolicy(instructions), hostedTools),
1142
+ {
1143
+ clock,
1144
+ },
1145
+ ),
1133
1146
  messages,
1134
1147
  tools: hostedTools,
1135
- toolRuntime: createHostedProviderToolRuntime(env, signal),
1148
+ toolRuntime: createHostedProviderToolRuntime(env, signal, clock),
1136
1149
  env,
1137
1150
  signal,
1138
1151
  });
@@ -1618,7 +1631,7 @@ function hostedManagedSecrets() {
1618
1631
  return ["TURSO_AUTH_TOKEN", "TURSO_DATABASE_URL"];
1619
1632
  }
1620
1633
 
1621
- function createHostedProviderToolRuntime(env, signal) {
1634
+ function createHostedProviderToolRuntime(env, signal, clock) {
1622
1635
  return {
1623
1636
  definitions: hostedTools,
1624
1637
  execute(request) {
@@ -1628,6 +1641,7 @@ function createHostedProviderToolRuntime(env, signal) {
1628
1641
  env,
1629
1642
  signal,
1630
1643
  invocation: "chat",
1644
+ clock,
1631
1645
  });
1632
1646
  },
1633
1647
  };
@@ -1646,6 +1660,10 @@ async function executeHostedTool(request) {
1646
1660
 
1647
1661
  const secrets = resolveToolSecrets(tool, request.env);
1648
1662
  const database = createHostedDatabaseRunner(request.env);
1663
+ const clock = request.clock ?? buildRuntimeClockContext({
1664
+ timeZone: manifest.timeZone,
1665
+ env: request.env,
1666
+ });
1649
1667
  const output = await runToolWithTimeout(
1650
1668
  tool,
1651
1669
  input,
@@ -1653,6 +1671,7 @@ async function executeHostedTool(request) {
1653
1671
  secrets,
1654
1672
  database,
1655
1673
  signal: request.signal,
1674
+ clock,
1656
1675
  runtime: {
1657
1676
  environment: "hosted",
1658
1677
  invocation: request.invocation ?? "tool",
@@ -1717,6 +1736,7 @@ async function runToolWithTimeout(tool, input, context, timeoutMs) {
1717
1736
  secrets: context.secrets,
1718
1737
  signal: controller.signal,
1719
1738
  runtime: context.runtime,
1739
+ clock: context.clock,
1720
1740
  database: context.database,
1721
1741
  db: context.database,
1722
1742
  storage: {
@@ -12,7 +12,7 @@ export async function startContainerRuntime(options: ContainerRuntimeOptions = {
12
12
  const capsuleRoot = resolve(options.capsuleRoot ?? process.env.AGENTKIT_CAPSULE_ROOT ?? process.cwd());
13
13
  const hostname = options.hostname ?? process.env.HOST ?? "0.0.0.0";
14
14
  const port = options.port ?? parsePort(process.env.PORT) ?? 4123;
15
- return startAgentDevServer(capsuleRoot, { hostname, port });
15
+ return startAgentDevServer(capsuleRoot, { hostname, port, access: "enforce" });
16
16
  }
17
17
 
18
18
  function parsePort(value: string | undefined): number | undefined {
@@ -1,4 +1,4 @@
1
- import { mkdir, readFile, writeFile } from "node:fs/promises";
1
+ import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
 
4
4
  import type { LocalDeployState } from "../../core/deploy-state";
@@ -38,15 +38,19 @@ export async function prepareVpsDeploy(
38
38
  const manifest = JSON.parse(await readFile(container.manifestPath, "utf8")) as {
39
39
  name: string;
40
40
  secrets: string[];
41
+ access: {
42
+ mode: "private" | "token" | "public";
43
+ };
41
44
  };
42
45
 
43
46
  await mkdir(buildDir, { recursive: true });
44
47
  const envExample = renderEnvExample(manifest.secrets);
45
48
  await writeFile(composePath, renderCompose(manifest.name));
46
49
  await writeFile(envExamplePath, envExample);
47
- await writeFile(envPath, envExample);
50
+ await writeFile(envPath, envExample, { mode: 0o600 });
51
+ await chmod(envPath, 0o600);
48
52
  await writeFile(secretsPath, renderSecretsChecklist(manifest.secrets));
49
- await writeFile(handoffPath, renderProductionHandoff(options.host, manifest.name));
53
+ await writeFile(handoffPath, renderProductionHandoff(options.host, manifest.name, manifest.access.mode));
50
54
 
51
55
  const state: LocalDeployState = {
52
56
  deploy_id: `vps_${slugify(manifest.name)}`,
@@ -71,7 +75,8 @@ export async function prepareVpsDeploy(
71
75
  },
72
76
  created_at: new Date().toISOString(),
73
77
  };
74
- await writeFile(statePath, `${JSON.stringify(state, null, 2)}\n`);
78
+ await writeFile(statePath, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
79
+ await chmod(statePath, 0o600);
75
80
 
76
81
  return {
77
82
  target: "vps",
@@ -88,7 +93,7 @@ export async function prepareVpsDeploy(
88
93
  `docker compose -f ${composePath} --env-file ${envPath} config`,
89
94
  `docker compose -f ${composePath} --env-file ${envPath} up -d --build`,
90
95
  `curl -fsS https://${options.host}/health`,
91
- `curl -fsS https://${options.host}/_agentkit`,
96
+ `curl -fsS -H "Authorization: Bearer $AGENTKIT_PRIVATE_TOKEN" https://${options.host}/_agentkit`,
92
97
  ],
93
98
  };
94
99
  }
@@ -145,7 +150,7 @@ ${lines}
145
150
  `;
146
151
  }
147
152
 
148
- function renderProductionHandoff(host: string, agentName: string): string {
153
+ function renderProductionHandoff(host: string, agentName: string, accessMode: "private" | "token" | "public"): string {
149
154
  return `# VPS Production Handoff
150
155
 
151
156
  ## Deploy
@@ -169,12 +174,14 @@ docker compose -f .agentkit/build/vps/compose.yaml --env-file .agentkit/build/vp
169
174
 
170
175
  \`\`\`sh
171
176
  curl -fsS https://${host}/health
172
- curl -fsS https://${host}/_agentkit
177
+ curl -fsS -H "Authorization: Bearer $AGENTKIT_PRIVATE_TOKEN" https://${host}/_agentkit
173
178
  curl -fsS -X POST https://${host}/chat \\
174
- -H 'content-type: application/json' \\
179
+ -H 'content-type: application/json'${renderChatAuthHeader(accessMode)} \\
175
180
  -d '{"message":{"role":"user","content":"hello"}}'
176
181
  \`\`\`
177
182
 
183
+ Set AGENTKIT_PRIVATE_TOKEN for privileged AgentKit endpoints. Chat access follows access.mode: ${accessMode}.
184
+
178
185
  ## Database
179
186
 
180
187
  - Default database: SQLite at /data/agentkit.db in the agentkit_data volume.
@@ -195,6 +202,16 @@ docker compose -f .agentkit/build/vps/compose.yaml --env-file .agentkit/build/vp
195
202
  `;
196
203
  }
197
204
 
205
+ function renderChatAuthHeader(accessMode: "private" | "token" | "public"): string {
206
+ if (accessMode === "public") {
207
+ return "";
208
+ }
209
+
210
+ const secretName = accessMode === "private" ? "AGENTKIT_PRIVATE_TOKEN" : "AGENTKIT_ACCESS_TOKEN";
211
+ return ` \\
212
+ -H "Authorization: Bearer $${secretName}"`;
213
+ }
214
+
198
215
  function slugify(value: string): string {
199
216
  const slug = value
200
217
  .toLowerCase()
@@ -2,12 +2,14 @@ import { loadAgentCapsule } from "./config";
2
2
  import { openPreparedCapsuleStore } from "./database";
3
3
  import { loadCapsuleEnv } from "./env";
4
4
  import { isAgentKitError } from "./errors";
5
+ import { buildRuntimeClockContext } from "./prompt-context";
5
6
  import { createToolRuntime, type ToolCallResult } from "./tools";
6
7
 
7
8
  export type RunToolOptions = {
8
9
  name: string;
9
10
  input?: unknown;
10
11
  signal?: AbortSignal;
12
+ now?: Date;
11
13
  };
12
14
 
13
15
  export async function runToolFromCwd(cwd: string, options: RunToolOptions): Promise<ToolCallResult> {
@@ -30,6 +32,11 @@ export async function runToolFromCwd(cwd: string, options: RunToolOptions): Prom
30
32
  conversationId: conversation.id,
31
33
  env,
32
34
  signal: options.signal,
35
+ clock: buildRuntimeClockContext({
36
+ timeZone: capsule.config.timeZone,
37
+ env,
38
+ now: options.now,
39
+ }),
33
40
  runtime: {
34
41
  environment: "local",
35
42
  invocation: "tool",
@@ -2,6 +2,7 @@ import type { AgentTool, JsonSchema, ToolContext, ToolRenderResult, ToolRuntimeC
2
2
  import type { SqliteAgentKitStore } from "../storage/sqlite";
3
3
  import { createLocalDatabaseRunner } from "./database";
4
4
  import { AgentKitError } from "./errors";
5
+ import type { RuntimeClockContext } from "./prompt-context";
5
6
 
6
7
  export type ToolCallRequest = {
7
8
  name: string;
@@ -31,6 +32,7 @@ export type ToolRuntimeOptions = {
31
32
  signal?: AbortSignal;
32
33
  defaultTimeoutMs?: number;
33
34
  runtime: ToolRuntimeContext;
35
+ clock: RuntimeClockContext;
34
36
  };
35
37
 
36
38
  const DEFAULT_TOOL_TIMEOUT_MS = 30_000;
@@ -89,6 +91,7 @@ async function executeToolCall(
89
91
  secrets,
90
92
  database,
91
93
  runtime: options.runtime,
94
+ clock: options.clock,
92
95
  signal: options.signal,
93
96
  },
94
97
  tool.timeoutMs ?? options.defaultTimeoutMs ?? DEFAULT_TOOL_TIMEOUT_MS,
@@ -98,7 +101,7 @@ async function executeToolCall(
98
101
  validateSchema(output, tool.outputSchema, `tool "${tool.name}" output`);
99
102
  }
100
103
 
101
- const rendered = renderToolOutput(tool, input, output, options.runtime);
104
+ const rendered = renderToolOutput(tool, input, output, options.runtime, options.clock);
102
105
 
103
106
  options.store.completeToolCall({
104
107
  toolCallId: toolCall.id,
@@ -135,6 +138,7 @@ function renderToolOutput(
135
138
  input: unknown,
136
139
  output: unknown,
137
140
  runtime: ToolRuntimeContext,
141
+ clock: RuntimeClockContext,
138
142
  ): ToolRenderResult | undefined {
139
143
  if (!tool.render) {
140
144
  return undefined;
@@ -143,6 +147,7 @@ function renderToolOutput(
143
147
  const rendered = tool.render(output, {
144
148
  input,
145
149
  runtime,
150
+ clock,
146
151
  });
147
152
 
148
153
  if (typeof rendered === "string") {
@@ -278,7 +283,7 @@ function collectConfiguredSecretValues(tool: AgentTool | undefined, env: Record<
278
283
  async function runToolWithTimeout(
279
284
  tool: AgentTool,
280
285
  input: unknown,
281
- context: Pick<ToolContext, "secrets" | "database" | "runtime"> & { signal?: AbortSignal },
286
+ context: Pick<ToolContext, "secrets" | "database" | "runtime" | "clock"> & { signal?: AbortSignal },
282
287
  timeoutMs: number,
283
288
  ): Promise<unknown> {
284
289
  const controller = new AbortController();
@@ -306,6 +311,7 @@ async function runToolWithTimeout(
306
311
  secrets: context.secrets,
307
312
  signal: controller.signal,
308
313
  runtime: context.runtime,
314
+ clock: context.clock,
309
315
  database: context.database,
310
316
  db: context.database,
311
317
  storage: {
@@ -124,13 +124,18 @@ Answer clearly, ask for missing context when needed, and do not claim to have pe
124
124
  },
125
125
  {
126
126
  path: "evals/smoke.eval.ts",
127
- contents: `export default {
127
+ contents: `import { defineEval } from "@andreprado/agentkit";
128
+
129
+ export default defineEval({
128
130
  name: "smoke",
129
131
  input: "Say hello in one short sentence.",
130
132
  expect: {
131
- contains: "hello",
133
+ response: {
134
+ caseInsensitiveContains: "hello",
135
+ maxLength: 160,
136
+ },
132
137
  },
133
- };
138
+ });
134
139
  `,
135
140
  },
136
141
  {
@@ -83,6 +83,7 @@ export default defineAgent({
83
83
  name: "test",
84
84
  model: "fake",
85
85
  },
86
+ timeZone: "America/Sao_Paulo",
86
87
  instructions: "./prompts/instructions.md",
87
88
  secrets: [],
88
89
  tools: [listarHorariosDisponiveis, consultarConsulta, agendarConsulta, alterarConsulta],
@@ -282,7 +283,7 @@ export const listarHorariosDisponiveis = defineTool<ListarHorariosInput, { data:
282
283
  },
283
284
  async execute(input, ctx) {
284
285
  const date = normalizeDate(input.data);
285
- const dateError = validateAppointmentDate(date);
286
+ const dateError = validateAppointmentDate(date, ctx.clock.now);
286
287
 
287
288
  if (dateError) {
288
289
  return {
@@ -409,6 +410,7 @@ export const agendarConsulta = defineTool<AgendarConsultaInput, AgendaOutput>({
409
410
  data,
410
411
  horario,
411
412
  confirmadoPeloCliente: input.confirmadoPeloCliente,
413
+ now: ctx.clock.now,
412
414
  });
413
415
 
414
416
  if (!validation.ok) {
@@ -482,6 +484,7 @@ export const alterarConsulta = defineTool<AlterarConsultaInput, AgendaOutput>({
482
484
  data,
483
485
  horario,
484
486
  confirmadoPeloCliente: input.confirmadoPeloCliente,
487
+ now: ctx.clock.now,
485
488
  });
486
489
 
487
490
  if (!contact.ok) {
@@ -575,9 +578,9 @@ export const alterarConsulta = defineTool<AlterarConsultaInput, AgendaOutput>({
575
578
 
576
579
  async function validateScheduleRequest(
577
580
  db: DatabaseRunner,
578
- input: { data: string; horario: string; confirmadoPeloCliente: boolean },
581
+ input: { data: string; horario: string; confirmadoPeloCliente: boolean; now: Date },
579
582
  ): Promise<{ ok: boolean; mensagem: string; disponiveis: string[] }> {
580
- const dateError = validateAppointmentDate(input.data);
583
+ const dateError = validateAppointmentDate(input.data, input.now);
581
584
 
582
585
  if (dateError) {
583
586
  return {
@@ -698,7 +701,7 @@ function normalizeTime(input: string): string {
698
701
  return \`\${match[1].padStart(2, "0")}:\${match[2]}\`;
699
702
  }
700
703
 
701
- function validateAppointmentDate(data: string): string | null {
704
+ function validateAppointmentDate(data: string, now: Date): string | null {
702
705
  if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(data)) {
703
706
  return "Use a data no formato YYYY-MM-DD.";
704
707
  }
@@ -710,20 +713,20 @@ function validateAppointmentDate(data: string): string | null {
710
713
  return "Esta data nao existe. Confirme a data com o cliente.";
711
714
  }
712
715
 
713
- if (data < todayInClinicTimezone()) {
716
+ if (data < todayInClinicTimezone(now)) {
714
717
  return "Nao agende consultas em datas passadas.";
715
718
  }
716
719
 
717
720
  return null;
718
721
  }
719
722
 
720
- function todayInClinicTimezone(): string {
723
+ function todayInClinicTimezone(now: Date): string {
721
724
  const parts = new Intl.DateTimeFormat("en-US", {
722
725
  timeZone: CLINIC_TIME_ZONE,
723
726
  year: "numeric",
724
727
  month: "2-digit",
725
728
  day: "2-digit",
726
- }).formatToParts(new Date());
729
+ }).formatToParts(now);
727
730
  const byType = Object.fromEntries(parts.map((part) => [part.type, part.value]));
728
731
  return \`\${byType.year}-\${byType.month}-\${byType.day}\`;
729
732
  }
@@ -838,13 +841,18 @@ Quando o cliente quiser alterar a própria consulta:
838
841
  },
839
842
  {
840
843
  path: "evals/smoke.eval.ts",
841
- contents: `export default {
844
+ contents: `import { defineEval } from "@andreprado/agentkit";
845
+
846
+ export default defineEval({
842
847
  name: "smoke",
843
848
  input: "Oi, quero marcar uma consulta.",
844
849
  expect: {
845
- contains: "nome",
850
+ response: {
851
+ containsAny: ["nome", "Nome"],
852
+ notRegex: ["API_KEY|secret|token"],
853
+ },
846
854
  },
847
- };
855
+ });
848
856
  `,
849
857
  },
850
858
  {
@@ -13,11 +13,12 @@ Use this when the owner asks for an agent in plain language.
13
13
  2. If `AGENT_SPEC.md` does not exist, create it from the owner's plain-language request with `npm run agentkit -- spec init --brief "<owner request>"`. If it exists, update it directly before changing behavior.
14
14
  3. Infer the first useful local version from the owner's brief and the spec. Do not ask the owner to fill a form.
15
15
  4. Edit `prompts/instructions.md` for behavior, boundaries, intake questions, escalation rules, and tool-use policy.
16
- 5. Add tools only when the agent needs action, live data, authorization-sensitive data, or durable writes.
17
- 6. Add database tables to `schema.sql` or ordered `migrations/*.sql` when the agent owns records.
18
- 7. Add `sync.ts` and `seed.sql` with `npm run agentkit -- sync init` when the agent depends on external catalogs or recurring imports.
19
- 8. Add or update evals for the main flow. Prefer multi-turn `turns` evals for real conversations.
20
- 9. Keep the capsule runnable on `test/fake` unless the owner has chosen a real provider.
16
+ 5. For scheduling, deadlines, reminders, or any relative-date behavior, set `timeZone` in `agentkit.config.ts` to the business/user timezone. AgentKit injects the current date, weekday, timestamp, and timezone dynamically at runtime; do not hardcode today's date in prompts.
17
+ 6. Add tools only when the agent needs action, live data, authorization-sensitive data, or durable writes.
18
+ 7. Add database tables to `schema.sql` or ordered `migrations/*.sql` when the agent owns records.
19
+ 8. Add `sync.ts` and `seed.sql` with `npm run agentkit -- sync init` when the agent depends on external catalogs or recurring imports.
20
+ 9. Add or update evals for the main flow. Prefer multi-turn `turns` evals for real conversations.
21
+ 10. Keep the capsule runnable on `test/fake` unless the owner has chosen a real provider.
21
22
 
22
23
  ## Templates
23
24
 
@@ -13,8 +13,9 @@ Required intake:
13
13
  - Any urgency or special constraints
14
14
 
15
15
  Rules:
16
+ - Interpret today, tomorrow, weekdays, and vague time windows using the AgentKit runtime date context.
17
+ - If the user's scheduling timezone may differ from the business timezone, confirm the timezone before booking.
16
18
  - Do not diagnose, promise outcomes, or provide emergency guidance beyond directing urgent cases to appropriate human or emergency support.
17
19
  - Do not create, change, or cancel an appointment without explicit user confirmation.
18
20
  - Do not invent availability.
19
21
  - Use the scheduling tools for availability and writes.
20
-
@@ -57,6 +57,6 @@ npm run eval
57
57
  - Keep `.env`, `.agentkit/`, and `node_modules/` out of commits.
58
58
  - Keep secret names in `.env.schema`; keep secret values in ignored `.env` or hosted managed secrets.
59
59
  - Keep the first useful version runnable with `test/fake` unless the owner explicitly chooses a real provider.
60
+ - For scheduling or relative-date agents, set `timeZone` in `agentkit.config.ts`; AgentKit injects the current date, weekday, timestamp, and timezone dynamically at runtime.
60
61
  - Ask follow-up questions only when missing information blocks a safe local implementation.
61
62
  - Tell the owner when testing used `test/fake` instead of a real provider.
62
-