Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"postinstall": "node scripts/unpack-tools.cjs"
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.8.0",
"@modelcontextprotocol/sdk": "^1.22.0",
"@stablelib/base64": "^2.0.1",
"@stablelib/hex": "^2.0.1",
Expand All @@ -79,6 +80,7 @@
"@types/qrcode-terminal": "^0.12.2",
"@types/react": "^19.2.7",
"@types/tmp": "^0.2.6",
"ai": "^5.0.107",
"axios": "^1.13.2",
"chalk": "^5.6.2",
"cross-spawn": "^7.0.6",
Expand Down
154 changes: 154 additions & 0 deletions src/agent/AgentBackend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/**
* AgentBackend - Universal interface for AI agent backends
*
* This module defines the core abstraction for different agent backends
* (Claude, Codex, Gemini, OpenCode, etc.) that can be controlled through
* the Happy CLI and mobile app.
*
* The AgentBackend interface provides a unified way to:
* - Start and manage agent sessions
* - Send prompts and receive responses
* - Handle tool calls and permissions
* - Stream model output and events
*/

/** Unique identifier for an agent session */
export type SessionId = string;

/** Unique identifier for a tool call */
export type ToolCallId = string;

/**
* Messages emitted by an agent backend during a session.
* These messages are forwarded to the Happy server and mobile app.
*/
export type AgentMessage =
| { type: 'model-output'; textDelta?: string; fullText?: string }
| { type: 'status'; status: 'starting' | 'running' | 'idle' | 'stopped' | 'error'; detail?: string }
| { type: 'tool-call'; toolName: string; args: Record<string, unknown>; callId: ToolCallId }
| { type: 'tool-result'; toolName: string; result: unknown; callId: ToolCallId }
| { type: 'permission-request'; id: string; reason: string; payload: unknown }
| { type: 'permission-response'; id: string; approved: boolean }
| { type: 'fs-edit'; description: string; diff?: string; path?: string }
| { type: 'terminal-output'; data: string }
| { type: 'event'; name: string; payload: unknown }
| { type: 'token-count'; [key: string]: unknown } // Token count information (format may vary)
| { type: 'exec-approval-request'; call_id: string; [key: string]: unknown } // Exec approval request (like Codex exec_approval_request)
| { type: 'patch-apply-begin'; call_id: string; auto_approved?: boolean; changes: Record<string, unknown> } // Patch operation begin (like Codex patch_apply_begin)
| { type: 'patch-apply-end'; call_id: string; stdout?: string; stderr?: string; success: boolean } // Patch operation end (like Codex patch_apply_end)

/** MCP server configuration for tools */
export interface McpServerConfig {
command: string;
args?: string[];
env?: Record<string, string>;
}

/** Transport type for agent communication */
export type AgentTransport = 'native-claude' | 'mcp-codex' | 'acp';

/** Agent identifier */
export type AgentId = 'claude' | 'codex' | 'gemini' | 'opencode' | 'claude-acp' | 'codex-acp';

/**
* Configuration for creating an agent backend
*/
export interface AgentBackendConfig {
/** Working directory for the agent */
cwd: string;

/** Name of the agent */
agentName: AgentId;

/** Transport protocol to use */
transport: AgentTransport;

/** Environment variables to pass to the agent */
env?: Record<string, string>;

/** MCP servers to make available to the agent */
mcpServers?: Record<string, McpServerConfig>;
}

/**
* Configuration specific to ACP-based agents
*/
export interface AcpAgentConfig extends AgentBackendConfig {
transport: 'acp';

/** Command to spawn the ACP agent */
command: string;

/** Arguments for the agent command */
args?: string[];
}

/**
* Result of starting a session
*/
export interface StartSessionResult {
sessionId: SessionId;
}

/**
* Handler function type for agent messages
*/
export type AgentMessageHandler = (msg: AgentMessage) => void;

/**
* Universal interface for agent backends.
*
* All agent implementations (Claude, Codex, Gemini, etc.) should implement
* this interface to be usable through the Happy CLI and mobile app.
*/
export interface AgentBackend {
/**
* Start a new agent session.
*
* @param initialPrompt - Optional initial prompt to send to the agent
* @returns Promise resolving to session information
*/
startSession(initialPrompt?: string): Promise<StartSessionResult>;

/**
* Send a prompt to an existing session.
*
* @param sessionId - The session to send the prompt to
* @param prompt - The user's prompt text
*/
sendPrompt(sessionId: SessionId, prompt: string): Promise<void>;

/**
* Cancel the current operation in a session.
*
* @param sessionId - The session to cancel
*/
cancel(sessionId: SessionId): Promise<void>;

/**
* Register a handler for agent messages.
*
* @param handler - Function to call when messages are received
*/
onMessage(handler: AgentMessageHandler): void;

/**
* Remove a previously registered message handler.
*
* @param handler - The handler to remove
*/
offMessage?(handler: AgentMessageHandler): void;

/**
* Respond to a permission request.
*
* @param requestId - The ID of the permission request
* @param approved - Whether the permission was granted
*/
respondToPermission?(requestId: string, approved: boolean): Promise<void>;

/**
* Clean up resources and close the backend.
*/
dispose(): Promise<void>;
}
89 changes: 89 additions & 0 deletions src/agent/AgentRegistry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* AgentRegistry - Registry for agent backend factories
*
* This module provides a central registry for creating agent backends.
* It allows registering factory functions for different agent types
* and creating instances of those backends.
*/

import type { AgentBackend, AgentId } from './AgentBackend';

/** Options passed to agent factory functions */
export interface AgentFactoryOptions {
/** Working directory for the agent */
cwd: string;

/** Environment variables to pass to the agent */
env?: Record<string, string>;
}

/** Factory function type for creating agent backends */
export type AgentFactory = (opts: AgentFactoryOptions) => AgentBackend;

/**
* Registry for agent backend factories.
*
* Use this to register and create agent backends by their identifier.
*
* @example
* ```ts
* const registry = new AgentRegistry();
* registry.register('gemini', createGeminiBackend);
*
* const backend = registry.create('gemini', { cwd: process.cwd() });
* await backend.startSession('Hello!');
* ```
*/
export class AgentRegistry {
private factories = new Map<AgentId, AgentFactory>();

/**
* Register a factory function for an agent type.
*
* @param id - The agent identifier
* @param factory - Factory function to create the backend
*/
register(id: AgentId, factory: AgentFactory): void {
this.factories.set(id, factory);
}

/**
* Check if an agent type is registered.
*
* @param id - The agent identifier to check
* @returns true if the agent is registered
*/
has(id: AgentId): boolean {
return this.factories.has(id);
}

/**
* Get the list of registered agent identifiers.
*
* @returns Array of registered agent IDs
*/
list(): AgentId[] {
return Array.from(this.factories.keys());
}

/**
* Create an agent backend instance.
*
* @param id - The agent identifier
* @param opts - Options for creating the backend
* @returns The created agent backend
* @throws Error if the agent type is not registered
*/
create(id: AgentId, opts: AgentFactoryOptions): AgentBackend {
const factory = this.factories.get(id);
if (!factory) {
const available = this.list().join(', ') || 'none';
throw new Error(`Unknown agent: ${id}. Available agents: ${available}`);
}
return factory(opts);
}
}

/** Global agent registry instance */
export const agentRegistry = new AgentRegistry();

Loading