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

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 (56) hide show
  1. package/README.md +2 -0
  2. package/docs/guides/add-channel.md +63 -0
  3. package/docs/guides/channel-security.md +32 -0
  4. package/docs/guides/connect-telegram.md +58 -0
  5. package/docs/guides/connect-whatsapp-zapster.md +65 -0
  6. package/docs/guides/run-evals.md +73 -25
  7. package/docs/llms-full.txt +24 -9
  8. package/docs/llms.txt +2 -0
  9. package/package.json +1 -1
  10. package/src/cli/cloud-client.ts +30 -10
  11. package/src/cli/commands/channels.ts +2 -0
  12. package/src/cli/deploy-readiness.ts +32 -11
  13. package/src/cli/index.ts +20 -6
  14. package/src/cloud/client.ts +4 -3
  15. package/src/cloud/contracts.ts +1 -1
  16. package/src/create-project.ts +1 -1
  17. package/src/index.ts +110 -1
  18. package/src/providers/pi.ts +14 -1
  19. package/src/providers/test.ts +36 -0
  20. package/src/runtime/channel-test-harness.ts +2 -0
  21. package/src/runtime/channels/telegram.ts +326 -10
  22. package/src/runtime/channels/whatsapp-zapster.ts +319 -0
  23. package/src/runtime/channels.ts +47 -1
  24. package/src/runtime/chat.ts +59 -42
  25. package/src/runtime/config.ts +96 -4
  26. package/src/runtime/core/manifest.ts +35 -3
  27. package/src/runtime/deploy-readiness.ts +3 -3
  28. package/src/runtime/dev-server.ts +243 -17
  29. package/src/runtime/env.ts +8 -3
  30. package/src/runtime/evals.ts +404 -69
  31. package/src/runtime/inspect.ts +46 -0
  32. package/src/runtime/prompt-context.ts +141 -0
  33. package/src/runtime/runtime-contract.ts +17 -7
  34. package/src/runtime/targets/cloudflare/build.ts +25 -3
  35. package/src/runtime/targets/container/server.ts +1 -1
  36. package/src/runtime/targets/vps/deploy.ts +25 -8
  37. package/src/runtime/tool-runner.ts +7 -0
  38. package/src/runtime/tools.ts +8 -2
  39. package/src/runtime/transcription.ts +483 -0
  40. package/src/templates/blank.ts +8 -3
  41. package/src/templates/dentista.ts +18 -10
  42. package/src/templates/skills/agentkit-build-agent/SKILL.md +6 -5
  43. package/src/templates/skills/agentkit-build-agent/templates/appointment-intake.instructions.md +2 -1
  44. package/src/templates/skills/agentkit-capsule/SKILL.md +1 -1
  45. package/src/templates/skills/agentkit-channels/SKILL.md +34 -1
  46. package/src/templates/skills/agentkit-channels/references/channel-debugging.md +13 -0
  47. package/src/templates/skills/agentkit-channels/references/telegram.md +32 -0
  48. package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +29 -0
  49. package/src/templates/skills/agentkit-evals/SKILL.md +53 -13
  50. package/src/templates/skills/agentkit-evals/templates/multi-turn.eval.md +13 -6
  51. package/src/templates/skills/agentkit-evals/templates/no-leak.eval.md +8 -4
  52. package/src/templates/skills/agentkit-evals/templates/smoke.eval.md +8 -4
  53. package/src/templates/skills/agentkit-evals/templates/tool-call.eval.md +16 -7
  54. package/src/templates/skills/agentkit-prompts/SKILL.md +3 -1
  55. package/src/templates/skills/agentkit-tools/SKILL.md +2 -1
  56. 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,10 +32,12 @@ 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"];
38
39
  channels: SharedAgentManifest["channels"];
40
+ transcription: SharedAgentManifest["transcription"];
39
41
  knowledge: SharedAgentManifest["knowledge"];
40
42
  access: SharedAgentManifest["access"];
