Skip to content

feat(agent): add-on system with ztk as the first built-in#2279

Draft
joshsny wants to merge 1 commit into
mainfrom
posthog-code/add-ons-with-ztk
Draft

feat(agent): add-on system with ztk as the first built-in#2279
joshsny wants to merge 1 commit into
mainfrom
posthog-code/add-ons-with-ztk

Conversation

@joshsny
Copy link
Copy Markdown
Contributor

@joshsny joshsny commented May 21, 2026

Problem

PostHog Code tasks need a way to opt in to extra agent behaviors per-task — currently the only knobs are buried inside claudeCode.options or mcpServers, and there's no symmetric path for cloud and local. The first concrete use case is ztk (https://github.com/codejunkie99/ztk), a CLI that wraps shell commands to reduce LLM token consumption, but the surface should support future capabilities (PostHog-aware system-prompt addenda, MCP-based integrations, etc.) without bespoke plumbing each time.

Changes

Adds a typed add-on system to @posthog/agent and a task.options.add_ons configuration channel so a user can enable extensions by name when kicking off a task. Architecture:

  • Unified registry, per-adapter capability honoring. packages/agent/src/add-ons/ defines AddOnDefinition (name, options schema, optional prepare, contribute) and AddOnRegistry. The same registry is consumed by both Claude and Codex adapters; each honors only the slots it can (Claude consumes all of env / systemPromptAppend / preToolUse / postToolUse; Codex consumes only systemPromptAppend because codex-acp exposes no pre-execution tool interception). Add-ons that need command rewriting declare supportedAdapters: ["claude"] and are skipped on Codex with a logged warning.
  • Single transport channel. Add-on config rides on _meta.addOns of the ACP newSession request, alongside the existing claudeCode / jsonSchema channels. Resolved in ClaudeAcpAgent.createSession (Claude) and CodexAcpAgent.{newSession, loadSession, unstable_resumeSession, unstable_forkSession} (Codex).
  • Cloud parity. AgentServerConfig.addOns and a matching --addOns <json> CLI flag on the sandbox agent server forward the same JSON onto the cloud newSession, so a task carries its add-ons regardless of where it runs.
  • First built-in: ztk. ztkAddOn contributes a Claude PreToolUse hook that rewrites tool_input.command for Bash tool calls into ztk run [--skip-permissions] -- <original>. Options: binaryPath, skipPermissions. Binary resolution checks the configured path, then $PATH, then known install locations (/opt/homebrew/bin, /usr/local/bin, /usr/bin), then ~/.local/bin.
  • Task model. Adds an optional options?: TaskOptions field (with add_ons inside) to both the agent-package Task type and the apps/code shared Task type, so the renderer and main process can populate it ahead of the Django migration.

Follow-ups not in this PR

  • Django migration. The Django Task model needs options = models.JSONField(default=dict, blank=True) plus a matching serializer field. Until that lands the field is absent on every API response and add-ons can only be exercised programmatically (e.g. by callers that build _meta.addOns directly).
  • UI. No task-creation UI changes yet — task.options.add_ons is settable from code but there's no "Add-ons" disclosure in the task input form.

How did you test this?

  • Added 19 unit tests across registry.test.ts (empty config, env merging with last-wins precedence, systemPromptAppend concatenation, pre/postToolUse aggregation, adapter gating, unknown-name warnings, options-validation failure, prepare/contribute ordering, prepare failures propagate, duplicate-registration rejection) and ztk.test.ts (options schema rejects invalid input, missing-binary errors from prepare, Bash command rewriting, non-Bash pass-through, --skip-permissions, Codex skipped via adapter gating, end-to-end registry resolution, double-wrap protection, single-quote escaping for paths with quotes).
  • pnpm --filter agent typecheck clean.
  • pnpm --filter agent test src/add-ons — 19/19 pass. Pre-existing options.test.ts (3) still pass.
  • Did not run a live ztk-wrapped session against Claude; the hook surface and rewriting are exercised in unit tests but a smoke test with a real ztk binary is a sensible pre-merge check.

Publish to changelog?

no


Created with PostHog Code

Introduces a per-task add-on system that lets users enable opt-in agent
capabilities by name, configured from a `task.options.add_ons` JSON blob.
The first built-in is `ztk`, a Claude PreToolUse hook that wraps Bash
commands through the ztk token-reduction CLI.

Architecture
- New `packages/agent/src/add-ons/` module with a typed `AddOnDefinition`
  contract, a registry that resolves names → contributions for the
  active adapter, and a process-wide default registry populated with
  the built-in add-ons.
- Unified registry, per-adapter capability honoring. Claude consumes
  every contribution slot (env, systemPromptAppend, preToolUse,
  postToolUse) via `buildSessionOptions`. Codex honors only
  `systemPromptAppend`; `preToolUse` / `postToolUse` have no equivalent
  in the upstream `codex-acp` binary, so add-ons that need command
  interception declare `supportedAdapters: ["claude"]` and are skipped
  on Codex with a logged warning.
- Add-on configuration travels through `_meta.addOns` on the ACP
  `newSession` request, parallel to the existing `claudeCode` and
  `jsonSchema` channels. Resolved by the adapter's `newSession`,
  `loadSession`, `unstable_resumeSession`, and `unstable_forkSession`.

Cloud parity
- `AgentServerConfig.addOns` plus a matching `--addOns <json>` CLI flag
  on the sandbox agent server forwards the config to the cloud
  `newSession`, so a task carries its add-ons whether it runs locally
  or on the cloud sandbox.

Task model
- New `Task.options` JSONField on the Django Task model is the
  client-side home for add-on config (`task.options.add_ons`). The
  Django migration is not in this PR — until it lands the field
  is absent on every API response and add-ons can only be exercised
  programmatically.

ztk specifics
- `ztk` add-on contributes a single Claude `PreToolUse` hook that
  rewrites `tool_input.command` for `Bash` tool calls to
  `ztk run [--skip-permissions] -- <original>`. Options: `binaryPath`,
  `skipPermissions`. Binary resolution checks the configured path, then
  PATH, then known install locations, then `~/.local/bin`. Skipped
  silently on Codex.

Tests
- 19 new tests across `registry.test.ts` (merge semantics, adapter
  gating, unknown-name handling, options-validation failure, prepare
  ordering) and `ztk.test.ts` (Bash rewriting, double-wrap protection,
  shell-quote escaping, --skip-permissions, Codex skip).

Generated-By: PostHog Code
Task-Id: de63edf0-0b52-40b4-9ffc-4758850b10d3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant