@standardagents/cli 0.8.1
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 +81 -0
- package/dist/index.js +1328 -0
- package/dist/index.js.map +1 -0
- package/package.json +40 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1328 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import path3 from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import { parse, modify, applyEdits } from 'jsonc-parser';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
|
|
8
|
+
function findWranglerConfig(cwd) {
|
|
9
|
+
const jsonc = path3.join(cwd, "wrangler.jsonc");
|
|
10
|
+
const json = path3.join(cwd, "wrangler.json");
|
|
11
|
+
if (fs.existsSync(jsonc)) {
|
|
12
|
+
return jsonc;
|
|
13
|
+
}
|
|
14
|
+
if (fs.existsSync(json)) {
|
|
15
|
+
return json;
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
function readWranglerConfig(filePath) {
|
|
20
|
+
const text = fs.readFileSync(filePath, "utf-8");
|
|
21
|
+
const config = parse(text);
|
|
22
|
+
return { config, text };
|
|
23
|
+
}
|
|
24
|
+
function updateWranglerConfig(filePath, text, updates) {
|
|
25
|
+
let result = text;
|
|
26
|
+
if (updates.durable_objects) {
|
|
27
|
+
const edits = modify(result, ["durable_objects"], updates.durable_objects, {});
|
|
28
|
+
result = applyEdits(result, edits);
|
|
29
|
+
}
|
|
30
|
+
if (updates.migrations) {
|
|
31
|
+
const edits = modify(result, ["migrations"], updates.migrations, {});
|
|
32
|
+
result = applyEdits(result, edits);
|
|
33
|
+
}
|
|
34
|
+
if (updates.env) {
|
|
35
|
+
for (const [envName, envConfig] of Object.entries(updates.env)) {
|
|
36
|
+
if (envConfig.durable_objects) {
|
|
37
|
+
const edits = modify(result, ["env", envName, "durable_objects"], envConfig.durable_objects, {});
|
|
38
|
+
result = applyEdits(result, edits);
|
|
39
|
+
}
|
|
40
|
+
if (envConfig.migrations) {
|
|
41
|
+
const edits = modify(result, ["env", envName, "migrations"], envConfig.migrations, {});
|
|
42
|
+
result = applyEdits(result, edits);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
function writeWranglerConfig(filePath, content) {
|
|
49
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
50
|
+
}
|
|
51
|
+
var logger = {
|
|
52
|
+
success: (message) => {
|
|
53
|
+
console.log(chalk.green("\u2713"), message);
|
|
54
|
+
},
|
|
55
|
+
error: (message) => {
|
|
56
|
+
console.log(chalk.red("\u2717"), message);
|
|
57
|
+
},
|
|
58
|
+
warning: (message) => {
|
|
59
|
+
console.log(chalk.yellow("\u26A0"), message);
|
|
60
|
+
},
|
|
61
|
+
info: (message) => {
|
|
62
|
+
console.log(chalk.blue("\u2139"), message);
|
|
63
|
+
},
|
|
64
|
+
log: (message) => {
|
|
65
|
+
console.log(message);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// src/commands/init.ts
|
|
70
|
+
async function init(options = {}) {
|
|
71
|
+
var _a, _b, _c, _d, _e;
|
|
72
|
+
const cwd = process.cwd();
|
|
73
|
+
logger.info("Initializing Standard Agents configuration...");
|
|
74
|
+
const configPath = findWranglerConfig(cwd);
|
|
75
|
+
if (!configPath) {
|
|
76
|
+
logger.error("No wrangler.jsonc or wrangler.json found in current directory");
|
|
77
|
+
logger.log("\nTo use Standard Agents, you need a Cloudflare Workers project with wrangler configuration.");
|
|
78
|
+
logger.log("\nExample wrangler.jsonc:");
|
|
79
|
+
logger.log(`
|
|
80
|
+
{
|
|
81
|
+
"name": "my-agent",
|
|
82
|
+
"main": "server/index.ts",
|
|
83
|
+
"compatibility_date": "2025-08-13",
|
|
84
|
+
"compatibility_flags": ["nodejs_compat"],
|
|
85
|
+
|
|
86
|
+
"durable_objects": {
|
|
87
|
+
"bindings": [
|
|
88
|
+
{
|
|
89
|
+
"name": "AGENT_BUILDER_THREAD",
|
|
90
|
+
"class_name": "DurableThread"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"name": "AGENT_BUILDER",
|
|
94
|
+
"class_name": "DurableAgentBuilder"
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
"migrations": [
|
|
100
|
+
{
|
|
101
|
+
"tag": "v1",
|
|
102
|
+
"new_sqlite_classes": ["DurableThread", "DurableAgentBuilder"]
|
|
103
|
+
}
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
`);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
logger.success(`Found configuration: ${path3.relative(cwd, configPath)}`);
|
|
110
|
+
const { config, text } = readWranglerConfig(configPath);
|
|
111
|
+
const hasAgentBuilderThread = (_b = (_a = config.durable_objects) == null ? void 0 : _a.bindings) == null ? void 0 : _b.some(
|
|
112
|
+
(binding) => binding.name === "AGENT_BUILDER_THREAD"
|
|
113
|
+
);
|
|
114
|
+
const hasAgentBuilder = (_d = (_c = config.durable_objects) == null ? void 0 : _c.bindings) == null ? void 0 : _d.some(
|
|
115
|
+
(binding) => binding.name === "AGENT_BUILDER"
|
|
116
|
+
);
|
|
117
|
+
if (hasAgentBuilderThread && hasAgentBuilder && !options.force) {
|
|
118
|
+
logger.success("Standard Agents is already configured!");
|
|
119
|
+
logger.info("Use --force to overwrite existing configuration");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const updates = {};
|
|
123
|
+
updates.durable_objects = config.durable_objects || { bindings: [] };
|
|
124
|
+
if (options.force) {
|
|
125
|
+
updates.durable_objects.bindings = updates.durable_objects.bindings.filter(
|
|
126
|
+
(binding) => binding.name !== "AGENT_BUILDER_THREAD" && binding.name !== "AGENT_BUILDER"
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
if (!hasAgentBuilderThread || options.force) {
|
|
130
|
+
updates.durable_objects.bindings.push({
|
|
131
|
+
name: "AGENT_BUILDER_THREAD",
|
|
132
|
+
class_name: "DurableThread"
|
|
133
|
+
});
|
|
134
|
+
logger.info("Added AGENT_BUILDER_THREAD binding");
|
|
135
|
+
}
|
|
136
|
+
if (!hasAgentBuilder || options.force) {
|
|
137
|
+
updates.durable_objects.bindings.push({
|
|
138
|
+
name: "AGENT_BUILDER",
|
|
139
|
+
class_name: "DurableAgentBuilder"
|
|
140
|
+
});
|
|
141
|
+
logger.info("Added AGENT_BUILDER binding");
|
|
142
|
+
}
|
|
143
|
+
if (!config.migrations || config.migrations.length === 0) {
|
|
144
|
+
updates.migrations = [
|
|
145
|
+
{
|
|
146
|
+
tag: "v1",
|
|
147
|
+
new_sqlite_classes: ["DurableThread", "DurableAgentBuilder"]
|
|
148
|
+
}
|
|
149
|
+
];
|
|
150
|
+
logger.info("Added Durable Object migrations");
|
|
151
|
+
}
|
|
152
|
+
if ((_e = config.env) == null ? void 0 : _e.test) {
|
|
153
|
+
updates.env = updates.env || {};
|
|
154
|
+
updates.env.test = config.env.test;
|
|
155
|
+
if (!updates.env.test.durable_objects) {
|
|
156
|
+
updates.env.test.durable_objects = { bindings: [] };
|
|
157
|
+
}
|
|
158
|
+
const hasTestThread = updates.env.test.durable_objects.bindings.some(
|
|
159
|
+
(binding) => binding.name === "AGENT_BUILDER_THREAD"
|
|
160
|
+
);
|
|
161
|
+
const hasTestBuilder = updates.env.test.durable_objects.bindings.some(
|
|
162
|
+
(binding) => binding.name === "AGENT_BUILDER"
|
|
163
|
+
);
|
|
164
|
+
if (!hasTestThread) {
|
|
165
|
+
updates.env.test.durable_objects.bindings.push({
|
|
166
|
+
name: "AGENT_BUILDER_THREAD",
|
|
167
|
+
class_name: "DurableThread"
|
|
168
|
+
});
|
|
169
|
+
logger.info("Added test environment AGENT_BUILDER_THREAD binding");
|
|
170
|
+
}
|
|
171
|
+
if (!hasTestBuilder) {
|
|
172
|
+
updates.env.test.durable_objects.bindings.push({
|
|
173
|
+
name: "AGENT_BUILDER",
|
|
174
|
+
class_name: "DurableAgentBuilder"
|
|
175
|
+
});
|
|
176
|
+
logger.info("Added test environment AGENT_BUILDER binding");
|
|
177
|
+
}
|
|
178
|
+
if (!updates.env.test.migrations) {
|
|
179
|
+
updates.env.test.migrations = [
|
|
180
|
+
{
|
|
181
|
+
tag: "v1",
|
|
182
|
+
new_sqlite_classes: ["DurableThread", "DurableAgentBuilder"]
|
|
183
|
+
}
|
|
184
|
+
];
|
|
185
|
+
logger.info("Added test environment Durable Object migrations");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const updatedText = updateWranglerConfig(configPath, text, updates);
|
|
189
|
+
writeWranglerConfig(configPath, updatedText);
|
|
190
|
+
logger.success("Configuration updated successfully!");
|
|
191
|
+
logger.log("\nNext steps:");
|
|
192
|
+
logger.log("1. Create your agent definitions in agents/agents/");
|
|
193
|
+
logger.log("2. Start your development server");
|
|
194
|
+
}
|
|
195
|
+
var TOOLS_CLAUDE_MD = `# Custom Tools
|
|
196
|
+
|
|
197
|
+
This directory contains custom tools that your AI agents can call during execution.
|
|
198
|
+
|
|
199
|
+
## What Are Tools?
|
|
200
|
+
|
|
201
|
+
Tools are functions that extend your agent's capabilities beyond text generation. They allow agents to:
|
|
202
|
+
- Fetch external data (APIs, databases)
|
|
203
|
+
- Perform calculations
|
|
204
|
+
- Execute side effects (send emails, create records)
|
|
205
|
+
- Chain to other prompts or agents
|
|
206
|
+
|
|
207
|
+
## Creating a Tool
|
|
208
|
+
|
|
209
|
+
Create a new file in this directory following the naming convention:
|
|
210
|
+
|
|
211
|
+
\`\`\`
|
|
212
|
+
agents/tools/
|
|
213
|
+
\u251C\u2500\u2500 my_tool.ts # \u2713 snake_case recommended
|
|
214
|
+
\u251C\u2500\u2500 another_tool.ts # \u2713 Valid
|
|
215
|
+
\u2514\u2500\u2500 CamelCaseTool.ts # \u26A0 Works but triggers warning
|
|
216
|
+
\`\`\`
|
|
217
|
+
|
|
218
|
+
### Tool File Structure
|
|
219
|
+
|
|
220
|
+
Each tool file should export a default function created with \`defineTool\`:
|
|
221
|
+
|
|
222
|
+
\`\`\`typescript
|
|
223
|
+
import { defineTool } from '@standardagents/builder';
|
|
224
|
+
import { z } from 'zod';
|
|
225
|
+
|
|
226
|
+
// With arguments
|
|
227
|
+
export default defineTool(
|
|
228
|
+
'Description of what this tool does',
|
|
229
|
+
z.object({
|
|
230
|
+
param1: z.string().describe('Description of param1'),
|
|
231
|
+
param2: z.number().optional().describe('Optional parameter'),
|
|
232
|
+
}),
|
|
233
|
+
async (flow, args) => {
|
|
234
|
+
// Tool implementation
|
|
235
|
+
const result = await doSomething(args.param1, args.param2);
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
status: 'success',
|
|
239
|
+
result: JSON.stringify(result)
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
// Without arguments
|
|
245
|
+
export default defineTool(
|
|
246
|
+
'Simple tool with no parameters',
|
|
247
|
+
async (flow) => {
|
|
248
|
+
return {
|
|
249
|
+
status: 'success',
|
|
250
|
+
result: 'Done!'
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
);
|
|
254
|
+
\`\`\`
|
|
255
|
+
|
|
256
|
+
### FlowState Object
|
|
257
|
+
|
|
258
|
+
The \`flow\` parameter provides execution context:
|
|
259
|
+
|
|
260
|
+
\`\`\`typescript
|
|
261
|
+
interface FlowState {
|
|
262
|
+
env: Env; // Cloudflare bindings (KV, R2, etc.)
|
|
263
|
+
storage: DurableObjectStorage; // Thread's SQLite storage
|
|
264
|
+
threadId: string; // Current thread ID
|
|
265
|
+
agentId: string; // Current agent ID
|
|
266
|
+
currentSide: 'a' | 'b'; // Which side is executing
|
|
267
|
+
turnCount: number; // Current turn number
|
|
268
|
+
context: Record<string, any>; // Arbitrary state data
|
|
269
|
+
// ... and more
|
|
270
|
+
}
|
|
271
|
+
\`\`\`
|
|
272
|
+
|
|
273
|
+
### Return Value
|
|
274
|
+
|
|
275
|
+
Tools must return a ToolResult object:
|
|
276
|
+
|
|
277
|
+
\`\`\`typescript
|
|
278
|
+
interface ToolResult {
|
|
279
|
+
status: 'success' | 'error';
|
|
280
|
+
result?: string; // Success data
|
|
281
|
+
error?: string; // Error message
|
|
282
|
+
}
|
|
283
|
+
\`\`\`
|
|
284
|
+
|
|
285
|
+
## Tool Discovery
|
|
286
|
+
|
|
287
|
+
Tools are **auto-discovered** at runtime. No manual registration needed!
|
|
288
|
+
|
|
289
|
+
1. Vite plugin scans this directory on startup
|
|
290
|
+
2. Generates virtual module with dynamic imports
|
|
291
|
+
3. Tools become available to all agents
|
|
292
|
+
4. HMR (Hot Module Replacement) works in development
|
|
293
|
+
|
|
294
|
+
## Examples
|
|
295
|
+
|
|
296
|
+
### API Fetch Tool
|
|
297
|
+
|
|
298
|
+
\`\`\`typescript
|
|
299
|
+
import { defineTool } from '@standardagents/builder';
|
|
300
|
+
import { z } from 'zod';
|
|
301
|
+
|
|
302
|
+
export default defineTool(
|
|
303
|
+
'Fetch weather data for a city',
|
|
304
|
+
z.object({
|
|
305
|
+
city: z.string().describe('City name'),
|
|
306
|
+
units: z.enum(['metric', 'imperial']).optional(),
|
|
307
|
+
}),
|
|
308
|
+
async (flow, args) => {
|
|
309
|
+
try {
|
|
310
|
+
const response = await fetch(
|
|
311
|
+
\`https://api.weather.com/\${args.city}\`
|
|
312
|
+
);
|
|
313
|
+
const data = await response.json();
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
status: 'success',
|
|
317
|
+
result: JSON.stringify(data),
|
|
318
|
+
};
|
|
319
|
+
} catch (error) {
|
|
320
|
+
return {
|
|
321
|
+
status: 'error',
|
|
322
|
+
error: error.message,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
);
|
|
327
|
+
\`\`\`
|
|
328
|
+
|
|
329
|
+
### Thread Storage Tool
|
|
330
|
+
|
|
331
|
+
\`\`\`typescript
|
|
332
|
+
import { defineTool } from '@standardagents/builder';
|
|
333
|
+
import { z } from 'zod';
|
|
334
|
+
|
|
335
|
+
export default defineTool(
|
|
336
|
+
'Get custom data from thread storage',
|
|
337
|
+
z.object({
|
|
338
|
+
key: z.string().describe('Storage key to look up'),
|
|
339
|
+
}),
|
|
340
|
+
async (flow, args) => {
|
|
341
|
+
try {
|
|
342
|
+
// Use thread's SQLite storage
|
|
343
|
+
const result = await flow.storage.sql.exec(
|
|
344
|
+
\`SELECT value FROM custom_data WHERE key = ?\`,
|
|
345
|
+
args.key
|
|
346
|
+
).toArray();
|
|
347
|
+
|
|
348
|
+
if (result.length === 0) {
|
|
349
|
+
return {
|
|
350
|
+
status: 'error',
|
|
351
|
+
error: 'Data not found',
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
status: 'success',
|
|
357
|
+
result: JSON.stringify(result[0]),
|
|
358
|
+
};
|
|
359
|
+
} catch (error) {
|
|
360
|
+
return {
|
|
361
|
+
status: 'error',
|
|
362
|
+
error: error.message,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
);
|
|
367
|
+
\`\`\`
|
|
368
|
+
|
|
369
|
+
## Best Practices
|
|
370
|
+
|
|
371
|
+
1. **Descriptive Names**: Use clear, action-oriented names (e.g., \`fetch_weather\`, not \`weather\`)
|
|
372
|
+
2. **Detailed Descriptions**: The description helps the LLM understand when to use the tool
|
|
373
|
+
3. **Zod Descriptions**: Add \`.describe()\` to all schema fields for better LLM understanding
|
|
374
|
+
4. **Error Handling**: Always wrap in try/catch and return proper error status
|
|
375
|
+
5. **Type Safety**: Use Zod for runtime validation and TypeScript inference
|
|
376
|
+
6. **Keep It Simple**: Each tool should do one thing well
|
|
377
|
+
7. **Stateless**: Don't rely on global state; use FlowState for context
|
|
378
|
+
|
|
379
|
+
## Debugging
|
|
380
|
+
|
|
381
|
+
- Check console output for tool execution logs
|
|
382
|
+
- Tool errors appear in the logs table
|
|
383
|
+
- Use \`console.log\` within tools (visible in dev mode)
|
|
384
|
+
- Check message history to see tool calls and responses
|
|
385
|
+
|
|
386
|
+
## Testing
|
|
387
|
+
|
|
388
|
+
Tools can be tested independently:
|
|
389
|
+
|
|
390
|
+
\`\`\`typescript
|
|
391
|
+
import myTool from './my_tool';
|
|
392
|
+
|
|
393
|
+
const [description, schema, handler] = myTool;
|
|
394
|
+
|
|
395
|
+
// Mock FlowState
|
|
396
|
+
const mockFlow = {
|
|
397
|
+
env: mockEnv,
|
|
398
|
+
storage: mockStorage,
|
|
399
|
+
threadId: 'test-123',
|
|
400
|
+
// ...
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const result = await handler(mockFlow, { param1: 'test' });
|
|
404
|
+
console.log(result);
|
|
405
|
+
\`\`\`
|
|
406
|
+
|
|
407
|
+
## Limitations
|
|
408
|
+
|
|
409
|
+
- No nested directories (tools must be directly in this folder)
|
|
410
|
+
- Tool execution is **sequential** (no parallel execution)
|
|
411
|
+
- Tool results must be JSON-serializable strings
|
|
412
|
+
- Maximum execution time limited by Workers CPU time
|
|
413
|
+
|
|
414
|
+
## Related
|
|
415
|
+
|
|
416
|
+
- **Hooks**: \`agents/hooks/CLAUDE.md\` - Lifecycle hooks
|
|
417
|
+
- **APIs**: \`agents/api/CLAUDE.md\` - Thread-specific endpoints
|
|
418
|
+
- **Documentation**: Project root \`CLAUDE.md\` for architecture overview
|
|
419
|
+
`;
|
|
420
|
+
var HOOKS_CLAUDE_MD = `# Lifecycle Hooks
|
|
421
|
+
|
|
422
|
+
This directory contains lifecycle hooks that intercept and modify agent execution at key points.
|
|
423
|
+
|
|
424
|
+
## What Are Hooks?
|
|
425
|
+
|
|
426
|
+
Hooks are optional functions that run at specific points during agent execution:
|
|
427
|
+
- **Before** LLM requests (prefilter messages)
|
|
428
|
+
- **After** LLM responses (post-process messages)
|
|
429
|
+
- At other lifecycle events (message creation, tool execution, etc.)
|
|
430
|
+
|
|
431
|
+
Unlike tools (which agents explicitly call), hooks run **automatically** when their trigger point occurs.
|
|
432
|
+
|
|
433
|
+
## Using defineHook for Type Safety
|
|
434
|
+
|
|
435
|
+
All hooks should use the \`defineHook\` utility for strict typing:
|
|
436
|
+
|
|
437
|
+
\`\`\`typescript
|
|
438
|
+
import { defineHook } from '@standardagents/builder';
|
|
439
|
+
|
|
440
|
+
export default defineHook('filter_messages', async (state, rows) => {
|
|
441
|
+
// TypeScript knows exactly what state and rows are!
|
|
442
|
+
return rows;
|
|
443
|
+
});
|
|
444
|
+
\`\`\`
|
|
445
|
+
|
|
446
|
+
The \`defineHook\` function provides:
|
|
447
|
+
- **Strict typing** for hook parameters based on the hook name
|
|
448
|
+
- **IntelliSense** support in your editor
|
|
449
|
+
- **Type checking** to catch errors before runtime
|
|
450
|
+
- **Better documentation** through type hints
|
|
451
|
+
|
|
452
|
+
## Available Hooks
|
|
453
|
+
|
|
454
|
+
### \`filter_messages\`
|
|
455
|
+
|
|
456
|
+
**When**: Before message history is transformed to chat completion format
|
|
457
|
+
**Purpose**: Filter or modify SQL row data with access to ALL database columns
|
|
458
|
+
|
|
459
|
+
\`\`\`typescript
|
|
460
|
+
import { defineHook, type MessageRow } from '@standardagents/builder';
|
|
461
|
+
|
|
462
|
+
export default defineHook('filter_messages', async (state, rows) => {
|
|
463
|
+
// rows contains ALL columns from messages table:
|
|
464
|
+
// id, role, content, name, tool_calls, tool_call_id, log_id,
|
|
465
|
+
// created_at, request_sent_at, response_completed_at, status,
|
|
466
|
+
// silent, tool_status
|
|
467
|
+
|
|
468
|
+
// Your filtering logic here
|
|
469
|
+
return rows;
|
|
470
|
+
});
|
|
471
|
+
\`\`\`
|
|
472
|
+
|
|
473
|
+
**Common Use Cases**:
|
|
474
|
+
- Filter out failed tool messages (\`tool_status = 'error'\`)
|
|
475
|
+
- Remove messages older than a certain time
|
|
476
|
+
- Filter by status (pending, completed, failed)
|
|
477
|
+
- Access columns not available in chat format
|
|
478
|
+
- Filter based on database-specific metadata
|
|
479
|
+
|
|
480
|
+
**Example - Filter Out Failed Tools**:
|
|
481
|
+
\`\`\`typescript
|
|
482
|
+
export default defineHook('filter_messages', async (state, rows) => {
|
|
483
|
+
// Remove tool messages that failed execution
|
|
484
|
+
return rows.filter(row => {
|
|
485
|
+
if (row.role === 'tool' && row.tool_status === 'error') {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
return true;
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
\`\`\`
|
|
492
|
+
|
|
493
|
+
**Important**: This hook runs **before** messages are transformed to Message objects and receives raw SQL data. Use this when you need access to database columns like \`tool_status\`, \`silent\`, or \`status\`.
|
|
494
|
+
|
|
495
|
+
### \`prefilter_llm_history\`
|
|
496
|
+
|
|
497
|
+
**When**: Immediately after message transformation, before sending to LLM
|
|
498
|
+
**Purpose**: Modify, filter, or limit message history in chat completion format
|
|
499
|
+
|
|
500
|
+
\`\`\`typescript
|
|
501
|
+
import { defineHook } from '@standardagents/builder';
|
|
502
|
+
|
|
503
|
+
export default defineHook('prefilter_llm_history', async (state, messages) => {
|
|
504
|
+
// messages are in chat completion format (already transformed from SQL rows)
|
|
505
|
+
// Available fields: role, content, tool_calls, tool_call_id, name
|
|
506
|
+
|
|
507
|
+
// Your filtering logic here
|
|
508
|
+
return messages;
|
|
509
|
+
});
|
|
510
|
+
\`\`\`
|
|
511
|
+
|
|
512
|
+
**Common Use Cases**:
|
|
513
|
+
- Limit conversation history to last N messages
|
|
514
|
+
- Remove old tool messages to reduce token usage
|
|
515
|
+
- Summarize old messages before sending
|
|
516
|
+
- Add dynamic context based on message patterns
|
|
517
|
+
- Filter out sensitive information
|
|
518
|
+
|
|
519
|
+
**Example - Limit to Last 10 Messages**:
|
|
520
|
+
\`\`\`typescript
|
|
521
|
+
import { defineHook } from '@standardagents/builder';
|
|
522
|
+
|
|
523
|
+
export default defineHook('prefilter_llm_history', async (state, messages) => {
|
|
524
|
+
// Keep all system messages
|
|
525
|
+
const systemMessages = messages.filter(m => m.role === 'system');
|
|
526
|
+
|
|
527
|
+
// Take only last 10 non-system messages
|
|
528
|
+
const otherMessages = messages
|
|
529
|
+
.filter(m => m.role !== 'system')
|
|
530
|
+
.slice(-10);
|
|
531
|
+
|
|
532
|
+
return [...systemMessages, ...otherMessages];
|
|
533
|
+
});
|
|
534
|
+
\`\`\`
|
|
535
|
+
|
|
536
|
+
**Example - Remove Old Tool Messages**:
|
|
537
|
+
\`\`\`typescript
|
|
538
|
+
import { defineHook } from '@standardagents/builder';
|
|
539
|
+
|
|
540
|
+
export default defineHook('prefilter_llm_history', async (state, messages) => {
|
|
541
|
+
// Keep messages from last 5 turns, remove old tool messages
|
|
542
|
+
const recentThreshold = messages.length - 10;
|
|
543
|
+
|
|
544
|
+
return messages.filter((m, index) => {
|
|
545
|
+
if (m.role === 'tool' && index < recentThreshold) {
|
|
546
|
+
return false; // Remove old tool messages
|
|
547
|
+
}
|
|
548
|
+
return true;
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
\`\`\`
|
|
552
|
+
|
|
553
|
+
**Important**: The **filtered messages are logged**, so you can see exactly what was sent to the LLM in the logs table.
|
|
554
|
+
|
|
555
|
+
### \`post_process_message\`
|
|
556
|
+
|
|
557
|
+
**When**: After receiving a response from the LLM
|
|
558
|
+
**Purpose**: Modify or enhance the assistant's message
|
|
559
|
+
|
|
560
|
+
\`\`\`typescript
|
|
561
|
+
import { defineHook } from '@standardagents/builder';
|
|
562
|
+
|
|
563
|
+
export default defineHook('post_process_message', async (state, message) => {
|
|
564
|
+
// Your post-processing logic here
|
|
565
|
+
return message;
|
|
566
|
+
});
|
|
567
|
+
\`\`\`
|
|
568
|
+
|
|
569
|
+
**Common Use Cases**:
|
|
570
|
+
- Format or clean up LLM output
|
|
571
|
+
- Add metadata or tracking information
|
|
572
|
+
- Inject additional context into responses
|
|
573
|
+
- Transform tool call formats
|
|
574
|
+
|
|
575
|
+
**Example - Add Metadata**:
|
|
576
|
+
\`\`\`typescript
|
|
577
|
+
import { defineHook } from '@standardagents/builder';
|
|
578
|
+
|
|
579
|
+
export default defineHook('post_process_message', async (state, message) => {
|
|
580
|
+
if (message.content) {
|
|
581
|
+
message.content += \`\\n\\n_Generated at turn \${state.turnCount}_\`;
|
|
582
|
+
}
|
|
583
|
+
return message;
|
|
584
|
+
});
|
|
585
|
+
\`\`\`
|
|
586
|
+
|
|
587
|
+
**Note**: This hook is currently defined but **not yet invoked** in FlowEngine. It will be activated in a future update.
|
|
588
|
+
|
|
589
|
+
### Message Lifecycle Hooks
|
|
590
|
+
|
|
591
|
+
The following hooks run for **ANY** message being created or updated in the database, whether it's an agent message, tool message, user message, or system message.
|
|
592
|
+
|
|
593
|
+
#### \`before_create_message\`
|
|
594
|
+
|
|
595
|
+
**When**: Immediately before a message is inserted into the database
|
|
596
|
+
**Purpose**: Modify message data before it's stored
|
|
597
|
+
|
|
598
|
+
\`\`\`typescript
|
|
599
|
+
import { defineHook } from '@standardagents/builder';
|
|
600
|
+
|
|
601
|
+
export default defineHook('before_create_message', async (state, message) => {
|
|
602
|
+
// Your modification logic here
|
|
603
|
+
return message;
|
|
604
|
+
});
|
|
605
|
+
\`\`\`
|
|
606
|
+
|
|
607
|
+
**Message Structure**:
|
|
608
|
+
\`\`\`typescript
|
|
609
|
+
{
|
|
610
|
+
id: string;
|
|
611
|
+
role: string;
|
|
612
|
+
content: string | null;
|
|
613
|
+
tool_calls?: string | null;
|
|
614
|
+
tool_call_id?: string | null;
|
|
615
|
+
name?: string | null;
|
|
616
|
+
created_at: number;
|
|
617
|
+
status?: string;
|
|
618
|
+
silent?: boolean;
|
|
619
|
+
}
|
|
620
|
+
\`\`\`
|
|
621
|
+
|
|
622
|
+
**Common Use Cases**:
|
|
623
|
+
- Add prefixes or metadata to message content
|
|
624
|
+
- Modify message based on agent configuration
|
|
625
|
+
- Add tracking IDs or identifiers
|
|
626
|
+
- Transform message format before storage
|
|
627
|
+
|
|
628
|
+
**Example - Add Metadata**:
|
|
629
|
+
\`\`\`typescript
|
|
630
|
+
import { defineHook } from '@standardagents/builder';
|
|
631
|
+
|
|
632
|
+
export default defineHook('before_create_message', async (state, message) => {
|
|
633
|
+
// Add agent ID to all messages
|
|
634
|
+
if (message.content && message.role === 'assistant') {
|
|
635
|
+
message.name = state.agentConfig.title;
|
|
636
|
+
}
|
|
637
|
+
return message;
|
|
638
|
+
});
|
|
639
|
+
\`\`\`
|
|
640
|
+
|
|
641
|
+
#### \`after_create_message\`
|
|
642
|
+
|
|
643
|
+
**When**: Immediately after a message is inserted into the database
|
|
644
|
+
**Purpose**: Perform actions based on newly created messages
|
|
645
|
+
|
|
646
|
+
\`\`\`typescript
|
|
647
|
+
import { defineHook } from '@standardagents/builder';
|
|
648
|
+
|
|
649
|
+
export default defineHook('after_create_message', async (state, message) => {
|
|
650
|
+
// Your post-creation logic here
|
|
651
|
+
// No return value needed
|
|
652
|
+
});
|
|
653
|
+
\`\`\`
|
|
654
|
+
|
|
655
|
+
**Common Use Cases**:
|
|
656
|
+
- Log messages to external systems
|
|
657
|
+
- Trigger webhooks or notifications
|
|
658
|
+
- Update analytics or metrics
|
|
659
|
+
- Send real-time updates to monitoring services
|
|
660
|
+
|
|
661
|
+
**Example - Log to External Service**:
|
|
662
|
+
\`\`\`typescript
|
|
663
|
+
import { defineHook } from '@standardagents/builder';
|
|
664
|
+
|
|
665
|
+
export default defineHook('after_create_message', async (state, message) => {
|
|
666
|
+
// Log all assistant messages to analytics
|
|
667
|
+
if (message.role === 'assistant' && message.content) {
|
|
668
|
+
try {
|
|
669
|
+
await fetch('https://analytics.example.com/message', {
|
|
670
|
+
method: 'POST',
|
|
671
|
+
body: JSON.stringify({
|
|
672
|
+
threadId: state.threadId,
|
|
673
|
+
messageId: message.id,
|
|
674
|
+
contentLength: message.content.length,
|
|
675
|
+
})
|
|
676
|
+
});
|
|
677
|
+
} catch (error) {
|
|
678
|
+
console.error('Analytics logging failed:', error);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
\`\`\`
|
|
683
|
+
|
|
684
|
+
#### \`before_update_message\`
|
|
685
|
+
|
|
686
|
+
**When**: Immediately before a message is updated in the database
|
|
687
|
+
**Purpose**: Modify update data before it's applied
|
|
688
|
+
|
|
689
|
+
\`\`\`typescript
|
|
690
|
+
import { defineHook } from '@standardagents/builder';
|
|
691
|
+
|
|
692
|
+
export default defineHook('before_update_message', async (state, messageId, updates) => {
|
|
693
|
+
// Your modification logic here
|
|
694
|
+
return updates;
|
|
695
|
+
});
|
|
696
|
+
\`\`\`
|
|
697
|
+
|
|
698
|
+
**Common Use Cases**:
|
|
699
|
+
- Validate or sanitize updated content
|
|
700
|
+
- Add completion timestamps
|
|
701
|
+
- Transform status values
|
|
702
|
+
- Inject metadata into updates
|
|
703
|
+
|
|
704
|
+
**Example - Add Completion Timestamp**:
|
|
705
|
+
\`\`\`typescript
|
|
706
|
+
import { defineHook } from '@standardagents/builder';
|
|
707
|
+
|
|
708
|
+
export default defineHook('before_update_message', async (state, messageId, updates) => {
|
|
709
|
+
// Add custom completion timestamp for completed messages
|
|
710
|
+
if (updates.status === 'completed' && !updates.response_completed_at) {
|
|
711
|
+
updates.response_completed_at = Date.now() * 1000; // microseconds
|
|
712
|
+
}
|
|
713
|
+
return updates;
|
|
714
|
+
});
|
|
715
|
+
\`\`\`
|
|
716
|
+
|
|
717
|
+
#### \`after_update_message\`
|
|
718
|
+
|
|
719
|
+
**When**: Immediately after a message is updated in the database
|
|
720
|
+
**Purpose**: Perform actions based on message updates
|
|
721
|
+
|
|
722
|
+
\`\`\`typescript
|
|
723
|
+
import { defineHook } from '@standardagents/builder';
|
|
724
|
+
|
|
725
|
+
export default defineHook('after_update_message', async (state, messageId, updates) => {
|
|
726
|
+
// Your post-update logic here
|
|
727
|
+
// No return value needed
|
|
728
|
+
});
|
|
729
|
+
\`\`\`
|
|
730
|
+
|
|
731
|
+
**Common Use Cases**:
|
|
732
|
+
- Track message status changes
|
|
733
|
+
- Trigger notifications on completion
|
|
734
|
+
- Update external systems
|
|
735
|
+
- Log state transitions
|
|
736
|
+
|
|
737
|
+
**Example - Notify on Failure**:
|
|
738
|
+
\`\`\`typescript
|
|
739
|
+
import { defineHook } from '@standardagents/builder';
|
|
740
|
+
|
|
741
|
+
export default defineHook('after_update_message', async (state, messageId, updates) => {
|
|
742
|
+
// Send notification when message is marked as failed
|
|
743
|
+
if (updates.status === 'failed') {
|
|
744
|
+
console.log(\`[Alert] Message \${messageId} failed in thread \${state.threadId}\`);
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
\`\`\`
|
|
748
|
+
|
|
749
|
+
**Important**:
|
|
750
|
+
- \`before_*\` hooks must return the modified data object
|
|
751
|
+
- \`after_*\` hooks don't need to return anything
|
|
752
|
+
- All 4 hooks run for ANY message operation (agent, tool, user, system messages)
|
|
753
|
+
- Updates passed to update hooks contain only the fields being updated
|
|
754
|
+
|
|
755
|
+
## Creating a Hook
|
|
756
|
+
|
|
757
|
+
1. Create a file named exactly as the hook (e.g., \`prefilter_llm_history.ts\`)
|
|
758
|
+
2. Use \`defineHook\` to ensure correct typing
|
|
759
|
+
3. Return the modified data (or original if no changes)
|
|
760
|
+
|
|
761
|
+
\`\`\`typescript
|
|
762
|
+
// agents/hooks/prefilter_llm_history.ts
|
|
763
|
+
import { defineHook } from '@standardagents/builder';
|
|
764
|
+
|
|
765
|
+
export default defineHook('prefilter_llm_history', async (state, messages) => {
|
|
766
|
+
console.log(\`Processing \${messages.length} messages\`);
|
|
767
|
+
|
|
768
|
+
// Your logic here
|
|
769
|
+
|
|
770
|
+
return messages; // Return modified or original
|
|
771
|
+
});
|
|
772
|
+
\`\`\`
|
|
773
|
+
|
|
774
|
+
## Hook Lifecycle
|
|
775
|
+
|
|
776
|
+
1. **Discovery**: Vite plugin scans this directory on startup
|
|
777
|
+
2. **Virtual Module**: Generates \`virtual:agent-hooks\` with lazy imports
|
|
778
|
+
3. **Lazy Loading**: Hooks are loaded on first use (not at startup)
|
|
779
|
+
4. **Caching**: Once loaded, hooks are cached for performance
|
|
780
|
+
5. **HMR**: Changes trigger hot reload in development
|
|
781
|
+
|
|
782
|
+
## Error Handling
|
|
783
|
+
|
|
784
|
+
Hooks are designed to be **safe**:
|
|
785
|
+
- If hook file doesn't exist \u2192 Silently skipped (no error)
|
|
786
|
+
- If hook throws error \u2192 Logged to console, original data returned
|
|
787
|
+
- If hook returns invalid data \u2192 Original data used as fallback
|
|
788
|
+
|
|
789
|
+
This ensures hooks never break agent execution.
|
|
790
|
+
|
|
791
|
+
## FlowState Object
|
|
792
|
+
|
|
793
|
+
All hooks receive the FlowState context:
|
|
794
|
+
|
|
795
|
+
\`\`\`typescript
|
|
796
|
+
interface FlowState {
|
|
797
|
+
threadId: string; // Current thread ID
|
|
798
|
+
flowId: string; // Unique execution ID
|
|
799
|
+
agentConfig: Agent; // Full agent configuration
|
|
800
|
+
currentSide: 'a' | 'b'; // Which side is executing (dual_ai)
|
|
801
|
+
turnCount: number; // Current turn number
|
|
802
|
+
stopped: boolean; // Whether execution should stop
|
|
803
|
+
messageHistory: Message[]; // Full conversation history
|
|
804
|
+
env: Env; // Cloudflare bindings
|
|
805
|
+
storage: DurableObjectStorage; // Thread's SQLite storage
|
|
806
|
+
context: Record<string, any>; // Arbitrary state data
|
|
807
|
+
// ... and more
|
|
808
|
+
}
|
|
809
|
+
\`\`\`
|
|
810
|
+
|
|
811
|
+
Use this to make context-aware decisions in your hooks.
|
|
812
|
+
|
|
813
|
+
## Best Practices
|
|
814
|
+
|
|
815
|
+
1. **Keep Hooks Fast**: They run on every execution, avoid heavy operations
|
|
816
|
+
2. **Always Return Data**: Never return \`undefined\` or \`null\`
|
|
817
|
+
3. **Log Thoughtfully**: Use \`console.log\` sparingly (visible in logs)
|
|
818
|
+
4. **Handle Errors**: Wrap risky operations in try/catch
|
|
819
|
+
5. **Document Behavior**: Add comments explaining what your hook does
|
|
820
|
+
6. **Test Thoroughly**: Hooks affect ALL agent executions
|
|
821
|
+
|
|
822
|
+
## Debugging
|
|
823
|
+
|
|
824
|
+
- Check console output for hook loading/execution logs
|
|
825
|
+
- Hook errors are logged with \`[Hooks] \u2717\` prefix
|
|
826
|
+
- Inspect logs table to see filtered messages (for prefilter hook)
|
|
827
|
+
- Use \`console.log\` within hooks for debugging
|
|
828
|
+
|
|
829
|
+
## Performance Considerations
|
|
830
|
+
|
|
831
|
+
Hooks run on **every turn**, so:
|
|
832
|
+
- Avoid expensive operations (heavy computation, slow APIs)
|
|
833
|
+
- Cache results when possible
|
|
834
|
+
- Consider using FlowState context to skip unnecessary work
|
|
835
|
+
- Use async operations efficiently
|
|
836
|
+
|
|
837
|
+
## Message Data & Tool Status
|
|
838
|
+
|
|
839
|
+
The \`filter_messages\` hook receives SQL row data with ALL columns from the messages table:
|
|
840
|
+
|
|
841
|
+
\`\`\`typescript
|
|
842
|
+
interface MessageRow {
|
|
843
|
+
id: string;
|
|
844
|
+
role: 'system' | 'user' | 'assistant' | 'tool';
|
|
845
|
+
content: string | null;
|
|
846
|
+
name: string | null;
|
|
847
|
+
tool_calls: string | null; // JSON string of tool calls
|
|
848
|
+
tool_call_id: string | null; // For role='tool' messages
|
|
849
|
+
log_id: string | null; // Reference to logs table
|
|
850
|
+
created_at: number; // Microseconds timestamp
|
|
851
|
+
request_sent_at: number | null; // When request was sent to LLM
|
|
852
|
+
response_completed_at: number | null; // When response completed
|
|
853
|
+
status: 'pending' | 'completed' | 'failed' | null;
|
|
854
|
+
silent: number | null; // 1 if message should be hidden, 0/null otherwise
|
|
855
|
+
tool_status: 'success' | 'error' | null; // Status of tool execution (tool messages only)
|
|
856
|
+
}
|
|
857
|
+
\`\`\`
|
|
858
|
+
|
|
859
|
+
The \`tool_status\` column is automatically set when tool messages are created:
|
|
860
|
+
- \`'success'\` - Tool executed successfully
|
|
861
|
+
- \`'error'\` - Tool execution failed or threw an error
|
|
862
|
+
- \`null\` - Not a tool message (role !== 'tool')
|
|
863
|
+
|
|
864
|
+
## Testing
|
|
865
|
+
|
|
866
|
+
Example test for a hook:
|
|
867
|
+
|
|
868
|
+
\`\`\`typescript
|
|
869
|
+
import { defineHook } from '@standardagents/builder';
|
|
870
|
+
import prefilterHook from './prefilter_llm_history';
|
|
871
|
+
|
|
872
|
+
const mockState = {
|
|
873
|
+
turnCount: 5,
|
|
874
|
+
currentSide: 'a',
|
|
875
|
+
// ... other FlowState fields
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
const mockMessages = [
|
|
879
|
+
{ role: 'system', content: 'You are helpful' },
|
|
880
|
+
{ role: 'user', content: 'Hello' },
|
|
881
|
+
{ role: 'assistant', content: 'Hi there!' },
|
|
882
|
+
// ... more messages
|
|
883
|
+
];
|
|
884
|
+
|
|
885
|
+
const result = await prefilterHook(mockState, mockMessages);
|
|
886
|
+
console.log('Filtered:', result.length, 'messages');
|
|
887
|
+
\`\`\`
|
|
888
|
+
|
|
889
|
+
## Hook File Naming
|
|
890
|
+
|
|
891
|
+
**Critical**: Hook files must be named **exactly** as expected:
|
|
892
|
+
- \u2713 \`filter_messages.ts\`
|
|
893
|
+
- \u2713 \`prefilter_llm_history.ts\`
|
|
894
|
+
- \u2713 \`post_process_message.ts\`
|
|
895
|
+
- \u2713 \`before_create_message.ts\`
|
|
896
|
+
- \u2713 \`after_create_message.ts\`
|
|
897
|
+
- \u2713 \`before_update_message.ts\`
|
|
898
|
+
- \u2713 \`after_update_message.ts\`
|
|
899
|
+
- \u2717 \`filterMessages.ts\` (wrong case)
|
|
900
|
+
- \u2717 \`prefilterLLMHistory.ts\` (wrong case)
|
|
901
|
+
- \u2717 \`before-create-message.ts\` (wrong separator)
|
|
902
|
+
|
|
903
|
+
The file name determines which hook point it intercepts.
|
|
904
|
+
|
|
905
|
+
## Available Hooks
|
|
906
|
+
|
|
907
|
+
Currently implemented hooks:
|
|
908
|
+
- \`filter_messages\` - Filter SQL row data before transformation to chat format (access to all DB columns including tool_status)
|
|
909
|
+
- \`prefilter_llm_history\` - Filter messages before sending to LLM (after transformation to chat format)
|
|
910
|
+
- \`before_create_message\` - Before inserting message into database
|
|
911
|
+
- \`after_create_message\` - After message is created in database
|
|
912
|
+
- \`before_update_message\` - Before updating message in database
|
|
913
|
+
- \`after_update_message\` - After message is updated in database
|
|
914
|
+
- \`after_tool_call_success\` - After successful tool execution (can modify result or return null to remove)
|
|
915
|
+
- \`after_tool_call_failure\` - After failed tool execution (can modify error or return null to remove)
|
|
916
|
+
|
|
917
|
+
## Related
|
|
918
|
+
|
|
919
|
+
- **Tools**: \`agents/tools/CLAUDE.md\` - Custom tools
|
|
920
|
+
- **APIs**: \`agents/api/CLAUDE.md\` - Thread endpoints
|
|
921
|
+
- **Documentation**: Project root \`CLAUDE.md\` for architecture
|
|
922
|
+
`;
|
|
923
|
+
var API_CLAUDE_MD = `# Thread-Specific API Endpoints
|
|
924
|
+
|
|
925
|
+
This directory contains custom API endpoints that operate on specific threads.
|
|
926
|
+
|
|
927
|
+
## What Are Thread Endpoints?
|
|
928
|
+
|
|
929
|
+
Thread endpoints are API routes that:
|
|
930
|
+
- Automatically receive a specific thread's Durable Object instance
|
|
931
|
+
- Can access thread-local SQLite storage
|
|
932
|
+
- Perform operations in the context of a conversation
|
|
933
|
+
- Use file-based routing for automatic discovery
|
|
934
|
+
|
|
935
|
+
## File-Based Routing
|
|
936
|
+
|
|
937
|
+
Create files following this naming convention:
|
|
938
|
+
|
|
939
|
+
\`\`\`
|
|
940
|
+
agents/api/
|
|
941
|
+
\u251C\u2500\u2500 summary.get.ts # GET /agents/api/threads/:id/summary
|
|
942
|
+
\u251C\u2500\u2500 export.post.ts # POST /agents/api/threads/:id/export
|
|
943
|
+
\u251C\u2500\u2500 metadata.put.ts # PUT /agents/api/threads/:id/metadata
|
|
944
|
+
\u2514\u2500\u2500 archive.delete.ts # DELETE /agents/api/threads/:id/archive
|
|
945
|
+
\`\`\`
|
|
946
|
+
|
|
947
|
+
**Pattern**: \`{name}.{method}.ts\`
|
|
948
|
+
|
|
949
|
+
**Methods**: \`get\`, \`post\`, \`put\`, \`delete\`, \`patch\`
|
|
950
|
+
|
|
951
|
+
**URL**: \`/agents/api/threads/:id/{name}\`
|
|
952
|
+
|
|
953
|
+
## Creating a Thread Endpoint
|
|
954
|
+
|
|
955
|
+
Use \`defineThreadEndpoint\` from \`@standardagents/builder\`:
|
|
956
|
+
|
|
957
|
+
\`\`\`typescript
|
|
958
|
+
import { defineThreadEndpoint } from '@standardagents/builder';
|
|
959
|
+
|
|
960
|
+
export default defineThreadEndpoint(async (thread, request, env) => {
|
|
961
|
+
// thread is the DurableObject stub for the requested thread
|
|
962
|
+
// request is the incoming Request object
|
|
963
|
+
// env is the Cloudflare environment with bindings
|
|
964
|
+
|
|
965
|
+
// Access thread's storage directly
|
|
966
|
+
const messages = await thread.getMessages();
|
|
967
|
+
|
|
968
|
+
// Perform operations
|
|
969
|
+
const summary = generateSummary(messages);
|
|
970
|
+
|
|
971
|
+
// Return response
|
|
972
|
+
return new Response(JSON.stringify({ summary }), {
|
|
973
|
+
headers: { 'Content-Type': 'application/json' }
|
|
974
|
+
});
|
|
975
|
+
});
|
|
976
|
+
\`\`\`
|
|
977
|
+
|
|
978
|
+
## Thread Object
|
|
979
|
+
|
|
980
|
+
The \`thread\` parameter is a DurableObject stub with RPC methods:
|
|
981
|
+
|
|
982
|
+
\`\`\`typescript
|
|
983
|
+
interface DurableThread {
|
|
984
|
+
// Get messages from thread's SQLite storage
|
|
985
|
+
getMessages(limit?: number, offset?: number): Promise<Message[]>;
|
|
986
|
+
|
|
987
|
+
// Get execution logs
|
|
988
|
+
getLogs(limit?: number, offset?: number): Promise<Log[]>;
|
|
989
|
+
|
|
990
|
+
// Process a new message (starts agent execution)
|
|
991
|
+
processMessage(content: string, role?: string): Promise<Response>;
|
|
992
|
+
|
|
993
|
+
// Get thread metadata
|
|
994
|
+
getThreadMeta(threadId: string): Promise<ThreadMetadata>;
|
|
995
|
+
|
|
996
|
+
// Direct storage access (advanced)
|
|
997
|
+
storage: DurableObjectStorage;
|
|
998
|
+
}
|
|
999
|
+
\`\`\`
|
|
1000
|
+
|
|
1001
|
+
## Examples
|
|
1002
|
+
|
|
1003
|
+
### GET Summary Endpoint
|
|
1004
|
+
|
|
1005
|
+
\`\`\`typescript
|
|
1006
|
+
// agents/api/summary.get.ts
|
|
1007
|
+
import { defineThreadEndpoint } from '@standardagents/builder';
|
|
1008
|
+
|
|
1009
|
+
export default defineThreadEndpoint(async (thread, request, env) => {
|
|
1010
|
+
// Fetch messages from thread
|
|
1011
|
+
const messages = await thread.getMessages();
|
|
1012
|
+
|
|
1013
|
+
// Generate summary
|
|
1014
|
+
const userMessages = messages.filter(m => m.role === 'user');
|
|
1015
|
+
const assistantMessages = messages.filter(m => m.role === 'assistant');
|
|
1016
|
+
|
|
1017
|
+
return new Response(JSON.stringify({
|
|
1018
|
+
total_messages: messages.length,
|
|
1019
|
+
user_messages: userMessages.length,
|
|
1020
|
+
assistant_messages: assistantMessages.length,
|
|
1021
|
+
first_message: messages[0]?.content,
|
|
1022
|
+
last_message: messages[messages.length - 1]?.content,
|
|
1023
|
+
}), {
|
|
1024
|
+
headers: { 'Content-Type': 'application/json' }
|
|
1025
|
+
});
|
|
1026
|
+
});
|
|
1027
|
+
\`\`\`
|
|
1028
|
+
|
|
1029
|
+
**Usage**: \`GET /agents/api/threads/{threadId}/summary\`
|
|
1030
|
+
|
|
1031
|
+
### POST Export Endpoint
|
|
1032
|
+
|
|
1033
|
+
\`\`\`typescript
|
|
1034
|
+
// agents/api/export.post.ts
|
|
1035
|
+
import { defineThreadEndpoint } from '@standardagents/builder';
|
|
1036
|
+
|
|
1037
|
+
export default defineThreadEndpoint(async (thread, request, env) => {
|
|
1038
|
+
const { format } = await request.json();
|
|
1039
|
+
|
|
1040
|
+
// Get messages
|
|
1041
|
+
const messages = await thread.getMessages();
|
|
1042
|
+
|
|
1043
|
+
// Export based on format
|
|
1044
|
+
let content: string;
|
|
1045
|
+
let contentType: string;
|
|
1046
|
+
|
|
1047
|
+
if (format === 'json') {
|
|
1048
|
+
content = JSON.stringify(messages, null, 2);
|
|
1049
|
+
contentType = 'application/json';
|
|
1050
|
+
} else if (format === 'markdown') {
|
|
1051
|
+
content = messages
|
|
1052
|
+
.map(m => \`**\${m.role}**: \${m.content}\`)
|
|
1053
|
+
.join('\\n\\n');
|
|
1054
|
+
contentType = 'text/markdown';
|
|
1055
|
+
} else {
|
|
1056
|
+
return new Response('Invalid format', { status: 400 });
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
return new Response(content, {
|
|
1060
|
+
headers: {
|
|
1061
|
+
'Content-Type': contentType,
|
|
1062
|
+
'Content-Disposition': \`attachment; filename="thread-export.\${format}"\`
|
|
1063
|
+
}
|
|
1064
|
+
});
|
|
1065
|
+
});
|
|
1066
|
+
\`\`\`
|
|
1067
|
+
|
|
1068
|
+
**Usage**: \`POST /agents/api/threads/{threadId}/export\`
|
|
1069
|
+
|
|
1070
|
+
### Direct Storage Access
|
|
1071
|
+
|
|
1072
|
+
\`\`\`typescript
|
|
1073
|
+
// agents/api/stats.get.ts
|
|
1074
|
+
import { defineThreadEndpoint } from '@standardagents/builder';
|
|
1075
|
+
|
|
1076
|
+
export default defineThreadEndpoint(async (thread, request, env) => {
|
|
1077
|
+
// Access SQLite storage directly
|
|
1078
|
+
const storage = (thread as any).storage;
|
|
1079
|
+
|
|
1080
|
+
const cursor = await storage.sql.exec(\`
|
|
1081
|
+
SELECT
|
|
1082
|
+
COUNT(*) as count,
|
|
1083
|
+
role,
|
|
1084
|
+
AVG(LENGTH(content)) as avg_length
|
|
1085
|
+
FROM messages
|
|
1086
|
+
GROUP BY role
|
|
1087
|
+
\`);
|
|
1088
|
+
|
|
1089
|
+
const stats = cursor.toArray();
|
|
1090
|
+
|
|
1091
|
+
return new Response(JSON.stringify({ stats }), {
|
|
1092
|
+
headers: { 'Content-Type': 'application/json' }
|
|
1093
|
+
});
|
|
1094
|
+
});
|
|
1095
|
+
\`\`\`
|
|
1096
|
+
|
|
1097
|
+
## Request Handling
|
|
1098
|
+
|
|
1099
|
+
### Reading Request Body
|
|
1100
|
+
|
|
1101
|
+
\`\`\`typescript
|
|
1102
|
+
export default defineThreadEndpoint(async (thread, request, env) => {
|
|
1103
|
+
// JSON
|
|
1104
|
+
const body = await request.json();
|
|
1105
|
+
|
|
1106
|
+
// FormData
|
|
1107
|
+
const formData = await request.formData();
|
|
1108
|
+
|
|
1109
|
+
// Text
|
|
1110
|
+
const text = await request.text();
|
|
1111
|
+
|
|
1112
|
+
// ...
|
|
1113
|
+
});
|
|
1114
|
+
\`\`\`
|
|
1115
|
+
|
|
1116
|
+
### Query Parameters
|
|
1117
|
+
|
|
1118
|
+
\`\`\`typescript
|
|
1119
|
+
export default defineThreadEndpoint(async (thread, request, env) => {
|
|
1120
|
+
const url = new URL(request.url);
|
|
1121
|
+
const limit = url.searchParams.get('limit') || '10';
|
|
1122
|
+
|
|
1123
|
+
const messages = await thread.getMessages(parseInt(limit));
|
|
1124
|
+
|
|
1125
|
+
// ...
|
|
1126
|
+
});
|
|
1127
|
+
\`\`\`
|
|
1128
|
+
|
|
1129
|
+
### Headers
|
|
1130
|
+
|
|
1131
|
+
\`\`\`typescript
|
|
1132
|
+
export default defineThreadEndpoint(async (thread, request, env) => {
|
|
1133
|
+
const authHeader = request.headers.get('Authorization');
|
|
1134
|
+
|
|
1135
|
+
if (!authHeader) {
|
|
1136
|
+
return new Response('Unauthorized', { status: 401 });
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// ...
|
|
1140
|
+
});
|
|
1141
|
+
\`\`\`
|
|
1142
|
+
|
|
1143
|
+
## Error Handling
|
|
1144
|
+
|
|
1145
|
+
Always wrap in try/catch for robust error handling:
|
|
1146
|
+
|
|
1147
|
+
\`\`\`typescript
|
|
1148
|
+
export default defineThreadEndpoint(async (thread, request, env) => {
|
|
1149
|
+
try {
|
|
1150
|
+
// Your endpoint logic
|
|
1151
|
+
const result = await doSomething();
|
|
1152
|
+
|
|
1153
|
+
return new Response(JSON.stringify(result), {
|
|
1154
|
+
headers: { 'Content-Type': 'application/json' }
|
|
1155
|
+
});
|
|
1156
|
+
} catch (error) {
|
|
1157
|
+
console.error('Endpoint error:', error);
|
|
1158
|
+
|
|
1159
|
+
return new Response(
|
|
1160
|
+
JSON.stringify({
|
|
1161
|
+
error: error.message
|
|
1162
|
+
}),
|
|
1163
|
+
{
|
|
1164
|
+
status: 500,
|
|
1165
|
+
headers: { 'Content-Type': 'application/json' }
|
|
1166
|
+
}
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
});
|
|
1170
|
+
\`\`\`
|
|
1171
|
+
|
|
1172
|
+
## Environment Bindings
|
|
1173
|
+
|
|
1174
|
+
Access Cloudflare bindings via \`env\`:
|
|
1175
|
+
|
|
1176
|
+
\`\`\`typescript
|
|
1177
|
+
export default defineThreadEndpoint(async (thread, request, env) => {
|
|
1178
|
+
// Durable Objects
|
|
1179
|
+
const agentBuilder = env.AGENT_BUILDER.get(env.AGENT_BUILDER.idFromName("singleton"));
|
|
1180
|
+
const threadData = await agentBuilder.getThread(threadId);
|
|
1181
|
+
|
|
1182
|
+
// KV (if configured)
|
|
1183
|
+
const cache = await env.MY_KV.get('cache-key');
|
|
1184
|
+
|
|
1185
|
+
// R2 (if configured)
|
|
1186
|
+
const object = await env.MY_BUCKET.get('file.txt');
|
|
1187
|
+
|
|
1188
|
+
// ...
|
|
1189
|
+
});
|
|
1190
|
+
\`\`\`
|
|
1191
|
+
|
|
1192
|
+
## Auto-Discovery
|
|
1193
|
+
|
|
1194
|
+
Thread endpoints are **auto-discovered**:
|
|
1195
|
+
|
|
1196
|
+
1. Vite plugin scans this directory on startup
|
|
1197
|
+
2. Generates virtual module with dynamic imports
|
|
1198
|
+
3. Routes registered in the main router
|
|
1199
|
+
4. HMR works in development
|
|
1200
|
+
|
|
1201
|
+
No manual registration needed!
|
|
1202
|
+
|
|
1203
|
+
## URL Structure
|
|
1204
|
+
|
|
1205
|
+
All thread endpoints follow this pattern:
|
|
1206
|
+
|
|
1207
|
+
\`\`\`
|
|
1208
|
+
/agents/api/threads/:id/{endpoint-name}
|
|
1209
|
+
\`\`\`
|
|
1210
|
+
|
|
1211
|
+
Examples:
|
|
1212
|
+
- \`GET /agents/api/threads/abc-123/summary\`
|
|
1213
|
+
- \`POST /agents/api/threads/abc-123/export\`
|
|
1214
|
+
- \`PUT /agents/api/threads/abc-123/metadata\`
|
|
1215
|
+
|
|
1216
|
+
The \`:id\` is automatically used to fetch the correct Durable Object.
|
|
1217
|
+
|
|
1218
|
+
## Built-In Endpoints
|
|
1219
|
+
|
|
1220
|
+
Standard Agents includes built-in thread endpoints (these are in the framework, not this directory):
|
|
1221
|
+
|
|
1222
|
+
- \`GET /agents/api/threads/:id/messages\` - Get message history
|
|
1223
|
+
- \`POST /agents/api/threads/:id/messages\` - Send a message
|
|
1224
|
+
- \`DELETE /agents/api/threads/:id\` - Delete thread
|
|
1225
|
+
- \`GET /agents/api/threads/:id/logs\` - Get execution logs
|
|
1226
|
+
|
|
1227
|
+
Your custom endpoints extend these built-in routes.
|
|
1228
|
+
|
|
1229
|
+
## Best Practices
|
|
1230
|
+
|
|
1231
|
+
1. **Descriptive Names**: Use clear endpoint names (e.g., \`summary\`, \`export\`)
|
|
1232
|
+
2. **Proper HTTP Methods**: Use GET for reads, POST for creates, etc.
|
|
1233
|
+
3. **Error Handling**: Always wrap in try/catch
|
|
1234
|
+
4. **Type Safety**: Use TypeScript interfaces for request/response
|
|
1235
|
+
5. **Performance**: Be mindful of SQLite query performance
|
|
1236
|
+
6. **Authentication**: Add auth checks if endpoints are sensitive
|
|
1237
|
+
7. **CORS**: Add CORS headers if accessed from browser
|
|
1238
|
+
|
|
1239
|
+
## Testing
|
|
1240
|
+
|
|
1241
|
+
Test endpoints with curl or any HTTP client:
|
|
1242
|
+
|
|
1243
|
+
\`\`\`bash
|
|
1244
|
+
# GET summary
|
|
1245
|
+
curl http://localhost:8787/agents/api/threads/abc-123/summary
|
|
1246
|
+
|
|
1247
|
+
# POST export
|
|
1248
|
+
curl -X POST http://localhost:8787/agents/api/threads/abc-123/export \\
|
|
1249
|
+
-H "Content-Type: application/json" \\
|
|
1250
|
+
-d '{"format": "json"}'
|
|
1251
|
+
\`\`\`
|
|
1252
|
+
|
|
1253
|
+
## Limitations
|
|
1254
|
+
|
|
1255
|
+
- No nested directories (endpoints must be directly in this folder)
|
|
1256
|
+
- Thread ID must be in URL path (handled automatically)
|
|
1257
|
+
- No support for multiple path parameters beyond thread ID
|
|
1258
|
+
- Response must be a Web API \`Response\` object
|
|
1259
|
+
|
|
1260
|
+
## Related
|
|
1261
|
+
|
|
1262
|
+
- **Tools**: \`agents/tools/CLAUDE.md\` - Custom tools
|
|
1263
|
+
- **Hooks**: \`agents/hooks/CLAUDE.md\` - Lifecycle hooks
|
|
1264
|
+
- **Built-in APIs**: \`packages/builder/src/api/\` - Framework endpoints
|
|
1265
|
+
- **Documentation**: Project root \`CLAUDE.md\` for architecture
|
|
1266
|
+
`;
|
|
1267
|
+
async function scaffold() {
|
|
1268
|
+
const cwd = process.cwd();
|
|
1269
|
+
logger.info("Scaffolding Standard Agents directories...");
|
|
1270
|
+
const directories = [
|
|
1271
|
+
{
|
|
1272
|
+
path: path3.join(cwd, "agents", "tools"),
|
|
1273
|
+
claudeMd: TOOLS_CLAUDE_MD,
|
|
1274
|
+
name: "tools"
|
|
1275
|
+
},
|
|
1276
|
+
{
|
|
1277
|
+
path: path3.join(cwd, "agents", "hooks"),
|
|
1278
|
+
claudeMd: HOOKS_CLAUDE_MD,
|
|
1279
|
+
name: "hooks"
|
|
1280
|
+
},
|
|
1281
|
+
{
|
|
1282
|
+
path: path3.join(cwd, "agents", "api"),
|
|
1283
|
+
claudeMd: API_CLAUDE_MD,
|
|
1284
|
+
name: "api"
|
|
1285
|
+
}
|
|
1286
|
+
];
|
|
1287
|
+
let created = 0;
|
|
1288
|
+
let skipped = 0;
|
|
1289
|
+
for (const dir of directories) {
|
|
1290
|
+
if (!fs.existsSync(dir.path)) {
|
|
1291
|
+
fs.mkdirSync(dir.path, { recursive: true });
|
|
1292
|
+
logger.success(`Created directory: agents/${dir.name}`);
|
|
1293
|
+
created++;
|
|
1294
|
+
} else {
|
|
1295
|
+
logger.info(`Directory already exists: agents/${dir.name}`);
|
|
1296
|
+
}
|
|
1297
|
+
const claudeMdPath = path3.join(dir.path, "CLAUDE.md");
|
|
1298
|
+
if (!fs.existsSync(claudeMdPath)) {
|
|
1299
|
+
fs.writeFileSync(claudeMdPath, dir.claudeMd, "utf-8");
|
|
1300
|
+
logger.success(`Created documentation: agents/${dir.name}/CLAUDE.md`);
|
|
1301
|
+
created++;
|
|
1302
|
+
} else {
|
|
1303
|
+
logger.info(`Documentation already exists: agents/${dir.name}/CLAUDE.md (not overwriting)`);
|
|
1304
|
+
skipped++;
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
logger.log("");
|
|
1308
|
+
if (created > 0) {
|
|
1309
|
+
logger.success(`Scaffolding complete! Created ${created} file(s).`);
|
|
1310
|
+
}
|
|
1311
|
+
if (skipped > 0) {
|
|
1312
|
+
logger.info(`Skipped ${skipped} existing file(s).`);
|
|
1313
|
+
}
|
|
1314
|
+
logger.log("\nNext steps:");
|
|
1315
|
+
logger.log("1. Read the CLAUDE.md files in each directory for detailed documentation");
|
|
1316
|
+
logger.log("2. Create your first tool in agents/tools/");
|
|
1317
|
+
logger.log("3. Add lifecycle hooks in agents/hooks/ (optional)");
|
|
1318
|
+
logger.log("4. Create custom thread endpoints in agents/api/ (optional)");
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// src/index.ts
|
|
1322
|
+
var program = new Command();
|
|
1323
|
+
program.name("agentbuilder").description("CLI tool for AgentBuilder initialization").version("0.0.0");
|
|
1324
|
+
program.command("init").description("Initialize AgentBuilder configuration in wrangler.jsonc").option("--force", "Overwrite existing configuration").action(init);
|
|
1325
|
+
program.command("scaffold").description("Create agentbuilder directories (tools, hooks, api) with documentation").action(scaffold);
|
|
1326
|
+
program.parse();
|
|
1327
|
+
//# sourceMappingURL=index.js.map
|
|
1328
|
+
//# sourceMappingURL=index.js.map
|