41
43
  storage: {
@@ -192,10 +194,12 @@ function buildManifest(
192
194
  runtime: "edge",
193
195
  target: "cloudflare",
194
196
  provider: shared.provider,
197
+ timeZone: shared.timeZone,
195
198
  prompt: shared.prompt,
196
199
  secrets: shared.secrets,
197
200
  tools: shared.tools,
198
201
  channels: shared.channels,
202
+ transcription: shared.transcription,
199
203
  knowledge: shared.knowledge,
200
204
  access: shared.access,
201
205
  storage: {
@@ -538,10 +542,12 @@ function renderCloudflareWorker(input: { manifest: AgentManifest; instructions:
538
542
  const instructionsJson = JSON.stringify(input.instructions);
539
543
  const configImportPath = JSON.stringify(input.configPath);
540
544
  const providerImportPath = JSON.stringify(resolve(moduleDir, "../../../providers/index.ts"));
545
+ const promptContextImportPath = JSON.stringify(resolve(moduleDir, "../../prompt-context.ts"));
541
546
 
542
547
  return `import { DurableObject } from "cloudflare:workers";
543
548
  import agentConfig from ${configImportPath};
544
549
  import { getProviderAdapter } from ${providerImportPath};
550
+ import { appendRuntimePromptContext, buildRuntimeClockContext } from ${promptContextImportPath};
545
551
 
546
552
  const manifest = ${manifestJson};
547
553
  const instructions = ${instructionsJson};
@@ -1123,16 +1129,25 @@ function isRecord(value) {
1123
1129
 
1124
1130
  async function callHostedProvider(env, messages, signal) {
1125
1131
  const adapter = getProviderAdapter(manifest.provider);
1132
+ const clock = buildRuntimeClockContext({
1133
+ timeZone: manifest.timeZone,
1134
+ env,
1135
+ });
1126
1136
  const result = await adapter.run({
1127
1137
  agent: {
1128
1138
  name: manifest.name,
1129
1139
  runtime: manifest.runtime,
1130
1140
  provider: manifest.provider,
1131
1141
  },
1132
- instructions: withToolVisibilityInstructions(appendHostedKnowledgePromptPolicy(instructions), hostedTools),
1142
+ instructions: appendRuntimePromptContext(
1143
+ withToolVisibilityInstructions(appendHostedKnowledgePromptPolicy(instructions), hostedTools),
1144
+ {
1145
+ clock,
1146
+ },
1147
+ ),
1133
1148
  messages,
1134
1149
  tools: hostedTools,
1135
- toolRuntime: createHostedProviderToolRuntime(env, signal),
1150
+ toolRuntime: createHostedProviderToolRuntime(env, signal, clock),
1136
1151
  env,
1137
1152
  signal,
1138
1153
  });
@@ -1618,7 +1633,7 @@ function hostedManagedSecrets() {
1618
1633
  return ["TURSO_AUTH_TOKEN", "TURSO_DATABASE_URL"];
1619
1634
  }
1620
1635
 
1621
- function createHostedProviderToolRuntime(env, signal) {
1636
+ function createHostedProviderToolRuntime(env, signal, clock) {
1622
1637
  return {
1623
1638
  definitions: hostedTools,
1624
1639
  execute(request) {
@@ -1628,6 +1643,7 @@ function createHostedProviderToolRuntime(env, signal) {
1628
1643
  env,
1629
1644
  signal,
1630
1645
  invocation: "chat",
1646
+ clock,
1631
1647
  });
1632
1648
  },
1633
1649
  };
@@ -1646,6 +1662,10 @@ async function executeHostedTool(request) {
1646
1662
 
1647
1663
  const secrets = resolveToolSecrets(tool, request.env);
1648
1664
  const database = createHostedDatabaseRunner(request.env);
1665
+ const clock = request.clock ?? buildRuntimeClockContext({
1666
+ timeZone: manifest.timeZone,
1667
+ env: request.env,
1668
+ });
1649
1669
  const output = await runToolWithTimeout(
1650
1670
  tool,
1651
1671
  input,
@@ -1653,6 +1673,7 @@ async function executeHostedTool(request) {
1653
1673
  secrets,
1654
1674
  database,
1655
1675
  signal: request.signal,
1676
+ clock,
1656
1677
  runtime: {
1657
1678
  environment: "hosted",
1658
1679
  invocation: request.invocation ?? "tool",
@@ -1717,6 +1738,7 @@ async function runToolWithTimeout(tool, input, context, timeoutMs) {
1717
1738
  secrets: context.secrets,
1718
1739
  signal: controller.signal,
1719
1740
  runtime: context.runtime,
1741
+ clock: context.clock,
1720
1742
  database: context.database,
1721
1743
  db: context.database,
1722
1744
  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: {