Files
pi-agent-config/docs/scenario-authoring.md
Kai f25082223e docs: repository README, contributor rules, scenario template and authoring guide
README leads with the finding that motivated the repository -- pi emits the
skills section only when a tool named 'read' is active, so Curator's
--no-tools --skill combination made its policy unreachable -- with the measured
before/after table and instructions to reproduce it at zero token cost.

AGENTS.md sets seven rules for anyone changing this repository. The third is the
one that matters most: verify pi's behaviour with a probe rather than inferring it
from the docs. Three claims in the first draft of these documents were wrong and
were only corrected by running one.

The _template scenario carries the isolation defaults and inline warnings at the
places where mistakes have already cost time: --no-tools disabling the skills
mechanism, cwd anchoring .pi discovery, the read override being mandatory rather
than optional, and allowed-tools frontmatter not being enforced in 0.84.3.
2026-08-26 23:19:19 -07:00

5.2 KiB

Authoring a Scenario

Practical checklist. Rationale lives in isolation-baseline.md, personality-layering.md and gateway-patterns.md.

1. Decide whether you need a dedicated agent at all

You do when all of these hold:

  • the task is narrow and repeats,
  • it needs backend facts that only your service can supply,
  • and a wrong answer has a cost (a write, a purchase, a deletion).

You do not when the task is one-off exploration — use interactive pi for that.

2. Copy the template

cp -r scenarios/_template scenarios/<name>

Fill in every <PLACEHOLDER> in profile.toml. It is the single source of truth for the launch contract; deploy-scenario.sh renders it into <workspace>/.pi/launch.json, and your gateway should read that file and fail closed if it is missing. Silently running without --no-extensions widens the agent's reach.

3. Write the personality

.pi/SYSTEM.md replaces pi's default prompt, and the replacement branch contributes neither the tool list nor the guidelines. Cover all six sections: identity and negative identity, tool overview, fact authority, write discipline, untrusted data, output discipline.

Do not include any path to pi's own documentation. The default prompt does, along with an instruction to read it and follow cross-references — for a non-coding agent that is a ready-made escalation path for injected text.

Durable domain responsibilities go in .pi/APPEND_SYSTEM.md, not AGENTS.md: context files layer from every parent directory and AGENTS.override.md does not stop that. -nc does, and system-prompt files are unaffected by it.

4. Write the extension

Build on shared/extensions/pi-guard-base.ts:

import { installGuard, registerRestrictedRead, registerBridgeTools, textResult }
  from "<repo>/shared/extensions/pi-guard-base.ts";

const ALLOWED = ["read", "my_query", "my_propose"];

export default function myGuard(pi: ExtensionAPI) {
  registerRestrictedRead(pi, {
    roots: [`${WS}/.pi/skills`],   // MUST include the skill dirs
    base: WS,
    extensions: [".md"],
  });
  registerBridgeTools(pi, { baseUrl: process.env.MY_BRIDGE_URL!, token: process.env.MY_BRIDGE_TOKEN }, specs);
  installGuard(pi, { scenario: "<name>", allowedTools: ALLOWED });
}

Non-negotiable:

  • A restricted read is mandatory. pi emits the skills section only when a tool named read is active, and skill bodies load through it. Without it every SKILL.md is dead. The built-in read accepts absolute paths and can reach your credential files, so override it rather than enabling it.
  • Every tool needs promptSnippet, otherwise it is callable but absent from the prose tool list.
  • Every tool truncates its own output (50 KB / 2000 lines).
  • Throw to signal failure. Returning an error-shaped object does not mark the call failed.
  • Writes do not belong to the agent. Expose a propose_* tool that records a plan and echoes the resolved identity; let deterministic code decide.

5. Write the gateway

Use shared/lib/py/pi_rpc.py. It gives you a long-lived RPC process, session rotation, a per-turn deadline enforced with abort, process-group cleanup, a minimal env, and receipts harvested from tool_execution_end.

from pi_rpc import PiLaunchConfig, PiRpcClient

client = PiRpcClient(PiLaunchConfig(
    pi_bin="/home/claw/.npm-global/bin/pi",
    workspace=Path("/home/claw/pi-workspaces/<name>"),
    session_dir=Path("/home/claw/.local/share/pi-<name>/sessions"),
    provider="zenmux", model="...", thinking="high",
    session_id_prefix="<name>-<subject>",
    extensions=(ext_path,), skills=(skill_dir,),
    receipt_tools=frozenset({"my_write"}),
))
result = await client.prompt(user_text)
reply = result.receipts[0] if result.receipts else result.text

Render any user-visible state change from result.receipts, never from result.text. Prose guardrails have already failed once in production: with has_file: false the model still wrote "已成功加入库中".

6. Split stateless work out

Classification, extraction and synthesis need no session, no skills and a lower thinking level. Run them with --no-session and a terminating tool carrying constrainedSampling — the tool's parameters is your output schema. Do not regex JSON out of prose, and do not share a session with the conversational role; alternating "only output JSON" and "do not output JSON" across turns causes mode confusion.

7. Verify before deploying

scripts/deploy-scenario.sh <name>              # dry run; checks referenced paths
shared/extensions/tests/run-guard-checks.sh    # adapt for your tool set
scripts/pi-diff.sh <name>

Assert, with a probe rather than by reading:

  • the system prompt does not contain expert coding assistant
  • <available_skills> lists only your skills
  • active tools are exactly your allowlist
  • read reports source: cli, not builtin
  • reads of an outside path, a ../ traversal and an absolute path to your credential file are all denied

8. Record conformance

Add a row to the table in isolation-baseline.md. If a layer is a known gap, say so and link the follow-up rather than leaving it blank.