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:
- [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/workflowto build a 13-node directed graph featuring Vision OCR,AddFanOut/AddFanInparallel background investigation, andNewRequestInputEventHITL interaction loops. - [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. - [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.
- [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.
- [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.
- [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 ac1..c8editable Harness file tree. - Part 2 (Issue #17): Encapsulating Phase 4's
eval/datasets/and Phase 5's OpenTelemetry Spans into aLineCardAdapterforHarness², and analyzing why "Zero-Leakage Barriers" and "High-Resolution Behavioral Trajectories" are both indispensable. - Part 3 (Issue #18): Complete testing tutorial, disclosure of
ParallelvsSequential + LongTermMEMORYtwo-layer recursive experimental data (accuracy jumping from62.5%to100.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 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) andc5..c8(On-Demand)?Harness²sets an iron rule inOPENCODE_SURFACE: The total byte count ofc1..c4always-on files must never exceed24,000 bytes(AlwaysOnMaxBytes = 24000)! WhenHarness²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 intosystemprompt.mdcausing Context explosion. Instead, it will automatically create an on-demandc6skill file (skills/<name>/SKILL.md) and leave only a lightweight Trigger Line insystemprompt.md!
3. How Does Two-Level Recursion Work?
The Harness² evolution engine (harness2/core/recursion.py) includes two levels of recursion:
- 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 toc1..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.
-
- Level 2 (Outer): Harnessing-Level Recursion (
Experience Reflector):- Between multiple iterations ($k = 1 \dots K$), the outer
ReflectorPromptreviews 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 ofProposeand Agent execution.
- Between multiple iterations ($k = 1 \dots K$), the outer
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 In the code before modification:
- All Prompt templates for
VisionOCRNode,CompanyEnrichNode,ApplyEditNode(located ininternal/workflow/nodes_ingest.go), as well asQueryIntentNodeandEmailDraftNode(located ininternal/workflow/nodes_search.go), are hardcoded asfmt.Sprintf(...)string constants in the Go code. - The
llmagent.Config{Instruction: ...}forCompanyEnrichAgent(located ininternal/workflow/nodes_ingest.go) is also a fixed string hardcoded in a Go function. - 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 forHarness²to rewrite our.gosource code andgo buildagain 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,HITLConfirmNodepause/resume, Cloud SQL and GCS Artifact access, OpenTelemetry instrumentation. - External
c1..c8Harness file tree is responsible for the "Evolvable Brain and Skill Equipment": Dynamically hot-reloaded at runtime via the environment variableLINE_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..c8baseline file tree. -
internal/harness/harness.go: Responsible for loading thec1..c8file tree, validatingOPENCODE_SURFACE24KB 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 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). 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)) } } // ... } There are two very critical engineering details here:
- Mandatory Skill Trigger Reference Check: If
Harness²adds a skill inskills/jp_card_ocr/SKILL.mdbut forgets to write when to triggerjp_card_ocrinsystemprompt.mdorguardrails.md,Validate()will directly report an error and intercept it, avoiding the creation of "Orphan Skills" that will never be called. - Node-Level Dynamic Skill Matching (
MatchSkills+ComposeNodePrompt): When the Graph Workflow executesVisionOCRNode,CompanyEnrichNode,ApplyEditNode,QueryIntentNode, orEmailDraftNode, it only injectsc6skills related to that node and automatically attachesadk.harness.skillsandadk.harness.always_on_bytesattributes 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 } 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) 4. Summary and Next Article Preview
In this article (Part 1), we completed the goals of Issue #16:
- Gained a deep understanding of how Google Research
Harness²(arXiv:2610.14224) automatically evolves Agent capabilities through ac1..c8editable file tree and two-level recursion (Task-levelPropose-Probe-Compose+ Harnessing-levelExperience Reflector), without recompiling the main program or bloating the always-on context. - Inventoried the architectural bottlenecks of Phase 1~5 where prompts and normalization logic were hardcoded in the Go binary.
- Implemented
internal/harness/harness.goandharness/base/, allowing the 13 nodes of the ADK 2.0 Go Graph Workflow to support dynamic mounting of thec1..c8equipment tree viaLINE_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!
![[Agent Platform in Action (7)] When ADK Go Graph Workflow Meets Google Research Harness (Part 1): Two-Layer Recursive Agent Fram](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftgngihh3qblqv1ey9xpn.png)
