Back to Blogs
#agents#ai#architecture#go

[Agent Platform in Action (7)] When ADK Go Graph Workflow Meets Google Research Harness (Part 1): Two-Layer Recursive Agent Fram

Foreword: After the AI Agent Goes Online, What is the Next Bottleneck? Welcome back to...

Updated 12 min read
Cover for [Agent Platform in Action (7)] When ADK Go Graph Workflow Meets Google Research Harness (Part 1): Two-Layer Recursive Agent Fram

Harness² Part 1 Architecture: Current Architecture vs c1..c8 Editable Harness Tree

Foreword: After the AI Agent Goes Online, What is the Next Bottleneck?

Welcome back to the "Agent Platform in Action" series! In the previous six articles, we took the LINE AI Card Assistant (kkdai/line-card-assistant) through the complete five stages from a standalone prototype to production-grade cloud deployment:

  1. [Agent Platform in Action (1)] Building LINE AI Card Assistant with Go and Google ADK 2.0 Graph Workflow: Using google.golang.org/adk/v2/workflow to build a 13-node directed graph featuring Vision OCR, AddFanOut / AddFanIn parallel background investigation, and NewRequestInputEvent HITL interaction loops.
  2. [Agent Platform in Action (2)] From Monolithic Cloud Run to Google Cloud Agent Runtime: Completed Phase 1 (PR #10), standardizing the project into an agents-cli (adk_go) dual-channel architecture.
  3. [Agent Platform in Action (3)] Implementing Cross-Instance Business Card Recognition and Pause/Resume with ADK 2.0 GCS Artifact Service and Agent Platform Sessions: Completed the first half of Phase 2 (PR #11), solving the HITL "amnesia" of stateless containers and the issue of binary image data explosion.
  4. [Agent Platform in Action (4)] Completing the Architecture Puzzle with Cloud Run LINE Webhook Gateway, Agent Runtime, and Cloud SQL PostgreSQL: Completed the second half of Phase 2 & Phase 3 (PR #12), upgrading the business card database to Cloud SQL PostgreSQL and achieving two-layer stateless decoupling.
  5. [Agent Platform in Action (5)] Establishing Automated Evaluation Benchmarks with agents-cli eval and ADK 2.0 Go: Completed Phase 4 (PR #13), establishing a golden test set and a quality flywheel.
  6. [Agent Platform in Action (6)] Building a Full-Stack Observable LINE AI Card Assistant with agents-cli deploy, Terraform IaC, GitHub Actions CI/CD, and Cloud Trace: Completed Phase 5 (PR #14), adding OpenTelemetry Spans (workflow.node.*) to all 13 Graph Nodes and deploying to the cloud via Terraform and GitHub Actions.

Additionally, we have planned advanced Graph Workflow primitives for future ADK v2.6+ (such as Parallel dynamic dispatch, MultiRoute multi-way fan-out, reflection-repair loops, and strongly typed nodes) into the Future Roadmap Issue #15.

However, now that our Agent possesses Phase 4 Automated Evaluation and Phase 5 Full-Node Tracing (OpenTelemetry Spans), a deeper question emerges:

When evaluations find that the Agent performs poorly in certain real-world cases (e.g., Japanese-English bilingual cards, searching for colloquial nicknames in the Taiwan tech industry, multi-clause colloquial HITL modifications, or financial compliance icebreaker letters), must engineers manually check logs, manually modify Prompt strings in the Go source code, and then recompile and redeploy every single time?

Can we let a Meta-Agent (Engineer Agent) automatically read our evaluation results and OpenTelemetry execution trajectories, diagnose the reasons for lost points, write new Skills and Long-Term Memory itself, and complete a closed-loop self-evolution?

This is precisely the core proposition addressed by Google Research's recently published and open-sourced google-research/harness2 (Harness²: Recursive Harnessing for LLM Agents, arXiv:2610.14224)!

This series will consist of three articles (corresponding to Issue #16, Issue #17, Issue #18, and PR #19), guiding you through the complete integration of Google Research Harness² into our Go ADK 2.0 project:

  • Part 1 (This article, Issue #16): What is Google Harness²? What can it do? Inventorying the existing architecture before modification and decoupling hardcoded configurations in the Go binary into a c1..c8 editable Harness file tree.
  • Part 2 (Issue #17): Encapsulating Phase 4's eval/datasets/ and Phase 5's OpenTelemetry Spans into a LineCardAdapter for Harness², and analyzing why "Zero-Leakage Barriers" and "High-Resolution Behavioral Trajectories" are both indispensable.
  • Part 3 (Issue #18): Complete testing tutorial, disclosure of Parallel vs Sequential + LongTermMEMORY two-layer recursive experimental data (accuracy jumping from 62.5% to 100.0%), and outlook on the future development of Agents.

1. What is Google Research Harness²? What can it do?

1. From "Fine-tuning Model Weights" to "Optimizing Agent Harnessing"

In the era of Large Language Models (LLMs), the success of an AI Agent is often determined not just by the underlying model (like Gemini 2.5 Flash / Pro) itself, but by the "Agent Harness" layer wrapped around the model—namely, System Prompts, formatting and safety Guardrails, Long-Term Memory, Tool Descriptions, Sub-Agent settings, and On-Demand Skills.

Past automated prompt optimizations (like PromptBreeder, TextGrad, or DSPy) mostly targeted mutations of a single Prompt string. However, in a real enterprise-grade Agent, if you stuff all edge-case patching rules into the same System Prompt, it quickly leads to "Context Bloat": the number of Always-On Tokens increases, which not only squeezes the model's Output Token Budget but also causes different rules to interfere with each other!

Google Research's Harness² (Recursive Harnessing for LLM Agents, arXiv:2610.14224) breaks this limitation. It defines the entire Agent's equipment as a structured c1..c8 Editable Component Tree and automatically evolves this file tree through Two-Level Recursion:

flowchart TB
    subgraph Outer["Outer Recursion: Harnessing-Level Recursion (Experience accumulation across rounds k=1..K)"]
        Reflector["Experience Reflector\n(ReflectorPrompt)\nDistilling success and failure patterns across rounds"]
        Memory["c3: LongTermMEMORY.md\nPersisting domain experience across iterations"]
        Reflector -->|Write long-term rules| Memory
    end

    subgraph Inner["Inner Recursion: Task-Level Recursion (Closed-loop diagnosis and patching within a single round)"]
        Propose["1. Propose (Diagnostic Proposal)\nRead trajectory.md\nLocate behavioral defects and propose N candidate Patches"]
        Probe["2. Probe (Sandbox Probing)\nExecute N candidate Harnesses in parallel\nCompare trajectory and score changes before and after modification"]
        Compose["3. Compose (Conflict Resolution and Synthesis)\nRetain positive-gain Patches, discard negative interference\nSynthesize the next-generation best Harness H^(k)"]
        Propose --> Probe --> Compose
    end

    Memory --> Propose
    Compose --> Reflector

Enter fullscreen mode Exit fullscreen mode

2. The c1..c8 Eight Harness Components and the 24KB Budget Limit

In google-research/harness2/harness.py, a standard Agent Harness consists of 4 Always-On Components (c1..c4) and 4 On-Demand Components (c5..c8):

Component Code File Path Rule Loading Mode Responsibility in Harness²
c1 systemprompt.md Always-On Defines core roles, node responsibilities, and indexing rules for triggering c6 skills (Skill Trigger Lines).
c2 guardrails.md Always-On Defines non-violable output formats (e.g., pure JSON constraints), E.164 normalization invariants, and security/PII guardrails.
c3 LongTermMEMORY.md Always-On Domain lessons and industry compliance memories written across rounds by the outer Experience Reflector.
c4 tool_descriptions/*.md Always-On Tool parameter descriptions, search strategies, and normalization details (e.g., Japan long-distance prefix (0) cleaning rules).
c5 sub_agents/<name>/AGENT.md On-Demand Exclusive Instructions and YAML Frontmatter (mode: subagent) for delegated sub-agents (e.g., company_enrich).
c6 skills/<name>/SKILL.md On-Demand Professional skills dynamically injected only for specific domains or tasks (e.g., Japanese card OCR, Hsinchu Science Park colloquial search expansion).
c7 scripts/*.py On-Demand Auxiliary verification or data conversion scripts.
c8 plugin/*.ts On-Demand Runtime lifecycle and OpenTelemetry diagnostic hooks.

Important

Why distinguish between c1..c4 (Always-On) and c5..c8 (On-Demand)? Harness² sets an iron rule in OPENCODE_SURFACE: The total byte count of c1..c4 always-on files must never exceed 24,000 bytes (AlwaysOnMaxBytes = 24000)! When Harness² finds that a failure case requires lengthy domain knowledge (e.g., a department title mapping table for Japanese corporate cards or a colloquial abbreviation dictionary for Taiwan semiconductor parks), it won't force these rules into systemprompt.md causing Context explosion. Instead, it will automatically create an on-demand c6 skill file (skills/<name>/SKILL.md) and leave only a lightweight Trigger Line in systemprompt.md!

3. How Does Two-Level Recursion Work?

The Harness² evolution engine (harness2/core/recursion.py) includes two levels of recursion:

  1. Level 1 (Inner): Task-Level Recursion (Propose $\rightarrow$ Probe $\rightarrow$ Compose):
    • Propose (Diagnostic Proposal): The optimizer (Harvester LLM) observes tasks that lost points in the benchmark, the final output (Deliverable), and the execution trajectory (trajectory.md). It diagnoses which step the Agent made a wrong decision on and proposes multiple independent candidate patches (Candidate Patches, each being a set of additions or modifications to c1..c8).
    • Probe (Sandbox Probing): The system mounts each candidate Patch into a sandbox and runs the task once, collecting new scores and new execution trajectories to form a "Before vs. After" Behavioral Contrast.
    • Compose (Conflict Resolution and Synthesis): The optimizer compares the actual performance of all candidate Patches, merges modifications that truly bring positive score improvements into the new generation Harness $H^{(k)}$, and discards ineffective or side-effect-inducing modifications.
  2. Level 2 (Outer): Harnessing-Level Recursion (Experience Reflector):
    • Between multiple iterations ($k = 1 \dots K$), the outer ReflectorPrompt reviews the evolution history from $H^{(0)} \to H^{(1)} \to \dots \to H^{(k)}$: Which types of modifications succeeded? Which cross-domain constraints (e.g., financial industry compliance headers and personal data protection regulations) need to be remembered at a global level?
    • Distilled high-level experiences are written into c3: LongTermMEMORY.md, guiding the next round of Propose and Agent execution.

2. Inventorying the Current Architecture: Why Phase 1~5 Go Implementation Cannot Be Directly Optimized by Harness²?

Let's compare the left side (Before: Hardcoded Go Binary Architecture) of the architecture diagram at the beginning of the article and review the system architecture we completed in Phase 1 to Phase 5:

flowchart LR
    subgraph Before["Before: Phase 1~5 Solid Compilation Architecture (Hardcoded Go Binary)"]
        direction TB
        N1["nodes_ingest.go\nHardcoded VisionOCR / CleanAndDedup /\nCompanyEnrich / ApplyEdit Prompts"]
        N2["nodes_search.go\nHardcoded QueryIntent /\nEmailDraft Prompts"]
        N3["graph.go\nFixed SubAgent Instruction"]
        E["eval/datasets/basic-dataset.json\nPhase 4 Golden Test Set"]
        T["telemetry.go\nPhase 5 OpenTelemetry 13-Node Spans"]
    end

    Problem["Pain Points:\n1. Prompts hardcoded in .go string constants; re-go build required for every adjustment\n2. Lack of on-demand c6 Skills mechanism\n3. Phase 4 Eval and Phase 5 OTel Spans not yet connected into an automated optimization loop"]
    Before --> Problem

Enter fullscreen mode Exit fullscreen mode

In the code before modification:

  1. All Prompt templates for VisionOCRNode, CompanyEnrichNode, ApplyEditNode (located in internal/workflow/nodes_ingest.go), as well as QueryIntentNode and EmailDraftNode (located in internal/workflow/nodes_search.go), are hardcoded as fmt.Sprintf(...) string constants in the Go code.
  2. The llmagent.Config{Instruction: ...} for CompanyEnrichAgent (located in internal/workflow/nodes_ingest.go) is also a fixed string hardcoded in a Go function.
  3. When we want the Harness² Python evolution loop to modify system prompts, add a Japanese business card recognition skill (skills/jp_card_ocr/SKILL.md), or adjust phone number normalization guidelines (tool_descriptions/tool_guidance.md), it is impossible for Harness² to rewrite our .go source code and go build again for every single task!

Therefore, the first step in introducing Harness² (Issue #16) is to completely decouple the Go compilation phase from the Harness equipment phase:

  • Go Binary is responsible for "Graph Topology and Dual-Channel Execution Engine": RouterNode, parallel Fan-Out/Fan-In, HITLConfirmNode pause/resume, Cloud SQL and GCS Artifact access, OpenTelemetry instrumentation.
  • External c1..c8 Harness file tree is responsible for the "Evolvable Brain and Skill Equipment": Dynamically hot-reloaded at runtime via the environment variable LINE_CARD_HARNESS_DIR!

3. Implementation Part 1: Building internal/harness Loader and harness/base/ Initial Equipment Tree

To make the Go ADK 2.0 Graph Workflow natively understand the Harness² c1..c8 specification, we added two core directories to the project:

  • harness/base/: Stores the initial unevolved ($k=0$) c1..c8 baseline file tree.
  • internal/harness/harness.go: Responsible for loading the c1..c8 file tree, validating OPENCODE_SURFACE 24KB limits and YAML Frontmatter rules, and dynamically combining always-on context with on-demand skills for each Graph Node.

1. Establishing the harness/base/ Baseline c1..c8 File Tree

We extracted the basic settings previously scattered in nodes_ingest.go and nodes_search.go into the harness/base/ directory:

harness/base/
├── systemprompt.md # c1: Core roles and overview of Graph Node responsibilities
├── guardrails.md # c2: JSON output format and phone normalization guardrails
├── LongTermMEMORY.md # c3: Cross-round long-term memory (initially empty, to be written by Reflector)
├── tool_descriptions/
│ └── tool_guidance.md # c4: Guidance for google_search and format_company_profile
├── sub_agents/
│ └── company_enrich/
│ └── AGENT.md # c5: CompanyEnrichAgent delegated sub-agent definition
├── skills/ # c6: On-demand skills directory (to be generated by Propose-Probe-Compose evolution)
├── scripts/ # c7: Auxiliary scripts directory
└── plugin/ # c8: Diagnostic plugin directory
Enter fullscreen mode Exit fullscreen mode

For example, harness/base/sub_agents/company_enrich/AGENT.md uses a YAML Frontmatter format fully compatible with google-research/harness2:

---
mode: subagent
description: Searches Google to summarize company background and industry tags.
---
You are a professional business analysis assistant. Please use the google_search tool to query the company name provided by the user, then call the format_company_profile tool to return a two-sentence Traditional Chinese company profile and 3 to 5 technical or industry tags (e.g., AI, Cloud, SaaS, FinTech, Semiconductor).
Enter fullscreen mode Exit fullscreen mode

2. Implementing internal/harness/harness.go: 24KB Always-On Budget Validation and On-Demand Skill Injection

In internal/harness/harness.go, we implemented the validator Validate() and node prompt synthesizer ComposeNodePrompt() fully aligned with the Python version of OPENCODE_SURFACE.validate():

// AlwaysOnMaxBytes matches google-research/harness2 OPENCODE_SURFACE.always_on_max (24,000 bytes).
const AlwaysOnMaxBytes = 24000

// AlwaysOnFiles lists the four c1..c4 always-on files mounted into the agent context.
var AlwaysOnFiles = []string{
    "systemprompt.md",
    "guardrails.md",
    "LongTermMEMORY.md",
    "tool_descriptions/tool_guidance.md",
}

// Validate enforces Google Research Harness² OPENCODE_SURFACE invariants on the c1..c8 tree.
func (h *Harness) Validate() error {
    var problems []string
    for _, reqFile := range AlwaysOnFiles {
        if strings.TrimSpace(h.Files[reqFile]) == "" {
            problems = append(problems, fmt.Sprintf("missing or empty always-on component %q", reqFile))
        }
    }
    if total := h.AlwaysOnBytes(); total > AlwaysOnMaxBytes {
        problems = append(problems, fmt.Sprintf(
            "always-on c1..c4 byte count %d exceeds cap %d (every token here competes with the agent's output budget)",
            total, AlwaysOnMaxBytes,
        ))
    }

    combinedAlwaysOn := h.SystemPrompt + "\n" + h.Guardrails
    for dirName, skill := range h.Skills {
        if skill.Name != dirName {
            problems = append(problems, fmt.Sprintf("%s frontmatter name %q must match directory name %q", skill.RelPath, skill.Name, dirName))
        }
        if strings.TrimSpace(skill.Description) == "" {
            problems = append(problems, fmt.Sprintf("%s must include a non-empty description in YAML frontmatter", skill.RelPath))
        }
        if !strings.Contains(combinedAlwaysOn, dirName) {
            problems = append(problems, fmt.Sprintf("skill %q is never referenced in systemprompt.md or guardrails.md; add a trigger line naming `%s`", dirName, dirName))
        }
    }
    // ...
}
Enter fullscreen mode Exit fullscreen mode

There are two very critical engineering details here:

  1. Mandatory Skill Trigger Reference Check: If Harness² adds a skill in skills/jp_card_ocr/SKILL.md but forgets to write when to trigger jp_card_ocr in systemprompt.md or guardrails.md, Validate() will directly report an error and intercept it, avoiding the creation of "Orphan Skills" that will never be called.
  2. Node-Level Dynamic Skill Matching (MatchSkills + ComposeNodePrompt): When the Graph Workflow executes VisionOCRNode, CompanyEnrichNode, ApplyEditNode, QueryIntentNode, or EmailDraftNode, it only injects c6 skills related to that node and automatically attaches adk.harness.skills and adk.harness.always_on_bytes attributes to the OpenTelemetry Span:
// composeHarnessPrompt enriches a node's base prompt with the active Harness² c1..c8 tree
// and records the active harness metadata onto the node's OpenTelemetry span.
func composeHarnessPrompt(span trace.Span, nodeName, basePrompt, taskInput string) (string, *harness.Harness) {
    h := harness.LoadActive()
    composed, skills := h.ComposeNodePrompt(nodeName, basePrompt, taskInput)
    if span != nil {
        span.SetAttributes(
            AttrHarnessRoot.String(h.RootDir),
            AttrHarnessAlwaysOnBytes.Int(h.AlwaysOnBytes()),
        )
        if len(skills) > 0 {
            span.SetAttributes(AttrHarnessSkills.StringSlice(skills))
        }
    }
    return composed, h
}
Enter fullscreen mode Exit fullscreen mode

We can directly use the CLI to verify the always-on byte count and structural validity of harness/base:

$ go run ./cmd/harness-runner --validate-only --harness-dir=harness/base
OK (1599 always-on bytes)
Enter fullscreen mode Exit fullscreen mode

4. Summary and Next Article Preview

In this article (Part 1), we completed the goals of Issue #16:

  1. Gained a deep understanding of how Google Research Harness² (arXiv:2610.14224) automatically evolves Agent capabilities through a c1..c8 editable file tree and two-level recursion (Task-level Propose-Probe-Compose + Harnessing-level Experience Reflector), without recompiling the main program or bloating the always-on context.
  2. Inventoried the architectural bottlenecks of Phase 1~5 where prompts and normalization logic were hardcoded in the Go binary.
  3. Implemented internal/harness/harness.go and harness/base/, allowing the 13 nodes of the ADK 2.0 Go Graph Workflow to support dynamic mounting of the c1..c8 equipment tree via LINE_CARD_HARNESS_DIR.

With a hot-reloadable c1..c8 equipment tree in place, the next article [Agent Platform in Action (8)] When ADK Go Graph Workflow Meets Google Research Harness² (Part 2): Turning Phase 4 Eval Datasets and Phase 5 OpenTelemetry Spans into LineCardAdapter will enter Issue #17: revealing why we need to encapsulate Phase 4 test sets and Phase 5 OpenTelemetry Spans into a LineCardAdapter, and how the "Zero-Leakage Barrier" prevents the AI optimizer from cheating!