pleach
Cookbook

instrumentedCodingAgent

Coding-agent runtime whose executeStep(...) calls inherit a @pleach/observe sub-agent attribution scope. Every recordCall(...) downstream of the step carries [subagent, ...] without threading a recorder argument.

instrumentedCodingAgent wraps createCodingAgentRuntime from @pleach/coding-agent with a subagent(name).run(...) scope around each executeStep(...) call. The decomposed sub-roles inside a coding turn — planner, executor, critic, and the sandbox-backed tool calls in between — all sit inside that AsyncLocalStorage scope, so per-call cost rows on the observability dashboard come back tagged by the sub-agent that issued the LLM call.

Best fit: a coding-agent shop (the canonical audience), a frontier-lab direct enterprise (when the coding agent is one product line inside a large contract), and a sub-agent swarm (when each swarm worker is itself a coding agent and needs its own attribution leaf).

Quickstart

import { init } from "@pleach/observe";
import { memory } from "@pleach/observe/destinations";
import { createInMemorySandboxProvider } from "@pleach/sandbox/testing";
import { instrumentedCodingAgent } from "@pleach/recipes/coding-agent";

const dest = memory();
init({ destination: dest });

const agent = instrumentedCodingAgent({
  sandboxProvider: createInMemorySandboxProvider({ execHandlers: new Map() }),
  subagent: "coding-agent",
});

await agent.start();
try {
  const result = await agent.executeStep({
    userMessage: "fix the failing test in lib/parser.ts",
    stepId: "step-1",
  });
  console.log(result.ok, dest.rows); // rows tagged subagent: "coding-agent"
} finally {
  await agent.stop();
}

Regression-locked end-to-end in __tests__/cross-sku/codingAgentInstrumentedByObserve.integration.test.ts.

What it does

The recipe forwards every config field except subagent to createCodingAgentRuntime({...}) from @pleach/coding-agent, then wraps the returned runtime's executeStep(...) in observeSubagent(subagentName).run(...). The other lifecycle methods (start, stop, getContextSnapshot) pass through unchanged.

Compose freely with outer scopes — the recipe's inner subagent(subagent) scope nests beneath any outer scope:

import { subagent } from "@pleach/observe";

await subagent("tenant-abc").run(async () => {
  await agent.executeStep({ ... }); // path: ["tenant-abc", "coding-agent"]
});

This is the load-bearing composition pattern for multi-tenant coding-agent shops: the tenant is the outer scope, the coding-agent role is the inner scope, and the attribution path joins cleanly to billing on either column.

Config reference

interface InstrumentedCodingAgentConfig extends CodingAgentRuntimeConfig {
  /**
   * Sub-agent attribution label applied to all rows recorded
   * inside each executeStep(...) call. Defaults to
   * "coding-agent". Set to false to disable the ALS scope
   * (useful when the buyer is composing the recipe inside
   * their own outer scope).
   */
  subagent?: string | false;
}

interface InstrumentedCodingAgent extends CodingAgentRuntime {
  /**
   * The underlying coding-agent runtime — escape hatch for
   * advanced consumers (dispose, snapshot, ...).
   */
  readonly runtime: CodingAgentRuntime;
}

The remaining config fields (sandboxProvider, orchestratorConfig, plan / tool / system-prompt overrides, …) are forwarded to createCodingAgentRuntime from @pleach/coding-agent/runtime. See the SKU's own docs for the full surface.

Common gotchas

  • Nesting order matters. When wrapping inside an outer scope, the attribution path is [outer, subagent] — the outermost wrap is the leftmost path element. Reverse it and you'll group billing rows by sub-role instead of by tenant. Pick the order to match your downstream GROUP BY.
  • init({destination}) is a process-singleton. Call once at app boot, before driving any executeStep. The recipe does NOT call init itself; without it the subagent(...).run(...) wrap is a no-op pass-through.
  • subagent: false disables the inner scope. Use this when you are composing the recipe inside your own outer scope and don't want the recipe's default "coding-agent" label appearing on rows. Outer scopes still apply.
  • sandboxProvider is consumer-owned. The recipe is sandbox-agnostic — pass createInMemorySandboxProvider from @pleach/sandbox/testing for tests, or a real provider in production. The provider is forwarded unchanged.
  • Subpath import is load-bearing. Import from @pleach/recipes/coding-agent, not the root barrel.
  • Three optional peers. @pleach/coding-agent, @pleach/sandbox, and @pleach/observe are all OPTIONAL peers of @pleach/recipes. Install the trio alongside this recipe.

See also

On this page