Skip to content

Latest commit

 

History

History
301 lines (246 loc) · 17.2 KB

File metadata and controls

301 lines (246 loc) · 17.2 KB

Architecture

This document provides a bird's-eye view of the Agent Please codebase. It is intended to help contributors (human and AI) orient themselves quickly and understand how the pieces fit together.

For the full specification, see SPEC.md. For the upstream reference implementation, see vendor/symphony/SPEC.md.

System Purpose

Agent Please is a long-running TypeScript daemon that turns issue tracker tasks into autonomous Claude Code agent sessions. It continuously polls an issue tracker (GitHub Projects v2 or Asana), creates an isolated workspace for each eligible issue, renders a Liquid prompt template, and launches a Claude Code agent session inside that workspace via the @anthropic-ai/claude-agent-sdk.

The service is primarily a scheduler/runner — it does not perform full ticket management. The orchestrator writes only status labels to GitHub issues. All state transitions, PR creation, and comments are performed by the Claude Code agent using tools available in the runtime environment.

Entry Points

File Purpose
apps/agent-please/src/index.ts Binary entry point — calls runCli()
apps/agent-please/src/cli.ts CLI argument parsing (run, init, --port) and Nuxt server startup
packages/core/src/orchestrator.ts Core poll/dispatch/retry loop — start reading here for runtime behavior
apps/agent-please/server/plugins/01.orchestrator.ts Nitro plugin: creates & starts the Orchestrator on server boot
apps/agent-please/server/plugins/02.chat-bot.ts Nitro plugin: Chat SDK bot (GitHub + Slack adapters)
apps/agent-please/server/plugins/03.auth.ts Nitro plugin: Better Auth initialization and admin bootstrap
apps/agent-please/server/orpc/router.ts oRPC router — all typed API procedures (state, refresh, issues, sessions, projects)
apps/agent-please/server/api/webhooks/github.post.ts POST /api/webhooks/github — GitHub webhook handler
apps/agent-please/server/api/webhooks/slack.post.ts POST /api/webhooks/slack — Slack webhook handler
apps/agent-please/server/api/webhooks/asana.post.ts POST /api/webhooks/asana — Asana webhook handler
WORKFLOW.md User-authored config file in the target repository (not this repo) — defines tracker settings, hooks, agent limits, and the Liquid prompt template

Module Structure

agent-please/                      # Monorepo root (Bun + Turborepo)
├── apps/agent-please/             # Main Nuxt application (@pleaseai/agent)
│   ├── src/                      # CLI entry point
│   │   ├── index.ts              # Binary entry point — calls runCli()
│   │   ├── cli.ts                # CLI parsing (Commander) → starts Nuxt server
│   │   └── init.ts               # `agent-please init` — scaffolds GitHub Project + WORKFLOW.md
│   ├── app/                      # Nuxt client-side application
│   │   ├── app.vue               # Root component with <UApp>
│   │   ├── layouts/dashboard.vue # Nuxt UI Dashboard layout (sidebar + panels)
│   │   ├── pages/
│   │   │   ├── index.vue         # Dashboard: metrics, running/retry tables
│   │   │   ├── issues/[identifier].vue  # Issue detail: session, retry, events
│   │   │   ├── projects/         # Tracker project board view
│   │   │   ├── sessions/         # Session message viewer
│   │   │   └── login.vue         # Auth login page
│   │   ├── components/           # StateBadge, RunningTable, RetryTable
│   │   ├── composables/          # useOrchestratorState, useIssueDetail, useProjectBoard, useProjects, useSessionMessages
│   │   └── utils/                # format.ts, types.ts
│   ├── server/                   # Nitro server-side
│   │   ├── plugins/
│   │   │   ├── 01.orchestrator.ts # Nitro plugin: creates & starts Orchestrator
│   │   │   ├── 02.chat-bot.ts    # Nitro plugin: Chat SDK (GitHub + Slack adapters)
│   │   │   └── 03.auth.ts        # Nitro plugin: Better Auth initialization
│   │   ├── orpc/
│   │   │   ├── router.ts         # oRPC typed procedures (state, refresh, issues, sessions, projects)
│   │   │   ├── middleware.ts     # oRPC middleware (auth)
│   │   │   └── schemas.ts        # Zod schemas for oRPC
│   │   ├── routes/rpc/           # Nitro route handler — mounts oRPC at /rpc/*
│   │   ├── api/
│   │   │   ├── auth/[...all].ts  # Better Auth handler
│   │   │   ├── webhooks/github.post.ts # POST /api/webhooks/github
│   │   │   ├── webhooks/slack.post.ts  # POST /api/webhooks/slack
│   │   │   └── webhooks/asana.post.ts  # POST /api/webhooks/asana
│   │   └── utils/orchestrator.ts # useOrchestrator() helper
│   └── nuxt.config.ts            # Nuxt config (Bun preset, Nuxt UI)
├── packages/core/                # @pleaseai/agent-core — orchestrator business logic
│   └── src/
│       ├── orchestrator.ts       # Core loop: poll → reconcile → dispatch → retry
│       ├── config.ts             # YAML front matter → typed ServiceConfig
│       ├── workflow.ts           # WORKFLOW.md parser
│       ├── prompt-builder.ts     # Liquid template rendering
│       ├── agent-runner.ts       # Claude Code agent session via claude-agent-sdk
│       ├── workspace.ts          # Per-issue directory management, git worktrees
│       ├── tools.ts              # MCP tool server (asana_api, github_graphql)
│       ├── label.ts              # GitHub label management
│       ├── filter.ts             # Assignee and label filter matching
│       ├── webhook.ts            # GitHub webhook verification + event filtering
│       ├── dispatch-lock.ts      # Distributed dispatch lock (prevents duplicate agent dispatch)
│       ├── issue-comment-handler.ts # GitHub issue comment @mention dispatch handler
│       ├── relay-transport.ts    # WebSocket relay transport (cloud relay mode)
│       ├── db.ts                 # Agent run history storage (Kysely — bun:sqlite or Turso)
│       ├── db-types.ts           # Kysely database schema types
│       ├── server.ts             # Standalone HTTP server (used without Nuxt)
│       ├── state.ts              # Orchestrator state helpers
│       ├── logger.ts             # Structured logging (consola withTag)
│       ├── agent-env.ts          # Runtime env-var resolution for agent sessions
│       ├── session-renderer.ts   # Session message extraction (claude-agent-sdk)
│       ├── types.ts              # Shared type definitions
│       ├── tracker/              # Issue tracker adapters (GitHub, Asana)
│       └── index.ts              # Barrel export
├── (external) @pleaseai/relay-client  # WebSocket relay client (npm, from pleaseai/relay)
├── (external) @pleaseai/relay-server  # PartyServer relay server (npm, from pleaseai/relay)
├── apps/relay-worker/             # @pleaseai/relay-worker — Cloudflare Worker deployment
├── apps/docs/                    # @pleaseai/docs — Documentation site (Docus)
├── vendor/symphony/              # Upstream Symphony reference spec (read-only)
├── turbo.json                    # Turborepo task pipeline
├── eslint.config.ts              # @antfu/eslint-config
└── tsconfig.json                 # TypeScript strict mode, ESNext target

