> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/agentclientprotocol/typescript-sdk/llms.txt
> Use this file to discover all available pages before exploring further.

# Simple Agent Example

> A complete working example of an ACP Agent implementation in TypeScript

## Overview

This example demonstrates how to build a functional ACP Agent using the TypeScript SDK. The agent implements all required protocol methods and showcases key features including session management, tool calls, and permission requests.

## What This Example Demonstrates

<CardGroup cols={2}>
  <Card title="Session Management" icon="layer-group">
    Creating and managing agent sessions with unique IDs
  </Card>

  <Card title="Tool Calls" icon="wrench">
    Executing tool calls with different permission requirements
  </Card>

  <Card title="Permission Requests" icon="shield-check">
    Requesting user approval for sensitive operations
  </Card>

  <Card title="Streaming Updates" icon="stream">
    Sending real-time text chunks and status updates to the client
  </Card>
</CardGroup>

## Complete Code

Here's the full implementation from `src/examples/agent.ts`:

```typescript theme={null}
#!/usr/bin/env node

import * as acp from "@agentclientprotocol/sdk";
import { Readable, Writable } from "node:stream";

interface AgentSession {
  pendingPrompt: AbortController | null;
}

class ExampleAgent implements acp.Agent {
  private connection: acp.AgentSideConnection;
  private sessions: Map<string, AgentSession>;

  constructor(connection: acp.AgentSideConnection) {
    this.connection = connection;
    this.sessions = new Map();
  }

  async initialize(
    _params: acp.InitializeRequest,
  ): Promise<acp.InitializeResponse> {
    return {
      protocolVersion: acp.PROTOCOL_VERSION,
      agentCapabilities: {
        loadSession: false,
      },
    };
  }

  async newSession(
    _params: acp.NewSessionRequest,
  ): Promise<acp.NewSessionResponse> {
    const sessionId = Array.from(crypto.getRandomValues(new Uint8Array(16)))
      .map((b) => b.toString(16).padStart(2, "0"))
      .join("");

    this.sessions.set(sessionId, {
      pendingPrompt: null,
    });

    return {
      sessionId,
    };
  }

  async authenticate(
    _params: acp.AuthenticateRequest,
  ): Promise<acp.AuthenticateResponse | void> {
    // No auth needed - return empty response
    return {};
  }

  async setSessionMode(
    _params: acp.SetSessionModeRequest,
  ): Promise<acp.SetSessionModeResponse> {
    // Session mode changes not implemented in this example
    return {};
  }

  async prompt(params: acp.PromptRequest): Promise<acp.PromptResponse> {
    const session = this.sessions.get(params.sessionId);

    if (!session) {
      throw new Error(`Session ${params.sessionId} not found`);
    }

    session.pendingPrompt?.abort();
    session.pendingPrompt = new AbortController();

    try {
      await this.simulateTurn(params.sessionId, session.pendingPrompt.signal);
    } catch (err) {
      if (session.pendingPrompt.signal.aborted) {
        return { stopReason: "cancelled" };
      }

      throw err;
    }

    session.pendingPrompt = null;

    return {
      stopReason: "end_turn",
    };
  }

  private async simulateTurn(
    sessionId: string,
    abortSignal: AbortSignal,
  ): Promise<void> {
    // Send initial text chunk
    await this.connection.sessionUpdate({
      sessionId,
      update: {
        sessionUpdate: "agent_message_chunk",
        content: {
          type: "text",
          text: "I'll help you with that. Let me start by reading some files to understand the current situation.",
        },
      },
    });

    await this.simulateModelInteraction(abortSignal);

    // Send a tool call that doesn't need permission
    await this.connection.sessionUpdate({
      sessionId,
      update: {
        sessionUpdate: "tool_call",
        toolCallId: "call_1",
        title: "Reading project files",
        kind: "read",
        status: "pending",
        locations: [{ path: "/project/README.md" }],
        rawInput: { path: "/project/README.md" },
      },
    });

    await this.simulateModelInteraction(abortSignal);

    // Update tool call to completed
    await this.connection.sessionUpdate({
      sessionId,
      update: {
        sessionUpdate: "tool_call_update",
        toolCallId: "call_1",
        status: "completed",
        content: [
          {
            type: "content",
            content: {
              type: "text",
              text: "# My Project\n\nThis is a sample project...",
            },
          },
        ],
        rawOutput: { content: "# My Project\n\nThis is a sample project..." },
      },
    });

    await this.simulateModelInteraction(abortSignal);

    // Send more text
    await this.connection.sessionUpdate({
      sessionId,
      update: {
        sessionUpdate: "agent_message_chunk",
        content: {
          type: "text",
          text: " Now I understand the project structure. I need to make some changes to improve it.",
        },
      },
    });

    await this.simulateModelInteraction(abortSignal);

    // Send a tool call that DOES need permission
    await this.connection.sessionUpdate({
      sessionId,
      update: {
        sessionUpdate: "tool_call",
        toolCallId: "call_2",
        title: "Modifying critical configuration file",
        kind: "edit",
        status: "pending",
        locations: [{ path: "/project/config.json" }],
        rawInput: {
          path: "/project/config.json",
          content: '{"database": {"host": "new-host"}}',
        },
      },
    });

    // Request permission for the sensitive operation
    const permissionResponse = await this.connection.requestPermission({
      sessionId,
      toolCall: {
        toolCallId: "call_2",
        title: "Modifying critical configuration file",
        kind: "edit",
        status: "pending",
        locations: [{ path: "/home/user/project/config.json" }],
        rawInput: {
          path: "/home/user/project/config.json",
          content: '{"database": {"host": "new-host"}}',
        },
      },
      options: [
        {
          kind: "allow_once",
          name: "Allow this change",
          optionId: "allow",
        },
        {
          kind: "reject_once",
          name: "Skip this change",
          optionId: "reject",
        },
      ],
    });

    if (permissionResponse.outcome.outcome === "cancelled") {
      return;
    }

    switch (permissionResponse.outcome.optionId) {
      case "allow": {
        await this.connection.sessionUpdate({
          sessionId,
          update: {
            sessionUpdate: "tool_call_update",
            toolCallId: "call_2",
            status: "completed",
            rawOutput: { success: true, message: "Configuration updated" },
          },
        });

        await this.simulateModelInteraction(abortSignal);

        await this.connection.sessionUpdate({
          sessionId,
          update: {
            sessionUpdate: "agent_message_chunk",
            content: {
              type: "text",
              text: " Perfect! I've successfully updated the configuration. The changes have been applied.",
            },
          },
        });
        break;
      }
      case "reject": {
        await this.simulateModelInteraction(abortSignal);

        await this.connection.sessionUpdate({
          sessionId,
          update: {
            sessionUpdate: "agent_message_chunk",
            content: {
              type: "text",
              text: " I understand you prefer not to make that change. I'll skip the configuration update.",
            },
          },
        });
        break;
      }
      default:
        throw new Error(
          `Unexpected permission outcome ${permissionResponse.outcome}`,
        );
    }
  }

  private simulateModelInteraction(abortSignal: AbortSignal): Promise<void> {
    return new Promise((resolve, reject) =>
      setTimeout(() => {
        // In a real agent, you'd pass this abort signal to the LLM client
        if (abortSignal.aborted) {
          reject();
        } else {
          resolve();
        }
      }, 1000),
    );
  }

  async cancel(params: acp.CancelNotification): Promise<void> {
    this.sessions.get(params.sessionId)?.pendingPrompt?.abort();
  }
}

const input = Writable.toWeb(process.stdout);
const output = Readable.toWeb(process.stdin) as ReadableStream<Uint8Array>;

const stream = acp.ndJsonStream(input, output);
new acp.AgentSideConnection((conn) => new ExampleAgent(conn), stream);
```

