@andreprado/agentkit 0.1.0-alpha.2 → 0.1.0-alpha.20
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 +67 -6
- package/docs/guides/add-channel.md +118 -7
- package/docs/guides/add-knowledge.md +144 -0
- package/docs/guides/add-managed-composio.md +163 -0
- package/docs/guides/add-tool.md +1 -1
- package/docs/guides/channel-security.md +97 -32
- package/docs/guides/connect-discord.md +178 -0
- package/docs/guides/connect-slack.md +126 -0
- package/docs/guides/connect-telegram.md +78 -1
- package/docs/guides/connect-whatsapp-zapster.md +112 -8
- package/docs/guides/create-agent.md +45 -4
- package/docs/guides/debug-channel.md +147 -0
- package/docs/guides/improve-from-production.md +151 -0
- package/docs/guides/prepare-deploy.md +47 -17
- package/docs/guides/replay-production-traces.md +72 -0
- package/docs/guides/run-evals.md +147 -20
- package/docs/guides/security-rules.md +7 -6
- package/docs/guides/send-feedback.md +135 -0
- package/docs/guides/use-provider.md +27 -3
- package/docs/llms-full.txt +303 -55
- package/docs/llms.txt +57 -7
- package/package.json +2 -5
- package/src/cli/args.ts +57 -0
- package/src/cli/cloud-client.ts +377 -0
- package/src/cli/commands/channels.ts +1315 -0
- package/src/cli/commands/feedback.ts +438 -0
- package/src/cli/commands/knowledge.ts +136 -0
- package/src/cli/commands/transcribe.ts +171 -0
- package/src/cli/constants.ts +4 -0
- package/src/cli/deploy-chat-ui.ts +535 -0
- package/src/cli/deploy-readiness.ts +481 -0
- package/src/cli/flags.ts +162 -0
- package/src/cli/help.ts +236 -0
- package/src/cli/index.ts +1167 -1005
- package/src/cli/process.ts +31 -0
- package/src/cloud/artifact.ts +139 -0
- package/src/cloud/client.ts +80 -0
- package/src/cloud/contracts.ts +63 -0
- package/src/cloud/index.ts +3 -0
- package/src/create-project.ts +21 -6
- package/src/index.ts +479 -7
- package/src/providers/pi.ts +70 -16
- package/src/providers/test.ts +88 -1
- package/src/providers/types.ts +7 -0
- package/src/runtime/channel-buffer.ts +30 -0
- package/src/runtime/channel-test-harness.ts +8 -1
- package/src/runtime/channels/discord.ts +896 -0
- package/src/runtime/channels/slack.ts +646 -0
- package/src/runtime/channels/telegram.ts +466 -23
- package/src/runtime/channels/whatsapp-meta.ts +9 -0
- package/src/runtime/channels/whatsapp-zapster.ts +677 -40
- package/src/runtime/channels.ts +86 -3
- package/src/runtime/chat.ts +130 -38
- package/src/runtime/config.ts +483 -18
- package/src/runtime/core/manifest.ts +103 -5
- package/src/runtime/core/targets.ts +5 -5
- package/src/runtime/database.ts +93 -2
- package/src/runtime/db-commands.ts +9 -0
- package/src/runtime/deploy-readiness.ts +46 -4
- package/src/runtime/deploy.ts +1 -1
- package/src/runtime/dev-server.ts +759 -41
- package/src/runtime/env.ts +8 -3
- package/src/runtime/evals.ts +589 -43
- package/src/runtime/improve.ts +868 -0
- package/src/runtime/inspect.ts +194 -4
- package/src/runtime/integrations/composio.ts +423 -0
- package/src/runtime/knowledge/chunk.ts +333 -0
- package/src/runtime/knowledge/config.ts +135 -0
- package/src/runtime/knowledge/embeddings.ts +133 -0
- package/src/runtime/knowledge/ingest.ts +521 -0
- package/src/runtime/knowledge/prompt-policy.ts +30 -0
- package/src/runtime/knowledge/retrieve.ts +303 -0
- package/src/runtime/knowledge/schema.ts +100 -0
- package/src/runtime/knowledge/tool.ts +64 -0
- package/src/runtime/knowledge/vector.ts +258 -0
- package/src/runtime/prompt-context.ts +141 -0
- package/src/runtime/runtime-contract.ts +86 -8
- package/src/runtime/skills.ts +95 -0
- package/src/runtime/spec.ts +152 -0
- package/src/runtime/sync.ts +144 -0
- package/src/runtime/targets/cloudflare/build.ts +1430 -185
- package/src/runtime/targets/container/server.ts +1 -1
- package/src/runtime/targets/vps/deploy.ts +26 -9
- package/src/runtime/tool-runner.ts +9 -1
- package/src/runtime/tools.ts +128 -2
- package/src/runtime/traces.ts +41 -0
- package/src/runtime/transcription.ts +483 -0
- package/src/storage/sqlite.ts +149 -3
- package/src/templates/blank.ts +76 -17
- package/src/templates/dentista.ts +1011 -0
- package/src/templates/index.ts +2 -0
- package/src/templates/skills/agentkit-build-agent/SKILL.md +52 -0
- package/src/templates/skills/agentkit-build-agent/templates/appointment-intake.instructions.md +21 -0
- package/src/templates/skills/agentkit-build-agent/templates/sales-qualifier.instructions.md +17 -0
- package/src/templates/skills/agentkit-build-agent/templates/support-agent.instructions.md +16 -0
- package/src/templates/skills/agentkit-capsule/SKILL.md +70 -0
- package/src/templates/skills/agentkit-capsule/references/docs-router.md +15 -0
- package/src/templates/skills/agentkit-channels/SKILL.md +104 -0
- package/src/templates/skills/agentkit-channels/references/channel-buffering.md +65 -0
- package/src/templates/skills/agentkit-channels/references/channel-debugging.md +66 -0
- package/src/templates/skills/agentkit-channels/references/discord.md +93 -0
- package/src/templates/skills/agentkit-channels/references/slack.md +56 -0
- package/src/templates/skills/agentkit-channels/references/telegram.md +72 -0
- package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +77 -0
- package/src/templates/skills/agentkit-database/SKILL.md +45 -0
- package/src/templates/skills/agentkit-database/templates/appointments.schema.sql +15 -0
- package/src/templates/skills/agentkit-database/templates/leads.schema.sql +17 -0
- package/src/templates/skills/agentkit-deploy/SKILL.md +50 -0
- package/src/templates/skills/agentkit-evals/SKILL.md +109 -0
- package/src/templates/skills/agentkit-evals/templates/multi-turn.eval.md +29 -0
- package/src/templates/skills/agentkit-evals/templates/no-leak.eval.md +18 -0
- package/src/templates/skills/agentkit-evals/templates/smoke.eval.md +18 -0
- package/src/templates/skills/agentkit-evals/templates/tool-call.eval.md +27 -0
- package/src/templates/skills/agentkit-improve/SKILL.md +86 -0
- package/src/templates/skills/agentkit-improve/references/replay-side-effects.md +18 -0
- package/src/templates/skills/agentkit-improve/references/trace-packets.md +22 -0
- package/src/templates/skills/agentkit-improve/templates/regression.eval.md +18 -0
- package/src/templates/skills/agentkit-integrations/SKILL.md +76 -0
- package/src/templates/skills/agentkit-knowledge/SKILL.md +43 -0
- package/src/templates/skills/agentkit-knowledge/templates/faq.md +14 -0
- package/src/templates/skills/agentkit-knowledge/templates/policies.md +14 -0
- package/src/templates/skills/agentkit-knowledge/templates/prices.csv +3 -0
- package/src/templates/skills/agentkit-prompts/SKILL.md +47 -0
- package/src/templates/skills/agentkit-prompts/templates/knowledge-grounded-faq.instructions.md +11 -0
- package/src/templates/skills/agentkit-provider/SKILL.md +60 -0
- package/src/templates/skills/agentkit-security/SKILL.md +56 -0
- package/src/templates/skills/agentkit-tools/SKILL.md +37 -0
- package/src/templates/skills/agentkit-tools/examples/database-write.tool.md +35 -0
- package/src/templates/skills/agentkit-tools/examples/eval-safe-external-action.tool.md +37 -0
- package/src/templates/skills/agentkit-tools/examples/lookup-order.tool.md +46 -0
- package/src/templates/skills/agentkit-troubleshooting/SKILL.md +76 -0
- package/src/templates/support.ts +77 -18
- package/docs/guides/channels-production-handoff.md +0 -99
- package/docs/portable-deploy-release-checklist.md +0 -41
- package/src/runtime/targets/cloudflare/deploy.ts +0 -5475
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, extname, join } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
|
|
5
|
+
import { createClient } from "@libsql/client";
|
|
6
|
+
|
|
7
|
+
import type { DatabaseRow } from "../../index";
|
|
8
|
+
|
|
9
|
+
export const KNOWLEDGE_VECTOR_TABLE = "agentkit_knowledge_vectors";
|
|
10
|
+
export const KNOWLEDGE_VECTOR_INDEX = "agentkit_knowledge_vectors_embedding_idx";
|
|
11
|
+
const KNOWLEDGE_VECTOR_SOURCE_INDEX = "agentkit_knowledge_vectors_source_idx";
|
|
12
|
+
|
|
13
|
+
export type LocalKnowledgeVectorStatus = {
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
provider: "libsql" | "none" | "json_fallback";
|
|
16
|
+
chunksIndexed: number;
|
|
17
|
+
message: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function localKnowledgeVectorDatabasePath(databasePath: string): string {
|
|
21
|
+
const extension = extname(databasePath);
|
|
22
|
+
const stem = extension ? basename(databasePath, extension) : basename(databasePath);
|
|
23
|
+
return join(dirname(databasePath), `${stem}.vectors.db`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function localKnowledgeVectorDatabaseExists(databasePath: string): Promise<boolean> {
|
|
27
|
+
return access(localKnowledgeVectorDatabasePath(databasePath)).then(
|
|
28
|
+
() => true,
|
|
29
|
+
() => false,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function rebuildLocalKnowledgeVectorIndex(options: {
|
|
34
|
+
databasePath: string;
|
|
35
|
+
dimensions: number | null;
|
|
36
|
+
}): Promise<LocalKnowledgeVectorStatus> {
|
|
37
|
+
if (!isPositiveInteger(options.dimensions)) {
|
|
38
|
+
return {
|
|
39
|
+
enabled: false,
|
|
40
|
+
provider: "none",
|
|
41
|
+
chunksIndexed: 0,
|
|
42
|
+
message: "Knowledge embeddings are disabled, so no local vector index was created.",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const mainClient = createClient({ url: pathToFileURL(options.databasePath).href });
|
|
47
|
+
const vectorClient = createClient({ url: pathToFileURL(localKnowledgeVectorDatabasePath(options.databasePath)).href });
|
|
48
|
+
const dimensions = options.dimensions;
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
await vectorClient.batch(
|
|
52
|
+
[
|
|
53
|
+
`DROP INDEX IF EXISTS ${KNOWLEDGE_VECTOR_INDEX}`,
|
|
54
|
+
`DROP INDEX IF EXISTS ${KNOWLEDGE_VECTOR_SOURCE_INDEX}`,
|
|
55
|
+
`DROP TABLE IF EXISTS ${KNOWLEDGE_VECTOR_TABLE}`,
|
|
56
|
+
`
|
|
57
|
+
CREATE TABLE ${KNOWLEDGE_VECTOR_TABLE} (
|
|
58
|
+
id INTEGER PRIMARY KEY,
|
|
59
|
+
chunk_id TEXT NOT NULL UNIQUE,
|
|
60
|
+
source_id TEXT NOT NULL,
|
|
61
|
+
source_key TEXT NOT NULL,
|
|
62
|
+
content TEXT NOT NULL,
|
|
63
|
+
title TEXT,
|
|
64
|
+
section TEXT,
|
|
65
|
+
locator TEXT,
|
|
66
|
+
embedding F32_BLOB(${dimensions}) NOT NULL,
|
|
67
|
+
embedding_hash TEXT NOT NULL,
|
|
68
|
+
created_at TEXT NOT NULL
|
|
69
|
+
)
|
|
70
|
+
`,
|
|
71
|
+
`CREATE INDEX ${KNOWLEDGE_VECTOR_SOURCE_INDEX} ON ${KNOWLEDGE_VECTOR_TABLE}(source_id)`,
|
|
72
|
+
`CREATE INDEX ${KNOWLEDGE_VECTOR_INDEX} ON ${KNOWLEDGE_VECTOR_TABLE}(libsql_vector_idx(embedding, 'metric=cosine'))`,
|
|
73
|
+
],
|
|
74
|
+
"write",
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const rows = await mainClient.execute({
|
|
78
|
+
sql: `
|
|
79
|
+
SELECT
|
|
80
|
+
c.id AS chunk_id,
|
|
81
|
+
c.source_id,
|
|
82
|
+
c.content,
|
|
83
|
+
c.title,
|
|
84
|
+
c.section,
|
|
85
|
+
c.locator,
|
|
86
|
+
c.embedding_json,
|
|
87
|
+
c.embedding_hash,
|
|
88
|
+
c.created_at,
|
|
89
|
+
s.source_key
|
|
90
|
+
FROM agentkit_knowledge_chunks c
|
|
91
|
+
JOIN agentkit_knowledge_sources s ON s.id = c.source_id
|
|
92
|
+
WHERE c.embedding_json IS NOT NULL
|
|
93
|
+
AND c.embedding_hash IS NOT NULL
|
|
94
|
+
AND c.embedding_dimensions = ?
|
|
95
|
+
ORDER BY c.created_at DESC, c.ordinal ASC
|
|
96
|
+
`,
|
|
97
|
+
args: [dimensions],
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
let chunksIndexed = 0;
|
|
101
|
+
|
|
102
|
+
for (const row of rows.rows) {
|
|
103
|
+
const chunkId = readString(row, "chunk_id");
|
|
104
|
+
const sourceId = readString(row, "source_id");
|
|
105
|
+
const sourceKey = readString(row, "source_key");
|
|
106
|
+
const content = readString(row, "content");
|
|
107
|
+
const embeddingJson = readString(row, "embedding_json");
|
|
108
|
+
const embeddingHash = readString(row, "embedding_hash");
|
|
109
|
+
const createdAt = readString(row, "created_at");
|
|
110
|
+
|
|
111
|
+
if (!chunkId || !sourceId || !sourceKey || !content || !embeddingJson || !embeddingHash || !createdAt) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
await vectorClient.execute({
|
|
116
|
+
sql: `
|
|
117
|
+
INSERT INTO ${KNOWLEDGE_VECTOR_TABLE} (
|
|
118
|
+
chunk_id,
|
|
119
|
+
source_id,
|
|
120
|
+
source_key,
|
|
121
|
+
content,
|
|
122
|
+
title,
|
|
123
|
+
section,
|
|
124
|
+
locator,
|
|
125
|
+
embedding,
|
|
126
|
+
embedding_hash,
|
|
127
|
+
created_at
|
|
128
|
+
)
|
|
129
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, vector32(?), ?, ?)
|
|
130
|
+
`,
|
|
131
|
+
args: [
|
|
132
|
+
chunkId,
|
|
133
|
+
sourceId,
|
|
134
|
+
sourceKey,
|
|
135
|
+
content,
|
|
136
|
+
readNullableString(row, "title"),
|
|
137
|
+
readNullableString(row, "section"),
|
|
138
|
+
readNullableString(row, "locator"),
|
|
139
|
+
embeddingJson,
|
|
140
|
+
embeddingHash,
|
|
141
|
+
createdAt,
|
|
142
|
+
],
|
|
143
|
+
});
|
|
144
|
+
chunksIndexed += 1;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
await vectorClient.execute("PRAGMA wal_checkpoint(TRUNCATE)").catch(() => undefined);
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
enabled: true,
|
|
151
|
+
provider: "libsql",
|
|
152
|
+
chunksIndexed,
|
|
153
|
+
message: `Local libSQL vector database rebuilt with ${chunksIndexed} chunk${chunksIndexed === 1 ? "" : "s"}.`,
|
|
154
|
+
};
|
|
155
|
+
} catch (error) {
|
|
156
|
+
return {
|
|
157
|
+
enabled: false,
|
|
158
|
+
provider: "json_fallback",
|
|
159
|
+
chunksIndexed: 0,
|
|
160
|
+
message: `Local libSQL vector index unavailable; using JSON embedding fallback. ${formatVectorError(error)}`,
|
|
161
|
+
};
|
|
162
|
+
} finally {
|
|
163
|
+
mainClient.close();
|
|
164
|
+
vectorClient.close();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function searchLocalKnowledgeVectorRows(options: {
|
|
169
|
+
databasePath: string;
|
|
170
|
+
queryEmbedding: number[];
|
|
171
|
+
limit: number;
|
|
172
|
+
}): Promise<DatabaseRow[]> {
|
|
173
|
+
const vectorDatabasePath = localKnowledgeVectorDatabasePath(options.databasePath);
|
|
174
|
+
|
|
175
|
+
await access(vectorDatabasePath);
|
|
176
|
+
|
|
177
|
+
const client = createClient({ url: pathToFileURL(vectorDatabasePath).href });
|
|
178
|
+
const vector = JSON.stringify(options.queryEmbedding);
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const result = await client.execute({
|
|
182
|
+
sql: `
|
|
183
|
+
SELECT
|
|
184
|
+
c.id AS chunk_id,
|
|
185
|
+
c.source_id AS source_id,
|
|
186
|
+
c.content AS content,
|
|
187
|
+
c.title AS title,
|
|
188
|
+
c.section AS section,
|
|
189
|
+
c.locator AS locator,
|
|
190
|
+
c.source_key AS source_key,
|
|
191
|
+
vector_distance_cos(c.embedding, vector32(?)) AS vector_distance
|
|
192
|
+
FROM vector_top_k('${KNOWLEDGE_VECTOR_INDEX}', vector32(?), ?) nearest
|
|
193
|
+
JOIN ${KNOWLEDGE_VECTOR_TABLE} c ON c.id = nearest.id
|
|
194
|
+
ORDER BY vector_distance ASC
|
|
195
|
+
`,
|
|
196
|
+
args: [vector, vector, options.limit],
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return result.rows.map(normalizeLibsqlRow);
|
|
200
|
+
} finally {
|
|
201
|
+
client.close();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function normalizeLibsqlRow(row: unknown): DatabaseRow {
|
|
206
|
+
const result: DatabaseRow = {};
|
|
207
|
+
|
|
208
|
+
if (!row || typeof row !== "object") {
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
for (const [key, value] of Object.entries(row)) {
|
|
213
|
+
if (key === "length" || /^\d+$/.test(key)) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (typeof value === "bigint") {
|
|
218
|
+
result[key] = Number.isSafeInteger(Number(value)) ? Number(value) : String(value);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
223
|
+
result[key] = value;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
result[key] = String(value);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function isPositiveInteger(value: number | null): value is number {
|
|
234
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function readString(row: unknown, key: string): string | null {
|
|
238
|
+
if (!row || typeof row !== "object" || !(key in row)) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const value = (row as Record<string, unknown>)[key];
|
|
243
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function readNullableString(row: unknown, key: string): string | null {
|
|
247
|
+
if (!row || typeof row !== "object" || !(key in row)) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const value = (row as Record<string, unknown>)[key];
|
|
252
|
+
return typeof value === "string" ? value : null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function formatVectorError(error: unknown): string {
|
|
256
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
257
|
+
return message.length <= 240 ? message : `${message.slice(0, 237)}...`;
|
|
258
|
+
}
|
|
@@ -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,74 @@ 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
|
|
|
38
|
+
if (options.auth) {
|
|
39
|
+
const missingAdmin = await fetchJson(`${baseUrl}/_agentkit`);
|
|
40
|
+
expect(missingAdmin.status).toBe(401);
|
|
41
|
+
expect(missingAdmin.body.error.code).toBe("unauthorized");
|
|
42
|
+
|
|
43
|
+
const invalidAdmin = await fetchJson(`${baseUrl}/_agentkit`, {
|
|
44
|
+
headers: { Authorization: "Bearer invalid-admin-token" },
|
|
45
|
+
});
|
|
46
|
+
expect(invalidAdmin.status).toBe(401);
|
|
47
|
+
expect(invalidAdmin.body.error.code).toBe("unauthorized");
|
|
48
|
+
|
|
49
|
+
const missingChat = await fetchJson(`${baseUrl}/chat`, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: { "content-type": "application/json" },
|
|
52
|
+
body: JSON.stringify({ message: "hello" }),
|
|
53
|
+
});
|
|
54
|
+
expect(missingChat.status).toBe(401);
|
|
55
|
+
expect(missingChat.body.error.code).toBe("unauthorized");
|
|
56
|
+
|
|
57
|
+
const invalidChat = await fetchJson(`${baseUrl}/chat`, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: { "content-type": "application/json", Authorization: "Bearer invalid-chat-token" },
|
|
60
|
+
body: JSON.stringify({ message: "hello" }),
|
|
61
|
+
});
|
|
62
|
+
expect(invalidChat.status).toBe(401);
|
|
63
|
+
expect(invalidChat.body.error.code).toBe("unauthorized");
|
|
64
|
+
|
|
65
|
+
const missingTool = await fetchJson(`${baseUrl}/tools/${encodeURIComponent(options.tool.name)}`, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: { "content-type": "application/json" },
|
|
68
|
+
body: JSON.stringify({ input: options.tool.input }),
|
|
69
|
+
});
|
|
70
|
+
expect(missingTool.status).toBe(401);
|
|
71
|
+
expect(missingTool.body.error.code).toBe("unauthorized");
|
|
72
|
+
|
|
73
|
+
const invalidTool = await fetchJson(`${baseUrl}/tools/${encodeURIComponent(options.tool.name)}`, {
|
|
74
|
+
method: "POST",
|
|
75
|
+
headers: { "content-type": "application/json", Authorization: "Bearer invalid-admin-token" },
|
|
76
|
+
body: JSON.stringify({ input: options.tool.input }),
|
|
77
|
+
});
|
|
78
|
+
expect(invalidTool.status).toBe(401);
|
|
79
|
+
expect(invalidTool.body.error.code).toBe("unauthorized");
|
|
80
|
+
|
|
81
|
+
const missingConversations = await fetchJson(`${baseUrl}/v1/conversations`);
|
|
82
|
+
expect(missingConversations.status).toBe(401);
|
|
83
|
+
expect(missingConversations.body.error.code).toBe("unauthorized");
|
|
84
|
+
|
|
85
|
+
const invalidConversations = await fetchJson(`${baseUrl}/v1/conversations`, {
|
|
86
|
+
headers: { Authorization: "Bearer invalid-admin-token" },
|
|
87
|
+
});
|
|
88
|
+
expect(invalidConversations.status).toBe(401);
|
|
89
|
+
expect(invalidConversations.body.error.code).toBe("unauthorized");
|
|
90
|
+
}
|
|
91
|
+
|
|
34
92
|
const unauthorized = await fetchJson(`${baseUrl}/chat`, {
|
|
35
93
|
method: "POST",
|
|
36
|
-
headers: { "content-type": "application/json" },
|
|
94
|
+
headers: withHeaders({ "content-type": "application/json" }, options.auth?.chat),
|
|
37
95
|
body: JSON.stringify({ message: { role: "assistant", content: "not allowed" } }),
|
|
38
96
|
});
|
|
39
97
|
expect(unauthorized.status).toBe(400);
|
|
@@ -41,21 +99,35 @@ export async function assertRuntimeContract(
|
|
|
41
99
|
|
|
42
100
|
const chat = await fetchJson(`${baseUrl}/chat`, {
|
|
43
101
|
method: "POST",
|
|
44
|
-
headers: { "content-type": "application/json" },
|
|
45
|
-
body: JSON.stringify({ message: { role: "user", content: "
|
|
102
|
+
headers: withHeaders({ "content-type": "application/json" }, options.auth?.chat),
|
|
103
|
+
body: JSON.stringify({ message: { role: "user", content: "my name is Ada" } }),
|
|
46
104
|
});
|
|
47
105
|
expect(chat.status).toBe(200);
|
|
48
106
|
expect(chat.body).toMatchObject({
|
|
49
107
|
message: {
|
|
50
108
|
role: "assistant",
|
|
51
|
-
content: "Echo:
|
|
109
|
+
content: "Echo: my name is Ada",
|
|
52
110
|
},
|
|
53
111
|
});
|
|
54
112
|
expect(typeof chat.body.conversationId).toBe("string");
|
|
55
113
|
|
|
114
|
+
const continuedChat = await fetchJson(`${baseUrl}/chat`, {
|
|
115
|
+
method: "POST",
|
|
116
|
+
headers: withHeaders({ "content-type": "application/json" }, options.auth?.chat),
|
|
117
|
+
body: JSON.stringify({ message: "what is my name?", conversationId: chat.body.conversationId }),
|
|
118
|
+
});
|
|
119
|
+
expect(continuedChat.status).toBe(200);
|
|
120
|
+
expect(continuedChat.body).toMatchObject({
|
|
121
|
+
conversationId: chat.body.conversationId,
|
|
122
|
+
message: {
|
|
123
|
+
role: "assistant",
|
|
124
|
+
content: "Your name is Ada.",
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
56
128
|
const tool = await fetchJson(`${baseUrl}/tools/${encodeURIComponent(options.tool.name)}`, {
|
|
57
129
|
method: "POST",
|
|
58
|
-
headers: { "content-type": "application/json" },
|
|
130
|
+
headers: withHeaders({ "content-type": "application/json" }, options.auth?.admin),
|
|
59
131
|
body: JSON.stringify({ input: options.tool.input }),
|
|
60
132
|
});
|
|
61
133
|
expect(tool.status).toBe(200);
|
|
@@ -64,7 +136,7 @@ export async function assertRuntimeContract(
|
|
|
64
136
|
output: options.tool.expectedOutput,
|
|
65
137
|
});
|
|
66
138
|
|
|
67
|
-
const conversations = await fetchJson(`${baseUrl}/v1/conversations
|
|
139
|
+
const conversations = await fetchJson(`${baseUrl}/v1/conversations`, { headers: options.auth?.admin });
|
|
68
140
|
expect(conversations.status).toBe(200);
|
|
69
141
|
expect(conversations.body.conversations.length).toBeGreaterThanOrEqual(1);
|
|
70
142
|
expect(conversations.body.conversations.some((conversation: { id: string }) => conversation.id === chat.body.conversationId)).toBe(true);
|
|
@@ -77,3 +149,9 @@ async function fetchJson(url: string, init?: RequestInit): Promise<{ status: num
|
|
|
77
149
|
body: await response.json(),
|
|
78
150
|
};
|
|
79
151
|
}
|
|
152
|
+
|
|
153
|
+
function withHeaders(required: HeadersInit, extra: HeadersInit | undefined): Headers {
|
|
154
|
+
const headers = new Headers(extra);
|
|
155
|
+
new Headers(required).forEach((value, key) => headers.set(key, value));
|
|
156
|
+
return headers;
|
|
157
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join, relative } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
export type DefaultSkillFile = {
|
|
6
|
+
path: string;
|
|
7
|
+
contents: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type DefaultSkillStatus = {
|
|
11
|
+
status: "current" | "missing" | "outdated";
|
|
12
|
+
checked: number;
|
|
13
|
+
missing: string[];
|
|
14
|
+
outdated: string[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
|
|
19
|
+
export async function syncDefaultSkillFiles(projectPath: string): Promise<{ files: string[] }> {
|
|
20
|
+
const files = await loadDefaultSkillFiles();
|
|
21
|
+
|
|
22
|
+
for (const file of files) {
|
|
23
|
+
const absolutePath = join(projectPath, file.path);
|
|
24
|
+
|
|
25
|
+
await mkdir(dirname(absolutePath), { recursive: true });
|
|
26
|
+
await writeFile(absolutePath, file.contents);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return { files: files.map((file) => file.path) };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function checkDefaultSkillStatus(projectPath: string): Promise<DefaultSkillStatus> {
|
|
33
|
+
const files = await loadDefaultSkillFiles();
|
|
34
|
+
const missing: string[] = [];
|
|
35
|
+
const outdated: string[] = [];
|
|
36
|
+
|
|
37
|
+
for (const file of files) {
|
|
38
|
+
const absolutePath = join(projectPath, file.path);
|
|
39
|
+
|
|
40
|
+
if (!(await pathExists(absolutePath))) {
|
|
41
|
+
missing.push(file.path);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const current = await readFile(absolutePath, "utf8");
|
|
46
|
+
if (current !== file.contents) {
|
|
47
|
+
outdated.push(file.path);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
status: missing.length > 0 ? "missing" : outdated.length > 0 ? "outdated" : "current",
|
|
53
|
+
checked: files.length,
|
|
54
|
+
missing,
|
|
55
|
+
outdated,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function loadDefaultSkillFiles(): Promise<DefaultSkillFile[]> {
|
|
60
|
+
const sourceRoot = join(moduleDir, "..", "templates", "skills");
|
|
61
|
+
const sourceFiles = await listFiles(sourceRoot);
|
|
62
|
+
|
|
63
|
+
return Promise.all(
|
|
64
|
+
sourceFiles.map(async (sourcePath) => ({
|
|
65
|
+
path: join("skills", relative(sourceRoot, sourcePath)).replaceAll("\\", "/"),
|
|
66
|
+
contents: await readFile(sourcePath, "utf8"),
|
|
67
|
+
})),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function listFiles(directory: string): Promise<string[]> {
|
|
72
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
73
|
+
const files = await Promise.all(
|
|
74
|
+
entries.map(async (entry) => {
|
|
75
|
+
const path = join(directory, entry.name);
|
|
76
|
+
|
|
77
|
+
if (entry.isDirectory()) {
|
|
78
|
+
return listFiles(path);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return entry.isFile() ? [path] : [];
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
return files.flat().sort();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function pathExists(path: string): Promise<boolean> {
|
|
89
|
+
try {
|
|
90
|
+
await stat(path);
|
|
91
|
+
return true;
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|