Post

System Instructions Architecture in Antigravity

This article is part of the Antigravity Engineering Series.

Your agent writes code like a chatbot — overly polite, hedging every suggestion with “you might want to consider…”, emitting markdown explanations where you need raw code. You want it to act like a senior engineer: direct, opinionated, testing everything it ships.

This is a configuration problem, not a training problem. The system instructions control who the agent thinks it is, how it communicates, and what rules it follows. The Antigravity SDK provides two modes: one safe (append to defaults) and one nuclear (replace everything). Understanding when to use each — and what’s in the defaults you’d be replacing — is the difference between a well-tuned agent and one that ignores safety rules because you accidentally discarded them. It’s the architectural equivalent of the difference between extending a base configuration and overriding it entirely.


The Two Modes

---
title: "The Two Modes. Templated instructions safely extend defaults, while custom instructions replace everything."
---
flowchart LR
    subgraph Templated["TemplatedSystemInstructions (Recommended)"]
        direction TB
        A["Default safety rules ✅"]
        B["Default engineering standards ✅"]
        C["Default operational guidelines ✅"]
        D["+ Your identity override"]
        E["+ Your custom sections"]
    end

    subgraph Custom["CustomSystemInstructions (Advanced)"]
        direction TB
        F["Your text replaces EVERYTHING"]
        G["Default safety rules ❌"]
        H["Default engineering standards ❌"]
        I["Default operational guidelines ❌"]
    end

    style Templated fill:#1a1a2e,stroke:#34a853,color:#eee
    style Custom fill:#1a1a2e,stroke:#dc3545,color:#eee
1
2
3
4
5
from google.antigravity.types import (
    TemplatedSystemInstructions,  # Recommended
    CustomSystemInstructions,     # Advanced — use with caution
    SystemInstructionSection,
)

The union type SystemInstructions = CustomSystemInstructions | TemplatedSystemInstructions accepts either mode.


TemplatedSystemInstructions — The Safe Path

This is the recommended approach. It preserves all default instructions and lets you add an identity and custom sections on top — the open-closed principle applied to system prompts: open for extension, closed for modification.

Setting the identity

The identity field overrides the agent’s self-description without touching safety rules:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from google.antigravity import LocalAgentConfig
from google.antigravity.types import (
    CapabilitiesConfig,
    TemplatedSystemInstructions,
)

config = LocalAgentConfig(
    capabilities=CapabilitiesConfig(),
    system_instructions=TemplatedSystemInstructions(
        identity=(
            "You are a senior backend engineer specializing in PostgreSQL "
            "optimization and database schema design. You have 15 years of "
            "experience with high-scale transactional systems. You are direct, "
            "opinionated, and always back your recommendations with data."
        ),
    ),
)

The identity replaces the default self-description (e.g., “You are a helpful coding assistant”) but keeps everything else: security mandates, coding standards, tool usage guidelines.

Adding custom sections

SystemInstructionSection lets you append named sections to the system prompt. Each section has a title and content:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
config = LocalAgentConfig(
    capabilities=CapabilitiesConfig(),
    system_instructions=TemplatedSystemInstructions(
        identity="You are a senior backend engineer.",
        sections=[
            SystemInstructionSection(
                title="coding_standards",
                content=(
                    "- Always use type hints on function signatures\n"
                    "- Prefer composition over inheritance\n"
                    "- Write tests before implementation (TDD)\n"
                    "- Use snake_case for Python, camelCase for TypeScript"
                ),
            ),
            SystemInstructionSection(
                title="project_context",
                content=(
                    "This is a payment processing platform. The codebase uses:\n"
                    "- Python 3.12 with FastAPI\n"
                    "- PostgreSQL 16 with SQLAlchemy 2.0\n"
                    "- Redis for caching and rate limiting\n"
                    "- The test suite must pass before any PR is opened"
                ),
            ),
            SystemInstructionSection(
                title="communication_style",
                content=(
                    "- Be direct. Skip pleasantries.\n"
                    "- When you're unsure, say so explicitly.\n"
                    "- Show the diff, not a description of the diff.\n"
                    "- If a test fails, fix it — don't explain why it failed."
                ),
            ),
        ],
    ),
)

Sections are appended to the system prompt in registration order. The title field is used for organization and deduplication — two sections with the same title will both appear.


CustomSystemInstructions — The Nuclear Option

This replaces everything. The default system prompt — including all safety rules, engineering standards, and tool usage guidelines — is discarded. Use this only when you understand what you’re discarding:

1
2
3
4
5
6
7
8
9
10
11
config = LocalAgentConfig(
    capabilities=CapabilitiesConfig(),
    system_instructions=CustomSystemInstructions(
        text=(
            "You are an API documentation writer. Your sole purpose is to "
            "read Python source files and produce OpenAPI-compliant YAML "
            "documentation. Never execute code. Never modify source files. "
            "Output only YAML."
        ),
    ),
)

