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/.changeset/fuzzy-beans-thank.md b/.changeset/fuzzy-beans-thank.md new file mode 100644 index 0000000..3405294 --- /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 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. +Changed: BREAKING: Run renamed Tasks. 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/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/.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/chat/src/index.ts b/agents/chat/src/index.ts index 478d1a5..39042d0 100644 --- a/agents/chat/src/index.ts +++ b/agents/chat/src/index.ts @@ -6,129 +6,118 @@ 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"; +import { ActivityAccess, 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 { + build(build: ToolBuilder) { + return { + ai: build(AI), + 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; - - const previousActivities = await this.plot.getThread(activity); + 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.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.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([ - this.plot.createActivity({ - title: response.output!.message.title, - note: response.output!.message.note, + 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: item.title, + note: item.note, parent: activity, priority: activity.priority, - type: activity.type, - }), - ...(response.output!.action_items?.map((item: any) => - this.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.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 01736df..5b013d8 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, @@ -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"; @@ -24,31 +24,25 @@ type StoredCalendarAuth = { authToken: string; }; -type CalendarSelectionContext = { - provider: CalendarProvider; - calendarId: string; - calendarName: string; - 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 { + build(build: ToolBuilder) { + return { + googleCalendar: build(GoogleCalendar), + outlookCalendar: build(OutlookCalendar), + plot: build(Plot, { + activity: { + access: ActivityAccess.Create, + }, + }), + }; } 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}`); } @@ -89,24 +83,18 @@ export default class 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.googleCalendar.requestAuth( - googleCallback + const googleAuthLink = await this.tools.googleCalendar.requestAuth( + this.onAuthComplete, + "google" ); - const outlookAuthLink = await this.outlookCalendar.requestAuth( - outlookCallback + const outlookAuthLink = await this.tools.outlookCalendar.requestAuth( + this.onAuthComplete, + "outlook" ); // 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(), @@ -131,7 +119,7 @@ export default class extends Agent { async startSync( provider: CalendarProvider, calendarId: string, - options?: SyncOptions + _options?: SyncOptions ): Promise { const authToken = await this.getAuthToken(provider); if (!authToken) { @@ -140,13 +128,14 @@ export default class 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( @@ -180,12 +169,18 @@ export default class extends Agent { return results; } - async handleEvent(activity: Activity, _context?: any): Promise { - await this.plot.createActivity(activity); + 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; @@ -193,7 +188,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 +195,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(), @@ -229,12 +223,13 @@ export default class 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({ @@ -252,7 +247,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(), @@ -263,42 +258,40 @@ export default class 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.plot.createActivity({ + 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/README.md b/sdk/README.md index c9c04c8..95abb20 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -94,20 +94,25 @@ 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 { + build(build: ToolBuilder) { + return { + plot: build(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 +175,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 `build` 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. +build(build: ToolBuilder) { + return { + plot: build(Plot), + googleCalendar: build(GoogleCalendar), + }; } +// Store, Tasks, and Callbacks methods are available directly: +// this.get(), this.set(), this.callback(), this.run(), etc. ``` ### Plot @@ -193,21 +199,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 +234,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 +255,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 +279,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 build method +build(build: ToolBuilder) { + return { + network: build(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 +308,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) @@ -307,12 +322,12 @@ const callback = await this.callback("handleEvent", { }); // Execute callback -const result = await this.callCallback(callback, { +const result = await this.run(callback, { data: eventData, }); // Delete callback -await this.deleteCallback(token); +await this.deleteCallback(callback); await this.deleteAllCallbacks(); // Delete all ``` @@ -325,7 +340,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 +357,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 +368,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 +400,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/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/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/commands/deploy.ts b/sdk/cli/commands/deploy.ts index 9f3cfc1..f32f869 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"); @@ -194,6 +187,7 @@ export async function deployCommand(options: DeployOptions) { // Build the agent let requestBody: { module: string; + sourcemap?: string; name: string; description?: string; environment: string; @@ -213,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"); @@ -226,6 +221,7 @@ export async function deployCommand(options: DeployOptions) { requestBody = { module: moduleContent, + sourcemap: sourcemapContent, name: deploymentName!, description: deploymentDescription, environment: environment, @@ -280,6 +276,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 +291,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..98efe18 100644 --- a/sdk/cli/templates/AGENTS.template.md +++ b/sdk/cli/templates/AGENTS.template.md @@ -20,16 +20,19 @@ 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 { + build(build: ToolBuilder) { + return { + plot: build(Plot), + }; } async activate(priority: Pick) { @@ -48,18 +51,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 `build` method: ```typescript -constructor(id: string, protected tools: Tools) { - super(id, tools); - this.toolName = tools.get(ToolClass); +build(build: ToolBuilder) { + return { + toolName: build(ToolClass), + }; } ``` -All `tools.get()` calls must occur in the constructor 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.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 `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) @@ -71,11 +75,11 @@ 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/agent` - Manage other agents +- `@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/agents` - Manage other agents **Critical**: Never use instance variables for state. They are lost after function execution. Always use Store methods. @@ -108,10 +112,12 @@ 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 authLink = await this.tools.externalTool.requestAuth( + this.onAuthComplete, + "google" + ); - await this.plot.createActivity({ + await this.tools.plot.createActivity({ type: ActivityType.Task, title: "Connect your account", links: [authLink], @@ -122,7 +128,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 +144,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 +172,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(this.onLinkClicked, "context"); const callbackLink: ActivityLink = { title: "Click me", type: ActivityLinkType.callback, @@ -175,7 +181,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], @@ -188,30 +194,26 @@ Common pattern for OAuth authentication: ```typescript async activate(_priority: Pick) { - // Create callback for auth completion - const callback = await this.callback.create("onAuthComplete", { - provider: "google", - }); - - // Request auth link from tool - const authLink = await this.googleTool.requestAuth(callback); + // 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.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; - +async onAuthComplete(authResult: { authToken: string }, provider: string) { // 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 +226,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", { + await this.tools.calendarTool.startSync( + authToken, calendarId, - }); - - await this.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.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,12 +260,13 @@ private async createCalendarSelectionActivity( const links: ActivityLink[] = []; for (const calendar of calendars) { - const token = await this.callback.create("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)" : ""}`, @@ -272,21 +275,28 @@ private async createCalendarSelectionActivity( }); } - await this.plot.createActivity({ + await this.tools.plot.createActivity({ type: ActivityType.Note, title: "Which calendars would you like to connect?", links, }); } -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.create("handleEvent", { - provider: context.provider, - calendarId: context.calendarId, - }); - - await this.tool.startSync(context.authToken, context.calendarId, callback); + await this.tools.tool.startSync( + authToken, + calendarId, + this.handleEvent, + provider, + calendarId + ); } ``` @@ -312,40 +322,40 @@ 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); // Process results for (const item of result.items) { - await this.plot.createActivity(item); + await this.tools.plot.createActivity(item); } 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.plot.createActivity({ + await this.tools.plot.createActivity({ type: ActivityType.Note, note: `Sync complete: ${state.itemsProcessed + result.items.length} items processed`, }); @@ -363,7 +373,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 +385,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 `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. - **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..a3f93a7 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 `build` method: ```typescript -constructor(protected tools: Tools) { - super(id, tools); - this.plot = tools.get(Plot); - // Store, Run, and Callback methods are available directly via this +build(build: ToolBuilder) { + return { + plot: build(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); +build(build: ToolBuilder) { + return { + googleCalendar: build(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/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 639e19b..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,37 +30,41 @@ "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", "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/callbacks": { + "types": "./dist/tools/callbacks.d.ts", + "default": "./dist/tools/callbacks.js" }, - "./tools/callback": { - "types": "./dist/tools/callback.d.ts", - "default": "./dist/tools/callback.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" + "./utils/types": { + "types": "./dist/utils/types.d.ts", + "default": "./dist/utils/types.js" }, "./common/calendar": { "types": "./dist/common/calendar.d.ts", @@ -90,38 +98,38 @@ "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", "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/prebuild.ts b/sdk/prebuild.ts index 62cae90..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 @@ -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; } @@ -60,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 _ @@ -154,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 = `/** @@ -170,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 430f657..932b015 100644 --- a/sdk/src/agent.ts +++ b/sdk/src/agent.ts @@ -1,12 +1,7 @@ -import { type Activity, type Priority } from "./plot"; -import type { - Callback, - CallbackContext, - CallbackMethods, - CallbackTool, -} from "./tools/callback"; -import type { Run } from "./tools/run"; -import type { Store } from "./tools/store"; +import { type Priority } from "./plot"; +import { type ITool } from "./tool"; +import type { Callback } from "./tools/callbacks"; +import type { InferTools, ToolBuilder, ToolShed } from "./utils/types"; /** * Base class for all agents. @@ -14,62 +9,76 @@ import type { Store } from "./tools/store"; * 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 { - * private plot: Plot; - * - * constructor(id: string, tools: Tools) { - * super(id, tools); - * this.plot = tools.get(Plot); - * } + * build(build: ToolBuilder) { + * return { + * plot: build(Plot), + * }; + * } * * async activate(priority: Pick) { * // Initialize agent for the given priority - * await this.plot.createActivity({ - * type: ActivityType.Note, - * note: "Hello, good looking!", - * }); - * } - * - * async activity(activity: Activity) { - * // Process new activity + * await this.tools.plot.createActivity({ + * type: ActivityType.Note, + * note: "Hello, good looking!", + * }); * } * } * ``` */ -export abstract class Agent { - protected id: string; - private _callbackTool: CallbackTool; - private _store: Store; - private _runTool: Run; +export abstract class Agent { + constructor(protected id: string, private toolShed: ToolShed) {} - 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); + /** + * 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. * - * @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>( - functionName: K, - context?: CallbackContext + protected async callback( + fn: Function, + ...extraArgs: any[] ): Promise { - return this._callbackTool.create(functionName, context); + return this.tools.callbacks.create(fn as any, ...extraArgs); } /** @@ -79,7 +88,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 +97,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(); } /** @@ -98,8 +107,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._callbackTool.callCallback(token, args); + protected async run(token: Callback, ...args: []): Promise { + return this.tools.callbacks.run(token, ...args); } /** @@ -110,19 +119,38 @@ 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); } /** * 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._store.set(key, value); + return this.tools.store.set(key, value); } /** @@ -132,7 +160,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 +169,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(); } /** @@ -152,21 +180,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._runTool.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._runTool.cancel(token); + protected async cancelTask(token: string): Promise { + return this.tools.tasks.cancelTask(token); } /** @@ -174,8 +202,8 @@ export abstract class Agent { * * @returns Promise that resolves when all cancellations are processed */ - protected async cancelAll(): Promise { - return this._runTool.cancelAll(); + protected async cancelAllTasks(): Promise { + return this.tools.tasks.cancelAllTasks(); } /** @@ -192,253 +220,36 @@ export abstract class Agent { } /** - * Called when an activity needs to be processed by this agent. + * Called when a new version of the agent is deployed to an existing priority. * - * This method is invoked when activities are routed to this agent, - * either through explicit assignment or through filtering rules. + * 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. * - * @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 + * @returns Promise that resolves when upgrade is complete */ - activity( - _activity: Activity, - _changes?: { - previous: Activity; - tagsAdded: Record; - tagsRemoved: Record; - } - ): Promise { + upgrade(): 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 {} - -export type ToolConstructor = - | (abstract new (id: string, tools: Tools) => T) - | (new (id: string, tools: Tools) => T); - -/** - * Base class for regular tools. - * - * Regular tools run 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); - * } - * - * async getCalendars() { - * // 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); - } /** - * Creates a persistent callback to a method on this tool. + * Called when the agent is removed from a priority. * - * @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 - */ - protected async callback>( - functionName: K, - context?: CallbackContext - ): Promise { - return this._callbackTool.create(functionName, context); - } - - /** - * Deletes a specific callback by its token. + * This method should contain cleanup logic such as removing webhooks, + * cleaning up external resources, or performing final data operations. * - * @param token - The callback token to delete - * @returns Promise that resolves when the callback is deleted + * @returns Promise that resolves when deactivation is complete */ - protected async deleteCallback(token: Callback): Promise { - return this._callbackTool.delete(token); - } - - /** - * Deletes all callbacks for this tool. - * - * @returns Promise that resolves when all callbacks are deleted - */ - protected async deleteAllCallbacks(): Promise { - return this._callbackTool.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 callCallback(token: Callback, args?: any): Promise { - return this._callbackTool.callCallback(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._store.get(key); - } - - /** - * Stores a value in persistent storage. - * - * @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._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._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._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 run( - callback: Callback, - options?: { runAt?: Date } - ): Promise { - return this._runTool.run(callback, options); - } - - /** - * Cancels a previously scheduled execution. - * - * @param token - The cancellation token returned by run() with runAt option - * @returns Promise that resolves when the cancellation is processed - */ - protected async cancel(token: string): Promise { - return this._runTool.cancel(token); + deactivate(): Promise { + return Promise.resolve(); } /** - * Cancels all scheduled executions for this tool. - * - * @returns Promise that resolves when all cancellations are processed + * Waits for tool initialization to complete. + * Called automatically by the entrypoint before lifecycle methods. + * @internal */ - protected async cancelAll(): Promise { - return this._runTool.cancelAll(); + async waitForReady(): Promise { + await this.toolShed.waitForReady(); } } - -/** - * 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 Tools { - /** - * Retrieves a tool instance by its class reference. - * - * @template T - The expected type of the tool - * @param ToolClass - The tool class reference - * @returns 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; -} 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/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 721b670..6fb3fff 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 */ @@ -88,7 +96,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", @@ -151,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", @@ -167,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; }; @@ -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) */ @@ -281,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; + tags: Partial> | null; + /** Array of actor IDs (users, contacts, or agents) mentioned in this activity via @-mentions */ + mentions: ActorId[] | null; }; /** @@ -324,7 +334,7 @@ export type ActivityUpdate = Pick & | "doneAt" | "note" | "title" - | "source" + | "meta" | "links" | "recurrenceRule" | "recurrenceDates" @@ -332,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 86% rename from sdk/src/tools/agent.ts rename to sdk/src/tools/agents.ts index e936633..6899bcb 100644 --- a/sdk/src/tools/agent.ts +++ b/sdk/src/tools/agents.ts @@ -28,6 +28,31 @@ export type Log = { message: string; }; +/** + * Agent permissions returned after deployment. + * 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": { + * "https://api.example.com/*": ["use"], + * "https://googleapis.com/*": ["use"] + * }, + * "plot": { + * "activity:mentioned": ["read", "write", "update"], + * "priority": ["read", "write", "update"] + * } + * } + * ``` + */ +export type AgentPermissions = Record>; + /** * Built-in tool for managing agents and deployments. * @@ -37,21 +62,20 @@ export type Log = { * @example * ```typescript * class AgentBuilderAgent extends Agent { - * private agent: AgentManager; - * - * constructor(id: string, tools: Tools) { - * 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 AgentManager extends ITool { +export abstract class Agents extends ITool { /** * Creates a new agent ID and grants access to people in the current priority. * @@ -164,6 +188,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..8c9d0d4 100644 --- a/sdk/src/tools/ai.ts +++ b/sdk/src/tools/ai.ts @@ -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); * } diff --git a/sdk/src/tools/callback.ts b/sdk/src/tools/callback.ts deleted file mode 100644 index defee25..0000000 --- a/sdk/src/tools/callback.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { ITool, type Tools } from ".."; - -/** - * Represents a callback token for persistent function references. - * - * Callbacks enable tools and agents to create persistent references to functions - * that can survive worker restarts and be invoked across different execution contexts. - * - * This is a branded string type to prevent mixing callback tokens with regular strings. - * - * @example - * ```typescript - * const callback = await this.callback.create("onCalendarSelected", { - * calendarId: "primary", - * provider: "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 - * across worker invocations and restarts. This is essential for webhook handlers, - * scheduled operations, and user interaction flows that need to survive runtime - * boundaries. - * - * **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. - * - * **When to use callbacks:** - * - Webhook handlers that need persistent function references - * - Scheduled operations that run after worker timeouts - * - 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. - * - * @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 - * return `https://api.plot.day/webhook/${callback}`; - * } - * - * async handleWebhook(data: any, context?: { webhookType: string }) { - * console.log("Webhook received:", data, context); - * } - * } - * ``` - */ -export abstract class CallbackTool extends ITool { - /** - * Creates a persistent callback to the tool's parent. - * Returns a callback token that can be used to call the callback later. - * - * @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 - */ - abstract create>( - _functionName: K, - _context?: CallbackContext, - ): Promise; - - /** - * Deletes a specific callback by its token. - * - * @param callback - The callback token to delete - * @returns Promise that resolves when the callback is deleted - */ - abstract delete(_callback: Callback): Promise; - - /** - * Deletes all callbacks for the tool's parent. - * - * @returns Promise that resolves when all callbacks are deleted - */ - abstract deleteAll(): Promise; - - /** - * Executes a callback by its token. - * - * @param callback - The callback token returned by create() - * @param args - Optional arguments to pass to the callback function - * @returns Promise resolving to the callback result - */ - abstract callCallback(_callback: Callback, _args?: any): Promise; -} diff --git a/sdk/src/tools/callbacks.ts b/sdk/src/tools/callbacks.ts new file mode 100644 index 0000000..ed4937c --- /dev/null +++ b/sdk/src/tools/callbacks.ts @@ -0,0 +1,107 @@ +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. + * + * Callbacks enable tools and agents to create persistent references to functions + * that can survive worker restarts and be invoked across different execution contexts. + * + * This is a branded string type to prevent mixing callback tokens with regular strings. + * + * @example + * ```typescript + * const callback = await this.callback(this.onCalendarSelected, "primary", "google"); + * ``` + */ +export type Callback = string & { readonly __brand: "Callback" }; + +/** + * Built-in tool for creating and managing persistent callback references. + * + * 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. + * + * **Note:** Callback methods are also available directly on Agent and Tool classes + * via `this.callback()`, `this.deleteCallback()`, `this.deleteAllCallbacks()`, and + * `this.run()`. This is the recommended approach for most use cases. + * + * **When to use callbacks:** + * - Webhook handlers that need persistent function references + * - Scheduled operations that run after worker timeouts + * - User interaction links (ActivityLinkType.callback) + * - Cross-tool communication that survives restarts + * + * **Type Safety:** + * 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(this.handleWebhook, "calendar"); + * return `https://api.plot.day/webhook/${callback}`; + * } + * + * async handleWebhook(data: any, webhookType: string) { + * console.log("Webhook received:", data, webhookType); + * } + * } + * ``` + */ +export abstract class Callbacks extends ITool { + /** + * Creates a persistent callback to a method on the current class. + * + * @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( + _fn: Function, + ..._extraArgs: any[] + ): 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 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 createFromParent( + _fn: Function, + ..._extraArgs: any[] + ): Promise; + + /** + * Deletes a specific callback by its token. + * + * @param callback - The callback token to delete + * @returns Promise that resolves when the callback is deleted + */ + abstract delete(_callback: Callback): Promise; + + /** + * Deletes all callbacks for the tool's parent. + * + * @returns Promise that resolves when all callbacks are deleted + */ + abstract deleteAll(): Promise; + + /** + * Executes a callback by its token. + * + * @param callback - The callback token returned by create() + * @param args - Optional arguments to pass to the callback function + * @returns Promise resolving to the callback result + */ + abstract run(_callback: Callback, ..._args: any[]): Promise; +} diff --git a/sdk/src/tools/index.ts b/sdk/src/tools/index.ts index 2405d89..0baf6b4 100644 --- a/sdk/src/tools/index.ts +++ b/sdk/src/tools/index.ts @@ -1,8 +1,8 @@ -export * from "./agent"; +export * from "./agents"; 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 71% rename from sdk/src/tools/auth.ts rename to sdk/src/tools/integrations.ts index 9ebbdc3..108d258 100644 --- a/sdk/src/tools/auth.ts +++ b/sdk/src/tools/integrations.ts @@ -1,24 +1,25 @@ -import { type ActivityLink, type Callback, ITool } from ".."; +import { type ActivityLink, ITool } from ".."; +import { type NoFunctions } from "./callbacks"; /** * 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,33 +30,39 @@ 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 { /** * Initiates an OAuth authentication flow. * * 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; /** @@ -73,7 +80,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 +108,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/network.ts b/sdk/src/tools/network.ts new file mode 100644 index 0000000..0b9bd81 --- /dev/null +++ b/sdk/src/tools/network.ts @@ -0,0 +1,150 @@ +import { ITool } from ".."; + +/** + * 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 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 + * - Automatic callback routing to parent tool/agent + * - Support for all HTTP methods + * - Context preservation for callback execution + * + * @example + * ```typescript + * class MyAgent extends Agent { + * build(build: ToolBuilder) { + * return { + * // Request HTTP access to specific APIs + * network: build(Network, { + * urls: [ + * 'https://api.github.com/*', + * 'https://api.openai.com/*' + * ] + * }) + * }; + * } + * } + * ``` + * + * @example + * ```typescript + * class CalendarTool extends Tool { + * build(build: ToolBuilder) { + * return { + * network: build(Network, { + * urls: ['https://www.googleapis.com/calendar/*'] + * }) + * }; + * } + * + * async setupCalendarWebhook(calendarId: string) { + * // Create webhook URL that will call onCalendarEvent + * const webhookUrl = await this.tools.network.createWebhook("onCalendarEvent", { + * calendarId, + * provider: "google" + * }); + * + * // Register webhook with Google Calendar API + * await this.registerWithGoogleCalendar(calendarId, webhookUrl); + * + * return webhookUrl; + * } + * + * async onCalendarEvent(request: WebhookRequest, context: any) { + * console.log("Calendar event received:", { + * method: request.method, + * calendarId: context.calendarId, + * body: request.body + * }); + * + * // Process the calendar event change + * await this.processCalendarChange(request.body); + * } + * + * async cleanup(webhookUrl: string) { + * await this.tools.network.deleteWebhook(webhookUrl); + * } + * } + * ``` + */ +export abstract class Network extends ITool { + 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. + * + * Generates a unique HTTP endpoint that will invoke the callback function + * when requests are received. The callback receives the WebhookRequest plus any extraArgs. + * + * @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< + 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. + * + * Removes the webhook endpoint and stops processing requests to that URL. + * Any subsequent requests to the deleted webhook will return 404. + * + * @param url - The webhook URL to delete + * @returns Promise that resolves when the webhook is deleted + */ + abstract deleteWebhook(_url: string): Promise; +} diff --git a/sdk/src/tools/plot.ts b/sdk/src/tools/plot.ts index 95a015b..78b2ab5 100644 --- a/sdk/src/tools/plot.ts +++ b/sdk/src/tools/plot.ts @@ -1,14 +1,50 @@ import { type Activity, - type ActivitySource, + type ActivityMeta, type ActivityUpdate, + type ActorId, type Contact, ITool, type NewActivity, type NewPriority, type Priority, + type Tag, } from ".."; +export enum ActivityAccess { + /** + * Create new Activity on a thread where the agent was mentioned. + * Add/remove tags on Activity where the agent was mentioned. + */ + Respond, + /** + * Create new, top-level Activity. + * Create new Activity in a thread the agent created. + * All Respond permissions. + */ + Create, +} + +export enum PriorityAccess { + /** + * Create new priority. + * Update Priority created by the agent. + */ + Create, + /** + * Update and archive Priority created by others. + * All Create permissions. + */ + Full, +} + +export enum ContactAccess { + /** Read existing contacts. */ + Read, + /** Create and update contacts. */ + Write, +} + /** * Built-in tool for interacting with the core Plot data layer. * @@ -21,7 +57,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 +78,51 @@ import { * ``` */ export abstract class Plot extends ITool { + 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. * @@ -54,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. * @@ -107,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. * @@ -131,18 +213,27 @@ 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; + + /** + * 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. @@ -155,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/sdk/src/tools/store.ts b/sdk/src/tools/store.ts index fbad936..b58990e 100644 --- a/sdk/src/tools/store.ts +++ b/sdk/src/tools/store.ts @@ -1,4 +1,4 @@ -import { ITool, type Tools } from ".."; +import { ITool } 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 83% rename from sdk/src/tools/run.ts rename to sdk/src/tools/tasks.ts index e605d30..fd7fa72 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 } from ".."; +import type { Callback } from "./callbacks"; /** * Run background tasks and scheduled jobs. @@ -10,7 +10,7 @@ import type { Callback } from "./callback"; * 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 "./callback"; * * // 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 "./callback"; * const callback = await this.callback("processBatch", { * batchNumber: context.batchNumber + 1 * }); - * await this.run(callback); + * await this.runTask(callback); * } * } * @@ -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. * @@ -63,14 +63,14 @@ export abstract class Run 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) */ - abstract run( + abstract runTask( _callback: Callback, - _options?: { runAt?: Date }, + _options?: { runAt?: Date } ): Promise; /** @@ -79,10 +79,10 @@ export abstract class Run 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 Run extends ITool { * * @returns Promise that resolves when all cancellations are processed */ - abstract cancelAll(): Promise; + abstract cancelAllTasks(): Promise; } diff --git a/sdk/src/tools/webhook.ts b/sdk/src/tools/webhook.ts deleted file mode 100644 index 20b0a63..0000000 --- a/sdk/src/tools/webhook.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { ITool, type Tools } from ".."; - -/** - * Built-in tool for creating and managing webhook endpoints. - * - * 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. - * - * **Webhook Characteristics:** - * - Persistent across worker restarts - * - Automatic callback routing to parent tool/agent - * - Support for all HTTP methods - * - Context preservation for callback execution - * - * @example - * ```typescript - * class CalendarTool extends Tool { - * private webhook: Webhook; - * - * constructor(id: string, tools: Tools) { - * super(); - * this.webhook = tools.get(Webhook); - * } - * - * async setupCalendarWebhook(calendarId: string) { - * // Create webhook URL that will call onCalendarEvent - * const webhookUrl = await this.webhook.create("onCalendarEvent", { - * calendarId, - * provider: "google" - * }); - * - * // Register webhook with Google Calendar API - * await this.registerWithGoogleCalendar(calendarId, webhookUrl); - * - * return webhookUrl; - * } - * - * async onCalendarEvent(request: WebhookRequest, context: any) { - * console.log("Calendar event received:", { - * method: request.method, - * calendarId: context.calendarId, - * body: request.body - * }); - * - * // Process the calendar event change - * await this.processCalendarChange(request.body); - * } - * - * async cleanup(webhookUrl: string) { - * await this.webhook.delete(webhookUrl); - * } - * } - * ``` - */ -export abstract class Webhook 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. - * - * @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 - * @returns Promise resolving to the webhook URL - */ - abstract create(_callbackName: string, _context?: any): Promise; - - /** - * Deletes an existing webhook endpoint. - * - * Removes the webhook endpoint and stops processing requests to that URL. - * Any subsequent requests to the deleted webhook will return 404. - * - * @param url - The webhook URL to delete - * @returns Promise that resolves when the webhook is deleted - */ - abstract delete(_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/utils/types.ts b/sdk/src/utils/types.ts new file mode 100644 index 0000000..120e934 --- /dev/null +++ b/sdk/src/utils/types.ts @@ -0,0 +1,120 @@ +/** + * 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 { Callbacks } from "../tools/callbacks"; +import type { Store } from "../tools/store"; +import type { Tasks } from "../tools/tasks"; + +// ============================================================================ +// Type utilities for 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 an instance build method. + */ +export type ExtractBuildReturn = T extends { + build: (...args: any[]) => infer R; +} + ? R + : {}; + +/** + * Built-in tools available to all agents and tools. + */ +export type BuiltInTools = { + callbacks: Callbacks; + store: Store; + tasks: Tasks; +}; + +/** + * Infers the complete set of tools available to an agent or tool, + * combining tools declared in build with built-in tools. + */ +export type InferTools = PromiseValues> & BuiltInTools; + +/** + * Infers the options type from a constructor's second parameter. + */ +export type InferOptions = T extends { + Options: infer O; +} + ? O + : unknown; + +/** + * Function type for building tool dependencies. + * Used in build methods to request tool instances. + */ +export type ToolBuilder = any>( + ToolClass: TC, + options?: InferOptions +) => Promise>; + +/** + * Interface for managing tool initialization and lifecycle. + * Implemented by the agent runtime to provide tools to agents and tools. + */ +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; + + /** + * Get resolved tools (throws if not ready) + */ + getTools(): T; +} + +// ============================================================================ +// Type utilities for 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/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/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/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 b69d3cd..278391c 100644 --- a/tools/google-calendar/src/google-calendar.ts +++ b/tools/google-calendar/src/google-calendar.ts @@ -1,23 +1,23 @@ import { + type Activity, type ActivityLink, type NewActivity, Tool, - type Tools, + type ToolBuilder, } from "@plotday/sdk"; import { type Calendar, type CalendarAuth, 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, @@ -27,11 +27,6 @@ import { transformGoogleEvent, } from "./google-api"; -type AuthSuccessContext = { - authToken: string; - callbackToken: Callback; -}; - /** * Google Calendar integration tool. * @@ -99,42 +94,46 @@ 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 +{ + build(build: ToolBuilder) { + return { + integrations: build(Integrations), + network: build(Network, { + urls: ["https://www.googleapis.com/calendar/*"], + }), + }; } - 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 authorization + // 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.createFromParent( + callback, + ...extraArgs + ); // Request auth and return the activity link - return await this.auth.request( + return await this.tools.integrations.request( { provider: AuthProvider.Google, level: AuthLevel.User, scopes: calendarScopes, }, - authCallback + this.onAuthSuccess, + authToken, + callbackToken ); } @@ -146,7 +145,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"); } @@ -155,6 +154,7 @@ export class GoogleCalendar extends Tool implements CalendarTool { } async getCalendars(authToken: string): Promise { + console.log("Fetching Google Calendar list"); const api = await this.getApi(authToken); const data = (await api.call( "GET", @@ -167,6 +167,7 @@ export class GoogleCalendar extends Tool implements CalendarTool { primary?: boolean; }>; }; + console.log("Got Google Calendar list", data.items); return data.items.map((item) => ({ id: item.id, @@ -176,22 +177,31 @@ export class GoogleCalendar extends Tool implements CalendarTool { })); } - 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); + console.log("Saving callback"); + + // Create callback token for parent + const callbackToken = await this.tools.callbacks.createFromParent( + callback, + ...extraArgs + ); + await this.set("event_callback_token", callbackToken); + console.log("Setting up watch"); // Setup webhook for this calendar await this.setupCalendarWatch(authToken, calendarId, authToken); // 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, @@ -202,17 +212,19 @@ export class GoogleCalendar extends Tool implements CalendarTool { 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, - 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) { @@ -229,10 +241,11 @@ export class GoogleCalendar extends Tool implements CalendarTool { calendarId: string, opaqueAuthToken: string ): Promise { - const webhookUrl = await this.webhook.create("onCalendarWebhook", { + const webhookUrl = await this.tools.network.createWebhook( + this.onCalendarWebhook, calendarId, - authToken: opaqueAuthToken, - }); + opaqueAuthToken + ); // Check if webhook URL is localhost if (URL.parse(webhookUrl)?.hostname === "localhost") { @@ -268,17 +281,13 @@ export class GoogleCalendar extends Tool implements CalendarTool { 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}` ); @@ -310,12 +319,13 @@ export class GoogleCalendar extends Tool implements CalendarTool { 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( @@ -354,9 +364,7 @@ export class GoogleCalendar extends Tool implements CalendarTool { 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, @@ -374,10 +382,10 @@ export class GoogleCalendar extends Tool implements CalendarTool { recurrenceDates: activityData.recurrenceDates || null, recurrence: null, occurrence: null, - source: activityData.source || null, + meta: activityData.meta ?? null, }; - await this.callCallback(callbackToken, activity); + await this.run(callbackToken as any, activity); } } } catch (error) { @@ -402,7 +410,7 @@ export class GoogleCalendar extends Tool implements CalendarTool { 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, @@ -420,21 +428,22 @@ export class GoogleCalendar extends Tool implements CalendarTool { recurrenceDates: null, recurrence: null, // Would need to find master activity occurrence: new Date(originalStartTime), - source: activityData.source || null, + meta: activityData.meta ?? null, }; - await this.callCallback(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 @@ -445,9 +454,7 @@ export class GoogleCalendar extends Tool implements CalendarTool { 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"); @@ -463,7 +470,7 @@ export class GoogleCalendar extends Tool implements CalendarTool { } // Trigger incremental sync - await this.startIncrementalSync(context.calendarId, context.authToken); + await this.startIncrementalSync(calendarId, authToken); } private async startIncrementalSync( @@ -483,29 +490,32 @@ export class GoogleCalendar extends Tool implements CalendarTool { }; 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.callCallback(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-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/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/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 334dc39..e53c978 100644 --- a/tools/google-contacts/src/google-contacts.ts +++ b/tools/google-contacts/src/google-contacts.ts @@ -1,13 +1,18 @@ -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"; + type Authorization, + 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,49 +248,44 @@ 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); + build(build: ToolBuilder) { + return { + integrations: build(Integrations), + }; } - async requestAuth( - callbackFunctionName: string, - callbackContext?: any - ): 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", ]; - // 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 + // Create callback token for parent + const callbackToken = await (this.tools as any).callbacks.createFromParent( + callback, + ...extraArgs ); - await this.set(`auth_callback_token:${opaqueToken}`, callbackToken); - - // Create callback for auth completion - const authCallback = await this.callback("onAuthSuccess", { - toolName: "google-contacts", - opaqueToken, - }); // Request auth and return the activity link - return await this.auth.request( + return await this.tools.integrations.request( { provider: AuthProvider.Google, level: AuthLevel.User, scopes: contactsScopes, }, - authCallback + this.onAuthSuccess, + opaqueToken, + callbackToken ); } @@ -307,12 +307,12 @@ export default class extends Tool implements GoogleContacts { return result.contacts; } - async startSync( + async startSync< + TCallback extends (contacts: Contact[], ...args: any[]) => any + >( authToken: string, - callbackFunctionName: string, - options?: { - context?: any; - } + callback: TCallback, + ...extraArgs: any[] ): Promise { const storedAuthToken = await this.get( `auth_token:${authToken}` @@ -323,10 +323,10 @@ export default class extends Tool implements GoogleContacts { ); } - // Register the callback - const callbackToken = await this.callback( - callbackFunctionName, - options?.context + // Create callback token for parent + const callbackToken = await this.tools.callbacks.createFromParent( + callback, + ...extraArgs ); await this.set(`contacts_callback_token:${authToken}`, callbackToken); @@ -338,11 +338,8 @@ 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", { - batchNumber: 1, - authToken, - }); - await this.run(callback); + const syncCallback = await this.callback(this.syncBatch, 1, authToken); + await this.run(syncCallback); } async stopSync(authToken: string): Promise { @@ -351,11 +348,11 @@ export default class extends Tool implements 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 { @@ -390,11 +387,12 @@ export default class extends Tool implements 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.run(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` @@ -412,40 +410,28 @@ export default class extends Tool implements GoogleContacts { contacts: Contact[], authToken: string ): Promise { - const callbackToken = await this.get( + const callbackToken = await this.get( `contacts_callback_token:${authToken}` ); if (callbackToken) { - await this.callCallback(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.callCallback(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 6e3a800..4545ad5 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,22 +10,19 @@ export type ContactAuth = { authToken: string; }; -export interface GoogleContacts extends Tool { - requestAuth( - callbackFunctionName: string, - callbackContext?: any +export interface GoogleContacts extends ITool { + requestAuth any>( + callback: TCallback, + ...extraArgs: any[] ): Promise; getContacts(authToken: string): Promise; - startSync( + startSync any>( authToken: string, - callbackFunctionName: string, - options?: { - context?: any; - } + callback: TCallback, + ...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/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/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 ade00ad..3b60936 100644 --- a/tools/outlook-calendar/src/outlook-calendar.ts +++ b/tools/outlook-calendar/src/outlook-calendar.ts @@ -2,27 +2,23 @@ import { type Activity, type ActivityLink, ActivityType, + type NewActivity, Tool, - type Tools, + type ToolBuilder, } from "@plotday/sdk"; import type { Calendar, CalendarAuth, 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"; - -type AuthSuccessContext = { - token: string; -}; + Integrations, +} from "@plotday/sdk/tools/integrations"; +import { Network, type WebhookRequest } from "@plotday/sdk/tools/network"; // Import types from the existing outlook.ts file type CalendarConfig = { @@ -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 { @@ -222,36 +218,41 @@ 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 +{ + build(build: ToolBuilder) { + return { + integrations: build(Integrations), + network: build(Network, { + urls: ["https://graph.microsoft.com/*"], + }), + }; } - async requestAuth(callback: Callback): Promise { - // Generate opaque token for this auth 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.createFromParent( + callback, + ...extraArgs + ); // 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, scopes: ["https://graph.microsoft.com/calendars.readwrite"], }, - authCallback + this.onAuthSuccess, + token, + callbackToken ); } @@ -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"); } @@ -302,27 +303,34 @@ export class OutlookCalendar extends Tool implements CalendarTool { ]; } - 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.createFromParent( + 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) { @@ -341,10 +349,11 @@ 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( + this.onOutlookWebhook, calendarId, - authToken: opaqueAuthToken, - }); + opaqueAuthToken + ); config.webhookUrl = webhookUrl; @@ -376,12 +385,11 @@ export class OutlookCalendar extends Tool implements CalendarTool { }); } - 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; @@ -467,14 +475,8 @@ export class OutlookCalendar extends Tool implements CalendarTool { } // 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, @@ -484,17 +486,13 @@ export class OutlookCalendar extends Tool implements CalendarTool { title: event.name || null, parent: null, links: null, - priority: { - id: "default", - title: "Default", - }, recurrenceRule: recurrenceRule || null, recurrenceExdates: null, recurrenceDates: null, recurrence: null, occurrence: null, - source: { - type: "outlook-calendar-event", + meta: { + source: `outlook-calendar:${event.id}`, id: event.id, calendarId: syncState.calendarId, }, @@ -530,41 +528,16 @@ export class OutlookCalendar extends Tool implements CalendarTool { 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.callCallback(callbackToken, activity); + await this.run(callbackToken as any, activity); } } @@ -584,9 +557,13 @@ export class OutlookCalendar extends Tool implements CalendarTool { ); } - 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) { @@ -603,11 +580,11 @@ export class OutlookCalendar extends Tool implements CalendarTool { 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); } } } @@ -628,36 +605,27 @@ export class OutlookCalendar extends Tool implements CalendarTool { 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 { - console.log("Outlook Calendar authentication successful", authResult); - // 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.callCallback(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); } } 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"] }