@andreprado/agentkit 0.1.0-alpha.13 → 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.
- package/README.md +2 -0
- package/docs/guides/run-evals.md +73 -25
- package/docs/llms-full.txt +24 -9
- package/docs/llms.txt +2 -0
- package/package.json +1 -1
- package/src/cli/cloud-client.ts +34 -9
- package/src/cli/deploy-readiness.ts +32 -11
- package/src/cli/index.ts +20 -6
- package/src/cloud/client.ts +4 -3
- package/src/cloud/contracts.ts +1 -1
- package/src/create-project.ts +1 -1
- package/src/index.ts +24 -1
- package/src/providers/pi.ts +14 -1
- package/src/providers/test.ts +36 -0
- package/src/runtime/chat.ts +59 -42
- package/src/runtime/config.ts +33 -10
- package/src/runtime/core/manifest.ts +3 -3
- package/src/runtime/deploy-readiness.ts +3 -3
- package/src/runtime/dev-server.ts +171 -13
- package/src/runtime/env.ts +8 -3
- package/src/runtime/evals.ts +404 -69
- package/src/runtime/inspect.ts +12 -0
- package/src/runtime/prompt-context.ts +141 -0
- package/src/runtime/runtime-contract.ts +17 -7
- package/src/runtime/targets/cloudflare/build.ts +23 -3
- package/src/runtime/targets/container/server.ts +1 -1
- package/src/runtime/targets/vps/deploy.ts +25 -8
- package/src/runtime/tool-runner.ts +7 -0
- package/src/runtime/tools.ts +8 -2
- package/src/templates/blank.ts +8 -3
- package/src/templates/dentista.ts +18 -10
- package/src/templates/skills/agentkit-build-agent/SKILL.md +6 -5
- package/src/templates/skills/agentkit-build-agent/templates/appointment-intake.instructions.md +2 -1
- package/src/templates/skills/agentkit-capsule/SKILL.md +1 -1
- package/src/templates/skills/agentkit-evals/SKILL.md +53 -13
- package/src/templates/skills/agentkit-evals/templates/multi-turn.eval.md +13 -6
- package/src/templates/skills/agentkit-evals/templates/no-leak.eval.md +8 -4
- package/src/templates/skills/agentkit-evals/templates/smoke.eval.md +8 -4
- package/src/templates/skills/agentkit-evals/templates/tool-call.eval.md +16 -7
- package/src/templates/skills/agentkit-prompts/SKILL.md +3 -1
- package/src/templates/skills/agentkit-tools/SKILL.md +2 -1
- 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:
|
|
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",
|
package/src/runtime/tools.ts
CHANGED
|
@@ -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: {
|
package/src/templates/blank.ts
CHANGED
|
@@ -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: `
|
|
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
|
-
|
|
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(
|
|
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: `
|
|
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
|
-
|
|
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.
|
|
17
|
-
6. Add
|
|
18
|
-
7. Add
|
|
19
|
-
8. Add
|
|
20
|
-
9.
|
|
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
|
|
package/src/templates/skills/agentkit-build-agent/templates/appointment-intake.instructions.md
CHANGED
|
@@ -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
|
-
|