What you lose

The SDK’s default system prompt contains three categories that CustomSystemInstructions discards:

CategoryWhat It ContainsRisk of Losing It
Core MandatesCredential protection, secret handling, file permission rulesAgent may expose secrets in conversation
Engineering StandardsCode style, testing requirements, linting rulesAgent may write inconsistent or untested code
Operational GuidelinesResponse format, tool usage patterns, brevity rulesAgent may produce verbose, unstructured output

When CustomSystemInstructions is appropriate:

  • Building a non-coding agent (documentation writer, chatbot, data analyst)
  • The default instructions conflict with your use case
  • You’ve audited the defaults and are deliberately replacing them

When it’s not:

  • You just want to change the tone or add project context → use TemplatedSystemInstructions
  • You want the agent to “be more aggressive” → set identity, don’t replace everything

GeminiConfig and Model Selection

System instructions interact with model configuration. The GeminiConfig controls which model processes those instructions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from google.antigravity.types import (
    GeminiConfig,
    ModelConfig,
    ModelEntry,
    GenerationConfig,
    ThinkingLevel,
)

gemini = GeminiConfig(
    api_key="your-api-key",   # Or set GEMINI_API_KEY env var
    models=ModelConfig(
        default=ModelEntry(
            name="gemini-3.5-flash",
            generation=GenerationConfig(
                thinking_level=ThinkingLevel.HIGH,  # More reasoning
            ),
        ),
        image_generation=ModelEntry(
            name="gemini-3.1-flash-image-preview",
        ),
    ),
)

ThinkingLevel

Controls how much reasoning the model does before responding — a cost/quality tradeoff that maps directly to the complexity of the task:

LevelUse CaseToken Cost
MINIMALSimple file reads, search, navigationLowest
LOWStraightforward edits, boilerplate generationLow
MEDIUMMulti-file changes, debuggingMedium
HIGHArchitecture decisions, complex refactoringHighest
1
2
3
4
5
6
7
8
9
10
11
12
13
# Same task, different thinking levels:

# MINIMAL — fast, cheap, sometimes wrong on complex tasks
fast_model = ModelEntry(
    name="gemini-3.5-flash",
    generation=GenerationConfig(thinking_level=ThinkingLevel.MINIMAL),
)

# HIGH — slower, expensive, better for complex reasoning
smart_model = ModelEntry(
    name="gemini-3.5-flash",
    generation=GenerationConfig(thinking_level=ThinkingLevel.HIGH),
)

Per-model API keys

Different models can use different API keys:

1
2
3
4
5
6
7
8
9
10
models = ModelConfig(
    default=ModelEntry(
        name="gemini-3.1-pro-preview",
        api_key="key-for-pro",  # Overrides GeminiConfig.api_key
    ),
    image_generation=ModelEntry(
        name="gemini-3.1-flash-image-preview",
        api_key="key-for-image",  # Separate key for image model
    ),
)

Vertex AI backend

For enterprise deployments, switch to the Vertex AI backend:

1
2
3
4
5
gemini = GeminiConfig(
    vertex=True,
    project="my-gcp-project",
    location="us-central1",
)

Structured Output with response_schema

response_schema compiles a Pydantic model or JSON schema into the finish tool’s schema. The agent returns structured data instead of free text — transforming the agent from a conversational endpoint into a typed service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
from pydantic import BaseModel, Field
from google.antigravity import Agent, LocalAgentConfig
from google.antigravity.types import CapabilitiesConfig, BuiltinTools


class CodeReviewResult(BaseModel):
    """Structured output from a code review agent."""
    files_reviewed: list[str] = Field(description="Files that were reviewed")
    issues: list[dict] = Field(
        description="Issues found, each with 'file', 'line', 'severity', 'message'"
    )
    test_coverage_delta: float = Field(
        description="Estimated change in test coverage (-1.0 to +1.0)"
    )
    recommendation: str = Field(
        description="One of: 'approve', 'request_changes', 'needs_discussion'"
    )
    summary: str = Field(description="One-paragraph summary of findings")


