UNPKG

@mastra/core

Version:
487 lines (486 loc) 15.2 kB
import { h as GoalStateProcessor } from "./task-state-processor-C9agcUfw.js"; //#region src/signals/signal-provider.ts /** * Abstract base for signal providers. * * A SignalProvider monitors external sources and pushes notification signals * into agent threads. It combines three capabilities: * * 1. **Subscription tracking** — built-in registry of which threads are subscribed to which external resources * 2. **External monitoring** — polling or webhook-driven event ingestion * 3. **Optional processor/tool integration** — providers can expose input/output processors and tools * * Not all signal providers are processors. A provider that only polls an API * and pushes notifications needs no processor hooks at all. Providers that * need to intercept agent execution (e.g., injecting subscription hints) can * return processors via `getInputProcessors()` / `getOutputProcessors()`. * Providers that expose agent tools (e.g., subscribe/unsubscribe commands) * can return them via `getTools()`. * * ## Usage * * ```ts * const agent = new Agent({ * signals: [new MySignalProvider()], * }); * ``` * * The Agent automatically: * - Calls `connect(this)` to establish the bidirectional link * - Registers any processors returned by `getInputProcessors()` / `getOutputProcessors()` * - Merges any tools returned by `getTools()` * - Starts polling if `pollInterval` is defined * * ## Building a Provider * * Extend this class, implement the abstract `id` field, and override * whichever hooks your provider needs: * * ```ts * class SlackSignals extends SignalProvider<'slack-signals'> { * readonly id = 'slack-signals'; * readonly pollInterval = 30_000; // poll every 30s * * async poll(subscriptions: SignalSubscription[]) { * for (const sub of subscriptions) { * // check Slack, emit notifications for changes * } * } * } * ``` * * @experimental Agent signals are experimental and may change in a future release. */ var SignalProvider = class { name; /** * The Mastra instance this provider is registered with. * Set by the framework when the agent is registered with Mastra. */ mastra; /** * @internal Called when the provider's agent is registered with a Mastra instance. */ __registerMastra(mastra) { this.mastra = mastra; } /** * The agent this provider is connected to. * Set automatically when passed to `Agent({ signals: [...] })`. */ #connectedAgent; /** * In-memory subscription registry. * Key: `${resourceId}:${threadId}:${externalResourceId}` */ #subscriptions = /* @__PURE__ */ new Map(); /** * Index: externalResourceId → set of subscription keys */ #subscriptionsByResource = /* @__PURE__ */ new Map(); /** * Index: `${resourceId}:${threadId}` → set of subscription keys */ #subscriptionsByThread = /* @__PURE__ */ new Map(); /** Active polling timer, if any */ #pollTimer; /** Guard to prevent overlapping poll cycles */ #isPollRunning = false; /** * Called by the Agent constructor to establish the bidirectional link. * Override to perform additional setup (always call `super.connect(agent)`). */ connect(agent) { this.#connectedAgent = agent; } /** * Whether this provider is already connected to an agent. * Used to skip re-wiring when an Agent is forked via `__fork()`. */ get isConnected() { return this.#connectedAgent !== void 0; } /** * The connected agent. Available after `connect()` has been called. * Use this to send signals and notification signals back into agent threads. */ get agent() { return this.#connectedAgent; } /** * Subscribe a thread to an external resource. * * @param target - The thread to receive signals * @param externalResourceId - Provider-specific resource identifier * (e.g., `"github:mastra-ai/mastra#123"`, `"slack:C0B01RW7A4T"`) * @param metadata - Optional provider-specific metadata for the subscription */ subscribe(target, externalResourceId, metadata = {}) { const key = this.#subscriptionKey(target, externalResourceId); const existing = this.#subscriptions.get(key); if (existing) { existing.metadata = { ...existing.metadata, ...metadata }; return existing; } const subscription = { id: crypto.randomUUID(), providerId: this.id, threadId: target.threadId, resourceId: target.resourceId, externalResourceId, subscribedAt: /* @__PURE__ */ new Date(), metadata }; this.#subscriptions.set(key, subscription); let resourceSet = this.#subscriptionsByResource.get(externalResourceId); if (!resourceSet) { resourceSet = /* @__PURE__ */ new Set(); this.#subscriptionsByResource.set(externalResourceId, resourceSet); } resourceSet.add(key); const threadKey = this.#threadKey(target); let threadSet = this.#subscriptionsByThread.get(threadKey); if (!threadSet) { threadSet = /* @__PURE__ */ new Set(); this.#subscriptionsByThread.set(threadKey, threadSet); } threadSet.add(key); return subscription; } /** * Unsubscribe a thread from an external resource. * * @returns `true` if a subscription was removed, `false` if none existed */ unsubscribe(target, externalResourceId) { const key = this.#subscriptionKey(target, externalResourceId); if (!this.#subscriptions.get(key)) return false; this.#subscriptions.delete(key); const resourceSet = this.#subscriptionsByResource.get(externalResourceId); if (resourceSet) { resourceSet.delete(key); if (resourceSet.size === 0) this.#subscriptionsByResource.delete(externalResourceId); } const threadKey = this.#threadKey(target); const threadSet = this.#subscriptionsByThread.get(threadKey); if (threadSet) { threadSet.delete(key); if (threadSet.size === 0) this.#subscriptionsByThread.delete(threadKey); } return true; } /** * Get all active subscriptions for this provider. */ getSubscriptions() { return [...this.#subscriptions.values()]; } /** * Get all subscriptions for a specific external resource. * * @example * ```ts * const subs = this.getSubscriptionsForResource('github:mastra-ai/mastra#123'); * for (const sub of subs) { * await this.notify({ ... }, { resourceId: sub.resourceId, threadId: sub.threadId }); * } * ``` */ getSubscriptionsForResource(externalResourceId) { const keys = this.#subscriptionsByResource.get(externalResourceId); if (!keys) return []; return [...keys].map((key) => this.#subscriptions.get(key)).filter(Boolean); } /** * Get all subscriptions for a specific thread. */ getSubscriptionsForThread(target) { const threadKey = this.#threadKey(target); const keys = this.#subscriptionsByThread.get(threadKey); if (!keys) return []; return [...keys].map((key) => this.#subscriptions.get(key)).filter(Boolean); } /** * Check if a thread is subscribed to a specific external resource. */ hasSubscription(target, externalResourceId) { return this.#subscriptions.has(this.#subscriptionKey(target, externalResourceId)); } /** * Remove all subscriptions for a thread. */ unsubscribeAll(target) { const threadSubscriptions = this.getSubscriptionsForThread(target); let removed = 0; for (const sub of threadSubscriptions) if (this.unsubscribe(target, sub.externalResourceId)) removed++; return removed; } /** * Total number of active subscriptions. */ get subscriptionCount() { return this.#subscriptions.size; } /** * Optional poll interval in milliseconds. * When defined, the framework calls `poll()` on this interval * with all active subscriptions. * * Set to `undefined` or `0` for webhook-only providers that don't poll. */ pollInterval; /** * Start the polling timer. Called automatically by the Agent after `connect()`. * Can also be called manually to restart polling after `stopPolling()`. */ startPolling() { if (this.#pollTimer) return; const interval = this.pollInterval; if (!interval || interval <= 0 || typeof this.poll !== "function") return; this.#pollTimer = setInterval(() => { if (this.#isPollRunning) return; const subscriptions = this.getSubscriptions(); if (subscriptions.length === 0) return; this.#isPollRunning = true; Promise.resolve(this.poll(subscriptions)).catch((error) => { console.warn(`[${this.id}] poll failed:`, error); }).finally(() => { this.#isPollRunning = false; }); }, interval); this.#pollTimer.unref?.(); } /** * Stop the polling timer. */ stopPolling() { if (this.#pollTimer) { clearInterval(this.#pollTimer); this.#pollTimer = void 0; } } /** * Called on shutdown. Override to clean up resources. * Default implementation stops polling and clears all subscriptions. */ stop() { this.stopPolling(); this.#subscriptions.clear(); this.#subscriptionsByResource.clear(); this.#subscriptionsByThread.clear(); } /** * Send a notification signal to the connected agent. * Convenience wrapper around `this.agent.sendNotificationSignal()`. * * @throws If no agent is connected */ async notify(notification, target) { const agent = this.#connectedAgent; if (!agent) throw new Error(`[${this.id}] Cannot send notification: no agent connected. Was this provider passed to Agent({ signals: [...] })?`); await agent.sendNotificationSignal(notification, { resourceId: target.resourceId, threadId: target.threadId, ...target.ifIdle ? { ifIdle: target.ifIdle } : {} }); } #subscriptionKey(target, externalResourceId) { return `${target.resourceId}:${target.threadId}:${externalResourceId}`; } #threadKey(target) { return `${target.resourceId}:${target.threadId}`; } }; /** * Type guard to check if an object is a SignalProvider. * * @experimental Agent signals are experimental and may change in a future release. */ function isSignalProvider(obj) { return obj instanceof SignalProvider; } //#endregion //#region src/signals/webhook-signal-provider.ts /** * A generic webhook-based signal provider. * * Receives external events via HTTP webhooks and routes them to * subscribed agent threads as notification signals. * * ## Usage * * ```ts * const webhooks = new WebhookSignalProvider({ * extractResourceId: (payload) => (payload as any).repository, * buildNotification: (payload, sub) => ({ * source: 'ci', * kind: 'build-status', * priority: 'medium', * summary: `Build ${(payload as any).status} for ${sub.externalResourceId}`, * }), * }); * * const agent = new Agent({ * signals: [webhooks], * }); * * // Subscribe a thread to a resource * webhooks.subscribeThread( * { threadId: 'thread-1', resourceId: 'user-1' }, * 'my-org/my-repo', * ); * * // Later, when a webhook fires: * await webhooks.handleWebhook({ * body: { repository: 'my-org/my-repo', status: 'failed' }, * headers: {}, * }); * ``` * * @experimental Agent signals are experimental and may change in a future release. */ var WebhookSignalProvider = class extends SignalProvider { id; name; #options; constructor(options = {}) { super(); this.id = options.id ?? "webhook-signals"; this.name = options.name ?? "Webhook Signals"; this.#options = options; } /** * Create signal inputs for subscribing/unsubscribing threads via signals. */ static signals = { subscribe(resource) { return { type: "reactive", tagName: "webhook-subscribe", contents: `Subscribe to webhook resource: ${resource}`, attributes: { resource } }; }, unsubscribe(resource) { return { type: "reactive", tagName: "webhook-unsubscribe", contents: `Unsubscribe from webhook resource: ${resource}`, attributes: { resource } }; } }; /** * Programmatically subscribe a thread to an external resource. */ subscribeThread(target, externalResourceId, metadata) { return this.subscribe(target, externalResourceId, metadata); } /** * Programmatically unsubscribe a thread from an external resource. */ unsubscribeThread(target, externalResourceId) { return this.unsubscribe(target, externalResourceId); } /** * Handle an incoming webhook. Matches the payload against subscriptions * and emits notification signals to matching threads. */ async handleWebhook(request) { const payload = request.body; const resourceIds = [...new Set(this.#extractResourceIds(payload))]; if (resourceIds.length === 0) return { status: 200, body: { matched: 0 } }; let matched = 0; for (const resourceId of resourceIds) { const subscriptions = this.getSubscriptionsForResource(resourceId); for (const subscription of subscriptions) { const notification = this.#buildNotification(payload, subscription); try { await this.notify(notification, { threadId: subscription.threadId, resourceId: subscription.resourceId }); matched++; } catch (error) { console.warn(`[${this.id}] Failed to notify thread ${subscription.threadId}:`, error); } } } return { status: 200, body: { matched } }; } #extractResourceIds(payload) { if (this.#options.extractResourceId) { const result = this.#options.extractResourceId(payload); if (!result) return []; return Array.isArray(result) ? result : [result]; } if (payload && typeof payload === "object") { const obj = payload; if (typeof obj.resource === "string") return [obj.resource]; if (typeof obj.externalResourceId === "string") return [obj.externalResourceId]; } return []; } #buildNotification(payload, subscription) { if (this.#options.buildNotification) return this.#options.buildNotification(payload, subscription); return { source: this.id, kind: "webhook-event", priority: "medium", summary: `Webhook event for ${subscription.externalResourceId}`, payload, dedupeKey: `${this.id}:${subscription.externalResourceId}:${Date.now()}`, coalesceKey: `${this.id}:${subscription.externalResourceId}` }; } }; //#endregion //#region src/agent/goal/signal-provider.ts /** * Bundles the {@link GoalStateProcessor} behind a single agent registration so * the agent's current objective is projected onto the state-signal lane. * * The objective is held in the thread-scoped `threadState` domain (under * `type: 'goal'`) and is set via {@link Agent.setObjective}; this provider only * projects it onto the model context. The Agent auto-registers this provider * when configured with `goal`, so configuring `goal` alone is enough. * * Goals require a memory-backed thread (`threadId` + `resourceId`) and a Mastra * `storage` instance. Without memory the objective methods no-op. * * @example * ```ts * import { Agent } from '@mastra/core/agent'; * * // `goal` auto-registers the GoalSignalProvider — no need to add it to * // `signals` yourself. * const agent = new Agent({ * name: 'worker', * instructions: '...', * model, * memory, * goal: { judge: judgeModel }, * }); * ``` * * @experimental Agent signals are experimental and may change in a future release. */ var GoalSignalProvider = class extends SignalProvider { id = "goal-signals"; #processor = new GoalStateProcessor(); getInputProcessors() { return [this.#processor]; } }; //#endregion export { isSignalProvider as i, WebhookSignalProvider as n, SignalProvider as r, GoalSignalProvider as t }; //# sourceMappingURL=signal-provider-Dtdpbuf1.js.map