@mastra/core
Version:
487 lines (486 loc) • 15.2 kB
JavaScript
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