async def main():
    config = LocalAgentConfig(
        capabilities=CapabilitiesConfig(
            enabled_tools=BuiltinTools.read_only(),
        ),
        system_instructions=TemplatedSystemInstructions(
            identity="You are a meticulous code reviewer.",
            sections=[
                SystemInstructionSection(
                    title="review_standards",
                    content=(
                        "Focus on: security vulnerabilities, "
                        "performance regressions, test coverage gaps, "
                        "and API contract violations."
                    ),
                ),
            ],
        ),
        response_schema=CodeReviewResult,
    )

    async with Agent(config) as agent:
        response = await agent.chat(
            "Review the changes in the latest PR: files in src/auth/ and src/billing/"
        )

        # structured_output() returns a dict, not a Pydantic instance.
        # Parse it into the model yourself:
        raw = await response.structured_output()
        if raw:
            result = CodeReviewResult(**raw)
            print(f"Recommendation: {result.recommendation}")
            print(f"Issues: {len(result.issues)}")
            print(f"Coverage delta: {result.test_coverage_delta:+.0%}")


if __name__ == "__main__":
    import asyncio
    asyncio.run(main())

structured_output() returns a dict, not a Pydantic model instance. You must parse it yourself: CodeReviewResult(**raw). This also gives you Pydantic’s validation — if the model returns data that doesn’t match your schema, ValidationError fires immediately rather than failing downstream.

The FINISH tool must remain in enabled_tools for structured output to work. If you disable it, the agent can’t return results.

Schema validation and auto-retry

The response_schema constraint isn’t enforced at the model level — the SDK validates the model’s output against your Pydantic schema and auto-retries if it doesn’t match. In practice, this means:

  • The model’s first attempt may omit required fields (e.g., missing severity on some issues)
  • The SDK logs a warning and retries with the validation error as feedback
  • The second attempt typically succeeds
1
2
3
4
WARNING: The model produced an invalid tool call.
("invalid_signature: arguments do not match schema:
  - at '/issues/1': missing property 'severity'
  - at '/issues/2': missing property 'severity'")

This retry is transparent — your code receives only the valid final result. But it does mean you pay for an extra round-trip. If your schema is complex (many nested required fields), consider simplifying it or adding clearer Field(description=...) hints to reduce first-attempt failures.


Practical Personas

Backend engineer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
TemplatedSystemInstructions(
    identity=(
        "You are a senior backend engineer with deep expertise in "
        "distributed systems, PostgreSQL, and API design. You prioritize "
        "correctness over speed, write tests before implementation, "
        "and treat every database migration as a potential production incident."
    ),
    sections=[
        SystemInstructionSection(
            title="standards",
            content="- TDD mandatory\n- Type hints on all functions\n- No raw SQL — use the ORM",
        ),
    ],
)

Frontend designer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
TemplatedSystemInstructions(
    identity=(
        "You are a senior frontend developer specializing in React, "
        "TypeScript, and design systems. You think in components, "
        "write accessible HTML by default, and treat console.log "
        "as a code smell."
    ),
    sections=[
        SystemInstructionSection(
            title="design_system",
            content="- Use design tokens from tokens.json\n- No inline styles\n- All interactive elements need aria labels",
        ),
    ],
)

DevOps specialist

1
2
3
4
5
6
7
8
9
10
11
12
13
14
TemplatedSystemInstructions(
    identity=(
        "You are a DevOps engineer focused on reliability and observability. "
        "You write infrastructure as code, never make manual changes, "
        "and treat every outage as a learning opportunity. You're paranoid "
        "about security and treat secrets like radioactive material."
    ),
    sections=[
        SystemInstructionSection(
            title="infrastructure_rules",
            content="- Never hardcode secrets\n- All changes via Terraform\n- Every alert needs a runbook",
        ),
    ],
)

The Product Surface

FeatureTypeSource
TemplatedSystemInstructionsSafe mode — append to defaultsgoogle.antigravity.types
CustomSystemInstructionsAdvanced — full replacementgoogle.antigravity.types
SystemInstructionSectionNamed appendable sectionsgoogle.antigravity.types
GeminiConfigModel backend + authgoogle.antigravity.types
ModelEntryPer-model config + API keygoogle.antigravity.types
ThinkingLevelReasoning depth controlgoogle.antigravity.types
response_schemaStructured output via PydanticLocalAgentConfig field

What You Now Know

System instructions are a two-mode system: TemplatedSystemInstructions (safe, recommended) appends your identity and sections to the defaults. CustomSystemInstructions (nuclear, advanced) replaces everything — including the safety rules you probably want to keep.

The identity field is the single most impactful configuration you can set. It transforms the agent from a generic assistant into a specialist who reasons about your domain. Combine it with SystemInstructionSection for project-specific rules, ThinkingLevel for cost/quality tradeoff, and response_schema for structured output — and you have a fully customized agent that behaves like a team member, not a chatbot.

This is ultimately a question of interface design: how do you tell a system what kind of system it should be? The answer, as in most well-designed architectures, is through explicit, declarative configuration — not through hints scattered across conversations.


Companion code for this post is available at antigravity-system-instructions.

This post is licensed under CC BY 4.0 by the author.