426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
322 lines
11 KiB
TypeScript
322 lines
11 KiB
TypeScript
/**
|
|
* Top-level runtime `Service` and the plugin's lifecycle owner. A process-wide
|
|
* singleton keyed by agent, it discovers every configured account, spins up a
|
|
* `FarcasterAgentManager` (plus per-account cast and message services) for each,
|
|
* and starts/stops them. `registerSendHandlers` binds the cast service's
|
|
* `handleSendPost`/`fetchFeed`/`searchPosts` into the runtime as a `farcaster`
|
|
* post connector when the runtime supports it. Also exposes account/manager
|
|
* lookups (used by the profile provider and webhook route) and a `healthCheck`
|
|
* that pings each account's Neynar profile.
|
|
*/
|
|
import {
|
|
ChannelType,
|
|
type Content,
|
|
type IAgentRuntime,
|
|
type Memory,
|
|
Service,
|
|
type UUID,
|
|
} from "@elizaos/core";
|
|
import { FarcasterAgentManager } from "../managers/AgentManager";
|
|
import { FARCASTER_SERVICE_NAME } from "../types";
|
|
import {
|
|
getFarcasterFid,
|
|
hasFarcasterEnabled,
|
|
listFarcasterAccountIds,
|
|
normalizeFarcasterAccountId,
|
|
resolveDefaultFarcasterAccountId,
|
|
validateFarcasterConfig,
|
|
} from "../utils/config";
|
|
import { FarcasterCastService } from "./CastService";
|
|
import { FarcasterMessageService } from "./MessageService";
|
|
|
|
type FarcasterPostConnectorRegistration = {
|
|
source: string;
|
|
label?: string;
|
|
description?: string;
|
|
capabilities?: string[];
|
|
contexts?: string[];
|
|
metadata?: Record<string, unknown>;
|
|
postHandler: (runtime: IAgentRuntime, content: Content) => Promise<Memory>;
|
|
fetchFeed?: FarcasterCastService["fetchFeed"];
|
|
searchPosts?: FarcasterCastService["searchPosts"];
|
|
contentShaping?: {
|
|
systemPromptFragment?: string;
|
|
constraints?: Record<string, unknown>;
|
|
};
|
|
accountId?: string;
|
|
};
|
|
|
|
type RuntimeWithPostConnector = IAgentRuntime & {
|
|
registerPostConnector?: (registration: FarcasterPostConnectorRegistration) => void;
|
|
};
|
|
|
|
interface AgentAccounts {
|
|
defaultAccountId: string;
|
|
managers: Map<string, FarcasterAgentManager>;
|
|
messageServices: Map<string, FarcasterMessageService>;
|
|
castServices: Map<string, FarcasterCastService>;
|
|
}
|
|
|
|
export class FarcasterService extends Service {
|
|
private static instance?: FarcasterService;
|
|
private agents = new Map<UUID, AgentAccounts>();
|
|
|
|
static serviceType = FARCASTER_SERVICE_NAME;
|
|
|
|
readonly description = "Farcaster integration service for sending and receiving casts";
|
|
readonly capabilityDescription = "The agent is able to send and receive messages on farcaster";
|
|
|
|
private static getInstance(): FarcasterService {
|
|
if (!FarcasterService.instance) {
|
|
FarcasterService.instance = new FarcasterService();
|
|
}
|
|
return FarcasterService.instance;
|
|
}
|
|
|
|
async initialize(runtime: IAgentRuntime): Promise<void> {
|
|
await FarcasterService.start(runtime);
|
|
}
|
|
|
|
static async start(runtime: IAgentRuntime): Promise<Service> {
|
|
const service = FarcasterService.getInstance();
|
|
if (service.agents.has(runtime.agentId)) {
|
|
runtime.logger.warn({ agentId: runtime.agentId }, "Farcaster service already started");
|
|
return service;
|
|
}
|
|
|
|
if (!hasFarcasterEnabled(runtime)) {
|
|
runtime.logger.debug({ agentId: runtime.agentId }, "Farcaster service not enabled");
|
|
return service;
|
|
}
|
|
|
|
const accounts: AgentAccounts = {
|
|
defaultAccountId: normalizeFarcasterAccountId(resolveDefaultFarcasterAccountId(runtime)),
|
|
managers: new Map(),
|
|
messageServices: new Map(),
|
|
castServices: new Map(),
|
|
};
|
|
service.agents.set(runtime.agentId, accounts);
|
|
|
|
for (const accountId of listFarcasterAccountIds(runtime)) {
|
|
if (!hasFarcasterEnabled(runtime, accountId)) {
|
|
continue;
|
|
}
|
|
|
|
const farcasterConfig = validateFarcasterConfig(runtime, accountId);
|
|
const manager = new FarcasterAgentManager(runtime, farcasterConfig);
|
|
accounts.managers.set(accountId, manager);
|
|
accounts.messageServices.set(
|
|
accountId,
|
|
new FarcasterMessageService(manager.client, runtime, accountId)
|
|
);
|
|
accounts.castServices.set(
|
|
accountId,
|
|
new FarcasterCastService(manager.client, runtime, accountId)
|
|
);
|
|
|
|
await manager.start();
|
|
runtime.logger.success({ agentId: runtime.agentId, accountId }, "Farcaster client started");
|
|
}
|
|
|
|
return service;
|
|
}
|
|
|
|
static async stop(runtime: IAgentRuntime): Promise<void> {
|
|
const service = FarcasterService.getInstance();
|
|
const accounts = service.agents.get(runtime.agentId);
|
|
if (accounts) {
|
|
for (const manager of accounts.managers.values()) {
|
|
await manager.stop();
|
|
}
|
|
service.agents.delete(runtime.agentId);
|
|
runtime.logger.info({ agentId: runtime.agentId }, "Farcaster client stopped");
|
|
} else {
|
|
runtime.logger.debug({ agentId: runtime.agentId }, "Farcaster service not running");
|
|
}
|
|
}
|
|
|
|
static registerSendHandlers(runtime: IAgentRuntime, serviceInstance: FarcasterService): void {
|
|
const accounts = serviceInstance.agents.get(runtime.agentId);
|
|
if (!accounts || accounts.castServices.size === 0) {
|
|
runtime.logger.warn(
|
|
{ src: "plugin:farcaster", agentId: runtime.agentId },
|
|
"Cannot register Farcaster post connector; cast service is not initialized"
|
|
);
|
|
return;
|
|
}
|
|
|
|
const withPostConnector = runtime as RuntimeWithPostConnector;
|
|
if (typeof withPostConnector.registerPostConnector !== "function") {
|
|
return;
|
|
}
|
|
|
|
for (const castService of accounts.castServices.values()) {
|
|
const accountId = castService.getAccountId();
|
|
withPostConnector.registerPostConnector({
|
|
source: "farcaster",
|
|
accountId,
|
|
label: "Farcaster",
|
|
description:
|
|
"Farcaster public cast connector for publishing casts and reading or searching the authenticated account's recent feed.",
|
|
capabilities: ["post", "fetch_feed", "search_posts"],
|
|
contexts: ["social", "social_posting", "connectors"],
|
|
metadata: {
|
|
accountId,
|
|
service: FARCASTER_SERVICE_NAME,
|
|
},
|
|
postHandler: castService.handleSendPost.bind(castService),
|
|
fetchFeed: castService.fetchFeed.bind(castService),
|
|
searchPosts: castService.searchPosts.bind(castService),
|
|
contentShaping: {
|
|
systemPromptFragment:
|
|
"For Farcaster casts, write a conversational public cast under 320 characters. If replying, keep enough context for a public thread.",
|
|
constraints: {
|
|
maxLength: 320,
|
|
supportsMarkdown: false,
|
|
channelType: ChannelType.FEED,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
runtime.logger.info(
|
|
{ src: "plugin:farcaster", agentId: runtime.agentId },
|
|
"Registered Farcaster post connector"
|
|
);
|
|
}
|
|
|
|
async stop(): Promise<void> {
|
|
for (const [agentId, accounts] of Array.from(this.agents.entries())) {
|
|
const runtime = accounts.managers.values().next().value?.runtime;
|
|
runtime?.logger.debug("Stopping Farcaster service");
|
|
try {
|
|
if (runtime) {
|
|
await FarcasterService.stop(runtime);
|
|
} else {
|
|
this.agents.delete(agentId);
|
|
}
|
|
} catch (error) {
|
|
runtime?.logger.error({ agentId, error }, "Error stopping Farcaster service");
|
|
}
|
|
}
|
|
}
|
|
|
|
getMessageService(agentId: UUID, accountId?: string): FarcasterMessageService | undefined {
|
|
return this.getMessageServiceForAccount(accountId, agentId);
|
|
}
|
|
|
|
getCastService(agentId: UUID, accountId?: string): FarcasterCastService | undefined {
|
|
return this.getCastServiceForAccount(accountId, agentId);
|
|
}
|
|
|
|
getMessageServiceForAccount(
|
|
accountId: string | undefined,
|
|
agentId?: UUID
|
|
): FarcasterMessageService | undefined {
|
|
const resolvedAgentId = agentId ?? this.firstAgentId();
|
|
if (!resolvedAgentId) return undefined;
|
|
const accounts = this.agents.get(resolvedAgentId);
|
|
if (!accounts) return undefined;
|
|
const id = accountId ? normalizeFarcasterAccountId(accountId) : accounts.defaultAccountId;
|
|
return accounts.messageServices.get(id);
|
|
}
|
|
|
|
getCastServiceForAccount(
|
|
accountId: string | undefined,
|
|
agentId?: UUID
|
|
): FarcasterCastService | undefined {
|
|
const resolvedAgentId = agentId ?? this.firstAgentId();
|
|
if (!resolvedAgentId) return undefined;
|
|
const accounts = this.agents.get(resolvedAgentId);
|
|
if (!accounts) return undefined;
|
|
const id = accountId ? normalizeFarcasterAccountId(accountId) : accounts.defaultAccountId;
|
|
return accounts.castServices.get(id);
|
|
}
|
|
|
|
getManagerForAccount(
|
|
accountId: string | undefined,
|
|
agentId?: UUID
|
|
): FarcasterAgentManager | undefined {
|
|
const resolvedAgentId = agentId ?? this.firstAgentId();
|
|
if (!resolvedAgentId) return undefined;
|
|
const accounts = this.agents.get(resolvedAgentId);
|
|
if (!accounts) return undefined;
|
|
const id = accountId ? normalizeFarcasterAccountId(accountId) : accounts.defaultAccountId;
|
|
return accounts.managers.get(id);
|
|
}
|
|
|
|
getDefaultAccountId(agentId?: UUID): string | undefined {
|
|
const resolvedAgentId = agentId ?? this.firstAgentId();
|
|
return resolvedAgentId ? this.agents.get(resolvedAgentId)?.defaultAccountId : undefined;
|
|
}
|
|
|
|
listAccountIds(agentId?: UUID): string[] {
|
|
const resolvedAgentId = agentId ?? this.firstAgentId();
|
|
const accounts = resolvedAgentId ? this.agents.get(resolvedAgentId) : undefined;
|
|
return accounts ? Array.from(accounts.managers.keys()) : [];
|
|
}
|
|
|
|
getManagersForAgent(agentId?: UUID): Map<string, FarcasterAgentManager> {
|
|
const resolvedAgentId = agentId ?? this.firstAgentId();
|
|
const accounts = resolvedAgentId ? this.agents.get(resolvedAgentId) : undefined;
|
|
return new Map(accounts?.managers ?? []);
|
|
}
|
|
|
|
async healthCheck(): Promise<{
|
|
healthy: boolean;
|
|
details: Record<string, unknown>;
|
|
}> {
|
|
const managerStatuses: Record<string, unknown> = {};
|
|
let overallHealthy = true;
|
|
|
|
for (const [agentId, accounts] of Array.from(this.agents.entries())) {
|
|
managerStatuses[agentId] = {};
|
|
for (const [accountId, manager] of Array.from(accounts.managers.entries())) {
|
|
try {
|
|
const fid = getFarcasterFid(manager.runtime, accountId);
|
|
if (!fid) {
|
|
throw new Error("FARCASTER_FID not configured");
|
|
}
|
|
const profile = await manager.client.getProfile(fid);
|
|
(managerStatuses[agentId] as Record<string, unknown>)[accountId] = {
|
|
status: "healthy",
|
|
fid: profile.fid,
|
|
username: profile.username,
|
|
};
|
|
} catch (error) {
|
|
(managerStatuses[agentId] as Record<string, unknown>)[accountId] = {
|
|
status: "unhealthy",
|
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
};
|
|
overallHealthy = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
healthy: overallHealthy,
|
|
details: {
|
|
activeManagers: Array.from(this.agents.values()).reduce(
|
|
(total, accounts) => total + accounts.managers.size,
|
|
0
|
|
),
|
|
managerStatuses,
|
|
},
|
|
};
|
|
}
|
|
|
|
getActiveManagers(): Map<string, FarcasterAgentManager> {
|
|
return new Map(
|
|
Array.from(this.agents.entries()).flatMap(([agentId, accounts]) =>
|
|
Array.from(accounts.managers.entries()).map(
|
|
([accountId, manager]) => [`${agentId}:${accountId}`, manager] as const
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
private firstAgentId(): UUID | undefined {
|
|
return this.agents.keys().next().value;
|
|
}
|
|
}
|