Maniac Docs
Permissions

HITL & checkpoints

Human-in-the-loop pause and resume, RunCheckpoint snapshots, SessionPermissionCache, and durable RunCheckpointStore.

HITL & checkpoints

When a policy (or guardrail) returns requires_approval: true, the runner pauses the agent loop, emits an approval trace event (phase: "requested"), and returns AgentResult.status === "paused" with a RunCheckpoint and pending_approvals.

The caller surfaces approvals to a human, then resumes with runAgentResume or runAgentResumeStream, supplying ApprovalResponse[].

Pause flow

sequenceDiagram
  participant Runner
  participant Policy
  participant Human
  Runner->>Policy: evaluate(pendingAction)
  Policy-->>Runner: requires_approval: true
  Runner->>Runner: mint RunCheckpoint
  Runner-->>Human: paused + pending_approvals
  Human-->>Runner: ApprovalResponse[]
  Runner->>Runner: runAgentResume(checkpoint, responses)

RunCheckpoint

A durable snapshot of run state at the pause point:

interface RunCheckpoint {
  schema_version: 1 | 2;
  spec_id: string;
  iteration: number;
  usage: { prompt: number; completion: number };
  root_messages: Message[];
  subagent_sessions: SerializedSession[];
  pending: PendingApproval[];
  checkpoint_id?: string | null;
}

pending lists every tool call awaiting a decision. Each PendingApproval carries an id (match this in ApprovalResponse), the tool call payload, and attribution metadata.

Resume API

import { runAgent, runAgentResume } from "@maniac-ai/agents";

const paused = await runAgent(spec, "Delete account 12345");
if (paused.status !== "paused") throw new Error("expected pause");

const result = await runAgentResume(
  spec,
  paused.checkpoint!,
  [{ id: "call_delete", decision: "approve" }]
);
// result.status === "completed"

ApprovalResponse.decision is "approve" or "deny". Denied calls return an error result to the model; approved calls execute and the loop continues.

Streaming resume

import { runAgentStream, runAgentResumeStream } from "@maniac-ai/agents";

for await (const env of runAgentStream(spec, query)) {
  if (env.type === "paused") {
  // surface env.pending to reviewer before terminal result
  }
}

for await (const env of runAgentResumeStream(spec, checkpoint, responses)) {
  if (env.type === "result") { /* done */ }
}

See StreamEnvelope for the tagged union consumed by stream iterators.

SessionPermissionCache

Wrap any inner PermissionPolicy with a TTL cache so repeated identical calls within a session skip re-evaluation.

import { SessionPermissionCache, StaticPermissionPolicy } from "@maniac-ai/agents";

const policy = new SessionPermissionCache(
  new StaticPermissionPolicy([/* rules */], { allowed: true, requires_approval: false }),
  { ttlSeconds: 300 }
);

Cache key: principal:toolset:tool:hash(args).

Important: requires_approval decisions are never cached — each approval pause is one-shot. After the human approves, call remember() to pin an allow decision for the rest of the session ("always allow this" UX):

policy.remember(pendingAction, { allowed: true, requires_approval: false });

RunCheckpointStore

For channel bots and long-lived hosts, persist checkpoints to Memory so approval buttons survive process restarts.

import { RunCheckpointStore } from "@maniac-ai/agents/memory";

const store = new RunCheckpointStore(memory, { claimTtlSeconds: 86_400 });

await store.save("support", checkpoint, { threadId: "slack-thread-123" });
const claimed = await store.claim("support", checkpointId, { threadId: "slack-thread-123" });

Lifecycle states: pendingresolvingresolved. Claim TTL prevents double-resolution when multiple workers handle the same approval webhook.

The Slack channel example (packages/agents/examples/channels-slack-bot.ts) combines StaticPermissionPolicy, ChatToolset({ requireApproval: true }), and serveChannels for end-to-end HITL.

Trace events

Approval phases emit approval trace events you can render in chat UIs:

PhaseMeaning
requestedRun paused, awaiting human
resolvedHuman responded, resume in progress

Export via Tracer or OTelTracer.

On this page