From fee051dcb33729826cb31910e74fbdf8f57acdeb Mon Sep 17 00:00:00 2001 From: Kris Braun Date: Sun, 19 Oct 2025 18:05:53 -0400 Subject: [PATCH 1/2] Generate agent-guide.ts from AGENTS.template.md --- .changeset/tall-rules-tap.md | 5 + sdk/prebuild.ts | 20 ++ sdk/src/agents-guide.ts | 439 +---------------------------------- sdk/src/plot.ts | 4 +- 4 files changed, 30 insertions(+), 438 deletions(-) create mode 100644 .changeset/tall-rules-tap.md diff --git a/.changeset/tall-rules-tap.md b/.changeset/tall-rules-tap.md new file mode 100644 index 0000000..63512ce --- /dev/null +++ b/.changeset/tall-rules-tap.md @@ -0,0 +1,5 @@ +--- +"@plotday/sdk": patch +--- + +Changed: Generate agent-guide.ts from AGENT.template.md diff --git a/sdk/prebuild.ts b/sdk/prebuild.ts index e35e9ed..62cae90 100644 --- a/sdk/prebuild.ts +++ b/sdk/prebuild.ts @@ -153,4 +153,24 @@ export default llmDocs; const indexPath = join(llmDocsDir, "index.ts"); writeFileSync(indexPath, indexContent, "utf-8"); +// Generate agents-guide-template.ts from AGENTS.template.md +const agentsTemplatePath = join(__dirname, "cli", "templates", "AGENTS.template.md"); +if (existsSync(agentsTemplatePath)) { + const agentsTemplateContent = readFileSync(agentsTemplatePath, "utf-8"); + const agentsGuideContent = `/** + * Generated agents guide template + * + * This file is auto-generated during build. Do not edit manually. + * Generated from: cli/templates/AGENTS.template.md + */ + +export default ${JSON.stringify(agentsTemplateContent)}; +`; + const agentsGuideOutputPath = join(llmDocsDir, "agents-guide-template.ts"); + writeFileSync(agentsGuideOutputPath, agentsGuideContent, "utf-8"); + console.log(`✓ Generated agents-guide-template.ts from AGENTS.template.md`); +} else { + console.warn(`Warning: AGENTS.template.md not found at ${agentsTemplatePath}`); +} + console.log(`✓ Generated ${typeFiles.length} LLM documentation files in src/llm-docs/`); diff --git a/sdk/src/agents-guide.ts b/sdk/src/agents-guide.ts index 7df6860..54f8721 100644 --- a/sdk/src/agents-guide.ts +++ b/sdk/src/agents-guide.ts @@ -2,442 +2,9 @@ * Agent Implementation Guide * * This guide is used by AI systems to generate Plot agents. - * Single source of truth - manually synced from bin/templates/AGENTS.template.md - * - * To update: Copy content from AGENTS.template.md to this constant. + * Auto-generated from cli/templates/AGENTS.template.md during build. */ -export const AGENTS_GUIDE = `# Agent Implementation Guide for LLMs - -This document provides context for AI assistants generating or modifying agent code. - -## Architecture Overview - -Plot agents are TypeScript classes that extend the \`Agent\` base class. Agents interact with external services and Plot's core functionality through a tool-based architecture. - -### Runtime Environment - -**Critical**: All agent and tool functions are executed in a sandboxed, ephemeral environment with limited resources: - -- **Memory is temporary**: Anything stored in memory (e.g. as a variable in the agent/tool object) is lost after the function completes. Use the Store tool instead. Only use memory for temporary caching. -- **Limited resources**: Each execution has limited CPU time and memory -- **Limited runtime**: Agents execute in a Cloudflare Worker environment with the \`nodejs_compat\` flag. This means many but not all Node.js APIs are available. -- **Use the Run tool**: Split large operations into smaller batches with \`run.now(functionName, context)\` -- **Store intermediate state**: Use the Store tool to persist state between batches -- **Examples**: Syncing large datasets, processing many API calls, or performing batch operations - -## Agent Structure Pattern - -\`\`\`typescript -import { - type Activity, - Agent, - type Priority, - type Tools, -} from "@plotday/sdk"; -import { Plot } from "@plotday/sdk/tools/plot"; - -export default class MyAgent extends Agent { - private plot: Plot; - - constructor(protected tools: Tools) { - super(tools); - this.plot = tools.get(Plot); - // Store, Run, and Callback methods are available directly via this - } - - async activate(priority) { - // Called when agent is enabled for a priority - // Common actions: request auth, create setup activities - } - - async activity(activity, changes) { - // Called when an activity is created or updated in the priority - // where this agent is added (or its child priorities). - // - // IMPORTANT: check that changes is null if you only want to process new activities - // - // Common actions: process external events, update activities - } -} -\`\`\` - -## Tool System - -### Accessing Tools - -Tools are made available through the \`tools\` parameter in the constructor: - -\`\`\`typescript -constructor(protected tools: Tools) { - super(); - this.toolName = tools.get(ToolClass); -} -\`\`\` - -All \`tools.get()\` calls must occur in the constructor as they are used for dependency analysis. -Assign tool instances to class properties for use in other methods. - -### Built-in Tools (Always Available) - -The following tools are always available to agents. For complete API documentation including all methods, parameters, return types, and detailed examples, refer to the TypeScript definitions which are provided with full JSDoc when generating agents. - -**Available Built-in Tools:** -- **Plot** (\`@plotday/sdk/tools/plot\`): Core data layer - create/update activities, priorities, contacts -- **AI** (\`@plotday/sdk/tools/ai\`): LLM access for text generation, structured output, reasoning - - Use ModelPreferences to specify \`speed\` (fast/balanced/capable) and \`cost\` (low/medium/high) -- **Store** (\`@plotday/sdk/tools/store\`): Persistent key-value storage (also available via \`this.set()\`, \`this.get()\`) -- **Run** (\`@plotday/sdk/tools/run\`): Queue batched work (also available via \`this.run()\`) -- **Callback** (\`@plotday/sdk/tools/callback\`): Persistent function references (also available via \`this.callback()\`) -- **Auth** (\`@plotday/sdk/tools/auth\`): OAuth2 authentication flows -- **Webhook** (\`@plotday/sdk/tools/webhook\`): HTTP webhook management -- **AgentManager** (\`@plotday/sdk/tools/agent\`): Manage other agents - -See the SDK type definitions for full API documentation with usage examples. - -**Critical**: Never use instance variables for state. They are lost after function execution. Always use Store methods. - -### External Tools (Add to package.json) - -Add tool dependencies to \`package.json\`: - -\`\`\`json -{ - "dependencies": { - "@plotday/sdk": "workspace:^", - "@plotday/tool-google-calendar": "workspace:^" - } -} -\`\`\` - -#### Common External Tools - -- \`@plotday/tool-google-calendar\`: Google Calendar integration -- \`@plotday/tool-outlook-calendar\`: Outlook Calendar integration -- \`@plotday/tool-google-contacts\`: Google Contacts integration - -## Lifecycle Methods - -### activate(priority: Pick) - -Called when the agent is enabled for a priority. Common patterns: - -**Request Authentication:** - -\`\`\`typescript -async activate(_priority: Pick) { - const callback = await this.callback.create("onAuthComplete", { provider: "google" }); - const authLink = await this.externalTool.requestAuth(callback); - - await this.plot.createActivity({ - type: ActivityType.Task, - title: "Connect your account", - start: new Date(), - links: [authLink], - }); -} -\`\`\` - -**Store Parent Activity for Later:** - -\`\`\`typescript -const activity = await this.plot.createActivity({ - type: ActivityType.Task, - title: "Setup", - start: new Date(), -}); - -await this.set("setup_activity_id", activity.id); -\`\`\` - -### activity(activity: Activity) - -Called when an activity is routed to the agent. Common patterns: - -**Create Activities from External Events:** - -\`\`\`typescript -async activity(activity: Activity) { - await this.plot.createActivity(activity); -} -\`\`\` - -**Update Based on User Action:** - -\`\`\`typescript -async activity(activity: Activity) { - if (activity.completed) { - await this.handleCompletion(activity); - } -} -\`\`\` - -## Activity Links - -Activity links enable user interaction: - -\`\`\`typescript -import { type ActivityLink, ActivityLinkType } from "@plotday/sdk"; - -// URL link -const urlLink: ActivityLink = { - title: "Open website", - type: ActivityLinkType.url, - url: "https://example.com", -}; - -// Callback link (uses Callback tool) -const token = await this.callback.create("onLinkClicked", { data: "context" }); -const callbackLink: ActivityLink = { - title: "Click me", - type: ActivityLinkType.callback, - token: token, -}; - -// Add to activity -await this.plot.createActivity({ - type: ActivityType.Task, - title: "Task with links", - links: [urlLink, callbackLink], -}); -\`\`\` - -## Authentication Pattern - -Common pattern for OAuth authentication: - -\`\`\`typescript -async activate(_priority: Pick) { - // Create callback for auth completion - const callback = await this.callback.create("onAuthComplete", { - provider: "google", - }); - - // Request auth link from tool - const authLink = await this.googleTool.requestAuth(callback); - - // Create activity with auth link - const activity = await this.plot.createActivity({ - type: ActivityType.Task, - title: "Connect Google account", - start: new Date(), - links: [authLink], - }); - - // Store for later use - await this.store.set("auth_activity_id", activity.id); -} - -async onAuthComplete(authResult: { authToken: string }, context?: any) { - const provider = context?.provider; - - // Store auth token - await this.store.set(\`\${provider}_auth\`, authResult.authToken); - - // Continue setup flow - await this.setupSyncOptions(authResult.authToken); -} -\`\`\` - -## Sync Pattern - -Pattern for syncing external data with callbacks: - -\`\`\`typescript -async startSync(calendarId: string): Promise { - const authToken = await this.store.get("auth_token"); - - // Create callback for event handling - const callback = await this.callback.create("handleEvent", { - calendarId, - }); - - await this.calendarTool.startSync(authToken, calendarId, callback); -} - -async handleEvent(activity: Activity, context?: any): Promise { - // Process incoming event from external service - await this.plot.createActivity(activity); -} - -async stopSync(calendarId: string): Promise { - const authToken = await this.store.get("auth_token"); - await this.calendarTool.stopSync(authToken, calendarId); -} -\`\`\` - -## Calendar Selection Pattern - -Pattern for letting users select from multiple calendars/accounts: - -\`\`\`typescript -private async createCalendarSelectionActivity( - provider: string, - calendars: Calendar[], - authToken: string -): Promise { - const links: ActivityLink[] = []; - - for (const calendar of calendars) { - const token = await this.callback.create("onCalendarSelected", { - provider, - calendarId: calendar.id, - calendarName: calendar.name, - authToken, - }); - - links.push({ - title: \`📅 \${calendar.name}\${calendar.primary ? " (Primary)" : ""}\`, - type: ActivityLinkType.callback, - token: token, - }); - } - - await this.plot.createActivity({ - type: ActivityType.Task, - title: "Which calendars would you like to connect?", - start: new Date(), - links, - }); -} - -async onCalendarSelected(link: ActivityLink, context: any): Promise { - // Start sync for selected calendar - const callback = await this.callback.create("handleEvent", { - provider: context.provider, - calendarId: context.calendarId, - }); - - await this.tool.startSync(context.authToken, context.calendarId, callback); -} -\`\`\` - -## Batch Processing Pattern - -**Important**: Because agents run in an ephemeral environment with limited execution time (~10 seconds), you must break long operations into batches. Each batch runs independently in a new execution context. - -### Key Principles - -1. **Store state between batches**: Use the Store tool to persist progress -2. **Queue next batch**: Use the Run tool to schedule the next chunk -3. **Clean up when done**: Delete stored state after completion -4. **Handle failures**: Store enough state to resume if a batch fails - -### Example Implementation - -\`\`\`typescript -async startSync(resourceId: string): Promise { - // Initialize state in Store (persists between executions) - await this.set(\`sync_state_\${resourceId}\`, { - nextPageToken: null, - batchNumber: 1, - itemsProcessed: 0, - }); - - // Queue first batch using run method - const callback = await this.callback("syncBatch", { resourceId }); - await this.run(callback); -} - -async syncBatch(args: any, context: { resourceId: string }): Promise { - // Load state from Store (set by previous execution) - const state = await this.get(\`sync_state_\${context.resourceId}\`); - - // Process one batch (keep under time limit) - const result = await this.fetchBatch(state.nextPageToken); - - // Process results - for (const item of result.items) { - await this.plot.createActivity(item); - } - - if (result.nextPageToken) { - // Update state in Store for next batch - await this.set(\`sync_state_\${context.resourceId}\`, { - nextPageToken: result.nextPageToken, - batchNumber: state.batchNumber + 1, - itemsProcessed: state.itemsProcessed + result.items.length, - }); - - // Queue next batch (runs in new execution context) - const nextCallback = await this.callback("syncBatch", context); - await this.run(nextCallback); - } else { - // Cleanup when complete - await this.clear(\`sync_state_\${context.resourceId}\`); - - // Optionally notify user of completion - await this.plot.createActivity({ - type: ActivityType.Note, - note: \`Sync complete: \${state.itemsProcessed + result.items.length} items processed\`, - }); - } -} -\`\`\` - -## Error Handling - -Always handle errors gracefully and communicate them to users: - -\`\`\`typescript -try { - await this.externalOperation(); -} catch (error) { - console.error("Operation failed:", error); - - await this.plot.createActivity({ - type: ActivityType.Note, - note: \`Failed to complete operation: \${error.message}\`, - }); -} -\`\`\` - -## Common Pitfalls - -1. **Don't use instance variables for state** - Anything stored in memory is lost after function execution. Always use the Store tool for data that needs to persist. -2. **Don't forget timeout limits** - Each execution has ~10 seconds. Break long operations into batches with the Run tool. -3. **Don't assume execution order** - Batches may run on different workers. Store all necessary state between executions. -4. **Always use Callback tool for persistent references** - Direct function references don't survive worker restarts. -5. **Store auth tokens** - Don't re-request authentication unnecessarily. -6. **Clean up callbacks and stored state** - Delete callbacks and Store entries when no longer needed. -7. **Handle missing auth gracefully** - Check for stored auth before operations. -8. **Batch size matters** - Process enough items per batch to be efficient, but few enough to stay under time limits. -9. **Processing self-created activities** - Other users may change an Activity created by the agent, resulting in an \`activity\` call. - Be sure to check the \`changes\` parameter and/or \`activity.author\` to avoid re-processing. - -## Type Patterns - -### Entity Types - -Follow Plot's entity type patterns: - -\`\`\`typescript -export type Activity = { - id: string; // Required - type: ActivityType; // Required - title: string | null; // Nullable (not optional) - note: string | null; // Nullable (not optional) - start: Date | string | null; // Nullable (not optional) -}; - -export type NewActivity = { - type: Activity["type"]; // Only type is required -} & Partial>; -\`\`\` - -This pattern distinguishes between: - -- Omitted fields (\`undefined\` in Partial types) -- Explicitly set to null (clearing a value) -- Set to a value - -## Testing - -Before deploying, verify: +import agentsGuideTemplate from "./llm-docs/agents-guide-template.js"; -1. Linting passes: \`{{packageManager}} lint\` -2. All dependencies are in package.json -3. Authentication flow works end-to-end -4. Batch operations handle pagination correctly -5. Error cases are handled gracefully -`; +export const AGENTS_GUIDE = agentsGuideTemplate; diff --git a/sdk/src/plot.ts b/sdk/src/plot.ts index 2cdc917..6bb2888 100644 --- a/sdk/src/plot.ts +++ b/sdk/src/plot.ts @@ -221,14 +221,14 @@ export type Activity = { type: AuthorType; }; /** - * Start time of a scheduled activity. + * Start time of a scheduled activity. Notes are not typically scheduled unless they're about specific times. * For recurring events, this represents the start of the first occurrence. * Can be a Date object for timed events or a date string in "YYYY-MM-DD" format for all-day events. * Null for activities without scheduled start times. */ start: Date | string | null; /** - * End time of a scheduled activity. + * End time of a scheduled activity. Notes are not typically scheduled unless they're about specific times. * For recurring events, this represents the end of the first occurrence. * Can be a Date object for timed events or a date string in "YYYY-MM-DD" format for all-day events. * Null for tasks or activities without defined end times. From 8f506185825c0c54bcee4cf4abc59168d607e664 Mon Sep 17 00:00:00 2001 From: Kris Braun Date: Sun, 19 Oct 2025 18:14:10 -0400 Subject: [PATCH 2/2] Add AGENTS hint about not setting Activity.start --- sdk/cli/templates/AGENTS.template.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sdk/cli/templates/AGENTS.template.md b/sdk/cli/templates/AGENTS.template.md index 5c7eeac..d8fe628 100644 --- a/sdk/cli/templates/AGENTS.template.md +++ b/sdk/cli/templates/AGENTS.template.md @@ -382,6 +382,8 @@ try { 6. **Clean up callbacks and stored state** - Delete callbacks and Store entries when no longer needed. 7. **Handle missing auth gracefully** - Check for stored auth before operations. 8. **Batch size matters** - Process enough items per batch to be efficient, but few enough to stay under time limits. +9. **Processing self-created activities** - Other users may change an Activity created by the agent, resulting in an \`activity\` call. Be sure to check the \`changes === null\` and/or \`activity.author.id !== this.id\` to avoid re-processing. +10. Activity with type ActivityType.Note typically do not have a start or end set, unless they're a note about a specific day or time that shouldn't be shown until then. ## Type Patterns