## Code Walkthrough

<Steps>
  <Step title="Session State Management">
    The agent maintains a map of active sessions, each with an `AbortController` to handle cancellation:

    ```typescript theme={null}
    interface AgentSession {
      pendingPrompt: AbortController | null;
    }

    private sessions: Map<string, AgentSession>;
    ```
  </Step>

  <Step title="Initialize Method">
    Returns the protocol version and agent capabilities to the client:

    ```typescript theme={null}
    async initialize(_params: acp.InitializeRequest): Promise<acp.InitializeResponse> {
      return {
        protocolVersion: acp.PROTOCOL_VERSION,
        agentCapabilities: {
          loadSession: false,
        },
      };
    }
    ```
  </Step>

  <Step title="New Session Method">
    Creates a unique session ID and initializes session state:

    ```typescript theme={null}
    async newSession(_params: acp.NewSessionRequest): Promise<acp.NewSessionResponse> {
      const sessionId = Array.from(crypto.getRandomValues(new Uint8Array(16)))
        .map((b) => b.toString(16).padStart(2, "0"))
        .join("");

      this.sessions.set(sessionId, { pendingPrompt: null });
      return { sessionId };
    }
    ```
  </Step>

  <Step title="Prompt Method">
    Handles user prompts with cancellation support:

    ```typescript theme={null}
    async prompt(params: acp.PromptRequest): Promise<acp.PromptResponse> {
      const session = this.sessions.get(params.sessionId);
      
      // Cancel any existing prompt
      session.pendingPrompt?.abort();
      session.pendingPrompt = new AbortController();

      try {
        await this.simulateTurn(params.sessionId, session.pendingPrompt.signal);
      } catch (err) {
        if (session.pendingPrompt.signal.aborted) {
          return { stopReason: "cancelled" };
        }
        throw err;
      }

      return { stopReason: "end_turn" };
    }
    ```
  </Step>

  <Step title="Sending Text Chunks">
    Stream text responses to the client using `sessionUpdate`:

    ```typescript theme={null}
    await this.connection.sessionUpdate({
      sessionId,
      update: {
        sessionUpdate: "agent_message_chunk",
        content: {
          type: "text",
          text: "I'll help you with that...",
        },
      },
    });
    ```
  </Step>

  <Step title="Tool Calls Without Permission">
    Execute read-only tool calls that don't require user approval:

    ```typescript theme={null}
    // Send pending tool call
    await this.connection.sessionUpdate({
      sessionId,
      update: {
        sessionUpdate: "tool_call",
        toolCallId: "call_1",
        title: "Reading project files",
        kind: "read",
        status: "pending",
        locations: [{ path: "/project/README.md" }],
        rawInput: { path: "/project/README.md" },
      },
    });

    // Update to completed
    await this.connection.sessionUpdate({
      sessionId,
      update: {
        sessionUpdate: "tool_call_update",
        toolCallId: "call_1",
        status: "completed",
        content: [/* results */],
      },
    });
    ```
  </Step>

  <Step title="Permission Requests">
    Request user permission for sensitive operations:

    ```typescript theme={null}
    const permissionResponse = await this.connection.requestPermission({
      sessionId,
      toolCall: {
        toolCallId: "call_2",
        title: "Modifying critical configuration file",
        kind: "edit",
        status: "pending",
        locations: [{ path: "/home/user/project/config.json" }],
        rawInput: { /* ... */ },
      },
      options: [
        { kind: "allow_once", name: "Allow this change", optionId: "allow" },
        { kind: "reject_once", name: "Skip this change", optionId: "reject" },
      ],
    });

    // Handle the user's response
    if (permissionResponse.outcome.outcome === "cancelled") {
      return;
    }
    ```
  </Step>

  <Step title="Cancellation Support">
    Handle cancellation requests from the client:

    ```typescript theme={null}
    async cancel(params: acp.CancelNotification): Promise<void> {
      this.sessions.get(params.sessionId)?.pendingPrompt?.abort();
    }
    ```
  </Step>

  <Step title="Connection Setup">
    Initialize the agent connection using stdio streams:

    ```typescript theme={null}
    const input = Writable.toWeb(process.stdout);
    const output = Readable.toWeb(process.stdin) as ReadableStream<Uint8Array>;

    const stream = acp.ndJsonStream(input, output);
    new acp.AgentSideConnection((conn) => new ExampleAgent(conn), stream);
    ```
  </Step>
