From 2521bea23150d7cdc449914a28a3977068df51ae Mon Sep 17 00:00:00 2001 From: Kris Braun Date: Thu, 23 Oct 2025 18:26:51 -0400 Subject: [PATCH 1/5] Change tool use pattern to support permissions; rename several tools --- .changeset/fuzzy-beans-thank.md | 12 + agents/chat/src/index.ts | 28 +- agents/events/src/index.ts | 38 ++- sdk/README.md | 91 ++++--- sdk/cli/commands/deploy.ts | 62 +++-- sdk/cli/templates/AGENTS.template.md | 93 ++++--- sdk/cli/templates/README.template.md | 29 ++- sdk/package.json | 52 ++-- sdk/src/agent.ts | 244 +++++++++--------- sdk/src/plot.ts | 2 +- sdk/src/tools/agent.ts | 26 +- sdk/src/tools/ai.ts | 8 +- sdk/src/tools/{callback.ts => callbacks.ts} | 8 +- sdk/src/tools/index.ts | 8 +- sdk/src/tools/{auth.ts => integrations.ts} | 28 +- sdk/src/tools/{webhook.ts => network.ts} | 118 ++++++--- sdk/src/tools/plot.ts | 7 +- sdk/src/tools/store.ts | 2 +- sdk/src/tools/{run.ts => tasks.ts} | 8 +- tools/google-calendar/README.md | 10 +- tools/google-calendar/src/google-calendar.ts | 46 ++-- tools/google-contacts/README.md | 10 +- tools/google-contacts/src/google-contacts.ts | 62 ++--- tools/google-contacts/src/types.ts | 5 +- tools/outlook-calendar/README.md | 10 +- .../outlook-calendar/src/outlook-calendar.ts | 37 ++- 26 files changed, 560 insertions(+), 484 deletions(-) create mode 100644 .changeset/fuzzy-beans-thank.md rename sdk/src/tools/{callback.ts => callbacks.ts} (96%) rename sdk/src/tools/{auth.ts => integrations.ts} (79%) rename sdk/src/tools/{webhook.ts => network.ts} (59%) rename sdk/src/tools/{run.ts => tasks.ts} (95%) diff --git a/.changeset/fuzzy-beans-thank.md b/.changeset/fuzzy-beans-thank.md new file mode 100644 index 0000000..9ad7ae6 --- /dev/null +++ b/.changeset/fuzzy-beans-thank.md @@ -0,0 +1,12 @@ +--- +"@plotday/tool-outlook-calendar": minor +"@plotday/tool-google-calendar": minor +"@plotday/tool-google-contacts": minor +"@plotday/sdk": minor +--- + +Changed: BREAKING: Agents and Tools now use a static Init() function to gain access to tools, which are then available via this.tools. +Changed: BREAKING: Webhook functionality has been moved into the Network tool. +Changed: BREAKING: CallbackTool renamed Callbacks. +Changed: BREAKING: Auth renamed Integrations. +Changed: BREAKING: Run renamed Tasks. diff --git a/agents/chat/src/index.ts b/agents/chat/src/index.ts index 478d1a5..f05398b 100644 --- a/agents/chat/src/index.ts +++ b/agents/chat/src/index.ts @@ -6,19 +6,17 @@ import { Agent, AuthorType, Tag, - type Tools, + type ToolBuilder, } from "@plotday/sdk"; import { AI, type AIMessage } from "@plotday/sdk/tools/ai"; import { Plot } from "@plotday/sdk/tools/plot"; -export default class extends Agent { - private ai: AI; - private plot: Plot; - - constructor(id: string, protected tools: Tools) { - super(id, tools); - this.ai = tools.get(AI); - this.plot = tools.get(Plot); +export default class ChatAgent extends Agent { + static Init(tools: ToolBuilder) { + return { + ai: tools.init(AI), + plot: tools.init(Plot), + }; } async activity( @@ -31,7 +29,7 @@ export default class extends Agent { ) { if (changes) return; - const previousActivities = await this.plot.getThread(activity); + const previousActivities = await this.tools.plot.getThread(activity); if ( activity.note?.includes("@chat") || @@ -40,7 +38,7 @@ export default class extends Agent { ) ) { // Add Thinking tag to indicate processing has started - await this.plot.updateActivity({ + await this.tools.plot.updateActivity({ id: activity.id, tags: { [Tag.Agent]: true, @@ -96,14 +94,14 @@ You can also create tasks, but should only do so when the user explicitly asks y ), }); - const response = await this.ai.prompt({ + const response = await this.tools.ai.prompt({ model: { speed: "balanced", cost: "low" }, messages, outputSchema: schema, }); await Promise.all([ - this.plot.createActivity({ + this.tools.plot.createActivity({ title: response.output!.message.title, note: response.output!.message.note, parent: activity, @@ -111,7 +109,7 @@ You can also create tasks, but should only do so when the user explicitly asks y type: activity.type, }), ...(response.output!.action_items?.map((item: any) => - this.plot.createActivity({ + this.tools.plot.createActivity({ title: item.title, note: item.note, parent: activity, @@ -123,7 +121,7 @@ You can also create tasks, but should only do so when the user explicitly asks y ]); // Remove Thinking tag after response is created - await this.plot.updateActivity({ + await this.tools.plot.updateActivity({ id: activity.id, tags: { [Tag.Agent]: false, diff --git a/agents/events/src/index.ts b/agents/events/src/index.ts index 01736df..bee31ff 100644 --- a/agents/events/src/index.ts +++ b/agents/events/src/index.ts @@ -5,7 +5,7 @@ import { ActivityType, Agent, type Priority, - type Tools, + type ToolBuilder, } from "@plotday/sdk"; import type { Calendar, @@ -31,24 +31,21 @@ type CalendarSelectionContext = { authToken: string; }; -export default class extends Agent { - private googleCalendar: GoogleCalendar; - private outlookCalendar: OutlookCalendar; - private plot: Plot; - - constructor(id: string, protected tools: Tools) { - super(id, tools); - this.googleCalendar = tools.get(GoogleCalendar); - this.outlookCalendar = tools.get(OutlookCalendar); - this.plot = tools.get(Plot); +export default class EventsAgent extends Agent { + static Init(tools: ToolBuilder) { + return { + googleCalendar: tools.init(GoogleCalendar), + outlookCalendar: tools.init(OutlookCalendar), + plot: tools.init(Plot), + }; } private getProviderTool(provider: CalendarProvider): CalendarTool { switch (provider) { case "google": - return this.googleCalendar; + return this.tools.googleCalendar; case "outlook": - return this.outlookCalendar; + return this.tools.outlookCalendar; default: throw new Error(`Unknown calendar provider: ${provider}`); } @@ -98,15 +95,15 @@ export default class extends Agent { }); // Get auth links from both calendar tools - const googleAuthLink = await this.googleCalendar.requestAuth( + const googleAuthLink = await this.tools.googleCalendar.requestAuth( googleCallback ); - const outlookAuthLink = await this.outlookCalendar.requestAuth( + const outlookAuthLink = await this.tools.outlookCalendar.requestAuth( outlookCallback ); // Create activity with both auth links - const connectActivity = await this.plot.createActivity({ + const connectActivity = await this.tools.plot.createActivity({ type: ActivityType.Task, title: "Connect your calendar", start: new Date(), @@ -181,7 +178,7 @@ export default class extends Agent { } async handleEvent(activity: Activity, _context?: any): Promise { - await this.plot.createActivity(activity); + await this.tools.plot.createActivity(activity); } async onAuthComplete(authResult: CalendarAuth, context?: any): Promise { @@ -193,7 +190,6 @@ export default class extends Agent { // Store the auth token for later use await this.addStoredAuth(provider, authResult.authToken); - console.log(`${provider} Calendar authentication completed`); try { // Fetch available calendars for this provider @@ -201,7 +197,7 @@ export default class extends Agent { const calendars = await tool.getCalendars(authResult.authToken); if (calendars.length === 0) { - await this.plot.createActivity({ + await this.tools.plot.createActivity({ type: ActivityType.Note, note: `I couldn't find any calendars for that account.`, parent: await this.getParentActivity(), @@ -252,7 +248,7 @@ export default class extends Agent { } // Create the calendar selection activity - await this.plot.createActivity({ + await this.tools.plot.createActivity({ type: ActivityType.Task, title: `Which calendars would you like to connect?`, start: new Date(), @@ -291,7 +287,7 @@ export default class extends Agent { `Started syncing ${context.provider} calendar: ${context.calendarName}` ); - await this.plot.createActivity({ + await this.tools.plot.createActivity({ type: ActivityType.Note, note: `Reading your ${context.calendarName} calendar`, parent: await this.getParentActivity(), diff --git a/sdk/README.md b/sdk/README.md index c9c04c8..8ffa813 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -94,20 +94,19 @@ This will prompt you for: Edit `src/index.ts` to add your agent logic: ```typescript -import { type Activity, ActivityType, Agent, type Tools } from "@plotday/sdk"; +import { type Activity, ActivityType, Agent, type Priority, type ToolBuilder } from "@plotday/sdk"; import { Plot } from "@plotday/sdk/tools/plot"; -export default class extends Agent { - private plot: Plot; - - constructor(id: string, tools: Tools) { - super(id, tools); - this.plot = tools.get(Plot); +export default class MyAgent extends Agent { + static Init(tools: ToolBuilder) { + return { + plot: tools.init(Plot), + }; } - async activate(priority: { id: string }) { + async activate(priority: Pick) { // Called when the agent is activated for a priority - await this.plot.createActivity({ + await this.tools.plot.createActivity({ type: ActivityType.Note, title: "Welcome! Your agent is now active.", }); @@ -170,19 +169,20 @@ Activities are grouped within nested contexts called Priorities (e.g. Work, Proj Tools provide functionality to agents. They can be: -- **Built-in Tools** - Core Plot functionality (Plot, Store, Auth, etc.). +- **Built-in Tools** - Core Plot functionality (Plot, Store, Integrations, etc.). - **Custom Tools** - Extra packages that add capabilities using the built-in tools. They often implement integrations with external services (Google Calendar, Outlook, etc.). -Access tools via the `tools.get()` method in your agent constructor. Store, Run, and Callback methods are available directly on the Agent class: +Declare tools in the static `Init` method. Store, Tasks, and Callbacks methods are available directly on the Agent class: ```typescript -constructor(id: string, tools: Tools) { - super(id, tools); - this.plot = tools.get(Plot); - this.googleCalendar = tools.get(GoogleCalendar); - // Store, Run, and Callback methods are available directly: - // this.get(), this.set(), this.callback(), this.run(), etc. +static Init(tools: ToolBuilder) { + return { + plot: tools.init(Plot), + googleCalendar: tools.init(GoogleCalendar), + }; } +// Store, Tasks, and Callbacks methods are available directly: +// this.get(), this.set(), this.callback(), this.run(), etc. ``` ### Plot @@ -193,21 +193,21 @@ Core tool for creating and managing activities and priorities. import { Plot } from "@plotday/sdk/tools/plot"; // Create activities -await this.plot.createActivity({ +await this.tools.plot.createActivity({ type: ActivityType.Task, title: "My task", }); // Update activities -await this.plot.updateActivity(activity.id, { +await this.tools.plot.updateActivity(activity.id, { doneAt: new Date(), }); // Delete activities -await this.plot.deleteActivity(activity.id); +await this.tools.plot.deleteActivity(activity.id); // Create priorities -await this.plot.createPriority({ +await this.tools.plot.createPriority({ title: "Work", }); ``` @@ -228,16 +228,16 @@ await this.clear("sync_token"); await this.clearAll(); ``` -### Auth +### Integrations OAuth authentication for external services. ```typescript -import { Auth, AuthLevel, AuthProvider, type Authorization } from "@plotday/sdk/tools/auth"; +import { Integrations, AuthLevel, AuthProvider, type Authorization } from "@plotday/sdk/tools/integrations"; // Request authentication const authCallback = await this.callback("onAuthComplete", { provider: "google" }); -const authLink = await this.auth.request( +const authLink = await this.tools.integrations.request( { provider: AuthProvider.Google, level: AuthLevel.User, @@ -249,19 +249,19 @@ const authLink = await this.auth.request( // Handle auth completion async onAuthComplete(authorization: Authorization, context: any) { // Get access token - const authToken = await this.auth.get(authorization); + const authToken = await this.tools.integrations.get(authorization); console.log("Access token:", authToken?.token); } ``` **Type References:** -- [AuthProvider enum](https://github.com/plotday/plot/blob/main/sdk/src/tools/auth.ts#L78-L83) - Google, Microsoft -- [AuthLevel enum](https://github.com/plotday/plot/blob/main/sdk/src/tools/auth.ts#L90-L95) - Priority, User +- [AuthProvider enum](https://github.com/plotday/plot/blob/main/sdk/src/tools/integrations.ts#L82-L87) - Google, Microsoft +- [AuthLevel enum](https://github.com/plotday/plot/blob/main/sdk/src/tools/integrations.ts#L94-L99) - Priority, User -### Run +### Tasks -Queue background tasks and scheduled operations. Run methods are available directly on Agent and Tool classes. +Queue background tasks and scheduled operations. Tasks methods are available directly on Agent and Tool classes. ```typescript // Execute immediately (no import needed - available directly) @@ -273,15 +273,24 @@ const reminderCallback = await this.callback("sendReminder", { userId: "123" }); await this.run(reminderCallback, { runAt: new Date("2025-01-15T10:00:00Z") }); ``` -### Webhook +### Network -Create webhook endpoints for real-time notifications from external services. +Request HTTP access permissions and create webhook endpoints for real-time notifications from external services. ```typescript -import { Webhook, type WebhookRequest } from "@plotday/sdk/tools/webhook"; +import { Network, type WebhookRequest } from "@plotday/sdk/tools/network"; + +// Declare HTTP access in Init method +static Init(tools: ToolBuilder) { + return { + network: tools.init(Network, { + urls: ['https://api.example.com/*'] + }) + }; +} // Create webhook endpoint -const webhookUrl = await this.webhook.create( +const webhookUrl = await this.tools.network.createWebhook( "onCalendarUpdate", { calendarId: "primary" } ); @@ -293,12 +302,12 @@ async onCalendarUpdate(request: WebhookRequest, context: any) { } // Delete webhook endpoint -await this.webhook.delete(webhookUrl); +await this.tools.network.deleteWebhook(webhookUrl); ``` -### Callback +### Callbacks -Create persistent function references for webhooks and auth flows. Callback methods are available directly on Agent and Tool classes. +Create persistent function references for webhooks and auth flows. Callbacks methods are available directly on Agent and Tool classes. ```typescript // Create callback (no import needed - available directly) @@ -312,7 +321,7 @@ const result = await this.callCallback(callback, { }); // Delete callback -await this.deleteCallback(token); +await this.deleteCallback(callback); await this.deleteAllCallbacks(); // Delete all ``` @@ -325,7 +334,7 @@ import { AI } from "@plotday/sdk/tools/ai"; import { Type } from "typebox"; // Simple text generation with fast, low-cost model -const response = await this.ai.prompt({ +const response = await this.tools.ai.prompt({ model: { speed: "fast", cost: "low" }, prompt: "Explain quantum computing in simple terms", }); @@ -342,7 +351,7 @@ const schema = Type.Object({ summary: Type.String({ description: "Brief summary" }), }); -const response = await this.ai.prompt({ +const response = await this.tools.ai.prompt({ model: { speed: "balanced", cost: "medium" }, prompt: "Categorize this email: Meeting at 3pm tomorrow", outputSchema: schema, @@ -353,7 +362,7 @@ console.log(response.output.category); // "work" | "personal" | "urgent" console.log(response.output.priority); // number // Tool calling -const response = await this.ai.prompt({ +const response = await this.tools.ai.prompt({ model: { speed: "fast", cost: "medium" }, prompt: "What's 15% of $250?", tools: { @@ -385,7 +394,7 @@ const PersonSchema = Type.Object({ }); // Use in AI prompt -const response = await this.ai.prompt({ +const response = await this.tools.ai.prompt({ model: { speed: "balanced", cost: "medium" }, prompt: "Extract: John Doe, 30 years old, john@example.com", outputSchema: PersonSchema, diff --git a/sdk/cli/commands/deploy.ts b/sdk/cli/commands/deploy.ts index 9f3cfc1..ff7ed18 100644 --- a/sdk/cli/commands/deploy.ts +++ b/sdk/cli/commands/deploy.ts @@ -3,10 +3,10 @@ import * as fs from "fs"; import * as path from "path"; import prompts from "prompts"; -import * as out from "../utils/output"; -import { getGlobalTokenPath } from "../utils/token"; import { bundleAgent } from "../utils/bundle"; +import * as out from "../utils/output"; import { handleSSEStream } from "../utils/sse"; +import { getGlobalTokenPath } from "../utils/token"; interface DeployOptions { dir: string; @@ -33,12 +33,6 @@ interface PackageJson { env?: Record; } -interface AgentSource { - displayName: string; - dependencies: Record; - files: Record; -} - export async function deployCommand(options: DeployOptions) { const agentPath = path.resolve(process.cwd(), options.dir); @@ -59,10 +53,9 @@ export async function deployCommand(options: DeployOptions) { // No package.json - check for plot-agent.md as fallback const specPath = path.join(agentPath, "plot-agent.md"); if (fs.existsSync(specPath)) { - out.info( - "No package.json found, but plot-agent.md exists", - ["Generating agent from spec first..."] - ); + out.info("No package.json found, but plot-agent.md exists", [ + "Generating agent from spec first...", + ]); // Import and run generate command const { generateCommand } = await import("./generate"); @@ -280,6 +273,11 @@ export async function deployCommand(options: DeployOptions) { }, })) as any; + if (!result) { + out.error("Upload failed"); + process.exit(1); + } + // Handle dryRun response if (options.dryRun) { if (result.errors && result.errors.length > 0) { @@ -290,24 +288,36 @@ export async function deployCommand(options: DeployOptions) { process.exit(1); } else { out.success("Validation passed - agent is ready to deploy"); - out.info( - "Run without --dry-run to deploy", - [`plot agent deploy`] - ); + out.info("Run without --dry-run to deploy", [`plot agent deploy`]); } return; } - // Show dependencies from API response - const dependencies = result.dependencies; - if (dependencies && dependencies.length > 0) { - const deps = dependencies.map((depId: string) => - depId - .split("-") - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(" ") - ); - out.progress("Dependencies: " + deps.join(", ")); + // Show permissions from API response + const permissions = result.permissions; + if (permissions && Object.keys(permissions).length > 0) { + out.info("Permissions:", []); + for (const [toolName, toolPermissions] of Object.entries(permissions)) { + console.log(` ${toolName}:`); + + // Display each permission property + if (toolPermissions && typeof toolPermissions === "object") { + for (const [key, value] of Object.entries(toolPermissions)) { + if (Array.isArray(value)) { + if (value.length > 0) { + console.log(` ${key}:`); + for (const item of value) { + console.log(` - ${item}`); + } + } else { + console.log(` ${key}: []`); + } + } else { + console.log(` ${key}: ${JSON.stringify(value)}`); + } + } + } + } } // Show success with relevant info diff --git a/sdk/cli/templates/AGENTS.template.md b/sdk/cli/templates/AGENTS.template.md index 23f37fb..35d79ca 100644 --- a/sdk/cli/templates/AGENTS.template.md +++ b/sdk/cli/templates/AGENTS.template.md @@ -20,16 +20,14 @@ Plot agents are TypeScript classes that extend the `Agent` base class. Agents in ## Agent Structure Pattern ```typescript -import { type Activity, Agent, type Priority, type Tools } from "@plotday/sdk"; +import { type Activity, Agent, type Priority, type ToolBuilder } from "@plotday/sdk"; import { Plot } from "@plotday/sdk/tools/plot"; -export default class MyAgent extends Agent { - private plot: Plot; - - constructor(id: string, protected tools: Tools) { - super(id, tools); - this.plot = tools.get(Plot); - // Store, Run, and Callback methods are available directly via this +export default class MyAgent extends Agent { + static Init(tools: ToolBuilder) { + return { + plot: tools.init(Plot), + }; } async activate(priority: Pick) { @@ -48,18 +46,19 @@ export default class MyAgent extends Agent { ### Accessing Tools -All tools are accessed through the `tools` parameter in the constructor: +All tools are declared in the static `Init` method: ```typescript -constructor(id: string, protected tools: Tools) { - super(id, tools); - this.toolName = tools.get(ToolClass); +static Init(tools: ToolBuilder) { + return { + toolName: tools.init(ToolClass), + }; } ``` -All `tools.get()` calls must occur in the constructor as they are used for dependency analysis. +All `tools.init()` calls must occur in the `Init` method as they are used for dependency analysis. -IMPORTANT: http access is restricted to URLs requested via `tools.enableInternet([url1, url2, ...])` in the constructor. Wildcards are supported. Use `tools.enableInternet(['*'])` if full access is needed. +IMPORTANT: HTTP access is restricted to URLs requested via `tools.init(Network, { urls: [url1, url2, ...] })` in the `Init` method. Wildcards are supported. Use `tools.init(Network, { urls: ['*'] })` if full access is needed. ### Built-in Tools (Always Available) @@ -71,10 +70,10 @@ For complete API documentation of built-in tools including all methods, types, a - `@plotday/sdk/tools/ai` - LLM integration (text generation, structured output, reasoning) - Use ModelPreferences to specify `speed` (fast/balanced/capable) and `cost` (low/medium/high) - `@plotday/sdk/tools/store` - Persistent key-value storage (also via `this.set()`, `this.get()`) -- `@plotday/sdk/tools/run` - Queue batched work (also via `this.run()`) -- `@plotday/sdk/tools/callback` - Persistent function references (also via `this.callback()`) -- `@plotday/sdk/tools/auth` - OAuth2 authentication flows -- `@plotday/sdk/tools/webhook` - HTTP webhook management +- `@plotday/sdk/tools/tasks` - Queue batched work (also via `this.run()`) +- `@plotday/sdk/tools/callbacks` - Persistent function references (also via `this.callback()`) +- `@plotday/sdk/tools/integrations` - OAuth2 authentication flows +- `@plotday/sdk/tools/network` - HTTP access permissions and webhook management - `@plotday/sdk/tools/agent` - Manage other agents **Critical**: Never use instance variables for state. They are lost after function execution. Always use Store methods. @@ -108,10 +107,10 @@ Called when the agent is enabled for a priority. Common patterns: ```typescript async activate(_priority: Pick) { - const callback = await this.callback.create("onAuthComplete", { provider: "google" }); - const authLink = await this.externalTool.requestAuth(callback); + const callback = await this.callback("onAuthComplete", { provider: "google" }); + const authLink = await this.tools.externalTool.requestAuth(callback); - await this.plot.createActivity({ + await this.tools.plot.createActivity({ type: ActivityType.Task, title: "Connect your account", links: [authLink], @@ -122,7 +121,7 @@ async activate(_priority: Pick) { **Store Parent Activity for Later:** ```typescript -const activity = await this.plot.createActivity({ +const activity = await this.tools.plot.createActivity({ type: ActivityType.Task, title: "Setup", }); @@ -138,7 +137,7 @@ Called when an activity is routed to the agent. Common patterns: ```typescript async activity(activity: Activity) { - await this.plot.createActivity(activity); + await this.tools.plot.createActivity(activity); } ``` @@ -166,8 +165,8 @@ const urlLink: ActivityLink = { url: "https://example.com", }; -// Callback link (uses Callback tool) -const token = await this.callback.create("onLinkClicked", { data: "context" }); +// Callback link (uses Callbacks tool) +const token = await this.callback("onLinkClicked", { data: "context" }); const callbackLink: ActivityLink = { title: "Click me", type: ActivityLinkType.callback, @@ -175,7 +174,7 @@ const callbackLink: ActivityLink = { }; // Add to activity -await this.plot.createActivity({ +await this.tools.plot.createActivity({ type: ActivityType.Task, title: "Task with links", links: [urlLink, callbackLink], @@ -189,29 +188,29 @@ Common pattern for OAuth authentication: ```typescript async activate(_priority: Pick) { // Create callback for auth completion - const callback = await this.callback.create("onAuthComplete", { + const callback = await this.callback("onAuthComplete", { provider: "google", }); // Request auth link from tool - const authLink = await this.googleTool.requestAuth(callback); + const authLink = await this.tools.googleTool.requestAuth(callback); // Create activity with auth link - const activity = await this.plot.createActivity({ + const activity = await this.tools.plot.createActivity({ type: ActivityType.Task, title: "Connect Google account", links: [authLink], }); // Store for later use - await this.store.set("auth_activity_id", activity.id); + await this.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); + await this.set(`${provider}_auth`, authResult.authToken); // Continue setup flow await this.setupSyncOptions(authResult.authToken); @@ -224,24 +223,24 @@ Pattern for syncing external data with callbacks: ```typescript async startSync(calendarId: string): Promise { - const authToken = await this.store.get("auth_token"); + const authToken = await this.get("auth_token"); // Create callback for event handling - const callback = await this.callback.create("handleEvent", { + const callback = await this.callback("handleEvent", { calendarId, }); - await this.calendarTool.startSync(authToken, calendarId, callback); + await this.tools.calendarTool.startSync(authToken, calendarId, callback); } async handleEvent(activity: Activity, context?: any): Promise { // Process incoming event from external service - await this.plot.createActivity(activity); + await this.tools.plot.createActivity(activity); } async stopSync(calendarId: string): Promise { - const authToken = await this.store.get("auth_token"); - await this.calendarTool.stopSync(authToken, calendarId); + const authToken = await this.get("auth_token"); + await this.tools.calendarTool.stopSync(authToken, calendarId); } ``` @@ -258,7 +257,7 @@ private async createCalendarSelectionActivity( const links: ActivityLink[] = []; for (const calendar of calendars) { - const token = await this.callback.create("onCalendarSelected", { + const token = await this.callback("onCalendarSelected", { provider, calendarId: calendar.id, calendarName: calendar.name, @@ -272,7 +271,7 @@ private async createCalendarSelectionActivity( }); } - await this.plot.createActivity({ + await this.tools.plot.createActivity({ type: ActivityType.Note, title: "Which calendars would you like to connect?", links, @@ -281,12 +280,12 @@ private async createCalendarSelectionActivity( async onCalendarSelected(link: ActivityLink, context: any): Promise { // Start sync for selected calendar - const callback = await this.callback.create("handleEvent", { + const callback = await this.callback("handleEvent", { provider: context.provider, calendarId: context.calendarId, }); - await this.tool.startSync(context.authToken, context.calendarId, callback); + await this.tools.tool.startSync(context.authToken, context.calendarId, callback); } ``` @@ -326,7 +325,7 @@ async syncBatch(args: any, context: { resourceId: string }): Promise { // Process results for (const item of result.items) { - await this.plot.createActivity(item); + await this.tools.plot.createActivity(item); } if (result.nextPageToken) { @@ -345,7 +344,7 @@ async syncBatch(args: any, context: { resourceId: string }): Promise { await this.clear(`sync_state_${context.resourceId}`); // Optionally notify user of completion - await this.plot.createActivity({ + await this.tools.plot.createActivity({ type: ActivityType.Note, note: `Sync complete: ${state.itemsProcessed + result.items.length} items processed`, }); @@ -363,7 +362,7 @@ try { } catch (error) { console.error("Operation failed:", error); - await this.plot.createActivity({ + await this.tools.plot.createActivity({ type: ActivityType.Note, note: `Failed to complete operation: ${error.message}`, }); @@ -375,9 +374,9 @@ try { - **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. - **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. - Most activity should be `type = ActivityType.Note` with a `title` and `note`, and no `start` or `end`. This represents a typical message. `start` and `end` should only be used for a note if it should be displayed for a specific date or time, such as a birthday. -- Don't add the Tools instance as an instance variable. Any tools needed must bet rieved via \`this.tools.get(ToolClass)\` in the constructor and assigned to instance variables. -- **Don't forget runtime limits** - Each execution has ~10 seconds. Break long operations into batches with the Run tool. Process enough items per batch to be efficient, but few enough to stay under time limits. -- **Always use Callback tool for persistent references** - Direct function references don't survive worker restarts. +- Tools are declared in the static `Init` method and accessed via `this.tools.toolName` in agent methods. +- **Don't forget runtime limits** - Each execution has ~10 seconds. Break long operations into batches with the Tasks tool. Process enough items per batch to be efficient, but few enough to stay under time limits. +- **Always use Callbacks tool for persistent references** - Direct function references don't survive worker restarts. - **Store auth tokens** - Don't re-request authentication unnecessarily. - **Clean up callbacks and stored state** - Delete callbacks and Store entries when no longer needed. - **Handle missing auth gracefully** - Check for stored auth before operations. diff --git a/sdk/cli/templates/README.template.md b/sdk/cli/templates/README.template.md index 5b23417..17652c0 100644 --- a/sdk/cli/templates/README.template.md +++ b/sdk/cli/templates/README.template.md @@ -48,23 +48,25 @@ Called when an activity is routed to this agent. Use this to: ### Using Tools -Agents access functionality through tools. Get tools in the constructor: +Agents access functionality through tools. Declare tools in the static `Init` method: ```typescript -constructor(protected tools: Tools) { - super(id, tools); - this.plot = tools.get(Plot); - // Store, Run, and Callback methods are available directly via this +static Init(tools: ToolBuilder) { + return { + plot: tools.init(Plot), + }; } +// Store, Tasks, and Callbacks methods are available directly via this ``` #### Built-in Tools - **Plot**: Create, update, and delete activities - **Store**: Persist data across agent invocations -- **Auth**: Request OAuth authentication from external services -- **Run**: Queue background tasks and batch operations -- **Callback**: Create persistent function references for webhooks +- **Integrations**: Request OAuth authentication from external services +- **Tasks**: Queue background tasks and batch operations +- **Callbacks**: Create persistent function references for webhooks +- **Network**: HTTP access permissions and webhook management #### External Tools @@ -82,11 +84,12 @@ Add external tool dependencies to `package.json`: Then use them in your agent: ```typescript -import { GoogleCalendar } from "@plotday/sdk/tools/google-calendar"; +import GoogleCalendarTool from "@plotday/tool-google-calendar"; -constructor(id: string, tools: Tools) { - super(); - this.googleCalendar = tools.get(GoogleCalendar); +static Init(tools: ToolBuilder) { + return { + googleCalendar: tools.init(GoogleCalendarTool), + }; } ``` @@ -118,7 +121,7 @@ const token = await this.get("sync_token"); - **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 CPU time**: Each execution has limited CPU time (typically 10 seconds) and memory (128MB) -- **Use the Run tool**: Queue separate chunks of work with `run.now(functionName, context)` +- **Use the Tasks tool**: Queue separate chunks of work with `this.run(callback)` - **Break long operations**: Split large operations into smaller batches that can be processed independently - **Store intermediate state**: Use the Store tool to persist state between batches - **Examples**: Syncing large datasets, processing many API calls, or performing batch operations diff --git a/sdk/package.json b/sdk/package.json index 639e19b..f1a9b12 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -34,30 +34,30 @@ "types": "./dist/tools/ai.d.ts", "default": "./dist/tools/ai.js" }, - "./tools/auth": { - "types": "./dist/tools/auth.d.ts", - "default": "./dist/tools/auth.js" + "./tools/integrations": { + "types": "./dist/tools/integrations.d.ts", + "default": "./dist/tools/integrations.js" }, - "./tools/callback": { - "types": "./dist/tools/callback.d.ts", - "default": "./dist/tools/callback.js" + "./tools/callbacks": { + "types": "./dist/tools/callbacks.d.ts", + "default": "./dist/tools/callbacks.js" + }, + "./tools/network": { + "types": "./dist/tools/network.d.ts", + "default": "./dist/tools/network.js" }, "./tools/plot": { "types": "./dist/tools/plot.d.ts", "default": "./dist/tools/plot.js" }, - "./tools/run": { - "types": "./dist/tools/run.d.ts", - "default": "./dist/tools/run.js" + "./tools/tasks": { + "types": "./dist/tools/tasks.d.ts", + "default": "./dist/tools/tasks.js" }, "./tools/store": { "types": "./dist/tools/store.d.ts", "default": "./dist/tools/store.js" }, - "./tools/webhook": { - "types": "./dist/tools/webhook.d.ts", - "default": "./dist/tools/webhook.js" - }, "./common/calendar": { "types": "./dist/common/calendar.d.ts", "default": "./dist/common/calendar.js" @@ -98,30 +98,30 @@ "types": "./dist/llm-docs/tools/ai.d.ts", "default": "./dist/llm-docs/tools/ai.js" }, - "./llm-docs/tools/auth": { - "types": "./dist/llm-docs/tools/auth.d.ts", - "default": "./dist/llm-docs/tools/auth.js" + "./llm-docs/tools/integrations": { + "types": "./dist/llm-docs/tools/integrations.d.ts", + "default": "./dist/llm-docs/tools/integrations.js" }, - "./llm-docs/tools/callback": { - "types": "./dist/llm-docs/tools/callback.d.ts", - "default": "./dist/llm-docs/tools/callback.js" + "./llm-docs/tools/callbacks": { + "types": "./dist/llm-docs/tools/callbacks.d.ts", + "default": "./dist/llm-docs/tools/callbacks.js" + }, + "./llm-docs/tools/network": { + "types": "./dist/llm-docs/tools/network.d.ts", + "default": "./dist/llm-docs/tools/network.js" }, "./llm-docs/tools/plot": { "types": "./dist/llm-docs/tools/plot.d.ts", "default": "./dist/llm-docs/tools/plot.js" }, - "./llm-docs/tools/run": { - "types": "./dist/llm-docs/tools/run.d.ts", - "default": "./dist/llm-docs/tools/run.js" + "./llm-docs/tools/tasks": { + "types": "./dist/llm-docs/tools/tasks.d.ts", + "default": "./dist/llm-docs/tools/tasks.js" }, "./llm-docs/tools/store": { "types": "./dist/llm-docs/tools/store.d.ts", "default": "./dist/llm-docs/tools/store.js" }, - "./llm-docs/tools/webhook": { - "types": "./dist/llm-docs/tools/webhook.d.ts", - "default": "./dist/llm-docs/tools/webhook.js" - }, "./llm-docs/common/calendar": { "types": "./dist/llm-docs/common/calendar.d.ts", "default": "./dist/llm-docs/common/calendar.js" diff --git a/sdk/src/agent.ts b/sdk/src/agent.ts index 430f657..eaa9f5b 100644 --- a/sdk/src/agent.ts +++ b/sdk/src/agent.ts @@ -3,10 +3,50 @@ import type { Callback, CallbackContext, CallbackMethods, - CallbackTool, -} from "./tools/callback"; -import type { Run } from "./tools/run"; + Callbacks, +} from "./tools/callbacks"; import type { Store } from "./tools/store"; +import type { Tasks } from "./tools/tasks"; + +// Type utilities for extracting types from Init method +type PromiseValues = { + [K in keyof T]: T[K] extends Promise ? U : T[K]; +}; + +// Break down InferTools into intermediate steps to avoid deep recursion +type ExtractInitReturn = T extends { + Init: (...args: any[]) => infer R; +} + ? R + : never; + +type ResolveInitTools = PromiseValues>; + +type BuiltInTools = { + callbacks: Callbacks infer I ? I : any>; + store: Store; + tasks: Tasks; +} & {}; // Counter reset with intersection + +// Note: Due to TypeScript limitations with self-referential generic types and static methods, +// this type may not properly infer tools in all cases. Use explicit type casts if needed. +type InferTools = T extends { + Init: (...args: any[]) => any; + new (...args: any[]): any; +} + ? ResolveInitTools & BuiltInTools + : never; + +type InferOptions = T extends { + Init: (tools: any, options?: infer O) => any; +} + ? O + : undefined; + +type HasInit = { + Init(tools: ToolBuilder, ...args: any[]): any; + new (id: string, tools: any, ...args: any[]): any; +}; /** * Base class for all agents. @@ -19,19 +59,18 @@ import type { Store } from "./tools/store"; * @example * ```typescript * class FlatteringAgent extends Agent { - * private plot: Plot; - * - * constructor(id: string, tools: Tools) { - * super(id, tools); - * this.plot = tools.get(Plot); - * } + * static Init(tools: ToolBuilder, options?: { greeting: string }) { + * return { + * plot: tools.init(Plot, PLOT_OPTIONS), + * }; + * } * * async activate(priority: Pick) { * // Initialize agent for the given priority - * await this.plot.createActivity({ - * type: ActivityType.Note, - * note: "Hello, good looking!", - * }); + * await this.tools.plot.createActivity({ + * type: ActivityType.Note, + * note: this.options.greeting || "Hello, good looking!", + * }); * } * * async activity(activity: Activity) { @@ -40,23 +79,12 @@ import type { Store } from "./tools/store"; * } * ``` */ -export abstract class Agent { - protected id: string; - private _callbackTool: CallbackTool; - private _store: Store; - private _runTool: Run; - - constructor(id: string, tools: Tools) { - this.id = id; - // Get specific tools in constructor for dependency detection - // Use dynamic imports to avoid circular dependencies with tool files - const { CallbackTool } = require("./tools/callback"); - const { Store } = require("./tools/store"); - const { Run } = require("./tools/run"); - this._callbackTool = tools.get(CallbackTool); - this._store = tools.get(Store); - this._runTool = tools.get(Run); - } +export abstract class Agent> { + constructor( + protected id: string, + protected tools: InferTools, + protected options: InferOptions + ) {} /** * Creates a persistent callback to a method on this agent. @@ -65,11 +93,13 @@ export abstract class Agent { * @param context - Optional context data to pass to the callback * @returns Promise resolving to a callback token */ - protected async callback>( + protected async callback< + K extends CallbackMethods> & string + >( functionName: K, - context?: CallbackContext + context?: CallbackContext, K> ): Promise { - return this._callbackTool.create(functionName, context); + return this.tools.callbacks.create(functionName, context); } /** @@ -79,7 +109,7 @@ export abstract class Agent { * @returns Promise that resolves when the callback is deleted */ protected async deleteCallback(token: Callback): Promise { - return this._callbackTool.delete(token); + return this.tools.callbacks.delete(token); } /** @@ -88,7 +118,7 @@ export abstract class Agent { * @returns Promise that resolves when all callbacks are deleted */ protected async deleteAllCallbacks(): Promise { - return this._callbackTool.deleteAll(); + return this.tools.callbacks.deleteAll(); } /** @@ -99,7 +129,7 @@ export abstract class Agent { * @returns Promise resolving to the callback result */ protected async callCallback(token: Callback, args?: any): Promise { - return this._callbackTool.callCallback(token, args); + return this.tools.callbacks.callCallback(token, args); } /** @@ -110,7 +140,7 @@ export abstract class Agent { * @returns Promise resolving to the stored value or null */ protected async get(key: string): Promise { - return this._store.get(key); + return this.tools.store.get(key); } /** @@ -122,7 +152,7 @@ export abstract class Agent { * @returns Promise that resolves when the value is stored */ protected async set(key: string, value: T): Promise { - return this._store.set(key, value); + return this.tools.store.set(key, value); } /** @@ -132,7 +162,7 @@ export abstract class Agent { * @returns Promise that resolves when the key is removed */ protected async clear(key: string): Promise { - return this._store.clear(key); + return this.tools.store.clear(key); } /** @@ -141,7 +171,7 @@ export abstract class Agent { * @returns Promise that resolves when all keys are removed */ protected async clearAll(): Promise { - return this._store.clearAll(); + return this.tools.store.clearAll(); } /** @@ -156,7 +186,7 @@ export abstract class Agent { callback: Callback, options?: { runAt?: Date } ): Promise { - return this._runTool.run(callback, options); + return this.tools.tasks.run(callback, options); } /** @@ -166,7 +196,7 @@ export abstract class Agent { * @returns Promise that resolves when the cancellation is processed */ protected async cancel(token: string): Promise { - return this._runTool.cancel(token); + return this.tools.tasks.cancel(token); } /** @@ -175,7 +205,7 @@ export abstract class Agent { * @returns Promise that resolves when all cancellations are processed */ protected async cancelAll(): Promise { - return this._runTool.cancelAll(); + return this.tools.tasks.cancelAll(); } /** @@ -222,47 +252,39 @@ export abstract class Agent { export abstract class ITool {} export type ToolConstructor = - | (abstract new (id: string, tools: Tools) => T) - | (new (id: string, tools: Tools) => T); + | (abstract new (id: string, tools: any, options?: any) => T) + | (new (id: string, tools: any, options?: any) => T); /** * Base class for regular tools. * - * Regular tools run in isolation and can only access other tools declared + * Regular tools.tasks in isolation and can only access other tools declared * in their tool.json dependencies. They are ideal for external API integrations * and reusable functionality that doesn't require Plot's internal infrastructure. * * @example * ```typescript * class GoogleCalendarTool extends Tool { - * constructor(id: string, tools: Tools) { - * super(id, tools); - * this.auth = tools.get(Auth); + * static Init(tools: ToolBuilder, options?: { clientId: string }) { + * return { + * auth: tools.init(Integrations, AUTH_OPTIONS), + * network: tools.init(Network, NETWORK_OPTIONS), + * }; * } * * async getCalendars() { + * const token = await this.tools.auth.get(...); * // Implementation * } * } * ``` */ -export abstract class Tool implements ITool { - protected id: string; - private _callbackTool: CallbackTool; - private _store: Store; - private _runTool: Run; - - constructor(id: string, tools: Tools) { - this.id = id; - // Get specific tools in constructor for dependency detection - // Use dynamic imports to avoid circular dependencies with tool files - const { CallbackTool } = require("./tools/callback"); - const { Store } = require("./tools/store"); - const { Run } = require("./tools/run"); - this._callbackTool = tools.get(CallbackTool); - this._store = tools.get(Store); - this._runTool = tools.get(Run); - } +export abstract class Tool> implements ITool { + constructor( + protected id: string, + protected tools: InferTools, + protected options: InferOptions + ) {} /** * Creates a persistent callback to a method on this tool. @@ -271,11 +293,13 @@ export abstract class Tool implements ITool { * @param context - Optional context data to pass to the callback * @returns Promise resolving to a callback token */ - protected async callback>( + protected async callback< + K extends CallbackMethods> & string + >( functionName: K, - context?: CallbackContext + context: CallbackContext, K> ): Promise { - return this._callbackTool.create(functionName, context); + return this.tools.callbacks.create(functionName, context); } /** @@ -285,7 +309,7 @@ export abstract class Tool implements ITool { * @returns Promise that resolves when the callback is deleted */ protected async deleteCallback(token: Callback): Promise { - return this._callbackTool.delete(token); + return this.tools.callbacks.delete(token); } /** @@ -294,7 +318,7 @@ export abstract class Tool implements ITool { * @returns Promise that resolves when all callbacks are deleted */ protected async deleteAllCallbacks(): Promise { - return this._callbackTool.deleteAll(); + return this.tools.callbacks.deleteAll(); } /** @@ -305,7 +329,7 @@ export abstract class Tool implements ITool { * @returns Promise resolving to the callback result */ protected async callCallback(token: Callback, args?: any): Promise { - return this._callbackTool.callCallback(token, args); + return this.tools.callbacks.callCallback(token, args); } /** @@ -316,7 +340,7 @@ export abstract class Tool implements ITool { * @returns Promise resolving to the stored value or null */ protected async get(key: string): Promise { - return this._store.get(key); + return this.tools.store.get(key); } /** @@ -328,7 +352,7 @@ export abstract class Tool implements ITool { * @returns Promise that resolves when the value is stored */ protected async set(key: string, value: T): Promise { - return this._store.set(key, value); + return this.tools.store.set(key, value); } /** @@ -338,7 +362,7 @@ export abstract class Tool implements ITool { * @returns Promise that resolves when the key is removed */ protected async clear(key: string): Promise { - return this._store.clear(key); + return this.tools.store.clear(key); } /** @@ -347,7 +371,7 @@ export abstract class Tool implements ITool { * @returns Promise that resolves when all keys are removed */ protected async clearAll(): Promise { - return this._store.clearAll(); + return this.tools.store.clearAll(); } /** @@ -362,7 +386,7 @@ export abstract class Tool implements ITool { callback: Callback, options?: { runAt?: Date } ): Promise { - return this._runTool.run(callback, options); + return this.tools.tasks.run(callback, options); } /** @@ -372,7 +396,7 @@ export abstract class Tool implements ITool { * @returns Promise that resolves when the cancellation is processed */ protected async cancel(token: string): Promise { - return this._runTool.cancel(token); + return this.tools.tasks.cancel(token); } /** @@ -381,7 +405,7 @@ export abstract class Tool implements ITool { * @returns Promise that resolves when all cancellations are processed */ protected async cancelAll(): Promise { - return this._runTool.cancelAll(); + return this.tools.tasks.cancelAll(); } } @@ -391,54 +415,26 @@ export abstract class Tool implements ITool { * This interface provides type-safe access to tools that have been declared * as dependencies in the agent.json or tool.json configuration files. */ -export interface Tools { +export interface ToolBuilder { /** - * Retrieves a tool instance by its class reference. + * Initializes a tool instance by its class reference, returning a promise. * * @template T - The expected type of the tool - * @param ToolClass - The tool class reference - * @returns The tool instance + * @template O - The options type expected by the tool's Init method + * @param ToolClass - The tool class reference with Init method + * @param options - Optional options to pass to the tool's Init method + * @returns Promise resolving to the tool instance * @throws When the tool is not found or not properly configured */ - get(ToolClass: ToolConstructor): T; - - /** - * Enables HTTP access to the specified URLs for this agent or tool. - * - * **IMPORTANT**: This method must be called in the Agent or Tool constructor - * to request HTTP access permissions. Without calling this method, all outbound - * HTTP requests (fetch, etc.) will be blocked. - * - * @param urls - Array of URL patterns to allow. Supports wildcards: - * - `*` - Allow access to all URLs - * - `https://*.example.com` - Allow access to all subdomains - * - `https://api.example.com/*` - Allow access to all paths on the domain - * - `https://api.example.com/v1/*` - Allow access to specific path prefix - * - * @example - * ```typescript - * class MyAgent extends Agent { - * constructor(id: string, tools: Tools) { - * super(id, tools); - * // Request HTTP access to specific APIs - * tools.enableInternet([ - * 'https://api.github.com/*', - * 'https://api.openai.com/*' - * ]); - * } - * } - * ``` - * - * @example - * ```typescript - * class MyTool extends Tool { - * constructor(id: string, tools: Tools) { - * super(id, tools); - * // Request unrestricted HTTP access - * tools.enableInternet(['*']); - * } - * } - * ``` - */ - enableInternet(urls: string[]): void; + init< + T extends ITool, + O = T extends { Init: (tools: any, options?: infer Opt) => any } + ? Opt + : never + >( + ToolClass: ToolConstructor & { + Init: (tools: ToolBuilder, options?: O) => any; + }, + options?: O + ): Promise; } diff --git a/sdk/src/plot.ts b/sdk/src/plot.ts index 721b670..85c9223 100644 --- a/sdk/src/plot.ts +++ b/sdk/src/plot.ts @@ -88,7 +88,7 @@ export enum ActivityLinkType { * url: "https://calendar.google.com/event/123", * }; * - * // Auth link - initiates OAuth flow + * // Integrations link - initiates OAuth flow * const authLink: ActivityLink = { * type: ActivityLinkType.auth, * title: "Continue with Google", diff --git a/sdk/src/tools/agent.ts b/sdk/src/tools/agent.ts index e936633..78d8d76 100644 --- a/sdk/src/tools/agent.ts +++ b/sdk/src/tools/agent.ts @@ -1,4 +1,4 @@ -import { type Callback, ITool } from ".."; +import { type Callback, ITool, type ToolBuilder } from ".."; /** * Agent source code structure containing dependencies and source files. @@ -28,6 +28,20 @@ export type Log = { message: string; }; +/** + * Agent permissions returned after deployment. + * Maps tool names to their respective permission configurations. + * + * @example + * ```typescript + * { + * "Network": { urls: ["https://api.example.com"] }, + * "Integrations": { provider: "google" } + * } + * ``` + */ +export type AgentPermissions = Record; + /** * Built-in tool for managing agents and deployments. * @@ -37,9 +51,9 @@ export type Log = { * @example * ```typescript * class AgentBuilderAgent extends Agent { - * private agent: AgentManager; + * private agent: Agents; * - * constructor(id: string, tools: Tools) { + * constructor(id: string, tools: ToolBuilder) { * super(); * this.agent = tools.get(AgentTool); * } @@ -51,7 +65,10 @@ export type Log = { * } * ``` */ -export abstract class AgentManager extends ITool { +export abstract class Agents extends ITool { + static Init(_tools: ToolBuilder, _options?: any): Record { + return {}; + } /** * Creates a new agent ID and grants access to people in the current priority. * @@ -164,6 +181,7 @@ export abstract class AgentManager extends ITool { ) ): Promise<{ version: string; + permissions: AgentPermissions; errors?: string[]; }>; diff --git a/sdk/src/tools/ai.ts b/sdk/src/tools/ai.ts index 8ff5b37..b1cfcb4 100644 --- a/sdk/src/tools/ai.ts +++ b/sdk/src/tools/ai.ts @@ -1,6 +1,6 @@ import type { Static, TSchema } from "typebox"; -import { ITool } from ".."; +import { ITool, type ToolBuilder } from ".."; /** * Built-in tool for prompting Large Language Models (LLMs). @@ -24,7 +24,7 @@ import { ITool } from ".."; * class SmartEmailTool extends Tool { * private ai: AI; * - * constructor(id: string, tools: Tools) { + * constructor(id: string, tools: ToolBuilder) { * super(); * this.ai = tools.get(AI); * } @@ -65,6 +65,10 @@ import { ITool } from ".."; * ``` */ export abstract class AI extends ITool { + static Init(_tools: ToolBuilder, _options?: any): Record { + return {}; + } + /** * Sends a request to an AI model and returns the response using the Vercel AI SDK. * diff --git a/sdk/src/tools/callback.ts b/sdk/src/tools/callbacks.ts similarity index 96% rename from sdk/src/tools/callback.ts rename to sdk/src/tools/callbacks.ts index defee25..ec9aeb8 100644 --- a/sdk/src/tools/callback.ts +++ b/sdk/src/tools/callbacks.ts @@ -1,4 +1,4 @@ -import { ITool, type Tools } from ".."; +import { ITool, type ToolBuilder } from ".."; /** * Represents a callback token for persistent function references. @@ -35,7 +35,7 @@ export type CallbackMethods = { */ export type CallbackContext = T[K] extends ( args: any, - context?: infer C, + context: infer C ) => any ? C : undefined; @@ -84,7 +84,7 @@ export type CallbackContext = T[K] extends ( * } * ``` */ -export abstract class CallbackTool extends ITool { +export abstract class Callbacks extends ITool { /** * Creates a persistent callback to the tool's parent. * Returns a callback token that can be used to call the callback later. @@ -95,7 +95,7 @@ export abstract class CallbackTool extends ITool { */ abstract create>( _functionName: K, - _context?: CallbackContext, + _context?: CallbackContext ): Promise; /** diff --git a/sdk/src/tools/index.ts b/sdk/src/tools/index.ts index 2405d89..0372699 100644 --- a/sdk/src/tools/index.ts +++ b/sdk/src/tools/index.ts @@ -1,8 +1,8 @@ export * from "./agent"; export * from "./ai"; -export * from "./auth"; -export * from "./callback"; +export * from "./callbacks"; +export * from "./integrations"; +export * from "./network"; export * from "./plot"; -export * from "./run"; export * from "./store"; -export * from "./webhook"; +export * from "./tasks"; diff --git a/sdk/src/tools/auth.ts b/sdk/src/tools/integrations.ts similarity index 79% rename from sdk/src/tools/auth.ts rename to sdk/src/tools/integrations.ts index 9ebbdc3..62d16e6 100644 --- a/sdk/src/tools/auth.ts +++ b/sdk/src/tools/integrations.ts @@ -1,24 +1,24 @@ -import { type ActivityLink, type Callback, ITool } from ".."; +import { type ActivityLink, type Callback, ITool, ToolBuilder } from ".."; /** * Built-in tool for managing OAuth authentication flows. * - * The Auth tool provides a unified interface for requesting user authorization + * The Integrations tool provides a unified interface for requesting user authorization * from external service providers like Google and Microsoft. It handles the * OAuth flow creation, token management, and callback integration. * * @example * ```typescript * class CalendarTool extends Tool { - * private auth: Auth; + * private auth: Integrations; * - * constructor(id: string, tools: Tools) { + * constructor(id: string, tools: ToolBuilder) { * super(); - * this.auth = tools.get(Auth); + * this.integrations = tools.get(Integrations); * } * * async requestAuth() { - * return await this.auth.request({ + * return await this.integrations.request({ * provider: AuthProvider.Google, * level: AuthLevel.User, * scopes: ["https://www.googleapis.com/auth/calendar.readonly"] @@ -29,12 +29,16 @@ import { type ActivityLink, type Callback, ITool } from ".."; * } * * async onAuthComplete(authResult: Authorization, context: any) { - * const authToken = await this.auth.get(authResult); + * const authToken = await this.integrations.get(authResult); * } * } * ``` */ -export abstract class Auth extends ITool { +export abstract class Integrations extends ITool { + static Init(_tools: ToolBuilder, _options?: any): Record { + return {}; + } + /** * Initiates an OAuth authentication flow. * @@ -73,7 +77,7 @@ export abstract class Auth extends ITool { * Enumeration of supported OAuth providers. * * Each provider has different OAuth endpoints, scopes, and token formats. - * The Auth tool handles the provider-specific implementation details. + * The Integrations tool handles the provider-specific implementation details. */ export enum AuthProvider { /** Google OAuth provider for Google Workspace services */ @@ -101,11 +105,11 @@ export enum AuthLevel { * based on provider, scopes, and authorization ID. */ export type Authorization = { - /** Unique identifier for this authorization */ + /** Unique identifier for this.integrationsorization */ id: string; - /** The OAuth provider this authorization is for */ + /** The OAuth provider this.integrationsorization is for */ provider: AuthProvider; - /** Array of OAuth scopes this authorization covers */ + /** Array of OAuth scopes this.integrationsorization covers */ scopes: string[]; }; diff --git a/sdk/src/tools/webhook.ts b/sdk/src/tools/network.ts similarity index 59% rename from sdk/src/tools/webhook.ts rename to sdk/src/tools/network.ts index 20b0a63..20493a4 100644 --- a/sdk/src/tools/webhook.ts +++ b/sdk/src/tools/network.ts @@ -1,11 +1,49 @@ -import { ITool, type Tools } from ".."; +import { ITool, type ToolBuilder } from ".."; /** - * Built-in tool for creating and managing webhook endpoints. + * Represents an incoming webhook request. + * + * This object is passed to webhook callback functions and contains all + * the information about the HTTP request that triggered the webhook. + * + * @example + * ```typescript + * async onWebhookReceived(request: WebhookRequest, context: any) { + * console.log(`${request.method} request received`); + * console.log("Headers:", request.headers); + * console.log("Query params:", request.params); + * console.log("Body:", request.body); + * console.log("Context:", context); + * } + * ``` + */ +export type WebhookRequest = { + /** HTTP method of the request (GET, POST, etc.) */ + method: string; + /** HTTP headers from the request */ + headers: Record; + /** Query string parameters from the request URL */ + params: Record; + /** Request body (parsed as JSON if applicable) */ + body: any; +}; + +/** + * Built-in tool for requesting HTTP access permissions and managing webhooks. * - * The Webhook tool enables agents and tools to create HTTP endpoints that - * external services can call to trigger callbacks. This is essential for - * real-time integrations with services like Google Calendar, GitHub, Slack, etc. + * The Network tool serves two purposes: + * 1. Declares which URLs an agent or tool is allowed to access via HTTP/HTTPS + * 2. Provides webhook creation and management for receiving HTTP callbacks + * + * **IMPORTANT**: Must be requested in the Agent or Tool Init method to declare + * HTTP access permissions. Without requesting this tool with the appropriate URLs, + * all outbound HTTP requests (fetch, etc.) will be blocked. + * + * **Permission Patterns:** + * - `*` - Allow access to all URLs + * - `https://*.example.com` - Allow access to all subdomains + * - `https://api.example.com/*` - Allow access to all paths on the domain + * - `https://api.example.com/v1/*` - Allow access to specific path prefix * * **Webhook Characteristics:** * - Persistent across worker restarts @@ -15,17 +53,35 @@ import { ITool, type Tools } from ".."; * * @example * ```typescript - * class CalendarTool extends Tool { - * private webhook: Webhook; + * class MyAgent extends Agent { + * static Init(tools: ToolBuilder) { + * return { + * // Request HTTP access to specific APIs + * network: tools.init(Network, { + * urls: [ + * 'https://api.github.com/*', + * 'https://api.openai.com/*' + * ] + * }) + * }; + * } + * } + * ``` * - * constructor(id: string, tools: Tools) { - * super(); - * this.webhook = tools.get(Webhook); + * @example + * ```typescript + * class CalendarTool extends Tool { + * static Init(tools: ToolBuilder) { + * return { + * network: tools.init(Network, { + * urls: ['https://www.googleapis.com/calendar/*'] + * }) + * }; * } * * async setupCalendarWebhook(calendarId: string) { * // Create webhook URL that will call onCalendarEvent - * const webhookUrl = await this.webhook.create("onCalendarEvent", { + * const webhookUrl = await this.tools.network.createWebhook("onCalendarEvent", { * calendarId, * provider: "google" * }); @@ -48,12 +104,16 @@ import { ITool, type Tools } from ".."; * } * * async cleanup(webhookUrl: string) { - * await this.webhook.delete(webhookUrl); + * await this.tools.network.deleteWebhook(webhookUrl); * } * } * ``` */ -export abstract class Webhook extends ITool { +export abstract class Network extends ITool { + static Init(_tools: ToolBuilder, _options?: any): Record { + return {}; + } + /** * Creates a new webhook endpoint. * @@ -65,7 +125,7 @@ export abstract class Webhook extends ITool { * @param context - Optional context data to pass to the callback function * @returns Promise resolving to the webhook URL */ - abstract create(_callbackName: string, _context?: any): Promise; + abstract createWebhook(_callbackName: string, _context?: any): Promise; /** * Deletes an existing webhook endpoint. @@ -76,33 +136,5 @@ export abstract class Webhook extends ITool { * @param url - The webhook URL to delete * @returns Promise that resolves when the webhook is deleted */ - abstract delete(_url: string): Promise; + abstract deleteWebhook(_url: string): Promise; } - -/** - * Represents an incoming webhook request. - * - * This object is passed to webhook callback functions and contains all - * the information about the HTTP request that triggered the webhook. - * - * @example - * ```typescript - * async onWebhookReceived(request: WebhookRequest, context: any) { - * console.log(`${request.method} request received`); - * console.log("Headers:", request.headers); - * console.log("Query params:", request.params); - * console.log("Body:", request.body); - * console.log("Context:", context); - * } - * ``` - */ -export type WebhookRequest = { - /** HTTP method of the request (GET, POST, etc.) */ - method: string; - /** HTTP headers from the request */ - headers: Record; - /** Query string parameters from the request URL */ - params: Record; - /** Request body (parsed as JSON if applicable) */ - body: any; -}; diff --git a/sdk/src/tools/plot.ts b/sdk/src/tools/plot.ts index 95a015b..0a9ea5c 100644 --- a/sdk/src/tools/plot.ts +++ b/sdk/src/tools/plot.ts @@ -7,6 +7,7 @@ import { type NewActivity, type NewPriority, type Priority, + type ToolBuilder, } from ".."; /** @@ -21,7 +22,7 @@ import { * class MyAgent extends Agent { * private plot: Plot; * - * constructor(id: string, tools: Tools) { + * constructor(id: string, tools: ToolBuilder) { * super(); * this.plot = tools.get(Plot); * } @@ -42,6 +43,10 @@ import { * ``` */ export abstract class Plot extends ITool { + static Init(_tools: ToolBuilder, _options?: any): Record { + return {}; + } + /** * Creates a new activity in the Plot system. * diff --git a/sdk/src/tools/store.ts b/sdk/src/tools/store.ts index fbad936..1cf1a60 100644 --- a/sdk/src/tools/store.ts +++ b/sdk/src/tools/store.ts @@ -1,4 +1,4 @@ -import { ITool, type Tools } from ".."; +import { ITool, type ToolBuilder } from ".."; /** * Built-in tool for persistent key-value storage. diff --git a/sdk/src/tools/run.ts b/sdk/src/tools/tasks.ts similarity index 95% rename from sdk/src/tools/run.ts rename to sdk/src/tools/tasks.ts index e605d30..3eebb85 100644 --- a/sdk/src/tools/run.ts +++ b/sdk/src/tools/tasks.ts @@ -1,5 +1,5 @@ -import { ITool, type Tools } from ".."; -import type { Callback } from "./callback"; +import { ITool, type ToolBuilder } from ".."; +import type { Callback } from "./callbacks"; /** * Run background tasks and scheduled jobs. @@ -55,7 +55,7 @@ import type { Callback } from "./callback"; * } * ``` */ -export abstract class Run extends ITool { +export abstract class Tasks extends ITool { /** * Queues a callback to execute in a separate worker context. * @@ -70,7 +70,7 @@ export abstract class Run extends ITool { */ abstract run( _callback: Callback, - _options?: { runAt?: Date }, + _options?: { runAt?: Date } ): Promise; /** diff --git a/tools/google-calendar/README.md b/tools/google-calendar/README.md index 95f3f83..c7dcd90 100644 --- a/tools/google-calendar/README.md +++ b/tools/google-calendar/README.md @@ -13,21 +13,21 @@ npm install @plotday/tool-google-calendar @plotday/sdk ```typescript import { Agent, Tools } from "@plotday/sdk"; import { GoogleCalendar } from "@plotday/tool-google-calendar"; -import { Auth, AuthLevel, AuthProvider } from "@plotday/sdk/tools/auth"; +import { Integrations, AuthLevel, AuthProvider } from "@plotday/sdk/tools/integrations"; export default class extends Agent { private googleCalendar: GoogleCalendar; - private auth: Auth; + private auth: Integrations; constructor(id: string, tools: Tools) { super(); this.googleCalendar = tools.get(GoogleCalendar); - this.auth = tools.get(Auth); + this.integrations = tools.get(Integrations); } async activate(priority: { id: string }) { // Request Google Calendar access - const authLink = await this.auth.request( + const authLink = await this.integrations.request( { provider: AuthProvider.Google, level: AuthLevel.User, @@ -43,7 +43,7 @@ export default class extends Agent { } async onAuthComplete(authorization: any, context: any) { - const authToken = await this.auth.get(authorization); + const authToken = await this.integrations.get(authorization); // Get available calendars const calendars = await this.googleCalendar.getCalendars(authToken); diff --git a/tools/google-calendar/src/google-calendar.ts b/tools/google-calendar/src/google-calendar.ts index b69d3cd..4021486 100644 --- a/tools/google-calendar/src/google-calendar.ts +++ b/tools/google-calendar/src/google-calendar.ts @@ -2,7 +2,7 @@ import { type ActivityLink, type NewActivity, Tool, - type Tools, + type ToolBuilder, } from "@plotday/sdk"; import { type Calendar, @@ -10,14 +10,14 @@ import { type CalendarTool, type SyncOptions, } from "@plotday/sdk/common/calendar"; +import { type Callback } from "@plotday/sdk/tools/callbacks"; import { - Auth, AuthLevel, AuthProvider, type Authorization, -} from "@plotday/sdk/tools/auth"; -import { type Callback } from "@plotday/sdk/tools/callback"; -import { Webhook, type WebhookRequest } from "@plotday/sdk/tools/webhook"; + Integrations, +} from "@plotday/sdk/tools/integrations"; +import { Network, type WebhookRequest } from "@plotday/sdk/tools/network"; import { GoogleApi, @@ -99,14 +99,17 @@ type AuthSuccessContext = { * } * ``` */ -export class GoogleCalendar extends Tool implements CalendarTool { - private auth: Auth; - private webhook: Webhook; - - constructor(id: string, protected tools: Tools) { - super(id, tools); - this.auth = tools.get(Auth); - this.webhook = tools.get(Webhook); +export class GoogleCalendar + extends Tool + implements CalendarTool +{ + static Init(tools: ToolBuilder) { + return { + integrations: tools.init(Integrations), + network: tools.init(Network, { + urls: ["https://www.googleapis.com/calendar/*"], + }), + }; } async requestAuth(callback: Callback): Promise { @@ -115,7 +118,7 @@ export class GoogleCalendar extends Tool implements CalendarTool { "https://www.googleapis.com/auth/calendar.events", ]; - // Generate opaque token for this authorization + // Generate opaque token for this.integrationsorization const authToken = crypto.randomUUID(); // Use the provided callback token @@ -128,7 +131,7 @@ export class GoogleCalendar extends Tool implements CalendarTool { } satisfies AuthSuccessContext); // Request auth and return the activity link - return await this.auth.request( + return await this.tools.integrations.request( { provider: AuthProvider.Google, level: AuthLevel.User, @@ -146,7 +149,7 @@ export class GoogleCalendar extends Tool implements CalendarTool { throw new Error("Authorization no longer available"); } - const token = await this.auth.get(authorization); + const token = await this.tools.integrations.get(authorization); if (!token) { throw new Error("Authorization no longer available"); } @@ -229,10 +232,13 @@ export class GoogleCalendar extends Tool implements CalendarTool { calendarId: string, opaqueAuthToken: string ): Promise { - const webhookUrl = await this.webhook.create("onCalendarWebhook", { - calendarId, - authToken: opaqueAuthToken, - }); + const webhookUrl = await this.tools.network.createWebhook( + "onCalendarWebhook", + { + calendarId, + authToken: opaqueAuthToken, + } + ); // Check if webhook URL is localhost if (URL.parse(webhookUrl)?.hostname === "localhost") { diff --git a/tools/google-contacts/README.md b/tools/google-contacts/README.md index 6bb02db..33b5552 100644 --- a/tools/google-contacts/README.md +++ b/tools/google-contacts/README.md @@ -13,21 +13,21 @@ npm install @plotday/tool-google-contacts @plotday/sdk ```typescript import { Agent, Tools } from "@plotday/sdk"; import { GoogleContacts } from "@plotday/tool-google-contacts"; -import { Auth, AuthLevel, AuthProvider } from "@plotday/sdk/tools/auth"; +import { Integrations, AuthLevel, AuthProvider } from "@plotday/sdk/tools/integrations"; export default class extends Agent { private googleContacts: GoogleContacts; - private auth: Auth; + private auth: Integrations; constructor(id: string, tools: Tools) { super(); this.googleContacts = tools.get(GoogleContacts); - this.auth = tools.get(Auth); + this.integrations = tools.get(Integrations); } async activate(priority: { id: string }) { // Request Google Contacts access - const authLink = await this.auth.request( + const authLink = await this.integrations.request( { provider: AuthProvider.Google, level: AuthLevel.User, @@ -43,7 +43,7 @@ export default class extends Agent { } async onAuthComplete(authorization: any, context: any) { - const authToken = await this.auth.get(authorization); + const authToken = await this.integrations.get(authorization); // Start syncing contacts await this.googleContacts.startSync(authToken, "onContact"); diff --git a/tools/google-contacts/src/google-contacts.ts b/tools/google-contacts/src/google-contacts.ts index 334dc39..d1c9d88 100644 --- a/tools/google-contacts/src/google-contacts.ts +++ b/tools/google-contacts/src/google-contacts.ts @@ -1,13 +1,17 @@ -import { Tool, type Tools } from "@plotday/sdk"; +import { Tool, type ToolBuilder } from "@plotday/sdk"; +import { type Callback } from "@plotday/sdk/tools/callbacks"; import { - Auth, AuthLevel, AuthProvider, type AuthToken, -} from "@plotday/sdk/tools/auth"; -import { type Callback } from "@plotday/sdk/tools/callback"; + Integrations, +} from "@plotday/sdk/tools/integrations"; -import type { Contact, ContactAuth, GoogleContacts } from "./types"; +import type { + Contact, + ContactAuth, + GoogleContacts as IGoogleContacts, +} from "./types"; type ContactTokens = { connections?: { @@ -243,34 +247,26 @@ async function getGoogleContacts( }; } -export default class extends Tool implements GoogleContacts { +export default class GoogleContacts + extends Tool + implements IGoogleContacts +{ static readonly id = "google-contacts"; - private auth: Auth; - - constructor(id: string, protected tools: Tools) { - super(id, tools); - this.auth = tools.get(Auth); + static Init(tools: ToolBuilder) { + return { + integrations: tools.init(Integrations), + }; } - async requestAuth( - callbackFunctionName: string, - callbackContext?: any - ): Promise { + async requestAuth(callback: Callback): Promise { const contactsScopes = [ "https://www.googleapis.com/auth/contacts.readonly", "https://www.googleapis.com/auth/contacts.other.readonly", ]; - // Generate opaque token for this authorization const opaqueToken = crypto.randomUUID(); - - // Register the callback for auth completion with opaque token - const callbackToken = await this.callback( - callbackFunctionName, - callbackContext - ); - await this.set(`auth_callback_token:${opaqueToken}`, callbackToken); + await this.set(`auth_callback_token:${opaqueToken}`, callback); // Create callback for auth completion const authCallback = await this.callback("onAuthSuccess", { @@ -279,7 +275,7 @@ export default class extends Tool implements GoogleContacts { }); // Request auth and return the activity link - return await this.auth.request( + return await this.tools.integrations.request( { provider: AuthProvider.Google, level: AuthLevel.User, @@ -307,13 +303,7 @@ export default class extends Tool implements GoogleContacts { return result.contacts; } - async startSync( - authToken: string, - callbackFunctionName: string, - options?: { - context?: any; - } - ): Promise { + async startSync(authToken: string, callback: Callback): Promise { const storedAuthToken = await this.get( `auth_token:${authToken}` ); @@ -324,11 +314,7 @@ export default class extends Tool implements GoogleContacts { } // Register the callback - const callbackToken = await this.callback( - callbackFunctionName, - options?.context - ); - await this.set(`contacts_callback_token:${authToken}`, callbackToken); + await this.set(`contacts_callback_token:${authToken}`, callback); // Start initial sync const initialState: ContactSyncState = { @@ -338,11 +324,11 @@ export default class extends Tool implements GoogleContacts { await this.set(`sync_state:${authToken}`, initialState); // Start sync batch using run tool for long-running operation - const callback = await this.callback("syncBatch", { + const sync = await this.callback("syncBatch", { batchNumber: 1, authToken, }); - await this.run(callback); + await this.run(sync); } async stopSync(authToken: string): Promise { diff --git a/tools/google-contacts/src/types.ts b/tools/google-contacts/src/types.ts index 6e3a800..0c6301d 100644 --- a/tools/google-contacts/src/types.ts +++ b/tools/google-contacts/src/types.ts @@ -1,4 +1,4 @@ -import type { ActivityLink, Tool } from "@plotday/sdk"; +import type { ActivityLink, ITool } from "@plotday/sdk"; export type Contact = { email: string; @@ -10,7 +10,7 @@ export type ContactAuth = { authToken: string; }; -export interface GoogleContacts extends Tool { +export interface GoogleContacts extends ITool { requestAuth( callbackFunctionName: string, callbackContext?: any @@ -28,4 +28,3 @@ export interface GoogleContacts extends Tool { stopSync(authToken: string): Promise; } - diff --git a/tools/outlook-calendar/README.md b/tools/outlook-calendar/README.md index e376715..77d2d68 100644 --- a/tools/outlook-calendar/README.md +++ b/tools/outlook-calendar/README.md @@ -13,21 +13,21 @@ npm install @plotday/tool-outlook-calendar @plotday/sdk ```typescript import { Agent, Tools } from "@plotday/sdk"; import { OutlookCalendar } from "@plotday/tool-outlook-calendar"; -import { Auth, AuthLevel, AuthProvider } from "@plotday/sdk/tools/auth"; +import { Integrations, AuthLevel, AuthProvider } from "@plotday/sdk/tools/integrations"; export default class extends Agent { private outlookCalendar: OutlookCalendar; - private auth: Auth; + private auth: Integrations; constructor(id: string, tools: Tools) { super(); this.outlookCalendar = tools.get(OutlookCalendar); - this.auth = tools.get(Auth); + this.integrations = tools.get(Integrations); } async activate(priority: { id: string }) { // Request Outlook Calendar access - const authLink = await this.auth.request( + const authLink = await this.integrations.request( { provider: AuthProvider.Microsoft, level: AuthLevel.User, @@ -43,7 +43,7 @@ export default class extends Agent { } async onAuthComplete(authorization: any, context: any) { - const authToken = await this.auth.get(authorization); + const authToken = await this.integrations.get(authorization); // Get available calendars const calendars = await this.outlookCalendar.getCalendars(authToken); diff --git a/tools/outlook-calendar/src/outlook-calendar.ts b/tools/outlook-calendar/src/outlook-calendar.ts index ade00ad..646c4b5 100644 --- a/tools/outlook-calendar/src/outlook-calendar.ts +++ b/tools/outlook-calendar/src/outlook-calendar.ts @@ -3,7 +3,7 @@ import { type ActivityLink, ActivityType, Tool, - type Tools, + type ToolBuilder, } from "@plotday/sdk"; import type { Calendar, @@ -11,14 +11,14 @@ import type { CalendarTool, SyncOptions, } from "@plotday/sdk/common/calendar"; +import { type Callback } from "@plotday/sdk/tools/callbacks"; import { - Auth, AuthLevel, AuthProvider, type Authorization, -} from "@plotday/sdk/tools/auth"; -import { type Callback } from "@plotday/sdk/tools/callback"; -import { Webhook, type WebhookRequest } from "@plotday/sdk/tools/webhook"; + Integrations, +} from "@plotday/sdk/tools/integrations"; +import { Network, type WebhookRequest } from "@plotday/sdk/tools/network"; type AuthSuccessContext = { token: string; @@ -222,18 +222,19 @@ const outlookApi = { * } * ``` */ -export class OutlookCalendar extends Tool implements CalendarTool { - private auth: Auth; - private webhook: Webhook; - - constructor(id: string, protected tools: Tools) { - super(id, tools); - this.auth = tools.get(Auth); - this.webhook = tools.get(Webhook); +export class OutlookCalendar + extends Tool + implements CalendarTool +{ + static Init(tools: ToolBuilder, _options: any) { + return { + integrations: tools.init(Integrations), + network: tools.init(Network), + }; } async requestAuth(callback: Callback): Promise { - // Generate opaque token for this auth request + // Generate opaque token for this.integrations request const token = crypto.randomUUID(); // Store the callback token for auth completion @@ -245,7 +246,7 @@ export class OutlookCalendar extends Tool implements CalendarTool { } satisfies AuthSuccessContext); // Request Microsoft authentication and return the activity link - return await this.auth.request( + return await this.tools.integrations.request( { provider: AuthProvider.Microsoft, level: AuthLevel.User, @@ -266,7 +267,7 @@ export class OutlookCalendar extends Tool implements CalendarTool { throw new Error("Authorization no longer available"); } - const token = await this.auth.get(authorization); + const token = await this.tools.integrations.get(authorization); if (!token) { throw new Error("Authorization no longer available"); } @@ -341,7 +342,7 @@ export class OutlookCalendar extends Tool implements CalendarTool { ): Promise { const { config, credentials } = await this.getApi(authToken); - const webhookUrl = await this.webhook.create("onOutlookWebhook", { + const webhookUrl = await this.tools.network.createWebhook("onOutlookWebhook", { calendarId, authToken: opaqueAuthToken, }); @@ -639,8 +640,6 @@ export class OutlookCalendar extends Tool implements CalendarTool { authResult: Authorization, context: AuthSuccessContext ): Promise { - console.log("Outlook Calendar authentication successful", authResult); - // Store the actual auth token using opaque token as key await this.set(`authorization:${context.token}`, authResult); From 49b4dc94e08906a89799903610325c5fe7ebe10b Mon Sep 17 00:00:00 2001 From: Kris Braun Date: Fri, 24 Oct 2025 17:33:40 -0400 Subject: [PATCH 2/5] Renamed callCallback, run, cancel, and cancelAll Agent/Tool functions --- .changeset/sparkly-candles-camp.md | 7 ++++ .changeset/sweet-clouds-dance.md | 5 +++ sdk/README.md | 2 +- sdk/src/agent.ts | 36 +++++++++---------- sdk/src/tools/callbacks.ts | 4 +-- sdk/src/tools/tasks.ts | 14 ++++---- tools/google-calendar/src/google-calendar.ts | 10 ++++-- tools/google-contacts/src/google-contacts.ts | 8 ++--- .../outlook-calendar/src/outlook-calendar.ts | 4 +-- 9 files changed, 53 insertions(+), 37 deletions(-) create mode 100644 .changeset/sparkly-candles-camp.md create mode 100644 .changeset/sweet-clouds-dance.md diff --git a/.changeset/sparkly-candles-camp.md b/.changeset/sparkly-candles-camp.md new file mode 100644 index 0000000..53b0fce --- /dev/null +++ b/.changeset/sparkly-candles-camp.md @@ -0,0 +1,7 @@ +--- +"@plotday/tool-outlook-calendar": patch +"@plotday/tool-google-calendar": patch +"@plotday/tool-google-contacts": patch +--- + +Changed: Update for new callback function names diff --git a/.changeset/sweet-clouds-dance.md b/.changeset/sweet-clouds-dance.md new file mode 100644 index 0000000..9b235ce --- /dev/null +++ b/.changeset/sweet-clouds-dance.md @@ -0,0 +1,5 @@ +--- +"@plotday/sdk": minor +--- + +Changed: BREAKING: Renamed callCallback, run, cancel, and cancelAll Agent/Tool functions diff --git a/sdk/README.md b/sdk/README.md index 8ffa813..d7338fe 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -316,7 +316,7 @@ const callback = await this.callback("handleEvent", { }); // Execute callback -const result = await this.callCallback(callback, { +const result = await this.run(callback, { data: eventData, }); diff --git a/sdk/src/agent.ts b/sdk/src/agent.ts index eaa9f5b..adddea1 100644 --- a/sdk/src/agent.ts +++ b/sdk/src/agent.ts @@ -128,8 +128,8 @@ export abstract class Agent> { * @param args - Optional arguments to pass to the callback * @returns Promise resolving to the callback result */ - protected async callCallback(token: Callback, args?: any): Promise { - return this.tools.callbacks.callCallback(token, args); + protected async run(token: Callback, args?: any): Promise { + return this.tools.callbacks.run(token, args); } /** @@ -182,21 +182,21 @@ export abstract class Agent> { * @param options.runAt - If provided, schedules execution at this time; otherwise runs immediately * @returns Promise resolving to a cancellation token (only for scheduled executions) */ - protected async run( + protected async runTask( callback: Callback, options?: { runAt?: Date } ): Promise { - return this.tools.tasks.run(callback, options); + return this.tools.tasks.runTask(callback, options); } /** * Cancels a previously scheduled execution. * - * @param token - The cancellation token returned by run() with runAt option + * @param token - The cancellation token returned by runTask() with runAt option * @returns Promise that resolves when the cancellation is processed */ - protected async cancel(token: string): Promise { - return this.tools.tasks.cancel(token); + protected async cancelTask(token: string): Promise { + return this.tools.tasks.cancelTask(token); } /** @@ -204,8 +204,8 @@ export abstract class Agent> { * * @returns Promise that resolves when all cancellations are processed */ - protected async cancelAll(): Promise { - return this.tools.tasks.cancelAll(); + protected async cancelAllTasks(): Promise { + return this.tools.tasks.cancelAllTasks(); } /** @@ -328,8 +328,8 @@ export abstract class Tool> implements ITool { * @param args - Optional arguments to pass to the callback * @returns Promise resolving to the callback result */ - protected async callCallback(token: Callback, args?: any): Promise { - return this.tools.callbacks.callCallback(token, args); + protected async run(token: Callback, args?: any): Promise { + return this.tools.callbacks.run(token, args); } /** @@ -382,21 +382,21 @@ export abstract class Tool> implements ITool { * @param options.runAt - If provided, schedules execution at this time; otherwise runs immediately * @returns Promise resolving to a cancellation token (only for scheduled executions) */ - protected async run( + protected async runTask( callback: Callback, options?: { runAt?: Date } ): Promise { - return this.tools.tasks.run(callback, options); + return this.tools.tasks.runTask(callback, options); } /** * Cancels a previously scheduled execution. * - * @param token - The cancellation token returned by run() with runAt option + * @param token - The cancellation token returned by runTask() with runAt option * @returns Promise that resolves when the cancellation is processed */ - protected async cancel(token: string): Promise { - return this.tools.tasks.cancel(token); + protected async cancelTask(token: string): Promise { + return this.tools.tasks.cancelTask(token); } /** @@ -404,8 +404,8 @@ export abstract class Tool> implements ITool { * * @returns Promise that resolves when all cancellations are processed */ - protected async cancelAll(): Promise { - return this.tools.tasks.cancelAll(); + protected async cancelAllTasks(): Promise { + return this.tools.tasks.cancelAllTasks(); } } diff --git a/sdk/src/tools/callbacks.ts b/sdk/src/tools/callbacks.ts index ec9aeb8..0b08ce1 100644 --- a/sdk/src/tools/callbacks.ts +++ b/sdk/src/tools/callbacks.ts @@ -50,7 +50,7 @@ export type CallbackContext = T[K] extends ( * * **Note:** Callback methods are also available directly on Agent and Tool classes * via `this.callback()`, `this.deleteCallback()`, `this.deleteAllCallbacks()`, and - * `this.callCallback()`. This is the recommended approach for most use cases. + * `this.run()`. This is the recommended approach for most use cases. * * **When to use callbacks:** * - Webhook handlers that need persistent function references @@ -120,5 +120,5 @@ export abstract class Callbacks extends ITool { * @param args - Optional arguments to pass to the callback function * @returns Promise resolving to the callback result */ - abstract callCallback(_callback: Callback, _args?: any): Promise; + abstract run(_callback: Callback, _args?: any): Promise; } diff --git a/sdk/src/tools/tasks.ts b/sdk/src/tools/tasks.ts index 3eebb85..bd4983a 100644 --- a/sdk/src/tools/tasks.ts +++ b/sdk/src/tools/tasks.ts @@ -10,7 +10,7 @@ import type { Callback } from "./callbacks"; * retries on failure. * * **Note:** Run methods are also available directly on Agent and Tool classes - * via `this.run()`, `this.cancel()`, and `this.cancelAll()`. + * via `this.runTask()`, `this.cancelTask()`, and `this.cancelAllTasks()`. * This is the recommended approach for most use cases. * * **Best Practices:** @@ -27,7 +27,7 @@ import type { Callback } from "./callbacks"; * * // Create callback and queue first batch using built-in methods * const callback = await this.callback("processBatch", { batchNumber: 1 }); - * await this.run(callback); + * await this.runTask(callback); * } * * async processBatch(args: any, context: { batchNumber: number }) { @@ -41,7 +41,7 @@ import type { Callback } from "./callbacks"; * const callback = await this.callback("processBatch", { * batchNumber: context.batchNumber + 1 * }); - * await this.run(callback); + * await this.runTask(callback); * } * } * @@ -68,7 +68,7 @@ export abstract class Tasks extends ITool { * @param options.runAt - If provided, schedules execution at this time; otherwise runs immediately * @returns Promise resolving to a cancellation token (only for scheduled executions) */ - abstract run( + abstract runTask( _callback: Callback, _options?: { runAt?: Date } ): Promise; @@ -79,10 +79,10 @@ export abstract class Tasks extends ITool { * Prevents a scheduled function from executing. No error is thrown * if the token is invalid or the execution has already completed. * - * @param token - The cancellation token returned by run() with runAt option + * @param token - The cancellation token returned by runTask() with runAt option * @returns Promise that resolves when the cancellation is processed */ - abstract cancel(_token: string): Promise; + abstract cancelTask(_token: string): Promise; /** * Cancels all scheduled executions for this tool/agent. @@ -92,5 +92,5 @@ export abstract class Tasks extends ITool { * * @returns Promise that resolves when all cancellations are processed */ - abstract cancelAll(): Promise; + abstract cancelAllTasks(): Promise; } diff --git a/tools/google-calendar/src/google-calendar.ts b/tools/google-calendar/src/google-calendar.ts index 4021486..e742a3d 100644 --- a/tools/google-calendar/src/google-calendar.ts +++ b/tools/google-calendar/src/google-calendar.ts @@ -185,8 +185,11 @@ export class GoogleCalendar callback: Callback, options?: SyncOptions ): Promise { + console.log("Saving callback"); + // Store the callback token await this.set("event_callback_token", callback); + console.log("Setting up watch"); // Setup webhook for this calendar await this.setupCalendarWatch(authToken, calendarId, authToken); @@ -205,6 +208,7 @@ export class GoogleCalendar await this.set(`sync_state_${calendarId}`, initialState); + console.log("Starting sync"); // Start sync batch using run tool for long-running operation const syncCallback = await this.callback("syncBatch", { calendarId, @@ -383,7 +387,7 @@ export class GoogleCalendar source: activityData.source || null, }; - await this.callCallback(callbackToken, activity); + await this.run(callbackToken, activity); } } } catch (error) { @@ -429,7 +433,7 @@ export class GoogleCalendar source: activityData.source || null, }; - await this.callCallback(callbackToken, activity); + await this.run(callbackToken, activity); } } @@ -508,7 +512,7 @@ export class GoogleCalendar const authSuccessResult: CalendarAuth = { authToken: context.authToken, }; - await this.callCallback(context.callbackToken, authSuccessResult); + await this.run(context.callbackToken, authSuccessResult); // Clean up the callback token await this.clear(`auth_callback_token:${context.authToken}`); diff --git a/tools/google-contacts/src/google-contacts.ts b/tools/google-contacts/src/google-contacts.ts index d1c9d88..87efb77 100644 --- a/tools/google-contacts/src/google-contacts.ts +++ b/tools/google-contacts/src/google-contacts.ts @@ -328,7 +328,7 @@ export default class GoogleContacts batchNumber: 1, authToken, }); - await this.run(sync); + await this.runTask(sync); } async stopSync(authToken: string): Promise { @@ -380,7 +380,7 @@ export default class GoogleContacts batchNumber: batchNumber + 1, authToken, }); - await this.run(callback); + await this.runTask(callback); } else { console.log( `Google Contacts sync completed after ${batchNumber} batches` @@ -402,7 +402,7 @@ export default class GoogleContacts `contacts_callback_token:${authToken}` ); if (callbackToken) { - await this.callCallback(callbackToken, contacts); + await this.run(callbackToken, contacts); } } @@ -428,7 +428,7 @@ export default class GoogleContacts authToken: opaqueToken, }; - await this.callCallback(callbackToken, authSuccessResult); + await this.run(callbackToken, authSuccessResult); // Clean up the callback token await this.clear(`auth_callback_token:${opaqueToken}`); diff --git a/tools/outlook-calendar/src/outlook-calendar.ts b/tools/outlook-calendar/src/outlook-calendar.ts index 646c4b5..c016963 100644 --- a/tools/outlook-calendar/src/outlook-calendar.ts +++ b/tools/outlook-calendar/src/outlook-calendar.ts @@ -565,7 +565,7 @@ export class OutlookCalendar // Call the event callback const callbackToken = await this.get("event_callback_token"); if (callbackToken) { - await this.callCallback(callbackToken, activity); + await this.run(callbackToken, activity); } } @@ -652,7 +652,7 @@ export class OutlookCalendar authToken: context.token, }; - await this.callCallback(callbackToken, authSuccessResult); + await this.run(callbackToken, authSuccessResult); // Clean up the callback token await this.clear(`auth_callback_token:${context.token}`); From 02c6a1e834b9aa645f29191ed59ee5b66b70c32a Mon Sep 17 00:00:00 2001 From: Kris Braun Date: Sat, 25 Oct 2025 16:21:21 -0400 Subject: [PATCH 3/5] Improved callback ergonomics; improved stack traces --- .changeset/lazy-snakes-refuse.md | 5 + .changeset/warm-beds-jump.md | 8 + agents/events/src/index.ts | 101 +++--- sdk/cli/commands/agent-logs.ts | 43 ++- sdk/cli/commands/deploy.ts | 3 + sdk/cli/utils/bundle.ts | 11 + sdk/package.json | 4 + sdk/prebuild.ts | 3 +- sdk/src/agent.ts | 287 ++++++++++++------ sdk/src/common/calendar.ts | 24 +- sdk/src/plot.ts | 16 +- sdk/src/tools/callbacks.ts | 87 +++--- sdk/src/tools/integrations.ts | 17 +- sdk/src/tools/network.ts | 18 +- sdk/src/tools/tasks.ts | 4 +- sdk/src/utils/types.ts | 140 +++++++++ tools/google-calendar/src/google-calendar.ts | 144 ++++----- tools/google-contacts/src/google-contacts.ts | 98 +++--- tools/google-contacts/src/types.ts | 18 +- .../outlook-calendar/src/outlook-calendar.ts | 171 +++++------ 20 files changed, 741 insertions(+), 461 deletions(-) create mode 100644 .changeset/lazy-snakes-refuse.md create mode 100644 .changeset/warm-beds-jump.md create mode 100644 sdk/src/utils/types.ts diff --git a/.changeset/lazy-snakes-refuse.md b/.changeset/lazy-snakes-refuse.md new file mode 100644 index 0000000..a4711b8 --- /dev/null +++ b/.changeset/lazy-snakes-refuse.md @@ -0,0 +1,5 @@ +--- +"@plotday/sdk": minor +--- + +Added: Improved stack traces diff --git a/.changeset/warm-beds-jump.md b/.changeset/warm-beds-jump.md new file mode 100644 index 0000000..30806e1 --- /dev/null +++ b/.changeset/warm-beds-jump.md @@ -0,0 +1,8 @@ +--- +"@plotday/tool-outlook-calendar": minor +"@plotday/tool-google-calendar": minor +"@plotday/tool-google-contacts": minor +"@plotday/sdk": minor +--- + +Changed: BREAKING: Improved callback ergonomics and types to use functions instead of strings diff --git a/agents/events/src/index.ts b/agents/events/src/index.ts index bee31ff..a8a57f8 100644 --- a/agents/events/src/index.ts +++ b/agents/events/src/index.ts @@ -24,13 +24,6 @@ type StoredCalendarAuth = { authToken: string; }; -type CalendarSelectionContext = { - provider: CalendarProvider; - calendarId: string; - calendarName: string; - authToken: string; -}; - export default class EventsAgent extends Agent { static Init(tools: ToolBuilder) { return { @@ -86,20 +79,14 @@ export default class EventsAgent extends Agent { } async activate(_priority: Pick) { - // Create callbacks for auth completion - const googleCallback = await this.callback("onAuthComplete", { - provider: "google", - }); - const outlookCallback = await this.callback("onAuthComplete", { - provider: "outlook", - }); - // Get auth links from both calendar tools const googleAuthLink = await this.tools.googleCalendar.requestAuth( - googleCallback + this.onAuthComplete, + "google" ); const outlookAuthLink = await this.tools.outlookCalendar.requestAuth( - outlookCallback + this.onAuthComplete, + "outlook" ); // Create activity with both auth links @@ -128,7 +115,7 @@ export default class EventsAgent extends Agent { async startSync( provider: CalendarProvider, calendarId: string, - options?: SyncOptions + _options?: SyncOptions ): Promise { const authToken = await this.getAuthToken(provider); if (!authToken) { @@ -137,13 +124,14 @@ export default class EventsAgent extends Agent { const tool = this.getProviderTool(provider); - // Create callback for event handling - const eventCallback = await this.callback("handleEvent", { - options, - context: { provider, calendarId }, - }); - - await tool.startSync(authToken, calendarId, eventCallback); + // Start sync with event handling callback + await tool.startSync( + authToken, + calendarId, + this.handleEvent, + provider, + calendarId + ); } async stopSync( @@ -177,12 +165,18 @@ export default class EventsAgent extends Agent { return results; } - async handleEvent(activity: Activity, _context?: any): Promise { + async handleEvent( + activity: Activity, + _provider: CalendarProvider, + _calendarId: string + ): Promise { await this.tools.plot.createActivity(activity); } - async onAuthComplete(authResult: CalendarAuth, context?: any): Promise { - const provider = context?.provider as CalendarProvider; + async onAuthComplete( + authResult: CalendarAuth, + provider: CalendarProvider + ): Promise { if (!provider) { console.error("No provider specified in auth context"); return; @@ -225,12 +219,13 @@ export default class EventsAgent extends Agent { // Create callback links for each calendar for (const calendar of calendars) { - const token = await this.callback("onCalendarSelected", { + const token = await this.callback( + this.onCalendarSelected, provider, - calendarId: calendar.id, - calendarName: calendar.name, - authToken, - } as CalendarSelectionContext); + calendar.id, + calendar.name, + authToken + ); if (calendar.primary) { links.unshift({ @@ -259,42 +254,40 @@ export default class EventsAgent extends Agent { async onCalendarSelected( _link: ActivityLink, - context: CalendarSelectionContext + provider: CalendarProvider, + calendarId: string, + calendarName: string, + authToken: string ): Promise { - console.log("Calendar selectedwith context:", context); - if (!context) { - console.error("No context found in calendar selection callback"); - return; - } + console.log("Calendar selected with context:", { + provider, + calendarId, + calendarName, + }); try { // Start sync for the selected calendar - const tool = this.getProviderTool(context.provider); - - // Create callback for event handling - const eventCallback = await this.callback("handleEvent", { - provider: context.provider, - calendarId: context.calendarId, - }); + const tool = this.getProviderTool(provider); + // Start sync with event handling callback await tool.startSync( - context.authToken, - context.calendarId, - eventCallback + authToken, + calendarId, + this.handleEvent, + provider, + calendarId ); - console.log( - `Started syncing ${context.provider} calendar: ${context.calendarName}` - ); + console.log(`Started syncing ${provider} calendar: ${calendarName}`); await this.tools.plot.createActivity({ type: ActivityType.Note, - note: `Reading your ${context.calendarName} calendar`, + note: `Reading your ${calendarName} calendar`, parent: await this.getParentActivity(), }); } catch (error) { console.error( - `Failed to start sync for calendar ${context.calendarName}:`, + `Failed to start sync for calendar ${calendarName}:`, error ); } diff --git a/sdk/cli/commands/agent-logs.ts b/sdk/cli/commands/agent-logs.ts index bf6ae27..ffbbd29 100644 --- a/sdk/cli/commands/agent-logs.ts +++ b/sdk/cli/commands/agent-logs.ts @@ -120,7 +120,12 @@ export async function agentLogsCommand(options: AgentLogsOptions) { if (event === "log") { // Format log entry const { timestamp, severity, message } = data; - const time = new Date(timestamp).toLocaleTimeString(); + const time = new Date(timestamp).toLocaleTimeString("en-US", { + hour12: true, + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }); // Color-code by severity let severityColor = ""; @@ -143,9 +148,43 @@ export async function agentLogsCommand(options: AgentLogsOptions) { const reset = "\x1b[0m"; const gray = "\x1b[90m"; + // Split message into lines for multi-line formatting + const lines = message.split("\n"); + const firstLine = lines[0]; + const restLines = lines.slice(1); + + // Print first line with timestamp/severity console.log( - `${gray}[${time}]${reset} ${severityColor}${severityLabel}${reset} ${message}` + `${gray}[${time}]${reset} ${severityColor}${severityLabel}${reset} ${firstLine}` ); + + // Print continuation lines with box-drawing characters for stack frames + // Indent = 13 chars "[01:38:09 PM] " + 7 chars "ERROR " = 20 spaces + const indent = " ".repeat(20); + for (let i = 0; i < restLines.length; i++) { + const line = restLines[i]; + + // Check if this is a stack trace line (starts with whitespace + "at") + const isStackFrame = /^\s+at\s/.test(line); + + if (isStackFrame) { + // Find if this is the last stack frame + const remainingLines = restLines.slice(i + 1); + const hasMoreStackFrames = remainingLines.some( + (l: string) => /^\s+at\s/.test(l) + ); + + // Remove leading spaces from the line (but keep "at" and everything after) + const trimmedLine = line.trimStart(); + + // Add box-drawing character: ┊ for middle frames, └ for last frame + const prefix = hasMoreStackFrames ? "┊" : "└"; + console.log(`${indent}${prefix} ${trimmedLine}`); + } else { + // Regular continuation line (like "Error: Debugging") + console.log(`${indent}${line}`); + } + } } }, onError: (error) => { diff --git a/sdk/cli/commands/deploy.ts b/sdk/cli/commands/deploy.ts index ff7ed18..f32f869 100644 --- a/sdk/cli/commands/deploy.ts +++ b/sdk/cli/commands/deploy.ts @@ -187,6 +187,7 @@ export async function deployCommand(options: DeployOptions) { // Build the agent let requestBody: { module: string; + sourcemap?: string; name: string; description?: string; environment: string; @@ -206,6 +207,7 @@ export async function deployCommand(options: DeployOptions) { }); const moduleContent = result.code; + const sourcemapContent = result.sourcemap; if (result.warnings.length > 0) { out.warning("Build completed with warnings"); @@ -219,6 +221,7 @@ export async function deployCommand(options: DeployOptions) { requestBody = { module: moduleContent, + sourcemap: sourcemapContent, name: deploymentName!, description: deploymentDescription, environment: environment, diff --git a/sdk/cli/utils/bundle.ts b/sdk/cli/utils/bundle.ts index 58485d2..a91883d 100644 --- a/sdk/cli/utils/bundle.ts +++ b/sdk/cli/utils/bundle.ts @@ -9,6 +9,7 @@ export interface BundleOptions { export interface BundleResult { code: string; + sourcemap?: string; warnings: string[]; } @@ -80,8 +81,18 @@ export async function bundleAgent( const bundlePath = path.join(buildDir, "index.js"); const code = fs.readFileSync(bundlePath, "utf-8"); + // Read the sourcemap if it exists + let sourcemapContent: string | undefined; + if (sourcemap) { + const sourcemapPath = path.join(buildDir, "index.js.map"); + if (fs.existsSync(sourcemapPath)) { + sourcemapContent = fs.readFileSync(sourcemapPath, "utf-8"); + } + } + return { code, + sourcemap: sourcemapContent, warnings, }; } diff --git a/sdk/package.json b/sdk/package.json index f1a9b12..6be9a93 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -58,6 +58,10 @@ "types": "./dist/tools/store.d.ts", "default": "./dist/tools/store.js" }, + "./utils/types": { + "types": "./dist/utils/types.d.ts", + "default": "./dist/utils/types.js" + }, "./common/calendar": { "types": "./dist/common/calendar.d.ts", "default": "./dist/common/calendar.js" diff --git a/sdk/prebuild.ts b/sdk/prebuild.ts index 62cae90..42c13c0 100644 --- a/sdk/prebuild.ts +++ b/sdk/prebuild.ts @@ -36,7 +36,8 @@ for (const [exportPath, exportValue] of Object.entries(exports)) { exportPath === "./tsconfig.base.json" || exportPath === "./agents-guide" || exportPath === "./sdk-docs" || - exportPath.startsWith("./llm-docs") + exportPath.startsWith("./llm-docs") || + exportPath.startsWith("./utils/") ) { continue; } diff --git a/sdk/src/agent.ts b/sdk/src/agent.ts index adddea1..ff31db4 100644 --- a/sdk/src/agent.ts +++ b/sdk/src/agent.ts @@ -1,52 +1,15 @@ -import { type Activity, type Priority } from "./plot"; +import { type Activity, type ActorId, type Priority, type Tag } from "./plot"; +import type { Callback } from "./tools/callbacks"; import type { - Callback, - CallbackContext, CallbackMethods, - Callbacks, -} from "./tools/callbacks"; -import type { Store } from "./tools/store"; -import type { Tasks } from "./tools/tasks"; - -// Type utilities for extracting types from Init method -type PromiseValues = { - [K in keyof T]: T[K] extends Promise ? U : T[K]; -}; - -// Break down InferTools into intermediate steps to avoid deep recursion -type ExtractInitReturn = T extends { - Init: (...args: any[]) => infer R; -} - ? R - : never; - -type ResolveInitTools = PromiseValues>; - -type BuiltInTools = { - callbacks: Callbacks infer I ? I : any>; - store: Store; - tasks: Tasks; -} & {}; // Counter reset with intersection - -// Note: Due to TypeScript limitations with self-referential generic types and static methods, -// this type may not properly infer tools in all cases. Use explicit type casts if needed. -type InferTools = T extends { - Init: (...args: any[]) => any; - new (...args: any[]): any; -} - ? ResolveInitTools & BuiltInTools - : never; - -type InferOptions = T extends { - Init: (tools: any, options?: infer O) => any; -} - ? O - : undefined; + HasInit, + InferOptions, + InferTools, + NoFunctions, + ToolBuilder, +} from "./utils/types"; -type HasInit = { - Init(tools: ToolBuilder, ...args: any[]): any; - new (id: string, tools: any, ...args: any[]): any; -}; +export type { ToolBuilder }; /** * Base class for all agents. @@ -79,7 +42,7 @@ type HasInit = { * } * ``` */ -export abstract class Agent> { +export abstract class Agent { constructor( protected id: string, protected tools: InferTools, @@ -89,17 +52,27 @@ export abstract class Agent> { /** * Creates a persistent callback to a method on this agent. * - * @param functionName - The name of the method to callback - * @param context - Optional context data to pass to the callback - * @returns Promise resolving to a callback token + * ExtraArgs are strongly typed to match the method's signature after the first argument. + * + * @param fn - The method to callback + * @param extraArgs - Additional arguments to pass (type-checked, must be serializable) + * @returns Promise resolving to a persistent callback token + * + * @example + * ```typescript + * const callback = await this.callback(this.onWebhook, "calendar", 123); + * ``` */ protected async callback< - K extends CallbackMethods> & string + K extends CallbackMethods>, + TMethod extends InstanceType[K] = InstanceType[K] >( - functionName: K, - context?: CallbackContext, K> + fn: TMethod, + ...extraArgs: TMethod extends (arg: any, ...rest: infer R) => any + ? NoFunctions + : [] ): Promise { - return this.tools.callbacks.create(functionName, context); + return this.tools.callbacks.create(fn, ...extraArgs); } /** @@ -128,8 +101,8 @@ export abstract class Agent> { * @param args - Optional arguments to pass to the callback * @returns Promise resolving to the callback result */ - protected async run(token: Callback, args?: any): Promise { - return this.tools.callbacks.run(token, args); + protected async run(token: Callback, ...args: []): Promise { + return this.tools.callbacks.run(token, ...args); } /** @@ -146,6 +119,25 @@ export abstract class Agent> { /** * Stores a value in persistent storage. * + * **Important**: Values must be JSON-serializable. Functions, Symbols, and undefined values + * cannot be stored directly. + * + * **For function references**: Use callbacks instead of storing functions directly. + * + * @example + * ```typescript + * // ❌ WRONG: Cannot store functions directly + * await this.set("handler", this.myHandler); + * + * // ✅ CORRECT: Create a callback token first + * const token = await this.callback(this.myHandler, "arg1", "arg2"); + * await this.set("handler_token", token); + * + * // Later, execute the callback + * const token = await this.get("handler_token"); + * await this.run(token, args); + * ``` + * * @template T - The type of value being stored * @param key - The storage key to use * @param value - The value to store (must be JSON-serializable) @@ -221,6 +213,31 @@ export abstract class Agent> { return Promise.resolve(); } + /** + * Called when a new version of the agent is deployed to an existing priority. + * + * This method should contain migration logic for updating old data structures + * or setting up new resources that weren't needed by the previous version. + * It is called with the new version for each active priorityAgent. + * + * @returns Promise that resolves when upgrade is complete + */ + upgrade(): Promise { + return Promise.resolve(); + } + + /** + * Called when the agent is removed from a priority. + * + * This method should contain cleanup logic such as removing webhooks, + * cleaning up external resources, or performing final data operations. + * + * @returns Promise that resolves when deactivation is complete + */ + deactivate(): Promise { + return Promise.resolve(); + } + /** * Called when an activity needs to be processed by this agent. * @@ -236,8 +253,8 @@ export abstract class Agent> { _activity: Activity, _changes?: { previous: Activity; - tagsAdded: Record; - tagsRemoved: Record; + tagsAdded: Record; + tagsRemoved: Record; } ): Promise { return Promise.resolve(); @@ -251,10 +268,6 @@ export abstract class Agent> { */ export abstract class ITool {} -export type ToolConstructor = - | (abstract new (id: string, tools: any, options?: any) => T) - | (new (id: string, tools: any, options?: any) => T); - /** * Base class for regular tools. * @@ -279,7 +292,7 @@ export type ToolConstructor = * } * ``` */ -export abstract class Tool> implements ITool { +export abstract class Tool implements ITool { constructor( protected id: string, protected tools: InferTools, @@ -289,17 +302,27 @@ export abstract class Tool> implements ITool { /** * Creates a persistent callback to a method on this tool. * - * @param functionName - The name of the method to callback - * @param context - Optional context data to pass to the callback - * @returns Promise resolving to a callback token + * ExtraArgs are strongly typed to match the method's signature after the first argument. + * + * @param fn - The method to callback + * @param extraArgs - Additional arguments to pass (type-checked, must be serializable) + * @returns Promise resolving to a persistent callback token + * + * @example + * ```typescript + * const callback = await this.callback(this.onWebhook, "calendar", 123); + * ``` */ protected async callback< - K extends CallbackMethods> & string + K extends CallbackMethods>, + TMethod extends InstanceType[K] = InstanceType[K] >( - functionName: K, - context: CallbackContext, K> + fn: TMethod, + ...extraArgs: TMethod extends (arg: any, ...rest: infer R) => any + ? NoFunctions + : [] ): Promise { - return this.tools.callbacks.create(functionName, context); + return this.tools.callbacks.create(fn, ...extraArgs); } /** @@ -346,6 +369,25 @@ export abstract class Tool> implements ITool { /** * Stores a value in persistent storage. * + * **Important**: Values must be JSON-serializable. Functions, Symbols, and undefined values + * cannot be stored directly. + * + * **For function references**: Use callbacks instead of storing functions directly. + * + * @example + * ```typescript + * // ❌ WRONG: Cannot store functions directly + * await this.set("handler", this.myHandler); + * + * // ✅ CORRECT: Create a callback token first + * const token = await this.callback(this.myHandler, "arg1", "arg2"); + * await this.set("handler_token", token); + * + * // Later, execute the callback + * const token = await this.get("handler_token"); + * await this.run(token, args); + * ``` + * * @template T - The type of value being stored * @param key - The storage key to use * @param value - The value to store (must be JSON-serializable) @@ -407,34 +449,81 @@ export abstract class Tool> implements ITool { protected async cancelAllTasks(): Promise { return this.tools.tasks.cancelAllTasks(); } -} -/** - * Interface for accessing tool dependencies. - * - * This interface provides type-safe access to tools that have been declared - * as dependencies in the agent.json or tool.json configuration files. - */ -export interface ToolBuilder { - /** - * Initializes a tool instance by its class reference, returning a promise. - * - * @template T - The expected type of the tool - * @template O - The options type expected by the tool's Init method - * @param ToolClass - The tool class reference with Init method - * @param options - Optional options to pass to the tool's Init method - * @returns Promise resolving to the tool instance - * @throws When the tool is not found or not properly configured - */ - init< - T extends ITool, - O = T extends { Init: (tools: any, options?: infer Opt) => any } - ? Opt - : never - >( - ToolClass: ToolConstructor & { - Init: (tools: ToolBuilder, options?: O) => any; - }, - options?: O - ): Promise; + /** + * Called before the agent's activate method, starting from the deepest tool dependencies. + * + * This method is called in a depth-first manner, with the deepest dependencies + * being called first, bubbling up to the top-level tools before the agent's + * activate method is called. + * + * @param _priority - The priority context containing the priority ID + * @returns Promise that resolves when pre-activation is complete + */ + preActivate(_priority: Priority): Promise { + return Promise.resolve(); + } + + /** + * Called after the agent's activate method, starting from the top-level tools. + * + * This method is called in reverse order, with top-level tools being called + * first, then cascading down to the deepest dependencies. + * + * @param _priority - The priority context containing the priority ID + * @returns Promise that resolves when post-activation is complete + */ + postActivate(_priority: Priority): Promise { + return Promise.resolve(); + } + + /** + * Called before the agent's upgrade method, starting from the deepest tool dependencies. + * + * This method is called in a depth-first manner, with the deepest dependencies + * being called first, bubbling up to the top-level tools before the agent's + * upgrade method is called. + * + * @returns Promise that resolves when pre-upgrade is complete + */ + preUpgrade(): Promise { + return Promise.resolve(); + } + + /** + * Called after the agent's upgrade method, starting from the top-level tools. + * + * This method is called in reverse order, with top-level tools being called + * first, then cascading down to the deepest dependencies. + * + * @returns Promise that resolves when post-upgrade is complete + */ + postUpgrade(): Promise { + return Promise.resolve(); + } + + /** + * Called before the agent's deactivate method, starting from the deepest tool dependencies. + * + * This method is called in a depth-first manner, with the deepest dependencies + * being called first, bubbling up to the top-level tools before the agent's + * deactivate method is called. + * + * @returns Promise that resolves when pre-deactivation is complete + */ + preDeactivate(): Promise { + return Promise.resolve(); + } + + /** + * Called after the agent's deactivate method, starting from the top-level tools. + * + * This method is called in reverse order, with top-level tools being called + * first, then cascading down to the deepest dependencies. + * + * @returns Promise that resolves when post-deactivation is complete + */ + postDeactivate(): Promise { + return Promise.resolve(); + } } diff --git a/sdk/src/common/calendar.ts b/sdk/src/common/calendar.ts index ddd824d..62c98e6 100644 --- a/sdk/src/common/calendar.ts +++ b/sdk/src/common/calendar.ts @@ -1,4 +1,4 @@ -import type { ActivityLink, Callback } from "../index"; +import type { Activity, ActivityLink } from "../index"; /** * Represents successful calendar authorization. @@ -100,10 +100,16 @@ export interface CalendarTool { /** * Initiates the authorization flow for the calendar service. * - * @param callback - Function to call when auth completes. The ActivityLink is passed to the callback. + * @param callback - Function receiving (auth, ...extraArgs) when auth completes + * @param extraArgs - Additional arguments to pass to the callback (type-checked) * @returns Promise resolving to an ActivityLink to initiate the auth flow */ - requestAuth(callback: Callback): Promise; + requestAuth any>( + callback: TCallback, + ...extraArgs: TCallback extends (auth: any, ...rest: infer R) => any + ? R + : [] + ): Promise; /** * Retrieves the list of calendars accessible to the authenticated user. @@ -127,16 +133,18 @@ export interface CalendarTool { * * @param authToken - Authorization token for calendar access * @param calendarId - ID of the calendar to sync - * @param callback - Function to call for each synced event - * @param options - Optional sync configuration + * @param callback - Function receiving (activity, ...extraArgs) for each synced event + * @param extraArgs - Additional arguments to pass to the callback (type-checked) * @returns Promise that resolves when sync setup is complete * @throws When auth token is invalid or calendar doesn't exist */ - startSync( + startSync any>( authToken: string, calendarId: string, - callback: Callback, - options?: SyncOptions + callback: TCallback, + ...extraArgs: TCallback extends (activity: any, ...rest: infer R) => any + ? R + : [] ): Promise; /** diff --git a/sdk/src/plot.ts b/sdk/src/plot.ts index 85c9223..d0193ee 100644 --- a/sdk/src/plot.ts +++ b/sdk/src/plot.ts @@ -2,11 +2,19 @@ import { type Tag } from "./tag"; export { Tag } from "./tag"; +/** + * Represents a unique user, contact, or agent in Plot. + * + * Note contacts (i.e. people not using Plot) are also represented by ActorIds. They may be + * people interacting with other services that are connected. + */ +export type ActorId = string & { readonly __brand: "ActorId" }; + /** * Represents a priority context within Plot. * - * Priorities are organizational units that group related activities and agents. - * They serve as the primary context for agent activation and activity management. + * Priorities are similar to projects in other apps. All Activity is in a Priority. + * Priorities can be nested. */ export type Priority = { /** Unique identifier for the priority */ @@ -221,7 +229,7 @@ export type Activity = { /** Information about who created the activity */ author: { /** Unique identifier for the author */ - id: string; + id: ActorId; /** Display name for the author */ name: string | null; /** Type of author (User, Contact, or Agent) */ @@ -284,7 +292,7 @@ export type Activity = { /** Reference to the external system that created this activity */ source: ActivitySource | null; /** Tags attached to this activity. Maps tag ID to array of actor IDs who added that tag. */ - tags: Partial> | null; + tags: Partial> | null; }; /** diff --git a/sdk/src/tools/callbacks.ts b/sdk/src/tools/callbacks.ts index 0b08ce1..cdf4044 100644 --- a/sdk/src/tools/callbacks.ts +++ b/sdk/src/tools/callbacks.ts @@ -1,4 +1,8 @@ -import { ITool, type ToolBuilder } from ".."; +import { ITool } from ".."; +import type { CallbackMethods, NoFunctions, NonFunction } from "../utils/types"; + +// Re-export types for consumers +export type { CallbackMethods, NoFunctions, NonFunction }; /** * Represents a callback token for persistent function references. @@ -10,40 +14,15 @@ import { ITool, type ToolBuilder } from ".."; * * @example * ```typescript - * const callback = await this.callback.create("onCalendarSelected", { - * calendarId: "primary", - * provider: "google" - * }); + * const callback = await this.callback(this.onCalendarSelected, "primary", "google"); * ``` */ export type Callback = string & { readonly __brand: "Callback" }; -/** - * Extracts method names from a type that match the callback signature. - * Callback methods must accept (args: any, context?: any) and return Promise - * or accept (args: any) and return Promise. - */ -export type CallbackMethods = { - [K in keyof T]: T[K] extends (args: any, context?: any) => Promise - ? K - : never; -}[keyof T]; - -/** - * Extracts the context parameter type for a specific callback method. - * Returns undefined if the method doesn't accept a context parameter. - */ -export type CallbackContext = T[K] extends ( - args: any, - context: infer C -) => any - ? C - : undefined; - /** * Built-in tool for creating and managing persistent callback references. * - * The CallbackTool enables agents and tools to create callback links that persist + * The Callbacks tool enables agents and tools to create callback links that persist * across worker invocations and restarts. This is essential for webhook handlers, * scheduled operations, and user interaction flows that need to survive runtime * boundaries. @@ -58,44 +37,54 @@ export type CallbackContext = T[K] extends ( * - User interaction links (ActivityLinkType.callback) * - Cross-tool communication that survives restarts * - * **Security note:** Callbacks are hardcoded to target the tool's parent for security. - * * **Type Safety:** - * For full type safety, cast the callback tool to include your agent/tool type: - * `tools.get(CallbackTool) as CallbackTool` - * This enables autocomplete for method names and type-checked context parameters. + * Callbacks are fully type-safe - extraArgs are type-checked against the function signature. * * @example * ```typescript * class MyTool extends Tool { * async setupWebhook() { * // Using built-in callback method (recommended) - * const callback = await this.callback("handleWebhook", { - * webhookType: "calendar" - * }); - * - * // Use callback in webhook URL or activity link + * const callback = await this.callback(this.handleWebhook, "calendar"); * return `https://api.plot.day/webhook/${callback}`; * } * - * async handleWebhook(data: any, context?: { webhookType: string }) { - * console.log("Webhook received:", data, context); + * async handleWebhook(data: any, webhookType: string) { + * console.log("Webhook received:", data, webhookType); * } * } * ``` */ export abstract class Callbacks extends ITool { /** - * Creates a persistent callback to the tool's parent. - * Returns a callback token that can be used to call the callback later. + * Creates a persistent callback to a method on TParent (the current class). + * ExtraArgs are strongly typed to match the function signature after the first arg. + * + * @param fn - The function to callback on TParent + * @param extraArgs - Additional arguments to pass to the function (type-checked, must be serializable) + * @returns Promise resolving to a persistent callback token + */ + abstract create< + K extends CallbackMethods, + TFn extends TParent[K] = TParent[K] + >( + _fn: TFn, + ..._extraArgs: TFn extends (arg: any, ...rest: infer R) => any + ? NoFunctions + : [] + ): Promise; + + /** + * Creates a persistent callback to a function from the parent agent/tool. + * Use this when the callback function is passed in from outside this class. * - * @param functionName - The name of the function to call on the parent tool/agent - * @param context - Optional context data to pass to the callback function (type-checked when TParent is specified) - * @returns Promise resolving to a callback token + * @param fn - The function to callback + * @param extraArgs - Additional arguments to pass to the function (must be serializable, validated at runtime) + * @returns Promise resolving to a persistent callback token */ - abstract create>( - _functionName: K, - _context?: CallbackContext + abstract createParent( + _fn: Function, + ..._extraArgs: NonFunction[] ): Promise; /** @@ -120,5 +109,5 @@ export abstract class Callbacks extends ITool { * @param args - Optional arguments to pass to the callback function * @returns Promise resolving to the callback result */ - abstract run(_callback: Callback, _args?: any): Promise; + abstract run(_callback: Callback, ..._args: any[]): Promise; } diff --git a/sdk/src/tools/integrations.ts b/sdk/src/tools/integrations.ts index 62d16e6..2f790f2 100644 --- a/sdk/src/tools/integrations.ts +++ b/sdk/src/tools/integrations.ts @@ -1,4 +1,5 @@ -import { type ActivityLink, type Callback, ITool, ToolBuilder } from ".."; +import { type ActivityLink, ITool, type ToolBuilder } from ".."; +import { type NoFunctions } from "./callbacks"; /** * Built-in tool for managing OAuth authentication flows. @@ -44,22 +45,28 @@ export abstract class Integrations extends ITool { * * Creates an authentication link that users can click to authorize access * to the specified provider with the requested scopes. When authorization - * completes, the optional callback will be invoked with the results. + * completes, the callback will be invoked with the Authorization and any extraArgs. * * @param auth - Authentication configuration * @param auth.provider - The OAuth provider to authenticate with * @param auth.level - The authorization level (priority-scoped or user-scoped) * @param auth.scopes - Array of OAuth scopes to request - * @param callback - Callback receiving an Authorization + * @param callback - Function receiving (authorization, ...extraArgs) + * @param extraArgs - Additional arguments to pass to the callback (type-checked, must be serializable) * @returns Promise resolving to an ActivityLink for the auth flow */ - abstract request( + abstract request< + TCallback extends (auth: Authorization, ...args: any[]) => any + >( _auth: { provider: AuthProvider; level: AuthLevel; scopes: string[]; }, - _callback: Callback + _callback: TCallback, + ..._extraArgs: TCallback extends (auth: any, ...rest: infer R) => any + ? NoFunctions + : [] ): Promise; /** diff --git a/sdk/src/tools/network.ts b/sdk/src/tools/network.ts index 20493a4..aedc2ea 100644 --- a/sdk/src/tools/network.ts +++ b/sdk/src/tools/network.ts @@ -117,15 +117,21 @@ export abstract class Network extends ITool { /** * Creates a new webhook endpoint. * - * Generates a unique HTTP endpoint that will invoke the specified callback - * function on the parent tool/agent when requests are received. The context - * data will be passed to the callback along with the request information. + * Generates a unique HTTP endpoint that will invoke the callback function + * when requests are received. The callback receives the WebhookRequest plus any extraArgs. * - * @param callbackName - Name of the function to call on the parent when webhook is triggered - * @param context - Optional context data to pass to the callback function + * @param callback - Function receiving (request, ...extraArgs) + * @param extraArgs - Additional arguments to pass to the callback (type-checked) * @returns Promise resolving to the webhook URL */ - abstract createWebhook(_callbackName: string, _context?: any): Promise; + abstract createWebhook< + TCallback extends (request: WebhookRequest, ...args: any[]) => any + >( + _callback: TCallback, + ..._extraArgs: TCallback extends (req: any, ...rest: infer R) => any + ? R + : [] + ): Promise; /** * Deletes an existing webhook endpoint. diff --git a/sdk/src/tools/tasks.ts b/sdk/src/tools/tasks.ts index bd4983a..fd7fa72 100644 --- a/sdk/src/tools/tasks.ts +++ b/sdk/src/tools/tasks.ts @@ -1,4 +1,4 @@ -import { ITool, type ToolBuilder } from ".."; +import { ITool } from ".."; import type { Callback } from "./callbacks"; /** @@ -63,7 +63,7 @@ export abstract class Tasks extends ITool { * in an isolated execution environment with limited resources. Use this * for breaking up long-running operations into manageable chunks. * - * @param callback - The callback token created with `this.callback()` + * @param callback - Callback created with `this.callback()` * @param options - Optional configuration for the execution * @param options.runAt - If provided, schedules execution at this time; otherwise runs immediately * @returns Promise resolving to a cancellation token (only for scheduled executions) diff --git a/sdk/src/utils/types.ts b/sdk/src/utils/types.ts new file mode 100644 index 0000000..b385840 --- /dev/null +++ b/sdk/src/utils/types.ts @@ -0,0 +1,140 @@ +/** + * Internal type utilities for SDK implementation. + * + * This file contains advanced TypeScript type utilities used internally + * by the SDK to provide type-safe APIs. Most developers don't need to + * reference these types directly - they work behind the scenes to power + * the Agent and Tool APIs. + * + * @internal + */ + +import type { ITool } from "../agent"; +import type { Callbacks } from "../tools/callbacks"; +import type { Store } from "../tools/store"; +import type { Tasks } from "../tools/tasks"; + +// ============================================================================ +// Type utilities from agent.ts +// ============================================================================ + +/** + * Unwraps Promise types to their resolved values. + * Converts { foo: Promise } to { foo: string } + */ +export type PromiseValues = { + [K in keyof T]: T[K] extends Promise ? U : T[K]; +}; + +/** + * Extracts the return type from a static Init method. + */ +export type ExtractInitReturn = T extends { + Init: (...args: any[]) => infer R; +} + ? R + : never; + +/** + * Resolves the tools returned by Init, unwrapping any Promises. + */ +export type ResolveInitTools = PromiseValues>; + +/** + * Built-in tools available to all agents and tools. + */ +export type BuiltInTools = { + callbacks: Callbacks infer I ? I : any>; + store: Store; + tasks: Tasks; +} & {}; // Counter reset with intersection + +/** + * Infers the complete set of tools available to an agent or tool, + * combining tools declared in Init with built-in tools. + */ +export type InferTools = T extends { + Init: (...args: any[]) => any; + new (...args: any[]): any; +} + ? ResolveInitTools & BuiltInTools + : never; + +/** + * Infers the options type from a static Init method's second parameter. + */ +export type InferOptions = T extends { + Init: (tools: any, options?: infer O) => any; +} + ? O + : undefined; + +/** + * Constraint for types that have both Init static method and constructor. + */ +export type HasInit = { + Init(tools: ToolBuilder, ...args: any[]): any; + new (id: string, tools: any, ...args: any[]): any; +}; + +/** + * Constructor type for Tool classes (can be abstract or concrete). + */ +export type ToolConstructor = + | (abstract new (id: string, tools: any, options?: any) => T) + | (new (id: string, tools: any, options?: any) => T); + +/** + * Interface for accessing tool dependencies. + * Used in static Init methods to initialize tool dependencies. + */ +export interface ToolBuilder { + /** + * Initializes a tool instance by its class reference, returning a promise. + * + * @template T - The expected type of the tool + * @template O - The options type expected by the tool's Init method + * @param ToolClass - The tool class reference with Init method + * @param options - Optional options to pass to the tool's Init method + * @returns Promise resolving to the tool instance + * @throws When the tool is not found or not properly configured + */ + init< + T extends ITool, + O = T extends { Init: (tools: any, options?: infer Opt) => any } + ? Opt + : never + >( + ToolClass: ToolConstructor & { + Init: (tools: ToolBuilder, options?: O) => any; + }, + options?: O + ): Promise; +} + +// ============================================================================ +// Type utilities from callbacks.ts +// ============================================================================ + +/** + * Represents any non-function type. + */ +export type NonFunction = Exclude any>; + +/** + * Filters out function properties from a type, keeping only data properties. + * For arrays, keeps the array structure while filtering functions from elements. + */ +export type NoFunctions = T extends (...args: any[]) => any + ? never + : T extends object + ? { [K in keyof T]: T[K] extends (...args: any[]) => any ? never : T[K] } + : T; + +/** + * Extracts method names from a type that are functions. + * Used to type-check callback method references. + */ +export type CallbackMethods = { + [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never; +}[keyof T]; diff --git a/tools/google-calendar/src/google-calendar.ts b/tools/google-calendar/src/google-calendar.ts index e742a3d..9c822cb 100644 --- a/tools/google-calendar/src/google-calendar.ts +++ b/tools/google-calendar/src/google-calendar.ts @@ -1,4 +1,5 @@ import { + type Activity, type ActivityLink, type NewActivity, Tool, @@ -8,7 +9,6 @@ import { type Calendar, type CalendarAuth, type CalendarTool, - type SyncOptions, } from "@plotday/sdk/common/calendar"; import { type Callback } from "@plotday/sdk/tools/callbacks"; import { @@ -27,11 +27,6 @@ import { transformGoogleEvent, } from "./google-api"; -type AuthSuccessContext = { - authToken: string; - callbackToken: Callback; -}; - /** * Google Calendar integration tool. * @@ -112,23 +107,22 @@ export class GoogleCalendar }; } - async requestAuth(callback: Callback): Promise { + async requestAuth< + TCallback extends (auth: CalendarAuth, ...args: any[]) => any + >(callback: TCallback, ...extraArgs: any[]): Promise { + console.log("Requesting Google Calendar auth"); const calendarScopes = [ "https://www.googleapis.com/auth/calendar.calendarlist.readonly", "https://www.googleapis.com/auth/calendar.events", ]; - // Generate opaque token for this.integrationsorization + // Generate opaque token for authorization const authToken = crypto.randomUUID(); - // Use the provided callback token - const callbackToken = callback; - - // Create callback for auth completion - const authCallback = await this.callback("onAuthSuccess", { - authToken, - callbackToken, - } satisfies AuthSuccessContext); + const callbackToken = await this.tools.callbacks.createParent( + callback, + ...extraArgs + ); // Request auth and return the activity link return await this.tools.integrations.request( @@ -137,7 +131,9 @@ export class GoogleCalendar level: AuthLevel.User, scopes: calendarScopes, }, - authCallback + this.onAuthSuccess, + authToken, + callbackToken ); } @@ -158,6 +154,7 @@ export class GoogleCalendar } async getCalendars(authToken: string): Promise { + console.log("Fetching Google Calendar list"); const api = await this.getApi(authToken); const data = (await api.call( "GET", @@ -170,6 +167,7 @@ export class GoogleCalendar primary?: boolean; }>; }; + console.log("Got Google Calendar list", data.items); return data.items.map((item) => ({ id: item.id, @@ -179,16 +177,22 @@ export class GoogleCalendar })); } - async startSync( + async startSync< + TCallback extends (activity: Activity, ...args: any[]) => any + >( authToken: string, calendarId: string, - callback: Callback, - options?: SyncOptions + callback: TCallback, + ...extraArgs: any[] ): Promise { console.log("Saving callback"); - // Store the callback token - await this.set("event_callback_token", callback); + // Create callback token for parent + const callbackToken = await this.tools.callbacks.createParent( + callback, + ...extraArgs + ); + await this.set("event_callback_token", callbackToken); console.log("Setting up watch"); // Setup webhook for this calendar @@ -196,8 +200,8 @@ export class GoogleCalendar // Start initial sync const now = new Date(); - const min = options?.timeMin || new Date(now.getFullYear() - 2, 0, 1); - const max = options?.timeMax || new Date(now.getFullYear() + 1, 11, 31); + const min = new Date(now.getFullYear() - 2, 0, 1); + const max = new Date(now.getFullYear() + 1, 11, 31); const initialState: SyncState = { calendarId, @@ -210,16 +214,17 @@ export class GoogleCalendar console.log("Starting sync"); // Start sync batch using run tool for long-running operation - const syncCallback = await this.callback("syncBatch", { - calendarId, - batchNumber: 1, - mode: "full", + const syncCallback = await this.callback( + this.syncBatch, + 1, + "full", authToken, - }); + calendarId + ); await this.run(syncCallback); } - async stopSync(authToken: string, calendarId: string): Promise { + async stopSync(_authToken: string, calendarId: string): Promise { // Stop webhook const watchData = await this.get(`calendar_watch_${calendarId}`); if (watchData) { @@ -237,11 +242,9 @@ export class GoogleCalendar opaqueAuthToken: string ): Promise { const webhookUrl = await this.tools.network.createWebhook( - "onCalendarWebhook", - { - calendarId, - authToken: opaqueAuthToken, - } + this.onCalendarWebhook, + calendarId, + opaqueAuthToken ); // Check if webhook URL is localhost @@ -278,17 +281,13 @@ export class GoogleCalendar console.log("Calendar watch setup complete", { watchId, calendarId }); } - async syncBatch({ - calendarId, - batchNumber, - mode, - authToken, - }: { - calendarId: string; - batchNumber: number; - mode: "full" | "incremental"; - authToken: string; - }): Promise { + async syncBatch( + _args: any, + batchNumber: number, + mode: "full" | "incremental", + authToken: string, + calendarId: string + ): Promise { console.log( `Starting Google Calendar sync batch ${batchNumber} (${mode}) for calendar ${calendarId}` ); @@ -320,12 +319,13 @@ export class GoogleCalendar await this.set(`sync_state_${calendarId}`, result.state); if (result.state.more) { - const syncCallback = await this.callback("syncBatch", { - calendarId, - batchNumber: batchNumber + 1, + const syncCallback = await this.callback( + this.syncBatch, + batchNumber + 1, mode, authToken, - }); + calendarId + ); await this.run(syncCallback); } else { console.log( @@ -364,9 +364,7 @@ export class GoogleCalendar const activityData = transformGoogleEvent(event, calendarId); // Convert to full Activity and call callback - const callbackToken = await this.get( - "event_callback_token" - ); + const callbackToken = await this.get("event_callback_token"); if (callbackToken && activityData.type) { const activity: NewActivity = { type: activityData.type, @@ -387,7 +385,7 @@ export class GoogleCalendar source: activityData.source || null, }; - await this.run(callbackToken, activity); + await this.run(callbackToken as any, activity); } } } catch (error) { @@ -412,7 +410,7 @@ export class GoogleCalendar const activityData = transformGoogleEvent(event, calendarId); - const callbackToken = await this.get("event_callback_token"); + const callbackToken = await this.get("event_callback_token"); if (callbackToken && activityData.type) { const activity: NewActivity = { type: activityData.type, @@ -433,18 +431,19 @@ export class GoogleCalendar source: activityData.source || null, }; - await this.run(callbackToken, activity); + await this.run(callbackToken as any, activity); } } async onCalendarWebhook( request: WebhookRequest, - context: any + calendarId: string, + authToken: string ): Promise { console.log("Received calendar webhook notification", { headers: request.headers, params: request.params, - calendarId: context.calendarId, + calendarId, }); // Validate webhook authenticity @@ -455,9 +454,7 @@ export class GoogleCalendar throw new Error("Invalid webhook headers"); } - const watchData = await this.get( - `calendar_watch_${context.calendarId}` - ); + const watchData = await this.get(`calendar_watch_${calendarId}`); if (!watchData || watchData.watchId !== channelId) { console.warn("Unknown or expired webhook notification"); @@ -473,7 +470,7 @@ export class GoogleCalendar } // Trigger incremental sync - await this.startIncrementalSync(context.calendarId, context.authToken); + await this.startIncrementalSync(calendarId, authToken); } private async startIncrementalSync( @@ -493,29 +490,32 @@ export class GoogleCalendar }; await this.set(`sync_state_${calendarId}`, incrementalState); - const syncCallback = await this.callback("syncBatch", { - calendarId, - batchNumber: 1, - mode: "incremental", + const syncCallback = await this.callback( + this.syncBatch, + 1, + "incremental", authToken, - }); + calendarId + ); await this.run(syncCallback); } async onAuthSuccess( authResult: Authorization, - context: AuthSuccessContext + authToken: string, + callback: Callback ): Promise { // Store the actual auth token using opaque token as key - await this.set(`authorization:${context.authToken}`, authResult); + await this.set(`authorization:${authToken}`, authResult); const authSuccessResult: CalendarAuth = { - authToken: context.authToken, + authToken, }; - await this.run(context.callbackToken, authSuccessResult); + + await this.run(callback, authSuccessResult); // Clean up the callback token - await this.clear(`auth_callback_token:${context.authToken}`); + await this.clear(`auth_callback_token:${authToken}`); } } diff --git a/tools/google-contacts/src/google-contacts.ts b/tools/google-contacts/src/google-contacts.ts index 87efb77..46a969c 100644 --- a/tools/google-contacts/src/google-contacts.ts +++ b/tools/google-contacts/src/google-contacts.ts @@ -4,6 +4,7 @@ import { AuthLevel, AuthProvider, type AuthToken, + type Authorization, Integrations, } from "@plotday/sdk/tools/integrations"; @@ -259,20 +260,21 @@ export default class GoogleContacts }; } - async requestAuth(callback: Callback): Promise { + async requestAuth< + TCallback extends (auth: ContactAuth, ...args: any[]) => any + >(callback: TCallback, ...extraArgs: any[]): Promise { const contactsScopes = [ "https://www.googleapis.com/auth/contacts.readonly", "https://www.googleapis.com/auth/contacts.other.readonly", ]; const opaqueToken = crypto.randomUUID(); - await this.set(`auth_callback_token:${opaqueToken}`, callback); - // Create callback for auth completion - const authCallback = await this.callback("onAuthSuccess", { - toolName: "google-contacts", - opaqueToken, - }); + // Create callback token for parent + const callbackToken = await this.tools.callbacks.createParent( + callback, + ...extraArgs + ); // Request auth and return the activity link return await this.tools.integrations.request( @@ -281,7 +283,9 @@ export default class GoogleContacts level: AuthLevel.User, scopes: contactsScopes, }, - authCallback + this.onAuthSuccess, + opaqueToken, + callbackToken ); } @@ -303,7 +307,13 @@ export default class GoogleContacts return result.contacts; } - async startSync(authToken: string, callback: Callback): Promise { + async startSync< + TCallback extends (contacts: Contact[], ...args: any[]) => any + >( + authToken: string, + callback: TCallback, + ...extraArgs: any[] + ): Promise { const storedAuthToken = await this.get( `auth_token:${authToken}` ); @@ -313,8 +323,12 @@ export default class GoogleContacts ); } - // Register the callback - await this.set(`contacts_callback_token:${authToken}`, callback); + // Create callback token for parent + const callbackToken = await this.tools.callbacks.createParent( + callback, + ...extraArgs + ); + await this.set(`contacts_callback_token:${authToken}`, callbackToken); // Start initial sync const initialState: ContactSyncState = { @@ -324,11 +338,8 @@ export default class GoogleContacts await this.set(`sync_state:${authToken}`, initialState); // Start sync batch using run tool for long-running operation - const sync = await this.callback("syncBatch", { - batchNumber: 1, - authToken, - }); - await this.runTask(sync); + const syncCallback = await this.callback(this.syncBatch, 1, authToken); + await this.run(syncCallback); } async stopSync(authToken: string): Promise { @@ -337,11 +348,11 @@ export default class GoogleContacts await this.clear(`contacts_callback_token:${authToken}`); } - async syncBatch(context: { - batchNumber: number; - authToken: string; - }): Promise { - const { batchNumber, authToken } = context; + async syncBatch( + _args: any, + batchNumber: number, + authToken: string + ): Promise { console.log(`Starting Google Contacts sync batch ${batchNumber}`); try { @@ -376,11 +387,12 @@ export default class GoogleContacts await this.set(`sync_state:${authToken}`, result.state); if (result.state.more) { - const callback = await this.callback("syncBatch", { - batchNumber: batchNumber + 1, - authToken, - }); - await this.runTask(callback); + const nextCallback = await this.callback( + this.syncBatch, + batchNumber + 1, + authToken + ); + await this.run(nextCallback); } else { console.log( `Google Contacts sync completed after ${batchNumber} batches` @@ -398,40 +410,28 @@ export default class GoogleContacts contacts: Contact[], authToken: string ): Promise { - const callbackToken = await this.get( + const callbackToken = await this.get( `contacts_callback_token:${authToken}` ); if (callbackToken) { - await this.run(callbackToken, contacts); + await this.run(callbackToken as any, contacts); } } - async onAuthSuccess(authResult: any, context: any): Promise { + async onAuthSuccess( + authResult: Authorization, + opaqueToken: string, + callbackToken: Callback + ): Promise { console.log("Google Contacts authentication successful", authResult); - // Extract opaque token from context - const opaqueToken = context?.opaqueToken; - if (!opaqueToken) { - console.error("No opaque token found in auth context"); - return; - } - // Store the actual auth token using opaque token as key await this.set(`auth_token:${opaqueToken}`, authResult); - // Retrieve and call the stored callback - const callbackToken = await this.get( - `auth_callback_token:${opaqueToken}` - ); - if (callbackToken) { - const authSuccessResult: ContactAuth = { - authToken: opaqueToken, - }; - - await this.run(callbackToken, authSuccessResult); + const authSuccessResult: ContactAuth = { + authToken: opaqueToken, + }; - // Clean up the callback token - await this.clear(`auth_callback_token:${opaqueToken}`); - } + await this.run(callbackToken, authSuccessResult); } } diff --git a/tools/google-contacts/src/types.ts b/tools/google-contacts/src/types.ts index 0c6301d..0583d61 100644 --- a/tools/google-contacts/src/types.ts +++ b/tools/google-contacts/src/types.ts @@ -11,19 +11,21 @@ export type ContactAuth = { }; export interface GoogleContacts extends ITool { - requestAuth( - callbackFunctionName: string, - callbackContext?: any + requestAuth any>( + callback: TCallback, + ...extraArgs: TCallback extends (auth: any, ...rest: infer R) => any + ? R + : [] ): Promise; getContacts(authToken: string): Promise; - startSync( + startSync any>( authToken: string, - callbackFunctionName: string, - options?: { - context?: any; - } + callback: TCallback, + ...extraArgs: TCallback extends (contacts: any, ...rest: infer R) => any + ? R + : [] ): Promise; stopSync(authToken: string): Promise; diff --git a/tools/outlook-calendar/src/outlook-calendar.ts b/tools/outlook-calendar/src/outlook-calendar.ts index c016963..9a3b478 100644 --- a/tools/outlook-calendar/src/outlook-calendar.ts +++ b/tools/outlook-calendar/src/outlook-calendar.ts @@ -2,6 +2,7 @@ import { type Activity, type ActivityLink, ActivityType, + type NewActivity, Tool, type ToolBuilder, } from "@plotday/sdk"; @@ -9,7 +10,6 @@ import type { Calendar, CalendarAuth, CalendarTool, - SyncOptions, } from "@plotday/sdk/common/calendar"; import { type Callback } from "@plotday/sdk/tools/callbacks"; import { @@ -20,10 +20,6 @@ import { } from "@plotday/sdk/tools/integrations"; import { Network, type WebhookRequest } from "@plotday/sdk/tools/network"; -type AuthSuccessContext = { - token: string; -}; - // Import types from the existing outlook.ts file type CalendarConfig = { outlookClientId: string; @@ -111,10 +107,10 @@ function parseOutlookRecurrenceCount(recurrenceData: any): number | null { // Simplified version of the cal.* functions from outlook.ts const outlookApi = { sync: async ( - config: CalendarConfig, - credentials: CalendarCredentials, + _config: CalendarConfig, + _credentials: CalendarCredentials, syncState: OutlookSyncState, - limit: number + _limit: number ) => { // This would contain the actual Outlook API sync logic // For now, return empty result @@ -124,7 +120,7 @@ const outlookApi = { }; }, - transform: (rawEvent: RawEvent, accountEmail: string): Event => { + transform: (rawEvent: RawEvent, _accountEmail: string): Event => { // This would contain the transformation logic from outlook.ts const event = rawEvent.data; const id = rawEvent.id; @@ -141,10 +137,10 @@ const outlookApi = { }, watch: async ( - config: CalendarConfig, - credentials: CalendarCredentials, - calendarId: string, - existingWatch?: { watchId: string; watchSecret: string } + _config: CalendarConfig, + _credentials: CalendarCredentials, + _calendarId: string, + _existingWatch?: { watchId: string; watchSecret: string } ) => { // This would contain the webhook setup logic return { @@ -233,17 +229,17 @@ export class OutlookCalendar }; } - async requestAuth(callback: Callback): Promise { - // Generate opaque token for this.integrations request + async requestAuth< + TCallback extends (auth: CalendarAuth, ...args: any[]) => any + >(callback: TCallback, ...extraArgs: any[]): Promise { + // Generate opaque token for authorization const token = crypto.randomUUID(); - // Store the callback token for auth completion - await this.set(`auth_callback_token:${token}`, callback); - - // Create callback for auth completion - const authCallback = await this.callback("onAuthSuccess", { - token, - } satisfies AuthSuccessContext); + // Create callback token for parent + const callbackToken = await this.tools.callbacks.createParent( + callback, + ...extraArgs + ); // Request Microsoft authentication and return the activity link return await this.tools.integrations.request( @@ -252,7 +248,9 @@ export class OutlookCalendar level: AuthLevel.User, scopes: ["https://graph.microsoft.com/calendars.readwrite"], }, - authCallback + this.onAuthSuccess, + token, + callbackToken ); } @@ -303,27 +301,34 @@ export class OutlookCalendar ]; } - async startSync( + async startSync< + TCallback extends (activity: Activity, ...args: any[]) => any + >( authToken: string, calendarId: string, - callback: Callback, - options?: SyncOptions + callback: TCallback, + ...extraArgs: any[] ): Promise { - // Store the callback token - await this.set("event_callback_token", callback); + // Create callback token for parent + const callbackToken = await this.tools.callbacks.createParent( + callback, + ...extraArgs + ); + await this.set("event_callback_token", callbackToken); // Setup webhook for this calendar await this.setupOutlookWatch(authToken, calendarId, authToken); // Start sync batch using run tool for long-running operation - const syncCallback = await this.callback("syncOutlookBatch", { + const syncCallback = await this.callback( + this.syncOutlookBatch, calendarId, - authToken, - }); + authToken + ); await this.run(syncCallback); } - async stopSync(authToken: string, calendarId: string): Promise { + async stopSync(_authToken: string, calendarId: string): Promise { // Stop webhook const watchData = await this.get(`outlook_watch_${calendarId}`); if (watchData) { @@ -342,10 +347,11 @@ export class OutlookCalendar ): Promise { const { config, credentials } = await this.getApi(authToken); - const webhookUrl = await this.tools.network.createWebhook("onOutlookWebhook", { + const webhookUrl = await this.tools.network.createWebhook( + this.onOutlookWebhook, calendarId, - authToken: opaqueAuthToken, - }); + opaqueAuthToken + ); config.webhookUrl = webhookUrl; @@ -377,12 +383,11 @@ export class OutlookCalendar }); } - async syncOutlookBatch(context: { - calendarId: string; - authToken: string; - }): Promise { - const { calendarId, authToken } = context; - + async syncOutlookBatch( + _args: any, + calendarId: string, + authToken: string + ): Promise { let config: CalendarConfig; let credentials: CalendarCredentials; @@ -468,14 +473,8 @@ export class OutlookCalendar } // Create Activity from Outlook event - const activity: Activity = { - id: event.id, + const activity: NewActivity = { type: ActivityType.Event, - author: { - id: "outlook-calendar", - name: "Outlook Calendar", - type: "system" as any, - }, start: event.startsAt || null, end: event.endsAt || null, recurrenceUntil: null, @@ -485,10 +484,6 @@ export class OutlookCalendar title: event.name || null, parent: null, links: null, - priority: { - id: "default", - title: "Default", - }, recurrenceRule: recurrenceRule || null, recurrenceExdates: null, recurrenceDates: null, @@ -531,41 +526,16 @@ export class OutlookCalendar if (masterEventId) { const originalStart = fromMsDate(event.data.originalStart); if (originalStart) { - activity.recurrence = { - id: masterEventId, - type: ActivityType.Event, - author: activity.author, - start: null, - end: null, - recurrenceUntil: null, - recurrenceCount: null, - doneAt: null, - note: null, - title: null, - parent: null, - links: null, - priority: activity.priority, - recurrenceRule: null, - recurrenceExdates: null, - recurrenceDates: null, - recurrence: null, - occurrence: null, - source: { - type: "outlook-calendar-event", - id: masterEventId, - calendarId: syncState.calendarId, - }, - tags: null, - }; + // TODO: Set recurrence activity.occurrence = originalStart; } } } // Call the event callback - const callbackToken = await this.get("event_callback_token"); + const callbackToken = await this.get("event_callback_token"); if (callbackToken) { - await this.run(callbackToken, activity); + await this.run(callbackToken as any, activity); } } @@ -585,9 +555,13 @@ export class OutlookCalendar ); } - async onOutlookWebhook(request: WebhookRequest, context: any): Promise { + async onOutlookWebhook( + request: WebhookRequest, + calendarId: string, + authToken: string + ): Promise { console.log("Received Outlook calendar webhook notification", { - calendarId: context.calendarId, + calendarId, }); if (request.params?.validationToken) { @@ -604,11 +578,11 @@ export class OutlookCalendar for (const notification of notifications) { if (notification.changeType) { console.log( - `Calendar ${notification.changeType} notification for ${context.calendarId}` + `Calendar ${notification.changeType} notification for ${calendarId}` ); // Trigger incremental sync - await this.startIncrementalSync(context.calendarId, context.authToken); + await this.startIncrementalSync(calendarId, authToken); } } } @@ -629,34 +603,27 @@ export class OutlookCalendar return; } - const callback = await this.callback("syncOutlookBatch", { + const callback = await this.callback( + this.syncOutlookBatch, calendarId, - authToken, - }); + authToken + ); await this.run(callback); } async onAuthSuccess( authResult: Authorization, - context: AuthSuccessContext + token: string, + callbackToken: Callback ): Promise { // Store the actual auth token using opaque token as key - await this.set(`authorization:${context.token}`, authResult); + await this.set(`authorization:${token}`, authResult); - // Retrieve and call the stored callback - const callbackToken = await this.get( - `auth_callback_token:${context.token}` - ); - if (callbackToken) { - const authSuccessResult: CalendarAuth = { - authToken: context.token, - }; - - await this.run(callbackToken, authSuccessResult); + const authSuccessResult: CalendarAuth = { + authToken: token, + }; - // Clean up the callback token - await this.clear(`auth_callback_token:${context.token}`); - } + await this.run(callbackToken, authSuccessResult); } } From 28a31024b52287420f58c0d92d14f2da0bb8dead Mon Sep 17 00:00:00 2001 From: Kris Braun Date: Sun, 26 Oct 2025 16:23:03 -0400 Subject: [PATCH 4/5] Simplify tool building --- .changeset/fuzzy-beans-thank.md | 2 +- agents/chat/src/index.ts | 8 +- agents/events/src/index.ts | 10 +- sdk/README.md | 28 +- sdk/cli/commands/create.ts | 69 +++- sdk/cli/templates/AGENTS.template.md | 113 +++--- sdk/cli/templates/README.template.md | 10 +- sdk/package.json | 16 +- sdk/prebuild.ts | 25 +- sdk/src/agent.ts | 364 +++--------------- sdk/src/index.ts | 1 + sdk/src/plot.ts | 19 +- sdk/src/tool.ts | 313 +++++++++++++++ sdk/src/tools/{agent.ts => agents.ts} | 16 +- sdk/src/tools/ai.ts | 6 +- sdk/src/tools/callbacks.ts | 24 +- sdk/src/tools/index.ts | 2 +- sdk/src/tools/integrations.ts | 6 +- sdk/src/tools/network.ts | 20 +- sdk/src/tools/plot.ts | 70 +++- sdk/src/tools/store.ts | 2 +- sdk/src/utils/types.ts | 98 ++--- sdk/tsconfig.base.json | 3 +- tools/google-calendar/package.json | 2 +- tools/google-calendar/src/google-api.ts | 5 +- tools/google-calendar/src/google-calendar.ts | 16 +- tools/google-calendar/tsconfig.build.json | 13 - tools/google-calendar/tsconfig.json | 3 - tools/google-contacts/package.json | 2 +- tools/google-contacts/src/google-contacts.ts | 10 +- tools/google-contacts/src/types.ts | 8 +- tools/google-contacts/tsconfig.build.json | 13 - tools/google-contacts/tsconfig.json | 3 - tools/outlook-calendar/package.json | 2 +- .../outlook-calendar/src/outlook-calendar.ts | 16 +- tools/outlook-calendar/tsconfig.build.json | 13 - tools/outlook-calendar/tsconfig.json | 3 - 37 files changed, 701 insertions(+), 633 deletions(-) create mode 100644 sdk/src/tool.ts rename sdk/src/tools/{agent.ts => agents.ts} (94%) delete mode 100644 tools/google-calendar/tsconfig.build.json delete mode 100644 tools/google-contacts/tsconfig.build.json delete mode 100644 tools/outlook-calendar/tsconfig.build.json diff --git a/.changeset/fuzzy-beans-thank.md b/.changeset/fuzzy-beans-thank.md index 9ad7ae6..3405294 100644 --- a/.changeset/fuzzy-beans-thank.md +++ b/.changeset/fuzzy-beans-thank.md @@ -5,7 +5,7 @@ "@plotday/sdk": minor --- -Changed: BREAKING: Agents and Tools now use a static Init() function to gain access to tools, which are then available via this.tools. +Changed: BREAKING: Agents and Tools now define a build() method to gain access to tools, which are then available via this.tools. Changed: BREAKING: Webhook functionality has been moved into the Network tool. Changed: BREAKING: CallbackTool renamed Callbacks. Changed: BREAKING: Auth renamed Integrations. diff --git a/agents/chat/src/index.ts b/agents/chat/src/index.ts index f05398b..9f02a56 100644 --- a/agents/chat/src/index.ts +++ b/agents/chat/src/index.ts @@ -11,11 +11,11 @@ import { import { AI, type AIMessage } from "@plotday/sdk/tools/ai"; import { Plot } from "@plotday/sdk/tools/plot"; -export default class ChatAgent extends Agent { - static Init(tools: ToolBuilder) { +export default class ChatAgent extends Agent { + build(build: ToolBuilder) { return { - ai: tools.init(AI), - plot: tools.init(Plot), + ai: build(AI), + plot: build(Plot), }; } diff --git a/agents/events/src/index.ts b/agents/events/src/index.ts index a8a57f8..6a1888b 100644 --- a/agents/events/src/index.ts +++ b/agents/events/src/index.ts @@ -24,12 +24,12 @@ type StoredCalendarAuth = { authToken: string; }; -export default class EventsAgent extends Agent { - static Init(tools: ToolBuilder) { +export default class EventsAgent extends Agent { + build(build: ToolBuilder) { return { - googleCalendar: tools.init(GoogleCalendar), - outlookCalendar: tools.init(OutlookCalendar), - plot: tools.init(Plot), + googleCalendar: build(GoogleCalendar), + outlookCalendar: build(OutlookCalendar), + plot: build(Plot), }; } diff --git a/sdk/README.md b/sdk/README.md index d7338fe..95abb20 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -94,13 +94,19 @@ This will prompt you for: Edit `src/index.ts` to add your agent logic: ```typescript -import { type Activity, ActivityType, Agent, type Priority, type ToolBuilder } from "@plotday/sdk"; +import { + type Activity, + ActivityType, + Agent, + type Priority, + type ToolBuilder, +} from "@plotday/sdk"; import { Plot } from "@plotday/sdk/tools/plot"; -export default class MyAgent extends Agent { - static Init(tools: ToolBuilder) { +export default class MyAgent extends Agent { + build(build: ToolBuilder) { return { - plot: tools.init(Plot), + plot: build(Plot), }; } @@ -172,13 +178,13 @@ Tools provide functionality to agents. They can be: - **Built-in Tools** - Core Plot functionality (Plot, Store, Integrations, etc.). - **Custom Tools** - Extra packages that add capabilities using the built-in tools. They often implement integrations with external services (Google Calendar, Outlook, etc.). -Declare tools in the static `Init` method. Store, Tasks, and Callbacks methods are available directly on the Agent class: +Declare tools in the `build` method. Store, Tasks, and Callbacks methods are available directly on the Agent class: ```typescript -static Init(tools: ToolBuilder) { +build(build: ToolBuilder) { return { - plot: tools.init(Plot), - googleCalendar: tools.init(GoogleCalendar), + plot: build(Plot), + googleCalendar: build(GoogleCalendar), }; } // Store, Tasks, and Callbacks methods are available directly: @@ -280,10 +286,10 @@ Request HTTP access permissions and create webhook endpoints for real-time notif ```typescript import { Network, type WebhookRequest } from "@plotday/sdk/tools/network"; -// Declare HTTP access in Init method -static Init(tools: ToolBuilder) { +// Declare HTTP access in build method +build(build: ToolBuilder) { return { - network: tools.init(Network, { + network: build(Network, { urls: ['https://api.example.com/*'] }) }; diff --git a/sdk/cli/commands/create.ts b/sdk/cli/commands/create.ts index ecc552f..4de846e 100644 --- a/sdk/cli/commands/create.ts +++ b/sdk/cli/commands/create.ts @@ -2,6 +2,7 @@ import { execSync } from "child_process"; import * as fs from "fs"; import * as path from "path"; import prompts from "prompts"; + import * as out from "../utils/output"; import { detectPackageManager } from "../utils/packageManager"; @@ -127,17 +128,24 @@ export async function createCommand(options: CreateOptions) { const agentTemplate = `import { type Activity, Agent, - type Tools, + type Priority, + type ToolBuilder, } from "@plotday/sdk"; +import { Plot } from "@plotday/sdk/tools/plot"; + +export default class MyAgent extends Agent { + build(build: ToolBuilder) { + return { + plot: build(Plot), + }; + } -export default class extends Agent { - constructor(id: string, tools: Tools) { - super(); + async activate(_priority: Pick) { + // Called when agent is enabled for a priority } async activity(activity: Activity) { - // Implement your agent logic here - console.log("Received activity:", activity); + // Called when an activity is routed to this agent } } `; @@ -145,34 +153,59 @@ export default class extends Agent { // Detect and use appropriate package manager const packageManager = detectPackageManager(); - const packageManagerCommand = packageManager === "npm" ? "npm run" : packageManager; + const packageManagerCommand = + packageManager === "npm" ? "npm run" : packageManager; // Copy README.md from template - const readmeTemplatePath = path.join(__dirname, "..", "templates", "README.template.md"); + const readmeTemplatePath = path.join( + __dirname, + "..", + "templates", + "README.template.md" + ); try { let readmeContent = fs.readFileSync(readmeTemplatePath, "utf-8"); // Replace template variables - readmeContent = readmeContent.replace(/\{\{displayName\}\}/g, response.displayName); + readmeContent = readmeContent.replace( + /\{\{displayName\}\}/g, + response.displayName + ); readmeContent = readmeContent.replace(/\{\{name\}\}/g, response.name); - readmeContent = readmeContent.replace(/\{\{packageManager\}\}/g, packageManagerCommand); + readmeContent = readmeContent.replace( + /\{\{packageManager\}\}/g, + packageManagerCommand + ); fs.writeFileSync(path.join(agentPath, "README.md"), readmeContent); } catch (error) { console.warn("Warning: Could not copy README template"); } // Copy AGENTS.md from template - const agentsTemplatePath = path.join(__dirname, "..", "templates", "AGENTS.template.md"); + const agentsTemplatePath = path.join( + __dirname, + "..", + "templates", + "AGENTS.template.md" + ); try { let agentsContent = fs.readFileSync(agentsTemplatePath, "utf-8"); // Replace template variables - agentsContent = agentsContent.replace(/\{\{packageManager\}\}/g, packageManagerCommand); + agentsContent = agentsContent.replace( + /\{\{packageManager\}\}/g, + packageManagerCommand + ); fs.writeFileSync(path.join(agentPath, "AGENTS.md"), agentsContent); } catch (error) { console.warn("Warning: Could not copy AGENTS template"); } // Copy CLAUDE.md from template - const claudeTemplatePath = path.join(__dirname, "..", "templates", "CLAUDE.template.md"); + const claudeTemplatePath = path.join( + __dirname, + "..", + "templates", + "CLAUDE.template.md" + ); try { const claudeContent = fs.readFileSync(claudeTemplatePath, "utf-8"); fs.writeFileSync(path.join(agentPath, "CLAUDE.md"), claudeContent); @@ -194,16 +227,16 @@ build/ // Silently fail - not critical } - const installCommand = packageManager === "yarn" ? "yarn" : `${packageManager} install`; + const installCommand = + packageManager === "yarn" ? "yarn" : `${packageManager} install`; // Install dependencies try { execSync(installCommand, { cwd: agentPath, stdio: "ignore" }); } catch (error) { - out.warning( - "Couldn't install dependencies", - [`Run '${installCommand}' in ${agentDir}`] - ); + out.warning("Couldn't install dependencies", [ + `Run '${installCommand}' in ${agentDir}`, + ]); } out.success(`${response.displayName} created`); diff --git a/sdk/cli/templates/AGENTS.template.md b/sdk/cli/templates/AGENTS.template.md index 35d79ca..98efe18 100644 --- a/sdk/cli/templates/AGENTS.template.md +++ b/sdk/cli/templates/AGENTS.template.md @@ -20,13 +20,18 @@ Plot agents are TypeScript classes that extend the `Agent` base class. Agents in ## Agent Structure Pattern ```typescript -import { type Activity, Agent, type Priority, type ToolBuilder } from "@plotday/sdk"; +import { + type Activity, + Agent, + type Priority, + type ToolBuilder, +} from "@plotday/sdk"; import { Plot } from "@plotday/sdk/tools/plot"; -export default class MyAgent extends Agent { - static Init(tools: ToolBuilder) { +export default class MyAgent extends Agent { + build(build: ToolBuilder) { return { - plot: tools.init(Plot), + plot: build(Plot), }; } @@ -46,19 +51,19 @@ export default class MyAgent extends Agent { ### Accessing Tools -All tools are declared in the static `Init` method: +All tools are declared in the `build` method: ```typescript -static Init(tools: ToolBuilder) { +build(build: ToolBuilder) { return { - toolName: tools.init(ToolClass), + toolName: build(ToolClass), }; } ``` -All `tools.init()` calls must occur in the `Init` method as they are used for dependency analysis. +All `build()` calls must occur in the `build` method as they are used for dependency analysis. -IMPORTANT: HTTP access is restricted to URLs requested via `tools.init(Network, { urls: [url1, url2, ...] })` in the `Init` method. Wildcards are supported. Use `tools.init(Network, { urls: ['*'] })` if full access is needed. +IMPORTANT: HTTP access is restricted to URLs requested via `build(Network, { urls: [url1, url2, ...] })` in the `build` method. Wildcards are supported. Use `build(Network, { urls: ['*'] })` if full access is needed. ### Built-in Tools (Always Available) @@ -74,7 +79,7 @@ For complete API documentation of built-in tools including all methods, types, a - `@plotday/sdk/tools/callbacks` - Persistent function references (also via `this.callback()`) - `@plotday/sdk/tools/integrations` - OAuth2 authentication flows - `@plotday/sdk/tools/network` - HTTP access permissions and webhook management -- `@plotday/sdk/tools/agent` - Manage other agents +- `@plotday/sdk/tools/agents` - Manage other agents **Critical**: Never use instance variables for state. They are lost after function execution. Always use Store methods. @@ -107,8 +112,10 @@ Called when the agent is enabled for a priority. Common patterns: ```typescript async activate(_priority: Pick) { - const callback = await this.callback("onAuthComplete", { provider: "google" }); - const authLink = await this.tools.externalTool.requestAuth(callback); + const authLink = await this.tools.externalTool.requestAuth( + this.onAuthComplete, + "google" + ); await this.tools.plot.createActivity({ type: ActivityType.Task, @@ -166,7 +173,7 @@ const urlLink: ActivityLink = { }; // Callback link (uses Callbacks tool) -const token = await this.callback("onLinkClicked", { data: "context" }); +const token = await this.callback(this.onLinkClicked, "context"); const callbackLink: ActivityLink = { title: "Click me", type: ActivityLinkType.callback, @@ -187,13 +194,11 @@ Common pattern for OAuth authentication: ```typescript async activate(_priority: Pick) { - // Create callback for auth completion - const callback = await this.callback("onAuthComplete", { - provider: "google", - }); - - // Request auth link from tool - const authLink = await this.tools.googleTool.requestAuth(callback); + // Request auth link from tool with callback + const authLink = await this.tools.googleTool.requestAuth( + this.onAuthComplete, + "google" + ); // Create activity with auth link const activity = await this.tools.plot.createActivity({ @@ -206,9 +211,7 @@ async activate(_priority: Pick) { await this.set("auth_activity_id", activity.id); } -async onAuthComplete(authResult: { authToken: string }, context?: any) { - const provider = context?.provider; - +async onAuthComplete(authResult: { authToken: string }, provider: string) { // Store auth token await this.set(`${provider}_auth`, authResult.authToken); @@ -225,15 +228,15 @@ Pattern for syncing external data with callbacks: async startSync(calendarId: string): Promise { const authToken = await this.get("auth_token"); - // Create callback for event handling - const callback = await this.callback("handleEvent", { + await this.tools.calendarTool.startSync( + authToken, calendarId, - }); - - await this.tools.calendarTool.startSync(authToken, calendarId, callback); + this.handleEvent, + calendarId + ); } -async handleEvent(activity: Activity, context?: any): Promise { +async handleEvent(activity: Activity, calendarId: string): Promise { // Process incoming event from external service await this.tools.plot.createActivity(activity); } @@ -257,12 +260,13 @@ private async createCalendarSelectionActivity( const links: ActivityLink[] = []; for (const calendar of calendars) { - const token = await this.callback("onCalendarSelected", { + const token = await this.callback( + this.onCalendarSelected, provider, - calendarId: calendar.id, - calendarName: calendar.name, - authToken, - }); + calendar.id, + calendar.name, + authToken + ); links.push({ title: `📅 ${calendar.name}${calendar.primary ? " (Primary)" : ""}`, @@ -278,14 +282,21 @@ private async createCalendarSelectionActivity( }); } -async onCalendarSelected(link: ActivityLink, context: any): Promise { +async onCalendarSelected( + link: ActivityLink, + provider: string, + calendarId: string, + calendarName: string, + authToken: string +): Promise { // Start sync for selected calendar - const callback = await this.callback("handleEvent", { - provider: context.provider, - calendarId: context.calendarId, - }); - - await this.tools.tool.startSync(context.authToken, context.calendarId, callback); + await this.tools.tool.startSync( + authToken, + calendarId, + this.handleEvent, + provider, + calendarId + ); } ``` @@ -311,14 +322,14 @@ async startSync(resourceId: string): Promise { itemsProcessed: 0, }); - // Queue first batch using run method - const callback = await this.callback("syncBatch", { resourceId }); - await this.run(callback); + // Queue first batch using runTask method + const callback = await this.callback(this.syncBatch, resourceId); + await this.runTask(callback); } -async syncBatch(args: any, context: { resourceId: string }): Promise { +async syncBatch(args: any, resourceId: string): Promise { // Load state from Store (set by previous execution) - const state = await this.get(`sync_state_${context.resourceId}`); + const state = await this.get(`sync_state_${resourceId}`); // Process one batch (keep under time limit) const result = await this.fetchBatch(state.nextPageToken); @@ -330,18 +341,18 @@ async syncBatch(args: any, context: { resourceId: string }): Promise { if (result.nextPageToken) { // Update state in Store for next batch - await this.set(`sync_state_${context.resourceId}`, { + await this.set(`sync_state_${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); + const nextCallback = await this.callback(this.syncBatch, resourceId); + await this.runTask(nextCallback); } else { // Cleanup when complete - await this.clear(`sync_state_${context.resourceId}`); + await this.clear(`sync_state_${resourceId}`); // Optionally notify user of completion await this.tools.plot.createActivity({ @@ -374,7 +385,7 @@ try { - **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. - **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. - Most activity should be `type = ActivityType.Note` with a `title` and `note`, and no `start` or `end`. This represents a typical message. `start` and `end` should only be used for a note if it should be displayed for a specific date or time, such as a birthday. -- Tools are declared in the static `Init` method and accessed via `this.tools.toolName` in agent methods. +- Tools are declared in the `build` method and accessed via `this.tools.toolName` in agent methods. - **Don't forget runtime limits** - Each execution has ~10 seconds. Break long operations into batches with the Tasks tool. Process enough items per batch to be efficient, but few enough to stay under time limits. - **Always use Callbacks tool for persistent references** - Direct function references don't survive worker restarts. - **Store auth tokens** - Don't re-request authentication unnecessarily. diff --git a/sdk/cli/templates/README.template.md b/sdk/cli/templates/README.template.md index 17652c0..a3f93a7 100644 --- a/sdk/cli/templates/README.template.md +++ b/sdk/cli/templates/README.template.md @@ -48,12 +48,12 @@ Called when an activity is routed to this agent. Use this to: ### Using Tools -Agents access functionality through tools. Declare tools in the static `Init` method: +Agents access functionality through tools. Declare tools in the `build` method: ```typescript -static Init(tools: ToolBuilder) { +build(build: ToolBuilder) { return { - plot: tools.init(Plot), + plot: build(Plot), }; } // Store, Tasks, and Callbacks methods are available directly via this @@ -86,9 +86,9 @@ Then use them in your agent: ```typescript import GoogleCalendarTool from "@plotday/tool-google-calendar"; -static Init(tools: ToolBuilder) { +build(build: ToolBuilder) { return { - googleCalendar: tools.init(GoogleCalendarTool), + googleCalendar: build(GoogleCalendarTool), }; } ``` diff --git a/sdk/package.json b/sdk/package.json index 6be9a93..4610396 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -18,6 +18,10 @@ "types": "./dist/agent.d.ts", "default": "./dist/agent.js" }, + "./tool": { + "types": "./dist/tool.d.ts", + "default": "./dist/tool.js" + }, "./plot": { "types": "./dist/plot.d.ts", "default": "./dist/plot.js" @@ -26,9 +30,9 @@ "types": "./dist/tag.d.ts", "default": "./dist/tag.js" }, - "./tools/agent": { - "types": "./dist/tools/agent.d.ts", - "default": "./dist/tools/agent.js" + "./tools/agents": { + "types": "./dist/tools/agents.d.ts", + "default": "./dist/tools/agents.js" }, "./tools/ai": { "types": "./dist/tools/ai.d.ts", @@ -94,9 +98,9 @@ "types": "./dist/llm-docs/tag.d.ts", "default": "./dist/llm-docs/tag.js" }, - "./llm-docs/tools/agent": { - "types": "./dist/llm-docs/tools/agent.d.ts", - "default": "./dist/llm-docs/tools/agent.js" + "./llm-docs/tools/agents": { + "types": "./dist/llm-docs/tools/agents.d.ts", + "default": "./dist/llm-docs/tools/agents.js" }, "./llm-docs/tools/ai": { "types": "./dist/llm-docs/tools/ai.d.ts", diff --git a/sdk/prebuild.ts b/sdk/prebuild.ts index 42c13c0..a5c08ff 100644 --- a/sdk/prebuild.ts +++ b/sdk/prebuild.ts @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync } from "fs"; +import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs"; import { dirname, join } from "path"; import { fileURLToPath } from "url"; @@ -9,8 +9,8 @@ const __dirname = dirname(__filename); interface TypeFile { file: string; importPath: string; - docFilePath: string; // Path where doc file will be written (e.g., "tools/agent.ts") - varName: string; // Valid JS identifier for imports (e.g., "toolsAgent") + docFilePath: string; // Path where doc file will be written (e.g., "tools/agents.ts") + varName: string; // Valid JS identifier for imports (e.g., "toolsAgents") } // Generate SDK documentation files for LLM consumption @@ -61,13 +61,13 @@ for (const [exportPath, exportValue] of Object.entries(exports)) { const importPath = exportPath === "." ? "@plotday/sdk" : `@plotday/sdk${exportPath.slice(1)}`; - // docFilePath mirrors the source file structure (e.g., "tools/agent.ts") + // docFilePath mirrors the source file structure (e.g., "tools/agents.ts") // This is the path where the doc file will be written const docFilePath = sourceFile; // varName is a valid JavaScript identifier for use in index.ts imports // Convert path separators and hyphens to create valid identifiers - // e.g., "tools/agent.ts" -> "toolsAgent", "common/calendar.ts" -> "commonCalendar" + // e.g., "tools/agents.ts" -> "toolsAgents", "common/calendar.ts" -> "commonCalendar" const varName = sourceFile .replace(/\.ts$/, "") // Remove .ts extension .replace(/\//g, "_") // Replace / with _ @@ -155,7 +155,12 @@ 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"); +const agentsTemplatePath = join( + __dirname, + "cli", + "templates", + "AGENTS.template.md" +); if (existsSync(agentsTemplatePath)) { const agentsTemplateContent = readFileSync(agentsTemplatePath, "utf-8"); const agentsGuideContent = `/** @@ -171,7 +176,11 @@ export default ${JSON.stringify(agentsTemplateContent)}; 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.warn( + `Warning: AGENTS.template.md not found at ${agentsTemplatePath}` + ); } -console.log(`✓ Generated ${typeFiles.length} LLM documentation files in src/llm-docs/`); +console.log( + `✓ Generated ${typeFiles.length} LLM documentation files in src/llm-docs/` +); diff --git a/sdk/src/agent.ts b/sdk/src/agent.ts index ff31db4..932b015 100644 --- a/sdk/src/agent.ts +++ b/sdk/src/agent.ts @@ -1,15 +1,7 @@ -import { type Activity, type ActorId, type Priority, type Tag } from "./plot"; +import { type Priority } from "./plot"; +import { type ITool } from "./tool"; import type { Callback } from "./tools/callbacks"; -import type { - CallbackMethods, - HasInit, - InferOptions, - InferTools, - NoFunctions, - ToolBuilder, -} from "./utils/types"; - -export type { ToolBuilder }; +import type { InferTools, ToolBuilder, ToolShed } from "./utils/types"; /** * Base class for all agents. @@ -17,14 +9,14 @@ export type { ToolBuilder }; * Agents are activated in a Plot priority and have access to that priority and all * its descendants. * - * Override method to handle events. + * Override build() to declare tool dependencies and lifecycle methods to handle events. * * @example * ```typescript * class FlatteringAgent extends Agent { - * static Init(tools: ToolBuilder, options?: { greeting: string }) { + * build(build: ToolBuilder) { * return { - * plot: tools.init(Plot, PLOT_OPTIONS), + * plot: build(Plot), * }; * } * @@ -32,22 +24,41 @@ export type { ToolBuilder }; * // Initialize agent for the given priority * await this.tools.plot.createActivity({ * type: ActivityType.Note, - * note: this.options.greeting || "Hello, good looking!", + * note: "Hello, good looking!", * }); * } - * - * async activity(activity: Activity) { - * // Process new activity - * } * } * ``` */ -export abstract class Agent { - constructor( - protected id: string, - protected tools: InferTools, - protected options: InferOptions - ) {} +export abstract class Agent { + constructor(protected id: string, private toolShed: ToolShed) {} + + /** + * Gets the initialized tools for this agent. + * @throws Error if called before initialization is complete + */ + protected get tools(): InferTools { + return this.toolShed.getTools>(); + } + + /** + * Declares tool dependencies for this agent. + * Return an object mapping tool names to build() promises. + * + * @param build - The build function to use for declaring dependencies + * @returns Object mapping tool names to tool promises + * + * @example + * ```typescript + * build(build: ToolBuilder) { + * return { + * plot: build(Plot), + * calendar: build(GoogleCalendar, { apiKey: "..." }), + * }; + * } + * ``` + */ + abstract build(build: ToolBuilder): Record>; /** * Creates a persistent callback to a method on this agent. @@ -63,16 +74,11 @@ export abstract class Agent { * const callback = await this.callback(this.onWebhook, "calendar", 123); * ``` */ - protected async callback< - K extends CallbackMethods>, - TMethod extends InstanceType[K] = InstanceType[K] - >( - fn: TMethod, - ...extraArgs: TMethod extends (arg: any, ...rest: infer R) => any - ? NoFunctions - : [] + protected async callback( + fn: Function, + ...extraArgs: any[] ): Promise { - return this.tools.callbacks.create(fn, ...extraArgs); + return this.tools.callbacks.create(fn as any, ...extraArgs); } /** @@ -239,291 +245,11 @@ export abstract class Agent { } /** - * Called when an activity needs to be processed by this agent. - * - * This method is invoked when activities are routed to this agent, - * either through explicit assignment or through filtering rules. - * - * @param _activity - The activity to process - * @param _changes - Optional changes object containing the previous version of the activity for updates, - * along with tags that were added or removed - * @returns Promise that resolves when processing is complete - */ - activity( - _activity: Activity, - _changes?: { - previous: Activity; - tagsAdded: Record; - tagsRemoved: Record; - } - ): Promise { - return Promise.resolve(); - } -} - -/** - * Interface for tools. Tools should extend Tool. Several built-in tools - * implement this interface directly since they're securely proxied - * outside the agent runtime. - */ -export abstract class ITool {} - -/** - * Base class for regular tools. - * - * Regular tools.tasks in isolation and can only access other tools declared - * in their tool.json dependencies. They are ideal for external API integrations - * and reusable functionality that doesn't require Plot's internal infrastructure. - * - * @example - * ```typescript - * class GoogleCalendarTool extends Tool { - * static Init(tools: ToolBuilder, options?: { clientId: string }) { - * return { - * auth: tools.init(Integrations, AUTH_OPTIONS), - * network: tools.init(Network, NETWORK_OPTIONS), - * }; - * } - * - * async getCalendars() { - * const token = await this.tools.auth.get(...); - * // Implementation - * } - * } - * ``` - */ -export abstract class Tool implements ITool { - constructor( - protected id: string, - protected tools: InferTools, - protected options: InferOptions - ) {} - - /** - * Creates a persistent callback to a method on this tool. - * - * ExtraArgs are strongly typed to match the method's signature after the first argument. - * - * @param fn - The method to callback - * @param extraArgs - Additional arguments to pass (type-checked, must be serializable) - * @returns Promise resolving to a persistent callback token - * - * @example - * ```typescript - * const callback = await this.callback(this.onWebhook, "calendar", 123); - * ``` - */ - protected async callback< - K extends CallbackMethods>, - TMethod extends InstanceType[K] = InstanceType[K] - >( - fn: TMethod, - ...extraArgs: TMethod extends (arg: any, ...rest: infer R) => any - ? NoFunctions - : [] - ): Promise { - return this.tools.callbacks.create(fn, ...extraArgs); - } - - /** - * Deletes a specific callback by its token. - * - * @param token - The callback token to delete - * @returns Promise that resolves when the callback is deleted - */ - protected async deleteCallback(token: Callback): Promise { - return this.tools.callbacks.delete(token); - } - - /** - * Deletes all callbacks for this tool. - * - * @returns Promise that resolves when all callbacks are deleted - */ - protected async deleteAllCallbacks(): Promise { - return this.tools.callbacks.deleteAll(); - } - - /** - * Executes a callback by its token. - * - * @param token - The callback token to execute - * @param args - Optional arguments to pass to the callback - * @returns Promise resolving to the callback result - */ - protected async run(token: Callback, args?: any): Promise { - return this.tools.callbacks.run(token, args); - } - - /** - * Retrieves a value from persistent storage by key. - * - * @template T - The expected type of the stored value - * @param key - The storage key to retrieve - * @returns Promise resolving to the stored value or null - */ - protected async get(key: string): Promise { - return this.tools.store.get(key); - } - - /** - * Stores a value in persistent storage. - * - * **Important**: Values must be JSON-serializable. Functions, Symbols, and undefined values - * cannot be stored directly. - * - * **For function references**: Use callbacks instead of storing functions directly. - * - * @example - * ```typescript - * // ❌ WRONG: Cannot store functions directly - * await this.set("handler", this.myHandler); - * - * // ✅ CORRECT: Create a callback token first - * const token = await this.callback(this.myHandler, "arg1", "arg2"); - * await this.set("handler_token", token); - * - * // Later, execute the callback - * const token = await this.get("handler_token"); - * await this.run(token, args); - * ``` - * - * @template T - The type of value being stored - * @param key - The storage key to use - * @param value - The value to store (must be JSON-serializable) - * @returns Promise that resolves when the value is stored - */ - protected async set(key: string, value: T): Promise { - return this.tools.store.set(key, value); - } - - /** - * Removes a specific key from persistent storage. - * - * @param key - The storage key to remove - * @returns Promise that resolves when the key is removed - */ - protected async clear(key: string): Promise { - return this.tools.store.clear(key); - } - - /** - * Removes all keys from this tool's storage. - * - * @returns Promise that resolves when all keys are removed - */ - protected async clearAll(): Promise { - return this.tools.store.clearAll(); - } - - /** - * Queues a callback to execute in a separate worker context. - * - * @param callback - The callback token created with `this.callback()` - * @param options - Optional configuration for the execution - * @param options.runAt - If provided, schedules execution at this time; otherwise runs immediately - * @returns Promise resolving to a cancellation token (only for scheduled executions) - */ - protected async runTask( - callback: Callback, - options?: { runAt?: Date } - ): Promise { - return this.tools.tasks.runTask(callback, options); - } - - /** - * Cancels a previously scheduled execution. - * - * @param token - The cancellation token returned by runTask() with runAt option - * @returns Promise that resolves when the cancellation is processed - */ - protected async cancelTask(token: string): Promise { - return this.tools.tasks.cancelTask(token); - } - - /** - * Cancels all scheduled executions for this tool. - * - * @returns Promise that resolves when all cancellations are processed - */ - protected async cancelAllTasks(): Promise { - return this.tools.tasks.cancelAllTasks(); - } - - /** - * Called before the agent's activate method, starting from the deepest tool dependencies. - * - * This method is called in a depth-first manner, with the deepest dependencies - * being called first, bubbling up to the top-level tools before the agent's - * activate method is called. - * - * @param _priority - The priority context containing the priority ID - * @returns Promise that resolves when pre-activation is complete - */ - preActivate(_priority: Priority): Promise { - return Promise.resolve(); - } - - /** - * Called after the agent's activate method, starting from the top-level tools. - * - * This method is called in reverse order, with top-level tools being called - * first, then cascading down to the deepest dependencies. - * - * @param _priority - The priority context containing the priority ID - * @returns Promise that resolves when post-activation is complete - */ - postActivate(_priority: Priority): Promise { - return Promise.resolve(); - } - - /** - * Called before the agent's upgrade method, starting from the deepest tool dependencies. - * - * This method is called in a depth-first manner, with the deepest dependencies - * being called first, bubbling up to the top-level tools before the agent's - * upgrade method is called. - * - * @returns Promise that resolves when pre-upgrade is complete - */ - preUpgrade(): Promise { - return Promise.resolve(); - } - - /** - * Called after the agent's upgrade method, starting from the top-level tools. - * - * This method is called in reverse order, with top-level tools being called - * first, then cascading down to the deepest dependencies. - * - * @returns Promise that resolves when post-upgrade is complete + * Waits for tool initialization to complete. + * Called automatically by the entrypoint before lifecycle methods. + * @internal */ - postUpgrade(): Promise { - return Promise.resolve(); - } - - /** - * Called before the agent's deactivate method, starting from the deepest tool dependencies. - * - * This method is called in a depth-first manner, with the deepest dependencies - * being called first, bubbling up to the top-level tools before the agent's - * deactivate method is called. - * - * @returns Promise that resolves when pre-deactivation is complete - */ - preDeactivate(): Promise { - return Promise.resolve(); - } - - /** - * Called after the agent's deactivate method, starting from the top-level tools. - * - * This method is called in reverse order, with top-level tools being called - * first, then cascading down to the deepest dependencies. - * - * @returns Promise that resolves when post-deactivation is complete - */ - postDeactivate(): Promise { - return Promise.resolve(); + async waitForReady(): Promise { + await this.toolShed.waitForReady(); } } diff --git a/sdk/src/index.ts b/sdk/src/index.ts index 215616c..bef38a5 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -1,5 +1,6 @@ export * from "./agent"; export * from "./plot"; export * from "./tag"; +export * from "./tool"; export * from "./tools"; export { getSDKDocumentation } from "./sdk-docs"; diff --git a/sdk/src/plot.ts b/sdk/src/plot.ts index d0193ee..6fb3fff 100644 --- a/sdk/src/plot.ts +++ b/sdk/src/plot.ts @@ -159,15 +159,15 @@ export type ActivityLink = }; /** - * Represents the source of an activity from an external system. + * Represents metadata about an activity, typically from an external system. * - * Activity sources enable tracking where activities originated from, + * Activity metadata enables tracking where activities originated from, * which is useful for synchronization, deduplication, and linking * back to external systems. * * @example * ```typescript - * const googleCalendarSource: ActivitySource = { + * const googleCalendarMeta: ActivityMeta = { * type: "google-calendar-event", * id: "event-123", * calendarId: "primary", @@ -175,9 +175,9 @@ export type ActivityLink = * }; * ``` */ -export type ActivitySource = { +export type ActivityMeta = { /** The type identifier for the source system */ - type: string; + source: string; /** Additional source-specific properties */ [key: string]: any; }; @@ -289,10 +289,12 @@ export type Activity = { * Used to identify which occurrence of a recurring event this exception replaces. */ occurrence: Date | null; - /** Reference to the external system that created this activity */ - source: ActivitySource | null; + /** Metadata about the activity, typically from an external system that created it */ + meta: ActivityMeta | null; /** Tags attached to this activity. Maps tag ID to array of actor IDs who added that tag. */ tags: Partial> | null; + /** Array of actor IDs (users, contacts, or agents) mentioned in this activity via @-mentions */ + mentions: ActorId[] | null; }; /** @@ -332,7 +334,7 @@ export type ActivityUpdate = Pick & | "doneAt" | "note" | "title" - | "source" + | "meta" | "links" | "recurrenceRule" | "recurrenceDates" @@ -340,6 +342,7 @@ export type ActivityUpdate = Pick & | "recurrenceUntil" | "recurrenceCount" | "occurrence" + | "mentions" > > & { parent?: Pick | null; diff --git a/sdk/src/tool.ts b/sdk/src/tool.ts new file mode 100644 index 0000000..e17c723 --- /dev/null +++ b/sdk/src/tool.ts @@ -0,0 +1,313 @@ +import { type Priority } from "./plot"; +import type { Callback } from "./tools/callbacks"; +import type { + InferOptions, + InferTools, + ToolBuilder, + ToolShed, +} from "./utils/types"; + +export type { ToolBuilder }; + +/** + * Abstrtact parent for both built-in tools and regular Tools. + * Regular tools extend Tool. + */ +export abstract class ITool {} + +/** + * Base class for regular tools. + * + * Regular tools run in isolation and can only access other tools declared + * in their build method. They are ideal for external API integrations + * and reusable functionality that doesn't require Plot's internal infrastructure. + * + * @example + * ```typescript + * class GoogleCalendarTool extends Tool { + * constructor(id: string, options: { clientId: string }) { + * super(id, options); + * } + * + * build(tools: ToolBuilder) { + * return { + * auth: tools.build(Integrations), + * network: tools.build(Network), + * }; + * } + * + * async getCalendars() { + * const token = await this.tools.auth.get(...); + * // Implementation + * } + * } + * ``` + */ +export abstract class Tool implements ITool { + constructor( + protected id: string, + protected options: InferOptions, + private toolShed: ToolShed + ) {} + + /** + * Gets the initialized tools for this tool. + * @throws Error if called before initialization is complete + */ + protected get tools() { + return this.toolShed.getTools>(); + } + + /** + * Declares tool dependencies for this tool. + * Return an object mapping tool names to build() promises. + * Default implementation returns empty object (no custom tools). + * + * @param build - The build function to use for declaring dependencies + * @returns Object mapping tool names to tool promises + * + * @example + * ```typescript + * build(build: ToolBuilder) { + * return { + * network: build(Network, { urls: ["https://api.example.com/*"] }), + * }; + * } + * ``` + */ + build(_build: ToolBuilder): Record> { + return {}; + } + + /** + * Creates a persistent callback to a method on this tool. + * + * ExtraArgs are strongly typed to match the method's signature after the first argument. + * + * @param fn - The method to callback + * @param extraArgs - Additional arguments to pass (type-checked, must be serializable) + * @returns Promise resolving to a persistent callback token + * + * @example + * ```typescript + * const callback = await this.callback(this.onWebhook, "calendar", 123); + * ``` + */ + protected async callback( + fn: Function, + ...extraArgs: any[] + ): Promise { + return this.tools.callbacks.create(fn, ...extraArgs); + } + + /** + * Deletes a specific callback by its token. + * + * @param token - The callback token to delete + * @returns Promise that resolves when the callback is deleted + */ + protected async deleteCallback(token: Callback): Promise { + return this.tools.callbacks.delete(token); + } + + /** + * Deletes all callbacks for this tool. + * + * @returns Promise that resolves when all callbacks are deleted + */ + protected async deleteAllCallbacks(): Promise { + return this.tools.callbacks.deleteAll(); + } + + /** + * Executes a callback by its token. + * + * @param token - The callback token to execute + * @param args - Optional arguments to pass to the callback + * @returns Promise resolving to the callback result + */ + protected async run(token: Callback, args?: any): Promise { + return this.tools.callbacks.run(token, args); + } + + /** + * Retrieves a value from persistent storage by key. + * + * @template T - The expected type of the stored value + * @param key - The storage key to retrieve + * @returns Promise resolving to the stored value or null + */ + protected async get(key: string): Promise { + return this.tools.store.get(key); + } + + /** + * Stores a value in persistent storage. + * + * **Important**: Values must be JSON-serializable. Functions, Symbols, and undefined values + * cannot be stored directly. + * + * **For function references**: Use callbacks instead of storing functions directly. + * + * @example + * ```typescript + * // ❌ WRONG: Cannot store functions directly + * await this.set("handler", this.myHandler); + * + * // ✅ CORRECT: Create a callback token first + * const token = await this.callback(this.myHandler, "arg1", "arg2"); + * await this.set("handler_token", token); + * + * // Later, execute the callback + * const token = await this.get("handler_token"); + * await this.run(token, args); + * ``` + * + * @template T - The type of value being stored + * @param key - The storage key to use + * @param value - The value to store (must be JSON-serializable) + * @returns Promise that resolves when the value is stored + */ + protected async set(key: string, value: T): Promise { + return this.tools.store.set(key, value); + } + + /** + * Removes a specific key from persistent storage. + * + * @param key - The storage key to remove + * @returns Promise that resolves when the key is removed + */ + protected async clear(key: string): Promise { + return this.tools.store.clear(key); + } + + /** + * Removes all keys from this tool's storage. + * + * @returns Promise that resolves when all keys are removed + */ + protected async clearAll(): Promise { + return this.tools.store.clearAll(); + } + + /** + * Queues a callback to execute in a separate worker context. + * + * @param callback - The callback token created with `this.callback()` + * @param options - Optional configuration for the execution + * @param options.runAt - If provided, schedules execution at this time; otherwise runs immediately + * @returns Promise resolving to a cancellation token (only for scheduled executions) + */ + protected async runTask( + callback: Callback, + options?: { runAt?: Date } + ): Promise { + return this.tools.tasks.runTask(callback, options); + } + + /** + * Cancels a previously scheduled execution. + * + * @param token - The cancellation token returned by runTask() with runAt option + * @returns Promise that resolves when the cancellation is processed + */ + protected async cancelTask(token: string): Promise { + return this.tools.tasks.cancelTask(token); + } + + /** + * Cancels all scheduled executions for this tool. + * + * @returns Promise that resolves when all cancellations are processed + */ + protected async cancelAllTasks(): Promise { + return this.tools.tasks.cancelAllTasks(); + } + + /** + * Called before the agent's activate method, starting from the deepest tool dependencies. + * + * This method is called in a depth-first manner, with the deepest dependencies + * being called first, bubbling up to the top-level tools before the agent's + * activate method is called. + * + * @param _priority - The priority context containing the priority ID + * @returns Promise that resolves when pre-activation is complete + */ + preActivate(_priority: Priority): Promise { + return Promise.resolve(); + } + + /** + * Called after the agent's activate method, starting from the top-level tools. + * + * This method is called in reverse order, with top-level tools being called + * first, then cascading down to the deepest dependencies. + * + * @param _priority - The priority context containing the priority ID + * @returns Promise that resolves when post-activation is complete + */ + postActivate(_priority: Priority): Promise { + return Promise.resolve(); + } + + /** + * Called before the agent's upgrade method, starting from the deepest tool dependencies. + * + * This method is called in a depth-first manner, with the deepest dependencies + * being called first, bubbling up to the top-level tools before the agent's + * upgrade method is called. + * + * @returns Promise that resolves when pre-upgrade is complete + */ + preUpgrade(): Promise { + return Promise.resolve(); + } + + /** + * Called after the agent's upgrade method, starting from the top-level tools. + * + * This method is called in reverse order, with top-level tools being called + * first, then cascading down to the deepest dependencies. + * + * @returns Promise that resolves when post-upgrade is complete + */ + postUpgrade(): Promise { + return Promise.resolve(); + } + + /** + * Called before the agent's deactivate method, starting from the deepest tool dependencies. + * + * This method is called in a depth-first manner, with the deepest dependencies + * being called first, bubbling up to the top-level tools before the agent's + * deactivate method is called. + * + * @returns Promise that resolves when pre-deactivation is complete + */ + preDeactivate(): Promise { + return Promise.resolve(); + } + + /** + * Called after the agent's deactivate method, starting from the top-level tools. + * + * This method is called in reverse order, with top-level tools being called + * first, then cascading down to the deepest dependencies. + * + * @returns Promise that resolves when post-deactivation is complete + */ + postDeactivate(): Promise { + return Promise.resolve(); + } + + /** + * Waits for tool initialization to complete. + * Called automatically by the entrypoint before lifecycle methods. + * @internal + */ + async waitForReady(): Promise { + await this.toolShed.waitForReady(); + } +} diff --git a/sdk/src/tools/agent.ts b/sdk/src/tools/agents.ts similarity index 94% rename from sdk/src/tools/agent.ts rename to sdk/src/tools/agents.ts index 78d8d76..7acd000 100644 --- a/sdk/src/tools/agent.ts +++ b/sdk/src/tools/agents.ts @@ -1,4 +1,4 @@ -import { type Callback, ITool, type ToolBuilder } from ".."; +import { type Callback, ITool } from ".."; /** * Agent source code structure containing dependencies and source files. @@ -51,24 +51,20 @@ export type AgentPermissions = Record; * @example * ```typescript * class AgentBuilderAgent extends Agent { - * private agent: Agents; - * - * constructor(id: string, tools: ToolBuilder) { - * super(); - * this.agent = tools.get(AgentTool); + * build(build: ToolBuilder) { + * return { + * agents: build.get(Agents) + * } * } * * async activate() { - * const agentId = await this.agent.create(); + * const agentId = await this.tools.agents.create(); * // Display agent ID to user * } * } * ``` */ export abstract class Agents extends ITool { - static Init(_tools: ToolBuilder, _options?: any): Record { - return {}; - } /** * Creates a new agent ID and grants access to people in the current priority. * diff --git a/sdk/src/tools/ai.ts b/sdk/src/tools/ai.ts index b1cfcb4..8c9d0d4 100644 --- a/sdk/src/tools/ai.ts +++ b/sdk/src/tools/ai.ts @@ -1,6 +1,6 @@ import type { Static, TSchema } from "typebox"; -import { ITool, type ToolBuilder } from ".."; +import { ITool } from ".."; /** * Built-in tool for prompting Large Language Models (LLMs). @@ -65,10 +65,6 @@ import { ITool, type ToolBuilder } from ".."; * ``` */ export abstract class AI extends ITool { - static Init(_tools: ToolBuilder, _options?: any): Record { - return {}; - } - /** * Sends a request to an AI model and returns the response using the Vercel AI SDK. * diff --git a/sdk/src/tools/callbacks.ts b/sdk/src/tools/callbacks.ts index cdf4044..ed4937c 100644 --- a/sdk/src/tools/callbacks.ts +++ b/sdk/src/tools/callbacks.ts @@ -55,23 +55,17 @@ export type Callback = string & { readonly __brand: "Callback" }; * } * ``` */ -export abstract class Callbacks extends ITool { +export abstract class Callbacks extends ITool { /** - * Creates a persistent callback to a method on TParent (the current class). - * ExtraArgs are strongly typed to match the function signature after the first arg. + * Creates a persistent callback to a method on the current class. * - * @param fn - The function to callback on TParent - * @param extraArgs - Additional arguments to pass to the function (type-checked, must be serializable) + * @param fn - The function to callback + * @param extraArgs - Additional arguments to pass to the function (must be serializable) * @returns Promise resolving to a persistent callback token */ - abstract create< - K extends CallbackMethods, - TFn extends TParent[K] = TParent[K] - >( - _fn: TFn, - ..._extraArgs: TFn extends (arg: any, ...rest: infer R) => any - ? NoFunctions - : [] + abstract create( + _fn: Function, + ..._extraArgs: any[] ): Promise; /** @@ -82,9 +76,9 @@ export abstract class Callbacks extends ITool { * @param extraArgs - Additional arguments to pass to the function (must be serializable, validated at runtime) * @returns Promise resolving to a persistent callback token */ - abstract createParent( + abstract createFromParent( _fn: Function, - ..._extraArgs: NonFunction[] + ..._extraArgs: any[] ): Promise; /** diff --git a/sdk/src/tools/index.ts b/sdk/src/tools/index.ts index 0372699..0baf6b4 100644 --- a/sdk/src/tools/index.ts +++ b/sdk/src/tools/index.ts @@ -1,4 +1,4 @@ -export * from "./agent"; +export * from "./agents"; export * from "./ai"; export * from "./callbacks"; export * from "./integrations"; diff --git a/sdk/src/tools/integrations.ts b/sdk/src/tools/integrations.ts index 2f790f2..108d258 100644 --- a/sdk/src/tools/integrations.ts +++ b/sdk/src/tools/integrations.ts @@ -1,4 +1,4 @@ -import { type ActivityLink, ITool, type ToolBuilder } from ".."; +import { type ActivityLink, ITool } from ".."; import { type NoFunctions } from "./callbacks"; /** @@ -36,10 +36,6 @@ import { type NoFunctions } from "./callbacks"; * ``` */ export abstract class Integrations extends ITool { - static Init(_tools: ToolBuilder, _options?: any): Record { - return {}; - } - /** * Initiates an OAuth authentication flow. * diff --git a/sdk/src/tools/network.ts b/sdk/src/tools/network.ts index aedc2ea..0b9bd81 100644 --- a/sdk/src/tools/network.ts +++ b/sdk/src/tools/network.ts @@ -1,4 +1,4 @@ -import { ITool, type ToolBuilder } from ".."; +import { ITool } from ".."; /** * Represents an incoming webhook request. @@ -54,10 +54,10 @@ export type WebhookRequest = { * @example * ```typescript * class MyAgent extends Agent { - * static Init(tools: ToolBuilder) { + * build(build: ToolBuilder) { * return { * // Request HTTP access to specific APIs - * network: tools.init(Network, { + * network: build(Network, { * urls: [ * 'https://api.github.com/*', * 'https://api.openai.com/*' @@ -71,9 +71,9 @@ export type WebhookRequest = { * @example * ```typescript * class CalendarTool extends Tool { - * static Init(tools: ToolBuilder) { + * build(build: ToolBuilder) { * return { - * network: tools.init(Network, { + * network: build(Network, { * urls: ['https://www.googleapis.com/calendar/*'] * }) * }; @@ -110,9 +110,13 @@ export type WebhookRequest = { * ``` */ export abstract class Network extends ITool { - static Init(_tools: ToolBuilder, _options?: any): Record { - return {}; - } + static readonly Options: { + /** + * All network access is blocked except the specified URLs. + * Wildcards (*) are supported for domains and paths. + */ + urls: string[]; + }; /** * Creates a new webhook endpoint. diff --git a/sdk/src/tools/plot.ts b/sdk/src/tools/plot.ts index 0a9ea5c..f766bcd 100644 --- a/sdk/src/tools/plot.ts +++ b/sdk/src/tools/plot.ts @@ -1,15 +1,67 @@ import { type Activity, - type ActivitySource, + type ActivityMeta, type ActivityUpdate, + type ActorId, type Contact, ITool, type NewActivity, type NewPriority, type Priority, - type ToolBuilder, + type Tag, } from ".."; +/** + * Handler function for activity intent callbacks. + * Called when an activity with an at-mention matches a registered intent. + */ +export type IntentHandler = (activity: Activity) => Promise; + +/** + * Callbacks for activity events. + */ +export type PlotActivityCallbacks = { + /** + * Called when an activity is updated. + * + * @param activity - The updated activity + * @param changes - Optional changes object containing the previous version and tag modifications + */ + updated?: ( + activity: Activity, + changes?: { + previous: Activity; + tagsAdded: Record; + tagsRemoved: Record; + } + ) => Promise; + + /** + * Intent handlers for activity mentions. + * When an activity mentions this agent, the system will match the activity + * content against these intent descriptions and call the matching handler. + * + * @example + * ```typescript + * intents: { + * "Schedule or reschedule calendar events": this.onSchedulingRequest, + * "Find available meeting times": this.onAvailabilityRequest + * } + * ``` + */ + intents?: Record; +}; + +/** + * Options for configuring the Plot tool. + */ +export type PlotOptions = { + /** + * Activity event callbacks. + */ + activity?: PlotActivityCallbacks; +}; + /** * Built-in tool for interacting with the core Plot data layer. * @@ -43,9 +95,7 @@ import { * ``` */ export abstract class Plot extends ITool { - static Init(_tools: ToolBuilder, _options?: any): Record { - return {}; - } + static readonly Options: PlotOptions; /** * Creates a new activity in the Plot system. @@ -136,18 +186,16 @@ export abstract class Plot extends ITool { abstract getThread(_activity: Activity): Promise; /** - * Finds an activity by its external source reference. + * Finds an activity by its metadata. * * This method enables lookup of activities that were created from external - * systems, using the source information to locate the corresponding Plot activity. + * systems, using the metadata to locate the corresponding Plot activity. * Useful for preventing duplicate imports and maintaining sync state. * - * @param source - The external source reference to search for + * @param meta - The activity metadata to search for * @returns Promise resolving to the matching activity or null if not found */ - abstract getActivityBySource( - _source: ActivitySource - ): Promise; + abstract getActivityByMeta(_meta: ActivityMeta): Promise; /** * Adds contacts to the Plot system. diff --git a/sdk/src/tools/store.ts b/sdk/src/tools/store.ts index 1cf1a60..b58990e 100644 --- a/sdk/src/tools/store.ts +++ b/sdk/src/tools/store.ts @@ -1,4 +1,4 @@ -import { ITool, type ToolBuilder } from ".."; +import { ITool } from ".."; /** * Built-in tool for persistent key-value storage. diff --git a/sdk/src/utils/types.ts b/sdk/src/utils/types.ts index b385840..120e934 100644 --- a/sdk/src/utils/types.ts +++ b/sdk/src/utils/types.ts @@ -8,14 +8,12 @@ * * @internal */ - -import type { ITool } from "../agent"; import type { Callbacks } from "../tools/callbacks"; import type { Store } from "../tools/store"; import type { Tasks } from "../tools/tasks"; // ============================================================================ -// Type utilities from agent.ts +// Type utilities for agent.ts // ============================================================================ /** @@ -27,93 +25,75 @@ export type PromiseValues = { }; /** - * Extracts the return type from a static Init method. + * Extracts the return type from an instance build method. */ -export type ExtractInitReturn = T extends { - Init: (...args: any[]) => infer R; +export type ExtractBuildReturn = T extends { + build: (...args: any[]) => infer R; } ? R - : never; - -/** - * Resolves the tools returned by Init, unwrapping any Promises. - */ -export type ResolveInitTools = PromiseValues>; + : {}; /** * Built-in tools available to all agents and tools. */ -export type BuiltInTools = { - callbacks: Callbacks infer I ? I : any>; +export type BuiltInTools = { + callbacks: Callbacks; store: Store; tasks: Tasks; -} & {}; // Counter reset with intersection +}; /** * Infers the complete set of tools available to an agent or tool, - * combining tools declared in Init with built-in tools. + * combining tools declared in build with built-in tools. */ -export type InferTools = T extends { - Init: (...args: any[]) => any; - new (...args: any[]): any; -} - ? ResolveInitTools & BuiltInTools - : never; +export type InferTools = PromiseValues> & BuiltInTools; /** - * Infers the options type from a static Init method's second parameter. + * Infers the options type from a constructor's second parameter. */ export type InferOptions = T extends { - Init: (tools: any, options?: infer O) => any; + Options: infer O; } ? O - : undefined; + : unknown; /** - * Constraint for types that have both Init static method and constructor. + * Function type for building tool dependencies. + * Used in build methods to request tool instances. */ -export type HasInit = { - Init(tools: ToolBuilder, ...args: any[]): any; - new (id: string, tools: any, ...args: any[]): any; -}; +export type ToolBuilder = any>( + ToolClass: TC, + options?: InferOptions +) => Promise>; /** - * Constructor type for Tool classes (can be abstract or concrete). + * Interface for managing tool initialization and lifecycle. + * Implemented by the agent runtime to provide tools to agents and tools. */ -export type ToolConstructor = - | (abstract new (id: string, tools: any, options?: any) => T) - | (new (id: string, tools: any, options?: any) => T); +export interface ToolShed { + /** + * Build function for requesting tool dependencies + */ + build: ToolBuilder; + + /** + * Whether tools are ready (all promises resolved) + */ + readonly ready: boolean; + + /** + * Wait for all tool promises to resolve + */ + waitForReady(): Promise; -/** - * Interface for accessing tool dependencies. - * Used in static Init methods to initialize tool dependencies. - */ -export interface ToolBuilder { /** - * Initializes a tool instance by its class reference, returning a promise. - * - * @template T - The expected type of the tool - * @template O - The options type expected by the tool's Init method - * @param ToolClass - The tool class reference with Init method - * @param options - Optional options to pass to the tool's Init method - * @returns Promise resolving to the tool instance - * @throws When the tool is not found or not properly configured + * Get resolved tools (throws if not ready) */ - init< - T extends ITool, - O = T extends { Init: (tools: any, options?: infer Opt) => any } - ? Opt - : never - >( - ToolClass: ToolConstructor & { - Init: (tools: ToolBuilder, options?: O) => any; - }, - options?: O - ): Promise; + getTools(): T; } // ============================================================================ -// Type utilities from callbacks.ts +// Type utilities for callbacks.ts // ============================================================================ /** diff --git a/sdk/tsconfig.base.json b/sdk/tsconfig.base.json index bfe6091..65ed0f9 100644 --- a/sdk/tsconfig.base.json +++ b/sdk/tsconfig.base.json @@ -22,7 +22,8 @@ "noUnusedParameters": false, "preserveWatchOutput": true, "skipLibCheck": true, - "strict": true + "strict": true, + "disableSizeLimit": true }, "exclude": ["node_modules"] } diff --git a/tools/google-calendar/package.json b/tools/google-calendar/package.json index 182d001..fc6141b 100644 --- a/tools/google-calendar/package.json +++ b/tools/google-calendar/package.json @@ -20,7 +20,7 @@ "LICENSE" ], "scripts": { - "build": "tsc --project tsconfig.build.json", + "build": "tsc", "clean": "rm -rf dist" }, "dependencies": { diff --git a/tools/google-calendar/src/google-api.ts b/tools/google-calendar/src/google-api.ts index ef6c18d..2c24d78 100644 --- a/tools/google-calendar/src/google-api.ts +++ b/tools/google-calendar/src/google-api.ts @@ -194,8 +194,8 @@ export function transformGoogleEvent( note: event.description || null, start, end, - source: { - type: "google-calendar-event", + meta: { + source: `google-calendar:${event.id}`, id: event.id, calendarId: calendarId, htmlLink: event.htmlLink, @@ -290,4 +290,3 @@ export async function syncGoogleCalendar( state: nextState, }; } - diff --git a/tools/google-calendar/src/google-calendar.ts b/tools/google-calendar/src/google-calendar.ts index 9c822cb..278391c 100644 --- a/tools/google-calendar/src/google-calendar.ts +++ b/tools/google-calendar/src/google-calendar.ts @@ -95,13 +95,13 @@ import { * ``` */ export class GoogleCalendar - extends Tool + extends Tool implements CalendarTool { - static Init(tools: ToolBuilder) { + build(build: ToolBuilder) { return { - integrations: tools.init(Integrations), - network: tools.init(Network, { + integrations: build(Integrations), + network: build(Network, { urls: ["https://www.googleapis.com/calendar/*"], }), }; @@ -119,7 +119,7 @@ export class GoogleCalendar // Generate opaque token for authorization const authToken = crypto.randomUUID(); - const callbackToken = await this.tools.callbacks.createParent( + const callbackToken = await this.tools.callbacks.createFromParent( callback, ...extraArgs ); @@ -188,7 +188,7 @@ export class GoogleCalendar console.log("Saving callback"); // Create callback token for parent - const callbackToken = await this.tools.callbacks.createParent( + const callbackToken = await this.tools.callbacks.createFromParent( callback, ...extraArgs ); @@ -382,7 +382,7 @@ export class GoogleCalendar recurrenceDates: activityData.recurrenceDates || null, recurrence: null, occurrence: null, - source: activityData.source || null, + meta: activityData.meta ?? null, }; await this.run(callbackToken as any, activity); @@ -428,7 +428,7 @@ export class GoogleCalendar recurrenceDates: null, recurrence: null, // Would need to find master activity occurrence: new Date(originalStartTime), - source: activityData.source || null, + meta: activityData.meta ?? null, }; await this.run(callbackToken as any, activity); diff --git a/tools/google-calendar/tsconfig.build.json b/tools/google-calendar/tsconfig.build.json deleted file mode 100644 index a057809..0000000 --- a/tools/google-calendar/tsconfig.build.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src", - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "noEmit": false - }, - "include": ["src/**/*.ts"] -} diff --git a/tools/google-calendar/tsconfig.json b/tools/google-calendar/tsconfig.json index 99ee4a9..14c8a2b 100644 --- a/tools/google-calendar/tsconfig.json +++ b/tools/google-calendar/tsconfig.json @@ -1,8 +1,5 @@ { "$schema": "https://json.schemastore.org/tsconfig", "extends": "@plotday/sdk/tsconfig.base.json", - "compilerOptions": { - "moduleResolution": "bundler" - }, "include": ["src/**/*.ts"] } diff --git a/tools/google-contacts/package.json b/tools/google-contacts/package.json index 47b3847..f47ff12 100644 --- a/tools/google-contacts/package.json +++ b/tools/google-contacts/package.json @@ -20,7 +20,7 @@ "LICENSE" ], "scripts": { - "build": "tsc --project tsconfig.build.json", + "build": "tsc", "clean": "rm -rf dist" }, "dependencies": { diff --git a/tools/google-contacts/src/google-contacts.ts b/tools/google-contacts/src/google-contacts.ts index 46a969c..e53c978 100644 --- a/tools/google-contacts/src/google-contacts.ts +++ b/tools/google-contacts/src/google-contacts.ts @@ -249,14 +249,14 @@ async function getGoogleContacts( } export default class GoogleContacts - extends Tool + extends Tool implements IGoogleContacts { static readonly id = "google-contacts"; - static Init(tools: ToolBuilder) { + build(build: ToolBuilder) { return { - integrations: tools.init(Integrations), + integrations: build(Integrations), }; } @@ -271,7 +271,7 @@ export default class GoogleContacts const opaqueToken = crypto.randomUUID(); // Create callback token for parent - const callbackToken = await this.tools.callbacks.createParent( + const callbackToken = await (this.tools as any).callbacks.createFromParent( callback, ...extraArgs ); @@ -324,7 +324,7 @@ export default class GoogleContacts } // Create callback token for parent - const callbackToken = await this.tools.callbacks.createParent( + const callbackToken = await this.tools.callbacks.createFromParent( callback, ...extraArgs ); diff --git a/tools/google-contacts/src/types.ts b/tools/google-contacts/src/types.ts index 0583d61..4545ad5 100644 --- a/tools/google-contacts/src/types.ts +++ b/tools/google-contacts/src/types.ts @@ -13,9 +13,7 @@ export type ContactAuth = { export interface GoogleContacts extends ITool { requestAuth any>( callback: TCallback, - ...extraArgs: TCallback extends (auth: any, ...rest: infer R) => any - ? R - : [] + ...extraArgs: any[] ): Promise; getContacts(authToken: string): Promise; @@ -23,9 +21,7 @@ export interface GoogleContacts extends ITool { startSync any>( authToken: string, callback: TCallback, - ...extraArgs: TCallback extends (contacts: any, ...rest: infer R) => any - ? R - : [] + ...extraArgs: any[] ): Promise; stopSync(authToken: string): Promise; diff --git a/tools/google-contacts/tsconfig.build.json b/tools/google-contacts/tsconfig.build.json deleted file mode 100644 index a057809..0000000 --- a/tools/google-contacts/tsconfig.build.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src", - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "noEmit": false - }, - "include": ["src/**/*.ts"] -} diff --git a/tools/google-contacts/tsconfig.json b/tools/google-contacts/tsconfig.json index 99ee4a9..14c8a2b 100644 --- a/tools/google-contacts/tsconfig.json +++ b/tools/google-contacts/tsconfig.json @@ -1,8 +1,5 @@ { "$schema": "https://json.schemastore.org/tsconfig", "extends": "@plotday/sdk/tsconfig.base.json", - "compilerOptions": { - "moduleResolution": "bundler" - }, "include": ["src/**/*.ts"] } diff --git a/tools/outlook-calendar/package.json b/tools/outlook-calendar/package.json index e7096a3..659632b 100644 --- a/tools/outlook-calendar/package.json +++ b/tools/outlook-calendar/package.json @@ -20,7 +20,7 @@ "LICENSE" ], "scripts": { - "build": "tsc --project tsconfig.build.json", + "build": "tsc", "clean": "rm -rf dist" }, "dependencies": { diff --git a/tools/outlook-calendar/src/outlook-calendar.ts b/tools/outlook-calendar/src/outlook-calendar.ts index 9a3b478..1d5830d 100644 --- a/tools/outlook-calendar/src/outlook-calendar.ts +++ b/tools/outlook-calendar/src/outlook-calendar.ts @@ -219,13 +219,13 @@ const outlookApi = { * ``` */ export class OutlookCalendar - extends Tool + extends Tool implements CalendarTool { - static Init(tools: ToolBuilder, _options: any) { + build(build: ToolBuilder) { return { - integrations: tools.init(Integrations), - network: tools.init(Network), + integrations: build(Integrations), + network: build(Network), }; } @@ -236,7 +236,7 @@ export class OutlookCalendar const token = crypto.randomUUID(); // Create callback token for parent - const callbackToken = await this.tools.callbacks.createParent( + const callbackToken = await this.tools.callbacks.createFromParent( callback, ...extraArgs ); @@ -310,7 +310,7 @@ export class OutlookCalendar ...extraArgs: any[] ): Promise { // Create callback token for parent - const callbackToken = await this.tools.callbacks.createParent( + const callbackToken = await this.tools.callbacks.createFromParent( callback, ...extraArgs ); @@ -489,8 +489,8 @@ export class OutlookCalendar recurrenceDates: null, recurrence: null, occurrence: null, - source: { - type: "outlook-calendar-event", + meta: { + source: `outlook-calendar:${event.id}`, id: event.id, calendarId: syncState.calendarId, }, diff --git a/tools/outlook-calendar/tsconfig.build.json b/tools/outlook-calendar/tsconfig.build.json deleted file mode 100644 index a057809..0000000 --- a/tools/outlook-calendar/tsconfig.build.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src", - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "noEmit": false - }, - "include": ["src/**/*.ts"] -} diff --git a/tools/outlook-calendar/tsconfig.json b/tools/outlook-calendar/tsconfig.json index 99ee4a9..14c8a2b 100644 --- a/tools/outlook-calendar/tsconfig.json +++ b/tools/outlook-calendar/tsconfig.json @@ -1,8 +1,5 @@ { "$schema": "https://json.schemastore.org/tsconfig", "extends": "@plotday/sdk/tsconfig.base.json", - "compilerOptions": { - "moduleResolution": "bundler" - }, "include": ["src/**/*.ts"] } From b3242e4adecea87011379ac2dd58712dc91729d7 Mon Sep 17 00:00:00 2001 From: Kris Braun Date: Sun, 26 Oct 2025 23:31:46 -0400 Subject: [PATCH 5/5] Require permissions for Plot access --- .changeset/full-queens-ask.md | 8 + agents/chat/src/index.ts | 183 +++++++++--------- agents/events/src/index.ts | 8 +- sdk/src/tools/agents.ts | 19 +- sdk/src/tools/plot.ts | 160 ++++++++------- .../outlook-calendar/src/outlook-calendar.ts | 4 +- 6 files changed, 212 insertions(+), 170 deletions(-) create mode 100644 .changeset/full-queens-ask.md diff --git a/.changeset/full-queens-ask.md b/.changeset/full-queens-ask.md new file mode 100644 index 0000000..70e4d32 --- /dev/null +++ b/.changeset/full-queens-ask.md @@ -0,0 +1,8 @@ +--- +"@plotday/tool-outlook-calendar": minor +"@plotday/tool-google-calendar": minor +"@plotday/tool-google-contacts": minor +"@plotday/sdk": minor +--- + +Changed: BREAKING: Creating and updating Activity using the Plot tool now requires requesting permission in options diff --git a/agents/chat/src/index.ts b/agents/chat/src/index.ts index 9f02a56..39042d0 100644 --- a/agents/chat/src/index.ts +++ b/agents/chat/src/index.ts @@ -9,124 +9,115 @@ import { type ToolBuilder, } from "@plotday/sdk"; import { AI, type AIMessage } from "@plotday/sdk/tools/ai"; -import { Plot } from "@plotday/sdk/tools/plot"; +import { ActivityAccess, Plot } from "@plotday/sdk/tools/plot"; export default class ChatAgent extends Agent { build(build: ToolBuilder) { return { ai: build(AI), - plot: build(Plot), + plot: build(Plot, { + activity: { + access: ActivityAccess.Respond, + intents: { + "Respond to general questions and requests": this.responsd, + }, + }, + }), }; } - async activity( - activity: Activity, - changes?: { - previous: Activity; - tagsAdded: Record; - tagsRemoved: Record; - } - ) { - if (changes) return; - + async responsd(activity: Activity) { const previousActivities = await this.tools.plot.getThread(activity); - if ( - activity.note?.includes("@chat") || - previousActivities.some((activity: any) => - activity.note.includes("@chat") - ) - ) { - // Add Thinking tag to indicate processing has started - await this.tools.plot.updateActivity({ - id: activity.id, - tags: { - [Tag.Agent]: true, - }, - }); + // Add Thinking tag to indicate processing has started + await this.tools.plot.updateActivity({ + id: activity.id, + tags: { + [Tag.Agent]: true, + }, + }); - const messages: AIMessage[] = [ - { - role: "system", - content: `You are an AI assistant inside of a productivity app. + const messages: AIMessage[] = [ + { + role: "system", + content: `You are an AI assistant inside of a productivity app. You respond helpfully to user requests. You can also create tasks, but should only do so when the user explicitly asks you to.`, - }, - ...previousActivities - .filter((a) => a.note ?? a.title) - .map( - (prevActivity) => - ({ - role: - prevActivity.author.type === AuthorType.Agent - ? "assistant" - : "user", - content: (prevActivity.note ?? prevActivity.title)!, - } satisfies AIMessage) - ), - ]; + }, + ...previousActivities + .filter((a) => a.note ?? a.title) + .map( + (prevActivity) => + ({ + role: + prevActivity.author.type === AuthorType.Agent + ? "assistant" + : "user", + content: (prevActivity.note ?? prevActivity.title)!, + } satisfies AIMessage) + ), + ]; - const schema = Type.Object({ - message: Type.Object({ - note: Type.String({ description: "Response to the user's prompt" }), - title: Type.String({ - description: "Short title for the response notee", - }), + const schema = Type.Object({ + message: Type.Object({ + note: Type.String({ description: "Response to the user's prompt" }), + title: Type.String({ + description: "Short title for the response notee", }), - action_items: Type.Optional( - Type.Array( - Type.Object({ - note: Type.Optional( - Type.String({ - description: - "Optional detailed description of the action item. Can include markdown. Only add when important details are needed beyond the title.", - }) - ), - title: Type.String({ + }), + action_items: Type.Optional( + Type.Array( + Type.Object({ + note: Type.Optional( + Type.String({ description: - "Succinct description of the action item (no markdown)", - }), + "Optional detailed description of the action item. Can include markdown. Only add when important details are needed beyond the title.", + }) + ), + title: Type.String({ + description: + "Succinct description of the action item (no markdown)", }), - { - description: "Tasks to create in response to the user's request.", - } - ) - ), - }); + }), + { + description: "Tasks to create in response to the user's request.", + } + ) + ), + }); - const response = await this.tools.ai.prompt({ - model: { speed: "balanced", cost: "low" }, - messages, - outputSchema: schema, - }); + const response = await this.tools.ai.prompt({ + model: { speed: "balanced", cost: "low" }, + messages, + outputSchema: schema, + }); - await Promise.all([ + await Promise.all([ + this.tools.plot.createActivity({ + title: response.output!.message.title, + note: response.output!.message.note, + parent: activity, + priority: activity.priority, + type: activity.type, + }), + ...(response.output!.action_items?.map((item: any) => this.tools.plot.createActivity({ - title: response.output!.message.title, - note: response.output!.message.note, + title: item.title, + note: item.note, parent: activity, priority: activity.priority, - type: activity.type, - }), - ...(response.output!.action_items?.map((item: any) => - this.tools.plot.createActivity({ - title: item.title, - note: item.note, - parent: activity, - priority: activity.priority, - type: ActivityType.Task, - start: new Date(), - }) - ) ?? []), - ]); + type: ActivityType.Task, + start: new Date(), + }) + ) ?? []), + ]); - // Remove Thinking tag after response is created - await this.tools.plot.updateActivity({ - id: activity.id, - tags: { - [Tag.Agent]: false, - }, - }); - } + // Remove Thinking tag after response is created + await this.tools.plot.updateActivity({ + id: activity.id, + tags: { + [Tag.Agent]: false, + }, + }); } } diff --git a/agents/events/src/index.ts b/agents/events/src/index.ts index 6a1888b..5b013d8 100644 --- a/agents/events/src/index.ts +++ b/agents/events/src/index.ts @@ -13,7 +13,7 @@ import type { CalendarTool, SyncOptions, } from "@plotday/sdk/common/calendar"; -import { Plot } from "@plotday/sdk/tools/plot"; +import { ActivityAccess, Plot } from "@plotday/sdk/tools/plot"; import { GoogleCalendar } from "@plotday/tool-google-calendar"; import { OutlookCalendar } from "@plotday/tool-outlook-calendar"; @@ -29,7 +29,11 @@ export default class EventsAgent extends Agent { return { googleCalendar: build(GoogleCalendar), outlookCalendar: build(OutlookCalendar), - plot: build(Plot), + plot: build(Plot, { + activity: { + access: ActivityAccess.Create, + }, + }), }; } diff --git a/sdk/src/tools/agents.ts b/sdk/src/tools/agents.ts index 7acd000..6899bcb 100644 --- a/sdk/src/tools/agents.ts +++ b/sdk/src/tools/agents.ts @@ -30,17 +30,28 @@ export type Log = { /** * Agent permissions returned after deployment. - * Maps tool names to their respective permission configurations. + * Nested structure mapping domains to entities to permission flags. + * + * Format: { domain: { entity: flags[] } } + * - domain: Tool name (e.g., "network", "plot") + * - entity: Domain-specific identifier (e.g., URL pattern, resource type) + * - flags: Array of permission flags ("read", "write", "update", "use") * * @example * ```typescript * { - * "Network": { urls: ["https://api.example.com"] }, - * "Integrations": { provider: "google" } + * "network": { + * "https://api.example.com/*": ["use"], + * "https://googleapis.com/*": ["use"] + * }, + * "plot": { + * "activity:mentioned": ["read", "write", "update"], + * "priority": ["read", "write", "update"] + * } * } * ``` */ -export type AgentPermissions = Record; +export type AgentPermissions = Record>; /** * Built-in tool for managing agents and deployments. diff --git a/sdk/src/tools/plot.ts b/sdk/src/tools/plot.ts index f766bcd..78b2ab5 100644 --- a/sdk/src/tools/plot.ts +++ b/sdk/src/tools/plot.ts @@ -11,56 +11,39 @@ import { type Tag, } from ".."; -/** - * Handler function for activity intent callbacks. - * Called when an activity with an at-mention matches a registered intent. - */ -export type IntentHandler = (activity: Activity) => Promise; - -/** - * Callbacks for activity events. - */ -export type PlotActivityCallbacks = { +export enum ActivityAccess { /** - * Called when an activity is updated. - * - * @param activity - The updated activity - * @param changes - Optional changes object containing the previous version and tag modifications + * Create new Activity on a thread where the agent was mentioned. + * Add/remove tags on Activity where the agent was mentioned. */ - updated?: ( - activity: Activity, - changes?: { - previous: Activity; - tagsAdded: Record; - tagsRemoved: Record; - } - ) => Promise; - + Respond, /** - * Intent handlers for activity mentions. - * When an activity mentions this agent, the system will match the activity - * content against these intent descriptions and call the matching handler. - * - * @example - * ```typescript - * intents: { - * "Schedule or reschedule calendar events": this.onSchedulingRequest, - * "Find available meeting times": this.onAvailabilityRequest - * } - * ``` + * Create new, top-level Activity. + * Create new Activity in a thread the agent created. + * All Respond permissions. */ - intents?: Record; -}; + Create, +} -/** - * Options for configuring the Plot tool. - */ -export type PlotOptions = { +export enum PriorityAccess { + /** + * Create new priority. + * Update Priority created by the agent. + */ + Create, /** - * Activity event callbacks. + * Update and archive Priority created by others. + * All Create permissions. */ - activity?: PlotActivityCallbacks; -}; + Full, +} + +export enum ContactAccess { + /** Read existing contacts. */ + Read, + /** Create and update contacts. */ + Write, +} /** * Built-in tool for interacting with the core Plot data layer. @@ -95,7 +78,50 @@ export type PlotOptions = { * ``` */ export abstract class Plot extends ITool { - static readonly Options: PlotOptions; + static readonly Options: { + /** + * Activity event callbacks. + */ + activity?: { + access?: ActivityAccess; + + /** + * Called when an activity is updated. + * + * @param activity - The updated activity + * @param changes - Optional changes object containing the previous version and tag modifications + */ + updated?: ( + activity: Activity, + changes?: { + previous: Activity; + tagsAdded: Record; + tagsRemoved: Record; + } + ) => Promise; + + /** + * Intent handlers for activity mentions. + * When an activity mentions this agent, the system will match the activity + * content against these intent descriptions and call the matching handler. + * + * @example + * ```typescript + * intents: { + * "Schedule or reschedule calendar events": this.onSchedulingRequest, + * "Find available meeting times": this.onAvailabilityRequest + * } + * ``` + */ + intents?: Record Promise>; + }; + priority?: { + access?: PriorityAccess; + }; + contact?: { + access?: ContactAccess; + }; + }; /** * Creates a new activity in the Plot system. @@ -109,6 +135,18 @@ export abstract class Plot extends ITool { */ abstract createActivity(_activity: NewActivity): Promise; + /** + * Creates multiple activities in a single batch operation. + * + * This method efficiently creates multiple activities at once, which is + * more performant than calling createActivity() multiple times individually. + * All activities are created with the same author and access control rules. + * + * @param activities - Array of activity data to create + * @returns Promise resolving to array of created activities + */ + abstract createActivities(_activities: NewActivity[]): Promise; + /** * Updates an existing activity in the Plot system. * @@ -162,17 +200,6 @@ export abstract class Plot extends ITool { */ abstract updateActivity(_activity: ActivityUpdate): Promise; - /** - * Creates a new priority in the Plot system. - * - * Priorities serve as organizational containers for activities and agents. - * The created priority will be automatically assigned a unique ID. - * - * @param priority - The priority data to create - * @returns Promise resolving to the complete created priority - */ - abstract createPriority(_priority: NewPriority): Promise; - /** * Retrieves all activities in the same thread as the specified activity. * @@ -197,6 +224,17 @@ export abstract class Plot extends ITool { */ abstract getActivityByMeta(_meta: ActivityMeta): Promise; + /** + * Creates a new priority in the Plot system. + * + * Priorities serve as organizational containers for activities and agents. + * The created priority will be automatically assigned a unique ID. + * + * @param priority - The priority data to create + * @returns Promise resolving to the complete created priority + */ + abstract createPriority(_priority: NewPriority): Promise; + /** * Adds contacts to the Plot system. * @@ -208,16 +246,4 @@ export abstract class Plot extends ITool { * @returns Promise that resolves when all contacts have been processed */ abstract addContacts(_contacts: Array): Promise; - - /** - * Creates multiple activities in a single batch operation. - * - * This method efficiently creates multiple activities at once, which is - * more performant than calling createActivity() multiple times individually. - * All activities are created with the same author and access control rules. - * - * @param activities - Array of activity data to create - * @returns Promise resolving to array of created activities - */ - abstract createActivities(_activities: NewActivity[]): Promise; } diff --git a/tools/outlook-calendar/src/outlook-calendar.ts b/tools/outlook-calendar/src/outlook-calendar.ts index 1d5830d..3b60936 100644 --- a/tools/outlook-calendar/src/outlook-calendar.ts +++ b/tools/outlook-calendar/src/outlook-calendar.ts @@ -225,7 +225,9 @@ export class OutlookCalendar build(build: ToolBuilder) { return { integrations: build(Integrations), - network: build(Network), + network: build(Network, { + urls: ["https://graph.microsoft.com/*"], + }), }; }