Data Flow

                    WORKFLOW.md (target repo)
                         │
                    ┌────▼────┐
                    │ Workflow │  Parse YAML front matter
                    │ Loader   │  + Liquid prompt body
                    └────┬────┘
                         │
                    ┌────▼────┐
                    │ Config   │  Typed ServiceConfig with
                    │ Layer    │  env-var ($VAR) resolution
                    └────┬────┘
                         │
          ┌──────────────▼──────────────┐
          │        Orchestrator         │
          │  poll → reconcile → dispatch│
          │  → retry (exponential)      │
          └──┬────────┬────────┬───────┘
             │        │        │
     ┌───────▼──┐ ┌───▼───┐ ┌─▼──────────┐
     │ Tracker  │ │Workspace│ │ Agent      │
     │ Client   │ │Manager │ │ Runner     │───▶ DB (Kysely/Turso)
     │(GitHub/  │ │(create,│ │(claude-    │    run history
     │ Asana)   │ │ hooks, │ │ agent-sdk) │
     └──────────┘ │worktree│ └────────────┘
                  └────────┘

          ┌──────────────────────────────┐
          │        Chat SDK Bot          │
          │  GitHub adapter + Slack      │
          │  @mention → status response  │
          └──────────────────────────────┘

Startup

Before the first tick, start() calls startupTerminalWorkspaceCleanup(), which fetches all issues in terminal states and removes their local workspaces. This keeps the workspace root clean after a service restart.

Orchestrator Tick Cycle

Each poll tick executes in order:

  1. Reconcile — Refresh running issue states from the tracker; terminate workers for terminal/non-active issues; detect stalled agents.
  2. Validate — Re-check config validity (supports live reload via file watcher).
  3. Create tracker adapter — Instantiate a TrackerAdapter from config. On adapter error, log and skip remaining steps.
  4. Process watched states — Dispatch agents for watched-state issues (e.g. Human Review) that have review activity (review decision or unresolved threads).
  5. Fetch candidates — Poll active issues from the tracker with optional assignee/label filters.
  6. Sort and dispatch — Priority ascending, then oldest first. Check global and per-state concurrency limits. Create workspace, run hooks, start agent session.
  7. Schedule next tick — Wait polling.interval_ms before repeating.

Agent Session Lifecycle

  1. createWorkspace() — Creates or reuses a per-issue directory (or git worktree if issue URL points to a GitHub repo). Runs after_create hook on first creation.
  2. runBeforeRunHook() — Executes the optional before_run shell hook.
  3. AppServerClient.startSession() — Validates workspace path against workspace.root (path traversal prevention) and assigns a local session UUID. No SDK communication occurs yet — the real session is established when runTurn() receives a system/init event.
  4. AppServerClient.runTurn() — Calls query() from @anthropic-ai/claude-agent-sdk with the rendered prompt. Translates SDK messages into orchestrator events (session_started, turn_completed, turn_failed, notification). Supports multi-turn: after each turn, refreshes issue state; continues if still active and under max_turns.
  5. runAfterRunHook() — Executes the optional after_run shell hook.
  6. On exit — normal exits schedule a 1s continuation retry; failures schedule exponential backoff retries up to max_retry_backoff_ms.

Architecture Invariants

These constraints must hold across the codebase. Violating them is a bug.

  1. Minimal tracker writes — The orchestrator does not write to the issue tracker except for the LabelService (label.ts) which applies dispatched/done/failed labels to GitHub issues. All state transitions, PR operations, and comments are performed by the agent.

  2. Workspace path validation — Every workspace path must be validated against config.workspace.root before any agent launch. The path must be a strict child of the root (not equal to it), and symlink traversal is checked. See validateWorkspacePath() and AppServerClient.validateWorkspaceCwd().

  3. Config immutability during tick — The config object is replaced atomically on reload, never mutated in place. The orchestrator reads config at the start of each tick.

  4. Concurrency limits are hardmax_concurrent_agents and per-state limits (max_concurrent_agents_by_state) are checked before every dispatch. A dispatch never exceeds these limits.

  5. Error types, not exceptions — Tracker operations, config validation, and workflow parsing return discriminated union error types (e.g., TrackerError, ValidationError, WorkflowError) instead of throwing. Check for 'code' in result before proceeding.

  6. Env-var indirection — Config values matching $ENV_VAR are resolved from process.env at config build time. The resolved values (including secrets) are stored in the ServiceConfig object — $ENV_VAR references do not persist past startup.

  7. No vendor modificationsvendor/symphony/ is a read-only submodule. It is excluded from linting, type-checking, and builds.

Cross-Cutting Concerns

Error Handling

The codebase uses discriminated union types for expected errors rather than exceptions:

  • TrackerError — Network failures, API errors, unexpected payloads from GitHub/Asana
  • ValidationError — Missing or invalid config fields
  • WorkflowError — YAML parse errors, missing files, invalid front matter
  • PromptBuildError — Liquid template parse/render failures
  • InitError — GitHub API failures during agent-please init

Each error type has a code field for programmatic matching. The isTrackerError(), isWorkflowError(), isPromptBuildError(), and isInitError() type guards are used for narrowing.

Testing

  • Runner: Bun test (Jest-compatible API)
  • Pattern: Unit tests co-located with source files (*.test.ts alongside *.ts)
  • Mocking: AppServerClient accepts an injectable queryFn for testing without the real Claude CLI. Tracker adapters are tested against mock GraphQL/REST responses. Workspace operations use spyOn(_git, 'spawnSync') to mock git commands.
  • Commands: bun run test (all), bun run test:app (agent-please only)

Logging

Structured logging via consola with tag-based namespacing (createLogger('tag') in logger.ts). Log lines are prefixed with module tags (e.g., [orchestrator], [db], [chat-bot]). Verbose mode can be toggled with setVerbose() to increase log level.

Configuration

Single-file configuration via WORKFLOW.md in the target repository:

  • YAML front matter — Tracker connection, polling interval, workspace root, hooks, agent limits, Claude CLI settings
  • Liquid template body — Prompt template rendered with issue context variables
  • Live reload — File watcher triggers re-parse; invalid configs are rejected with the last known-good config retained

Authentication

Tracker Method Config
GitHub Projects v2 PAT tracker.api_key or $GITHUB_TOKEN
GitHub Projects v2 GitHub App tracker.app_id + tracker.private_key + tracker.installation_id
Asana PAT tracker.api_key or $ASANA_ACCESS_TOKEN

GitHub App auth uses @octokit/auth-app to generate installation tokens. When both PAT and App credentials are present, PAT takes precedence.

MCP Tool Injection

The agent runner injects tracker-specific MCP tools into each Claude Code session:

  • github_graphql — Raw GraphQL queries/mutations using the service's authenticated Octokit
  • asana_api — Raw REST API calls using the service's Bearer token

These allow the agent to perform tracker writes (state transitions, comments) without needing separate credentials.

Chat Bot Integration

The Chat SDK plugin (02.chat-bot.ts) initializes platform adapters based on available configuration:

  • GitHub adapter — Requires github_projects tracker config + webhook secret. Handles @mention in issue comments via /api/webhooks/github.
  • Slack adapter — Requires SLACK_BOT_TOKEN + SLACK_SIGNING_SECRET env vars. Handles events via /api/webhooks/slack.

On @mention, the bot responds with current orchestrator status (running/retrying issues, token usage). The bot lifecycle is managed by the Nitro plugin (startup/shutdown).

Agent Run History

db.ts provides persistent storage for agent run records using Kysely:

  • Embedded mode — Local SQLite file via bun:sqlite + kysely-bun-sqlite (default: .agent-please/agent_runs.db)
  • Cloud mode — Turso remote database via @libsql/kysely-libsql using db.turso_url + db.turso_auth_token config

Records include issue identifier, session ID, duration, token usage, turn count, and status. The session messages API (/api/v1/sessions/:sessionId/messages) uses session-renderer.ts to extract and format messages from the Claude Agent SDK.