</Steps>

## Running the Example

<Note>
  This agent is designed to be launched by an ACP client. It communicates via stdin/stdout using the newline-delimited JSON format.
</Note>

### Option 1: Run with the Example Client

```bash theme={null}
npx tsx src/examples/client.ts
```

This will automatically spawn the agent and demonstrate a complete interaction.

### Option 2: Run from an ACP Client

Configure your ACP client (like [Zed](https://zed.dev)) to launch this agent:

```bash theme={null}
npx tsx path/to/agent.ts
```

### Option 3: Test Standalone

While the agent expects client communication via stdio, you can verify it runs:

```bash theme={null}
npx tsx src/examples/agent.ts
```

<Info>
  The agent will wait for JSON-RPC messages on stdin. Without a client, it will simply idle.
</Info>

## Key Concepts

### Session Management

Each agent session is independent and has its own state. Sessions are identified by unique IDs and can be cancelled independently.

### Tool Call Lifecycle

1. **Pending**: Tool call is created with `tool_call` update
2. **Executing**: Tool is running (optional progress updates)
3. **Permission Check**: For sensitive operations, request user approval
4. **Completed/Failed**: Final status sent via `tool_call_update`

### Permission Flow

For operations that modify files or perform sensitive actions:

1. Send initial tool call update
2. Call `connection.requestPermission()` with options
3. Wait for user response
4. Handle approval or rejection accordingly

### Streaming Updates

The agent streams responses in real-time using `sessionUpdate` notifications:

* `agent_message_chunk`: Text responses
* `tool_call`: New tool execution
* `tool_call_update`: Tool status changes
* `agent_thought_chunk`: Internal reasoning (optional)

## Next Steps

<CardGroup cols={2}>
  <Card title="Simple Client Example" icon="desktop" href="/examples/simple-client">
    See how to build a client that connects to this agent
  </Card>

  <Card title="API Reference" icon="book" href="https://agentclientprotocol.github.io/typescript-sdk/classes/AgentSideConnection.html">
    Explore the full AgentSideConnection API
  </Card>

  <Card title="Production Examples" icon="rocket" href="/examples/production-examples">
    Study real-world agent implementations
  </Card>

  <Card title="Protocol Overview" icon="network-wired" href="/concepts/protocol-overview">
    Learn more about the ACP protocol
  </Card>
</CardGroup>
